PR Guard
An open-source CLI that tells you how many people should review a pull request, with a small model on your CI runner.
pr-guard reads the diff and the description of a pull request, asks the model
TS-PRGuard five questions about it, adds deterministic checks (secrets,
destructive SQL, sensitive paths, size) and lets a YAML policy decide: no reviewer, one or two, or blocked. It
comments on the pull request, labels it and sets one pr-guard status, on GitHub, GitLab or Bitbucket.
It is open source (Apache-2.0): typestrict-lab/pr-guard. TS-PRGuard is a
model trained on typestrict.ai (150M parameters, an 8-bit ONNX file of 165 MB, Apache-2.0 too). On a stock
ubuntu-latest runner a check took 11 to 71 seconds and 460 MB to 1.9 GB of memory on pull requests of 48 to 1,937
changed lines. Memory stops growing once the diff fills what the model reads (5,000 characters per question, 14,000
for breaking change).
No reviewer is not a merge: the policy asks for no extra human approval. pr-guard comments, labels and sets a status,
and never merges; auto_merge: true only adds the pr-guard:auto-merge label for your own automation.
Quick start
# .github/workflows/pr-guard.yml
on: pull_request
permissions:
contents: read
pull-requests: write # the comment and labels
statuses: write # the pr-guard status
jobs:
pr-guard:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: typestrict-lab/pr-guard@v1@v1 is the action's major version; the binary it installs is v0.1.0. The action installs a released binary, caches the model and runs pr-guard check with the workflow's token.
fetch-depth: 0 brings the history, so the merge base with the target branch exists.
On a laptop:
curl -fsSL https://raw.githubusercontent.com/typestrict-lab/pr-guard/main/install.sh | sh
pr-guard check # this branch against its merge base
pr-guard check --staged --no-model # the staged changes, rules only, instantShadow and enforce
It starts in shadow mode: it reports on every pull request, the status always passes and says what it would ask
for. When its answers look right on your code, write mode: enforce in .github/pr-guard.yml and make pr-guard a
required status check. In enforce mode the status fails when the policy blocks, and waits until the pull request has
the approvals the verdict asks for.
The questions and the default policy
The model answers each question with the probability of every answer, in one pass on the CPU. Macro-F1 on held-out examples, from the model card:
| The model is asked | TS-PRGuard | Jev | Default in the policy |
|---|---|---|---|
| Does it do what the description says? | 0.928 | 0.807 | warns above 40% that it does not, blocks above 70% |
| How big is it to review? | 0.660 | 0.383 | warns when very large |
| Does it break existing users? | 0.870 | 0.910 | warns above 50% |
| Do the tests cover it? | 0.921 | 0.879 | warns above 50% that they do not |
| Does it introduce a vulnerability? | 0.904 | 0.755 | warns above 70% |
It beats Jev on 4 of 5 questions by the Refund calculation; on compatibility Jev is ahead.
The answers weigh into a score: 85% or more needs no reviewer, 60% or more one, below that two. A warning asks for at
least one reviewer, a block for two. The comment shows the arithmetic for each pull request. Every key is optional;
pr-guard init prints the whole default, commented:
mode: shadow # or enforce
model: pr-guard:1
reviewers: { none: 85%, one: 60% }
questions:
vulnerability: { weight: 3, warn: 70%, block: 90% }
rules:
secrets: block
sensitive_paths: { level: warn, paths: [infra/**, "**/auth/**"] }Each key is explained in docs/policy.md.
Choosing the model
--model, else $PR_GUARD_MODEL, else model in the policy:
| Value | What it is |
|---|---|
pr-guard:1 (default) | TS-PRGuard v1, pinned: the exact files this release of pr-guard was tested with, checked by SHA-256. |
pr-guard:latest | The newest TS-PRGuard published on Hugging Face, before a pr-guard release pins it. |
hf:<owner>/<repo>[@<revision>] | Any Hugging Face repository with the same files, such as your own model. $HF_TOKEN for a private one. |
https://… | A base URL serving decision_ai.json, tokenizer.json and onnx/model_8bit.onnx (a CDN or an internal mirror). |
| a directory | Used as it is. |
The model is downloaded once, checked file by file against its SHA-256 and cached in ~/.cache/pr-guard.
Your own model
TS-PRGuard learned from public pull requests. To have one that learned from yours, train a model on typestrict.ai with your pull requests as data (see Trainings), then point the policy at it:
model: hf:your-org/your-pr-modelPublishing a model puts the package in a Hugging Face repository with exactly the files
pr-guard reads. To keep it private, download the package (typestrict pull <model>) and serve it from your own URL, or
use the directory directly.
Other CIs and hosts
The full setup for each is in docs/ci.md and docs/hosts.md:
- GitLab CI: a CI/CD component on merge request pipelines; findings go to Code Quality and the test report.
- Bitbucket Pipelines: a pipe on the Docker image; the JUnit report shows on the pull request.
- CircleCI: an orb with
installandcheck; pass the target branch asbase. - Bitrise: a step that writes the JUnit report to the Tests tab.
- Jenkins: a Jenkinsfile example that runs the Docker image on pull request builds.
- pre-commit: a hook that runs the rules on the staged changes, without the model.
- Anything else: the install script, Homebrew, Docker (
ghcr.io/typestrict-lab/pr-guard:0.1.0) or Cargo, thenpr-guard check.
On GitLab it needs a GITLAB_TOKEN with the api scope, on Bitbucket a BITBUCKET_TOKEN; Bitbucket has no labels.
Privacy
No API, no key, no telemetry. The model runs on the CI runner, the code never leaves it, and the only calls pr-guard makes are the model download and the comment, labels and status on your own host. The action never runs code from the pull request: it only reads the diff that the checkout brought.