From caaad0efab86e1e0454ad5191dad36c29803c260 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Sat, 3 Oct 2026 22:44:30 +0100 Subject: [PATCH] feat(npm): support a private registry token --- .github/workflows/node-pr.yaml | 29 ++++++++++++++ .github/workflows/renovate-autoupdate.yaml | 45 +++++++++++++++++++++- README.md | 8 ++++ 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/.github/workflows/node-pr.yaml b/.github/workflows/node-pr.yaml index 911ba9d..47fd279 100644 --- a/.github/workflows/node-pr.yaml +++ b/.github/workflows/node-pr.yaml @@ -36,11 +36,25 @@ on: required: false type: boolean default: false + npm-registry-scope: + description: "Scope served by a private npm registry, for example '@fortawesome'. Used only when npm-registry-url and the npm-registry-token secret are also set." + required: false + type: string + default: "" + npm-registry-url: + description: "URL of the private npm registry, for example 'https://npm.fontawesome.com/'. Used only when npm-registry-scope and the npm-registry-token secret are also set." + required: false + type: string + default: "" runner: description: "Runner label, or JSON object such as {\"group\":\"ci\",\"labels\":[\"linux\"]} to select a runner group. Falls back to vars.SHARED_ACTIONS_RUNNER, then ubuntu-latest." required: false type: string default: "" + secrets: + npm-registry-token: + description: "Auth token for the private npm registry" + required: false concurrency: group: node-pr-${{ github.repository }}-${{ github.event_name }}-${{ github.head_ref || github.ref_name }}-${{ inputs.working-directory }} @@ -120,6 +134,21 @@ jobs: with: node-version: ${{ inputs.node-version }} + # The npmrc lives in RUNNER_TEMP, not the checkout, so no git command can pick the token up. + - name: Configure the private npm registry + env: + NPM_SCOPE: ${{ inputs.npm-registry-scope }} + NPM_URL: ${{ inputs.npm-registry-url }} + NPM_TOKEN: ${{ secrets.npm-registry-token }} + run: | + [ -n "$NPM_SCOPE" ] && [ -n "$NPM_URL" ] && [ -n "$NPM_TOKEN" ] || exit 0 + echo "::add-mask::$NPM_TOKEN" + url="${NPM_URL%/}/" + npmrc="$RUNNER_TEMP/private-registry.npmrc" + (umask 077 && printf '%s:registry=%s\n//%s:_authToken=%s\n' "$NPM_SCOPE" "$url" "${url#*://}" "$NPM_TOKEN" > "$npmrc") + echo "NPM_CONFIG_USERCONFIG=$npmrc" >> "$GITHUB_ENV" + echo "Using a private npm registry for $NPM_SCOPE." + - name: Install dependencies working-directory: ${{ inputs.working-directory }} env: diff --git a/.github/workflows/renovate-autoupdate.yaml b/.github/workflows/renovate-autoupdate.yaml index 0b59b21..bf37526 100644 --- a/.github/workflows/renovate-autoupdate.yaml +++ b/.github/workflows/renovate-autoupdate.yaml @@ -103,11 +103,25 @@ on: required: false type: boolean default: false + npm-registry-scope: + description: "Scope served by a private npm registry, for example '@fortawesome'. Used only when npm-registry-url and the npm-registry-token secret are also set." + required: false + type: string + default: "" + npm-registry-url: + description: "URL of the private npm registry, for example 'https://npm.fontawesome.com/'. Used only when npm-registry-scope and the npm-registry-token secret are also set." + required: false + type: string + default: "" runner: description: "Runner label, or JSON object such as {\"group\":\"ci\",\"labels\":[\"linux\"]} to select a runner group. Falls back to vars.SHARED_ACTIONS_RUNNER, then ubuntu-latest." required: false type: string default: "" + secrets: + npm-registry-token: + description: "Auth token for the private npm registry" + required: false concurrency: group: renovate-autoupdate-${{ github.repository }} @@ -149,8 +163,18 @@ jobs: NODE_DIR: ${{ inputs.node-working-directory }} PNPM_VERSION: ${{ inputs.pnpm-version }} GO_MOD_TIDY: ${{ inputs.go-mod-tidy }} + NPM_SCOPE: ${{ inputs.npm-registry-scope }} + NPM_URL: ${{ inputs.npm-registry-url }} + NPM_TOKEN: ${{ secrets.npm-registry-token }} CONFIG_FILE: ${{ runner.temp }}/renovate-autoupdate-config.json run: | + # The token goes to jq through the environment, never argv, and the config print below hides it. + export NPMRC="" + if [ -n "$NPM_SCOPE" ] && [ -n "$NPM_URL" ] && [ -n "$NPM_TOKEN" ]; then + echo "::add-mask::$NPM_TOKEN" + url="${NPM_URL%/}/" + NPMRC=$(printf '%s:registry=%s\n//%s:_authToken=%s\n' "$NPM_SCOPE" "$url" "${url#*://}" "$NPM_TOKEN") + fi jq -n \ --arg repo "$GITHUB_REPOSITORY" \ --arg age "$MIN_AGE" \ @@ -212,8 +236,9 @@ jobs: # Rules that disable by update type are ignored, but a version cap works. + (if $majors == "ignore" then [{matchManagers: ["gomod", "npm"], allowedVersions: "<{{add major 1}}.0.0"}] else [] end) ) - }' > "$CONFIG_FILE" - jq . "$CONFIG_FILE" + } + | if $ENV.NPMRC != "" then .npmrc = $ENV.NPMRC else . end' > "$CONFIG_FILE" + jq 'if has("npmrc") then .npmrc = "" else . end' "$CONFIG_FILE" - name: Run Renovate uses: renovatebot/github-action@v46.3.6 @@ -367,6 +392,22 @@ jobs: with: node-version: ${{ inputs.node-version }} + # The npmrc lives in RUNNER_TEMP, not the checkout, so no git command can pick the token up. + - name: Configure the private npm registry + if: steps.merge.outputs.skip != 'true' && steps.detect.outputs.node == 'true' + env: + NPM_SCOPE: ${{ inputs.npm-registry-scope }} + NPM_URL: ${{ inputs.npm-registry-url }} + NPM_TOKEN: ${{ secrets.npm-registry-token }} + run: | + [ -n "$NPM_SCOPE" ] && [ -n "$NPM_URL" ] && [ -n "$NPM_TOKEN" ] || exit 0 + echo "::add-mask::$NPM_TOKEN" + url="${NPM_URL%/}/" + npmrc="$RUNNER_TEMP/private-registry.npmrc" + (umask 077 && printf '%s:registry=%s\n//%s:_authToken=%s\n' "$NPM_SCOPE" "$url" "${url#*://}" "$NPM_TOKEN" > "$npmrc") + echo "NPM_CONFIG_USERCONFIG=$npmrc" >> "$GITHUB_ENV" + echo "Using a private npm registry for $NPM_SCOPE." + - name: Install Node dependencies if: steps.merge.outputs.skip != 'true' && steps.detect.outputs.node == 'true' working-directory: ${{ inputs.node-working-directory }} diff --git a/README.md b/README.md index 815575d..f1e9a94 100644 --- a/README.md +++ b/README.md @@ -122,10 +122,14 @@ jobs: | `pnpm-version` | | pnpm version for a repo with no `packageManager` field. Empty uses the field, or the newest pnpm. | | `require-tests` | `false` | Fail when there is no `test` script | | `lfs` | `false` | Git LFS checkout | +| `npm-registry-scope` | | Scope served by a private npm registry, for example `@fortawesome`. | +| `npm-registry-url` | | URL of that registry, for example `https://npm.fontawesome.com/`. | | `runner` | | Runner label or runner group, as in the other workflows | pnpm is used when `pnpm-lock.yaml` exists, yarn for `yarn.lock`, otherwise npm. pnpm and yarn run through Corepack, so set `packageManager` in `package.json`. +**Private npm registry.** Set `npm-registry-scope`, `npm-registry-url` and the `npm-registry-token` secret together; with any one missing nothing changes. The token goes into an npmrc outside the checkout (`NPM_CONFIG_USERCONFIG`), is masked in logs, and works for npm, pnpm and yarn classic. Yarn 2+ ignores npmrc and needs its own `.yarnrc.yml`. + ### `go-autoupdate.yaml` Automatic dependency updates for Go. See `renovate-autoupdate.yaml` for the Renovate-based replacement that also covers npm, pnpm and yarn. @@ -194,8 +198,12 @@ The workflow finds `go.mod` and `package.json` itself and tests what exists. A r | `release-workflow` | | Workflow file to dispatch after a merge, for example `release.yaml`. Give several as a comma-separated list. Each needs a `workflow_dispatch` trigger. Empty means no dispatch. | | `admin-merge` | `false` | Merge with `--admin`. The workflow token cannot bypass branch protection. | | `lfs` | `false` | Git LFS checkout | +| `npm-registry-scope` | | Scope served by a private npm registry, for example `@fortawesome`. | +| `npm-registry-url` | | URL of that registry, for example `https://npm.fontawesome.com/`. | | `runner` | | Runner label or runner group, as in the other workflows | +**Private npm registry.** Set `npm-registry-scope`, `npm-registry-url` and the `npm-registry-token` secret together; with any one missing nothing changes. Renovate and the install step both use it. The token goes into an npmrc outside the checkout (`NPM_CONFIG_USERCONFIG`), is masked in logs, and works for npm, pnpm and yarn classic. Yarn 2+ ignores npmrc and needs its own `.yarnrc.yml`. + **Failing updates.** The last 50 KB of the test output goes into a comment on one issue titled "Dependency updates failing tests". Each failing branch has its own comment, updated on every run, and a header with the commit and the run link. When a branch passes and merges, its comment is removed. When no failing branch is left, the issue closes. The output is also in the run summary. Without `issues: write` the run still works, but the issue is skipped. **Only what is tested is updated.** Renovate manages one Go module and one Node project: the ones in `go-working-directory` and `node-working-directory`. A second `package.json` (docs, e2e tests, another frontend) or a second `go.mod` is left alone, because nothing would test its updates.