Release pipeline
How tempest-react-sdk is published to npm — an automatic tag-push workflow,
with a manual fallback.
Overview
Local: GitHub Actions:
make release TAG=X.Y.Z
│
▼
┌─────────────────────────┐
│ scripts/release.sh: │
│ 1. branch release/vTAG │
│ 2. npm version TAG │
│ 3. close the CHANGELOG │
│ ([Unreleased] → │
│ [TAG] — date) │
│ 4. validate (lint + │
│ format + typecheck │
│ + test + build) │
│ 5. commit + tag local │
│ 6. push branch + tag │──────────► tag push triggers
│ 7. open PR via gh │ .github/workflows/release-npm.yml
└─────────────────────────┘ │
▼
┌────────────────────────────┐
│ 1. Checkout @ tag │
│ 2. tag == package.json? │
│ 3. Lint + format-check │
│ 4. Typecheck │
│ 5. Tests (vitest) │
│ 6. Build (vite + dts) │
│ 7. Smoke install │
│ 8. npm publish │
│ --provenance │
│ 9. read-back: does the │
│ registry serve it as │
│ dist-tag latest? │
│ 10. GitHub Release for the │
│ tag (notes = CHANGELOG,│
│ tarball attached) │
└────────────────────────────┘
The three surfaces move together: the git tag, the npm version and the GitHub
Release. The workflow fails when the tag does not describe the package.json
version, and fails when the registry is not serving that version as latest — so
"green workflow" really does mean "published and visible".
A tag push is the only way to publish. There is no "publish via PR merge" —
merging the release PR is only to sync main with the updated package.json +
RELEASES.md.
Commands
make release TAG=0.1.5
The full pipeline. Requires a clean working tree + a tag that doesn't exist locally/remotely.
It blocks if CHANGELOG.md doesn't mention [TAG] or [Unreleased] (with a
prompt to force continuation).
make release TAG=0.1.5 DRY_RUN=1
Identical, but stops before the push — you inspect the local branch and tag before continuing manually:
git push -u origin release/v0.1.5
git push origin v0.1.5
gh pr create --base main --head release/v0.1.5 --title "chore: release v0.1.5"
make release TAG=0.1.5 SKIP_VALIDATE=1
Skips local validation (npm ci, lint, format-check, typecheck, test, build,
pack dry-run). Use it only in emergencies — CI will validate again from scratch.
make validate
Runs all local validation without releasing. Equivalent to the CI validation block.
make publish
The manual fallback. Requires NPM_TOKEN in ~/.npmrc (a token with 2FA bypass)
or an interactive npm login. It does not trigger the workflow — it's a
direct publish.
npm config set //registry.npmjs.org/:_authToken=npm_xxx... --location=user
npm run build
make publish
Without a 2FA-bypass token, npm requires an OTP:
npm publish --access public --otp=123456
make releases
Lists every v*.*.* tag ordered by version (most recent first).
make releases-md
Regenerates RELEASES.md from the git tags. Called automatically by
scripts/release.sh after creating the tag.
make releases-check
A sync report across the three surfaces — one line per git tag, telling you whether the version exists on npm and whether the tag has a GitHub Release:
TAG NPM RELEASE STATUS
v0.24.0 ok ok sincronizado
v0.23.0 ok FALTA DESSINCRONIZADO
Read-only, always safe to run. Use it before and after a release.
make releases-sync / make releases-sync-dry
Creates the missing GitHub Releases for tags that already exist (backfill), with the notes taken from the matching CHANGELOG.md section. Tags that already have a Release are skipped — the script is idempotent and never rewrites an existing Release.
make releases-sync-dry # list what it would create, without creating anything
make releases-sync # actually create them
Needed because publishing to npm and cutting the Release only started moving together in v0.24.0: earlier tags existed in git and on npm, but with no GitHub Release.
Backfilled notes never inherit [Unreleased]
scripts/changelog.mjs notes <version> only falls back to the [Unreleased] block when given --allow-unreleased (which the workflow does, for a release cut before the section was dated). The backfill does not pass the flag — so an old tag never gets the next cycle's notes; with no section, the Release ships a pointer to CHANGELOG.md.
CI workflow (.github/workflows/release-npm.yml)
Triggered by:
push: tags: [v*.*.*]— the main flow.make release TAG=Xpushes a tag and the workflow fires automatically.workflow_dispatch— manual viagh workflow run release-npm.yml --ref main. Useful when a tag's publish failed and you want to re-run without bumping the version.
Steps in the publish job:
- Checkout (
actions/checkout@v5) withfetch-depth: 0. - Node 22 +
registry-url: https://registry.npmjs.org+ npm cache, plusnpm install -g npm@latest(Trusted Publishing needs npm >= 11.5.1). - Version guard — compares the tag (
GITHUB_REF_NAMEminus thev) againstpackage.json'sversionand aborts before any publish when they diverge. It also derivesprerelease(a version containing-) so the Release is marked correctly. Onworkflow_dispatchthe tag is derived frompackage.json. npm ci.- Lint (
npm run lint). - Format check (
npm run format:check). - Typecheck (
npm run typecheck). - Tests (
npm run test:run). - Build (
npm run build). - Smoke install — produces a tarball via
npm pack, installs it in/tmp/sdk-smokewithreact@^19 react-dom@^19(everything else ships as a direct dependency of the package), imports the package dynamically and validates that 20 core exports are present. npm publish --provenance --access publicvia Trusted Publishing (OIDC) — noNPM_TOKEN.id-token: writeis what enables the sigstore provenance attestation.- Registry read-back — confirms npm serves
tempest-react-sdk@<version>(up to 5 attempts, since the registry takes a moment to propagate) and thatdist-tags.latestpoints at it. Fails the job otherwise: a publish that landed under a different tag would no longer look green. - GitHub Release —
gh release create <tag>with the tarball attached and the notes extracted from theCHANGELOG.mdsection (scripts/changelog.mjs notes <version> --allow-unreleased), plus a link to the npm version. When the Release already exists it doesgh release edit+upload --clobberinstead of failing, so re-running the workflow for the same tag is safe. Requirescontents: writeon the job.
Secrets needed on GitHub
None. Publishing uses npm Trusted Publishing: npm publish exchanges the GitHub Actions OIDC identity for a short-lived token, so there is no NPM_TOKEN in the repository. What must exist is a Trusted Publisher on npmjs.com pointing at this repo + the release-npm.yml file.
The GITHUB_TOKEN used to cut the Release is provided automatically by the Actions runtime — only the job's permissions: contents: write needs declaring, and it is.
NPM_TOKEN is still the local fallback's business
make publish publishes from your machine and does need a token in ~/.npmrc (Classic Automation, or Granular with "Allow bypass 2FA" checked). That path produces no provenance and no GitHub Release — if you use the fallback, run make releases-sync afterwards so the Release is not left missing.
GITHUB_TOKEN is provided automatically by the Actions runtime.
Provenance signing
The publish includes --provenance when run in CI. This requires:
permissions: id-token: writein the workflow (already configured).- npm >= 11.5.1 on the runner (the upgrade step handles it).
- A Trusted Publisher configured on npmjs.com for this repository.
The result: every published version carries an attestation signed by sigstore, linking the tarball to the commit + workflow run that produced it. Visible on the registry as a "Verified provenance" badge.
A local manual publish cannot get provenance — there's no OIDC provider
outside CI. make publish always runs without --provenance.
History
See RELEASES.md (auto-generated via make releases-md) and CHANGELOG.md (written by hand before each release).
See also
CHANGELOG.md— change log per versionRELEASES.md— tag table with date and commitMakefile— target definitionsscripts/release.sh— the pipeline bash script