Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
38c63df
Publish only the release files whose checks passed
MaxGhenis Oct 11, 2026
f9481f4
Add the release scripts: version commit, artifact identity, PyPI stat…
MaxGhenis Oct 11, 2026
7071795
Report the required test checks on pull requests that change no code
MaxGhenis Oct 11, 2026
94401c2
Decide inside the PR workflow whether code changed, not with a second…
MaxGhenis Oct 11, 2026
2c13a21
Let the GitHub App push the version commit when a branch rule needs a…
MaxGhenis Oct 11, 2026
fafdb9a
Test every commit that lands on main, and support a merge queue
MaxGhenis Oct 11, 2026
a6050ea
Lock the test environment to current dependencies; fix the release di…
MaxGhenis Oct 11, 2026
827b277
Split the Python tests over four runners: three nearly ran out of memory
MaxGhenis Oct 11, 2026
a945b2a
Check every sdist file, and make a full re-run release what it versioned
MaxGhenis Oct 11, 2026
29afff6
Run ruff check in every Lint job, with F811 and F821 on for the package
MaxGhenis Oct 11, 2026
9d23d65
Lint every pull request, not only those that change code
MaxGhenis Oct 11, 2026
96e9cec
Address the split GPT-6.1 Sol review of #2306
MaxGhenis Oct 11, 2026
78036fe
Wire the tests' mandatory-data mode and harden main's already-tested …
MaxGhenis Oct 11, 2026
352af4c
Merge remote-tracking branch 'origin/ci-test-runtime-2099' into relea…
MaxGhenis Oct 11, 2026
4705430
Merge remote-tracking branch 'origin/main' into release-identity
MaxGhenis Oct 11, 2026
294ddc1
Merge remote-tracking branch 'origin/remove-stale-xfail-1968' into re…
MaxGhenis Oct 11, 2026
0879f6c
Count fragments as the tip's bump_version.py does, old or new
MaxGhenis Oct 11, 2026
496bfdb
Allowlist the DWP rate test's filter skips; dispatch the release to p…
MaxGhenis Oct 12, 2026
6379d45
Bound the policyengine.py dispatch steps and scope their token
MaxGhenis Oct 12, 2026
497a73c
Say where the PyPI credential lives; pin the tag step after the dispatch
MaxGhenis Oct 12, 2026
04ff99d
Merge remote-tracking branch 'origin/main' into release-identity
MaxGhenis Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions .github/actions/test-environment/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
name: Test environment
description: >
Install the locked test environment (uv.lock, with the dev extra) and the
policyengine-uk to test: the checked-out source tree, or a built wheel.
Puts the environment's bin directory first on PATH and says where the tests
are.

inputs:
dist-artifact:
description: >
Name of an artifact holding the built wheel. Empty to test the
checked-out source tree instead.
default: ""
wheel-sha256:
description: The sha256 the wheel in dist-artifact must have.
default: ""

outputs:
mode:
description: "source or wheel"
value: ${{ steps.install.outputs.mode }}
tests:
description: The policyengine_uk/tests directory to run.
value: ${{ steps.install.outputs.tests }}
rootdir:
description: The pytest rootdir, so node ids start with policyengine_uk/tests.
value: ${{ steps.install.outputs.rootdir }}

runs:
using: composite
steps:
- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Download the wheel
if: ${{ inputs.dist-artifact != '' }}
uses: actions/download-artifact@v8
with:
name: ${{ inputs.dist-artifact }}
path: ${{ runner.temp }}/dist
- name: Install the locked environment and policyengine-uk
id: install
shell: bash
env:
DIST_ARTIFACT: ${{ inputs.dist-artifact }}
WHEEL_SHA256: ${{ inputs.wheel-sha256 }}
run: |
set -euo pipefail
if [ -z "$DIST_ARTIFACT" ]; then
# The source tree, installed editable into .venv with the locked
# dependencies. --frozen installs uv.lock exactly as committed.
uv sync --frozen --extra dev --python 3.13
echo "$PWD/.venv/bin" >> "$GITHUB_PATH"
{
echo "mode=source"
echo "tests=policyengine_uk/tests"
echo "rootdir=$PWD"
} >> "$GITHUB_OUTPUT"
exit 0
fi

