Skip to content

Release: Harden npm package release paths - #79859

Closed
ciampo wants to merge 9 commits into
trunkfrom
codex/harden-npm-release-push
Closed

Release: Harden npm package release paths#79859
ciampo wants to merge 9 commits into
trunkfrom
codex/harden-npm-release-push

Conversation

@ciampo

@ciampo ciampo commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

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, and wp) 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-latest path, 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?

  • Create Lerna version commits and package tags locally with --no-push for latest, next, bugfix, and CLI-driven wp releases.
  • Publish npm packages with lerna publish from-package after preflight checks.
  • Add an explicit --resume mode for partial-publish recovery from an existing local version commit and package tags.
  • Require --resume runs to have local package version tags at HEAD before doing npm work.
  • Keep fresh-publish preflight strict: target npm versions must be absent.
  • Let resume preflight continue when expected versions already exist, but fail on mismatched versions or wrong dist-tags.
  • Compare npm registry versions without SemVer build metadata, matching npm's published version behavior for next releases.
  • Treat npm whoami as the hard credential gate and keep npm access list packages as a warning-only diagnostic.
  • Preserve local version metadata during retry/recovery instead of resetting it away after a partial publish.
  • Print explicit resume guidance when npm publication still fails after retry, so releasers do not start a fresh release after possible partial publication.
  • Push and verify the npm release branch by expected publish commit SHA using exact refs/heads/... refs.
  • Push only the package tags created for the current release using fully qualified tag refspecs.
  • Verify remote package tags peel to the publish commit.
  • Print copy-pasteable branch/tag recovery commands when npm publication succeeds but Git metadata publication fails.
  • Route the workflow wp dispatch through release-cli npm-wp instead of inline raw Lerna.
  • Scope package release workflow concurrency by target npm release branch, so latest and bugfix serialize on wp/latest.

TODO / Follow-ups

  • The npm package-version preflight intentionally checks target versions sequentially in this PR. A bounded-concurrency pass could reduce release latency for full releases, but it is deferred so the first hardening change keeps npm registry errors ordered and easy to diagnose.

Testing Instructions

  1. npm run test:unit -- tools/release/commands/test/packages.js
  2. npm run lint:js -- tools/release/cli.js tools/release/commands/packages.js tools/release/commands/test/packages.js
  3. Confirm the latest, development/next, bugfix, and wp release paths all use local lerna version ... --no-push, lerna publish from-package, and the shared metadata push/recovery flow.
  4. Confirm --resume skips versioning, requires local package tags at HEAD, and allows already-published expected versions only when the expected dist-tag points at that version.
  5. Confirm npm access-list failures warn but do not abort after npm whoami succeeds, 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.

@ciampo
ciampo requested a review from Copilot July 3, 2026 21:42
@ciampo ciampo self-assigned this Jul 3, 2026

This comment was marked as resolved.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

Size Change: +290 B (0%)

Total Size: 7.67 MB

📦 View Changed
Filename Size Change
build/scripts/block-library/index.min.js 332 kB +290 B (+0.09%)

compressed-size-action

@ciampo ciampo added [Type] Enhancement A suggestion for improvement. Gutenberg Plugin Issues or PRs related to Gutenberg Plugin management related efforts labels Jul 3, 2026
@ciampo ciampo changed the title Release: Harden npm metadata publishing Release: Harden npm package release paths Jul 3, 2026

This comment was marked as resolved.

Comment thread tools/release/commands/packages.js Outdated
Comment thread tools/release/commands/packages.js Outdated
@ciampo
ciampo requested a review from ramonjd July 6, 2026 06:24
@ciampo
ciampo marked this pull request as ready for review July 6, 2026 06:45
@ciampo
ciampo requested review from Mamaduka and t-hamano July 6, 2026 06:46
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

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 props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: ciampo <mciampini@git.wordpress.org>
Co-authored-by: ramonjd <ramonopoly@git.wordpress.org>
Co-authored-by: tyxla <tyxla@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@tyxla tyxla left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 }.`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 13 to 14
- latest
- bugfix

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread tools/release/commands/packages.js Outdated
return [
'Push and verify the release branch:',
`git push origin "${ publishCommit }:refs/heads/${ npmReleaseBranch }"`,
`git ls-remote --heads origin "${ npmReleaseBranch }"`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getTagPushCommands and getNpmReleaseGitRecoveryCommands build the printed recovery commands, while pushNpmReleaseGitMetadata builds the real push independently.

Should we unify and use the same underlying tooling?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@bgrgicak bgrgicak mentioned this pull request Jul 6, 2026
46 tasks
@ciampo

ciampo commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Wow, there's a lot going on here 😅

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:

Have we had a chance to test this on a fork or something?

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?

@ciampo ciampo closed this Jul 6, 2026
@t-hamano
t-hamano deleted the codex/harden-npm-release-push branch July 7, 2026 11:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Gutenberg Plugin Issues or PRs related to Gutenberg Plugin management related efforts [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants