Skip to content

Commit 494292f

Browse files
authored
Merge pull request #130 from PMDevSolutions/ci/harden-npm-trusted-publishing
fix(ci): harden npm trusted publishing (OIDC) in release workflows
2 parents 8a0ad3d + 29592c7 commit 494292f

2 files changed

Lines changed: 111 additions & 36 deletions

File tree

.github/workflows/publish-npm.yml

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,21 @@ name: Publish to npm
44
# release publish without waiting for the next release. (release-please.yml
55
# publishes automatically when it cuts a release.)
66
#
7-
# Auth uses npm Trusted Publishing (OIDC) — there is no NPM_TOKEN secret.
8-
# Requires id-token: write and npm >= 11.5.1, so we upgrade the runner's
9-
# bundled npm before publishing. Provenance is generated automatically.
7+
# Auth uses npm Trusted Publishing (OIDC). Tokenless: there is no NPM_TOKEN
8+
# secret. The job-level "id-token: write" permission lets npm exchange a GitHub
9+
# OIDC token for a short-lived publish token. Provenance is automatic. OIDC
10+
# requires npm >= 11.5.1 (newer than Node 22's bundled npm), so the steps below
11+
# upgrade and verify npm before publishing.
1012
#
11-
# NOTE: npm allows only ONE trusted publisher per package, configured here for
12-
# release-please.yml. To publish from THIS workflow instead, temporarily
13-
# repoint the package's trusted publisher (npmjs.com -> package Settings) to
14-
# publish-npm.yml, dispatch, then switch it back. A brand-new package's FIRST
15-
# publish cannot use OIDC at all — publish it once with a token (locally), then
16-
# configure trusted publishing.
13+
# TRUSTED-PUBLISHER BINDING: npm keys each package's trusted publisher on the
14+
# repo (PMDevSolutions/Claudius) AND the exact workflow FILENAME that runs
15+
# "npm publish". npm allows only ONE trusted publisher per package, and both
16+
# packages are normally bound to release-please.yml. To publish from THIS file
17+
# instead, temporarily repoint the package's trusted publisher (npmjs.com ->
18+
# package Settings) to publish-npm.yml, dispatch, then switch it back. Renaming
19+
# either workflow file breaks publishing until the trusted publisher is updated
20+
# to match the new filename. A brand-new package's FIRST publish cannot use OIDC
21+
# at all: publish it once with a token (locally), then configure OIDC.
1722
on:
1823
workflow_dispatch:
1924
inputs:
@@ -45,26 +50,52 @@ jobs:
4550
run:
4651
working-directory: ${{ inputs.package }}
4752
steps:
48-
- uses: actions/checkout@v4
53+
- uses: actions/checkout@v7
4954

50-
- uses: pnpm/action-setup@v4
55+
- uses: pnpm/action-setup@v6
5156
with:
5257
version: 9
5358

54-
# registry-url is required for OIDC trusted publishing: it points npm at
55-
# the registry to mint the short-lived token against. setup-node also
56-
# writes a placeholder _authToken, which npm ignores in favour of OIDC as
57-
# long as a matching trusted publisher is configured for this package.
58-
- uses: actions/setup-node@v4
59+
# registry-url makes setup-node write the registry that OIDC mints its
60+
# short-lived token against (required for trusted publishing). setup-node
61+
# also writes a deprecated always-auth key and a placeholder _authToken;
62+
# the "Sanitize npm config" step below removes both so they can't interfere.
63+
- uses: actions/setup-node@v6
5964
with:
6065
node-version: 22
6166
registry-url: https://registry.npmjs.org
6267
cache: pnpm
6368
cache-dependency-path: ${{ inputs.package }}/pnpm-lock.yaml
6469

