Skip to content

Commit 2fd714f

Browse files
claude[bot]claude
andauthored
ci: hourly full run on main, push narrows to the affected set, and a filer for a red one (#16991)
* ci: hourly full run on main; push narrows to the affected set - push on main computes the Test Core package set with --affected against github.event.before; a zero or unresolvable sha falls back to the full list, loudly. - schedule '0 * * * *' + workflow_dispatch on ci.yml and lint.yml run the full battery, with their own concurrency group so a merge cannot cancel them. - select-shard-timings-run reads the scheduled run, not the push run. - a new workflow_run filer opens or refreshes one deduplicated card per watched workflow when a scheduled run is red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU * ci: drive the card decision through the tested module fileOrRefreshCard holds the whole create-or-refresh judgement, so the acceptance property (one red run files one card; the next refreshes it and files no second) is driven by the self-test against a mutable board rather than asserted in a workflow comment. The filer keeps only the API callbacks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU * ci(shard-timings): zero candidate runs is NOT MEASURED, not a finding The refresh lane read every non-zero from the run selector as a refusal, so an EMPTY candidate list printed "NO ELIGIBLE RUN among the 0 ... every one was censored, failed, or has lost its artifacts" and failed the job. Nothing had been examined, and none of those three causes had occurred. Split into two legs with two exits and two messages: an empty candidate list exits EXIT_PREREQUISITE_NOT_MET (3, the repo-wide code), candidates that existed and were all rejected keep exit 1 and keep the sentence naming the causes that now apply. The workflow reads 3 as NOT MEASURED, leaves the dataset alone, stays green, and says loudly in its annotation and its step summary that a persistent NOT MEASURED is a defect rather than a steady state. The step's own run block is lifted out of the YAML and driven under bash against a stub node on all three exit codes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent fe2b755 commit 2fd714f

8 files changed

Lines changed: 2215 additions & 77 deletions

.github/workflows/ci.yml

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,55 @@ on:
3838
# branch-protection-required check MUST carry this trigger, or queue builds
3939
# wait forever on a check that never reports.
4040
merge_group:
41+
# ── THE HOURLY FULL RUN (#16467) ─────────────────────────────────────────
42+
# `push` above now computes the Test Core package set with `--affected`
43+
# against `github.event.before`, so a merge no longer re-tests the whole
44+
# workspace it just tested in the queue. Something still has to run the FULL
45+
# battery on `main`, on a clock, or "main is green" stops being a statement
46+
# about the workspace and becomes a statement about the last diff.
47+
#
48+
# This is that run. Minute 0 rather than an offset minute ON PURPOSE, and
49+
# against the sibling convention in test-nightly-tiers.yml: the reading this
50+
# feeds — `scripts/ci/select-shard-timings-run.mjs` — wants one complete run
51+
# per hour far more than it wants a short queue wait, and a run that starts
52+
# a few minutes late is still the hour's run. What it must NOT do is collide
53+
# with the next hour's, which the concurrency group below decides.
54+
#
55+
# ⛔ This is a NEW TRIGGER KEY, not a widening of `push:`. The ⛔ above still
56+
# binds: `push:` stays filtered to `main`.
57+
schedule:
58+
- cron: '0 * * * *'
59+
# The same full battery, on demand: an operator who has just landed a fix for
60+
# a red hourly run should not have to wait up to an hour to see it answered.
61+
# Treated as `full` by the selection script for the same reason `schedule` is.
62+
workflow_dispatch:
4163

4264
# Superseded runs on the same PR/branch waste runners and delay feedback;
4365
# cancel them. Push runs to main group by commit ref as well, so an in-flight
4466
# main run is cancelled only by a newer main push.
67+
#
68+
# ⭐ `github.event_name` IS LOAD-BEARING IN THIS KEY, and it is the whole
69+
# reason the hourly run above can finish (#16467). On a `schedule` event
70+
# `github.event.pull_request.number` is empty and `github.ref` is
71+
# `refs/heads/main` — BYTE-IDENTICAL to what a push to `main` produces. Without
72+
# the event name in the key the hourly run and the next merge share one group,
73+
# `cancel-in-progress: true` applies, and the next merge kills the hourly run.
74+
# That is not a hypothetical: this card's own measurement is that 36 of the
75+
# last 60 push runs on `main` were already cancelled that way, at a merge
76+
# cadence that would censor most hours.
77+
#
78+
# ⛔ Do not "simplify" this back to two segments. `scripts/ci/scheduled-full-
79+
# run.mjs --check-concurrency` evaluates this expression against a push-shaped
80+
# and a schedule-shaped context and reds when the two groups are equal, so the
81+
# collision cannot come back silently.
82+
#
83+
# `cancel-in-progress` stays `true` for every event including `schedule`: two
84+
# hourly runs overlapping means one of them is over an hour old, and an hour-old
85+
# measurement of `main` is strictly the worse of the two. Test Core's wall clock
86+
# is well inside the cadence (~39 shard-minutes across six shards), so this is
87+
# the exceptional path, not the normal one.
4588
concurrency:
46-
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
89+
group: ci-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
4790
cancel-in-progress: true
4891

4992
jobs:
@@ -54,11 +97,13 @@ jobs:
5497
contents: read
5598
pull-requests: read
5699
outputs:
57-
# On merge_group, everything counts as changed: dorny/paths-filter has no
58-
# merge_group support, and the queue build is the last validation before
59-
# main — the one place a skipped job can never be the right answer. A
60-
# skipped step's output is the empty string (falsy), so `|| 'true'`
61-
# supplies the merge-group value without touching PR/push behavior.
100+
# On merge_group — and on the `schedule` / `workflow_dispatch` full runs
101+
# (#16467) — everything counts as changed: dorny/paths-filter resolves a
102+
# diff only on `pull_request` and `push`, and each of these three events
103+
# is a place a skipped job can never be the right answer (the queue build
104+
# is the last validation before main; the hourly run IS the full battery).
105+
# A skipped step's output is the empty string (falsy), so `|| 'true'`
106+
# supplies their value without touching PR/push behavior.
62107
docs: ${{ steps.changes.outputs.docs || 'true' }}
63108
core: ${{ steps.changes.outputs.core || 'true' }}
64109
console: ${{ steps.changes.outputs.console || 'true' }}
@@ -100,9 +145,24 @@ jobs:
100145
- name: Checkout repository
101146
uses: actions/checkout@v7
102147

148+
# ⭐ SPELLED AS AN ALLOW-LIST, not as `!= 'merge_group'` (#16467). This
149+
# action reads a diff, and the only two events that hand it one it can
150+
# resolve unaided are `pull_request` (base vs head) and `push`
151+
# (`event.before` vs `after`). `merge_group` was the first event with
152+
# neither, and the exclusion was written as its name; `schedule` and
153+
# `workflow_dispatch` are the second and third, and under the old
154+
# spelling they would have RUN the action with no resolvable base.
155+
#
156+
# That failure direction is the dangerous one: every filter output would
157+
# come back `false`, `|| 'true'` supplies nothing (the output exists and
158+
# says `false`), and every downstream `!= 'false'` guard SKIPS — so the
159+
# hourly full run would have been an entirely green, entirely empty run
160+
# of nothing at all. An allow-list makes each new event fail toward
161+
# THE FILTER CONTRACT's half 1 instead: the step is skipped, its outputs
162+
# are the empty string, `|| 'true'` fills them in, and everything runs.
103163
- uses: dorny/paths-filter@v4
104164
id: changes
105-
if: github.event_name != 'merge_group'
165+
if: github.event_name == 'pull_request' || github.event_name == 'push'
106166
with:
107167
filters: |
108168
docs:
@@ -473,13 +533,21 @@ jobs:
473533
# on this shard" exit below, every shard still attests,
474534
# and Test Core is an honest green. It is NOT the #10057
475535
# case, which is pull_request-only (the script says why).
476-
# push unchanged: the FULL list.
536+
# push affected set against `github.event.before`, the commit
537+
# `main` was on before this merge landed (#16467). A zero
538+
# sha -- a first push, or a force-push that rewrote
539+
# history -- falls back to the FULL list, loudly.
540+
# schedule the FULL list: the hourly run IS the full battery, and
541+
# workflow_ the on-demand rerun of it. Neither carries a diff base,
542+
# dispatch and neither should: they are the run that says whether
543+
# the whole workspace is green on `main`.
477544
- name: Compute this shard's package set
478545
env:
479546
OS_SHARD_EVENT_NAME: ${{ github.event_name }}
480547
OS_SHARD_PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
481548
OS_SHARD_PR_PINNED_BASE_SHA: ${{ github.event.pull_request.base.sha }}
482549
OS_SHARD_MERGE_GROUP_BASE_SHA: ${{ github.event.merge_group.base_sha }}
550+
OS_SHARD_PUSH_BEFORE_SHA: ${{ github.event.before }}
483551
run: |
484552
bash scripts/ci/select-shard-packages.sh
485553
node scripts/partition-test-shards.mjs "$RUNNER_TEMP/turbo-ls.json" \

.github/workflows/lint.yml

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,24 @@ on:
2525
# queue builds or the queue stalls. This workflow has no PR-only steps, so
2626
# the trigger alone is enough.
2727
merge_group:
28+
# ── THE HOURLY FULL RUN (#16467), the half this file owns ────────────────
29+
# The card's ruling is that the hourly run covers BOTH required-context
30+
# files, not just ci.yml, and the reason is measured rather than symmetric:
31+
# the push-on-`main` run of THIS workflow is the only post-merge full-battery
32+
# run of the families `scripts/ci/select-gate-families.sh` scopes away on
33+
# merge groups (the PM dispatch-gates self-test, both ratchets, the
34+
# verify-lock self-test, the comment-mask corpus). A scoped family that goes
35+
# red on `main` after a queue build skipped it had, until this trigger, no
36+
# run that would notice and no filer that would say so.
37+
#
38+
# The selector already treats every event that is neither `merge_group` nor
39+
# `pull_request` as "run every family", so this trigger alone restores the
40+
# full battery here — no step in this file changes.
41+
#
42+
# ⛔ A NEW TRIGGER KEY, not a widening of `push:`; see ci.yml's `on:` block.
43+
schedule:
44+
- cron: '0 * * * *'
45+
workflow_dispatch:
2846

2947
# ── MEASURED 2026-08-25 (#12211) — a NEGATIVE result, recorded so it is not
3048
# re-measured. After the stale-ledger outage the queue's check set was measured
@@ -68,8 +86,14 @@ on:
6886
# Same policy as ci.yml: superseded runs on the same PR/branch waste runners
6987
# and delay feedback; cancel them. Push runs to main group by commit ref, so an
7088
# in-flight main run is cancelled only by a newer main push.
89+
#
90+
# ⭐ `github.event_name` is in the key for the reason ci.yml's concurrency block
91+
# states in full (#16467): on a `schedule` event the remaining two segments are
92+
# byte-identical to a push to `main`, so without it the next merge cancels the
93+
# hourly full run. `scripts/ci/scheduled-full-run.mjs --check-concurrency`
94+
# evaluates THIS expression too and reds if the two groups ever collapse again.
7195
concurrency:
72-
group: lint-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
96+
group: lint-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
7397
cancel-in-progress: true
7498

7599
# ─────────────────────────────────────────────────────────────────────────────
@@ -3402,6 +3426,35 @@ jobs:
34023426
- name: Test Core package selection self-test
34033427
run: pnpm check:select-shard-packages
34043428

3429+
# The hourly full run's own contract (#16467). Two halves, one script:
3430+
#
3431+
# --self-test the card's IDENTITY, DE-DUP and BODY, driven
3432+
# offline. The filer is a `workflow_run` workflow, so
3433+
# it can only ever run from the default branch and
3434+
# NOTHING on a pull request can exercise it — the
3435+
# same position merge-queue-triage.yml is in. A
3436+
# de-dup rule tested only by the live workflow gets
3437+
# its first real test on the night it files its
3438+
# second duplicate, and this one fires hourly.
3439+
# --check-concurrency
3440+
# evaluates ci.yml's and THIS file's
3441+
# `concurrency.group` expressions against a
3442+
# push-shaped and a schedule-shaped context and reds
3443+
# when the two collapse onto one group. They did
3444+
# until #16467: on a `schedule` event the other two
3445+
# segments are byte-identical to a push to `main`, so
3446+
# `cancel-in-progress` let the next merge kill the
3447+
# hourly run. It also refuses the two degenerate ways
3448+
# to make them differ — a run-unique key, and a
3449+
# constant one — since both switch cancellation off
3450+
# or on for every event in the file.
3451+
#
3452+
# Offline, no network, ~0.1 s.
3453+
- name: Hourly full run self-test and concurrency gate
3454+
run: |
3455+
node scripts/ci/scheduled-full-run.mjs --self-test
3456+
node scripts/ci/scheduled-full-run.mjs --check-concurrency
3457+
34053458
# Gate-family selection self-test (#16496). The "Select the gate families
34063459
# this run pays for" step at the top of this job is a thin call into
34073460
# scripts/ci/select-gate-families.sh, and its `merge_group` branch cannot

0 commit comments

Comments
 (0)