Skip to content

refactor(release)!: one immutable packaged commit per main sha; latest and vN move only forward; the build chain retires - #203

Merged
Vivswan merged 8 commits into
mainfrom
refactor/per-commit-build-tags
Sep 13, 2026
Merged

Vivswan merged 8 commits into
mainfrom
refactor/per-commit-build-tags

Conversation

@Vivswan

@Vivswan Vivswan commented Sep 13, 2026 •

Copy link
Copy Markdown
Owner
before:  main ... 445 -> 446 -> 447        build: root -> c1 -> ... -> c59 (one chain, Source: trailers)   latest -> c59, walked from the chain
after:   main ... -> A -> B -> C            A' B' C' = each commit's child + lib/index.js + lib/pkg/          build/445.<a7> build/446.<b7> build/447.<c7>
                                             latest -> C'   vN -> the release's packaged commit   vX.Y.Z frozen   build branch: retired, deleted after migration

What changed: every green main commit gets its own packaged commit under a create-once build/<position>.<sha7> tag, and latest and vN move only forward along main through one function; the build chain, its backfills, and the stderr classification of pushes are gone.

  • Build branch deleted after migration. Nothing lands on it; its ruleset is declared disabled, not dropped (the apply leaves an undeclared ruleset alone, so the live deletion rule would block the owner's delete), and the release-tags, major-release-tags, and latest-tag blocks keep their rules with reworded comments; the owner deletes the branch once the first build tags exist, latest names a tagged commit, and every consumer of a chain sha has repinned (the build-branch break in docs/upgrading/v2-to-v3.md, its last section).
  • latest and vN move only forward. One mover, --force-with-lease on 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 and latest moved back); a rerun of an older commit or release leaves the newer value where it is.
  • Ten build tags kept. Each green push and each release prunes the rest in one push; release tags keep their commits.
  • verify removed. Its checks repeated retag-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.
  • npm-confirm hold lengthened. 15 reads 20 s apart: three of the first five publishes were still unreadable after 80 s.
  • Three COMPAT(v3) paths deleted; check:compat reports an empty list. The npm bootstrap recipe is history (the hand-published version is unpublished).

Proof: bun run check green; 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):

Kind Added Deleted Net
scripts (release-pipeline.ts) +493 -937 -444
tests (fixture + 3 test files) +1318 -1786 -468
docs (8 files) +40 -37 +3
workflows + settings +62 -74 -12

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

Function main PR Invariant
ensureTag - 34 one packaged commit per sha, created once; a rerun verifies (both the build tag and the version tag)
ensurePackaged - 27 the build tag's commit is this checkout's build of the source
assertPackageOf assertPackages 56 + assertSameBuild 24 + verifyPackagedTag 33 51 a packaged commit is its source's child carrying exactly the build
packagedTreeOf treePlusBundle 38 + strippedManifestBlob 21 37 the packaged tree: source + build + manifest minus preparation scripts
builtTree packagedTree 31 + builtPaths 29 25 the fresh build is a build of the checked-out source
assertCarries 18 16 the required build files are regular files
movePointer publishLatest 53 + retagMajor's lease 38 + observeMajorLine 22 + isNewer 10 69 a pointer never moves to a non-descendant; a kept value is a package
pruneBuildTags - 25 at most ten build tags
fetchObserved - 16 observe-then-fetch reads the observed value or re-reads
observeRemote 20 19 the lease id and the peeled commit
push pushUnlessOvertaken 18 + OVERTAKEN_PUSH 18 + LOCK_LOST 10 13 a refusal is classified by re-reading origin, never by stderr
packageCommit advanceBuild 25 + advanceChain 56 14 the post-green step
packageRelease 68 43 the version tag created once on the package
retagMajor 38 21 the major moves through the one mover
descendantsOf placeAgainst 18 + placePublished 36 + furthest 21 + Placement 8 40 npm next never regresses
gitOrNo gitYesNo 13 + resolveCommit 18 18 + 6 a "no" from git is exit 1; anything else throws
newerRelease releaseOrder 15 6 npm latest never regresses
deleted verifyPublishedRefs 40, readBuildTip 16, chainPackaging 13, validateTip 27, appendChain 18, commitChain 25, chainCommitOfNewestSource 17, assertOnChain 15, workflowPaths 8, isPackagedPath 7, packagedEntries 16, the chain and verify types 49 - the chain, and a check retagMajor already makes
unchanged anchorReleasePr, 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) and expectPackage (37, the packaged-commit contract both test files assert) replaces the refused list (200+) and plantTag/wrongSource (60) that lived in the test files; plantCommit/plantCommitIn (47) replace rivalChainCommit, stripWorkflows, and the two in-test planters; subcommand (24) moved out of the pipeline test; buildTagOf/buildTags/packagedOf/positionOf/originHolds (34) replace buildTip/sourceTrailer/appendedSha; shallowClone (9) replaces shallowChecker (21); the fixture's own six-script list keeps a dropped preparation trigger visible in the packaged manifest.

Invariants (how each is enforced):