65-
# Trusted Publishing (OIDC) needs npm >= 11.5.1; Node 20 ships npm 10.x.
66-
- name: Upgrade npm for trusted publishing
67-
run: npm install -g npm@latest
70+
# setup-node's generated .npmrc (path exported as NPM_CONFIG_USERCONFIG)
71+
# carries a deprecated "always-auth" key (npm >= 11 warns: Unknown user
72+
# config "always-auth") and a placeholder _authToken that expands to empty.
73+
# Strip both so npm authenticates via OIDC instead of a bogus token; keep
74+
# the registry line so OIDC knows which registry to target.
75+
- name: Sanitize npm config for OIDC
76+
run: |
77+
for f in "${NPM_CONFIG_USERCONFIG:-}" "$HOME/.npmrc"; do
78+
[ -n "$f" ] && [ -f "$f" ] || continue
79+
sed -i '/^always-auth=/d; /_authToken=/d' "$f"
80+
echo "Sanitized $f:"
81+
cat "$f"
82+
done
83+
84+
# Trusted Publishing (OIDC) requires npm >= 11.5.1, newer than Node 22's
85+
# bundled npm. Pin to npm 11 (not @latest) for reproducible runs, assert
86+
# the version, and prepend the freshly installed npm's bin dir to PATH so
87+
# the Publish step uses it instead of Node's bundled npm.
88+
- name: Upgrade and verify npm for trusted publishing
89+
run: |
90+
npm install -g npm@^11
91+
NPM_GLOBAL_BIN="$(npm prefix -g)/bin"
92+
echo "$NPM_GLOBAL_BIN" >> "$GITHUB_PATH"
93+
NPM_VERSION="$("$NPM_GLOBAL_BIN/npm" --version)"
94+
echo "Resolved npm $NPM_VERSION from $NPM_GLOBAL_BIN"
95+
if [ "$(printf '%s\n' "11.5.1" "$NPM_VERSION" | sort -V | head -n1)" != "11.5.1" ]; then
96+
echo "::error::npm $NPM_VERSION is older than 11.5.1; trusted publishing (OIDC) requires >= 11.5.1" >&2
97+
exit 1
98+
fi
6899
69100
- name: Install
70101
run: pnpm install --frozen-lockfile

.github/workflows/release-please.yml

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ jobs:
1818
name: Release Please
1919
runs-on: ubuntu-latest
2020
steps:
21-
- uses: actions/checkout@v4
21+
- uses: actions/checkout@v7
2222

23-
- uses: googleapis/release-please-action@v4
23+
- uses: googleapis/release-please-action@v5
2424
id: release
2525
with:
2626
config-file: release-please-config.json
@@ -31,19 +31,19 @@ jobs:
3131
# the exact bytes jsDelivr serves at gh@<major>/cdn/.
3232
- name: Checkout release PR branch
3333
if: ${{ steps.release.outputs.pr }}
34-
uses: actions/checkout@v4
34+
uses: actions/checkout@v7
3535
with:
3636
ref: ${{ fromJSON(steps.release.outputs.pr).headBranchName }}
3737

3838
- name: Setup pnpm
3939
if: ${{ steps.release.outputs.pr }}
40-
uses: pnpm/action-setup@v4
40+
uses: pnpm/action-setup@v6
4141
with:
4242
version: 9
4343

4444
- name: Setup Node
4545
if: ${{ steps.release.outputs.pr }}
46-
uses: actions/setup-node@v4
46+
uses: actions/setup-node@v6
4747
with:
4848
node-version: 20
4949
cache: pnpm
@@ -76,32 +76,76 @@ jobs:
7676
gh release upload "${{ steps.release.outputs.tag_name }}" \
7777
cdn/claudius.iife.js cdn/claudius.css --clobber
7878
79-
# On merge, publish to npm via Trusted Publishing (OIDC) — no long-lived
80-
# NPM_TOKEN. Each package has a trusted publisher configured at npmjs.com
81-
# matching this repo + this workflow filename (release-please.yml); the
82-
# id-token: write permission above lets npm mint a short-lived token.
83-
# Trusted Publishing needs npm >= 11.5.1 (newer than Node's bundled npm),
84-
# so we upgrade npm first. Provenance is generated automatically. Runs
85-
# against the default checkout (main at the released commit), which carries
86-
# the version release-please just bumped.
79+
# ---------------------------------------------------------------------
80+
# Publish to npm via npm Trusted Publishing (OIDC). Tokenless: there is no
81+
# NPM_TOKEN secret. Each published package (claudius-chat-widget and
82+
# create-claudius) has a trusted publisher configured on npmjs.com, bound
83+
# to BOTH:
84+
# repo: PMDevSolutions/Claudius
85+
# workflow: release-please.yml (the filename of THIS workflow)
86+
# The job-level "id-token: write" permission lets npm exchange a GitHub
87+
# OIDC token for a short-lived publish token. Provenance is automatic.
88+
#
89+
# IMPORTANT: the trusted-publisher binding is keyed on this workflow's
90+
# FILENAME. Renaming this file, or running "npm publish" from a different
91+
# workflow, breaks publishing until each package's trusted publisher is
92+
# updated on npmjs.com to match the new filename.
93+
#
94+
# OIDC requires npm >= 11.5.1, newer than the npm bundled with Node 22, so
95+
# the steps below upgrade and verify npm first. Publishing runs against the
96+
# default checkout (main at the released commit), which carries the version
97+
# release-please just bumped.
98+
# ---------------------------------------------------------------------
8799
- name: Setup pnpm (npm publish)
88100
if: ${{ steps.release.outputs.release_created }}
89-
uses: pnpm/action-setup@v4
101+
uses: pnpm/action-setup@v6
90102
with:
91103
version: 9
92104

