Skip to content

Publish to npm

Publish to npm #20

Workflow file for this run

# npm publish — manual only (run when you want, via "Run workflow").
#
# Auth: npm trusted publishing via OIDC (no NPM_TOKEN). Configure the trusted
# publisher once at npmjs.com (Package → Settings → Trusted Publisher → GitHub
# Actions: this repo + this workflow file) before the first run.
#
# Idempotent: skips a version already on the registry, and asserts the END
# STATE (polls the registry) rather than trusting npm's exit code, because
# npm's OIDC flow can exit non-zero from a trailing web-auth 401 *after* the
# tarball + provenance are already published. `prepublishOnly` builds any
# generated artifacts (e.g. .d.ts) into the tarball at publish time.
name: Publish to npm
on:
workflow_dispatch: # manual "Run workflow" button — publish when you want
permissions:
contents: read
id-token: write # required: lets npm mint OIDC credentials
concurrency: # never let two publish runs race
group: npm-publish
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
- name: Upgrade npm (trusted publishing needs >= 11.5.1)
run: npm install -g npm@latest
- name: Install dependencies
run: npm ci
- name: Typecheck
run: npm run typecheck --if-present
- name: Test
run: npm test --if-present
- name: Skip if this version is already on npm
id: check
run: |
NAME=$(node -p "require('./package.json').name")
VERSION=$(node -p "require('./package.json').version")
PUBLISHED=$(npm view "$NAME@$VERSION" version 2>/dev/null || true)
if [ -n "$PUBLISHED" ]; then
echo "::notice::$NAME@$VERSION already published — skipping"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Publish
if: steps.check.outputs.skip == 'false'
run: |
# Don't trust npm's exit code (see header): publish, then assert the
# version is actually on the registry. A genuine failure still fails.
NAME=$(node -p "require('./package.json').name")
VERSION=$(node -p "require('./package.json').version")
PUBLISH_EXIT=0
npm publish --provenance --access public || PUBLISH_EXIT=$?
# Registry read-after-write can lag well past 15s. Poll generously
# (~2 min) so the end-state check doesn't false-negative a publish that
# actually succeeded. `npm view` caches, so --prefer-online bypasses it.
PUBLISHED=""
for _ in $(seq 1 20); do
PUBLISHED=$(npm view "$NAME@$VERSION" version --prefer-online 2>/dev/null || true)
[ "$PUBLISHED" = "$VERSION" ] && break
sleep 6
done
if [ "$PUBLISHED" = "$VERSION" ]; then
if [ "$PUBLISH_EXIT" -ne 0 ]; then
echo "::warning::npm publish exited $PUBLISH_EXIT but $NAME@$VERSION is on the registry (known npm OIDC post-publish web-auth 401); treating as success."
fi
echo "✅ Published $NAME@$VERSION"
elif [ "$PUBLISH_EXIT" -eq 0 ]; then
echo "::warning::npm publish exited 0 but $NAME@$VERSION not yet visible after ~2 min (registry reflection lag); treating as success."
echo "✅ Published $NAME@$VERSION (npm exit 0; registry lagging)"
else
echo "::error::npm publish failed (exit $PUBLISH_EXIT) and $NAME@$VERSION is NOT on the registry."
exit 1
fi