Skip to content

Commit 5dda600

Browse files
ci(lint): Lint & Repo Gates becomes an aggregate over eight parallel lanes; PM dispatch-gates runs its fast tier on PR and queue (#22991) (#23014)
Fixes #22991 Clause-②: no Executes ruling 6120820653 (letter D) on the card: `Lint & Repo Gates` becomes an aggregator over eight parallel `Lint · *` lanes, and `pnpm check:pm-dispatch-gates` runs its fast tier on `pull_request` / `merge_group` while `schedule` and `push` keep both tiers. ## What changed - **`.github/workflows/lint.yml`**: the 193-step `lint` job is split. Job id `lint` and `name: Lint & Repo Gates` are kept byte-identical (the required context is matched by check-run name). The job now runs one step: a verdict over `toJSON(needs)`, copied from the `typecheck` aggregate. It carries `OS_AGGREGATOR_MEMBERS` (equal to `needs:`), `if: always()` and `timeout-minutes: 10`. Every gate step moved verbatim into one of eight lanes: same `run:`, same `if:`, same comment block, same relative order. Each lane repeats the six setup steps, the gate-family selector when it carries a scoped step, and the unmeasured-tail reporter pair. Lane walls use the Type Check lanes' scheme: a checkout budget of 20 on the checkout step plus a body budget (2× the measured body maximum, 5-minute grain, floor 10). That gives 30 for seven lanes and 50 for the PM lane, whose body budget still has to cover both tiers on `schedule` / `push`. - **`scripts/pm/check-dispatch-gates.mjs`**: under `GITHUB_ACTIONS=true`, `GITHUB_EVENT_NAME` decides the tier. `pull_request` and `merge_group` get `--self-test --fast`. Every other event gets both tiers: `schedule`, `push`, `workflow_dispatch`, and any trigger nobody listed. `--slow` stays the manual override everywhere, and the chosen tier and its reason are printed before the spawn. Its own self-test covers each event. It also blanks the runner's own `GITHUB_EVENT_NAME`, which would otherwise leak into the cases on CI. - **`scripts/check-aggregator-roster.mjs`**: `lint` / `Lint & Repo Gates` is registered as a required aggregate, so deleting its roster is a refusal. It is added to both per-aggregator positive controls. The two `typecheck` refusal fixtures are anchored on typecheck's own roster: the unanchored pattern hit the first `OS_AGGREGATOR_MEMBERS` in lint.yml, which is now `lint`'s, and that is how the gate first spoke on this diff. - **`scripts/check-required-contexts.mjs`**: the pin must now run in a job the `Lint & Repo Gates` context carries. That is `lint` itself, or a lane it both declares and lists in `needs:`. The step may not carry an `if:`, and neither may the lane hosting it. New fixtures cover: declared and needed counts as wired; declared-only does not; needed-only does not; a lane-level `if:` is seen. ## Lanes (predicted) How the prediction was made: each lane's wall is rebuilt per run from the per-step timestamps of the 70 successful `Lint & Repo Gates` runs, 2026-10-12T04:40Z to 08:45Z (`pull_request` and `merge_group`, jobs API). Each rebuild uses that run's own setup steps plus the lane's steps as they ran there. The measured walls of this PR's own CI run go into the `os-dev-report` comment on the card. This body is written once, before that run exists. | lane | name | steps | p50 min | max min | timeout | |:--|:--|--:|--:|--:|--:| | `lint-eslint` | Lint · ESLint | 2 | 3.3 | 4.0 | 30 | | `lint-repo-tooling` | Lint · repo & tooling gates | 60 | 4.2 | 4.7 | 30 | | `lint-docs-code` | Lint · docs & code guards | 55 | 4.1 | 4.6 | 30 | | `lint-ci-engine` | Lint · CI, release & engine gates | 58 | 3.9 | 4.4 | 30 | | `lint-scoped-erasure` | Lint · scoped · query-options erasure | 2 | 3.5 | 5.0 | 30 | | `lint-scoped-slot-corpus` | Lint · scoped · slot lookup & corpus | 3 | 3.5 | 4.9 | 30 | | `lint-scoped-tooling` | Lint · scoped · tooling self-tests | 3 | 1.1 | 4.4 | 30 | | `lint-pm-dispatch-gates` | Lint · PM dispatch-gates | 1 | 1.1 | 5.1 (fast tier, estimated) | 50 | **Critical path.** Before: p50 17.8, p90 35.9, max 38.5 minutes. After (predicted): p50 4.2, p90 4.7, max 5.1 minutes on `pull_request` / `merge_group`, plus the aggregate's own seconds. On `schedule` / `push` the PM lane runs both tiers, about 15–16 minutes. How the cut differs from the PM's H2: - The ESLint lane and the three always-on lanes match H2. - H2's single scoped lane would be p50 6.4 and max 11.5 minutes in the same rebuild, over the 5-minute budget. So it is split three ways and the PM self-test stays alone. - `query-options erasure` (160 s) and `slot-lookup` (84 s) run together in 53 of 70 runs. Together they would put one lane over budget at median, so they sit in different lanes. ## Mechanism hypotheses, checked - **H1 holds.** The only inter-step reads in the old job were `steps.gate-families.outputs.*`: nine `if:` lines and one step `id:`. No `run:` in the job builds, writes `GITHUB_PATH`, or reads `RUNNER_TEMP` across steps. The one `GITHUB_ENV` write (`STORE_PATH`) is a setup step every lane repeats. Of the 196 scripts the job invokes, I scanned every write call site that has no temp-dir use. None writes a repo file that a later step reads; they are ledger `--write` modes or fixture strings. - **H2 refined**, as described in the cut above. - **H3 holds, with one deliberate difference.** The fast tier is a closed list (`pull_request`, `merge_group`) rather than "fast unless `schedule` or `push`". The ruling moved the slow tier off those two events only, so `workflow_dispatch` keeps both tiers. An event nobody listed also lands on the full battery rather than the shorter one. - **H4: the selector runs once per scoped lane.** It cost 0 s at the median and 1 s at p90 over the 70 runs. A separate `families` job would put a second full-history checkout and a runner pickup on the critical path of every scoped lane, and it would change all nine `if:` spellings, which `check:select-gate-families` pins. - **H5:** each lane's timeout is derived from its measured body, as in the table. The aggregate takes 10, which is lower than the old 55, as the ruling allows. - **H6:** two pins spoke, `check-aggregator-roster --self-test` and `check:required-contexts`. Both are updated, and neither is loosened (see "What changed"). The other five pins named in H6 stayed green unchanged. ## Proof the move is pure A one-off verifier parsed the old and the new lint.yml (the `yaml` package; PyYAML agrees on the result). It checked: - each of the 184 gate steps appears in exactly one lane, deep-equal to its old parse, in its old relative order; - the six setup steps are identical in every lane except the added checkout `timeout-minutes: 20`; - the selector is present, identical and ahead of the first scoped step in exactly the lanes that carry one; - the tail pair is last in every lane; - every other job, and the workflow's `name`, `on` and `concurrency`, are unchanged. Result: 0 problems. Comment edits only name the step a moved neighbour referred to ("the step above" became the step's name and its lane). They also note the per-lane order rule, the per-lane selector and the PM tier change where those notes live. ## Ablations (one-time, each restored and confirmed byte-identical to HEAD by `scripts/ablation-replace.mjs`) - `FAST_TIER_EVENTS` emptied: the first run **exited 0**, because the self-test looped over the constant under test. The fix names the two events literally; rerun red (2 cases). - The event branch forced to always-fast: red (5 cases). - `lint-ci-engine` deleted from the aggregate's `needs:`: `check:required-contexts` red, naming the unwired pin. - The `needs:` read in `jobsCarriedByLint` deleted: red on the declared-only fixture. - `lint` dropped from `REQUIRED_AGGREGATORS`: `check-aggregator-roster --self-test` red on the lint refusal fixture. ## Gates run locally at `79b0fd6d` (this head, after merging `origin/main`) Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, which found 54 commands; I ran all of them. - 52 ran in the foreground, all exit 0. Among them: `check-aggregator-roster` (and its `--self-test`), `check:required-contexts`, `check-ci-filter-parity`, `scripts/ci/scheduled-full-run.mjs --self-test`, `check-self-test-workflow-commands`, `check-step-collectors`, `check-self-test-wired`, `check:select-gate-families`, `check:workflow-step-name-quoting`, `check:workflow-status-functions` and `check:nul-bytes`. - `pnpm check:pm-dispatch-gates` ran detached, exit 0: the fast tier, 1828 cases, 264.9 s on this box. - `pnpm check:type-check-debt` is **NOT MEASURED**. Its `--self-test` passed. Its `--re-measure` half runs tsc over the ledgered package and was cut at 500 s (exit 124). It needs the build that CI's `Type Check · debt ledger` lane runs first, and this diff touches no TypeScript program. `--ran` reconciliation: 54 derived, 53 run, 1 NOT-MEASURED, 0 unrun. `actionlint` is not installed in this container; two YAML parsers agree. ## Acceptance notes - Every lane keeps `fetch-depth: 0`. I did not re-derive which of the ~190 gates read history. A gate that loses its history degrades to "not verified" rather than red, so a shallow checkout per lane (about 20 s each) is left to a later card. - A lane carrying a scoped step without its own selector would fail OPEN: the step would run on every PR. The one-off verifier above checks for this, but no checked-in pin does. `check:select-gate-families` reads lint.yml as text and does not pair each scoped `if:` with a selector in the same job. - The `typecheck` aggregate's inline verdict script still says, in a JS comment inside its `run:` text, that the roster gate runs "in the lint job". I left that `run:` text untouched; the YAML comment beside it is updated. - `scripts/pm/dispatch-gates.self-test.mjs` line 6 says the both-tier run is "the battery CI runs". That is still true on `schedule` / `push`, so it is left alone. - The PM lane's fast-tier time on a runner is an estimate: 253–281 s on this 4-core container, where CPU-bound gates run about 1.1–1.3× slower than on CI. This PR's run gives the real number. If the PM lane is over 5 minutes at median, the remedy is splitting the battery, not skipping it. - Eight lanes plus the aggregate replace one job: about seven more runner pickups and about seven more minutes of setup per run, in exchange for the wall. ## 维护者速读(草稿) **改了什么**:把 `Lint & Repo Gates` 这个必过检查从"一台机器串行跑 193 步"拆成 8 条并行车道,原名字保留做汇总(合并队列按名字认,所以不受影响)。另外,PM 派发工具的自测在 PR 和合并队列上只跑快速档,完整档改在每小时定时跑和合入 main 后跑。 **为什么改**:这个检查中位 17.8 分钟、最长 38.5 分钟,一半的合并队列都在等它。按 70 次真实运行重算,拆分后预计中位约 4.2 分钟、最长约 5.1 分钟(PR 和合并队列上)。 **风险与代价(含回滚)**:每次运行多占约 7 台机器、多约 7 分钟的安装时间(机器分钟换等待时间)。PM 工具的慢速档用例不再在 PR 上跑,有问题会在 main 上的定时运行里第一次变红(这是裁决接受的代价)。回滚就是 revert 这个 PR,必过检查名字没变,设置里不用动。 **席位意见**: **你要做的**:看本 PR 自己那次 CI 的各车道耗时(dev 报告评论里有),确认 8 条车道和汇总都是绿的,`Lint & Repo Gates` 仍出现在检查列表里;然后批准合入。 --- _Generated by [Claude Code](https://claude.ai/code/session_01LqNgdPZB2Jnr5HpGbaqLeB)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5187e57 commit 5dda600

4 files changed

Lines changed: 1658 additions & 661 deletions

File tree

0 commit comments

Comments
 (0)