🛡️ Hold every publishable manifest to one version before the tag - #845
Merged
Merged
Conversation
v0.13.0 published five binaries and not one package. The release branch had been bumped before `packages/git` existed and merged after it, so git stayed at 0.12.1 while every sibling moved to 0.13.0 — and the two tag-time gates disagreed about that. `publish-packages.yml` reads all eleven manifests and refused. `release.yml` read `packages/cli/deno.json` alone, matched, and published the irreversible half. Both gates now read the same set, and a third reads it before the tag exists. `scripts/lib/version-lockstep.ts` walks the workspace and reports every manifest that declares a different version from its siblings, and every `bun.lock` workspace entry gone stale or missing; its suite runs it against this repository, which is where the answer has to be true. `release.yml`'s preflight walks `packages/*` instead of naming one manifest, so a package added later joins the gate by existing. `packages/git` reaches 0.13.0 here, with the `bun.lock` entry it never had, so the workspace the new check guards is one it passes.
Review found the correction incomplete: the new check and the widened preflight read membership from `package.json`'s name, while the publish generator, the npm builder and `bumpManifests` read it from `deno.json`'s. A member the two manifests name differently therefore publishes packages while both gates ignore it — the same partial release the PR set out to prevent, reached another way. `scripts/lib/publishable-members.ts` now holds that rule once: identity from `deno.json`'s name, exclusion from `package.json`'s `private`, both manifests required. The lockstep check, `release.yml`'s preflight and the two membership assertions in the workflow suite all read it. The repository's own manifests agree about every name, so nothing in the tree can tell the two rules apart. `publishable-membership-agreement.test.ts` builds workspaces where they disagree on purpose and runs all three selectors over them — `publishableMembers`, the preflight's own shell lifted out of `release.yml`, and the real generator document over the fixture, since an eval block's selection rule cannot be imported (#237). Reverting either selector to `package.json` makes it fail.
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.
Why
v0.13.0published five binaries and zero packages. The 0.13.0 release branchwas bumped before #831 added
packages/gitand merged after it, so git stayedat
0.12.1while every sibling reached0.13.0.The two tag-time gates then disagreed about that.
publish-packages.ymlreadsall eleven manifests, found git, and refused.
release.ymlreadspackages/cli/deno.jsonalone, matched the tag, and published the half thatcannot be taken back. Both gates also run after the tag is pushed, which is
after the drift has already been merged.
What changes
Before:
release.ymlvalidates the tag againstpackages/cli/deno.json.packages/gitdeclares0.12.1;bun.lockhas no entry for it at all.After:
release.ymlvalidates the tag against every publishable manifest, walkedfrom
packages/*.scripts/lib/version-lockstep.tsanswers the same question on every pullrequest, against this repository.
packages/gitdeclares0.13.0andbun.lockrecords it.How it works
Membership comes from the workspace, never a list.
publishableMembersreuseslistWorkspacePaths(scripts/lib/workspace.ts) — the same walkbumpManifestsuses — so what the bump stamps and what the check reads cannot come apart.
Identity and exclusion both come from
package.json, because a private memberomits
deno.json'snameandexports, which leavespackage.jsonas theonly manifest every member fills in.
release.yml's preflight performs the same walk in shell rather than importingthe module: it runs before
deno task deps, on a checkout with nothinginstalled.
Review guide
Start with:
scripts/lib/version-lockstep.tsThen review:
versionLockstepFindings— the invariant, as the messages it emits.github/workflows/release.yml— the same walk, in shell, at tag timescripts/lib/bun-lockfile.ts— whybun.lockneeds a parse of its ownspecs/release-process-spec.md§2 and §3Look carefully at:
behind-the-times member does not also report eleven lockfile mismatches.
What must stay true
@executablemdmember declares one version across both itsmanifests — enforced by
versionLockstepFindings, checked byscripts/tests/version-lockstep.test.ts.packages/*inrelease.yml, checked by thetag-time version gatessuite inscripts/tests/publish-workflow-membership.test.ts.How to verify it
holds this workspace in lockstepproves the real tree is consistent andfails if any member drifts. Reverting
packages/gitto0.12.1makes itreport
the workspace declares more than one version: 0.12.1 (packages/git/…)— the exact v0.13.0 defect.
reports a lockfile entry left at the previous releasefails ifbun.lockrestamping is skipped; setting git's entry back to
0.12.1reportsbun.lock records packages/git at 0.12.1, not 0.13.0.reaches the same set by walking the workspace before publishing binariesfails if
release.ymlnarrows again; replacing the glob withpackages/cli/package.jsonmakes it fail.keeps a comma that closes nothing because it is inside a stringfails ifthe lockfile parse stops being string-aware.
TAG=v0.13.0exits 0;TAG=v0.14.0exits 1; and with git back at0.12.1,TAG=v0.13.0exits 1naming both of git's manifests.
Run:
deno test --allow-all --frozen scripts/tests/version-lockstep.test.ts scripts/tests/publish-workflow-membership.test.ts— 5 passed (21 steps).Also green under Node (
tsx --test, 11 pass) and Bun (bun test, 11 pass),plus
deno task lint,deno task check, anddeno task gen:publish-workflowleaving no diff.
Scope
Included
release.yml's preflight widened to every publishable manifest.packages/gitbrought to0.13.0, with itsbun.lockentry — the checkguards a workspace it passes.
Intentionally unchanged
publish-packages.ymlkeeps its generated, explicit manifest list. It isalready correct, CI already fails when it goes stale against the manifests,
and the new suite holds it to the full member set.
clion 0.12.0 and 0.12.1 is Build tagged npm packages without waiting for sibling registry propagation #843, notthis PR.
@executablemd/clistays at 0.11.0 on npm until that lands or thejob is re-run per spec §7.
New abstractions
publishableMembers/versionLockstepFindings(scripts/lib/version-lockstep.ts)exist because three places need one answer to "who is in the release, and at
what version": the new suite, and the prose in §2 that both tag gates
implement.
parseBunLockfile(scripts/lib/bun-lockfile.ts) exists becausebun.lockis the only file here that
JSON.parserefuses, and nothing else in therepository reads it.
Generated or mechanical changes
packages/git/{deno.json,package.json}: version field only.bun.lock: four insertions recordingpackages/git— the rootdevDependency, the cli dependency, the member entry, and the resolution line.
Hand-written in the shape the 0.13.0 release commit used, not regenerated.
Risks and limitations
bun.lockis stale well beyondpackages/git.bun install --lockfile-onlyalso wants to addsemver,@types/semver,@agentclientprotocol/sdkto acp, the whole dependency block forcode-review-agent, cli's devDependencies and test-support's, and to move
@effectionx/nodefrom0.2.4to the0.2.5the rootpackage.jsonpins.I did not take that: it is a dependency change, which AGENTS.md makes an
explicit act, and it does not belong in a gates PR. The new check only reads
workspace member versions, which is all it claims — it would not have
caught any of the above, and does not now. Worth its own issue.
merge result. On an ordinary PR it proves the branch, not the merge — so
"Require branches to be up to date before merging" (or a merge queue) is what
closes the v0.13.0 case exactly. Without it the drift is still caught on
main, one commit later, and still long before a tag.Scope confirmation