From 88c6999d42efc237209d4032bf689744cc11bd4b Mon Sep 17 00:00:00 2001 From: ianwremmel-ai-agent Date: Sat, 12 Sep 2026 16:53:52 +0000 Subject: [PATCH 1/2] ci(release): pass workspaces-update through the environment @semantic-release/npm runs `npm version` with --userconfig pointed at a temp file. Under trusted publishing that file is never written, because the plugin returns from verify-auth before it reaches set-npmrc-auth, so --userconfig displaces ~/.npmrc and workspaces-update falls back to its default of true. `npm version` then reifies the workspace against the "*" ranges that multi-semantic-release has just rewritten to the versions this run intends to publish, and dies with ETARGET on the first one the registry does not have yet. Environment variables outrank every npm config file, so the setting survives the --userconfig redirect. --- .github/workflows/push.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 3adc4b68..d127b34b 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -98,11 +98,17 @@ jobs: fetch-depth: 0 - uses: './.github/actions/setup' - run: | - npm config set workspaces-update false npx multi-semantic-release \ --deps.release=inherit \ --ignore-private-packages env: + # multi-semantic-release rewrites each package's "*" dependency + # ranges to the versions this run is about to publish, so reifying + # the workspace mid-run resolves versions the registry does not have + # yet. This has to travel as an env var: @semantic-release/npm runs + # `npm version` with --userconfig pointed at a temp file, which + # displaces ~/.npmrc, and env beats every config file. + npm_config_workspaces_update: 'false' GIT_AUTHOR_EMAIL: ${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com GIT_COMMITTER_EMAIL: From 5d843de6103e910711c998c60a566d687c29a47c Mon Sep 17 00:00:00 2001 From: ianwremmel-ai-agent Date: Sat, 12 Sep 2026 16:53:54 +0000 Subject: [PATCH 2/2] ci(release): add a script to register npm trusted publishers npm attaches a trusted publisher to a single package, not to an org or a scope, so each public workspace package needs its own configuration before the release job's OIDC token exchange is accepted. scripts/npm-trust walks the workspace and runs `npm trust github` for every one of them. The registry refuses to configure a package that does not exist yet, so a newly added package still needs one manual publish before the script can cover it. --- scripts/npm-trust | 63 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100755 scripts/npm-trust diff --git a/scripts/npm-trust b/scripts/npm-trust new file mode 100755 index 00000000..537377a7 --- /dev/null +++ b/scripts/npm-trust @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Registers GitHub Actions as the npm trusted publisher for every public +# workspace package. npm stores that relationship per package, and without it +# the OIDC token exchange in .github/workflows/push.yml gets rejected and +# @semantic-release/npm falls back to looking for an NPM_TOKEN. A newly added +# package needs a run of this script before its first release. +# +# Requires npm >= 11.15.0, write access to every package, and account-level +# 2FA. npm asks for an OTP on the first package; the npmjs.com prompt offers to +# skip 2FA for the next five minutes, which covers the rest of the run. +# +# Usage: scripts/npm-trust [package...] + +repo=code-like-a-carpenter/workbench +workflow=push.yml +required_npm=11.15.0 + +npm_version="$(npm --version)" +if [ "$(printf '%s\n%s\n' "$required_npm" "$npm_version" | sort -V | head -n1)" != "$required_npm" ]; then + echo "npm trust requires npm >= $required_npm, but npm is $npm_version" >&2 + exit 1 +fi + +if [ $# -gt 0 ]; then + packages=("$@") +else + mapfile -t packages < <( + npm pkg get name private --ws --json | node -e ' + const chunks = []; + process.stdin.on("data", (chunk) => chunks.push(chunk)); + process.stdin.on("end", () => { + for (const pkg of Object.values(JSON.parse(chunks.join("")))) { + if (pkg.private !== true) { + console.log(pkg.name); + } + } + }); + ' + ) +fi + +failed=() +for package in "${packages[@]}"; do + echo "==> $package" + if ! npm trust github "$package" \ + --file "$workflow" \ + --repo "$repo" \ + --allow-publish \ + --yes; then + failed+=("$package") + fi + # The registry rate limits the trust endpoint; npm's docs suggest a 2s gap. + sleep 2 +done + +if [ "${#failed[@]}" -gt 0 ]; then + echo "Failed to configure:" >&2 + printf ' %s\n' "${failed[@]}" >&2 + exit 1 +fi