Skip to content

publish

publish #8

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.
#
# When it goes wrong the error names neither OIDC nor trusted publishing:
#
# npm error code E404
# npm error 404 Not Found - PUT https://registry.npmjs.org/moshcode
# npm error 404 ... could not be found or you do not have permission
#
# That is an unauthenticated PUT, reported as though the package did not exist.
# It has two quite different causes, and it took v0.24.3 to tell them apart:
#
# 1. anything that leaves an empty auth token in an .npmrc, which stops npm
# attempting the exchange at all — see the setup-node note below; or
# 2. no trusted publisher registered for this package, or one registered
# against a different workflow filename.
#
# Check 1 first. It is in this file, and it is the one that looks like 2.
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
# Deliberately no `registry-url`. With it, setup-node always writes
#
# //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}
#
# into an .npmrc — correct for token auth, and quietly fatal here. Under
# trusted publishing there is no NODE_AUTH_TOKEN, so that line resolves to
# an empty token, and npm stops before ever attempting the OIDC exchange:
# it believes it already has credentials. The registry then answers the
# unauthenticated PUT with E404, which names nothing to do with OIDC and
# is what sent us looking at the npmjs.com config instead of at this file.
#
# actions/setup-node#1551. Without registry-url no .npmrc is written and
# npm defaults to registry.npmjs.org anyway, which is where we publish.
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
# Node 24 bundles npm 11, but pin the floor anyway rather than depend on
# what a runner image happens to ship: below 11.5.1 there is no trusted
# publishing, and the failure would again look like a credential problem.
- 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. `--provenance` is passed even though npm documents it as
# automatic under trusted publishing: reports differ on whether it really
# is, and asking for it explicitly costs nothing and cannot produce a
# weaker result. v0.24.2 published with an attestation using this flag.
- name: Publish
if: steps.published.outputs.already == 'false'
run: npm publish --access public --provenance
- 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