refactor(release)!: one immutable packaged commit per main sha; latest and vN move only forward; the build chain retires - #203
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Invalid hand-created refs can bypass package validation or distort pruning, and the bootstrap documentation generates the wrong version shape.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Replaces the packaged build chain with immutable per-main-commit packages and forward-only consumer pointers.
Changes:
- Adds
build/<position>.<sha7>tags, retention pruning, and lease-based pointer movement. - Updates release workflows, rulesets, migration guidance, and package documentation.
- Reworks fixture and workflow tests for the new topology and concurrency model.
File summaries
| File | Description |
|---|---|
.github/scripts/release-pipeline.ts |
Implements packaging, pruning, pointer movement, and ancestry-based versions. |
.github/workflows/post-green.yml |
Packages each green main commit. |
.github/workflows/update-release.yml |
Uses packaged commits throughout releases. |
.github/settings.local.yml |
Protects build tags and freezes the build branch. |
AGENTS.md |
Updates repository release invariants. |
docs/reference/library.md |
Documents package topology and versioning. |
docs/upgrading/v2-to-v3.md |
Adds the build-branch migration. |
test/scripts/release-pipeline-fixture.ts |
Adds package-tag and race fixtures. |
test/scripts/release-pipeline-build.test.ts |
Tests packaging, pruning, and pointer races. |
test/scripts/release-pipeline.test.ts |
Tests release and npm version behavior. |
test/docs/post-green-workflow.test.ts |
Updates workflow contract assertions. |
test/docs/npm-publish-workflows.test.ts |
Updates pre-release version expectations. |
Review details
- Files reviewed: 12/12 changed files
- Comments generated: 4
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
2b2c52f to
2ae732b
Compare
There was a problem hiding this comment.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
Note
This error may be related to your runner configuration. You can now configure runners for Copilot code review separately from Copilot cloud agent by creating a copilot-code-review.yml file with your setup steps. Read the docs for details.
2ae732b to
8edee26
Compare
File size check0 over a hard cap (fails), 10 warning(s).
Split the file, wrap the line, shorten or exempt the comment, or list the path in 4 managed file(s) skipped; repo-platform owns them. |
There was a problem hiding this comment.
🟡 Changes recommended
Build-tag pruning can delete a genuine retained tag when a forged but well-shaped tag sorts ahead of it.
Get a fresh assessment by requesting another Copilot review.
Review details
- Files reviewed: 16/16 changed files
- Comments generated: 2
- Review effort level: Balanced
8edee26 to
b04cbe6
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The retained no-bypass build ruleset prevents the documented build-branch deletion.
Get a fresh assessment by requesting another Copilot review.
Review details
- Files reviewed: 16/16 changed files
- Comments generated: 3
- Review effort level: Balanced
b04cbe6 to
b1a90cf
Compare
…t and vN move only forward; the build chain retires Every green main commit gets its own packaged commit, parented on that commit and tagged build/<first-parent position>.<sha7>: minted once with a plain push, verified on a rerun by parent and tree identity, never appended to a chain. Only the ten newest build tags are kept; release tags and latest keep their commits reachable. latest and vN move through one function, movePointer: forward along main under a compare-and-set on the value origin advertised, never back or sideways; a pointer whose commit is no package of a main commit (the frozen chain's tip, a hand push) is replaced with a report. Push failures are classified by re-reading origin, never by parsing git's stderr. The build branch is frozen at its last chain commit (its ruleset gains update); a build-tags ruleset blocks update and non_fast_forward on build/* and leaves deletion to the prune. Workflows stay in the packaged tree, since a child of the main commit shows no workflow change to GitHub's token check, so the root commit, the workflow strip, and the Source trailer go. The three COMPAT(v3) paths (the bundle-only chain parent arm, its test, the attestation.jsonl asset branch) are deleted. BREAKING CHANGE: the build branch no longer advances; a sha pinned from a build/* tag lives until ten newer commits are packaged; retag-major on a rerun of an older release leaves a newer release's major in place instead of failing.
One create-once loop (ensureTag) behind the build tag and the version tag; one package check (assertPackageOf) by parent, tree identity against a rebuild, and the required files; one mover (movePointer) with one kept path and no reason-string plumbing; the prune lists, validates the name shape, and deletes in one function. verifyPublishedRefs and the verify subcommand go: retagMajor already judges the tag, and its major-equals-package rule failed a stale rerun that retagMajor correctly leaves alone. The stderr classification, the hand-made-tag name check against main, the unreachable off-main arm, the replacement warning, the date guard, releaseOrder, and the comments that restated the code go with them. Tests assert the pointer's sha and whether it moved, plus origin's state, not reason strings; the fixture keeps one planted-case list and one push-plan runner. The npm-confirm hold grows to 15 reads (three of the first five publishes were unreadable after 80 s); the bootstrap recipe and its count-0 marker are history (the hand-published version is unpublished); the build branch's ruleset is removed so the owner can delete the branch once every consumer has repinned.
…d literally; docs: tags after v2.0.0 CodeQL flagged the two-character escape as incomplete; one helper escapes every metacharacter. The upgrade index said v2.0.0's tag sits both on a packaged commit and on main.
Two lines over the fleet's width cap split; one fixture helper asserts the packaged-commit contract both test files repeated; the unknown-source arm's comment says what the arm does today instead of planning its deletion.
…h can be deleted; a hand-minted package names no run The apply leaves an undeclared ruleset alone, so dropping the build entry would have kept its deletion rule live under the owner's delete. The two docs said every packaged commit names its workflow run; the documented hand recovery mints one without a run trailer.
There was a problem hiding this comment.
🟡 Changes recommended
The security documentation omits the intentional package manifest rewrite from its package integrity contract.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
.github/SECURITY.md:21
- The release topology description incorrectly says the source tree is carried unchanged.
For example, a source containing scripts.prepare produces a packaged child without that key. Name the manifest rewrite here so this verification section matches the package contract.
- The `vX.Y.Z` tags, the moving major, and `latest` point at packaged commits: each the child of its source commit on `main`, carrying that tree plus the bundle and library built from it, by the workflow run its message names when CI minted it (a package minted by hand in the release recovery names none; see below). `main` carries no executable bundle.
- Files reviewed: 16/16 changed files
- Comments generated: 1
- Review effort level: Balanced
…tion, referred to by title where it is cited
b1a90cf to
a1a6a01
Compare
…package.json's preparation scripts
There was a problem hiding this comment.
🟡 Changes recommended
A create-and-prune race can fail valid packaging runs, and several package-shape documentation statements remain inaccurate.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
.github/SECURITY.md:14
- This vulnerability definition omits the manifest rewrite performed for every packaged commit. For a source containing
scripts.prepare,packagedTreeOf()removes that key, so a valid package does not have exactly the source tree plus the two builds.
- Supply chain. A packaged commit whose bundle a rebuild of its parent (its source commit) does not reproduce, or whose tree is not that source's plus the bundle and library build, is a vulnerability. The next section says what each ref points at and how to verify it.
- Files reviewed: 16/16 changed files
- Comments generated: 3
- Review effort level: Balanced
… library.md names every file build:lib writes A rival that created the tag and pruned it between this run's push and its re-read left the ref absent, and the run threw as if the push were refused for good. Without reading git's words the two cases look alike, so ensureTag retries through its attempts and throws the last refusal only when they are spent.
There was a problem hiding this comment.
🔵 Needs a closer look
Pinning documentation still presents all commit SHAs as permanent despite pruning build-tag SHAs.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
README.md:59
- The version table still promises that any commit-SHA pin is frozen, but a SHA copied from one of the new
build/*tags can stop resolving after ten newer packages are minted. For example,uses: Vivswan/github-settings-as-code@<build-tag-sha>can break after pruning despite this page advertising "Byte-stable behavior." Restrict the durable SHA claim to commits held by release tags and name the temporary build-tag case.
docs/upgrading/README.md:17 - The generic commit-SHA row now conflicts with the new retention policy. A SHA obtained from
build/<position>.<sha7>may stop resolving when that tag is pruned, while this table still tells digest-pinning users that any commit SHA lasts forever. Limit this row to a release tag's commit SHA.
- Files reviewed: 16/16 changed files
- Comments generated: 0 new
- Review effort level: Balanced
What changed: every green main commit gets its own packaged commit under a create-once
build/<position>.<sha7>tag, andlatestandvNmove only forward along main through one function; the build chain, its backfills, and the stderr classification of pushes are gone.release-tags,major-release-tags, andlatest-tagblocks keep their rules with reworded comments; the owner deletes the branch once the first build tags exist,latestnames a tagged commit, and every consumer of a chain sha has repinned (the build-branch break indocs/upgrading/v2-to-v3.md, its last section).latestandvNmove only forward. One mover,--force-with-leaseon the value origin advertised, main's head read AFTER the pointer (the design change a codex round forced: read before, a rival's newer value looked like a hand push andlatestmoved back); a rerun of an older commit or release leaves the newer value where it is.verifyremoved. Its checks repeatedretag-major's, and its major-equals-package rule failed a stale rerun the mover correctly leaves alone; the verify job keeps the asset check alone.COMPAT(v3)paths deleted;check:compatreports an empty list. The npm bootstrap recipe is history (the hand-published version is unpublished).Proof:
bun run checkgreen; 141 pipeline tests over the fixture's bare origin (rerun no-op, A-then-B equals B-then-A, stale lease retried, no move to a non-descendant, prune and concurrent prune, re-mint after prune, the stale-head and observe-then-fetch races, planted tags refused); codex converged over eight rounds.Size: script -444 lines (1755 -> 1311; 1660 before #197); tests -509 net: the two test files -694, the fixture +185 because the planted-case list and the packaged-commit contract both test files assert now live once there; docs +3; workflows and settings -12.
Technical details
Accounting (against main 5dfc10a):
release-pipeline.ts)release-pipeline.ts: 1755 (main) -> 1311 lines (1660 before #197). Fixture 483 -> 627 (+144); the three test files 3835 -> 3365.Per-function table (lines including the docblock; the invariant each enforces):
ensureTagensurePackagedassertPackageOfassertPackages56 +assertSameBuild24 +verifyPackagedTag33packagedTreeOftreePlusBundle38 +strippedManifestBlob21builtTreepackagedTree31 +builtPaths29assertCarriesmovePointerpublishLatest53 +retagMajor's lease 38 +observeMajorLine22 +isNewer10pruneBuildTagsfetchObservedobserveRemotepushpushUnlessOvertaken18 +OVERTAKEN_PUSH18 +LOCK_LOST10packageCommitadvanceBuild25 +advanceChain56packageReleaseretagMajordescendantsOfplaceAgainst18 +placePublished36 +furthest21 +Placement8gitOrNogitYesNo13 +resolveCommit18newerReleasereleaseOrder15verifyPublishedRefs40,readBuildTip16,chainPackaging13,validateTip27,appendChain18,commitChain25,chainCommitOfNewestSource17,assertOnChain15,workflowPaths8,isPackagedPath7,packagedEntries16, the chain and verify types 49retagMajoralready makesanchorReleasePr,boundaryCheck,anchorCheck, the version and npm section,main(111)Fixture (483 -> 668):
PLANTED_PACKAGES(80, one list of hand-planted commits both test files run) andexpectPackage(37, the packaged-commit contract both test files assert) replaces therefusedlist (200+) andplantTag/wrongSource(60) that lived in the test files;plantCommit/plantCommitIn(47) replacerivalChainCommit,stripWorkflows, and the two in-test planters;subcommand(24) moved out of the pipeline test;buildTagOf/buildTags/packagedOf/positionOf/originHolds(34) replacebuildTip/sourceTrailer/appendedSha;shallowClone(9) replacesshallowChecker(21); the fixture's own six-script list keeps a dropped preparation trigger visible in the packaged manifest.Invariants (how each is enforced):
build/*ruleset (update, non_fast_forward, no bypass) +ensureTag; deletion only by the pruneassertPackageOf: parent edge, tree identity against a rebuild, required files, the fresh build's tree on a rerunmovePointer: descendant rule,--force-with-lease, head read after the pointer, kept value verifiedlatest,vNnever vanish;vX.Y.Znever movesnextandlatestnever regressensureTagverifies;movePointerleaves a value at or past the candidateDeletion pass, itemized:
verifyPublishedRefs+verify(1); one create-once loop with a lazy commit supplier (2);replaced/warn/reportMoveand the replacement message (3);movePointer's duplicate no-op paths and the unreachable off-main arm (4);fetchObservedboolean (5); the per-tag main check in the prune (6, accepted loss: a hand-madebuild/999999.xname; the name shape is still checked); the path-listing diagnostic,assertChildOf's ternary,assertCarriesfixed to the required files, the duplicate existence loop (7);commitPackaged/packagedEntries/buildTagRefinlined, types un-exported (8);pushRefusedfolded, the prune's plain push,reasonassembly gone (9); the pointer-result constructors,shouldStripManifest,plantEmptySubtree,withRemotePlans, thePREPARATION_SCRIPTSliteral test (10); onedescendantsOf(14);noticesoff the stable channel (15);releaseOrder->newerRelease(16); the date guard (17); nine restating comments (18).Doc edits beyond the pipeline docs:
CONTRIBUTING.md:38,README.md:59,docs/upgrading/README.md:17,.github/SECURITY.md:14,:15(no hand-published exception),:21,docs/start/getting-started.md:70;docs/reference/library.md(bootstrap recipe replaced by the trusted-publisher facts;latestsits on anextpre-release until the first stable release; the 15-read hold);docs/upgrading/v2-to-v3.md's build-branch break (the branch retires; the migration-complete condition; what to repin).Codex rounds: eight, converged. Found and fixed: the stale-main-head backward move; two observe-then-fetch crashes; the frozen branch's missing
updaterule (moot now that the ruleset goes); the same-package re-mint deadlock; the tag list judged against an older head (moot now that the check is deleted); the verify job losingGH_REPOwith its checkout; a fixture that derived its inputs from the list under test. Copilot's rounds found the hand-pushed bare pointer (built), the hand-made tag name (built, then deleted on the owner's line-count rule; recorded as an accepted loss), the upgrade index's v2.0.0 wording (fixed), the retired ruleset that would have blocked the branch delete (declared disabled), the run-trailer and manifest-strip wording in two docs (fixed), and a refused create whose ref reads absent after a rival's create-and-prune (now retried through the attempts); CodeQL flagged the two-character regex escape in a test (fixed with one helper).Rebase: stacked on #197 at df99c60, replayed onto 131780e, 033a9c6, f1f938e, 798fc42, 3bf8574, d92b738, 69fc7c4, 05e6d41, and 5dfc10a as main moved;
git range-diffagainst the previously pushed head 8edee26 shows the design commit changed only by the library.md replay over #205, the deletion pass unchanged, and a third commit for the CodeQL escape and the upgrade-index wording.First run after landing:
latestleaves the chain tip (logged, no warning);build/<pos>.<sha7>appears. Then the lead asks repo-platform to include this repo in its next sync so thebuild-tagsruleset lands and thebuildruleset goes.Recorded, not built: a branded verified-package type so
movePointercallers cannot pass an arbitrary commit/source pair; a real-git race of two prune pushes past their advertisements.Reviewer note (Copilot):
movePointerdeliberately tolerates another commit of the same package (same source, same tree): a re-mint after a prune is the same package. A different tree of the same source throws by design.BEGIN_COMMIT_OVERRIDE
refactor(release)!: one immutable packaged commit per main sha; latest and vN move only forward; the build chain retires
BREAKING CHANGE: the build branch gets nothing new and is deleted once every consumer has repinned; packaged commits live under build/. tags, the ten newest kept, so a sha pinned from one lives until ten newer commits are packaged.
A rerun of an older release's retag-major leaves a newer release's major in place instead of failing; the verify subcommand is gone.
END_COMMIT_OVERRIDE