Skip to content

fix(ci): give every main commit its own concurrency group — cancel-in-progress: false was not enough - #2767

Merged
rbuergi merged 1 commit into
mainfrom
fix/main-runs-must-not-be-evicted
Aug 30, 2026
Merged

rbuergi merged 1 commit into
mainfrom
fix/main-runs-must-not-be-evicted

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Three commits landed on main today without ever being built or shipped

They report cancelled, which reads like someone cancelled them. Nobody did — they were evicted while queued.

cancel-in-progress: false (#2412) protects the run that is already executing. GitHub also keeps exactly one pending run per concurrency group, and every main push shared build-test-refs/heads/main. So a third arrival evicted the second's queued run, which reported cancelled without running a step.

Measured 2026-08-30, one merge burst — note run_started_at == created_at even while a run is only queued, which is what makes this look like a mid-run cancellation:

commit queued cancelled
dca02bdd7 11:42:10 11:53:02 — 1 s after the next push was created
57de73c45 11:54:57 12:08:54 — 2 s after the next push
637ee3921 12:08:52 12:09:16 — 1 s after the next push

Both failures #2412 exists to prevent happened anyway

  1. Nothing built the tree that landed. With strict: false each PR is tested against the main it branched from, so the merged combination is first compiled by main's own run. Three landed commits never got one — this is the shape that put CS0246: MeshOperations on main on 2026-08-26.
  2. Nothing published. main-cd gates delivery on Consolidate test results succeeding for that SHA, which an evicted run never produces. Three CD runs reported green with every job skipped while ACR's newest image stayed several commits behind.

The fix

A push to main groups by github.sha, so no two main runs share a group and none can be evicted — which is exactly the cost #2412's comment says is deliberately accepted ("one full run per merge to main"). PRs still group by ref and still supersede, so #2316's ~28% saving is untouched.

The guard was green the whole time

MainRunsAreNeverCancelledGuard asserted that the cancel-in-progress expression evaluates false on main. That was true, and beside the point — the invariant is "a main run is never cancelled", and eviction violates it without touching that setting. A guard that checks the setting instead of the property is the same defect class this repo keeps finding.

It now also evaluates the group expression for two different main commits and requires the results to differ (and for two commits on one PR to match, so superseding is not silently disabled). Its evaluator gains value-faithful &&/|| — Actions returns the operand, not a bool, which is precisely how group: selects a string.

Verified by mutation, not by passing

probe result
restore the ref-grouped line that was live during the evictions 🔴 "puts every main commit in ONE concurrency group … yields build-test-refs/heads/main for two different commits"
restore the fix 🟢 11/11

(My first mutation attempt silently failed to apply and the suite passed — that "green" proved nothing and was redone. Noting it because it is the same false-pass shape this PR is about.)

Not in scope

main-cd.yml's push lane still groups by ref and can evict its own runs the same way; it already carries a partial fix for this (#2490, the reconcile lane split). Flagging rather than widening.

🤖 Generated with Claude Code

…-progress: false was not enough

#2412 set `cancel-in-progress: false` for main so merges could not kill each
other's runs. That protects the run already EXECUTING. GitHub also keeps
exactly ONE PENDING run per concurrency group, and every main push shared the
group `build-test-refs/heads/main` — so a third arrival EVICTED the queued one,
which then reported `cancelled` having never run a step.

Measured 2026-08-30, one merge burst (note run_started_at == created_at even
while a run is only queued, which is what makes this read as cancelled mid-run):

  dca02bd queued 11:42:10 -> cancelled 11:53:02, 1s after the next push
  57de73c queued 11:54:57 -> cancelled 12:08:54, 2s after the next push
  637ee39 queued 12:08:52 -> cancelled 12:09:16, 1s after the next push

Both failures #2412's own comment says it prevents happened anyway:

  1. Nothing built the tree that landed. With strict:false each PR is tested
     against the main it branched from, so the MERGED combination is first
     compiled by main's own run. Three landed commits never got one.
  2. Nothing published. main-cd gates delivery on `Consolidate test results`
     reaching success FOR THAT SHA; an evicted run never produces one. Three CD
     runs reported green with every job `skipped` while ACR's newest image
     stayed several commits behind.

Fix: a push to main groups by `github.sha`, so no two main runs share a group
and none can be evicted. PRs still group by ref and still supersede (#2316's
~28% saving is untouched). `cancel-in-progress` stays as the statement of
intent and for any future ref that groups by name.

MainRunsAreNeverCancelledGuard was GREEN through all of this: it asserted the
`cancel-in-progress` expression, which was correct and beside the point. It now
also evaluates the GROUP expression for two different main commits and requires
them to differ — the invariant is about the group, not the flag. Its expression
evaluator gains value-faithful `&&`/`||` (Actions returns the OPERAND, not a
bool), which is what `group:` uses to select a string.

Verified by MUTATION, not by passing: restoring the ref-grouped line makes the
new test fail naming the collision; restoring the fix makes it pass (11/11).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 30, 2026 12:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the “MeshWeaver Build and Test” workflow’s concurrency grouping so that each push to main uses a unique concurrency group per commit SHA, preventing queued main runs from being evicted (and reported as cancelled) during merge bursts. It also strengthens the guard test to validate the concurrency.group behavior and documents the user-visible impact in a What’s New entry.

Changes:

  • Change dotnet-test.yml concurrency group so main pushes group by github.sha (PRs still group by ref; workflow_dispatch still groups by run_id).
  • Extend MainRunsAreNeverCancelledGuard to evaluate group: and ensure main commits do not share a group while PR commits do.
  • Add a What’s New entry describing the fix and why it matters.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
test/MeshWeaver.Documentation.Test/MainRunsAreNeverCancelledGuard.cs Adds a guard asserting per-SHA grouping on main and updates the evaluator to preserve &&/`
src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-30-every-commit-that-lands-on-main-is-tested.md Documents the operational impact and the change for the What’s New feed.
.github/workflows/dotnet-test.yml Adjusts workflow concurrency grouping to avoid queued-run eviction on main while keeping PR superseding behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

722 tests  +1   530 ✅ +1   5m 14s ⏱️ +17s
  5 suites ±0   192 💤 ±0 
  5 files   ±0     0 ❌ ±0 

Results for commit 3203054. ± Comparison against base commit d1a8259.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 547 tests  ±0   1 547 ✅ ±0   5m 19s ⏱️ - 1m 7s
    8 suites ±0       0 💤 ±0 
    8 files   ±0       0 ❌ ±0 

Results for commit 3203054. ± Comparison against base commit d1a8259.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

   10 files  ±0     10 suites  ±0   5m 1s ⏱️ -49s
3 481 tests ±0  3 481 ✅ ±0  0 💤 ±0  0 ❌ ±0 
4 140 runs  +1  4 140 ✅ +1  0 💤 ±0  0 ❌ ±0 

Results for commit 3203054. ± Comparison against base commit d1a8259.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

917 tests  ±0   917 ✅ ±0   5m 9s ⏱️ -20s
  6 suites ±0     0 💤 ±0 
  6 files   ±0     0 ❌ ±0 

Results for commit 3203054. ± Comparison against base commit d1a8259.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

966 tests  ±0   962 ✅ ±0   6m 1s ⏱️ +2s
  6 suites ±0     4 💤 ±0 
  6 files   ±0     0 ❌ ±0 

Results for commit 3203054. ± Comparison against base commit d1a8259.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

801 tests  ±0   801 ✅ ±0   6m 44s ⏱️ -7s
  6 suites ±0     0 💤 ±0 
  6 files   ±0     0 ❌ ±0 

Results for commit 3203054. ± Comparison against base commit d1a8259.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

   41 files  ±0     41 suites  ±0   33m 31s ⏱️ - 2m 3s
8 434 tests +1  8 238 ✅ +1  196 💤 ±0  0 ❌ ±0 
9 093 runs  +2  8 897 ✅ +2  196 💤 ±0  0 ❌ ±0 

Results for commit 3203054. ± Comparison against base commit d1a8259.

@rbuergi
rbuergi merged commit 2ead783 into main Aug 30, 2026
26 checks passed
@rbuergi
rbuergi deleted the fix/main-runs-must-not-be-evicted branch August 30, 2026 12:59
@rbuergi

rbuergi commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Correction to the record: this was not a novel finding — MeshWeaver.Plugins fixed the same defect a day earlier, and I did not check the fleet for precedent before writing it.

MeshWeaver.Plugins/.github/workflows/ci.yml already keys its main concurrency group on github.sha, and its comment states the mechanism in the same terms:

🚨 A MAIN PUSH GETS ITS OWN GROUP, KEYED ON THE COMMIT — never github.ref.
cancel-in-progress: false (below) does NOT mean "nothing is cancelled". GitHub keeps at most ONE PENDING run per concurrency group: when a new run enters a group that already has a QUEUED run, the queued one is CANCELLED, whatever cancel-in-progress says. That flag only ever protects the run that is already IN PROGRESS.

That landed as Plugins #888 on 2026-08-29, driven by its own measurement — 22 of 25 main runs cancelled with jobs=0, started=0. Same defect, same fix, one repo over, one day before core's three commits were evicted.

Two things worth taking from that:

  1. The fix here is right and is now proven in production (three core merges inside 2½ minutes, all three runs concurrent). Nothing about it changes.
  2. The independent rediscovery is the cost of not looking. A fleet-shared CI defect solved in one repo should be swept across the others in the same change; instead core kept the hole for a day and paid three untested commits and a stalled publish for it. When a CI defect is found in one repo of this fleet, the next step is to grep the sibling repos for the same shape before writing the fix — the answer may already be written, with the measurement attached.

Filing that as the durable lesson rather than leaving this PR implying it was first.

@rbuergi

rbuergi commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up on the "main-cd's push lane still groups by ref" note above: it stays that way, deliberately, and here is why it must NOT get this PR's fix.

Observed today: four consecutive Continuous Delivery (main) runs cancelled between 15:29 and 15:38 — the same single-pending-slot eviction this PR removed from Build-and-Test.

But CD's serialization is load-bearing in a way Build-and-Test's was not. main-cd.yml (lines 65–67) records the invariant:

"Re-publishing a superseded commit is not merely low-value, it is UNSOUND: the version tag comes from the BUILDING run's number, so republishing older code would mint a higher -ci.<n> for it and every install would roll "forward" onto older code — breaking the monotonic-build-number invariant VersionSelect documents in red."

Group CD runs by SHA and two runs for consecutive commits can promote concurrently; whichever finishes last wins main/ci.<n>, and if that is the older commit, every self-updating install rolls backwards. The single ref-keyed lane is what makes "the last promote is the newest commit" true.

So the eviction of a queued CD run is a cost the design accepts, and the reconciler is its remedy — it heals HEAD on the hour, never a superseded commit. Measured today: three complete promoted sets (ci.6691, ci.6714, ci.6748) despite ~10 evicted runs. Nothing to fix here; recording it so the next reader does not apply this PR's fix to CD by analogy.

rbuergi added a commit that referenced this pull request Aug 30, 2026
…led (#2412) (#2799)

Toward #2412 — the structural fix, not a third partial one.

With strict:false every PR is tested against the main it branched from, so a
burst of merges lands a COMBINATION no run ever compiled. Measured twice:
`CS0246: MeshOperations` on 2026-08-26, and today a new guard (#2782) plus an
independently-landed gate primitive (#2792) that were each green alone and
red together — five consecutive red main runs and a documentation-only PR
caught in the blast. #2447 and #2767 each changed WHEN you find out; neither
builds the combination before it lands. A merge queue does.

This repo is PUBLIC, so GitHub's merge queue is available on the org's Team
plan (private repos need Enterprise Cloud — MeshWeaver.Plugins carries the
same trigger dormant for exactly that reason, and its own comment says so).
`mergeQueue(branch:"main")` currently resolves to null: available, not enabled.

What this change does:
- `merge_group:` trigger, so the queue's temporary gh-readonly-queue/main/…
  ref gets the required context. Without it an entry queues, times out and
  is ejected with no failing check to point at.
- The two DIFF gates (record signatures, type forwards) run on merge_group
  too, with base resolved to main — github.base_ref is EMPTY on merge_group,
  and a queue entry's base IS main by construction. Resolved inline at all
  four uses so the two gates cannot drift.
- Nothing else changes: concurrency groups the queue ref per entry (a
  superseded entry gets a new ref), the fork gates read null as "not a fork",
  HEAD_SHA falls through to github.sha, and the executed-and-green marker
  stays push-only.

🚨 Order matters: this must be ON MAIN before the `merge_queue` rule is added
to the `main pr protection` ruleset, or the first queued PR sits forever.
Enabling the rule is the next step, done via the ruleset API once this lands.

Verified: YAML parses with triggers [push, pull_request, merge_group,
workflow_dispatch]; MainRunsAreNeverCancelledGuard 11/11 (the concurrency
expressions are untouched).

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants