Repository navigation
ci(release): ship v1.x versions through a release PR - #1579
Merged
Merged
Conversation
v1.x now requires pull requests, so the land job's direct ref update to the release line fails with a 422 after the packages are already staged. The workflow gains a mode input. mode=release-pr derives the bump in a job with no write credential and passes it as a two-file patch to a job that installs nothing, which commits it via the release App and opens a chore(release) PR into the dispatch branch. mode=publish builds the newest first-parent commit that changed the package.json version, so it only ships what a merged release PR introduced, then tags, releases and stages as before. Nothing in the workflow writes to the release line.
RATTIPONG (BankPansuwan)
approved these changes
Oct 6, 2026
Martin Torp (mtorp)
requested a review
from Jeppe Fredsgaard Blaabjerg (jfblaa)
October 6, 2026 15:08
Jeppe Fredsgaard Blaabjerg (jfblaa)
approved these changes
Oct 6, 2026
Jeppe Fredsgaard Blaabjerg (jfblaa)
left a comment
Contributor
There was a problem hiding this comment.
[agent] Approving. The split into derive (installs, no write credential) and release-pr (App token, installs nothing, two-file patch guard) is a real improvement over verify holding contents:write next to pnpm install. I also like publishing the newest version-changing commit with release:preflight as the backstop. None of the points below block the merge.
Worth addressing before merge
- Release PR goes stale if v1.x moves before the merge. A squash merge of an out-of-date release PR ships everything that landed on v1.x after the PR was opened. Those commits never went through
bump.mts, so afeat:could ship inside a patch release, and commits without changelog entries won't show up in the version's section. The PR body tells the reviewer to re-dispatch, but nothing enforces it. Cheapest fix: require branches to be up to date before merging on v1.x, or add a check inverifythat the release commit's parent is the commit the bump was derived from (the parent of the App commit onnpm-publish-vX.Y.Z). - A re-run probably opens a new PR rather than refreshing the old one.
openReleaseBranchforce-resets an existingnpm-publish-vX.Y.ZtoparentShabefore the new commit is written. For a moment the head has no commits beyond base, and I believe GitHub auto-closes an open PR in that state.upsertPullRequestthen finds no open PR (state=open) and creates a new one. That still works, but it doesn't match "refreshes the same PR" in the description and docs, and review comments end up on a closed PR. I haven't confirmed this on a live repo. Either create the new commit on the branch directly with the reset as the fallback, or update the wording. - Superseded release PRs stay open. If a
feat:lands and the re-dispatch derives a different version, a newnpm-publish-v1.6.0PR opens and the oldnpm-publish-v1.5.1PR stays open and mergeable. Consider closing other opennpm-publish-v*PRs into the release line inopen-release-pr.mts, or at least mention it in the PR body.
Nits
mode: publishwithdry-run: truepacks HEAD, while the real run packs the located release commit. A dry run would be a more faithful preview if it also ran the locate step and logged which commit and version it would ship (it can still pack HEAD if you prefer).publishonly stays out ofrelease-prmode becauseverifyis skipped and implicitsuccess()propagates that. Addinginputs.mode == 'publish' &&topublish.ifmakes this explicit and avoids surprises if someone addsalways()later.- In "Check out the release commit",
git show "$COMMIT^:package.json"fails underset -ewith an unclear git error if the walk reaches the commit that created package.json. That's unlikely within 500 commits, so it's cosmetic.
A re-run used to reset npm-publish-vX.Y.Z to the base before writing the new commit. For a moment the head had no diff, which GitHub treats as a reason to close the open PR. The branch is now left alone and the new commit is force-moved onto it in one step. The failure cleanup only deletes a branch this run created. A re-dispatch that derives a different version used to leave the old npm-publish-v* PR open and mergeable. open-release-pr now closes those and deletes their branches. The publish job also names its mode in its own condition, and the release-commit walk no longer dies on the commit that created package.json.
Contributor
Author
|
Thanks for the review. Here is what I did with each point. Fixes are in 10bcf25.
Workflow and release tests pass and |
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
v1.x now requires pull requests. The publish workflow's last job tried to move v1.x straight to the bump commit with a ref update, and GitHub now rejects that with "Changes must be made through a pull request." By that point the tag, GitHub release and npm stages already existed, so run 37430526096 shipped 1.5.0 but left v1.x on 1.4.2. The 1.4.2 run failed the same way and was landed by hand in #1577.
This PR changes the flow so the workflow never writes to v1.x. A version bump now reaches the branch through a normal reviewed PR.
New flow
v1.xwithmode: release-pr. Withdry-run: true(the default) it only prints the next version. Withdry-run: falseit opens achore(release): X.Y.ZPR fromnpm-publish-vX.Y.Zintov1.x. Re-running it moves the same branch to a fresh commit, so the open PR is refreshed in place. If the re-run derives a different version, the oldernpm-publish-v*release PR is closed and its branch deleted.mode: publishanddry-run: false. It checks out the newest commit that changed thepackage.jsonversion, which is the merged release PR, and builds, tags, releases and stages it the same way as before. Commits merged after the release PR wait for the next release. An already-published version is still refused byrelease:preflight.pnpm stage approve, same as today.Credential boundaries
deriverunspnpm installandbump.mtsand holds no write credential. It hands the bump on as a patch.release-prinstalls nothing. It applies the patch, refuses anything that touches files other thanCHANGELOG.mdandpackage.json, mints the release App token, commits through the API (so the commit is signed), and opens the PR.verifyandpublishno longer touch the App token at all. Before,verifyheld a contents:write token in the same job aspnpm install.The npm trusted publisher bindings don't change. They still point at
publish-npm.ymlplus thepublish-npmenvironment.Before the first run
mint-app-token.mjschecks the grant before it mints a token, so if that's missing the run stops early with a link to the settings page.secrets-outside-env). As a follow-up, consider moving it into an environment that only allowsv1.x.Tests
test/release-workflow.test.mtsnow checks that only therelease-prjob mints the App token, that no job writes to the release line, and how each mode routes to its jobs.test/release-open-pr.test.mtscovers the two-file patch guard and the create-or-refresh PR logic.pnpm run check:tscpasses. Lint is clean apart from a formatting issue inscripts/ci/setup-node.mjsthat is already on v1.x.A separate PR will clean up the two broken releases (1.4.2 and 1.5.0).
Expected reviewer effort: Full code review
Note
Medium Risk
Changes the production v1.x release path and GitHub App permissions; mistakes could block releases or tag the wrong commit, though dry-run defaults and workflow contract tests reduce exposure.
Overview
v1.x npm releases now go through a reviewed release PR instead of the workflow fast-forwarding
v1.xafter publish (which broke on branch protection).The Publish to npm registry workflow adds a
modeinput:release-prderives the next version withbump.mts, exports a two-file patch from a newderivejob, andrelease-prapplies it withoutpnpm install, mints the release App token (now withpull_requests: write), commits viaopen-release-pr.mts, and opens/refreshes achore(release): X.Y.ZPR fromnpm-publish-vX.Y.Z.publishmode keeps verify/publish butverifyno longer bumps in-run; on real publishes it checks out the newest first-parent commit that changedpackage.jsonversion (the merged release PR). Thelandjob andpromote.mtsare removed.bump.mtsonly rewrites the working tree; GitHub writes moved togithub-api.mts(upsertPullRequest) andrelease-branch.mts(PR instead of fast-forward). Docs inCLAUDE.mdand workflow comments match the two-step flow. Tests assert credential boundaries and the new PR path.Reviewed by Cursor Bugbot for commit fce54f6. Configure here.