Prerequisites
- Python 3.10+
- No other runtime dependency — the test suite mocks every GitHub API call (
respx); you do not need a real GitHub token to develop or run tests. dockeronly if you’re changingDockerfile/action.ymland want to build the Action image locally.
Setup
pip install -e ".[dev]"
Repository Structure
src/repo_policy/ the package — see ARCHITECTURE.md for what each module owns
tests/ one test file per source module, plus test_idempotency.py and
test_policies_parity.py (cross-cutting regression guards)
docs/adrs/ why key decisions were made
docs/ci-cd.md release pipeline
docs/test-strategy.md what's tested and why
docs/troubleshooting.md
action.yml, Dockerfile the GitHub Action
.github/workflows/ ci.yml (lint, typecheck, a test matrix across every supported Python
version with coverage/format-check enforcement, package build, a wheel-
install smoke test, self-policy validation, workflow security scan, Docker
image build+scan); release.yml (semantic-release, signed release tag, PyPI
publish, SBOM + attestation); security.yml (pip-audit, CodeQL, a second
zizmor pass); policy-audit.yml (self-governance, read-only); e2e.yml
(nightly/manual `pytest -m e2e` against the live fixture repo)
Before opening a PR
ruff format --check src tests
ruff check src tests
mypy src
pytest --cov=repo_policy --cov-branch --cov-report=term-missing --cov-fail-under=95
CI runs this same test suite on every Python version repo-policy claims to support (3.10, 3.11,
3.12, 3.13, 3.14 — see requires-python in pyproject.toml), plus a wheel-install smoke test on
the oldest and newest of those. Total branch coverage must stay at or above 95%
(--cov-fail-under=95), and any formatting drift from ruff format --check blocks merging — run
ruff format src tests to fix it before committing.
Commit messages
This project uses Conventional Commits — releases and
version bumps are automated by python-semantic-release from commit history:
feat: ...→ minor version bumpfix: ...→ patch version bumpfeat!: ...or aBREAKING CHANGE:footer → major version bumpchore:,docs:,test:,ci:→ no release
Verifying a Release
Before trusting a downloaded release artifact (a wheel/sdist from PyPI, or the Docker Action
image), you can independently check each of the following. SECURITY.md’s “Verifying release
artifacts” and “Release Signing” sections have the full reasoning and threat model behind each
command; this is the quick-reference command list.
# 0. Fetch what was actually published — never a fresh local rebuild. Wheel/sdist builds are not
# proven byte-reproducible here (hatchling's own `Generator:` metadata line alone varies by
# version), and comparing a local rebuild's hash against a published digest is exactly the
# "rebuild and diff a hash" anti-pattern SECURITY.md's own Docker-digest discussion warns
# against — it produces false "tampering" conclusions from ordinary build-tool variance, not a
# real integrity signal. The wheel/sdist live on PyPI; the two SBOMs are GitHub release assets.
mkdir -p /tmp/repo-policy-verify && cd /tmp/repo-policy-verify
curl -s https://pypi.org/pypi/repo-policy/<version>/json | jq -r '.urls[].url' | xargs -n1 curl -sLO
gh release download v<version> --repo shipsolid/repo-policy # the two SBOM files
# 1. Release signature — the release tag itself, will carry a verifiable SSH signature from the
# dedicated release-bot identity once Task 10's signing pipeline is live (one-time: register the
# bot's public key as a trusted signer for its committer email). Runs in its own clone, not the
# /tmp/repo-policy-verify scratch dir above, since verifying a tag needs an actual repository.
git clone --quiet https://github.com/shipsolid/repo-policy.git /tmp/repo-policy-verify-git
cd /tmp/repo-policy-verify-git
git verify-tag v<version>
cd /tmp/repo-policy-verify
# 2. GitHub artifact attestations (Sigstore-backed build provenance), against the files fetched in
# step 0 above — never a local rebuild
gh attestation verify repo_policy-<version>-py3-none-any.whl --owner shipsolid
gh attestation verify repo_policy-<version>.tar.gz --owner shipsolid
gh attestation verify sbom-wheel.cdx.json --owner shipsolid
gh attestation verify sbom-docker.cdx.json --owner shipsolid
# 3. Wheel digest — confirm the download itself wasn't corrupted or tampered with in transit, by
# comparing it against the digest PyPI's own JSON API reports for that exact file (a
# transit-integrity check on a downloaded artifact, not a build-reproducibility claim)
sha256sum repo_policy-<version>-py3-none-any.whl
curl -s https://pypi.org/pypi/repo-policy/<version>/json | jq -r '.urls[].digests.sha256'
# 4. Container digest — the Docker Action image's attestation is keyed by digest, not a registry
# reference (this project never pushes the image to a registry); see SECURITY.md for why the
# digest itself isn't reproducible across rebuilds and what a digest-based attestation does and
# doesn't prove. Confirm the attestation exists for the digest you built:
gh api repos/shipsolid/repo-policy/attestations/<digest>
# 5. SBOM presence — both CycloneDX SBOMs are attached to the GitHub release
gh release view v<version> --json assets --jq '.assets[].name' | grep -E 'sbom-(wheel|docker)\.cdx\.json'
Adding a new policy field
- Add the field itself to the relevant model (
BranchPolicy,PullRequestPolicy,StatusChecksPolicy, orRepoSettingsPolicy) insrc/repo_policy/models.py. - For a
BranchPolicyfield, add oneFieldSpecentry toFIELD_SPECSinsrc/repo_policy/models.py— its permissive (no-op) default, whether its boolean polarity is inverted, whether GitHub Rulesets can represent it at all (ruleset_supported), and its display label. This is the single source of truth:diff._FIELDS/_SCHEMA_DEFAULTS/_INVERTED_FIELDS,models._RULESET_UNSUPPORTED_FIELDS,render._LABELS, andtests/test_policies_parity.py’s ruleset-unsupported set are all derived fromFIELD_SPECS— there is no second table to hand-edit for any of those anymore (seeFieldSpec’s own docstring for why that consolidation exists). - Map it to both backends in
src/repo_policy/policies/branch_protection.pyandsrc/repo_policy/policies/rulesets.py— unlessruleset_supported=False, in which case the ruleset backend only needs it threaded through_RULESET_UNSUPPORTED_FIELDS(already derived from step 2) rather than a real translation. - Add coverage in each affected test file — the diff engine (
tests/test_diff.py), both translators, the idempotency test (tests/test_idempotency.py), and aRESTRICTIVE_VALUESentry for the new field intests/test_policies_parity.py.