Publish to npm #20
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
| # 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 |