diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 29a13ace4..8b79c33b8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,14 +5,17 @@ on: types: [published, edited] workflow_dispatch: {} +permissions: + id-token: write + contents: read + packages: write + jobs: publish: name: Publish to NPM and GitHub Package Registry runs-on: ubuntu-latest if: ${{ github.event_name == 'workflow_dispatch' || !github.event.release.draft }} - permissions: - contents: read - packages: write + environment: npm steps: - name: Checkout branch uses: actions/checkout@v4 @@ -20,17 +23,16 @@ jobs: persist-credentials: false - name: Setup Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: 18 - registry-url: "https://registry.npmjs.org/" - always-auth: "true" + node-version: 22 + + - name: Upgrade npm for OIDC support + run: npm install -g npm@latest - # Add auth for the GitHub Package Registry publish. npm expands - # ${NODE_AUTH_TOKEN} at runtime per-step, so each publish step uses the - # right token for its registry. We do NOT route the @optimizely scope to - # GHR, so `npm ci` still resolves dependencies from npm; publish.sh passes - # --registry explicitly to target GHR. + # Configure auth for GHR only. npm registry auth is handled by OIDC + # (no .npmrc token entry needed — omitting registry-url from setup-node + # ensures npm falls through to OIDC for registry.npmjs.org). - name: Configure GitHub Package Registry auth run: echo "//npm.pkg.github.com/:_authToken=\${NODE_AUTH_TOKEN}" >> ~/.npmrc @@ -55,10 +57,12 @@ jobs: # current latest, otherwise v-last-published; beta/alpha/rc for # pre-releases. npm publishes first; if it fails the job stops before GHR, # keeping the registries in sync. + # + # npm publish uses OIDC trusted publishing (no NPM_PUBLISH_TOKEN needed). + # publish.sh adds --provenance for supply-chain attestations. - id: release name: Publish to NPM env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} TARBALL: ${{ steps.pack.outputs.tarball }} DRY_RUN: ${{ github.event_name == 'workflow_dispatch' }} run: scripts/publish.sh "https://registry.npmjs.org" "$TARBALL" diff --git a/scripts/publish.sh b/scripts/publish.sh index e9f874961..a10f0d0b0 100755 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -87,8 +87,12 @@ trap 'rm -f "$body_file"' EXIT # curl -sS (no --fail) returns 0 for any HTTP response, non-zero only on # network/protocol errors — so we can cleanly separate "server answered" from # "could not reach server". +auth_header="" +if [[ -n "${NODE_AUTH_TOKEN:-}" ]]; then + auth_header="Authorization: Bearer ${NODE_AUTH_TOKEN}" +fi if ! http_code=$(curl -sS -m 30 -o "$body_file" -w '%{http_code}' \ - -H "Authorization: Bearer ${NODE_AUTH_TOKEN:-}" "$packument_url"); then + ${auth_header:+-H "$auth_header"} "$packument_url"); then echo "ERROR: request to ${packument_url} failed (network/timeout); refusing to publish on an ambiguous result." >&2 exit 1 fi @@ -134,11 +138,18 @@ if [[ "$dry_run" == "true" ]]; then exit 0 fi +# Add --provenance for npm registry (OIDC trusted publishing generates +# supply-chain attestations). Not supported by GitHub Package Registry. +provenance_flag="" +if [[ "$registry" == *"registry.npmjs.org"* ]]; then + provenance_flag="--provenance" +fi + echo "Publishing ${pkg}@${version} (tag: ${tag}) to ${registry}" if [[ -n "$tarball" ]]; then # The tarball is a prebuilt artifact; skip lifecycle scripts (prepublishOnly # = test + build) that would otherwise run from the current package.json. - npm publish "$tarball" --registry "$registry" --tag "$tag" --ignore-scripts + npm publish "$tarball" --registry "$registry" --tag "$tag" --ignore-scripts $provenance_flag else - npm publish --registry "$registry" --tag "$tag" + npm publish --registry "$registry" --tag "$tag" $provenance_flag fi