Repository navigation
Conversation
The Test job took 59 minutes (run 38075384588): 2.6 minutes of YAML tests, then 55.8 minutes of pytest on four balanced xdist workers, 220 worker-minutes in all. No test dominates: 51 property-test files take 74% of it and the slowest test takes 3.5%. So the job is split rather than one test fixed. - `--shard K/N` (tests/conftest.py) keeps the tests whose node id hashes to shard K of N. The shard is a function of the node id alone, so every collected test is in exactly one shard, including tests added later. - The workflows run the YAML tests in one job and the Python tests as a matrix of three shards, N taken from the size of the matrix. - A final `Test` job runs .github/check_test_shards.py over the shards' JUnit files and an unsharded `pytest --collect-only -q` listing. It fails unless every collected test ran in exactly one shard and none failed. - Coverage is measured with sys.monitoring (COVERAGE_CORE=sysmon) instead of the C trace function. On one property-test file the two report the same 12,262 of 18,401 lines, and sys.monitoring adds 6% to the CPU time where the trace function adds 33%. Refs #2099 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…first On 10 October the 5-second final Test job waited 32 minutes for an ubuntu-latest runner after the shards finished (run 38085920460). It now runs on ubuntu-24.04-arm like the shards. The job-result check runs first, and the shard check still runs after a failure so its report names the failed tests. Record the measured job times by the timeouts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… it to fail #1968 (the model's 2023-24 Class 4 LPL was £11,908, not the £12,570 NICA 2022 s2 sets from 6 April 2023) was fixed by #2058 on 10 October. The non-strict xfail kept for it now passes silently as an XPASS, so the alignment test protected nothing for that year. Remove the marker so a regression to £11,908 fails the suite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The release workflow's Publish job needed only Versioning and checked out moving main, so on 11 October 2.126.6 reached PyPI while the merge's Test job was still running (runs 38136097710 and 38136097762). Versioning failed 20 times in two weeks when two runs raced to push the version commit. - versioning.yaml: make the version commit on the latest main (one run at a time), build the wheel and sdist once at that exact commit, check them against that commit's tree, run Lint, an install check on every supported Python and the full test suite on the installed wheel, and only then upload those same files, check PyPI serves them, and tag the commit. Pull requests that change this machinery run it as a dry run. - test-suite.yaml: one reusable test suite for pull requests, main and releases, in the locked environment (uv sync --frozen), from the source tree or an installed wheel. - check_test_shards.py now reads per-test outcome logs keyed by raw node id (pytest_outcome_log.py) instead of JUnit, and fails on any skip, expected failure, XPASS or skipped module that no reviewed entry in test_outcome_allowlist.toml accepts. xfail markers are strict. - code_changes.yaml: run on pyproject.toml, uv.lock and Makefile changes. The scripts the release jobs call (prepare_release.py, release_artifacts.py and the new publish-git-tag.sh) follow in the next commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e, tag - prepare_release.py makes the version commit on the latest main and pushes it, never forcing. When the push is rejected because main moved, it starts again from the new tip; any other rejection fails at once. It refuses a commit that changes more than the version line, CHANGELOG.md and the fragments it consumed. A main whose tip is already a version commit has nothing to release: a queued run used to cut an empty release because changelog.d always holds untyped files towncrier never consumes. - release_artifacts.py check-dist proves the wheel and sdist hold exactly the policyengine_uk/ files tracked at the version commit, byte for byte, with that version in their metadata. pypi-state classifies what PyPI serves for the version as absent, partial, complete or conflict, so Publish never uploads over different bytes and can confirm PyPI serves exactly ours. A local rebuild of 6d89338 is 'complete' against PyPI's 2.126.8. - publish-git-tag.sh tags the given SHA (not HEAD) with an annotated tag, is a no-op on a re-run, never moves a tag, and fails on any error instead of 'git push --tags || true'. Tests use real git repositories with a bare remote, a stand-in towncrier and hand-built wheels and sdists; the classification and artifact checks are property-tested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pr_code_changes.yaml runs only when code, tests, workflows, dependencies or the Makefile change. GitHub keeps a required check pending forever when a path filter skips its workflow, so requiring "Test / Result" would block a documentation-only pull request. pr_code_unchanged.yaml triggers on exactly the other pull requests and reports "Lint" and "Test / Result" as passes, through the suite's new skip input. A test keeps the two path lists complementary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… workflow The companion workflow on paths-ignore was not the complement of the pull request workflow's paths: paths-ignore runs when any file falls outside the list, so a pull request that changed code and documentation ran both, and the companion's passing "Test / Result" could satisfy a required check while the real one failed. Remove it. pr_code_changes.yaml now runs on every pull request to main. A Changes job lists the changed files (unquoted, renames as both paths) and .github/changed_code.py says whether any is code, by the main push filter's patterns, erring towards yes. With no code changed, the other jobs are skipped and the suite's Result passes without running, so the required checks still report. Tests keep the patterns equal to code_changes.yaml's push paths and check the classifier against a reference statement of a paths filter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 11, 2026
… bypass Requiring status checks on main would also block the release workflow's direct push of the version commit, so the pusher needs to be a ruleset bypass actor. With the repository variable RELEASE_PUSH_AS_APP set to true, Versioning checks out with the org GitHub App's token (from the existing APP_ID and APP_PRIVATE_KEY secrets) and pushes as the App. An admin can then add a ruleset with the App as bypass actor, set the variable and activate the ruleset, without another code change. An App push triggers workflows, so the main push tests skip version commits, which the release workflow already tests on the wheel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main push runs were cancelled when a newer commit landed, so 83001d5 and cfa0c55 never finished a Test on 11 October, and a conflict between #2251 and #2270, each green alone, showed only at 7343f94. Remove the main push workflow's concurrency group so every merged commit finishes its run. pr_code_changes.yaml also runs on merge_group, so a merge queue on main can require the same checks and test each pull request merged onto the latest main before it lands; the Changes job compares a group with the main it merges onto. Main then moves only to a commit the queue tested, so the main push workflow's new Tested job skips the suite for a commit that already has a passing "Test / Result" check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st check
uv.lock pinned policyengine-core 3.32.27, pandas 2.3.1, numpy 2.1.3 and
pytest 8.4.1, while CI installed the newest versions and so tested with
core 3.33.2, pandas 3.0.6, numpy 2.5.4 and pytest 9.1.1 (run 38149483980),
and policyengine.py, which runs the model in production, locks core 3.33.0
and pandas 3.0.5. Testing in the frozen environment from that lock would
have tested a dependency generation nobody runs. Refresh it with
uv lock --upgrade, so the locked environment is what CI tested until now.
On this PR's first run in the old lock's environment, two of the three
test shards lost their runners mid-run ("The runner has received a
shutdown signal", run 38153608009) while other pull requests' runs at the
same time did not. One property file's peak memory was the same in both
environments locally (0.80 and 0.81 GB), so the cause is not confirmed;
each shard now logs its memory every minute so a repeat would show it.
uv build writes a .gitignore into its output directory, which the
release check rightly refused as an extra file; remove it after building.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
|
Coordination note from #2324 (untyped changelog fragments, #2314). #2324 deletes the 93 untyped files in
It does not edit On a local merge of both branches, six tests in
A patch for this branch is in
🤖 Generated with Claude Code |
Each pytest-xdist worker's memory grows with the tests it runs (#2330). With three shards of four workers, the 16 GB ARM runners peaked at 15.6 GB used, 283 MB available, on this PR's second run (run 38156967136, logged by the per-minute memory lines), and two shards of the first run lost their runners mid-run. Four shards give each worker about a quarter fewer tests, which also shortens the job. Replace the stale 'about 9 GB' comment with the measured peak. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- check-dist checked only the first 50 sdist files outside policyengine_uk/, because the listing cap also capped the check (found by a partial GPT-6.1 Sol review). Check them all; cap only the listing. A regression test puts a changed README after 60 other files. - 'Re-run all jobs' re-ran Versioning, which found main ending in the version commit the first attempt pushed and released nothing: a green run with no release. On a re-run attempt, an untagged version commit at main's tip is now released. - The release artifacts overwrite an earlier attempt's, so a re-run of Build can upload them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5 tasks done
The same Lint step as #2355, in the pull request, main and release workflows this PR rewrites, so whichever of the two lands second keeps it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ruff formats and checks notebooks under docs/ and scripts/ as well as the package, so a documentation-only pull request can land a violation that then fails Lint for every later pull request: #1696 landed a notebook that failed the format check, and #1743 fixed it. Lint takes seconds, so run it on every pull request instead of only when the Changes job finds code. Found by the independent review of #2355. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Suite half (request changes): - A merge queue group always runs the tests. Its base_sha can be the group queued ahead rather than main, so diffing against it could call a code change 'no code' and pass Test / Result without running anything. - Main's Tested job counts only a successful merge_group run of the pull request workflow, not any passing 'Test / Result' check on the commit, which a pull request run on the same head commit (perhaps without the dataset) could also have made. - The suite skips only on an explicit code=false. - New require-data input: main pushes, merge queue groups and releases fail Result instead of accepting microsimulation skips when the dataset credentials are missing. - The outcome log keeps a skipped subtest (pytest 9) that its test's later passing call used to hide; make test-ci fails when pytest itself errors. - conftest.py, .gitignore and README.md count as code. Release half (approve, non-blocking): - Build with hatchling and its dependencies pinned (.github/build-constraints.txt); with these pins a build of 6d89338 reproduces PyPI's 2.126.8 wheel and sdist byte for byte, so a rebuild after a partial upload matches. - prepare_release.py --verify: one parent, the version subject, and only versioning changes; used for the release-sha input and the re-run path. It accepts main's real 6d89338 (2.126.8). - A push that landed although git reported a failure counts as pushed. - One Publish per version at a time. - The tag script fails when it cannot list origin's tags, and accepts a tag another run pushed at the same commit. - A new job installs the sdist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lookup - Set POLICYENGINE_UK_REQUIRE_TEST_DATA=1 (W3's conftest switch, #2335) wherever require-data is on: main pushes, merge queue groups and releases. A test that needs the dataset then fails with its own message instead of skipping. Unused until #2335 lands. - A manual dispatch of the main workflow always tests; the merge queue lookup applies only to pushes, and a failed lookup parses to zero. - Document the re-run path's residual and where docs-only pull requests build the documentation; the workflow-shape test covers the release workflow's require-data and the lookup. From the GPT-6.1 Sol delta review of 96e9cec (APPROVE, non-blocking). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#2324 makes bump_version.py count only the fragments towncrier consumes and exit when there are none. prepare_release.py now uses that script's own classification when it has one (so a commit with only untyped files releases nothing) and counts every file otherwise, as before. The tests pass with main's bump_version.py and with #2324's, so the two pull requests can land in either order. Based on the patch from #2324's session. Also merges #2303 (the stale #1968 xfail), which this pull request's own check rejects as an XPASS until it lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…olicyengine.py - CI on 0879f6c found 66 skips without an allowlist entry, all from test_projection_continues_from_last_published_rate (new on main from #1925): it is parametrised over every DWP rate parameter and skips those it does not apply to (not uprated, series ending before 2026-27, the two Universal Credit Act 2025 exceptions). One entry accepts exactly those three reasons for that test. - prepare_release.py reads the tip's bump_version.py without writing .github/__pycache__ into the checkout; the test's stand-in towncrier, which imported it, left a .pyc that its by-hand version commit then committed (CI writes bytecode; the local run did not). A test checks the checkout stays clean. - Publish, after PyPI serves exactly the tested files, sends policyengine.py the repository dispatch W4's certify-uk-release.yaml expects (event_type policyengine-uk-release, client_payload.version), with a token from the org App limited to that repository. Both steps continue on error: W4 also catches up every two hours, so a lost dispatch must never fail a release. - Publish runs in a 'pypi' environment, where the PyPI credential is to move, limited to main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Give the token and dispatch steps a 2-minute timeout, so a hang cannot use up the Publish job before the tag step. - Ask for contents: write only, which is all a repository dispatch needs. - Say what the pypi environment does today: limiting it to main is an admin setting, not set by this workflow. - Turn bytecode writing on in the clean-checkout test, so it tests prepare_release.py rather than the machine's PYTHONDONTWRITEBYTECODE. - Assert the dispatch condition and token scope in the shape test. - The DWP allowlist entry names the five parameters the Universal Credit Act 2025 ss.1 and 3 set apart, not two. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #2269 (test sharding) and includes #2303 (stale #1968 xfail), so their commits show here until they merge. Part of workstream W2 of the Autumn Budget CI plan: no policyengine-uk release reaches PyPI unless the exact files published passed the required checks.
The defect
Publishinversioning.yamlneeded onlyVersioningand checked out movingmain. On 11 October, Test for the merge 3fd35c1 started at 11:35:10Z (run 38136097762). Publish checked out the version-bump child 4d80b29 and uploaded 2.126.6 from 11:36:12Z (run 38136097710), while that Test was still running.mainwas and rangit push --tags || true.pyproject.toml,uv.lockandMakefile.Change
Release identity (
versioning.yaml, withprepare_release.py,release_artifacts.pyandpublish-git-tag.sh):main, one run at a time, never forcing. It retries whenmainmoves and fails on any other rejected push. It refuses a commit that changes more than the version line, the changelog and the consumed fragments. It counts fragments the way the tip'sbump_version.pydoes, so it works before and after Fail pull requests that add changelog fragments towncrier drops #2324. Amainwhose tip is already a version commit has nothing to release.uv buildonce, with the build backend pinned (.github/build-constraints.txt).check-distproves the wheel and sdist hold exactly thepolicyengine_uk/files tracked at that SHA, byte for byte, with that version in their metadata. The files' provenance is attested.pypienvironment, one run per version at a time. It re-verifies the hashes andcheck-dist, andpypi-staterefuses if PyPI holds other bytes under the version. It then uploads the same files and waits until PyPI serves exactly them. Next it sends policyengine.py apolicyengine-uk-releaserepository dispatch with the version (W4's certification; that step can't fail the release). Last, it tags that SHA: annotated, with the digests in the message, idempotent, and never moving a tag.release-shadispatch input re-releases an existing version commit afterprepare_release.py --verifychecks it.One test suite (
test-suite.yamland thetest-environmentaction), called by PR, merge-queue, main and release runs:uv sync --frozen --extra dev), from the source tree or an installed wheel.uv.lockis refreshed to what CI and production install (policyengine-core 3.33.2, pandas 3.0.6, pytest 9.1.1); the old lock was stale.Test / YAML,Test / Python K/4andTest / Result.POLICYENGINE_UK_REQUIRE_TEST_DATA=1.Test / Resultfails if the dataset wasn't present. Pull request runs don't require it.No silent skips:
pytest_outcome_log.pyrecords each test's outcome under its raw node id, including subtests. It detects XPASS.check_test_shards.pyfails on any failure, missing or duplicate run, skip, xfail, XPASS or module skip thattest_outcome_allowlist.tomldoesn't accept for this mode and dataset. There are seven entries:xfail_strict=true.make test-cireproduces the check locally.Required checks become requirable:
pr_code_changes.yamlruns on every PR tomainand onmerge_group. A Changes job (changed_code.py) decides whether code changed. If none did, the tests skip andTest / Resultpasses without running, so a required check never hangs on a path filter. Lint runs on every PR, withruff check(the same steps as Run ruff check in Lint, with F811 and F821 on for the package #2355).RELEASE_PUSH_AS_APP=true, the org GitHub App pushes the version commit, so a ruleset can let that App bypass required checks.Main push (
code_changes.yaml):pyproject.toml,uv.lock,Makefile,conftest.py,.gitignoreandREADME.md.Invariants
check-distproved they hold exactly the version commit's tracked package files. That commit is the one tagged. Covered bytest_release_artifacts.py: property tests for any single changed byte, plus fault cases.classifyis checked differentially against an independent statement of the rule.test_prepare_release.py, using real git with a bare remote, with bothbump_version.pyversions.test_ci_test_shards.py).Test / Result: the PR workflow has no path filter, and the code patterns equal the main push filter (test_ci_test_shards.py).continue-on-errorand a 2-minute timeout, and the token is scoped to policyengine.py withcontents: write(test_ci_test_shards.py).Verified
uv buildof 6d89338 with the pinned backend reproduces PyPI's 2.126.8 wheel and sdist byte for byte.check-distpasses them against that commit, andpypi-statereportscompleteagainst live PyPI.test_ci_test_shards.py,test_prepare_release.pyandtest_release_artifacts.pypass locally. ruff and actionlint are clean, and shellcheck is clean onpublish-git-tag.sh.Reviews
All in
~/reviews/budget-ci-w2/:review-2306-release-out.mdandreview-2306-suite-out.md; deltareview-2306-delta-96e9cecae.md; finalreview-2306-final-0879f6cf6.md(APPROVE).review-2306-delta2-496bfdbec.md(APPROVE). Its non-blocking suggestions are applied in 6379d45.Status
~/reviews/budget-ci-w2/admin-request.md, decision d1368):RELEASE_PUSH_AS_APP;Test / Result,Lint,Fix evidenceandChangelog fragments;pypienvironment to main and movePYPIinto it, or use trusted publishing.axiom: n/a: CI and release infrastructure
🤖 Generated with Claude Code