Skip to content

Commit 35c90d9

Browse files
jaredwrayclaude
andauthored
feat: registry-aware release workflow with provenance + OIDC (#1664)
* feat: add registry-aware release workflow with provenance + OIDC Add a release pipeline that publishes only the workspace packages whose manually-set version is ahead of the npm registry — versions are never bumped by the tooling. - scripts/release.mjs: enumerates publishable packages (pnpm), compares each local version against the registry (404/new/already-published), computes the full plan before publishing, and publishes in dependency (topological) order using pnpm only with provenance. Supports --dry-run (plan + `pnpm publish --dry-run` packaging validation), --json, and writes a GitHub step summary + outputs. - .github/workflows/release.yml: runs on push to main (package.json changes) and manual dispatch with a dry-run input; grants id-token: write for OIDC trusted publishing + provenance, with NPM_TOKEN as an optional fallback. - RELEASE.md: documents the flow and the one-time npmjs.com trusted publisher setup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F7cjEDzGAPQUmvQehU1sGF * fix: address review feedback on release tooling - release.mjs: encode all "/" in scoped package names for the registry URL (replaceAll) — fixes the CodeQL "incomplete string escaping" alert. - release.mjs: abort a real publish on the first failure so a failed dependency never leaves dependents publishing against a missing version; a dry run still validates every package. Report packages skipped by the abort. - release.mjs: add a 10s AbortSignal.timeout to registry fetches so a stalled registry fails fast into the existing retry/backoff. - release.yml: set persist-credentials: false on checkout (the job only reads the repo and never pushes). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F7cjEDzGAPQUmvQehU1sGF * refactor: make release fully tokenless (OIDC trusted publishing only) Remove the NODE_AUTH_TOKEN/NPM_TOKEN fallback so publishing relies solely on OIDC trusted publishing, eliminating the long-lived npm token. Each package must have a trusted publisher configured on npmjs.com; document that a brand-new package's first publish needs a one-time manual publish. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F7cjEDzGAPQUmvQehU1sGF * fix: guard against version rollbacks and off-main manual publishes Addresses two Codex review findings: - release.mjs: refuse to publish a package whose local version is behind the registry's `latest` dist-tag. Publishing under the default tag would move `latest` (and every `^`/`~` consumer) backward, so an accidental rollback now fails the run before anything is published. Adds a small spec-correct semver comparator (no new dependency). - release.yml: force a dry run for any manual dispatch from a ref other than `main`, so branch versions can't be published to npm (the publish step uses --no-git-checks). Real publishing only happens on main. The "abort on first publish failure" finding was already handled in the prior review-fixes commit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F7cjEDzGAPQUmvQehU1sGF * fix: trigger release on published GitHub release, not push to main Per review: the release workflow should run when a GitHub release is published (in lockstep with deploy-website.yml, which also runs on `released`) or on manual dispatch — not on every push to main. - Replace the push-to-main trigger with `release: types: [released]`. - A published release publishes for real; a manual run still honors its dry-run input and is forced to a dry run from any ref other than main. - Update RELEASE.md to describe the release-driven flow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F7cjEDzGAPQUmvQehU1sGF * docs: move release docs into CONTRIBUTING.md Per review: fold the release process into the existing CONTRIBUTING.md as a "Releasing" section instead of a standalone RELEASE.md file. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F7cjEDzGAPQUmvQehU1sGF * docs: add Releasing section to CONTRIBUTING.md Completes the move from RELEASE.md (the prior commit deleted the file but the staging of CONTRIBUTING.md was dropped due to a git add error). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F7cjEDzGAPQUmvQehU1sGF --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 917bd83 commit 35c90d9

3 files changed

Lines changed: 596 additions & 0 deletions

File tree

.github/workflows/release.yml

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
name: release
2+
3+
# Publishes workspace packages to npm whose manually-set version is ahead of
4+
# the registry. Versions are never bumped here — a human bumps each package's
5+
# `version` (typically via a release PR); publishing a GitHub release then
6+
# triggers this workflow, which figures out what actually needs publishing and
7+
# publishes it in dependency order. See scripts/release.mjs for the decision logic.
8+
#
9+
# Authentication uses OIDC trusted publishing (tokenless) with provenance:
10+
# - `id-token: write` lets GitHub mint the OIDC token pnpm exchanges for a
11+
# short-lived npm token and uses to sign the provenance attestation.
12+
# - Configure a "Trusted Publisher" for each package on npmjs.com pointing at
13+
# repo `jaredwray/cacheable` + workflow `release.yml`. Once configured, no
14+
# npm token is needed (pnpm 11 performs the OIDC exchange natively).
15+
# - This workflow is fully tokenless: every published package must have a
16+
# trusted publisher configured first. A brand-new package's first-ever
17+
# publish (which OIDC cannot do) needs a one-time manual `pnpm publish`.
18+
19+
on:
20+
# Manual runs — supports a dry run that prints the plan and validates
21+
# packaging without publishing anything.
22+
workflow_dispatch:
23+
inputs:
24+
dry-run:
25+
description: "Dry run (compute the plan + validate packaging, publish nothing)"
26+
type: boolean
27+
default: false
28+
# Publishing a (non-pre) GitHub release publishes the bumped packages to npm,
29+
# in lockstep with the website deploy, which also runs on `released`.
30+
release:
31+
types: [released]
32+
33+
# Never run two releases at once; let an in-flight publish finish rather than
34+
# cancelling it half-way through the package set.
35+
concurrency:
36+
group: release
37+
cancel-in-progress: false
38+
39+
permissions:
40+
contents: read
41+
id-token: write # required for OIDC trusted publishing + provenance
42+
43+
jobs:
44+
release:
45+
name: Release
46+
runs-on: ubuntu-latest # provenance/OIDC require a cloud-hosted runner
47+
48+
steps:
49+
- name: Checkout
50+
uses: actions/checkout@v4
51+
with:
52+
# This job only reads the repo (it never pushes), so don't leave the
53+
# GITHUB_TOKEN in .git/config for later steps / actions to read.
54+
persist-credentials: false
55+
56+
- name: Install pnpm
57+
uses: pnpm/action-setup@v6
58+
59+
- name: Use Node.js
60+
uses: actions/setup-node@v4
61+
with:
62+
node-version-file: .nvmrc
63+
registry-url: https://registry.npmjs.org # writes the .npmrc the registry expects
64+
cache: pnpm
65+
66+
- name: Enable Corepack
67+
run: corepack enable
68+
69+
- name: Install dependencies
70+
run: pnpm install --frozen-lockfile
71+
72+
- name: Build
73+
run: pnpm build
74+
75+
- name: Validate build output
76+
run: node scripts/test-build.mjs
77+
78+
- name: Release
79+
# A published release publishes for real. A manual (workflow_dispatch)
80+
# run honors its dry-run input, and is forced to a dry run from any ref
81+
# other than main so branch versions can't be pushed to npm.
82+
run: node scripts/release.mjs ${{ (github.event_name == 'workflow_dispatch' && (inputs.dry-run || github.ref != 'refs/heads/main')) && '--dry-run' || '' }}

CONTRIBUTING.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,68 @@ You can contribute changes to this repo by opening a pull request:
2828

2929
If you need more information on the steps to create a pull request, you can find a detailed walkthrough in the [GitHub documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork)
3030

31+
# Releasing
32+
33+
Maintainers publish releases to npm via the
34+
[`release`](.github/workflows/release.yml) GitHub workflow, driven by
35+
[`scripts/release.mjs`](scripts/release.mjs). Versions are set manually — the
36+
tooling never bumps them.
37+
38+
## How it works
39+
40+
1. **Set versions manually.** Bump the `version` field of each package you want
41+
to release (usually in a "release" PR) and merge it to `main`.
42+
2. **Publish a GitHub release.** Publishing a (non-pre) release triggers the
43+
`release` workflow — in lockstep with the website deploy, which also runs on
44+
`released`. (You can also run it manually from the Actions tab.)
45+
3. **The script decides what to publish.** For every publishable workspace
46+
package it compares the local `version` against the npm registry:
47+
- already published → **skip**
48+
- new version, or package not yet on npm → **publish**
49+
- below the registry's `latest`**refuse** (won't roll `latest` back)
50+
4. **It publishes in dependency order** (e.g. `@cacheable/utils` before
51+
`cacheable`), with provenance, using pnpm only, aborting on the first failure.
52+
53+
Private packages and the internal `@cacheable/benchmark` harness are never
54+
published (see `IGNORED_PACKAGES` in the script).
55+
56+
## Dry run
57+
58+
Validate a release without publishing anything — prints the plan and packs each
59+
package via `pnpm publish --dry-run`:
60+
61+
```sh
62+
node scripts/release.mjs --dry-run # locally
63+
```
64+
65+
Or run the **release** workflow manually from the Actions tab with the **Dry
66+
run** input checked. `node scripts/release.mjs --json` emits the plan as
67+
machine-readable JSON.
68+
69+
## Authentication: OIDC trusted publishing (+ provenance)
70+
71+
The workflow publishes **tokenlessly** via npm
72+
[trusted publishing](https://docs.npmjs.com/trusted-publishers/) over OIDC, and
73+
attaches a [provenance](https://docs.npmjs.com/generating-provenance-statements/)
74+
attestation. The job grants `id-token: write`, runs on a GitHub-hosted runner,
75+
and every package declares a matching public `repository` field — the
76+
preconditions provenance requires.
77+
78+
**One-time setup on npmjs.com — for each published package** add a Trusted
79+
Publisher under *Settings → Publishing access*:
80+
81+
- Provider: **GitHub Actions**
82+
- Repository: `jaredwray/cacheable`
83+
- Workflow filename: `release.yml`
84+
85+
The workflow is **fully tokenless** — there is no `NPM_TOKEN` secret. Every
86+
published package must have a trusted publisher configured before the workflow
87+
can publish it; until then that package's publish step fails to authenticate.
88+
89+
One caveat: OIDC cannot perform the *first ever* publish of a brand-new package.
90+
When adding a new package, publish its initial version once manually (e.g.
91+
`pnpm --filter <name> publish --access public` while logged in to npm), then add
92+
its trusted publisher so every subsequent release flows through this workflow.
93+
3194
# Code of Conduct
3295
Please refer to our [Code of Conduct](CODE_OF_CONDUCT.md) readme for how to contribute to this open source project and work within the community.

0 commit comments

Comments
 (0)