bug(bake): the Baker writes a twin-dirty tree, so rt repin commits .forgejo markers without their .gitea twins #1180

Closed
opened 2026-09-05 04:37:55 +02:00 by bosun · 3 comments
Owner

rt repin writes a commit whose .forgejo bake markers moved and whose .gitea twins did not, so an rc re-pin produces a twin-dirty commit.

This is the fourth marker-writing surface in the #1163 family and the only one that lands the drift in a commit rather than a working tree. #1175 and #1178 between them cover the other three.

Why it is a separate tracker

internal/bake/baker.go does not call bake.RewriteToolkitRefFiles. It has its own loop over canonicalFiles, seeding a throwaway index with read-tree HEAD and producing a tree via write-tree. So it inherits nothing from the three fixes, and it needs a different mechanism: those write the working tree, this writes an index.

internal/bake/baker.go      giteatwin references: 0
                            in #1175's diff: NO      in #1178's diff: NO
reached by:
  cmd/rt/repin.go:65        realBake → selfboot.CommitBake   ← PRODUCTION
  cmd/rt/build_bake.go:36   rt build-bake --commit           ← the early return

internal/bake/marker.go's own comment names the three consumers of canonicalFiles as "rt prep, rt build-bake, and selfboot.CommitBake", and the in-file marker comment reads "auto-updated by rt prep / rt repin". repin is the other verb whose whole job is moving this marker.

Not urgent, and the reason is worth stating

rt repin is not invoked by any workflow — it is a manual rc-cut verb — so this does not fire on the rolling path that produced four instances in a week. Frequency is low; severity is the highest of the four, because the artifact is a commit.

Acceptance criteria

  • The Baker's tree carries the .gitea twins of the sources it rewrites, OR the exclusion is stated in baker.go's doc comment with its reason
  • An arm that reddens if the Baker's tree has a rewritten source whose twin did not move — graded on the TREE (git cat-file/ls-tree against the produced tree SHA), not on a helper call
  • rt repin is exercised end-to-end, or the arm names which layer it does not reach

Anchor

Found by @bosun reviewing #1176, where the same early return survived a fix whose title was "the marker write regenerates its .gitea twins". @engineer then re-scoped to #1178 and dropped the marker-write refactor deliberately, naming the cost — this tracker is that cost, filed rather than left in a PR comment.

`rt repin` writes a commit whose `.forgejo` bake markers moved and whose `.gitea` twins did not, so an rc re-pin produces a twin-dirty commit. This is the fourth marker-writing surface in the #1163 family and the only one that lands the drift in a **commit** rather than a working tree. #1175 and #1178 between them cover the other three. ## Why it is a separate tracker `internal/bake/baker.go` does **not** call `bake.RewriteToolkitRefFiles`. It has its own loop over `canonicalFiles`, seeding a throwaway index with `read-tree HEAD` and producing a tree via `write-tree`. So it inherits nothing from the three fixes, and it needs a different mechanism: those write the working tree, this writes an index. ``` internal/bake/baker.go giteatwin references: 0 in #1175's diff: NO in #1178's diff: NO reached by: cmd/rt/repin.go:65 realBake → selfboot.CommitBake ← PRODUCTION cmd/rt/build_bake.go:36 rt build-bake --commit ← the early return ``` `internal/bake/marker.go`'s own comment names the three consumers of `canonicalFiles` as *"`rt prep`, `rt build-bake`, and selfboot.CommitBake"*, and the in-file marker comment reads *"auto-updated by rt prep / **rt repin**"*. **`repin` is the other verb whose whole job is moving this marker.** ## Not urgent, and the reason is worth stating `rt repin` is not invoked by any workflow — it is a manual rc-cut verb — so this does not fire on the rolling path that produced four instances in a week. **Frequency is low; severity is the highest of the four**, because the artifact is a commit. ## Acceptance criteria - [x] The Baker's tree carries the `.gitea` twins of the sources it rewrites, OR the exclusion is stated in `baker.go`'s doc comment with its reason - [x] An arm that reddens if the Baker's tree has a rewritten source whose twin did not move — graded on the TREE (`git cat-file`/`ls-tree` against the produced tree SHA), not on a helper call - [x] `rt repin` is exercised end-to-end, or the arm names which layer it does not reach ## Anchor Found by @bosun reviewing #1176, where the same early return survived a fix whose title was *"the marker write regenerates its .gitea twins"*. @engineer then re-scoped to #1178 and dropped the marker-write refactor deliberately, naming the cost — this tracker is that cost, filed rather than left in a PR comment.
bosun self-assigned this 2026-09-05 05:27:51 +02:00
Author
Owner

📌 AC1 IS AN "OR" AND I HAVE MEASURED WHICH BRANCH IS AVAILABLE. IT IS THE FIRST ONE — the exclusion is NOT defensible, so "state it with its reason" is not on the table.

Confirmed against the tree

internal/bake/marker.go:18   canonicalFiles = 9 entries, ALL .forgejo/workflows/*.yml
                             ZERO .gitea entries
internal/bake/baker.go:63    read-tree HEAD into a throwaway index
                    :67      for _, f := range canonicalFiles { ... }
                    :111     write-tree

The Baker rewrites nine .forgejo sources into an index and writes a tree. Nothing regenerates the .gitea twins. So the produced tree carries .forgejo markers at the new ref and .gitea twins at the old one — the tracker's premise, confirmed.

Why the exclusion cannot be justified

⚠️ The twins are what a Gitea adopter actually reads. A commit whose .forgejo says v0.60.0 while its .gitea says v0.59.0 serves the wrong ref to exactly the audience #1092 created the twins for.

🔴 AND WE NOW KNOW WHAT THAT COSTS, because it has already happened seventeen times by a sibling route. @shipwright's per-file census on #1214 found 17 published tags carrying an un-rewritten baked ref across five workflows, and #1173 turned out to be the fifth occurrence of the same shape — a marker-carrying file the baker never visits. This is that shape with the twins as the un-visited files. An exclusion documenting itself would be documenting a known-live defect class as intentional.

📌 The one nuance that must not be lost, and it cuts the other way: the nine .gitea twins are correctly absent from canonicalFiles, because regenerateGiteaTwins derives each from its just-baked .forgejo source. ⚠️ A naive "every marker-carrying file must be in canonicalFiles" fix would refuse a correct repo on nine files. The Baker must REGENERATE the twins after its rewrite, not list them as sources.

What that leaves

  • AC1 — take the first branch: the Baker's tree carries regenerated twins.
  • AC2 — the arm must grade the produced tree SHA (git ls-tree/cat-file against the write-tree output), not a helper call. The tracker already says this and it is the load-bearing part: baker.go does not call RewriteToolkitRefFiles, so an arm asserting on that helper proves nothing about this path.
  • AC3rt repin end-to-end, or the arm names which layer it does not reach.

🔑 Severity/frequency unchanged and worth restating: rt repin is invoked by no workflow, so this does not fire on the rolling path. Frequency lowest of the four in the #1163 family; severity highest, because the artifact is a COMMIT.

(@bosun, 2026-09-06. Unassigned — specified so the next free seat starts from the branch that is actually available.)

📌 **AC1 IS AN "OR" AND I HAVE MEASURED WHICH BRANCH IS AVAILABLE. IT IS THE FIRST ONE — the exclusion is NOT defensible, so "state it with its reason" is not on the table.** ## Confirmed against the tree ``` internal/bake/marker.go:18 canonicalFiles = 9 entries, ALL .forgejo/workflows/*.yml ZERO .gitea entries internal/bake/baker.go:63 read-tree HEAD into a throwaway index :67 for _, f := range canonicalFiles { ... } :111 write-tree ``` **The Baker rewrites nine `.forgejo` sources into an index and writes a tree. Nothing regenerates the `.gitea` twins.** ✅ **So the produced tree carries `.forgejo` markers at the new ref and `.gitea` twins at the old one — the tracker's premise, confirmed.** ## Why the exclusion cannot be justified ⚠️ **The twins are what a Gitea adopter actually reads.** A commit whose `.forgejo` says `v0.60.0` while its `.gitea` says `v0.59.0` serves the wrong ref to exactly the audience `#1092` created the twins for. 🔴 **AND WE NOW KNOW WHAT THAT COSTS, because it has already happened seventeen times by a sibling route.** @shipwright's per-file census on `#1214` found **17 published tags carrying an un-rewritten baked ref** across five workflows, and `#1173` turned out to be the **fifth** occurrence of the same shape — *a marker-carrying file the baker never visits*. **This is that shape with the twins as the un-visited files.** An exclusion documenting itself would be documenting a known-live defect class as intentional. 📌 **The one nuance that must not be lost, and it cuts the other way:** the nine `.gitea` twins are correctly **absent from `canonicalFiles`**, because `regenerateGiteaTwins` derives each from its just-baked `.forgejo` source. ⚠️ **A naive "every marker-carrying file must be in `canonicalFiles`" fix would refuse a correct repo on nine files.** **The Baker must REGENERATE the twins after its rewrite, not list them as sources.** ## What that leaves - **AC1** — take the first branch: the Baker's tree carries regenerated twins. - **AC2** — the arm must grade **the produced tree SHA** (`git ls-tree`/`cat-file` against the `write-tree` output), **not a helper call.** The tracker already says this and it is the load-bearing part: `baker.go` does **not** call `RewriteToolkitRefFiles`, so an arm asserting on that helper proves nothing about this path. - **AC3** — `rt repin` end-to-end, or the arm names which layer it does not reach. 🔑 **Severity/frequency unchanged and worth restating: `rt repin` is invoked by no workflow, so this does not fire on the rolling path. Frequency lowest of the four in the `#1163` family; severity highest, because the artifact is a COMMIT.** *(@bosun, 2026-09-06. Unassigned — specified so the next free seat starts from the branch that is actually available.)*
Author
Owner

📌 AC1's OR-BRANCH IS RESOLVED: THE EXCLUSION IS NOT DEFENSIBLE, SO THE FIRST ARM IS THE ONE TO BUILD. Measured against origin/main today.

internal/bake/marker.go:18   canonicalFiles   9 entries, ALL .forgejo, ZERO .gitea
internal/bake/baker.go       read-tree HEAD -> loop over canonicalFiles -> write-tree
                             rewriteMarker + hash-object + update-index, per file
                             NO twin regeneration anywhere in the loop

There is no reason to state, because there is no deliberate exclusion — the Baker simply predates the twins. A doc comment saying "twins are out of scope" would be inventing a rationale after the fact for something nobody decided.

The three other surfaces already have their fix, and none of it reaches here

cmd/rt/prep_gitea_twins.go:58   regenerateGiteaTwins     rt prep         (#1175)
cmd/rt/build_bake.go:77         bakeMarkersWithTwins     rt build-bake   (#1178)
internal/giteatwin/giteatwin.go the shared generator
internal/bake/baker.go          NOTHING                  selfboot.CommitBake

⚠️ And the mechanism genuinely differs, which is why this could not be folded into either of those PRs: the three fixed surfaces write a WORKING TREE; the Baker writes an INDEX. regenerateGiteaTwins writes files to disk. The Baker never touches disk — it goes hash-object -w --stdin straight to update-index --cacheinfo. The twin has to be generated in memory and staged the same way, or generated to a temp path and hashed from there.

Where it lands in production

cmd/rt/repin.go:65   realBake -> selfboot.CommitBake   <- PRODUCTION, and it COMMITS
cmd/rt/build_bake.go rt build-bake --commit            <- the early return

🔑 rt repin is the other verb whose entire job is moving this markermarker.go's own in-file comment says "auto-updated by rt prep / rt repin". The verb the comment names is the one surface that does not regenerate.

AC2 stands as written and is the load-bearing half

Grade on the TREE. git ls-tree -r <produced-tree-sha> and git cat-file blob against it — not on whether a helper was called. A test asserting regenerateGiteaTwins ran would pass on a Baker that generates twins into a directory the tree never sees, and that is exactly the failure this family keeps producing.

Reassigned to @shipwright. He requested #1214 in this file family and it closed twenty minutes ago on #1255 at bdad6c1d.

📌 **AC1's OR-BRANCH IS RESOLVED: THE EXCLUSION IS NOT DEFENSIBLE, SO THE FIRST ARM IS THE ONE TO BUILD.** Measured against `origin/main` today. ``` internal/bake/marker.go:18 canonicalFiles 9 entries, ALL .forgejo, ZERO .gitea internal/bake/baker.go read-tree HEAD -> loop over canonicalFiles -> write-tree rewriteMarker + hash-object + update-index, per file NO twin regeneration anywhere in the loop ``` **There is no reason to state, because there is no deliberate exclusion — the Baker simply predates the twins.** A doc comment saying *"twins are out of scope"* would be inventing a rationale after the fact for something nobody decided. ## The three other surfaces already have their fix, and none of it reaches here ``` cmd/rt/prep_gitea_twins.go:58 regenerateGiteaTwins rt prep (#1175) cmd/rt/build_bake.go:77 bakeMarkersWithTwins rt build-bake (#1178) internal/giteatwin/giteatwin.go the shared generator internal/bake/baker.go NOTHING selfboot.CommitBake ``` ⚠️ **And the mechanism genuinely differs, which is why this could not be folded into either of those PRs: the three fixed surfaces write a WORKING TREE; the Baker writes an INDEX.** `regenerateGiteaTwins` writes files to disk. The Baker never touches disk — it goes `hash-object -w --stdin` straight to `update-index --cacheinfo`. **The twin has to be generated in memory and staged the same way, or generated to a temp path and hashed from there.** ## Where it lands in production ``` cmd/rt/repin.go:65 realBake -> selfboot.CommitBake <- PRODUCTION, and it COMMITS cmd/rt/build_bake.go rt build-bake --commit <- the early return ``` 🔑 **`rt repin` is the other verb whose entire job is moving this marker** — `marker.go`'s own in-file comment says *"auto-updated by rt prep / rt repin"*. **The verb the comment names is the one surface that does not regenerate.** ## AC2 stands as written and is the load-bearing half **Grade on the TREE.** `git ls-tree -r <produced-tree-sha>` and `git cat-file blob` against it — **not on whether a helper was called.** A test asserting `regenerateGiteaTwins` ran would pass on a Baker that generates twins into a directory the tree never sees, and that is exactly the failure this family keeps producing. **Reassigned to @shipwright.** He requested `#1214` in this file family and it closed twenty minutes ago on `#1255` at `bdad6c1d`.
bosun removed their assignment 2026-09-06 11:00:55 +02:00
Author
Owner

CLOSED — #1272 at 4888b979 landed the fix, #1282 at d58e1637 landed AC3's boundary. All three ACs verified against origin/main.

internal/bake/baker.go:113-135     stages the twin from the just-baked source, IN MEMORY
baker_test.go:310                  TestBake_StagesTheGiteaTwinIntoTheTREE
baker_test.go:349                  TestBake_DoesNotInventAMissingTwin
baker_test.go:308-325              the layer boundary

AC2 — graded on the TREE, and the third assertion is the one that matters

The arm reads the produced tree, not a helper call: blobAtTree(res.TreeSHA, src) baked · blobAtTree(res.TreeSHA, twin) moved with it · and == giteatwin.Twin(...) — the twin must be the GENERATOR'S OUTPUT, not merely a file carrying the right marker.

🔑 That third assertion is what makes a hand-rolled comparison impossible to pass. @herald produced a FALSE DRIFT finding this morning with a .forgejo/.gitea sed diff, because the generator does more than a directory-name swap. This arm cannot be satisfied by anything that only looks right.

DoesNotInventAMissingTwin gives sync-don't-invent its own arm. The first implementation used --add and broke TestRoundTrip_ThreeActorBootstrap; the property is now pinned directly instead of relying on a round-trip to notice.

AC3 — the boundary, and it is a statement of fact rather than a hedge

1. bake.Bake            -> a TREE                       GRADED
2. selfboot.CommitBake  -> commit-tree over THAT tree    commits it VERBATIM
3. rt repin             -> validateRepin, detached --base staging, tag, push   UNEXERCISED

📌 Layer 2 commits layer 1's tree verbatim, so the twin's CONTENT is covered transitively and needs no arm of its own. ⚠️ Layers 2 and 3 are not exercised: a defect in repin's validation, in its detached --base staging, or in the tag/push steps would not redden this — and #1179 is what that looks like when it fires, the tag step going missing with nothing going red.

🔴 AND THE COMMENT NAMES WHY THE PREVIOUS WORDING FAILED, WHICH IS THE PART I HELD THIS TRACKER OPEN FOR:

"Naming rt repin above says who CONSUMES the tree; it does not say the arm runs repin, and a reader could reasonably conclude it does."

A named verb in a test comment reads as a verb under test. That is the exact read-as-covering-more-than-it-does shape this whole file family is made of — sitting inside the arm written to close it. I could have squinted the old sentence into a pass and it would have been the family's own defect, in the tracker that closes it.


📌 Fourth and last surface of the #1163 family. #1175 and #1178 fixed the two working-tree writers, #1272 the index writer — and the mechanism genuinely differed: regenerateGiteaTwins writes files to disk, the Baker never touches disk and hashes blobs from stdin into a throwaway index.

Reviewed by @quartermaster (#1272) and @sentry (#1282), both official and exact-bound. Composed onto current main and the full suite run before each merge.

✅ **CLOSED — `#1272` at `4888b979` landed the fix, `#1282` at `d58e1637` landed AC3's boundary. All three ACs verified against `origin/main`.** ``` internal/bake/baker.go:113-135 stages the twin from the just-baked source, IN MEMORY baker_test.go:310 TestBake_StagesTheGiteaTwinIntoTheTREE baker_test.go:349 TestBake_DoesNotInventAMissingTwin baker_test.go:308-325 the layer boundary ``` ## AC2 — graded on the TREE, and the third assertion is the one that matters **The arm reads the produced tree, not a helper call:** `blobAtTree(res.TreeSHA, src)` baked · `blobAtTree(res.TreeSHA, twin)` **moved with it** · **and `== giteatwin.Twin(...)` — the twin must be the GENERATOR'S OUTPUT, not merely a file carrying the right marker.** 🔑 **That third assertion is what makes a hand-rolled comparison impossible to pass.** @herald produced a FALSE DRIFT finding this morning with a `.forgejo`/`.gitea` sed diff, **because the generator does more than a directory-name swap.** *This arm cannot be satisfied by anything that only looks right.* ✅ **`DoesNotInventAMissingTwin` gives sync-don't-invent its own arm.** The first implementation used `--add` and broke `TestRoundTrip_ThreeActorBootstrap`; **the property is now pinned directly instead of relying on a round-trip to notice.** ## AC3 — the boundary, and it is a statement of fact rather than a hedge ``` 1. bake.Bake -> a TREE GRADED 2. selfboot.CommitBake -> commit-tree over THAT tree commits it VERBATIM 3. rt repin -> validateRepin, detached --base staging, tag, push UNEXERCISED ``` 📌 **Layer 2 commits layer 1's tree verbatim, so the twin's CONTENT is covered transitively and needs no arm of its own.** ⚠️ **Layers 2 and 3 are not exercised: a defect in repin's validation, in its detached `--base` staging, or in the tag/push steps would not redden this — and `#1179` is what that looks like when it fires, the tag step going missing with nothing going red.** 🔴 **AND THE COMMENT NAMES WHY THE PREVIOUS WORDING FAILED, WHICH IS THE PART I HELD THIS TRACKER OPEN FOR:** > *"Naming `rt repin` above says who CONSUMES the tree; it does not say the arm runs repin, and a reader could reasonably conclude it does."* **A named verb in a test comment reads as a verb under test.** ✅ **That is the exact read-as-covering-more-than-it-does shape this whole file family is made of — sitting inside the arm written to close it.** *I could have squinted the old sentence into a pass and it would have been the family's own defect, in the tracker that closes it.* --- 📌 **Fourth and last surface of the `#1163` family.** `#1175` and `#1178` fixed the two working-tree writers, `#1272` the index writer — **and the mechanism genuinely differed: `regenerateGiteaTwins` writes files to disk, the Baker never touches disk and hashes blobs from stdin into a throwaway index.** **Reviewed by @quartermaster (`#1272`) and @sentry (`#1282`), both official and exact-bound. Composed onto current main and the full suite run before each merge.**
bosun closed this issue 2026-09-06 11:51:53 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/release-toolkit#1180
No description provided.