Give long pipeline steps their own execution budget so a timeout reports failure, not cancelled - #61
Conversation
Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: #60
A job killed by 'timeout-minutes' is reported by GitHub as cancelled, not failed, so a genuine timeout on a pull request produced only a warning from check-pipeline-status.sh and nothing red. No step in the template owned a deadline, so the job clock was always the deadline. Add scripts/run-with-budget-warning.sh, which warns at 70% of its budget, signals the command's whole process group at the deadline and exits 124 with an ::error annotation naming the budget. Wrap dependency installation, secret scanning and the test run with it, and give both Docker build steps a step-level timeout-minutes, which a shell wrapper cannot own. Pin the invariant with a test: every step deadline in every workflow must sum to at most 70% of the job cap it sits under, leaving the remainder for unbudgeted setup, so the budget always expires first. Fixes #60
timeout-minutes alone lets a slow job report cancelled instead of failed, and no step in the pipeline owns a deadlinefailure, not cancelled
Working session summaryDone. PR #61 is ready and green: #61 Root cause. Fix.
Tests (85 pass, plus
CI confirms the wrapper is live: This summary was automatically extracted from the AI working session output. |
🤖 Solution Draft LogThis log file contains the complete execution trace of the AI solution draft process. 💰 Cost: $2.502964📊 Context and tokens usage:
Total: (86 new + 65.9K cache writes + 2.4M cache reads) input tokens, 25.0K output tokens, $2.502964 cost 🤖 Models used:
📎 Log file uploaded as Gist (1223KB)Now working session is ended, feel free to review and add any feedback on the solution draft. |
🎉 Auto-mergedThis pull request has been automatically merged by hive-mind.
Auto-merged by hive-mind with --auto-merge flag |
Summary
timeout-minuteswas the only deadline in the pipeline, and GitHub reports ajob killed by it as cancelled, not failed.
scripts/check-pipeline-status.shturns that into an error only on
main; on a pull request it is just a warning,because there a cancellation is normally a superseded run. So a genuine timeout
on a pull request produced nothing red, and on
mainthe error could not namethe deadline that was blown — no step owned one.
This PR gives the long steps their own deadline, so
timeout-minutesbecomes abackstop and never the deadline.
Fixes #60
Changes
scripts/run-with-budget-warning.sh— runs a command under an executionbudget:
set -mputs the command in its own process group, sopytest -nworkers and
pipchildren are signalled too instead of being orphaned ontothe runner (which is also why
timeout(1)alone is insufficient);::warningat 70% of the budget, while the overrun can still be acted on;::error title=<label> exceeded its execution budget::…and exit 124,matching
timeout(1), so the job reportsfailurenaming the budget;BUDGET_ENFORCE=falsewarns without killing (local runs), andCI_VERBOSE=trueadds a heartbeat naming what is still running.release.yml—lintandtestbudget installation separately fromthe work that follows, because a slow dependency resolve is the most common
cause of a Python job overrun and is exactly the part a
pytestbudget cannotsee: install 300s, secretlint 300s (cap 20m), install 300s + pytest 900s
(cap 30m). The two Docker builds run through
uses:actions, which a shellwrapper cannot own, so they get a step-level
timeout-minutes: 40under their60m caps — GitHub reports a step killed by that as a failed step.
Reproduction and verification
The issue's repro is a step allowed to run into the job clock:
Under the wrapper the same overrun exits 124 with an
::error, so the jobconcludes
failure.tests/test_run_with_budget_warning.pyasserts exactlythat, and that the process tree really dies:
Tests
tests/test_run_with_budget_warning.py— exit 124 and the::errorannotation on overrun, the whole process group terminated (no zombie
survivors), the warning arriving while the command is alive, exit-code
passthrough, the
BUDGET_ENFORCE=falseescape hatch, the off-by-defaultheartbeat, and rejection of a call with no command.
tests/test_workflows.py— the invariant that finds the next occurrence:every step deadline in every workflow must sum to at most 70% of the
timeout-minutesit sits under, leaving the remainder for unbudgeted setup;plus every job must declare a cap at all, and the budgeted steps must actually
invoke the wrapper.
All 85 tests pass locally, along with
ruff check,ruff format --checkandmypy src.