When Two Workflows Share a Name — The `${{ github.workflow }}` Concurrency Trap (W26)
Every mixed-content PR (touching both FitTracker source AND docs) produced two Build and Test status checks — one CANCELLED, one SUCCESS — blocking merge even with admin override. Diagnosed 2026-06-01 as W26 in the observed-patterns catalog. Root cause both ci.yml and ci-docs-skip.yml declared name CI, so their concurrency.group expressions using ${{ github.workflow }} resolved identically (ci-CI-refs/pull/N/merge) and the two files raced into mutual cancellation. Fix file-specific hardcoded group prefixes (ci-yml- and ci-docs-skip-). Now both run independently. Side benefit uncovered W28 (CoreSimulator out-of-date local env-flake) in the same session — Mac restart blocked on HADF Sub-exp 2 closure.
What do T1 / T2 / T3 mean?▾
W26 — The ${{ github.workflow }} Concurrency Trap
The symptom
PR #560 (C2 readiness-aware-training-alert) sat with:
Build and Test | CANCELLED | CI
Build and Test | SUCCESS | CI
mergeable=MERGEABLE state=BLOCKEDTwo checks with the same name. One green, one cancelled. The cancelled one was sufficient to block branch-protection merge even with --admin.
This was the second instance in the session. The first hit was the dispatcher pattern's own validation — PR #560's CI run on the W26 fix commit also produced a CANCELLED status before the fix landed on main.
The root cause
GitHub's workflow concurrency group expression evaluates ${{ github.workflow }} to the workflow name, not the file. Both files in the dispatcher pair declare name: CI:
# Before fix — ci.yml
name: CI
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }} # → "ci-CI-refs/pull/N/merge"
cancel-in-progress: true
# Before fix — ci-docs-skip.yml
name: CI
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }} # → "ci-CI-refs/pull/N/merge" — IDENTICAL
cancel-in-progress: trueOn every mixed-content PR (touching both FitTracker/** AND docs/** or .claude/**):
ci.ymlfires (paths includeFitTracker/**)ci-docs-skip.ymlALSO fires (paths-ignore not satisfied —.claude/**is outside the ignore list)- Both enter the same concurrency group
- The second arrival cancels the first per
cancel-in-progress: true - The cancelled status persists in the PR's required-status-check rollup
- Branch protection refuses to merge
The (4-line) fix
Replace ${{ github.workflow }} with file-specific hardcoded prefixes:
# ci.yml
concurrency:
group: ci-yml-${{ github.ref }}
cancel-in-progress: true
# ci-docs-skip.yml
concurrency:
group: ci-docs-skip-${{ github.ref }}
cancel-in-progress: trueEach workflow keeps its own intra-workflow cancellation behavior (a new push cancels the prior run of THAT workflow on the same branch) without colliding with the sibling. Mixed PRs continue to fire both workflows; both now run to completion; branch protection accepts both Build and Test SUCCESS statuses.
Trade-off acknowledged: mixed PRs cost an extra ~5 seconds of ubuntu-latest runner time per PR. The full mutual-exclusivity fix (dorny/paths-filter job-level skip) is a separate, larger PR — deferred unless ubuntu cost becomes a real concern.
What the original 2026-05-31 W26 placeholder got wrong
The placeholder note in the 2026-05-31 carryover memory attributed the cause to cancel-in-progress: true alone. That was the symptom. The actual root cause is the cross-workflow group collision. cancel-in-progress: true per workflow is correct + desirable (a new push to the same branch should cancel the prior run of the same workflow). Only the cross-workflow group sharing was wrong.
The W28 side discovery
While diagnosing W26, every local xcodebuild invocation produced:
CoreSimulator is out of date. Current version (1051.50.0) is older than build version (1051.54.0).
xcodebuild: error: Unable to find a destination matching ...
iOS 26.5 is not installed.Cataloged as W28 — operator-side CoreSimulator.framework daemon binary out of date (loaded at boot before Xcode bumped the on-disk version). Fix is a full Mac restart, blocked on HADF Sub-exp 2 closure (Sub-exp 2 dispatch state doesn't survive reboot). Until then: swiftc -parse for syntax validation + xcodebuild -list for pbxproj integrity + delegate the real build to CI.
Sibling patterns
- W17 (slot 36) — "Stale-base branch trap." Same "trust the explicit diagnostic, don't paper over with workarounds" class.
- W28 — operator-side macOS daemon class. Both W17 + W26 + W28 reinforce that fresh-process state is the reliable recovery path on macOS-side tooling drifts.
- The 2026-05-31 EOD session-close memory's W26 placeholder — corrected by this case study to identify the cross-workflow group sharing as the actual root cause.
Cross-references
- Catalog entry —
.claude/integrity/observed-patterns.mdW26 - W28 sibling — same catalog, env-flake entry added in PR #565
- Sibling case study slot 36 (
stale-base-branch-trap-w17.mdx) - v7.9 promotion context — slot 34 (
framework-v7-9-promotion.mdx)
- Fix introduces NEW redundant CI runs costing >5 min per mixed PR
- Branch protection breaks because the two workflows produce two `Build and Test` checks (would imply per-name match isn't N-of-1 — branch protection ACCEPTS multiple successes by same name)