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
68 changes: 45 additions & 23 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,26 +114,43 @@ concurrency:
cancel-in-progress: true

# ── THE TWO SPEEDS (#22998) ──────────────────────────────────────────────────
# A `pull_request` or `merge_group` run carries the FAST SET: `Build Core`,
# `Test Core` (affected-only) and `Build Docs`. `main`'s 4-hourly `schedule` run,
# its `push` runs and an on-demand `workflow_dispatch` carry the FULL BATTERY:
# the fast set plus the four jobs below that spell
# A `pull_request` run carries the FAST SET: `Build Core`, `Test Core`
# (affected-only) and `Build Docs`. A `merge_group` run carries the fast set
# plus the three `dogfood` shards (#30461, ruling record 6663883327, letter B),
# whose job alone spells
#
# github.event_name != 'pull_request'
#
# in its `if:`, so a dogfood red shows on the queue build of the entry that
# carries it rather than first on `main`'s push run after it. It is advisory
# there: the context is outside the required set (below), so the queue may
# merge the entry before or despite the shards.
# `main`'s 4-hourly `schedule` run, its `push` runs and an on-demand
# `workflow_dispatch` carry the FULL BATTERY: all of that plus the three jobs
# below that spell
#
# github.event_name != 'pull_request' && github.event_name != 'merge_group'
#
# in their `if:` — `temporal-conformance`, `dogfood` (three shards),
# `dogfood-verify` and `ui-published-artifacts`. Nothing is deleted: the jobs
# moved off the per-PR path, the last one never had one (#26717). A red in one of them is a main-red for the owning lane, and the
# publish lane reads the full battery before its approval (release.yml's
# `full-battery` job, `scripts/ci/scheduled-full-run.mjs --release-gate`).
# in their `if:` — `temporal-conformance`, `dogfood-verify` and
# `ui-published-artifacts`. Nothing is deleted: the jobs moved off the per-PR
# path, the last one never had one (#26717). A red in one of them is a main-red
# for the owning lane, and the publish lane reads the full battery before its
# approval (release.yml's `full-battery` job,
# `scripts/ci/scheduled-full-run.mjs --release-gate`). ⛔ No affected-only
# dogfood filter on the queue: the shards run on every entry, through the
# `core` output, which is 'true' on `merge_group` (letter C was not ruled).
# The queue enforces only the ruleset's required set, and `Dogfood Regression
# Gate` is outside it since #22998 (check:required-contexts' tombstone); this
# file does not decide whether it rejoins.
#
# Spelled as a DENY-list of the two per-PR events on purpose — the filter
# Spelled as a DENY-list of the per-PR events on purpose — the filter
# contract's half 1 below, applied to events: a trigger added to `on:` later
# RUNS these jobs, and the failure direction of a wrong guess is a slower PR,
# never a full battery that silently stopped running. `dogfood-gate` keeps
# `if: always()` (check:shard-attestation pins it): with both legs skipped it
# counts zero expected attestations and concludes success, exactly as on a
# filter-skipped diff.
# `if: always()` (check:shard-attestation pins it): with both legs skipped (a
# pull request) it counts zero expected attestations and concludes success,
# exactly as on a filter-skipped diff; on a queue build it counts the three
# shards' credentials and `dogfood-verify`'s skip as zero expected.

jobs:
filter:
Expand All @@ -158,8 +175,9 @@ jobs:
# 'true'` did.
docs: ${{ github.event_name != 'merge_group' && steps.changes.outputs.docs || 'true' }}
# Read by `test-plan`, `test` (ORed with `crosspkg`) and `build-core`,
# and on push / schedule by `dogfood`, `dogfood-verify` and
# `temporal-conformance` — see the `core` filter below and each job's `if:`.
# on merge_group / push / schedule by `dogfood`, and on push / schedule
# by `dogfood-verify` and `temporal-conformance` — see the `core` filter
# below and each job's `if:`.
core: ${{ github.event_name != 'merge_group' && steps.changes.outputs.core || 'true' }}
# Gates the `test` job ONLY, ORed with `core` — see the `crosspkg`
# filter below and the `test` job's `if:` (#9829, #10015).
Expand Down Expand Up @@ -2390,9 +2408,11 @@ jobs:
# below — the shard count can change without touching it.
name: Dogfood Regression Gate (${{ matrix.shard }}/3)
needs: filter
# See THE FILTER CONTRACT on the filter job's outputs (#4928). Full battery
# only: see THE TWO SPEEDS above `jobs:` (#22998).
if: ${{ !cancelled() && needs.filter.outputs.core != 'false' && github.event_name != 'pull_request' && github.event_name != 'merge_group' }}
# See THE FILTER CONTRACT on the filter job's outputs (#4928). The queue
# build and the full battery, never a pull request: see THE TWO SPEEDS
# above `jobs:` (#22998; `merge_group` left this deny-list by #30461,
# ruling record 6663883327). `dogfood-verify` below keeps both events.
if: ${{ !cancelled() && needs.filter.outputs.core != 'false' && github.event_name != 'pull_request' }}
runs-on: ubuntu-latest
# Backstop only — the stall guard on the test step is the primary detector
# for a #4250-style hang (see Test Core). 30 min is ~6× a shard (~5 min;
Expand Down Expand Up @@ -2704,11 +2724,13 @@ jobs:
overwrite: true