93105
- name: Setup Node (npm publish)
94106
if: ${{ steps.release.outputs.release_created }}
95-
uses: actions/setup-node@v4
107+
uses: actions/setup-node@v6
96108
with:
97109
node-version: 22
110+
# registry-url makes setup-node write the registry that OIDC mints its
111+
# short-lived token against (required for trusted publishing). The
112+
# "Sanitize npm config" step below removes the legacy always-auth key
113+
# and placeholder _authToken that setup-node also writes.
98114
registry-url: https://registry.npmjs.org
99115
cache: pnpm
100116
cache-dependency-path: widget/pnpm-lock.yaml
101117

102-
- name: Upgrade npm for trusted publishing
118+
# setup-node's generated .npmrc (path exported as NPM_CONFIG_USERCONFIG)
119+
# carries a deprecated "always-auth" key (npm >= 11 warns: Unknown user
120+
# config "always-auth") and a placeholder _authToken that expands to empty.
121+
# Strip both so npm authenticates via OIDC instead of a bogus token; keep
122+
# the registry line so OIDC knows which registry to target.
123+
- name: Sanitize npm config for OIDC
103124
if: ${{ steps.release.outputs.release_created }}
104-
run: npm install -g npm@latest
125+
run: |
126+
for f in "${NPM_CONFIG_USERCONFIG:-}" "$HOME/.npmrc"; do
127+
[ -n "$f" ] && [ -f "$f" ] || continue
128+
sed -i '/^always-auth=/d; /_authToken=/d' "$f"
129+
echo "Sanitized $f:"
130+
cat "$f"
131+
done
132+
133+
# Trusted Publishing (OIDC) requires npm >= 11.5.1, newer than Node 22's
134+
# bundled npm. Pin to npm 11 (not @latest) for reproducible runs, assert
135+
# the version, and prepend the freshly installed npm's bin dir to PATH so
136+
# the publish steps use it instead of Node's bundled npm.
137+
- name: Upgrade and verify npm for trusted publishing
138+
if: ${{ steps.release.outputs.release_created }}
139+
run: |
140+
npm install -g npm@^11
141+
NPM_GLOBAL_BIN="$(npm prefix -g)/bin"
142+
echo "$NPM_GLOBAL_BIN" >> "$GITHUB_PATH"
143+
NPM_VERSION="$("$NPM_GLOBAL_BIN/npm" --version)"
144+
echo "Resolved npm $NPM_VERSION from $NPM_GLOBAL_BIN"
145+
if [ "$(printf '%s\n' "11.5.1" "$NPM_VERSION" | sort -V | head -n1)" != "11.5.1" ]; then
146+
echo "::error::npm $NPM_VERSION is older than 11.5.1; trusted publishing (OIDC) requires >= 11.5.1" >&2
147+
exit 1
148+
fi
105149
106150
- name: Build and publish widget
107151
if: ${{ steps.release.outputs.release_created }}

0 commit comments

Comments
 (0)