Skip to content

Publish

Publish #15

Workflow file for this run

name: Publish
# Releasing is not a thing anyone does. Merge a version bump to main and the
# package ships: this workflow asks the registry whether package.json's version
# already exists, and publishes it if not.
#
# Why that check rather than a tag trigger: a tag pushed by GITHUB_TOKEN does
# not start another workflow, so "push a tag, let publish.yml notice" silently
# never runs. Asking npm what is published is also idempotent — re-running this,
# or pushing a tag by hand, cannot double-publish or fail confusingly.
on:
push:
branches: [main]
tags: ['v*']
# The reconciler, and the reason this is reliable. A merge made with
# GITHUB_TOKEN (auto-merge, or any bot) starts no workflow, so the push
# trigger above silently does not fire for exactly the merges that matter.
# This repo has no auto-merge today, which is precisely why the schedule
# belongs here now rather than after it is added and a release goes missing.
#
# The schedule asks the registry the same idempotent question on a timer:
# is package.json's version published? A release missed by any means goes
# out within the hour without anyone noticing it was missed.
schedule:
- cron: '17 * * * *'
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
permissions:
# Tagging the released commit, so a version on the registry can always be
# traced back to the tree it was built from.
contents: write
# npm provenance: proves on the registry that this tarball was built by
# this workflow from this commit.
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
# npm >= 11.5.1 is required for trusted publishing (OIDC); Node 24 ships it.
node-version: '24'
registry-url: 'https://registry.npmjs.org'
- name: Is this version already on the registry?
id: check
run: |
name=$(node -p "require('./package.json').name")
version=$(node -p "require('./package.json').version")
echo "version=$version" >> "$GITHUB_OUTPUT"
if npm view "$name@$version" version >/dev/null 2>&1; then
echo "→ $name@$version is already published; nothing to do."
echo "publish=false" >> "$GITHUB_OUTPUT"
else
echo "→ $name@$version is not on the registry; releasing it."
echo "publish=true" >> "$GITHUB_OUTPUT"
fi
- if: steps.check.outputs.publish == 'true'
run: npm ci --ignore-scripts
# Never publish something that would not have passed CI.
- if: steps.check.outputs.publish == 'true'
run: npm run verify
# Bootstrap auth. Trusted publishing (OIDC) needs no token and is the
# destination; until it is configured on the package, NPM_TOKEN is what
# authenticates. The token expires — token-health.yml warns before it does,
# rather than letting a release be the thing that discovers it.
- if: steps.check.outputs.publish == 'true'
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Tag the released commit
if: steps.check.outputs.publish == 'true'
env:
TAG: v${{ steps.check.outputs.version }}
run: |
# Tag after a successful publish, so a tag never claims a release that
# did not happen. Skipped silently if it already exists.
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "→ tag $TAG already exists"
else
git tag "$TAG"
git push origin "$TAG"
fi