npm dist-tag #4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Retire (or repoint) a dist-tag across the whole lockstep npm set. | |
| # | |
| # WHY THIS EXISTS. A dist-tag left pointing at a superseded prerelease is a live install | |
| # path: `npm i @metaobjectsdev/cli@next` served 1.0.0-rc.8 for as long as `next` outlived | |
| # the 1.0.0 cut. Removing it by hand is 14 registry calls, and the maintainer's LOCAL | |
| # granular token cannot make them — it 403s on `dist-tag rm` for every package and on | |
| # `npm profile get`, and passing `--otp` does not change the error, so the registry is not | |
| # asking for a second factor: that token simply lacks the right. The CI `NPM_TOKEN` is the | |
| # one that publishes, so it is the one that can retag. Running it here also means no token | |
| # ever has to be pasted onto a laptop. | |
| # | |
| # The set and its order come from scripts/publish-set.mjs — the single source of truth for | |
| # WHICH packages a release touches (CLAUDE.md: never hardcode the publish set; a new | |
| # publish path must read it too). Written to a file first so a non-zero exit fails the step | |
| # rather than yielding a silently short list. | |
| name: npm dist-tag | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'dist-tag to operate on (e.g. next)' | |
| required: true | |
| default: 'next' | |
| action: | |
| description: 'rm = remove the tag; ls = report only (no writes)' | |
| required: true | |
| default: 'ls' | |
| type: choice | |
| options: [ls, rm] | |
| jobs: | |
| dist-tag: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # NO `registry-url:` here. It makes setup-node write its own .npmrc and export | |
| # NPM_CONFIG_USERCONFIG at it, which SHADOWS the ~/.npmrc the step below writes — | |
| # npm then authenticates with setup-node's literal `${NODE_AUTH_TOKEN}` placeholder | |
| # and returns E401 "token seems to be invalid" for every package, which reads as a | |
| # dead secret rather than a shadowed config. Authenticate exactly the way | |
| # publish-npm.yml does, so the token that publishes the set is the token that retags it. | |
| - name: Set up Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: ${{ inputs.action }} '${{ inputs.tag }}' across the lockstep set | |
| env: | |
| NPM_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| TAG: ${{ inputs.tag }} | |
| ACTION: ${{ inputs.action }} | |
| run: | | |
| set -euo pipefail | |
| echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc | |
| trap 'rm -f ~/.npmrc' EXIT | |
| # Fail on the credential, once, before touching 14 packages. Without this an | |
| # auth problem arrives as fourteen identical per-package errors and looks like a | |
| # registry-side refusal of the operation. | |
| npm whoami >/dev/null 2>&1 || { echo "::error::NPM_TOKEN does not authenticate"; exit 1; } | |
| node scripts/publish-set.mjs > "${RUNNER_TEMP}/set.txt" | |
| test -s "${RUNNER_TEMP}/set.txt" | |
| fail=0 | |
| while read -r dir; do | |
| name=$(node -p "require('./${dir}/package.json').name") | |
| if [ "$ACTION" = "ls" ]; then | |
| printf '%-42s %s\n' "$name" "$(npm dist-tag ls "$name" | tr '\n' ' ')" | |
| else | |
| # A tag that is already absent is the desired end state, not a failure. | |
| if npm dist-tag ls "$name" | grep -q "^${TAG}:"; then | |
| npm dist-tag rm "$name" "$TAG" && echo " ✓ $name — removed '$TAG'" || { echo " ✗ $name"; fail=1; } | |
| else | |
| echo " · $name — no '$TAG' tag, nothing to do" | |
| fi | |
| fi | |
| done < "${RUNNER_TEMP}/set.txt" | |
| # Report every failure before exiting, so one run names them all. | |
| exit "$fail" |