chore(ci): the shared checkout+build prefix sits in twelve jobs, not five #1304

Closed
opened 2026-09-06 12:21:13 +02:00 by bosun · 2 comments
Member

The shared checkout+build prefix that #1253 consolidates sits in twelve jobs, not five — the other seven are differently-named contexts and are out of scope there.

Requested by @engineer, 2026-09-06, while building #1297 (step 1 of #1253). Filed by @bosun.

Measured

#1253's scope        5 jobs carrying `toolkit-self gate (PR's own rt)`
actually carrying
the shared prefix   12 jobs
out of scope there   7   — differently-named contexts

#1253's consolidation is correct as scoped: it targets the five identically-named self-gate jobs and reconciles exactly —

five jobs      3+4+5+3+4 = 19 steps, FIVE go builds
consolidated              11 steps,  ONE build
difference                 8 = 4 redundant checkouts + 4 redundant builds

⚠️ The remaining seven carry the same redundant setup and are not reachable by the same rename, because their contexts differ and are individually required. Consolidating them is a different operation with a different branch-protection cost.

📌 @engineer requested this rather than widening #1297 mid-flight, which was the right call: step 1 of a three-step migration is the worst place to grow scope.

What has to be established before anything moves

  • The seven are enumerated with their contexts and their step counts — the same reconciliation #1253 produced, so "cheaper" is a fact and not a hope
  • Whether their triggers form a UNION that a single job can carry, per #1253's own finding: a consolidated on: that is the INTERSECTION runs in strictly fewer situations than the jobs it replaces, and no drift arm can see that
  • Whether consolidating them is worth the branch-protection churn at all — seven required contexts retired and one added is a bigger blast radius than five

#1253 (the five), #1297 (step 1), #1177 (why every context's required-status is load-bearing)

Anchor

@engineer, 2026-09-06. Measured while proving the consolidated job is not a stub — he pushed a deliberate 49-word density break and watched both the incumbent fragment-check gate (12:16:24) and the consolidated job (12:17:25) go red.

📌 Why that demonstration was necessary is worth carrying here: a consolidated job whose nine gate steps ALL SKIPPED is GREEN too, and this instance exposes no job log — 404 on the API, 307 to a session login on the web route. So he had arithmetic (63s vs 34s) and not an observation, and arithmetic cannot distinguish ran and passed from skipped.

The shared checkout+build prefix that `#1253` consolidates sits in twelve jobs, not five — the other seven are differently-named contexts and are out of scope there. Requested by @engineer, 2026-09-06, while building `#1297` (step 1 of `#1253`). Filed by @bosun. ## Measured ``` #1253's scope 5 jobs carrying `toolkit-self gate (PR's own rt)` actually carrying the shared prefix 12 jobs out of scope there 7 — differently-named contexts ``` **`#1253`'s consolidation is correct as scoped**: it targets the five identically-named self-gate jobs and reconciles exactly — ``` five jobs 3+4+5+3+4 = 19 steps, FIVE go builds consolidated 11 steps, ONE build difference 8 = 4 redundant checkouts + 4 redundant builds ``` ⚠️ **The remaining seven carry the same redundant setup and are not reachable by the same rename**, because their contexts differ and are individually required. **Consolidating them is a different operation with a different branch-protection cost.** 📌 @engineer requested this rather than widening `#1297` mid-flight, which was the right call: step 1 of a three-step migration is the worst place to grow scope. ## What has to be established before anything moves - [x] The seven are enumerated with their contexts and their step counts — the same reconciliation `#1253` produced, so "cheaper" is a fact and not a hope - [x] Whether their triggers form a UNION that a single job can carry, per `#1253`'s own finding: a consolidated `on:` that is the INTERSECTION runs in strictly fewer situations than the jobs it replaces, and no drift arm can see that - [x] Whether consolidating them is worth the branch-protection churn at all — seven required contexts retired and one added is a bigger blast radius than five ## Related `#1253` (the five), `#1297` (step 1), `#1177` (why every context's required-status is load-bearing) ## Anchor @engineer, 2026-09-06. Measured while proving the consolidated job is not a stub — he pushed a deliberate 49-word density break and watched both the incumbent `fragment-check` gate (12:16:24) and the consolidated job (12:17:25) go red. 📌 **Why that demonstration was necessary is worth carrying here: a consolidated job whose nine gate steps ALL SKIPPED is GREEN too, and this instance exposes no job log — 404 on the API, 307 to a session login on the web route.** So he had arithmetic (63s vs 34s) and not an observation, and arithmetic cannot distinguish *ran and passed* from *skipped*.
Member

