repo-policy
Lightweight, declarative repository governance for GitHub. Define expected branch protection and ruleset configuration in YAML; audit, preview, and apply it locally or in CI.
Not a Terraform replacement — no state file, no backend. repo-policy is safe to adopt
incrementally on a live repository: by default it only ever touches branches you declare, and
never deletes anything you didn't ask it to manage.
Install
pip install repo-policy
Quick start
# policy.yml
version: 1
branches:
main:
pull_requests:
required: true
approvals: 2
code_owner_review: true
status_checks:
required: [build, test]
signed_commits: true
linear_history: true
allow_force_push: false
allow_deletion: false
repo-policy validate
repo-policy audit --repo acme/widgets
repo-policy plan --repo acme/widgets
repo-policy apply --repo acme/widgets
CLI Reference
| Command | Flags | Behavior | Exit codes |
|---|---|---|---|
--version |
— | Prints repo_policy.__version__ and exits; no network calls |
0 |
validate |
--config (default policy.yml) |
Parses and schema-validates the policy file only; no network calls | 0 valid, 2 invalid |
audit |
--config, --repo, --token |
Read-only compliance check | 0 compliant, 1 drift found, 2 invalid config, 3 API/auth error |
plan |
--config, --repo, --token |
Same diff engine as audit, renders a human-readable +/-/~/✓ preview |
same as audit |
apply |
--config, --repo, --token |
Executes only the changes plan would show, then re-verifies live state from scratch before reporting success |
0 mutations converged (incl. no-op), 1 mutations succeeded but a fresh post-apply check still finds drift, 2 invalid config or setup failure, 3 API/auth error or partial-application failure |
Token resolution order: --token → GITHUB_TOKEN → GH_TOKEN. Repo resolution order:
--repo owner/name → $GITHUB_REPOSITORY → the local git origin remote.
See ARCHITECTURE.md's "Apply Outcomes and Exit Codes" for the full preflight /
mutate / verify breakdown behind apply's row above.
Schema & Validation
repo-policy fails closed on ambiguous or malformed policy.yml input rather than guessing what
you meant (src/repo_policy/models.py, src/repo_policy/config.py):
- Unknown fields are rejected, not silently ignored — every schema model uses Pydantic's
extra="forbid", so a typo likestrcit:orsecret_scaning:is a validation error atvalidatetime, not a quietly-ignored no-op. - No implicit type coercion — every model also sets
strict=True: a quoted boolean (signed_commits: "true"),yes/no/on/off, or a numeric string in place of a real integer is rejected rather than silently coerced. - Duplicate YAML keys are rejected — a custom YAML loader fails a policy file with two
strict:keys, two entries for the same branch name, etc., instead of PyYAML's default silent last-key-wins behavior. pull_requests.approvalsmust be0–6— anything outside that range is rejected before any API call is made.
Supported fields:
| Section | Field | Type / range |
|---|---|---|
| top level | version |
must be 1 |
| top level | strict |
bool (default false) |
| top level | branches |
{branch name: branch policy} |
| top level | repo_settings |
optional, see below |
| per branch | enforcement |
branch_protection (default) | ruleset |
| per branch | strict |
bool, overrides the top-level default for this branch |
| per branch | pull_requests.required |
bool (default true) |
| per branch | pull_requests.approvals |
int, 0–6 (default 1) |
| per branch | pull_requests.code_owner_review |
bool |
| per branch | pull_requests.dismiss_stale_reviews |
bool |
| per branch | pull_requests.require_last_push_approval |
bool |
| per branch | status_checks.required |
list[str], no blank or duplicate entries |
| per branch | signed_commits, linear_history, allow_force_push, allow_deletion |
bool |
| per branch | enforce_admins, required_conversation_resolution, lock_branch, allow_fork_syncing, clear_restrictions |
bool — no GitHub Rulesets equivalent; rejected under enforcement: ruleset unless left at their permissive (no-op) value |
repo_settings |
delete_branch_on_merge, allow_update_branch, vulnerability_alerts, automated_security_fixes, private_vulnerability_reporting, secret_scanning, secret_scanning_push_protection |
bool |
Two cross-field rules are enforced at validation time, before any API call:
allow_fork_syncing: truerequireslock_branch: trueon the same branch — GitHub silently resetsallow_fork_syncingback tofalseotherwise.automated_security_fixes: truerequiresvulnerability_alerts: true, andsecret_scanning_push_protection: truerequiressecret_scanning: true— GitHub rejects enabling either one before its prerequisite.
repo-policy validate checks all of the above against a real policy.yml, entirely offline.
GitHub Action
Pin to an immutable full commit SHA — the same convention this repository's own CI uses for every
third-party Action it consumes (see .github/workflows/ci.yml), and the general supply-chain
hardening recommendation for any Action, including this one:
- uses: shipsolid/repo-policy@a4d736b7fa8268115f50e61b4398d8d6a7ee7e1a # v0.4.7
env:
GITHUB_TOKEN: ${{ secrets.REPO_POLICY_TOKEN }}
with:
config: .github/repository-policy.yml
mode: audit
Every release tag here (v<version>) is an annotated tag object, not a direct pointer to a
commit — so git rev-parse v<version> alone returns that tag object's own SHA, not the commit's,
and pinning uses: to it would silently defeat the point of pinning to a commit. Peel through the
tag to get the actual commit SHA to pin:
git rev-parse v<version>^{commit}
or find it at
github.com/shipsolid/repo-policy/tags → click the
release tag → the commit it points to. (Once the release-bot signing pipeline in SECURITY.md's
"Release Signing" section is live — see its Release Pipeline Setup Checklist for current status —
each of these annotated tags will also carry a verifiable SSH signature; that doesn't change which
SHA to pin here.)
Convenience alternative — @v0: a floating tag tracking the current major version (the same
convention actions/checkout and similar Actions use). Each release force-moves it to nest,
unpeeled, directly on top of that release's own annotated tag object (v0 → v<version> → the
release commit) — this nesting is what will let git verify-tag v0 keep verifying transitively
against the release-bot's signature after every move, once that signing pipeline is live (see
docs/ci-cd.md).
- uses: shipsolid/repo-policy@v0
@v0 is movable, not immutable. What it resolves to changes on every release without any
corresponding change to your own workflow file to review — convenient for staying current
automatically, but unsuitable anywhere change control requires a pinned, auditable dependency (the
same full-SHA-pinning convention this repository's own workflows follow for every third-party
Action they consume — see docs/ci-cd.md). Prefer the full-SHA form above unless
you have a specific reason to track the moving tag instead.
secrets.GITHUB_TOKEN will not work here, in any workflow, no matter what permissions: you
grant it — confirmed against a real repo. GitHub Actions' automatically-generated token has no
permission scope covering branch protection or ruleset administration; that's a platform
constraint, not something a workflow can opt into. Create a PAT with repo scope (classic) or
Administration: Read and write (fine-grained), store it as a repository secret — REPO_POLICY_TOKEN
above is just an example name — and reference that secret instead.
Self-governance
This repository governs itself with its own tool: .github/repository-policy.yml
declares main's branch protection and repo security settings, and
.github/workflows/policy-audit.yml runs repo-policy audit
against it on a daily schedule, on every change to the policy file or that workflow, and on demand.
- The audit workflow is read-only (
mode: audit) and needs aPOLICY_AUDIT_TOKENrepository secret — a fine-grained PAT scoped to this repository only, withAdministration: Read. That secret does not exist yet; creating it is a live-repo setup step for whoever holds admin access. .github/repository-policy.ymldocuments the intended branch protection for this repository — it has not yet been applied. Untilrepo-policy applyruns against the live repository (a manual, reviewed step — see SECURITY.md's Threat Model for whyapplyis never run unattended against a real repo from an untrusted trigger), it does not reflect live GitHub state.- If a change to
main's required status check ever leaves it unable to produce a passingrequiredresult — blocking the very fix that would repair it — see SECURITY.md's "Emergency Recovery" for the documented, auditable bypass procedure.
How it works
Every declared branch is diffed against live GitHub state and reconciled through one of two
backends, selected per branch with enforcement: branch_protection | ruleset (default
branch_protection). See ARCHITECTURE.md for the full data flow, safety model,
and known v1 limitations, and docs/adrs/ for why it's built this way.
Exit codes
This mapping is shared by audit, plan, and apply alike:
| Code | Meaning |
|---|---|
| 0 | Success / compliant / no-op |
| 1 | Drift detected (audit/plan), or apply's mutations succeeded but a fresh, independent post-apply check still finds drift or an unavailable declared setting |
| 2 | Invalid policy.yml, a setup failure before any API call (missing token, unresolvable --repo, GitHub-client construction failure), or a PolicyResolutionError after a successful read (GitHub's live state was unparseable, or resolving the declared policy against it produced an invalid combination) — this last case can happen even with a fully valid policy.yml |
| 3 | GitHub API/auth/transport error, or a partial-application failure (a mutation failed partway through an apply's mutation phase; zero or more earlier resources in that phase may already have succeeded) |
More docs
- ARCHITECTURE.md — design, data flow, safety model
- docs/adrs/ — why the key decisions were made
- docs/ci-cd.md — release pipeline, PyPI publishing, the
v0tag - docs/test-strategy.md — what's tested, and what mocking alone can't catch
- docs/troubleshooting.md · FAQ.md · SUPPORT.md
- SECURITY.md — token permissions and threat model
- CONTRIBUTING.md · ROADMAP.md · CHANGELOG.md
License
MIT
Release files for repo-policy 0.4.10
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| repo_policy-0.4.10.tar.gz | 208.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| repo_policy-0.4.10-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 256.6 kB
Release files / repo_policy-0.4.10.tar.gz
| Download URL | repo_policy-0.4.10.tar.gz |
|---|---|
| Size | 208.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ed910ae29618e2fe2faa7d8f35032306ef402a93a67e4138132075a4c57bdab1
|
|
BLAKE2b-256 checksum How to use checksums |
11011058fbb919fd3a730a5d2f5aa63ff9a94b3f0ac63a424e6ad15cf7a80d5b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.
Transparency logRelease files / repo_policy-0.4.10-py3-none-any.whl
| Download URL | repo_policy-0.4.10-py3-none-any.whl |
|---|---|
| Size | 47.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
814d10adcbd9c9ffe343a9880bf077f390bf0aad8c82bbbeb8142651ce967e30
|
|
BLAKE2b-256 checksum How to use checksums |
e4ffe6ec74fb922249cead7d6b8e45746cfb363730af0320d0370cd3a297911a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.
Transparency log