Skip to content

ci: every job is hard-cut at 45 minutes — a literal timeout-minutes, gated fleet-wide - #3067

Merged
rbuergi merged 1 commit into
mainfrom
ci/hard-cap-every-job-at-45-minutes
Sep 2, 2026
Merged

rbuergi merged 1 commit into
mainfrom
ci/hard-cap-every-job-at-45-minutes

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Maintainer, 2026-09-02: "hard cut ci runs after 45min — we pay all this."

What was wrong, measured this morning

GitHub's default job timeout is 360 minutes. The reusable module-pack lane's pack job had no timeout-minutes; dotnet test MeshWeaver.Mcp.Test wedged before its first test on every MeshWeaver.Plugins run (A total of 1 test files matched the specified pattern. then six hours of silence — e.g. Plugins run 33570175780, job 100064441682, 23:35→05:35Z). 19 runs sat in_progress at once, 11 of them on main, each holding a billed runner until GitHub's cut. The repo's three required checks needs: that job, so no Plugins PR could merge and main never reached publish-bake — which is why every satellite's release poll reports "upstream 'plugins' has no SEALED publication". One missing line; I cancelled the 17 hung runs by hand while writing this.

Across core, 26 jobs had no cap or a cap above 45 (full inventory in the guard's first run, quoted in the commit).

The rule, and the guard that enforces it

.github/scripts/check-workflow-timeouts.py — every job in every workflow either uses: a reusable workflow (exempt in the caller; GitHub ignores the caller's value, and the cap lives on the jobs inside the reusable file, which this same guard gates here) or declares timeout-minutes: <literal integer> with 1 ≤ value ≤ 45. An expression is refused: a cap that can only be evaluated at run time cannot be proven by reading the file. A tree with no workflows, a file with no jobs, or invalid YAML fail — never a vacuous pass. --self-test runs 12 cases and proves each fires on its defect and stays silent on its fix.

Wired in two places:

  • dotnet-test.yml (beside check-workflow-shell.py): self-test, then the gate on this tree.
  • node-repo-validate.yml (every satellite's validate job): gains platform-ref, fetches the platform's script at that ref and runs it against the caller's tree — the compile-check.py centralization; no per-repo copies. A ref it cannot be fetched at fails RED naming it.

What changed in the workflows

change jobs
timeout-minutes: 45 added 24 (incl. module-pack select/prepare/pack/verify, collect-results, every preflight, release-images, notify, …)
lowered to 45 alc-unload-probe 120 → 45 (measured 11–24 min), base-image-acr 60 → 45 (5–7 min), flake-repro 60 → 45 (18–21 min)
node-repo-publish-bake.yml the timeout-minutes input (default 40) is removed; the job carries a literal 45. No caller passed it (grep across Plugins/Education/Reinsurance/SocialMedia/Manufacturing).

Headroom, measured today (last 12 runs per workflow)

job duration
Plugins Portal hosts (shard 0/2) 25–30 min
core release-images / images 36–41 min — the closest to the wall; if it ever reaches 45 the fix is to split per architecture, not to raise the cap
Education Every course installs … (shard 3/4) 20–27 min
Reinsurance publish-bake ~20 min
core MeshWeaver Build and Test, main-cd no job ≥ 20 min

Satellites

Their own workflows carry jobs above 45 today (Plugins ci.yml:832 60, Education ci.yml:573/1293 60) and jobs without a cap (every preflight). Those get fixed in their repos with the lane pin bump that brings this guard; until then the guard cannot reach them (it runs at the pinned platform-ref). Recorded in the fleet CI audit.

Verification

  • python3 .github/scripts/check-workflow-timeouts.py --self-test → 12/12 + no-workflows-dir case.
  • python3 .github/scripts/check-workflow-timeouts.py --root . → 61 job(s) checked, 4 reusable-call job(s) exempt, 0 violation(s).
  • check-workflow-shell.py --self-test + tree run → 0 live findings; check-shared-rules.py --self-test 18/18.
  • Every workflow re-parses as YAML.

Internal CI change → no What's New entry.

🤖 Generated with Claude Code

https://claude.ai/code/session_01G4xPnGYEbdu8AVvR5jytyj

…gated fleet-wide

Maintainer, 2026-09-02: "hard cut ci runs after 45min — we pay all this."

GitHub's default job timeout is 360 minutes. The reusable module-pack lane's `pack` job had no
cap; `dotnet test MeshWeaver.Mcp.Test` wedged before its first test on every MeshWeaver.Plugins
run; 19 runs sat in_progress at once (11 on main), each holding a billed runner for six hours,
and because the repo's required checks `needs:` that job, no Plugins PR could merge and main
never reached publish-bake — every satellite starved of a sealed publication. One missing line.

- .github/scripts/check-workflow-timeouts.py: every non-`uses:` job in every workflow must carry
  a LITERAL `timeout-minutes` with 1 <= value <= 45; an expression is refused (a cap that is only
  evaluable at run time cannot be proven by reading the file); a tree with no workflows, a file
  with no jobs, or invalid YAML all fail — never a vacuous pass. --self-test proves every case
  fires on its defect and stays silent on its fix (12 cases).
- dotnet-test.yml runs the self-test, then the gate, beside check-workflow-shell.py.
- node-repo-validate.yml gains `platform-ref` and fetches the platform's guard at that ref to
  run it against the CALLER's tree — the compile-check.py centralization, no local copies.
- node-repo-publish-bake.yml: the job cap is a literal 45 (the `timeout-minutes` input, default
  40, is removed — no caller passed it; measured bake ~20 min).
- 24 core jobs gain `timeout-minutes: 45`; alc-unload-probe (120), base-image-acr (60) and
  flake-repro (60) are lowered to 45 (measured: 24, 7 and 21 min).
- AGENTS.md + .claude/skills/ci: the rule, the cost, and the headroom measured today
  (Plugins portal-host shards 25–30 min, release-images 36–41 min, Education shards ~27 min).

Internal CI change: no What's New entry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 2, 2026 07:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The changes consistently apply a clear CI policy and add a self-tested guard that should prevent regressions without introducing skip-trapdoors.

Pull request overview

Enforces a fleet-wide CI policy that every GitHub Actions job must have a literal timeout-minutes with a hard cap of 45 minutes, and adds a guard script + CI gates to prevent regressions.

Changes:

  • Added .github/scripts/check-workflow-timeouts.py (with self-test) and wired it into core CI (dotnet-test.yml) and the reusable satellite validation workflow (node-repo-validate.yml).
  • Added or lowered timeout-minutes: 45 across workflows/jobs; removed the configurable timeout input from node-repo-publish-bake.yml to keep the cap provable by static inspection.
  • Documented the 45-minute cap rule in AGENTS.md and the /ci skill.
File summaries
File Description
AGENTS.md Documents the new hard 45-minute job cap doctrine and guard enforcement.
.github/workflows/unlist-orphaned-packages.yml Adds timeout-minutes: 45 to prevent uncapped hangs.
.github/workflows/release-packages.yml Adds timeout-minutes: 45 to the publish job.
.github/workflows/release-images.yml Adds timeout-minutes: 45 to the images job.
.github/workflows/publish-github.yml Adds timeout-minutes: 45 to the publish job.
.github/workflows/publish-compiler-tool.yml Adds timeout-minutes: 45 to preflight + publish jobs.
.github/workflows/plugin-build.yml Adds timeout-minutes: 45 to preflight.
.github/workflows/notify-dependents.yml Adds timeout-minutes: 45 to the notify job.
.github/workflows/node-repo-validate.yml Fetches the platform guard at platform-ref and runs self-test + repo gate against caller workflows.
.github/workflows/node-repo-publish-bake.yml Removes timeout input and sets a literal timeout-minutes: 45 on the job.
.github/workflows/node-repo-module-pack.yml Adds timeout-minutes: 45 to key jobs to comply with the cap.
.github/workflows/hosting-operator.yml Adds timeout-minutes: 45 to scripts + image jobs.
.github/workflows/homebrew.yml Adds timeout-minutes: 45 to the preflight job.
.github/workflows/flake-repro.yml Lowers timeout from 60 → 45 to align with the cap.
.github/workflows/dotnet-test.yml Adds guard self-test + enforcement step; caps collect-results at 45.
.github/workflows/clients.yml Adds timeout-minutes: 45 to the python-sdk job.
.github/workflows/chart-gate.yml Adds timeout-minutes: 45 to the invariants job.
.github/workflows/chart-drift.yml Adds timeout-minutes: 45 to preflight/discover/matrix jobs.
.github/workflows/base-image-acr.yml Lowers timeout from 60 → 45 to align with the cap.
.github/workflows/auto-arm.yml Adds timeout-minutes: 45 to the arm job.
.github/workflows/alc-unload-probe.yml Lowers timeout from 120 → 45 to align with the cap.
.github/scripts/check-workflow-timeouts.py New guard enforcing literal per-job timeouts (≤45), with self-test and non-vacuous failures.
.claude/skills/ci/SKILL.md Adds the 45-minute cap guidance and checklist item to the CI skill.
Review details
  • Files reviewed: 23/23 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

231 tests   231 ✅  2m 35s ⏱️
  1 suites    0 💤
  1 files      0 ❌

Results for commit e626af0.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

602 tests   602 ✅  59s ⏱️
  2 suites    0 💤
  2 files      0 ❌

Results for commit e626af0.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

421 tests   421 ✅  1m 23s ⏱️
  1 suites    0 💤
  1 files      0 ❌

Results for commit e626af0.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    3 files      3 suites   1m 12s ⏱️
1 226 tests 1 034 ✅ 192 💤 0 ❌
1 227 runs  1 035 ✅ 192 💤 0 ❌

Results for commit e626af0.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

539 tests   537 ✅  1m 37s ⏱️
  4 suites    2 💤
  4 files      0 ❌

Results for commit e626af0.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 780 tests   1 780 ✅  2m 1s ⏱️
    3 suites      0 💤
    3 files        0 ❌

Results for commit e626af0.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Test Results

   14 files     14 suites   9m 50s ⏱️
4 799 tests 4 605 ✅ 194 💤 0 ❌
4 800 runs  4 606 ✅ 194 💤 0 ❌

Results for commit e626af0.

@rbuergi
rbuergi merged commit 1b044e5 into main Sep 2, 2026
30 of 31 checks passed
@rbuergi
rbuergi deleted the ci/hard-cap-every-job-at-45-minutes branch October 10, 2026 13:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants