Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


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

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

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

# ⭐ SPELLED AS AN ALLOW-LIST, not as `!= 'merge_group'` (#16467). This
# action reads a diff, and the only two events that hand it one it can
# resolve unaided are `pull_request` (base vs head) and `push`
# (`event.before` vs `after`). `merge_group` was the first event with
# neither, and the exclusion was written as its name; `schedule` and
# `workflow_dispatch` are the second and third, and under the old
# spelling they would have RUN the action with no resolvable base.
#
# That failure direction is the dangerous one: every filter output would
# come back `false`, `|| 'true'` supplies nothing (the output exists and
# says `false`), and every downstream `!= 'false'` guard SKIPS — so the
# hourly full run would have been an entirely green, entirely empty run
# of nothing at all. An allow-list makes each new event fail toward
# THE FILTER CONTRACT's half 1 instead: the step is skipped, its outputs
# are the empty string, `|| 'true'` fills them in, and everything runs.
- uses: dorny/paths-filter@v4
id: changes
if: github.event_name != 'merge_group'
if: github.event_name == 'pull_request' || github.event_name == 'push'
with:
filters: |
docs:
Expand Down Expand Up @@ -473,13 +533,21 @@ jobs:
# on this shard" exit below, every shard still attests,
# and Test Core is an honest green. It is NOT the #10057
# case, which is pull_request-only (the script says why).
# push unchanged: the FULL list.
# push affected set against `github.event.before`, the commit
# `main` was on before this merge landed (#16467). A zero
# sha -- a first push, or a force-push that rewrote
# history -- falls back to the FULL list, loudly.
# schedule the FULL list: the hourly run IS the full battery, and
# workflow_ the on-demand rerun of it. Neither carries a diff base,
# dispatch and neither should: they are the run that says whether
# the whole workspace is green on `main`.
- name: Compute this shard's package set
env:
OS_SHARD_EVENT_NAME: ${{ github.event_name }}
OS_SHARD_PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
OS_SHARD_PR_PINNED_BASE_SHA: ${{ github.event.pull_request.base.sha }}
OS_SHARD_MERGE_GROUP_BASE_SHA: ${{ github.event.merge_group.base_sha }}
OS_SHARD_PUSH_BEFORE_SHA: ${{ github.event.before }}
run: |
bash scripts/ci/select-shard-packages.sh
node scripts/partition-test-shards.mjs "$RUNNER_TEMP/turbo-ls.json" \
Expand Down
55 changes: 54 additions & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,24 @@ on:
# queue builds or the queue stalls. This workflow has no PR-only steps, so
# the trigger alone is enough.
merge_group:
# ── THE HOURLY FULL RUN (#16467), the half this file owns ────────────────
# The card's ruling is that the hourly run covers BOTH required-context
# files, not just ci.yml, and the reason is measured rather than symmetric:
# the push-on-`main` run of THIS workflow is the only post-merge full-battery
# run of the families `scripts/ci/select-gate-families.sh` scopes away on
# merge groups (the PM dispatch-gates self-test, both ratchets, the
# verify-lock self-test, the comment-mask corpus). A scoped family that goes
# red on `main` after a queue build skipped it had, until this trigger, no
# run that would notice and no filer that would say so.
#
# The selector already treats every event that is neither `merge_group` nor
# `pull_request` as "run every family", so this trigger alone restores the
# full battery here — no step in this file changes.
#
# ⛔ A NEW TRIGGER KEY, not a widening of `push:`; see ci.yml's `on:` block.
schedule:
- cron: '0 * * * *'
workflow_dispatch:

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

# ─────────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -3402,6 +3426,35 @@ jobs:
- name: Test Core package selection self-test
run: pnpm check:select-shard-packages

# The hourly full run's own contract (#16467). Two halves, one script:
#
# --self-test the card's IDENTITY, DE-DUP and BODY, driven
# offline. The filer is a `workflow_run` workflow, so
# it can only ever run from the default branch and
# NOTHING on a pull request can exercise it — the
# same position merge-queue-triage.yml is in. A
# de-dup rule tested only by the live workflow gets
# its first real test on the night it files its
# second duplicate, and this one fires hourly.
# --check-concurrency
# evaluates ci.yml's and THIS file's
# `concurrency.group` expressions against a
# push-shaped and a schedule-shaped context and reds
# when the two collapse onto one group. They did
# until #16467: on a `schedule` event the other two
# segments are byte-identical to a push to `main`, so
# `cancel-in-progress` let the next merge kill the
# hourly run. It also refuses the two degenerate ways
# to make them differ — a run-unique key, and a
# constant one — since both switch cancellation off
# or on for every event in the file.
#
# Offline, no network, ~0.1 s.
- name: Hourly full run self-test and concurrency gate
run: |
node scripts/ci/scheduled-full-run.mjs --self-test
node scripts/ci/scheduled-full-run.mjs --check-concurrency

# Gate-family selection self-test (#16496). The "Select the gate families
# this run pays for" step at the top of this job is a thin call into
# scripts/ci/select-gate-families.sh, and its `merge_group` branch cannot
Expand Down
Loading
Loading