From bac50d12ea4e54b98a711bda251878cca9be12a4 Mon Sep 17 00:00:00 2001 From: ShaneK Date: Wed, 19 Aug 2026 13:23:04 -0700 Subject: [PATCH] chore(cd): publish through the npm CLI so provenance works The release workflow has never completed a publish since #526 moved it off tokens. Four separate problems, none of which had run yet: - `npm install -g npm@latest` now resolves to npm 12, which requires Node >=22.22.2 while the workflow pins Node 20. Pinned to npm 11, the newest line that runs on Node 20 and still supports provenance and OIDC. - `lerna publish --provenance` fails outright. Lerna 4's CLI is yargs `.strict()` and does not declare that option, so it exits with "Unknown argument: provenance". - Lerna 4 uploads through `libnpmpublish@4`, which supports neither provenance nor OIDC trusted publishing, so no npm CLI upgrade can make `lerna publish` work. The release is now split: `lerna version` still handles bumps, changelogs, tags and the GitHub release, and `scripts/publish-packages.sh` does the upload with `npm publish --provenance`. - `permissions: contents: read` cannot push the release commit or tags, or create the GitHub release. Before #526 there was no permissions block, so the default write access applied. The publish script skips any package whose version is already on the registry, since lerna versions these independently and a release may bump only one of them. --- .github/workflows/cd.yml | 20 +++++++++++++++----- package.json | 3 ++- scripts/publish-packages.sh | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 6 deletions(-) create mode 100755 scripts/publish-packages.sh diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index f9237c4..9d4d25f 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -6,7 +6,9 @@ on: - stable permissions: - contents: read + # `lerna version` pushes the release commit and tags and creates the GitHub release, + # so the token needs write access to contents. id-token is for npm provenance. + contents: write id-token: write jobs: @@ -26,8 +28,11 @@ jobs: run: | git config user.name github-actions git config user.email github-actions@github.com - - name: 🟢 Ensure Latest npm - run: npm install -g npm@latest + # npm 12 requires Node >=22.22.2, so `npm@latest` cannot run on the Node 20 pinned + # above. npm 11 is the newest line that does, and it supports both provenance and + # OIDC trusted publishing. + - name: 🟢 Ensure Supported npm + run: npm install -g npm@11 shell: bash - name: 📦 Install Dependencies run: npm ci --no-package-lock @@ -35,9 +40,14 @@ jobs: - name: 🔄 Bootstrap run: npm run bootstrap -- --ignore-scripts shell: bash - - name: 🚀 Release - run: npm run publish:ci -- --provenance + - name: 🔖 Version + run: npm run version:ci shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # `lerna publish` uploads via libnpmpublish@4, which supports neither provenance nor + # OIDC trusted publishing, so the upload goes through the npm CLI instead. + - name: 🚀 Publish + run: npm run publish:ci + shell: bash diff --git a/package.json b/package.json index 184501b..dec91e3 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "format:check": "prettier \"**/*.ts\" --check", "prepublishOnly": "npm run build", "publish:testing": "lerna publish prerelease --preid=testing --exact --no-git-tag-version --no-push --dist-tag=testing", - "publish:ci": "lerna publish -m 'chore(release): publish [skip ci]' --exact --conventional-commits --yes --no-verify-access", + "version:ci": "lerna version -m 'chore(release): publish [skip ci]' --exact --conventional-commits --yes", + "publish:ci": "./scripts/publish-packages.sh", "publish:ci:testing": "lerna publish prerelease --preid=testing --no-git-tag-version --no-push --dist-tag=testing -m 'chore(release): publish [skip ci]' --exact --conventional-commits --yes", "cz": "git-cz" }, diff --git a/scripts/publish-packages.sh b/scripts/publish-packages.sh new file mode 100755 index 0000000..68a44f6 --- /dev/null +++ b/scripts/publish-packages.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# +# Publishes each package whose current version is not on the registry yet. +# +# Versioning still runs through `lerna version`, but the upload has to go through the +# npm CLI: lerna 4 publishes with libnpmpublish@4, which supports neither provenance +# nor OIDC trusted publishing, so `lerna publish` cannot satisfy npm's requirements. +# +# Run this after `npm run version:ci`, from the repo root. + +set -euo pipefail + +published=0 + +for manifest in packages/*/package.json; do + pkg_dir=$(dirname "$manifest") + name=$(node -p "require('./$manifest').name") + version=$(node -p "require('./$manifest').version") + + if [ "$(node -p "require('./$manifest').private === true")" = "true" ]; then + echo "skip $name (private)" + continue + fi + + # Lerna versions packages independently, so a release may bump only one of them. + if npm view "$name@$version" version >/dev/null 2>&1; then + echo "skip $name@$version (already on registry)" + continue + fi + + echo "publish $name@$version" + (cd "$pkg_dir" && npm publish --provenance --access public) + published=$((published + 1)) +done + +echo "published $published package(s)"