From 9ba1a7351eea6c8a02e84bd03192de42646bb1b0 Mon Sep 17 00:00:00 2001 From: Hedi Nasr Date: Tue, 27 Oct 2026 16:27:12 +0100 Subject: [PATCH] BUILD-13269: Add renovate action Run self-hosted Renovate installed from Repox, passing the selected Repox reader to the commands it runs, so repositories whose lockfiles pin the production Edge can replace the Mend-hosted app. --- .shellspec | 2 +- README.md | 91 ++++++++++++++++++++++++++++++++++++++++ renovate/action.yml | 62 +++++++++++++++++++++++++++ renovate/renovate.sh | 72 +++++++++++++++++++++++++++++++ sonar-project.properties | 2 +- spec/renovate_spec.sh | 90 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 317 insertions(+), 2 deletions(-) create mode 100644 renovate/action.yml create mode 100755 renovate/renovate.sh create mode 100644 spec/renovate_spec.sh diff --git a/.shellspec b/.shellspec index 21a664a7..a9d114e1 100644 --- a/.shellspec +++ b/.shellspec @@ -1,5 +1,5 @@ # kcov (coverage) options ---kcov-options "--include-pattern=build-poetry,config-uv,config-poetry,get-build-number,pr_cleanup,promote,build-gradle,config-maven,build-maven,config-npm,build-npm,build-yarn,shared,config-gradle,config-pip,update-release-channel,report-ci-metrics,config-nuget,config-bun" +--kcov-options "--include-pattern=build-poetry,config-uv,config-poetry,get-build-number,pr_cleanup,promote,build-gradle,config-maven,build-maven,config-npm,build-npm,build-yarn,shared,config-gradle,config-pip,update-release-channel,report-ci-metrics,config-nuget,config-bun,renovate" # --kcov-options "--exclude-pattern=.github,.idea,.git" # define minimum coverage (fail otherwise) diff --git a/README.md b/README.md index 619f49eb..9de91139 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ These badges show the status of workflows in dummy repositories that use (or sho - [`config-uv`](#config-uv) - [`config-bun`](#config-bun) - [`config-nuget`](#config-nuget) +- [`renovate`](#renovate) - [`promote`](#promote) - [`pr_cleanup`](#pr_cleanup) - [`code-signing`](#code-signing) @@ -1612,6 +1613,96 @@ steps: --- +## `renovate` + +Run self-hosted [Renovate](https://docs.renovatebot.com/) on the current repository. + +Use it instead of the Mend-hosted Renovate app when lockfiles pin the production JFrog Edge: Mend can't reach the Edge, so the +lockfile updates it makes fail or write SaaS URLs back. Run it on a self-hosted runner, and disable the Mend app for the +repository so the two don't open competing pull requests. + +The action installs the `renovate` npm package from the selected Repox and runs it on Node.js installed with mise. It passes +the selected Repox reader to the commands Renovate runs (lockfile updates and `postUpgradeTasks`). Package lookups follow the +repository's Renovate configuration; the shared `SonarSource/renovate-config` presets use SaaS Repox with the `REPOX_TOKEN` +secret, which the action sets. + +Renovate runs with `binarySource: global`, so the tools needed for lockfile updates must be on `PATH`. The action runs `mise`, +which installs the tools declared in the repository's mise configuration. + +### Requirements + +#### Required GitHub Permissions + +- `id-token: write` + +The other permissions come from the GitHub token used by Renovate. + +#### Required Vault Permissions + +- `renovate`: GitHub App token for the repository, unless `github-token` is set. It needs `checks`, `contents`, `issues`, + `pull_requests`, `statuses` and `workflows` write access, and `metadata` read access. +- `public-reader` or `private-reader`: Artifactory role for reading dependencies, for both SaaS Repox and the selected Repox + +#### Other Dependencies + +The tools used by the repository's lockfile updates and `postUpgradeTasks`, declared in its mise configuration. + +### Usage + +```yaml +on: + workflow_dispatch: + schedule: + - cron: "0 21 * * 1-5" + +jobs: + renovate: + runs-on: sonar-xs + permissions: + id-token: write + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: SonarSource/ci-github-actions/renovate@v2 + env: + RENOVATE_ALLOWED_COMMANDS: '["^bun install$"]' +``` + +Global-only Renovate options, such as `allowedCommands` or `force`, can't be set in the repository's Renovate configuration. Set +them as `RENOVATE_*` environment variables on the step, as above. + +### Inputs + +| Input | Description | Default | +|---------------------------|-------------------------------------------------------|----------------------------------------------------------------------| +| `renovate-version` | Version of the `renovate` npm package to run | `43.257.5` | +| `node-version` | Node.js version used to run Renovate | `24.11.0` | +| `github-token` | GitHub token used by Renovate | The repository's `renovate` GitHub App token from Vault | +| `artifactory-reader-role` | Suffix for the Artifactory reader role in Vault | `private-reader` for private repos, `public-reader` for public repos | +| `repox-url` | URL for Repox | Runner-dependent (see Edge section) | + +Pull requests opened with `GITHUB_TOKEN` don't trigger workflows, so CI doesn't run on them. + +### Input Environment Variables + +| Environment Variable | Description | +|---------------------------------|------------------------------------------------------------------------------------------------| +| `RENOVATE_*`, `LOG_LEVEL` | Passed to Renovate | +| `RENOVATE_REPOSITORIES` | Repositories to process. Defaults to the current repository | +| `RENOVATE_SECRETS` | JSON object merged with the `REPOX_TOKEN` secret set by the action | +| `RENOVATE_CUSTOM_ENV_VARIABLES` | JSON object merged with the variables the action sets for Renovate commands | + +### Environment Variables for Renovate Commands + +| Environment Variable | Description | +|----------------------------|------------------------------------------------| +| `ARTIFACTORY_URL` | Selected Repox Artifactory URL | +| `ARTIFACTORY_USERNAME` | Selected Repox reader username | +| `ARTIFACTORY_ACCESS_TOKEN` | Selected Repox reader access token | +| `BUN_CONFIG_REGISTRY` | npm registry URL under the selected Repox | +| `BUN_CONFIG_TOKEN` | Selected Repox reader access token | + +--- + ## `promote` This action promotes a build in JFrog Artifactory and updates the GitHub status check accordingly. diff --git a/renovate/action.yml b/renovate/action.yml new file mode 100644 index 00000000..7642abd1 --- /dev/null +++ b/renovate/action.yml @@ -0,0 +1,62 @@ +--- +name: Renovate +description: GitHub Action to run self-hosted Renovate on the current repository, installed from Repox and resolving lockfile updates + through the selected Repox +inputs: + renovate-version: + description: Version of the `renovate` npm package to run + default: 43.257.5 + node-version: + description: Node.js version used to run Renovate, installed with mise + default: 24.11.0 + github-token: + description: GitHub token used by Renovate. Defaults to the repository's `renovate` GitHub App token from Vault. + default: '' + artifactory-reader-role: + description: Suffix for the Artifactory reader role in Vault. Defaults to `private-reader` for private repositories, and `public-reader` + for public repositories. + default: '' + repox-url: + description: URL for Repox used to install Renovate and by the commands Renovate runs. Defaults to SaaS on GitHub-hosted runners and + the production Edge on self-hosted runners. Explicit URLs override runner selection. + default: ${{ runner.environment == 'github-hosted' && 'https://repox.jfrog.io' || 'https://repox-internal.sonar.build' }} + +runs: + using: composite + steps: + - name: Set local action paths + shell: bash + env: + ARTIFACTORY_READER_ROLE: ${{ inputs.artifactory-reader-role != '' && inputs.artifactory-reader-role || + (github.event.repository.visibility == 'public' && 'public-reader' || 'private-reader') }} + run: | + echo "ACTION_PATH_RENOVATE=${{ github.action_path }}" >> "$GITHUB_ENV" + echo "ARTIFACTORY_READER_ROLE=${ARTIFACTORY_READER_ROLE}" >> "$GITHUB_ENV" + + - uses: step-security/mise-action@a225bbd7c202ddf57dd76dff22bb7992f6314173 # v5.1.2 + with: + version: 2026.10.17 + + - uses: SonarSource/vault-action-wrapper@a7fa5fd4a4e4e0f2593f6137c0812cac196cf4ce # 3.7.0 + id: secrets + with: + # yamllint disable rule:line-length + url: ${{ contains(inputs.repox-url, '.dev.sonar.build') && 'https://vault.dev.sonar.build' || 'https://vault.sonar.build' }} + secrets: | + ${{ inputs.github-token == '' && 'development/github/token/{REPO_OWNER_NAME_DASH}-renovate token | RENOVATE_TOKEN;' || '' }} + development/artifactory/token/{REPO_OWNER_NAME_DASH}-${{ env.ARTIFACTORY_READER_ROLE }} access_token | REPOX_TOKEN; + development/${{ contains(inputs.repox-url, 'jfrog.io') && 'artifactory' || (contains(inputs.repox-url, '.dev.sonar.build') && 'artifactory-edge-dev' || 'artifactory-edge-prod') }}/token/{REPO_OWNER_NAME_DASH}-${{ env.ARTIFACTORY_READER_ROLE }} username | ARTIFACTORY_USERNAME; + development/${{ contains(inputs.repox-url, 'jfrog.io') && 'artifactory' || (contains(inputs.repox-url, '.dev.sonar.build') && 'artifactory-edge-dev' || 'artifactory-edge-prod') }}/token/{REPO_OWNER_NAME_DASH}-${{ env.ARTIFACTORY_READER_ROLE }} access_token | ARTIFACTORY_ACCESS_TOKEN; + # yamllint enable rule:line-length + + - name: Run Renovate + shell: bash + env: + RENOVATE_VERSION: ${{ inputs.renovate-version }} + NODE_VERSION: ${{ inputs.node-version }} + ARTIFACTORY_URL: ${{ format('{0}/artifactory', inputs.repox-url) }} + ARTIFACTORY_USERNAME: ${{ fromJSON(steps.secrets.outputs.vault).ARTIFACTORY_USERNAME }} + ARTIFACTORY_ACCESS_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).ARTIFACTORY_ACCESS_TOKEN }} + REPOX_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).REPOX_TOKEN }} + RENOVATE_TOKEN: ${{ inputs.github-token != '' && inputs.github-token || fromJSON(steps.secrets.outputs.vault).RENOVATE_TOKEN }} + run: $ACTION_PATH_RENOVATE/renovate.sh diff --git a/renovate/renovate.sh b/renovate/renovate.sh new file mode 100755 index 00000000..e0873a55 --- /dev/null +++ b/renovate/renovate.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# Run self-hosted Renovate on the current repository. +# +# Renovate is installed with npm from the selected Repox, and the commands it runs (lockfile updates, post-upgrade tasks) +# get the selected Repox reader through customEnvVariables. +# +# Required environment variables (must be explicitly provided): +# - RENOVATE_VERSION: Version of the renovate npm package +# - NODE_VERSION: Node.js version used to run Renovate +# - ARTIFACTORY_URL: URL to the selected Artifactory +# - ARTIFACTORY_USERNAME: Reader username for the selected Artifactory +# - ARTIFACTORY_ACCESS_TOKEN: Reader access token for the selected Artifactory +# - REPOX_TOKEN: SaaS Repox reader access token, exposed as the REPOX_TOKEN Renovate secret +# - RENOVATE_TOKEN: GitHub token used by Renovate +# +# Optional environment variables: +# - RENOVATE_SECRETS, RENOVATE_CUSTOM_ENV_VARIABLES: JSON objects merged with the values set by this script +# - Any other RENOVATE_* variable or LOG_LEVEL is passed to Renovate unchanged +# +# GitHub Actions auto-provided: +# - GITHUB_REPOSITORY: Repository processed when RENOVATE_REPOSITORIES is not set +# - RUNNER_TEMP: Directory where Renovate is installed + +set -euo pipefail + +: "${RENOVATE_VERSION:?}" "${NODE_VERSION:?}" "${ARTIFACTORY_URL:?}" "${ARTIFACTORY_USERNAME:?}" "${ARTIFACTORY_ACCESS_TOKEN:?}" +: "${REPOX_TOKEN:?}" "${RENOVATE_TOKEN:?}" "${GITHUB_REPOSITORY:?}" "${RUNNER_TEMP:?}" + +install_renovate() { + local registry="${ARTIFACTORY_URL}/api/npm/npm/" + RENOVATE_PREFIX="$RUNNER_TEMP/renovate" + NPM_CONFIG_USERCONFIG="$RUNNER_TEMP/renovate.npmrc" + export NPM_CONFIG_USERCONFIG + { + echo "registry=$registry" + echo "//${registry#https://}:_authToken=$ARTIFACTORY_ACCESS_TOKEN" + } > "$NPM_CONFIG_USERCONFIG" + echo "Installing renovate@$RENOVATE_VERSION from $registry" + mise exec "node@$NODE_VERSION" -- npm install --prefix "$RENOVATE_PREFIX" --no-audit --no-fund "renovate@$RENOVATE_VERSION" + return 0 +} + +export_renovate_config() { + local registry="${ARTIFACTORY_URL}/api/npm/npm/" + export RENOVATE_REPOSITORIES="${RENOVATE_REPOSITORIES:-$GITHUB_REPOSITORY}" + export RENOVATE_ONBOARDING="${RENOVATE_ONBOARDING:-false}" + export RENOVATE_BINARY_SOURCE="${RENOVATE_BINARY_SOURCE:-global}" + RENOVATE_SECRETS=$(jq -cn --arg token "$REPOX_TOKEN" --argjson extra "${RENOVATE_SECRETS:-"{}"}" \ + '{REPOX_TOKEN: $token} + $extra') + RENOVATE_CUSTOM_ENV_VARIABLES=$(jq -cn --arg url "$ARTIFACTORY_URL" --arg registry "$registry" \ + --arg username "$ARTIFACTORY_USERNAME" --arg token "$ARTIFACTORY_ACCESS_TOKEN" \ + --argjson extra "${RENOVATE_CUSTOM_ENV_VARIABLES:-"{}"}" \ + '{ARTIFACTORY_URL: $url, ARTIFACTORY_USERNAME: $username, ARTIFACTORY_ACCESS_TOKEN: $token, + BUN_CONFIG_REGISTRY: $registry, BUN_CONFIG_TOKEN: $token} + $extra') + export RENOVATE_SECRETS RENOVATE_CUSTOM_ENV_VARIABLES + echo "Renovate repositories: $RENOVATE_REPOSITORIES" + echo "Dependency registry for Renovate commands: $registry" + return 0 +} + +main() { + echo "::group::Install Renovate" + install_renovate + echo "::endgroup::" + export_renovate_config + mise exec "node@$NODE_VERSION" -- "$RENOVATE_PREFIX/node_modules/.bin/renovate" + return 0 +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main +fi diff --git a/sonar-project.properties b/sonar-project.properties index 05813fdd..b08e1dee 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -4,7 +4,7 @@ sonar.projectName=ci-github-actions sonar.sourceEncoding=UTF-8 -sonar.sources=build-poetry,get-build-number,pr_cleanup,promote,build-gradle,build-maven,config-npm,build-npm,build-yarn,shared,config-pip,config-poetry,cache,code-signing,config-gradle,config-maven,check-sca,update-release-channel,report-ci-metrics,config-nuget,config-bun +sonar.sources=build-poetry,get-build-number,pr_cleanup,promote,build-gradle,build-maven,config-npm,build-npm,build-yarn,shared,config-pip,config-poetry,cache,code-signing,config-gradle,config-maven,check-sca,update-release-channel,report-ci-metrics,config-nuget,config-bun,renovate sonar.tests=spec sonar.coverageReportPaths=coverage/coverage_data/sonar_coverage.xml diff --git a/spec/renovate_spec.sh b/spec/renovate_spec.sh new file mode 100644 index 00000000..61dfd623 --- /dev/null +++ b/spec/renovate_spec.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +eval "$(shellspec - -c) exit 1" + +Mock mise + echo "mise $*" +End + +export RENOVATE_VERSION="43.257.5" +export NODE_VERSION="24.11.0" +export ARTIFACTORY_URL="https://repox-internal.sonar.build/artifactory" +export ARTIFACTORY_USERNAME="test-user" +export ARTIFACTORY_ACCESS_TOKEN="edge-token" +export REPOX_TOKEN="saas-token" +export RENOVATE_TOKEN="github-token" +export GITHUB_REPOSITORY="my-org/my-repo" +RUNNER_TEMP=$(mktemp -d) +export RUNNER_TEMP + +EDGE_REGISTRY="https://repox-internal.sonar.build/artifactory/api/npm/npm/" + +Describe 'renovate/renovate.sh' + It 'does not run main when sourced' + When run source renovate/renovate.sh + The status should be success + The lines of output should equal 0 + The lines of error should equal 0 + End +End + +Include renovate/renovate.sh + +Describe 'install_renovate()' + It 'installs the pinned Renovate version from the selected Repox' + When call install_renovate + The status should be success + The line 1 should equal "Installing renovate@43.257.5 from $EDGE_REGISTRY" + The line 2 should equal \ + "mise exec node@24.11.0 -- npm install --prefix $RUNNER_TEMP/renovate --no-audit --no-fund renovate@43.257.5" + The contents of file "$RUNNER_TEMP/renovate.npmrc" should include "registry=$EDGE_REGISTRY" + The contents of file "$RUNNER_TEMP/renovate.npmrc" should include \ + "//repox-internal.sonar.build/artifactory/api/npm/npm/:_authToken=edge-token" + End +End + +Describe 'export_renovate_config()' + It 'sets the defaults, the REPOX_TOKEN secret and the reader for Renovate commands' + When call export_renovate_config + The status should be success + The line 1 should equal "Renovate repositories: my-org/my-repo" + The line 2 should equal "Dependency registry for Renovate commands: $EDGE_REGISTRY" + The variable RENOVATE_REPOSITORIES should equal "my-org/my-repo" + The variable RENOVATE_ONBOARDING should equal "false" + The variable RENOVATE_BINARY_SOURCE should equal "global" + The variable RENOVATE_SECRETS should equal '{"REPOX_TOKEN":"saas-token"}' + The variable RENOVATE_CUSTOM_ENV_VARIABLES should include "\"BUN_CONFIG_REGISTRY\":\"$EDGE_REGISTRY\"" + The variable RENOVATE_CUSTOM_ENV_VARIABLES should include '"BUN_CONFIG_TOKEN":"edge-token"' + The variable RENOVATE_CUSTOM_ENV_VARIABLES should include '"ARTIFACTORY_ACCESS_TOKEN":"edge-token"' + The variable RENOVATE_CUSTOM_ENV_VARIABLES should include '"ARTIFACTORY_USERNAME":"test-user"' + End + + It 'keeps values set by the caller' + export RENOVATE_REPOSITORIES="my-org/other-repo" + export RENOVATE_ONBOARDING="true" + export RENOVATE_SECRETS='{"OTHER":"value"}' + export RENOVATE_CUSTOM_ENV_VARIABLES='{"BUN_CONFIG_TOKEN":"override"}' + When call export_renovate_config + The status should be success + The output should include "Renovate repositories: my-org/other-repo" + The variable RENOVATE_ONBOARDING should equal "true" + The variable RENOVATE_SECRETS should equal '{"REPOX_TOKEN":"saas-token","OTHER":"value"}' + The variable RENOVATE_CUSTOM_ENV_VARIABLES should include '"BUN_CONFIG_TOKEN":"override"' + End +End + +Describe 'main()' + It 'installs and runs Renovate' + When run script renovate/renovate.sh + The status should be success + The line 1 should equal "::group::Install Renovate" + The output should include "::endgroup::" + The output should include "mise exec node@24.11.0 -- $RUNNER_TEMP/renovate/node_modules/.bin/renovate" + End + + It 'fails without a GitHub token' + export RENOVATE_TOKEN="" + When run script renovate/renovate.sh + The status should be failure + The error should include "RENOVATE_TOKEN" + End +End