Files
cereale/.github/workflows/release.yml
T
Claude f56d2811cb 📦 docs: cereale is on npm, and release.yml moves to trusted publishing
0.4.0 published, so the three places that said it had not stopped being true:
the README install section, the landing page's install panel, and the "what
cereale is not" item. All three now say `npm install cereale`, and the last
becomes a limitation that is actually still true — it is 0.x, where a minor
bump is allowed to break you.

An npm version badge joins the row, tinted the same brand brown as the rest.
It was held back deliberately while the package did not exist, because a
badge that renders "not found" is worse than no badge.

page.js loses the install-shell substitution: it rewrote the tarball filename
in a code block that no longer exists.

--- release.yml: trusted publishing ---

npm exchanges the workflow's short-lived GitHub identity token for a publish
token scoped to this package, so no long-lived npm token has to exist. The
header documents exactly what to enter on npmjs.com, including the two fields
npm checks against the OIDC claims and refuses on mismatch: the workflow
filename must match this file, and Environment must stay blank while the job
declares none.

The find that matters: Node 22 bundles npm 10.x, which has no OIDC code at
all. Verified by unpacking the CLI — lib/utils/oidc.js is absent in 11.4.2 and
present in 11.5.0. Since npm's OIDC step is deliberately non-throwing, an old
CLI would have skipped trusted publishing in silence and fallen back to token
auth while appearing to work. So the workflow raises npm and then asserts the
version, rather than assuming it.

NPM_TOKEN stays as a fallback for the same non-throwing reason: this can land
before the registry side is configured, and nothing breaks. Delete the secret
once a real run shows OIDC working.

--provenance stays explicit. Under OIDC npm enables it itself for a public
repo, but only when the flag is left at its default (config.isDefault check in
oidc.js), so passing it just skips that auto-enable and lands in the same
place — while remaining the only thing that produces an attestation on the
token path.

actionlint clean; the version guard tested against 10.9.7, 11.4.2, 11.5.0 and
12.0.2; the page re-rendered with no errors and no "not on npm" text left.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
2026-08-20 16:10:42 +00:00

133 lines
5.6 KiB
YAML

name: Publish to npm
# Deliberately manual. Pushing a tag does NOT publish — a tag is a decision to cut a release,
# not a decision to make it public and immutable, and npm's 72-hour unpublish window makes the
# second one hard to take back. Run this workflow from the Actions tab when you mean it.
#
# Authentication is by **trusted publishing** (OIDC): npm exchanges the workflow's
# short-lived GitHub identity token for a publish token scoped to this package, so no
# long-lived npm token has to exist. To enable it, on npmjs.com → the cereale package →
# Settings → Trusted publisher → GitHub Actions, set:
#
# Organization or user: avalon-vanguard
# Repository: cereale
# Workflow filename: release.yml
# Environment: (leave blank — this workflow does not use one)
#
# The workflow filename must match this file's name, and the Environment field must be
# blank unless a matching `environment:` is added to the publish job; npm checks both
# against the OIDC claims and refuses the exchange if either disagrees.
#
# NPM_TOKEN is kept as a fallback. npm's OIDC step is deliberately non-throwing — if the
# trusted publisher is not configured, or the CLI is too old, it logs and falls through
# to token auth. That makes the switch safe to land before the registry side is set up.
# Once a real run shows OIDC working, the NPM_TOKEN secret can be deleted.
#
# Before a real run:
# 1. Run once with dry_run left as `true` and read the file list it prints.
# 2. Run again with dry_run set to `false`.
on:
workflow_dispatch:
inputs:
dry_run:
description: 'Resolve and pack everything, but do not publish'
type: boolean
default: true
permissions:
contents: read
id-token: write # the OIDC identity npm exchanges, and provenance
jobs:
publish:
name: ${{ inputs.dry_run && 'Dry run' || 'Publish' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Tags are not fetched at the default depth, and the version gate below
# resolves one — without this it fails on every release, tag or no tag.
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: 'npm'
registry-url: 'https://registry.npmjs.org'
# Node 22 bundles npm 10.x, which has no OIDC support at all — it would skip
# trusted publishing in silence and fall back to token auth. Trusted publishing
# landed in npm 11.5.0 (lib/utils/oidc.js), so the version is raised and then
# asserted rather than assumed.
- name: Use an npm that can do trusted publishing
run: |
npm install -g npm@latest
V=$(npm --version)
echo "npm $V"
MAJ=${V%%.*}; REST=${V#*.}; MIN=${REST%%.*}
if [ "$MAJ" -lt 11 ] || { [ "$MAJ" -eq 11 ] && [ "$MIN" -lt 5 ]; }; then
echo "::error::npm $V has no OIDC support; trusted publishing needs >= 11.5.0"
exit 1
fi
- name: Install dependencies
run: npm ci
# The tag and the manifest disagreeing is the classic way to publish 0.3.0 as 0.2.0.
- name: Tag and package.json version must agree
run: |
VERSION=$(node -p "require('./package.json').version")
echo "package.json version: $VERSION"
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
echo "tag v$VERSION exists"
else
echo "::error::No tag v$VERSION. Tag the release commit before publishing."
exit 1
fi
if [ "$(git rev-parse HEAD)" != "$(git rev-parse "v$VERSION^{commit}")" ]; then
echo "::error::v$VERSION does not point at the commit being published."
exit 1
fi
- name: Refuse to republish a version already on the registry
run: |
VERSION=$(node -p "require('./package.json').version")
NAME=$(node -p "require('./package.json').name")
if npm view "$NAME@$VERSION" version >/dev/null 2>&1; then
echo "::error::$NAME@$VERSION is already published. Bump the version."
exit 1
fi
echo "$NAME@$VERSION is not on the registry yet."
# The same gate that guards every push: type-check, lint, 259 tests, build, and the
# checks that the published types stand alone and the landing page has no CDN deps.
- name: Verify
run: npm run verify
- name: Show exactly what would ship
run: npm publish --dry-run
- name: Publish
if: ${{ inputs.dry_run == false }}
# --provenance stays explicit. Under OIDC npm would enable it on its own for a
# public repo, but only when the flag was left at its default; asking for it
# directly just skips that auto-enable and reaches the same place. On the token
# fallback path it is the only thing that produces an attestation at all.
run: npm publish --provenance --access public
env:
# Fallback only. Ignored once the trusted publisher is configured, because npm
# sets its own short-lived token before reading credentials.
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Summary
run: |
VERSION=$(node -p "require('./package.json').version")
{
echo "### cereale@$VERSION"
if [ "${{ inputs.dry_run }}" = "true" ]; then
echo "Dry run — nothing was published."
else
echo "Published to https://www.npmjs.com/package/cereale/v/$VERSION"
fi
} >> "$GITHUB_STEP_SUMMARY"