Invariant Enforced by
one packaged commit per sha, never moved in place build/* ruleset (update, non_fast_forward, no bypass) + ensureTag; deletion only by the prune
a packaged commit is a build of exactly its parent assertPackageOf: parent edge, tree identity against a rebuild, required files, the fresh build's tree on a rerun
a pointer never moves to a non-descendant movePointer: descendant rule, --force-with-lease, head read after the pointer, kept value verified
A-then-B equals B-then-A independent artifacts, order-free moves, idempotent prune (test "two commits' runs end in the same state whichever finishes first")
latest, vN never vanish; vX.Y.Z never moves rulesets, unchanged
npm next and latest never regress #197's ancestry verdict, the lane, the 15-read hold
a rerun is a no-op ensureTag verifies; movePointer leaves a value at or past the candidate

Deletion pass, itemized: verifyPublishedRefs + verify (1); one create-once loop with a lazy commit supplier (2); replaced/warn/reportMove and the replacement message (3); movePointer's duplicate no-op paths and the unreachable off-main arm (4); fetchObserved boolean (5); the per-tag main check in the prune (6, accepted loss: a hand-made build/999999.x name; the name shape is still checked); the path-listing diagnostic, assertChildOf's ternary, assertCarries fixed to the required files, the duplicate existence loop (7); commitPackaged/packagedEntries/buildTagRef inlined, types un-exported (8); pushRefused folded, the prune's plain push, reason assembly gone (9); the pointer-result constructors, shouldStripManifest, plantEmptySubtree, withRemotePlans, the PREPARATION_SCRIPTS literal test (10); one descendantsOf (14); notices off 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; latest sits on a next pre-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 update rule (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 losing GH_REPO with 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-diff against 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: latest leaves 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 the build-tags ruleset lands and the build ruleset goes.

Recorded, not built: a branded verified-package type so movePointer callers cannot pass an arbitrary commit/source pair; a real-git race of two prune pushes past their advertisements.

Reviewer note (Copilot): movePointer deliberately 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

Copilot AI balanced review requested due to automatic review settings September 13, 2026 08:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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.

Comment thread .github/scripts/release-pipeline.ts Outdated
Comment thread .github/scripts/release-pipeline.ts Outdated
Comment thread .github/scripts/release-pipeline.ts
Comment thread docs/reference/library.md Outdated
@Vivswan Vivswan closed this Sep 13, 2026
@Vivswan Vivswan reopened this Sep 13, 2026
@Vivswan
Vivswan force-pushed the refactor/per-commit-build-tags branch from 2b2c52f to 2ae732b Compare September 13, 2026 08:50
Copilot AI review requested due to automatic review settings September 13, 2026 08:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

@Vivswan Vivswan added the merge-when-green Owner approved: merge once every gate is green label Sep 13, 2026
Copilot AI review requested due to automatic review settings September 13, 2026 10:07
@Vivswan
Vivswan force-pushed the refactor/per-commit-build-tags branch from 2ae732b to 8edee26 Compare September 13, 2026 10:07
@github-actions

github-actions Bot commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

File size check

0 over a hard cap (fails), 10 warning(s).

File Size Tier Cap
.github/scripts/release-pipeline.ts:6 156 chars warn 150
.github/scripts/release-pipeline.ts:449 151 chars warn 150
.github/scripts/release-pipeline.ts:1 30 comment lines (header) warn 25
.github/scripts/release-pipeline.ts:445 14 comment lines warn 10
.github/workflows/post-green.yml:27 153 chars warn 150
.github/workflows/post-green.yml:140 11 comment lines warn 10
.github/workflows/update-release.yml:67 164 chars warn 150
test/action/run.test.ts:759 151 chars warn 150
test/action/run.test.ts:780 155 chars warn 150
test/docs/guides.test.ts:468 155 chars warn 150

Split the file, wrap the line, shorten or exempt the comment, or list the path in .file-size-allow.local with a # reason.

4 managed file(s) skipped; repo-platform owns them.

Comment thread test/scripts/release-pipeline.test.ts Fixed
Comment thread test/scripts/release-pipeline.test.ts Fixed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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

Comment thread .github/scripts/release-pipeline.ts
Comment thread docs/upgrading/README.md Outdated
Copilot AI review requested due to automatic review settings September 13, 2026 10:28
@Vivswan
Vivswan force-pushed the refactor/per-commit-build-tags branch from 8edee26 to b04cbe6 Compare September 13, 2026 10:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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

Comment thread .github/settings.local.yml
Comment thread .github/SECURITY.md Outdated
Comment thread docs/reference/library.md Outdated
Copilot AI review requested due to automatic review settings September 13, 2026 11:35
@Vivswan
Vivswan force-pushed the refactor/per-commit-build-tags branch from b04cbe6 to b1a90cf Compare September 13, 2026 11:35
…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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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

Comment thread .github/SECURITY.md Outdated
…tion, referred to by title where it is cited
Copilot AI review requested due to automatic review settings September 13, 2026 11:55
@Vivswan
Vivswan force-pushed the refactor/per-commit-build-tags branch from b1a90cf to a1a6a01 Compare September 13, 2026 11:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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

Comment thread .github/scripts/release-pipeline.ts Outdated
Comment thread .github/SECURITY.md Outdated
Comment thread docs/reference/library.md Outdated
… 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.
Copilot AI review requested due to automatic review settings September 13, 2026 12:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 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

@Vivswan
Vivswan marked this pull request as ready for review September 13, 2026 12:33
@Vivswan
Vivswan merged commit eac7de6 into main Sep 13, 2026
36 checks passed
@Vivswan
Vivswan deleted the refactor/per-commit-build-tags branch September 13, 2026 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-when-green Owner approved: merge once every gate is green

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants