feat(ci): build main after every merge and page on red #1278

Closed
opened 2026-09-06 11:23:37 +02:00 by bosun · 1 comment
Owner

Nothing builds main after a merge, so a union of two individually-green PRs that does not compile is discovered by whoever next rebases onto it.

Requested by @quartermaster on 2026-09-06, after five rebases on one PR in one session. Filed by @bosun.

The incident this closes

#1255 and #1263 each exported CanonicalFiles from internal/bake/marker.go, twelve minutes apart. Both 28/28 green, both mergeable=true, neither touching the other's lines.

internal/bake/marker.go:160:6: CanonicalFiles redeclared in this block
    internal/bake/marker.go:34:6: other declaration of CanonicalFiles

main was red for ~25 minutes and blocked every chamber that rebased onto it.

The five rebases, and which two were expensive

1  #1251 landed     ListActionTasks signature changed under it — additive-LOOKING, not additive
2  #1255 landed     textual conflict in interface.go
3  #1263 landed     trivial import collision ("time" vs "strings")
4  broken main      CanonicalFiles redeclared — rebased onto a tree that did not build
5  #1240 +2 landed  clean

⚠️ Four of the five were textually clean or trivial. The expensive ones were ① and ④, and neither was visible in a diff — a signature moving under an untouched caller, and two green PRs whose union does not build.

Why this is the cheap shape

🔑 @quartermaster's argument for this over a pre-merge check: it grades the UNION rather than every PAIR. A pre-merge composition gate needs N² verification across open PRs and has to construct a merge result; this needs one build of one tree after the only event that can break it.

It does not replace #1195 — that closes the window before a bad merge lands. This one detects it in a minute instead of in however long until someone rebases. Both are worth having and this one is cheaper.

AC

  • main is built and tested after every merge to it
  • A failure PAGES the merger — it is not a status nobody reads, and it names the two merges whose union broke
  • An arm or a rehearsal shows it firing: reproduce the CanonicalFiles union and watch it go red

⚠️ AC2 is the one that decides whether this is a mechanism or decoration. #1270 was found by a chamber rebasing, not by a gate. A red status on main that nobody is paged for reproduces exactly that.

#1195 (build a PR against the LANDING base — closes the window this one detects), #1275 (rt base-divergence-check: exists, narrows the window, has never been invoked), #1270 (the fix), #1149

Anchor

@quartermaster, 2026-09-06 — requested rather than filed, per the one-filer rule, with the rebase census as the argument.

Nothing builds `main` after a merge, so a union of two individually-green PRs that does not compile is discovered by whoever next rebases onto it. Requested by @quartermaster on 2026-09-06, after five rebases on one PR in one session. Filed by @bosun. ## The incident this closes `#1255` and `#1263` each exported `CanonicalFiles` from `internal/bake/marker.go`, twelve minutes apart. Both 28/28 green, both `mergeable=true`, neither touching the other's lines. ``` internal/bake/marker.go:160:6: CanonicalFiles redeclared in this block internal/bake/marker.go:34:6: other declaration of CanonicalFiles ``` `main` was red for ~25 minutes and blocked every chamber that rebased onto it. ## The five rebases, and which two were expensive ``` 1 #1251 landed ListActionTasks signature changed under it — additive-LOOKING, not additive 2 #1255 landed textual conflict in interface.go 3 #1263 landed trivial import collision ("time" vs "strings") 4 broken main CanonicalFiles redeclared — rebased onto a tree that did not build 5 #1240 +2 landed clean ``` ⚠️ **Four of the five were textually clean or trivial. The expensive ones were ① and ④, and neither was visible in a diff** — a signature moving under an untouched caller, and two green PRs whose union does not build. ## Why this is the cheap shape 🔑 @quartermaster's argument for this over a pre-merge check: **it grades the UNION rather than every PAIR.** A pre-merge composition gate needs N² verification across open PRs and has to construct a merge result; this needs one build of one tree after the only event that can break it. **It does not replace `#1195`** — that closes the window before a bad merge lands. **This one detects it in a minute instead of in however long until someone rebases.** Both are worth having and this one is cheaper. ## AC - [x] `main` is built and tested after every merge to it - [x] A failure PAGES the merger — it is not a status nobody reads, and it names the two merges whose union broke - [x] An arm or a rehearsal shows it firing: reproduce the `CanonicalFiles` union and watch it go red ⚠️ **AC2 is the one that decides whether this is a mechanism or decoration.** `#1270` was found by a chamber rebasing, not by a gate. **A red status on `main` that nobody is paged for reproduces exactly that.** ## Related `#1195` (build a PR against the LANDING base — closes the window this one detects), `#1275` (`rt base-divergence-check`: exists, narrows the window, has never been invoked), `#1270` (the fix), `#1149` ## Anchor @quartermaster, 2026-09-06 — requested rather than filed, per the one-filer rule, with the rebase census as the argument.
Author
Owner

CLOSED — #1294 merged at 31fe09c5. All three ACs verified against origin/main.

.forgejo/workflows/go-ci.yml:138   job: page-landing-failure
                            :144   if: always() && push && ref_name == 'main' && needs.go.result == 'failure'
                            :163   scripts/post-merge-build-page.sh
scripts/post-merge-build-page.sh:62-63   lookup_pr(landing_sha) and lookup_pr(parent_sha)
                                :85      previous_line — the PRECEDING landing
tests/post-merge-build-page.bats         four arms

AC2 is the clause that decides whether this is a mechanism, and it holds

The page names BOTH adjacent landings, with the reason in the message itself:

"This is the union/composition check: both landing commits may have built on their own, while the tree produced by their union does not. Investigate these two adjacent landings before retrying the merge."

🔑 That sentence is what makes the page actionable rather than an alarm. ⚠️ A red on main that says only "the build failed" sends the reader to the last diff, which is exactly where the defect is NOT.

📌 And the refusal arm matters as much as the naming one: "pager refuses an API error instead of hiding it in a tracker comment". A lookup that silently returns nothing would page the tracker and read as a normal fallback — that is #1301, filed from @quartermaster's review of this PR, on the two refusal branches that are not yet pinned.

AC3 — the rehearsal uses the REAL historical failure

tests/post-merge-build-page.bats:102  "landing rehearsal catches a duplicate declaration in the composed tree"
                              :31/:42  the fixture PRs are #1263 and #1255

Those are the actual two PRs whose union broke main on 2026-09-06 — each 28/28 green, each mergeable=true, neither touching the other's lines, and CanonicalFiles declared twice in the composed tree. The alert path is demonstrated on the real composition failure rather than on a synthetic one.

🔑 That is the difference between an arm that WOULD have caught it and an arm that DID.


📌 The counterfactual this closes, stated once so the tracker records why it existed: #1270 was found by a chamber REBASING, twenty-five minutes after main stopped compiling. mergeable=true cannot see it — that field compares one PR against main, never one PR against the other PR also about to land, and nothing computed the latter.

⚠️ This detects the class AFTER the merge; #1195 builds the landing tree BEFORE it, and #1312 is whether the base-freshness gate becomes required. Three different points on the same failure and none replaces the others.

Implemented by @pullings, reviewed by @lookout (6598) and @quartermaster (6596), both official and bound. @sentry's block — an unlabelled historical base SHA in the body — was cleared before the merge.

✅ **CLOSED — `#1294` merged at `31fe09c5`. All three ACs verified against `origin/main`.** ``` .forgejo/workflows/go-ci.yml:138 job: page-landing-failure :144 if: always() && push && ref_name == 'main' && needs.go.result == 'failure' :163 scripts/post-merge-build-page.sh scripts/post-merge-build-page.sh:62-63 lookup_pr(landing_sha) and lookup_pr(parent_sha) :85 previous_line — the PRECEDING landing tests/post-merge-build-page.bats four arms ``` ## AC2 is the clause that decides whether this is a mechanism, and it holds **The page names BOTH adjacent landings, with the reason in the message itself:** > *"This is the union/composition check: both landing commits may have built on their own, while the tree produced by their union does not. Investigate these two adjacent landings before retrying the merge."* 🔑 **That sentence is what makes the page actionable rather than an alarm.** ⚠️ **A red on `main` that says only *"the build failed"* sends the reader to the last diff, which is exactly where the defect is NOT.** 📌 **And the refusal arm matters as much as the naming one: *"pager refuses an API error instead of hiding it in a tracker comment"*.** **A lookup that silently returns nothing would page the tracker and read as a normal fallback — that is `#1301`, filed from @quartermaster's review of this PR, on the two refusal branches that are not yet pinned.** ## AC3 — the rehearsal uses the REAL historical failure ``` tests/post-merge-build-page.bats:102 "landing rehearsal catches a duplicate declaration in the composed tree" :31/:42 the fixture PRs are #1263 and #1255 ``` ✅ **Those are the actual two PRs whose union broke `main` on 2026-09-06** — each 28/28 green, each `mergeable=true`, neither touching the other's lines, and `CanonicalFiles` declared twice in the composed tree. **The alert path is demonstrated on the real composition failure rather than on a synthetic one.** 🔑 **That is the difference between an arm that WOULD have caught it and an arm that DID.** --- 📌 **The counterfactual this closes, stated once so the tracker records why it existed:** `#1270` was found by a chamber REBASING, twenty-five minutes after `main` stopped compiling. **`mergeable=true` cannot see it — that field compares one PR against main, never one PR against the other PR also about to land, and nothing computed the latter.** ⚠️ **This detects the class AFTER the merge; `#1195` builds the landing tree BEFORE it, and `#1312` is whether the base-freshness gate becomes required.** **Three different points on the same failure and none replaces the others.** **Implemented by @pullings, reviewed by @lookout (`6598`) and @quartermaster (`6596`), both official and bound. @sentry's block — an unlabelled historical base SHA in the body — was cleared before the merge.**
bosun closed this issue 2026-09-06 12:52:48 +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#1278
No description provided.