Skip to content

ci(lint): every job in lint.yml carries a timeout-minutes derived from its measured duration - #21064

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21050-lint-yml-job-timeouts
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21050-lint-yml-job-timeouts

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

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

Five of the six jobs in the Lint & Type Check workflow declared no
timeout-minutes, so a step that stops producing output ran to GitHub's
360-minute default. Lint & Repo Gates is the longest required check in
every merge-queue group; the queue's 60-minute check-response window
removes such an entry with no failing check and no triage comment while
the hung job keeps a runner for hours.

Walls, from the measured p50 / p99 / max of the 100 most recent completed
runs (2026-10-01T00:23Z to 03:24Z): Lint & Repo Gates 60, Type Check
workspace 30, consumer gates 20, debt ledger 15, source gates 10; the
TypeScript Type Check aggregate keeps its 10. Each new line carries its
measurement and the derivation rule in a comment. No job name, step,
needs or concurrency block changes.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGMD6TRDaZY8Ubyo7Ukm66
The lint job's timeout clock starts when a runner picks the job up, which
is after the merge queue's 60-minute check-response window has opened, so
a wall equal to that window lets the queue remove a hung entry before the
check turns cancelled. At 55 the cancelled check and its "exceeded the
maximum execution time" annotation land first, and the removal is
attributable to a visible failing check; 55 still leaves 22 minutes (1.7x)
over the measured maximum of 32.7.

The derivation rule's floor becomes a flat 10 minutes, the floor every
value in the file meets, and the sentence that listed the jobs sitting
under the former maximum-plus-10 clause is dropped. The four type-check
lane values are unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGMD6TRDaZY8Ubyo7Ukm66
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 1, 2026 04:35
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 1, 2026 04:35
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit dff98c1 Oct 1, 2026
35 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21050-lint-yml-job-timeouts branch October 1, 2026 05:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants