Skip to content

v0.24.1 — the release that publishes itself #3

v0.24.1 — the release that publishes itself

v0.24.1 — the release that publishes itself #3

Workflow file for this run

# Publish the release to npm.
#
# moshcode's own install path is `curl … install.sh | sh`, and npm is a second
# channel rather than the primary one — which is exactly why it needs to be
# automatic. A channel that only updates when someone remembers is a channel
# that silently serves an old version forever.
#
# Runs on a published GitHub release, so the release itself is the trigger and
# there is no separate step to forget. `workflow_dispatch` is for re-running one
# that failed on something transient; it is safe because a version already on
# the registry is skipped rather than attempted.
#
# Authenticates by trusted publishing (OIDC) rather than a stored token: npm
# trades the short-lived token GitHub mints for this specific workflow run for
# permission to publish, so there is no long-lived credential in the repository
# to leak, rotate or forget.
#
# The other half of that trust lives on npmjs.com, under the package's Trusted
# Publisher settings, and it is pinned to the *filename* of this workflow.
# Renaming this file silently breaks publishing — npm will refuse the exchange
# because the run no longer matches what was configured.
name: publish
on:
release:
types: [published]
workflow_dispatch:
permissions:
contents: read
# The whole basis of the exchange: this is what lets the run mint the OIDC
# token npm authenticates against. Without it there is no credential at all
# and publishing fails outright.
id-token: write
jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
# pnpm version is read from the "packageManager" field in package.json.
# Do not pin a version here — it conflicts with packageManager and fails
# with ERR_PNPM_BAD_PM_VERSION.
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
registry-url: https://registry.npmjs.org
# Node 22 bundles npm 10, which predates trusted publishing and would fall
# back to looking for a token that no longer exists — an auth failure that
# reads as a credential problem rather than a version one. 11.5.1 is the
# floor; the check below says so plainly if that ever regresses.
- name: Install an npm that understands trusted publishing
run: |
npm install -g npm@latest
VERSION="$(npm --version)"
MINIMUM=11.5.1
echo "npm $VERSION"
# Lowest of the two must be the minimum, or this npm is older than it.
if [ "$(printf '%s\n%s\n' "$MINIMUM" "$VERSION" | sort -V | head -n1)" != "$MINIMUM" ]; then
echo "::error::npm $VERSION cannot use trusted publishing — $MINIMUM or later is required"
exit 1
fi
- run: pnpm install --frozen-lockfile
# Publishing is the one action here that cannot be taken back — npm will
# not let a version be replaced — so the tests run first, on the exact
# tree about to be packed.
- run: pnpm run --if-present test
env:
CI: true
- name: Read the version being published
id: version
run: |
VERSION="$(node -p "require('./package.json').version")"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "moshcode@$VERSION"
# A release tagged v0.25.0 carrying package.json 0.24.0 would publish the
# wrong tree under a version nobody can reuse. Cheap to check, impossible
# to undo.
- name: Check the tag matches package.json
if: github.event_name == 'release'
env:
TAG: ${{ github.event.release.tag_name }}
VERSION: ${{ steps.version.outputs.version }}
run: |
if [ "$TAG" != "v$VERSION" ]; then
echo "::error::release tag $TAG does not match package.json version $VERSION"
exit 1
fi
# Makes a re-run harmless. Without it, dispatching the workflow twice
# fails the second time on an E403 that reads like something broke.
- name: Skip if this version is already on npm
id: published
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
if npm view "moshcode@$VERSION" version >/dev/null 2>&1; then
echo "already=true" >> "$GITHUB_OUTPUT"
echo "moshcode@$VERSION is already published — nothing to do"
else
echo "already=false" >> "$GITHUB_OUTPUT"
fi
# No token, and no `--provenance` either: publishing through trusted
# publishing generates and attaches the attestation on its own.
- name: Publish
if: steps.published.outputs.already == 'false'
run: npm publish --access public
- name: Confirm the registry has it
if: steps.published.outputs.already == 'false'
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
# The registry is read-through cached, so a fresh publish can 404 for
# a moment. Retry rather than report a good publish as a failure.
for _ in 1 2 3 4 5; do
if [ "$(npm view "moshcode@$VERSION" version 2>/dev/null)" = "$VERSION" ]; then
echo "moshcode@$VERSION is on the registry"
exit 0
fi
sleep 5
done
echo "::error::published, but the registry does not report moshcode@$VERSION yet"
exit 1