All three "establish before anything moves" items are measured, on main at 0b1dd496 (post-#1253). My recommendation is DO NOT CONSOLIDATE THESE. The duplication worth removing is already gone; what is left is a shared code prefix that is not a shared consolidation opportunity.

① The enumeration — and it is 8 now, not 7, which reconciles exactly

workflow                      job     steps  required  triggers / pr-types
base-divergence-check.yml     check       3    -       pull_request / opened,synchronize,reopened
build-ref-check.yml           check       4    -       push + workflow_dispatch
check-self-bootstrap.yml      check       3   YES      pull_request + push
fork-pr-approval-notice.yml   notice      3    -       pull_request_target
gitea-twin-check.yml          check       3   YES      pull_request + push
mirror-drift-check.yml        check       3    -       release + workflow_dispatch
prep-order-check.yml          check       4   YES      pull_request / opened,synchronize,reopened
prepared-uncut-check.yml      self        3    -       push + schedule + workflow_dispatch

The tracker says seven. It is eight because base-divergence-check.yml landed during the #1253 migration — the same new workflow that made the AC3 timing comparison span two populations. 7 + 1 = 8, nothing unaccounted for.

② The triggers do NOT form a union a single job can carry — this is the answer to AC2 and it is a no

The five that #1253 consolidated shared one trigger: pull_request, branches: [main], no paths. These eight span six different trigger sets:

pull_request                          base-divergence-check, prep-order-check
pull_request + push                   check-self-bootstrap, gitea-twin-check
pull_request_target                   fork-pr-approval-notice
push + workflow_dispatch              build-ref-check
release + workflow_dispatch           mirror-drift-check
push + schedule + workflow_dispatch   prepared-uncut-check

🔴 fork-pr-approval-notice is pull_request_target and must never be merged with a pull_request job. That is a security boundary, not a scheduling detail: pull_request_target runs with repository secrets against a fork's base, and merging it into a job that also fires on pull_request would hand fork-controlled trigger conditions a job that holds secrets. This one is out on grounds that have nothing to do with cost.

A union across the rest would make each job fire on events it does not currently handle — release, schedule, push — which is strictly more running, not less. #1253's own union finding applies with the sign reversed: there the union was safe because the five differed only in types; here they differ in events.

③ Not worth the churn — and the cost figure that justified #1253 does not transfer

The 24s median belonged to the five self-gate jobs, and it was their GATES, not their prefix. Measured per workflow, successful runs:

check-self-bootstrap    n=826   median  5s      <- essentially prefix-only
fork-pr-approval-notice n=308   median  6s
gitea-twin-check        n=307   median 18s
prepared-uncut (self)   n= 44   median 25s
base-divergence-check   n= 10   median 26s
prep-order-check        n=128   median 27s
build-ref-check                 no successful run in the window — COULD NOT GRADE
mirror-drift-check              no successful run in the window — COULD NOT GRADE

check-self-bootstrap is the informative row: at a 5s median over 826 runs it is close to pure prefix, so a checkout+build costs ~5s here, not ~24s. Applying the five's rate to this population is exactly the error AC3 made on #1253, one population's number carried onto another.

Only two pairs share a trigger set exactly, so only two merges are even available:

base-divergence-check + prep-order-check    saves 1 prefix (~5s), retires 1 REQUIRED context
check-self-bootstrap  + gitea-twin-check    saves 1 prefix (~5s), retires 2 REQUIRED contexts

~10s and 2 slots, against running the ADD → REQUIRE → REMOVE migration twice more over three required contexts. #1253 took three PRs, a branch-protection edit, an attest mechanism and two ordering corrections to move five jobs safely. That price bought ~96s and 4 slots. This one buys ~10s.

⚠️ And consolidation makes WALL-CLOCK worse whenever slots are free

This is the part I had not appreciated before measuring, and it qualifies #1253 as well as this tracker.

A consolidated job runs its steps sequentially. Two 26s jobs in parallel finish in ~27s wall; merged, they finish in ~53s. #1253 improved wall-clock anyway (115s → 92s) because the queue was saturated — running=8 waiting=47 — so the five were never actually running in parallel.

Consolidation trades wall-clock for slots, and only pays when slots are contended. For five gate jobs under campaign load that was a clear win. For two 5s jobs on an idle queue it is a loss on both axes.

Recommendation

Close as won't-do, or hold it explicitly against a future state where the runner queue is the binding constraint again and a trigger-compatible pair is worth the protection churn. I am not proposing to widen it; #1253's scoping was right and the remainder is not the same problem wearing a different name.

What I would rather spend the effort on is the residual I flagged on #1320: the nine consolidated gates now share one checkout and one $RUNNER_TEMP where they had five, and that they do not collide is an audit I performed, not a property anything enforces. That is a live hazard in code that already shipped, against a hypothetical saving of 10s here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa

All three "establish before anything moves" items are measured, on `main` at `0b1dd496` (post-`#1253`). **My recommendation is DO NOT CONSOLIDATE THESE.** The duplication worth removing is already gone; what is left is a shared code prefix that is not a shared consolidation opportunity. ## ① The enumeration — and it is 8 now, not 7, which reconciles exactly ``` workflow job steps required triggers / pr-types base-divergence-check.yml check 3 - pull_request / opened,synchronize,reopened build-ref-check.yml check 4 - push + workflow_dispatch check-self-bootstrap.yml check 3 YES pull_request + push fork-pr-approval-notice.yml notice 3 - pull_request_target gitea-twin-check.yml check 3 YES pull_request + push mirror-drift-check.yml check 3 - release + workflow_dispatch prep-order-check.yml check 4 YES pull_request / opened,synchronize,reopened prepared-uncut-check.yml self 3 - push + schedule + workflow_dispatch ``` The tracker says seven. It is eight because `base-divergence-check.yml` landed during the `#1253` migration — the same new workflow that made the AC3 timing comparison span two populations. `7 + 1 = 8`, nothing unaccounted for. ## ② The triggers do NOT form a union a single job can carry — this is the answer to AC2 and it is a no The five that `#1253` consolidated shared one trigger: `pull_request`, `branches: [main]`, no `paths`. These eight span **six different trigger sets**: ``` pull_request base-divergence-check, prep-order-check pull_request + push check-self-bootstrap, gitea-twin-check pull_request_target fork-pr-approval-notice push + workflow_dispatch build-ref-check release + workflow_dispatch mirror-drift-check push + schedule + workflow_dispatch prepared-uncut-check ``` 🔴 **`fork-pr-approval-notice` is `pull_request_target` and must never be merged with a `pull_request` job.** That is a security boundary, not a scheduling detail: `pull_request_target` runs with repository secrets against a fork's *base*, and merging it into a job that also fires on `pull_request` would hand fork-controlled trigger conditions a job that holds secrets. **This one is out on grounds that have nothing to do with cost.** A union across the rest would make each job fire on events it does not currently handle — `release`, `schedule`, `push` — which is strictly *more* running, not less. `#1253`'s own union finding applies with the sign reversed: there the union was safe because the five differed only in `types`; here they differ in *events*. ## ③ Not worth the churn — and the cost figure that justified `#1253` does not transfer **The 24s median belonged to the five self-gate jobs, and it was their GATES, not their prefix.** Measured per workflow, successful runs: ``` check-self-bootstrap n=826 median 5s <- essentially prefix-only fork-pr-approval-notice n=308 median 6s gitea-twin-check n=307 median 18s prepared-uncut (self) n= 44 median 25s base-divergence-check n= 10 median 26s prep-order-check n=128 median 27s build-ref-check no successful run in the window — COULD NOT GRADE mirror-drift-check no successful run in the window — COULD NOT GRADE ``` `check-self-bootstrap` is the informative row: at a 5s median over 826 runs it is close to pure prefix, so **a checkout+build costs ~5s here, not ~24s.** Applying the five's rate to this population is exactly the error AC3 made on `#1253`, one population's number carried onto another. Only two pairs share a trigger set exactly, so only two merges are even available: ``` base-divergence-check + prep-order-check saves 1 prefix (~5s), retires 1 REQUIRED context check-self-bootstrap + gitea-twin-check saves 1 prefix (~5s), retires 2 REQUIRED contexts ``` **~10s and 2 slots, against running the ADD → REQUIRE → REMOVE migration twice more over three required contexts.** `#1253` took three PRs, a branch-protection edit, an attest mechanism and two ordering corrections to move five jobs safely. That price bought ~96s and 4 slots. This one buys ~10s. ## ⚠️ And consolidation makes WALL-CLOCK worse whenever slots are free This is the part I had not appreciated before measuring, and it qualifies `#1253` as well as this tracker. **A consolidated job runs its steps sequentially.** Two 26s jobs in parallel finish in ~27s wall; merged, they finish in ~53s. `#1253` improved wall-clock anyway (115s → 92s) **because the queue was saturated** — `running=8 waiting=47` — so the five were never actually running in parallel. > **Consolidation trades wall-clock for slots, and only pays when slots are contended.** For five gate jobs under campaign load that was a clear win. For two 5s jobs on an idle queue it is a loss on both axes. ## Recommendation **Close as won't-do**, or hold it explicitly against a future state where the runner queue is the binding constraint again *and* a trigger-compatible pair is worth the protection churn. I am not proposing to widen it; `#1253`'s scoping was right and the remainder is not the same problem wearing a different name. **What I would rather spend the effort on** is the residual I flagged on `#1320`: the nine consolidated gates now share one checkout and one `$RUNNER_TEMP` where they had five, and that they do not collide is an audit I performed, not a property anything enforces. That is a live hazard in code that already shipped, against a hypothetical saving of 10s here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
Author
Member

✅ CLOSING WITH A MEASURED NO. @engineer's recommendation is DO NOT CONSOLIDATE, and all three ACs are answered — two of them in the negative, which is a result rather than an absence of one.

AC1 — enumerated. It is EIGHT, not seven: base-divergence-check landed mid-migration, so 7 + 1 = 8 with nothing unaccounted for.

AC2 — NO. The triggers do not form a union a single job can carry.

distinct trigger sets across 34 workflows: 10        (reproduced by @bosun)
  workflow_call                       10
  pull_request                         9
  pull_request,push                    4
  push,workflow_dispatch               3
  workflow_dispatch                    3
  pull_request_target                  1   <- fork-pr-approval-notice.yml, ALONE
  …

The eight span SIX different sets. 🔴 And fork-pr-approval-notice is pull_request_target — a SECURITY boundary, not a scheduling detail. ⚠️ Merging it with a pull_request job would hand fork-controlled conditions a job holding secrets. That is not a cost to weigh against a saving; it is a reason the merge must not happen.

AC3 — NO, and the cost figure I would have used was wrong.

the 24s median belonged to the FIVE GATES, not to the prefix
check-self-bootstrap  near-pure prefix, 5s median over 826 runs
=> a checkout+build costs ~5s here
only TWO pairs share a trigger set
=> whole available saving ~10s and 2 slots
   against churning THREE required contexts through ADD -> REQUIRE -> REMOVE, twice

⚠️ That is #1253's AC3 basis error repeating in a new place, caught before it was acted on this time — a number measured on one population, carried to another where it does not apply.

🔑 And a finding that qualifies #1253 itself

A consolidated job runs its steps SEQUENTIALLY, so consolidation trades WALL-CLOCK for SLOTS, and only pays when slots are contended.

#1253 improved wall-clock only because the queue was saturated at running=8 waiting=47. ⚠️ Under an idle queue the same change would have made wall-clock WORSE — nine sequential steps where there were five parallel jobs.

📌 So #1253 was right for the conditions it ran in and is not a general rule, and this tracker is the evidence for the boundary. Worth carrying into any future consolidation proposal: measure the queue, not just the jobs.

✅ **CLOSING WITH A MEASURED NO. @engineer's recommendation is DO NOT CONSOLIDATE, and all three ACs are answered — two of them in the negative, which is a result rather than an absence of one.** **AC1 — enumerated. It is EIGHT, not seven:** `base-divergence-check` landed mid-migration, so `7 + 1 = 8` with nothing unaccounted for. **AC2 — NO. The triggers do not form a union a single job can carry.** ``` distinct trigger sets across 34 workflows: 10 (reproduced by @bosun) workflow_call 10 pull_request 9 pull_request,push 4 push,workflow_dispatch 3 workflow_dispatch 3 pull_request_target 1 <- fork-pr-approval-notice.yml, ALONE … ``` **The eight span SIX different sets.** 🔴 **And `fork-pr-approval-notice` is `pull_request_target` — a SECURITY boundary, not a scheduling detail.** ⚠️ **Merging it with a `pull_request` job would hand fork-controlled conditions a job holding secrets.** *That is not a cost to weigh against a saving; it is a reason the merge must not happen.* **AC3 — NO, and the cost figure I would have used was wrong.** ``` the 24s median belonged to the FIVE GATES, not to the prefix check-self-bootstrap near-pure prefix, 5s median over 826 runs => a checkout+build costs ~5s here only TWO pairs share a trigger set => whole available saving ~10s and 2 slots against churning THREE required contexts through ADD -> REQUIRE -> REMOVE, twice ``` ⚠️ **That is `#1253`'s AC3 basis error repeating in a new place, caught before it was acted on this time** — a number measured on one population, carried to another where it does not apply. ## 🔑 And a finding that qualifies `#1253` itself > **A consolidated job runs its steps SEQUENTIALLY, so consolidation trades WALL-CLOCK for SLOTS, and only pays when slots are contended.** **`#1253` improved wall-clock only because the queue was saturated at `running=8 waiting=47`.** ⚠️ **Under an idle queue the same change would have made wall-clock WORSE** — nine sequential steps where there were five parallel jobs. 📌 **So `#1253` was right for the conditions it ran in and is not a general rule, and this tracker is the evidence for the boundary.** *Worth carrying into any future consolidation proposal: measure the queue, not just the jobs.*
bosun closed this issue 2026-09-06 14:44:43 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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#1304
No description provided.