Release: Harden npm package release paths - #79859
Conversation
|
Size Change: +290 B (0%) Total Size: 7.67 MB 📦 View Changed
|
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
tyxla
left a comment
There was a problem hiding this comment.
Wow, there's a lot going on here 😅
Have we had a chance to test this on a fork or something?
| stdio: 'inherit', | ||
| } | ||
| log( | ||
| `>> Do not start a fresh release. After checking npm registry state, rerun the same release command from this checkout with --resume --repository-path ${ gitWorkingDirectoryPath }.` |
There was a problem hiding this comment.
For next releases, Lerna writes build metadata (+<sha>) into both package.json and the git tag, so version in getNpmReleasePackages will be ...-next.v.202607060000+abc123. On --resume that version is already published, but npm strips build metadata on publish, so npm view pkg@…+abc123 version --json returns the stripped ...202607060000. The check registryVersion !== version (and the identical dist-tag check) is therefore always true and will throw an error. --resume is suggested for npm-next, but it looks like it can never succeed in that scenario.
There was a problem hiding this comment.
Addressed with the help of an AI agent.
The comparison now happens against the npm registry version form by stripping SemVer build metadata from the local package version before checking the registry version and dist-tag. This keeps next recovery compatible with versions like 4.50.0-next.v.202607060000+sha, while still failing if the registry returns a different version or the expected dist-tag points elsewhere.
| - latest | ||
| - bugfix |
There was a problem hiding this comment.
getConfig('bugfix') and getConfig('latest') both resolve to npmReleaseBranch='wp/latest' / distTag='latest'. So a bugfix and a latest can run simultaneously since both check out wp/latest, both run lerna version patch --no-push and lerna publish from-package --dist-tag latest, and both push to origin/wp/latest, which may result in push failures or conflicting version bumps.
There was a problem hiding this comment.
I didn't think of that, it's true.
The workflow concurrency key is now scoped by "target npm release branch" instead of raw release type. latest and bugfix both serialize on wp/latest, development serializes on wp/next, and wp releases remain scoped by their WordPress version branch.
| return [ | ||
| 'Push and verify the release branch:', | ||
| `git push origin "${ publishCommit }:refs/heads/${ npmReleaseBranch }"`, | ||
| `git ls-remote --heads origin "${ npmReleaseBranch }"`, |
There was a problem hiding this comment.
git ls-remote --heads origin wp/latest matches by ref tail, so a sibling ref such as refs/heads/backport/wp/latest also matches and because it is earlier in sorting, is emitted first. If that happens verifyRemoteNpmReleaseBranch will compare the wrong commit and throw a false error after npm already published, failing the release. Should we match the exact refs/heads/<branch> ref instead of trusting the first line?
There was a problem hiding this comment.
Yeah, true.
I updated the branch verification to use the exact refs/heads/<branch> ref in git ls-remote, and the parser only accepts a line whose ref name exactly matches that branch. The printed recovery command uses the same exact ref form.
| } | ||
|
|
||
| const temporaryFolders = []; | ||
| if ( config.resume && ! config.gitWorkingDirectoryPath ) { |
There was a problem hiding this comment.
We're only validating the repository path, and nothing else. Is there a possibility that a prior run died earlier, so we're on the wrong HEAD? Should we add a check for that as well?
There was a problem hiding this comment.
Addressed.
publishVersionedPackagesToNpm() now fails before any npm work when --resume finds no release package tags at HEAD.
That keeps resume tied to an existing local version/tag state instead of allowing a plain checkout path to enter the recovery flow.
| try { | ||
| await runPhase( 'Release branch push', async () => { | ||
| log( '>> Pushing release branch to remote.' ); | ||
| await git.raw( |
There was a problem hiding this comment.
getTagPushCommands and getNpmReleaseGitRecoveryCommands build the printed recovery commands, while pushNpmReleaseGitMetadata builds the real push independently.
Should we unify and use the same underlying tooling?
There was a problem hiding this comment.
Yes, that's a good opportunity for unification.
Branch pushes and printed recovery commands now both use the same getBranchRefspec() helper, so the real push path and copy-paste recovery path stay aligned.
Yes, the PR grew in size progressively as I worked on it, and it received feedback. I'm actually going to break it down into 3 smaller PRs, for ease of review:
Not personally. Ideally we'd have some fork and / or sandboxed registry of sorts, but I'm not aware of any. Maybe @desrosj has better knowledge here? |
What?
Follow-up to the Gutenberg 23.5 npm package release failure.
This PR makes every workflow-dispatched npm package release path (
latest,development/next,bugfix, andwp) push Git metadata in explicit phases after npm publication succeeds.Why?
The previous Git push behavior coupled release branch updates with package tag publication. When that late push failed, npm had already been changed and the workflow did not print exact recovery commands. The same failure mode existed outside the original
npm-latestpath, so all npm release types now use the same hardened implementation.Note
This PR intentionally hardens the existing npm package release procedure. It keeps the current release conventions in place: packages are still published through Lerna, release metadata is still pushed back to the existing npm release branches, and per-package Git tags are still produced. Bigger process decisions, such as whether those package tags are still needed, are follow-ups.
How?
--no-pushforlatest,next,bugfix, and CLI-drivenwpreleases.lerna publish from-packageafter preflight checks.--resumemode for partial-publish recovery from an existing local version commit and package tags.--resumeruns to have local package version tags atHEADbefore doing npm work.nextreleases.npm whoamias the hard credential gate and keepnpm access list packagesas a warning-only diagnostic.refs/heads/...refs.wpdispatch throughrelease-cli npm-wpinstead of inline raw Lerna.latestandbugfixserialize onwp/latest.TODO / Follow-ups
Testing Instructions
npm run test:unit -- tools/release/commands/test/packages.jsnpm run lint:js -- tools/release/cli.js tools/release/commands/packages.js tools/release/commands/test/packages.jslatest,development/next,bugfix, andwprelease paths all use locallerna version ... --no-push,lerna publish from-package, and the shared metadata push/recovery flow.--resumeskips versioning, requires local package tags atHEAD, and allows already-published expected versions only when the expected dist-tag points at that version.npm whoamisucceeds, and failed publish retries print resume guidance.Testing Instructions for Keyboard
N/A
Screenshots or screencast
N/A
Use of AI Tools
This PR was prepared with assistance from OpenAI Codex.