Skip to content

Commit 7aa82a3

Browse files
ci(gates): every Lint and Type Check gate runs after a red and each lane ends in one verdict step; Test Core continues past a red package (#23212)
Fixes #23191 Clause-②: no **Governed surface, Tier H** (`.github/workflows/**`, and `AGENTS.md`): this PR stays a draft until the maintainer approves it. No seat readies, queues or arms it. ## What the ruling binds (verbatim, #23177 ruling B, comment 6139629628, item 1) > **A1, as the card wrote it, in this form:** every gate step of a `Lint · *` lane and of `Type Check · *` keeps running after a red (`continue-on-error: true` on the gate steps only; setup, install and build steps that later steps depend on stay fail-fast), and each lane ends with one verdict step that reads every gate's conclusion, prints the names of the red ones and exits 1 — the job's conclusion is still red, never silent. Test Core runs `turbo run test … --continue=dependencies-successful` so one package's red does not stop the others; its verdict stays red. The #13690 ordering text (`lint.yml` 151–159) and the "⛔ not continue-on-error" note (502–503) are rewritten in the same PR to say what now holds: the order no longer shortens a red run, the verdict step is what keeps the red. Governed surface: draft until the maintainer approves. ## What changes ### `.github/workflows/lint.yml` - **240 gate steps** across the 8 `Lint · *` lanes and the 4 `Type Check · *` lanes now carry `id:` and `continue-on-error: true` (eslint 2, repo & tooling 60, docs & code 55, CI/release/engine 58, scoped erasure 2, scoped slot/corpus 3, scoped tooling 3, PM dispatch-gates 1, source gates 33, workspace 2, debt ledger 2, consumer gates 19). Nothing else about any of them moved: base and head were parsed and every step compared with those two keys removed, and all 240 are identical (same `run:`, `if:`, `env:`, order and comment). - **Fail-fast, untouched:** checkout, Node and pnpm setup, the store path, the pnpm cache, the Turbo cache restore, the install, the gate-family selector (`id: gate-families`), the three workspace builds, and `Save Turbo cache (main only)`. - **Every lane ends with `Lane verdict`**: `if: ${{ !cancelled() }}`, env `OS_LANE_STEPS: ${{ toJSON(steps) }}` and `OS_LANE_JOB_STATUS: ${{ job.status }}`, `run: node scripts/ci/lane-verdict.mjs`. The two aggregates (`Lint & Repo Gates`, `TypeScript Type Check`) are unchanged: they read each lane's `result`, and a lane with a red gate still concludes `failure` because its verdict step exits 1. - **The unmeasured-tail reporter pair is replaced by the verdict** in the 8 Lint lanes, and with it each lane's `actions: read` scope (it bought only the reporter's jobs-API read). Behind a red gate the tail is now empty by construction; behind a failed setup step the verdict lists the gates that never ran, offline, from the `steps` context. - **One new gate step**, `Lane verdict self-test`, in `lint-ci-engine` beside `Aggregator roster gate`: the instrument's own self-test, the one place the outcome-versus-conclusion read can go red (on a green tree both readings agree). - **The two notes the ruling names are rewritten.** The #13690 `GATE ORDER IS DELIBERATE` note (the ruling's 151–159) is now `EVERY GATE RUNS, AND ONE VERDICT STEP KEEPS THE RED`: what is fail-fast and why, why `continue-on-error` alone would trade the red for silence, what the verdict reads, what a new gate step needs, the one gate that feeds another (`check:authorable-surface` writes the tree `check:docs` reads; the second refuses loudly on a missing tree), and that the order no longer shortens a red run. The `⛔ Nor is it continue-on-error` note (502–503) stood in the reporter pair's block in `lint-eslint`; that block is gone, and the verdict step at the same site carries the rewritten note. The `lint` aggregate's header and the `lint-repo-tooling` / `lint-eslint` headers that repeated "stops at its first red" are corrected too. ### `scripts/ci/lane-verdict.mjs` (new; its header is the authority) - Reads the lane's **roster from the workflow file** (`GITHUB_WORKFLOW_REF` names it, `GITHUB_JOB` the job): every step whose `continue-on-error` is set. The `steps` context holds only steps with an `id:`, so a roster taken from the context could never see a gate added without one; read from the file, such a gate is **UNREAD and red**, naming it. - Reads each gate's **`outcome`, never its `conclusion`**: under the flag a failed step's conclusion is `success`. - `skipped` is two facts, told apart by what is certain: a gate with its own `if:` in a job still `success` was turned off by that condition (the scoped families); any other `skipped` is "did not run", red. - A failed fail-fast step (`job.status` not `success`) is red and names the gates it hid. Zero gates, an unparseable context or a missing input is a refusal (exit 3, PREREQUISITE NOT MET), never a pass. - Prints every gate with its verdict, one `::error` annotation per red gate, and a job-summary table only when not green. Self-test: 31 cases over 9 named batteries with a floor and a handshake (the AGENTS.md shape). ### `.github/workflows/ci.yml` (Test Core) - Both turbo invocations of `Run this shard's tests` pass `--continue=dependencies-successful`. - A red leg no longer `break`s the leg loop: the CLI slice leg runs after a red whole-package leg too, `STATUS` keeps the first non-zero exit and the step exits with it, so the shard is red exactly when it was before. The `⛔ A failing leg STOPS the remaining ones` note is rewritten with the wall argument: the legs together are the shard's own planned work, so a red shard takes what a full one takes, inside the budget the 50-minute wall was sized to. - The attestation pair, the completeness guard, the timing steps and `check-shard-attestation`'s static pins are untouched (`pnpm check:shard-attestation` green below). ### `AGENTS.md` (one sentence) `That job runs its gates sequentially, so the first stale artifact masks every one behind it` became false with this change; it now says the lanes run every gate after a red, so one run reports every stale artifact. This file is outside the claim's declared surface; it is here because the change makes the sentence wrong. ### `scripts/report-unmeasured-gate-tail.mjs` (deleted) Its only caller was the step pair this PR replaces; nothing else in the tree imports or names it (`git grep` over the whole tree: zero hits outside the two files this PR edits). Its self-test wiring left with it, and `check:self-test-wired` is green below. ## The PR's mechanism assumptions, measured 1. **`outcome` versus `conclusion`.** GitHub records `outcome` before `continue-on-error` is applied and `conclusion` after it, so a failed gate under the flag reads `outcome: failure`, `conclusion: success`. The verdict reads `outcome`; the self-test battery `outcome, never conclusion` feeds it exactly that shape and goes red if `conclusion` were read. The live half (that the runner really records it so, and that a skipped scoped gate appears in the context as `skipped`) is what the proof run below reads. 2. **Existing infrastructure.** No gate forbids a step-level `continue-on-error` on these lanes: the guards that read the key pin job-level flags (`check-required-contexts`) or named steps elsewhere (ci.yml's attestation pair, `pr-automation.yml`, release.yml's gate job, the queue guard's toolchain install), none of which this touches. The `lint.yml` note about #16454 is about ci.yml's timing steps and is unchanged. The aggregators and `check-aggregator-roster.mjs` are unchanged and green. The unmeasured-tail printing is replaced, not duplicated. 3. **Test Core.** A three-package turbo 2.11.5 probe (`a#test` exits 1, `c` depends on `a`): with the flag `b` and `c` ran and turbo exited 1 with `Failed: a#test`; without it the run stopped at `a` (`Tasks: 1 successful, 4 total`), exit 1. The flag moves no task hash: `--dry=json` on this tree, plain against flagged, 5 of 5 identical, so main-seeded caches still replay. 4. **The proof on CI.** The branch carries one THROWAWAY commit, `5301220aa`, and its revert: a dead docs anchor that reddens `Docs anchors resolve to real headings` (the first gate of `Lint · repo & tooling gates`, 59 gates behind it) and a deliberately red test in `@objectstack/types` (one Test Core shard). The final head's tree is byte-identical to `5a7b95e15`, the commit before the throwaway. The readings from that run's jobs API (every later gate's step conclusion, the lane and aggregate conclusions, the verdict's named red, the Test Core shard's turbo tally) are in the dev report comment on #23191; this body was written once, at creation, before that run finished. ## Local verification Run on `1e695d6b1` (this branch merged with `origin/main` at `44e2d505c`). The next commit, `5a7b95e15`, changed comments only (the rewritten note in `lint-eslint`, one sentence of the verdict script's header); the same union re-run on `5a7b95e15` was still running when this body was written, and its reconciliation is in the dev report. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 68 derived commands; all 68 run and reconciled with `--ran`: **64 exit 0, 4 NOT MEASURED** (exit 3, PREREQUISITE NOT MET: `check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:sourcemap-no-sources-content` read built `dist/` trees; this diff changes no package source, and CI's Type Check and Build Core lanes run them). Among the 64: `check:pm-dispatch-gates`, `check:required-contexts`, `check:shard-attestation`, `check:select-gate-families`, `check:self-test-wired`, `check:self-test-workflow-commands`, `check:step-collectors`, `node scripts/check-aggregator-roster.mjs` (gate and self-test), `check:workflow-status-functions`, `check:workflow-step-name-quoting`, `check:nul-bytes`, `check:pm-governed-prose`, `check:type-check-debt`, `node scripts/ci/lane-verdict.mjs --self-test`. - A simulation of the in-job verdict over the real `lint.yml`, every lane, with a context built from the file: all 12 lanes OK when every gate succeeds (the four scoped lanes as "turned off by their own if:"), and every lane with an unconditional gate FAILED naming it when its first gate's outcome is `failure` under conclusion `success`. - ESLint, narrowed and proved: the diff's only linted file is `scripts/ci/lane-verdict.mjs` (`eslint --no-inline-config --format json`: 1 file, 0 errors, 0 warnings); `eslint.config.mjs` never enables type-aware linting, so no untouched file's verdict can move, and the deleted script has no importer. ## Acceptance notes - `check-test-completeness.mjs`'s NEITHER branch (a package that printed no summary on a run that did not complete is a note, not red) was written for a turbo that stopped at the first red. Under `--continue` nothing is cancelled except a failed build's dependents, so that note could now be upgraded to red for a silent package beside an unrelated red. Not done: it is the gate's own judging logic, which this card does not touch. Carrier: none. - A gate step added later without `continue-on-error` is fail-fast again (still red, one red per cycle); without an `id:` the verdict reds the lane naming it. The new note above `jobs:` says both. No structural gate for the first case was added (new gates default to no). ## 维护者速读(草稿) - **改了什么**:`Lint · *` 八条与 `Type Check · *` 四条车道里的 240 个门禁步骤,前一个红了后面照跑;每条车道最后一步「Lane verdict」读每个门禁的结果,点名红的并退出 1,车道和两个必需聚合(`Lint & Repo Gates`、`TypeScript Type Check`)照样红。Test Core 的 turbo 加 `--continue=dependencies-successful`,一个包红不再停掉同分片其它包。原「未测尾巴」报告步骤由裁决步取代,脚本删除。 - **为什么改**:按 #23177 裁决 B 第 1 项(A1)。过去一个红门禁会跳过同车道其后所有门禁、turbo 也在第一个红包处停下,一轮 CI 只能看到一个红,每个藏住的红多花一轮 CI 与一次 Version PR 刷新。 - **风险与代价(含回滚)**:门禁命令与判定一字未改,只改编排与汇总;红的运行现在会跑满(时长等于绿的运行,在现有 job 时限内)。风险点是裁决步本身:若它读错字段会让红变绿,所以它读 `outcome` 不读 `conclusion`、名单从 workflow 文件读、读不到即红,并有自测与 CI 实证。回滚:revert 本 PR 即恢复原 fail-fast 与报告步骤。 - **席位意见**: - **你要做的**:审阅后批准(Tier H,受管面须维护者批准);批准前本 PR 保持草稿。 --- _Generated by [Claude Code](https://claude.ai/code/session_012zwsLMxQBwHUYJgPyrpEvx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 917b1a0 commit 7aa82a3

5 files changed

Lines changed: 1271 additions & 1213 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -912,11 +912,35 @@ jobs:
912912
# happens even when a leg failed, because a red suite is exactly when
913913
# the completeness guard earns its keep.
914914
#
915-
# ⛔ A failing leg STOPS the remaining ones, the same way turbo stops
916-
# scheduling on the first failure inside one run. Carrying on would add
917-
# a second full suite to a job that is already red and already inside a
918-
# `timeout-minutes` wall — turning an informative red into a killed job with no
919-
# attestation at all, which is the #16173 failure mode itself.
915+
# ⭐ ONE PACKAGE'S RED STOPS NO OTHER PACKAGE (#23191; #23177 ruling B,
916+
# item A1, comment 6139629628). Both turbo invocations below pass
917+
# `--continue=dependencies-successful`: a failed `test` task no longer
918+
# cancels the tasks still queued beside it, and only a task whose own
919+
# dependency failed is withheld, because it would test a broken build.
920+
# A failed leg no longer skips the legs after it either. Turbo still
921+
# exits non-zero when any task failed, `STATUS` keeps the FIRST
922+
# non-zero exit, and the step exits with it, so the shard is red
923+
# exactly when it was before — and now names every red package of the
924+
# shard in one run instead of the first one turbo reached.
925+
#
926+
# Measured before this was written: a three-package probe (turbo
927+
# 2.11.5, `a#test` exits 1, `c` depends on `a`) ran `b` and `c` to
928+
# completion and exited 1 with `Failed: a#test`; without the flag the
929+
# same run stopped at `a` (`Tasks: 1 successful, 4 total`), also exit
930+
# 1. The flag moves no task hash (`--dry=json` on this tree, plain vs
931+
# `--continue=dependencies-successful`: 5 of 5 identical), so the
932+
# main-seeded cache above still replays.
933+
#
934+
# What it costs is time on a red shard, and the wall above already
935+
# holds it. A red leg now runs as long as a green one, and the legs
936+
# together are the shard's own planned work (each slice is placed at
937+
# its plain weight), so a red shard takes what a full one takes — the
938+
# shape of the slowest job the wall above was sized on (27m05s, every
939+
# package executed and the CLI as all 3 slices). A leg
940+
# the stall guard kills still lands inside p + s + 20m, the budget
941+
# the wall was sized to, whichever leg it is: the legs after it are
942+
# the rest of `s`, not a second suite on top of it. Before this, a red
943+
# stopped the run early, so the hidden reds cost another CI cycle each.
920944
#
921945
# `test test:repo` (#16466): six heavy packages split their suite into
922946
# two turbo tasks -- `test` (package-local inputs) and `test:repo`
@@ -938,7 +962,7 @@ jobs:
938962
if [ "$LEG" = __whole__ ]; then
939963
[ -n "$FILTERS" ] || continue
940964
LOG="$RUNNER_TEMP/test-core-packages.log"
941-
set -- pnpm turbo run test test:repo $FILTERS --concurrency=4 --summarize --log-order=stream
965+
set -- pnpm turbo run test test:repo $FILTERS --concurrency=4 --continue=dependencies-successful --summarize --log-order=stream
942966
else
943967
PKG="${LEG%%=*}"
944968
SLICE="${LEG#*=}"
@@ -1009,11 +1033,14 @@ jobs:
10091033
# is empty); measure-test-shard-timings.mjs resolves that digest
10101034
# against the k/n the partitioner can emit and refuses one it
10111035
# cannot, so a slice is never timed as the whole package.
1012-
set -- env "OS_TEST_SHARD=$SLICE" pnpm turbo run test "--filter=$PKG" --concurrency=4 --summarize --log-order=stream
1036+
set -- env "OS_TEST_SHARD=$SLICE" pnpm turbo run test "--filter=$PKG" --concurrency=4 --continue=dependencies-successful --summarize --log-order=stream
10131037
fi
10141038
LOGS="$LOGS $LOG"
10151039
node scripts/run-with-stall-guard.mjs --log "$LOG" --stall-minutes 10 \
1016-
--report-dir "$RUNNER_TEMP/stall-reports" -- "$@" || { STATUS=$?; break; }
1040+
--report-dir "$RUNNER_TEMP/stall-reports" -- "$@" || {
1041+
CODE=$?
1042+
if [ "$STATUS" -eq 0 ]; then STATUS=$CODE; fi
1043+
}
10171044
done
10181045
10191046
# Only over logs that EXIST, and via an explicit `if` rather than a

0 commit comments

Comments
 (0)