# The wheel the release will publish, and nothing else.
shopt -s nullglob
wheels=("$RUNNER_TEMP"/dist/*.whl)
if [ "${#wheels[@]}" -ne 1 ]; then
echo "::error::expected one wheel in $DIST_ARTIFACT, found ${#wheels[@]}"
exit 1
fi
wheel="${wheels[0]}"
actual=$(sha256sum "$wheel" | cut -d' ' -f1)
if [ -z "$WHEEL_SHA256" ] || [ "$actual" != "$WHEEL_SHA256" ]; then
echo "::error::$(basename "$wheel") has sha256 $actual; expected '$WHEEL_SHA256'"
exit 1
fi
echo "Testing $(basename "$wheel") (sha256 $actual)"

# The locked dependencies, without the project, in an environment
# outside the checkout. The wheel goes in on top, without resolving
# anything else.
venv="$RUNNER_TEMP/venv"
UV_PROJECT_ENVIRONMENT="$venv" uv sync --frozen --extra dev --no-install-project --python 3.13
uv pip install --python "$venv/bin/python" --no-deps "$wheel"

# Remove the checkout's copy of the package, so nothing can import
# the source tree instead of the wheel.
rm -rf policyengine_uk

# Confirm from an empty directory (Python puts the current directory
# on sys.path) that the import is the installed wheel.
mkdir -p "$RUNNER_TEMP/empty"
package=$(cd "$RUNNER_TEMP/empty" && "$venv/bin/python" - "$venv" <<'EOF'
import importlib.metadata
import pathlib
import sys

import policyengine_uk

package = pathlib.Path(policyengine_uk.__file__).resolve().parent
venv = pathlib.Path(sys.argv[1]).resolve()
if venv not in package.parents:
sys.exit(f"policyengine_uk was imported from {package}, not {venv}")
print(package)
EOF
)
installed=$("$venv/bin/python" -c 'import importlib.metadata as m; print(m.version("policyengine-uk"))')
echo "Installed policyengine-uk $installed at $package"
echo "$venv/bin" >> "$GITHUB_PATH"
{
echo "mode=wheel"
echo "tests=$package/tests"
echo "rootdir=$(dirname "$package")"
} >> "$GITHUB_OUTPUT"
13 changes: 13 additions & 0 deletions .github/build-constraints.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# The build backend the release workflow builds with (uv build
# --build-constraint), pinned with its dependencies so a rebuild of the same
# commit gives the same bytes. With these pins, a build of 6d89338ab
# reproduces PyPI's policyengine-uk 2.126.8 wheel and sdist byte for byte.
# A rebuild must match: if an upload stopped part way, "Re-run all jobs"
# rebuilds, and Publish refuses a file that differs from what PyPI has.
# Update these deliberately, in their own pull request.
hatchling==1.32.4
packaging==26.3
pathspec==1.1.1
pluggy==1.6.0
tomlkit==0.15.1
trove-classifiers==2026.9.21.13
52 changes: 52 additions & 0 deletions .github/changed_code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Say whether a pull request changes code, for pr_code_changes.yaml.

git -c core.quotePath=false diff --name-only --no-renames -z BASE HEAD > changed
CODE_PATHS="policyengine_uk/** ..." python3 .github/changed_code.py changed

prints ``code=true`` or ``code=false`` for ``$GITHUB_OUTPUT``. A path is code
if it matches one of ``CODE_PATHS`` (whitespace-separated): ``dir/**`` matches
every file under ``dir/``, anything else matches that one file, as in a
workflow's ``paths`` filter. An empty change list counts as code, so an
unexpected input runs the tests rather than skipping them.

When this says false, the pull request's tests are skipped and the required
"Test / Result" check passes without running anything, so a false negative
would let untested code through: it errs towards true.

Standard library only.
"""

import os
import sys


def is_code(path: str, patterns: list[str]) -> bool:
for pattern in patterns:
if pattern.endswith("/**"):
if path.startswith(pattern[:-2]):
return True
elif path == pattern:
return True
return False


def changes_code(changed: list[str], patterns: list[str]) -> bool:
return not changed or any(is_code(path, patterns) for path in changed)


def main(argv=None) -> int:
argv = sys.argv[1:] if argv is None else argv
patterns = os.environ.get("CODE_PATHS", "").split()
if not patterns or any("*" in p and not p.endswith("/**") for p in patterns):
print(f"CODE_PATHS must be dir/** or file paths: {patterns}", file=sys.stderr)
return 2
with open(argv[0], encoding="utf-8") as file:
changed = [path for path in file.read().split("\0") if path]
code = [path for path in changed if is_code(path, patterns)]
print(f"{len(code)} of {len(changed)} changed files are code", file=sys.stderr)
print(f"code={'true' if changes_code(changed, patterns) else 'false'}")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading
Loading