Repository navigation
Conversation
When several PRs merge before the versioning job gets a runner, every job but the newest commits a version bump on a stale parent and fails on push. Oct 6 00:00Z to Oct 8 16:28Z, 47 of 51 failed Push runs were exactly this: in all 47, main had already moved past the job's commit before the job started, and in every successful versioning run it had not. The newest merge's bump consumes all pending changelog fragments (for example, 7c857cf removed the three fragments from the 13:02Z burst on Oct 8), so the older jobs had nothing to do. The job now reads main's head first. If main has moved past github.sha it records a notice and skips the remaining steps, so the run succeeds and "Push" failures on main again mean something. If the lookup fails, the job proceeds as before. The job is not cancelled and keeps no concurrency group. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Five Push jobs had no timeout-minutes and so inherited GitHub's 360-minute default. Each now has a limit well above its Oct 6-8 maximum: ReleaseLock, ReleaseLockTests and Lint 10 min (max 0.4, 1.4 and 0.7), versioning 20 (max 12) and Publish 20 (max 1.3). A ReleaseLockTests run that hung for 45 min (37650158537) would have stopped at 10. The push Rest job's limit rises from 60 to 90 minutes, matching the PR Rest job after #9785. On push its p50 was 51 min and its successful max 58.8, and run 37719726248 hit the 60-minute limit, which skipped that version's Publish. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each full-suite test job on the bot's version-bump commit now takes a job-level concurrency group per shard (Baseline and Contrib groups include matrix.group), so a newer `Update PolicyEngine US` push cancels only the older run's matching shard. ReleaseLock, ReleaseLockTests, Lint, versioning and Publish have no group and are never cancelled; an older run whose shards are cancelled skips Publish because a needed job did not succeed, and the newer version (a superset of its changes) is published instead. Re-runs (run_attempt > 1) set cancel-in-progress to false, so re-running an old run waits for the newer shard instead of cancelling the newest release. Oct 6 00:00Z to Oct 8 16:28Z: 13 of 28 full-suite push runs had a newer version-bump push created before their last test job finished; per-shard cancellation would have freed about 1,690 runner-minutes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Closing this draft. It cancels a superseded full-suite shard on Context: the PolicyEngine org was at GitHub's 60-concurrent-job limit on 2026-10-06..08 (state and measurements: If merge bursts on |
Decision needed
Should policyengine-us stop publishing superseded versions to PyPI, in exchange for about 630 runner-min/day saved during merge bursts?
Cost: version gaps. When a newer
Update PolicyEngine UScommit reaches a full-suite shard while an older one is still testing, the older shard is cancelled. That older version's Publish is skipped, and only the newest version is published.Cost: publishing waits for a green suite. If the newest suite fails, nothing is published until a fix lands. Today, the older passing version would still ship.
Benefit. The change saves ~630 runner-min/day: 1,692 runner-min over the 2.687-day window. Excluding one duplicate push event, it saves ~420/day. The saving is concentrated in merge bursts, when two 23-job suites otherwise run at once on the org's 60 shared runner slots. Averaged over the day, 630 min is only 0.44 of a slot.
Alternative: test-then-bump.
This keeps every published version tested, but versions would still be skipped for superseded merges. The bump commit itself (version, lock root, changelog) would be built and checked by
release_build.pybut not fully re-tested. It is a larger change and needs its own review.Owner: Max, or the policyengine-us release owners.
Options:
Summary
Stacked on #10016 (base
ci-capacity/push-versioning-timeouts). The change touches only.github/workflows/push.yaml, plus a changelog fragment.Five Push test jobs each get a job-level concurrency group, one per matrix shard:
${{ github.workflow }}-${{ github.ref }}-baseline-${{ matrix.group }};…-contrib-${{ matrix.group }};…-household-api-partners;…-rest;…-microsimulation.All five set
cancel-in-progress: ${{ github.run_attempt == '1' }}. These are Baseline (11 shards), Contrib (9 shards), HouseholdAPIPartners, Rest and Microsimulation, which run only on the bot's version-bump commit. Thebaseline-andcontrib-prefixes keep Baselinestates-shard-1and Contribstates-shard-1in different groups.skipped, because a needed job did not succeed. This was observed in run 37719726248, where Rest was cancelled at its time limit. The newer bump commit contains every change in the older one.run_attempt > 1) do not cancel. A re-run of an old run's shard waits behind the newer run's shard, so it can never cancel the newest release's tests.Why job-level, not workflow-level
Workflow-level
concurrencymay use only thegithub,inputsandvarscontexts, so it cannot be per shard. Withcancel-in-progressit would also cancel the whole older run, including a merge push's versioning job or a Publish in flight. Job-level groups may also useneeds,strategyandmatrix(GitHub docs, workflow syntax forjobs.<job_id>.concurrency).Semantics this change relies on, from the docs:
cancel-in-progressthe running one too.Data and method
filter=all).completed_at − started_at, counted only for jobs that recorded a runner.policyengine[bot]version-bump commits, at a mean of 545 runner-min each.Savings model.
Expected runner-min/day saved
Invariants
needsis unchanged, so nothing reaches PyPI without every shard passing on the exact commit it builds.cancel-in-progressis false whenrun_attempt > 1.Risks and how they are bounded
Version gaps on PyPI. This is the decision above.
Two semantics are inferred, not stated in the docs.
needsenters its group when it is created, after itsneedsfinish.Check this on the first two overlapping bot pushes after merge: the older run's cancelled jobs should be exactly the shards that the newer run had reached. If the semantics differ, the effect is less saving, not a broken release, because Publish still needs every shard to succeed.
Groups are ordered by when a job enters them, not by commit order. If an older run's test jobs entered after a newer run's, the older run would cancel the newer one. The older version would then publish, and the newest would wait for the next bump. All of a run's test jobs need only ReleaseLock, so they enter together, and a mixed outcome where neither run publishes cannot come from entry order. In the window, 0 of the 378 pairs of full-suite runs had a newer run's test jobs created before an older run's.
A failed newest suite blocks publishing until a fix lands. Today, the older passing version would ship. This is part of the decision.
Runner budget
CLAUDE.md's "Test design and CI cost" section governs runner-budget changes. This PR adds no jobs, matrix entries, runners or concurrent processes. It only cancels already-superseded test jobs on main.
CI on this PR
pr.yamltriggers only on PRs intomain, and this is a draft stacked on #10016's branch, so no CI runs here yet. After #10016 merges, GitHub retargets this PR tomain. Marking it ready then runs the full PR suite.Verification
Run on this branch: #10016's commits, rebased onto the latest
origin/main(e44d7c3), plus this commit. The run used a fresh worktree withuv sync --locked --extra devon Python 3.14. This branch's push.yaml is byte-identical to the originally reviewed three-commit tip.actionlint .github/workflows/push.yaml(actionlint 1.7.12)RELEASE_LOCK_REAL_UV=1 python -m unittest discover -s .github/tests -p 'test_release_*.py'(includestest_release_build.py, which parses the Publish job)uv run pytest policyengine_us/tests/code_health/test_rest_python_groups.py policyengine_us/tests/code_health/test_make_test_matches_ci.py policyengine_us/tests/code_health/test_pr_runner_budget.pyuv run ruff format --check .(ruff 0.15.5)A property check on the parsed push.yaml also passes:
cancel-in-progressis${{ github.run_attempt == '1' }}.ghcases still pass.As a negative control, the "no groups" check from #10016 fails on this file, as it should.
axiom: n/a: CI infrastructure change
🤖 Generated with Claude Code