dogfood-gate:
# Stable check name for a SHARDED job (#3622 follow-up). Since #22998 its
# legs run in the full battery only (THE TWO SPEEDS above `jobs:`) and the
# context leaves the required set; on a PR or queue build both legs skip
# and this gate counts zero expected attestations. The history below is
# why the name and the `if: always()` stay.
# Stable check name for a SHARDED job (#3622 follow-up). Since #22998 the
# context is outside the required set (THE TWO SPEEDS above `jobs:`). On a
# PR both legs skip and this gate counts zero expected attestations; on a
# queue build (since #30461) the three dogfood shards run and are counted,
# while `dogfood-verify` skips and is judged with zero expected; the full
# battery runs and counts both. The history below is why the name and the
# `if: always()` stay.
#
# Branch protection required the context "Dogfood Regression Gate". Once
# the job became a 2-way matrix its checks publish as "Dogfood Regression
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1496,8 +1496,9 @@ jobs:
# ══════════════════════════════════════════════════════════════════════════
full-battery:
name: Full battery green at the version commit
# A PR or queue build carries ci.yml's FAST SET since #22998; the dogfood
# and temporal legs run only on main's push, 4-hourly schedule and
# A PR or queue build carries ci.yml's FAST SET since #22998 (a queue
# build adds the three dogfood shards since #30461); `dogfood-verify` and
# the temporal leg run only on main's push, 4-hourly schedule and
# dispatch runs. So "tested before a release" is read HERE, as a gate: the
# newest completed `schedule` or `push` runs of `CI` and `Lint & Type
# Check` whose head is the version commit or a first-parent ancestor of it
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/upgrade-harness-optin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
# its data" — a check each of the line's PRs passes BEFORE it merges. The
# harness that answers R2 is a dogfood test,
# `packages/qa/dogfood/test/upgrade-18x-harness.dogfood.test.ts`, and the
# dogfood jobs in `ci.yml` run only on `main`'s full battery (THE TWO SPEEDS,
# #22998): they skip `pull_request` and `merge_group`. So without this workflow
# the line's PRs get their first R2 answer after the merge.
# dogfood shards in `ci.yml` skip `pull_request` (THE TWO SPEEDS, #22998; they
# run on `merge_group` since #30461). So without this workflow the line's PRs
# get their first R2 answer in the merge queue, after the PR is enqueued.
#
# This workflow adds the missing TRIGGER for that ONE file, and nothing else.
# The test, its fixture and its assertions stay where they are, and the full
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -510,8 +510,8 @@ Even inside your own worktree, operate defensively:
`Build & E2E UI` and `Live E2E UI`; a check outside those ten is advisory
([why that matters](docs/agents/multi-agent-discipline.md#advisory-checks-ride-through)). A required context is
matched by check-run name, so a rename detaches its gate silently — treat those ten names as contract.
**Two speeds** — PR and queue runs carry the fast set; `main`'s scheduled and push runs carry the full
battery ([detail](docs/agents/multi-agent-discipline.md#two-speeds-which-runs-carry-which-battery)).
**Two speeds** — PR runs carry the fast set, queue runs add the dogfood shards; `main`'s scheduled and push runs
carry the full battery ([detail](docs/agents/multi-agent-discipline.md#two-speeds-which-runs-carry-which-battery)).
**Main-red:** a full-battery red is its lane's first work — triage files it under
the main-red convention, the lane's next PR fixes it, and ⛔ no lane PR arms the queue meanwhile.

Expand Down
11 changes: 7 additions & 4 deletions docs/agents/multi-agent-discipline.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,13 @@ A check outside those ten is advisory and rides through, and an advisory red

← [`AGENTS.md` › Multi-agent working discipline](../../AGENTS.md#multi-agent-working-discipline)

**Two speeds** (ruling record 6120820653): PR and queue runs carry the fast set, 10 minutes of
wall per queue entry; `main`'s scheduled (4-hourly) and push runs carry the full battery — dogfood, e2e,
`Temporal Conformance (live PG + MySQL)` — read by the publish lane before
any approval.
**Two speeds** (ruling record 6120820653): PR and queue runs carry the fast set, about 20 minutes
of wall per queue entry (median, 2026-11-05); `main`'s scheduled (4-hourly) and push runs
carry the full battery — dogfood, e2e, `Temporal Conformance (live PG + MySQL)` — read by the publish lane before
any approval. Queue runs also carry the three `Dogfood Regression Gate` shards (ruling record
6663883327), so a dogfood red shows on the queue build, advisory only; `Dogfood Verify CLI`,
Temporal Conformance and `UI Published Artifacts` stay full-battery only. The `Dogfood Regression
Gate` context is outside the required set, so the queue does not hold an entry on it.

## Re-arm awareness

Expand Down
9 changes: 8 additions & 1 deletion scripts/check-required-contexts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,14 @@ const UNATTRIBUTED_BATTERY = '(no battery open)';
* RETIRED_CONTEXT_NAMES row, for the reason the `Build Docs` / `Console Pin
* Gate` tombstone above gives: neither name is dead — both jobs still publish
* these check-runs on every full-battery run, and the instruction files name
* them correctly as full-battery jobs.
* where each runs.
* Since #30461 (ruling record 6663883327, letter B) the three dogfood shards
* also run on `merge_group`, so a queue build publishes `Dogfood Regression
* Gate` over their credentials; `Temporal Conformance` stays full-battery
* only. The context stays out of this registry and of the ruleset for now: the
* follow-up ruling on #30461 (A, in two steps, 6669982756) puts it back only
* after a clean 24-hour reading of the queue builds, the row here landing
* first and the ruleset edit after it.
*/
export const REQUIRED_CONTEXTS = [
{
Expand Down
5 changes: 3 additions & 2 deletions scripts/ci/scheduled-full-run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -798,8 +798,9 @@ export function renderBody({
// ---------------------------------------------------------------------------
//
// A PR or queue build carries the FAST SET since #22998 (ci.yml's THE TWO
// SPEEDS); the dogfood and temporal legs run only on `main`'s
// push, 4-hourly schedule and dispatch runs. "Tested before a release" is
// SPEEDS; a queue build adds the three dogfood shards since #30461);
// `dogfood-verify` and the temporal leg run only on `main`'s push, 4-hourly
// schedule and dispatch runs. "Tested before a release" is
// therefore a statement about those runs, and this half makes it a gate:
// release.yml's `full-battery` job, which `publish` needs, so a refusal here
// means no deployment is queued at the `release` environment at all.
Expand Down
17 changes: 15 additions & 2 deletions scripts/pm/check-expected-skips.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,14 @@ function gatedReason(workflow, wf, gate, matrix) {
}
if (t.kind === 'pr-event') {
const where = [t.events.includes('pull_request') && 'every PR head', t.events.includes('merge_group') && 'every queue build'].filter(Boolean).join(' and ');
return `full battery only: its \`if:\` excludes ${tick(t.events)}, so it skips on ${where} and is answered by main's push and scheduled runs`;
// "Full battery only" is said only when BOTH per-PR events are excluded:
// since #30461 ci.yml's `dogfood` shards exclude `pull_request` alone and
// run on every queue build, so the queue build answers them first.
if (PR_EVENTS.every((e) => t.events.includes(e))) {
return `full battery only: its \`if:\` excludes ${tick(t.events)}, so it skips on ${where} and is answered by main's push and scheduled runs`;
}
const answered = t.events.includes('pull_request') ? "the merge-queue build and main's push and scheduled runs" : "every PR head and main's push and scheduled runs";
return `its \`if:\` excludes ${tick(t.events)} alone, so it skips on ${where} and is answered by ${answered}`;
}
if (t.kind === 'event-action') {
return `its \`if:\` excludes the ${tick(t.actions)} pull_request event(s); each such event is its own run on the same head, beside the run that concluded`;
Expand Down Expand Up @@ -1284,6 +1291,8 @@ function fixtureRun(name, conclusion, suite = 1, extra = {}) {
const FILTER_WIDENED = (o) => `\${{ github.event_name != 'merge_group' && steps.changes.outputs.${o} || 'true' }}`;
const CORE_IF = "${{ !cancelled() && needs.filter.outputs.core != 'false' }}";
const FULL_BATTERY_IF = "${{ !cancelled() && needs.filter.outputs.core != 'false' && github.event_name != 'pull_request' && github.event_name != 'merge_group' }}";
// ci.yml's `dogfood` shards since #30461: the queue build runs them, a PR head skips them.
const QUEUE_AND_FULL_IF = "${{ !cancelled() && needs.filter.outputs.core != 'false' && github.event_name != 'pull_request' }}";
const EVENT_ACTION_IF = "github.event.action != 'labeled' && github.event.action != 'unlabeled' && github.event.action != 'edited'";
const optInIf = (label) => `contains(github.event.pull_request.labels.*.name, '${label}') && (github.event.action != 'labeled' || github.event.label.name == '${label}')`;
const FIXTURE_TREE = Object.freeze({
Expand All @@ -1307,7 +1316,7 @@ const FIXTURE_TREE = Object.freeze({
},
'test-gate': { name: 'Test Core', needs: ['test', 'filter'], if: 'always()' },
'temporal-conformance': { name: 'Temporal Conformance (live PG + MySQL)', needs: 'filter', if: FULL_BATTERY_IF },
dogfood: { name: 'Dogfood Regression Gate (${{ matrix.shard }}/3)', needs: 'filter', if: FULL_BATTERY_IF, strategy: { matrix: { shard: [1, 2, 3] } } },
dogfood: { name: 'Dogfood Regression Gate (${{ matrix.shard }}/3)', needs: 'filter', if: QUEUE_AND_FULL_IF, strategy: { matrix: { shard: [1, 2, 3] } } },
'dogfood-verify': { name: 'Dogfood Verify CLI', needs: 'filter', if: FULL_BATTERY_IF },
'dogfood-gate': { name: 'Dogfood Regression Gate', needs: ['dogfood', 'dogfood-verify', 'filter'], if: 'always()' },
'build-core': { name: 'Build Core', needs: 'filter', if: CORE_IF },
Expand Down Expand Up @@ -1531,6 +1540,7 @@ export function selfTest() {
t('jobGate: …nor always(), an absent if:, a non-string, or two ${{ }} blocks', [jg('always()'), jg(undefined), jg(true), jg("${{ needs.filter.outputs.a != 'false' }} && ${{ needs.filter.outputs.b != 'false' }}")].every((x) => x === null), true);
t('jobGate: …nor an if: whose parentheses do not balance', jg("!cancelled() && (needs.filter.outputs.core != 'false'"), null);
t('spelling: a gate renders in one short line', gateTermsSpelling(jg(FULL_BATTERY_IF).terms), 'filter core + pr-event pull_request|merge_group');
t('jobGate: the queue-and-full shape (#30461) is one pr-event term excluding pull_request alone', JSON.stringify(jg(QUEUE_AND_FULL_IF)?.terms.find((x) => x.kind === 'pr-event')?.events), JSON.stringify(['pull_request']));

// ---- the derivation, on the frozen fixture tree ----------------------------
const fx = fixtureRoster();
Expand All @@ -1556,6 +1566,9 @@ export function selfTest() {
t("reason: a queue-widened output says the merge-queue build runs it", fxRow('Build Core')?.reason.includes("merge-queue build widens it to 'true'"), true);
t("reason: a ui output says the queue reads its own diff", fxRow('Build UI Deps')?.reason.includes("against the queue's base"), true);
t('reason: a full-battery job says so', fxRow('Dogfood Verify CLI')?.reason.includes('full battery only'), true);
const queueRow = fxRow('Dogfood Regression Gate (${{ matrix.shard }}/3)');
t('reason: a job excluding pull_request alone is NOT called full battery only (#30461)', queueRow?.reason.includes('full battery only'), false);
t('reason: …it says only PR heads skip it and the merge-queue build answers it', queueRow?.reason.includes('skips on every PR head and is answered by the merge-queue build') && !queueRow?.reason.includes('every queue build'), true);
t('reason: an opt-out label names its label', fxRow('Check Changeset')?.reason.includes('`skip-changeset` label skips it'), true);
t('reason: a matrix job names its raw template', fxRow('Test Core (${{ matrix.shard }}/6)')?.reason.includes('raw `name:` template'), true);
t('reason: unread conjuncts are counted, never hidden', fxRow('Build UI Deps')?.reason.includes('1 further condition(s)'), true);
Expand Down
Loading
Loading