ci: build release tarballs without an OIDC token and narrow deploy-tests inputs
Two workflow hardenings.
Release: build where nothing can publish, publish where nothing runs
publish.yaml's publish job held the release environment and id-token: write, then restored cache: pnpm, ran pnpm install --frozen-lockfile and the whole build/check/test toolchain before pnpm --recursive publish --provenance. Every dev dependency, and whatever a restored cache brought back, ran while the OIDC token request credentials were in the job, so a compromised dependency could have exchanged them for a publish grant and published with valid provenance.
Now:
build(contents: read, noid-token, and nocache: what it packs is what gets published, so a restored pnpm store would be one more input, and a cache saved from it would be read by the default branch's other runs, which CodeQL flags asactions/cache-poisoning/poisonable-steponce the job is outside thereleaseenvironment): checks out the verified commit (persist-credentials: false), installs from the registry against the lockfile, builds, runs every check andpnpm testas before, makes the SBOM (moved here, as it needs pnpm's store), then packs each non-privatepackages/*package withpnpm pack --config.ignore-scripts=true, one at a time in pnpm's dependency order, and uploads the tarballs plus aSHA256SUMSlist in that order (artifacttarballs, one day).publish(environment: release,id-token: write): no cache, no install, nopnpm. It checks out the verified commit only to read manifests as data and the Node.js version, re-checks the tag, checks npm is 11.5.1 or later, downloads the artifact, runssha256sum --check --strict, refuses files the list does not name, and checks that the tarballs' own manifests are exactly this tag's public packages at its version. Then, in order,npm publish <tarball> --access public --provenance --tag <latest|next> --ignore-scripts(plus--dry-runfor a rehearsal). A version npm already holds is skipped, aspnpm -r publishskipped it, so a release that stopped partway can still be dispatched again.- Kept: the signed-tag verification (
verify), the "nothing has published a release for this tag" re-check before uploading, the provenance read-back and the draft job.release.yamland thereleaseenvironment are untouched, so npm's trusted publisher configuration still matches. The environment approval is now asked for afterbuildhas passed.
Why this is equivalent for what gets published:
- pnpm 12.16.1
pnpm packwritesworkspace:*andcatalog:ranges as concrete versions and foldspublishConfig(bin,exports) into the packed manifest, aspnpm publishdoes. Checked in a scratch workspace; that scratch run also ran the new pack, check and dry-run publish steps end to end, and the check refused a changed tarball, an extra file and a tree naming another package. npm publishwith a tarball reads the manifest out of the tarball, runs lifecycle scripts only for a directory, and does the OIDC exchange for any spec (lib/commands/publish.jsandlib/utils/oidc.jsin npm/cli). npm documents trusted publishing as needing npm 11.5.1+, Node 22.14+,id-token: writeand a cloud-hosted runner (Trusted publishing for npm packages). It does not document a tarball argument, so the first real release is where that gets confirmed.
workflow-security.test.ts now expects the fourth pinned checkout, and checks that id-token: write appears once, in a job that installs, restores and runs no pnpm, and that build holds no id-token and no cache.
What this does not do: the digests guard the hand-over between the two jobs, not the build. A dependency compromised inside build can still change what it packs, as it could before; what it can no longer do is use the OIDC token itself, to publish whatever it likes, under any name or version, with this workflow's provenance.
CodeQL reports actions/cache-poisoning/poisonable-step on the pack step (alert 50): it reads needs.verify.outputs.commit as untrusted, and build is no longer behind the release environment, which the old job was. That commit is the signed tag verify checked against release-signers.asc and main, and the same code ran with the same cache access in the old job. build no longer restores or saves a cache, which is the remedy the query names. A maintainer can dismiss the alert as a false positive, or ask for another arrangement.
Note for the next first publish: reverting the commit that retired bootstrap (CONTRIBUTING) now touches the restructured publish job, so the token-writing step needs re-applying by hand there.
deploy-tests
- Token file: it was written before the upstream Next.js
pnpm install && pnpm build, Chromium's install and Corepack's, all of them code of the requested Next.js ref and its dependencies. Now Preflight writes its own copy, hands the tool only the path (env -u TOKEN), and removes it on exit. The file the suite reads is written right beforeRun the suite. It is removed again right after the suite (if: always()). nextjsRef: a dispatch must name a release tag,^v\d+\.\d+\.\d+(-(canary|rc|beta)\.\d+)?$, checked inplanafter the existing single-line check. A release-tag ref is checked out asrefs/tags/<tag>, sinceactions/checkoutlooks for a branch of a short ref's name first. The nightly canary run still resolves its tag to a commit as before, and the pull-request and nightly runs useNIGHTLY_NEXTJS_REF. New plan tests cover accepted tags and refused refs (a branch, a SHA,16.4.3,v16.4.3-alpha.1,v16.4.3 --upload-pack=…, and others).persist-credentials: false: all three checkouts here already set it, so nothing changed.
Not done here, and why
- Pack and tarball-check steps as runnable, tested scripts (cubic P3, Greptile P2). The point of the
publishjob is that it runs nothing from the repository; moving its tarball check into a script in the checkout would put repository code into the one job that holds the OIDC token. So the steps stay inline.workflow-security.test.tschecks the jobs' shape (oneid-token: write, no install, cache or pnpm inpublish, noid-tokenor cache inbuild), and the scratch run described above exercised the steps end to end: a valid artifact, a changed tarball, an extra file, a tree naming another package, and a dry-run publish. A follow-up could run the same inline step bodies from a test, as the suite already runs theplanstep's body, without the job executing repository files. - Authenticating what
buildpacked (cubic P1). As "What this does not do" says,SHA256SUMSonly guards the hand-over between the two jobs; a dependency compromised insidebuildcan change both the tarballs and their digests. Checking the build itself needs a source the build cannot write, such as a second, independent build to compare against or reproducible tarballs checked elsewhere, which is a larger change than this PR. What this PR removes is that dependency's use of the OIDC token.
Actions stay pinned by full SHA. Checked locally: both workflows parse with yaml; the build and publish step scripts pass bash -n; ESLint and oxfmt on the changed files; workflow-security, provenance-security and population tests pass. actionlint and shellcheck are not installed here.
Summary by CodeRabbit
- Chores
- Deployment test runs now accept supported Next.js release tags and handle credentials during preflight and test execution, with cleanup after the run.
- Package releases are built and verified before publishing. Publishing checks package versions, skips versions already available, and includes provenance.
- Documentation
- Updated deployment testing guidance with release-tag requirements, examples, and token usage.