Skip to content

Commit dff98c1

Browse files
ci(lint): every job in lint.yml carries a timeout-minutes derived from its measured duration (#21064)
Fixes #21050 Clause-②: no ## What changed `.github/workflows/lint.yml` gains five `timeout-minutes` keys, each under a comment carrying its measurement and the derivation rule. Nothing else moves: no job `name:`, step, `needs:`, `if:`, `permissions:` or `concurrency:` changes. The diff against d1f8ce8 is 62 insertions, 0 deletions, in five hunks, each directly under a job's `runs-on:` line (two commits: 5e6d90c, then 51f3743 for the ruled patch round). | job id | check context (`name:`, byte-identical) | `timeout-minutes` before | after | measured p50 / p99 / max (min), n | |:--|:--|:--|:--|:--| | `lint` | `Lint & Repo Gates` (required) | none, so GitHub's default 360 | **55** | 15.8 / 32.6 / 32.7, 81 | | `typecheck-workspace` | `Type Check · workspace` | none | **30** | 13.0 / 16.0 / 16.0, 86 | | `typecheck-consumers` | `Type Check · consumer gates` | none | **20** | 7.7 / 10.1 / 10.1, 87 | | `typecheck-debt` | `Type Check · debt ledger` | none | **15** | 5.8 / 7.4 / 7.5, 92 | | `typecheck-source-gates` | `Type Check · source gates` | none | **10** | 3.2 / 3.5 / 3.7, 94 | | `typecheck` | `TypeScript Type Check` (required) | 10 | 10 (line untouched) | 0.1 / 0.1 / 0.2, 85 | Measurement (the card's, transcribed into the comments): the 100 most recent completed `lint.yml` runs, 2026-10-01T00:23Z to 03:24Z, 96 runs read, 525 successful job rows, job `started_at` to `completed_at` from the jobs API. Rule: about 2x the measured maximum, rounded to a 5-minute grain, never below 10 minutes, never above the merge queue's 60-minute `check_response_timeout_minutes`. For `lint` the window binds rather than 2x, and the wall sits below it, at 55, leaving 22 minutes (1.7x) over the measured maximum. The `lint` comment carries the full reasoning; the four lane comments are short and point at it. ## Why `Lint & Repo Gates` is the longest required check in every merge-queue group, and nothing in this workflow bounds a step that stops producing output: no step runs under `scripts/run-with-stall-guard.mjs`, so the job timeout is the only wall these jobs can have, and five of six declared none. A silent step would hold the job `in_progress` until GitHub's 360-minute default, while the queue (ruleset 12119582) gives up on the entry at 60 minutes with no failing check to read and no `Merge Queue Triage` comment, and the hung job keeps a runner for up to five more hours. ## What GitHub does at the wall: documented, plus one reading on this repository No local test can drive a job timeout. GitHub documents that a job exceeding `timeout-minutes` is cancelled. One reading on this repository agrees, read this session from the jobs and check-runs APIs: in run 36803445860 (`CI`, `merge_group`), job `Test Core (1/6)`, stopped by its 45-minute wall, has `conclusion: cancelled` and a `failure`-level annotation reading `The job has exceeded the maximum execution time of 45m0s`. For the four type-check lanes, a `cancelled` lane fails the required `TypeScript Type Check` aggregate (its verdict step fails on anything but `success`). For `lint`, `cancelled` is the conclusion of the required context itself. Ruleset 12119582, read this session: `merge_queue.check_response_timeout_minutes: 60`, and its required contexts include `Lint & Repo Gates` and `TypeScript Type Check`. ## Verification (head 51f3743, base d1f8ce8) - **Structure.** The file parses under PyYAML 6.0.1 and js-yaml 5.4.2. A per-job fingerprint (name, step count, SHA-256 over every key except `timeout-minutes`, plus the top-level keys and the job order) is identical before and after except for the five `timeout-minutes` values. `actionlint` is not on the PATH; nothing was installed. - **Mechanism assumption A1.** `git grep -c 'run-with-stall-guard' -- .github/workflows/lint.yml` answers 1, not 0. The hit is prose in the `Stall-guard self-test` step's comment, and no step runs under the guard. `pnpm check:stall-guard-budget` exits 0: 39 workflow files, 65 jobs, 730 steps, 10 guard-wrapped steps, none of them in this file. - **Mechanism assumption A2.** `node scripts/check-ci-filter-parity.mjs` (and its `--self-test`), `node scripts/check-required-contexts.mjs` (7 required contexts pinned across 3 workflows) and `node scripts/ci/select-shard-timings-run.mjs --self-test` all exit 0 on this diff. - **Derived gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, with no paths, derived the change set from git against merge base d1f8ce8 and printed 44 commands. 43 exit 0, including `pnpm check:pm-dispatch-gates` (1976 cases, 680 s). `pnpm check:type-check-debt` exits 3, PREREQUISITE NOT MET: its `--re-measure` half needs the built package closure, so it is **NOT MEASURED** locally, and CI runs it in the `Type Check · debt ledger` lane. Its `--self-test` half passed, and `pnpm check:type-check-coverage`, which reads this file, exits 0. `--ran` reconciliation: 44 accounted, 43 run, 1 NOT-MEASURED, 0 UNRUN. 15 of the 44 are `--self-test`-only commands that judge their own script, not this diff. - **Named and dispatch-time extras**, all exit 0: `pnpm check:doc-authoring`, `pnpm check:nul-bytes`, `pnpm check:pm-skill-id-lint`, `node scripts/check-issue-citations.mjs` (no citations added), `pnpm check:stall-guard`, `pnpm check:pm-governed-prose`, `pnpm check:route-ledger-census`, `pnpm check:select-shard-packages`, `node scripts/measure-test-shard-timings.mjs --self-test`, `node scripts/pr-labels.mjs --self-test`. - **Prose.** The added lines carry no tracker number and no model identifier, and no raw control bytes. - **Patch round at 51f3743.** `pnpm check:doc-authoring`, `pnpm check:pm-skill-id-lint`, `node scripts/check-issue-citations.mjs`, `pnpm check:nul-bytes`, `node scripts/check-ci-filter-parity.mjs`, `node scripts/check-required-contexts.mjs` and `pnpm check:stall-guard-budget` all exit 0. The file parses under PyYAML and js-yaml. The structural fingerprint against d1f8ce8 differs only in the five added `timeout-minutes` values (55/10/30/15/20), and the aggregate's 10 is unchanged. ## Acceptance notes - **Floor clause (ruled).** The rule's floor is a flat 10 minutes, which all six values meet. The earlier maximum-plus-10 wording, which four values did not meet, was dropped from the `lint` comment on the seat's ruling. - **Clock ordering (ruled).** A job's wall starts when a runner picks the job up, which is after the queue's 60-minute check-response window has opened, so a wall equal to that window would let the queue remove a hung entry before the check turns `cancelled`. On the seat's ruling the `lint` wall is 55. The `cancelled` check and its annotation land before the queue's window closes whenever pickup takes under five minutes, so the removal is attributable to a visible failing check. 55 leaves 22 minutes (1.7x) over the measured maximum of 32.7. - **Merge Queue Triage scope.** It subscribes to the `CI` workflow only (`workflow_run`, `workflows: [CI]`), not `Lint & Type Check`, and fires on `failure` only. A timed-out lint job gets no triage comment either way. Its header scopes it to "every red merge_group CI run", so this is its declared scope, not a defect. Noted, not filed; carrier: none. - **Card location detail.** The card places the `Stall-guard budget invariant` step in the `lint` job, but it sits in `typecheck-source-gates`. That has no effect on this change. `skip-changeset` applies: `.github/workflows/` publishes nothing, so no changeset was created and the seat applies the label. --- _Generated by [Claude Code](https://claude.ai/code/session_01PGMD6TRDaZY8Ubyo7Ukm66)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent dcd3309 commit dff98c1

1 file changed

Lines changed: 62 additions & 0 deletions

File tree

‎.github/workflows/lint.yml‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,41 @@ jobs:
178178
# mis-routed diagnoses on 2026-08-17 alone (#9258, PRs #9256/#9291).
179179
name: Lint & Repo Gates
180180
runs-on: ubuntu-latest
181+
# ⭐ JOB WALL — measured, and the only wall this job has. No step in this
182+
# workflow runs under `scripts/run-with-stall-guard.mjs` (if one ever does,
183+
# `pnpm check:stall-guard-budget` holds its cap under this number), so a
184+
# step that stops producing output — a build, an install, a network read
185+
# that never answers — is ended by nothing else. Without the key GitHub's
186+
# default is 360 minutes, and this is the longest REQUIRED check in every
187+
# merge-queue group. The queue does not wait that long: ruleset 12119582
188+
# (`main`) treats a required check that has not reported within 60 minutes
189+
# (`check_response_timeout_minutes`) as failed and removes the entry — with
190+
# no red check, and no `Merge Queue Triage` comment either (that workflow
191+
# fires on a `failure` conclusion only, and it watches `CI`, not this
192+
# workflow) — while the hung job holds a runner for up to five more hours,
193+
# still `in_progress`. With the wall, a hung run ends `cancelled`, carrying
194+
# GitHub's failure annotation "The job has exceeded the maximum execution
195+
# time of …", before the queue's own window closes.
196+
#
197+
# Measured: p50 15.8 / p99 32.6 / max 32.7 minutes (n = 81); a docs-only
198+
# change runs it in about 7–11, a code change in 25–33. The window, for
199+
# every job in this file: the 100 most recent completed runs of this
200+
# workflow, 2026-10-01T00:23Z → 03:24Z (96 runs read, 525 successful job
201+
# rows), job `started_at` → `completed_at` from the jobs API.
202+
#
203+
# The rule, for every job in this file: about 2× the measured maximum,
204+
# rounded to a 5-minute grain, never below 10 minutes, never above the
205+
# queue's 60-minute window. Here the window binds, not 2× (2 × 32.7 =
206+
# 65.4), and the cap is placed BELOW it, at 55: this job's clock starts
207+
# only once a runner picks it up, after the queue's has started, so the
208+
# five minutes are room for that pickup, and 55 still leaves 22 minutes
209+
# (1.7×) over the measured maximum. Above 60 the stretch between the queue
210+
# removing an entry and this check turning `cancelled` only grows, so when
211+
# the measured maximum nears the wall the remedy is a faster job (or the
212+
# queue's own setting, which is the maintainer's) — never a higher number
213+
# here. Re-measure over a fresh window before changing any value in this
214+
# file.
215+
timeout-minutes: 55
181216
permissions:
182217
contents: read
183218
# ⭐ Read-only, and it buys exactly one thing: the LAST step of this job
@@ -5099,6 +5134,13 @@ jobs:
50995134
typecheck-source-gates:
51005135
name: Type Check · source gates
51015136
runs-on: ubuntu-latest
5137+
# JOB WALL — why every job here has one, the measurement window and the
5138+
# rule: the `lint` job's `timeout-minutes` comment.
5139+
# Measured: p50 3.2 / p99 3.5 / max 3.7 minutes (n = 94); 10 is 2.7×
5140+
# the maximum.
5141+
# A lane stopped by its wall ends `cancelled`, which the required
5142+
# `TypeScript Type Check` aggregate at the bottom of this file fails on.
5143+
timeout-minutes: 10
51025144
permissions:
51035145
contents: read
51045146

@@ -5885,6 +5927,13 @@ jobs:
58855927
typecheck-workspace:
58865928
name: Type Check · workspace
58875929
runs-on: ubuntu-latest
5930+
# JOB WALL — why every job here has one, the measurement window and the
5931+
# rule: the `lint` job's `timeout-minutes` comment.
5932+
# Measured: p50 13.0 / p99 16.0 / max 16.0 minutes (n = 86); 2 × 16.0 =
5933+
# 32, 30 on the grain.
5934+
# A lane stopped by its wall ends `cancelled`, which the required
5935+
# `TypeScript Type Check` aggregate at the bottom of this file fails on.
5936+
timeout-minutes: 30
58885937
permissions:
58895938
contents: read
58905939

@@ -6092,6 +6141,12 @@ jobs:
60926141
typecheck-debt:
60936142
name: Type Check · debt ledger
60946143
runs-on: ubuntu-latest
6144+
# JOB WALL — why every job here has one, the measurement window and the
6145+
# rule: the `lint` job's `timeout-minutes` comment.
6146+
# Measured: p50 5.8 / p99 7.4 / max 7.5 minutes (n = 92); 2 × 7.5 = 15.
6147+
# A lane stopped by its wall ends `cancelled`, which the required
6148+
# `TypeScript Type Check` aggregate at the bottom of this file fails on.
6149+
timeout-minutes: 15
60956150
permissions:
60966151
contents: read
60976152

@@ -6322,6 +6377,13 @@ jobs:
63226377
typecheck-consumers:
63236378
name: Type Check · consumer gates
63246379
runs-on: ubuntu-latest
6380+
# JOB WALL — why every job here has one, the measurement window and the
6381+
# rule: the `lint` job's `timeout-minutes` comment.
6382+
# Measured: p50 7.7 / p99 10.1 / max 10.1 minutes (n = 87); 2 × 10.1 =
6383+
# 20.2, 20 on the grain.
6384+
# A lane stopped by its wall ends `cancelled`, which the required
6385+
# `TypeScript Type Check` aggregate at the bottom of this file fails on.
6386+
timeout-minutes: 20
63256387
permissions:
63266388
contents: read
63276389

0 commit comments

Comments
 (0)