Five gates judge a pull request on what the change adds, never on the repository's past. None of them needs an index, a model or an API key, and none stores anything.
| Gate | It fails the change when | Needs |
|---|---|---|
repowise coverage check | the tests ran too few of the changed lines | git and a coverage report |
repowise doc-drift --check | a document names a file, link, heading or command that is gone | git |
repowise security check | the change adds a secret or a risky call | git |
repowise health check | the change writes new complex, deeply nested, oversized or duplicated code | git |
repowise risk --fail-above-percentile P | the change is bigger and more spread out than P% of recent commits | git (at least 8 recent commits) |
Every flag is on Coverage and CI gates. What each gate
measures is on Test intelligence,
Documentation drift,
Security signals,
Code health and
repowise risk.
GitHub Actions
The repowise repository is itself an action. It installs repowise, runs the gates you pick, keeps going when one fails so the job reports all of them, and fails the step at the end.
on: pull_request
permissions:
contents: read
jobs:
repowise:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: pytest --cov=src --cov-report=lcov:coverage/lcov.info
- uses: repowise-dev/repowise@v0.60.0 # pin a release tag
with:
checks: coverage,doc-drift,security,health,risk
coverage-report: coverage/lcov.info
coverage-fail-under: 80
risk-fail-above-percentile: 95| Input | Default | Meaning |
|---|---|---|
checks | doc-drift,security | Gates to run: coverage, doc-drift, security, health, risk. Coverage needs a report, and health and risk are policy choices, so all three are opt-in. |
version | latest | repowise version to install. |
base | the pull request's target | Revision range for coverage, security, health and risk. |
coverage-report | coverage.paths, else discovery | Reports, one per line. A path, a glob, or path=prefix. |
coverage-fail-under | coverage.fail_under | Minimum patch coverage percent. |
coverage-base-report, coverage-max-drop | none | Report measured at the base, and the most project coverage may fall, in points. |
doc-drift-baseline, security-baseline | none | Committed baseline files. Only findings they do not list fail. |
security-fail-on | high | Lowest severity that fails: high, med, low. |
fail-on-baseline-growth | false | Fail doc drift and security when their baseline gains an entry over the change's base. |
health-fail-on | introduced | What fails the health gate: introduced, or worsened to fail on findings the change made worse too. |
health-fail-at | high | Lowest severity that fails the health gate: medium, high, critical. |
risk-fail-above-percentile | none | Fail above this percentile. Empty reports the rank without gating. |
upload-sarif | false | Send doc drift, security and health findings to code scanning. Needs security-events: write. |
comment | false | Post one pull-request comment summarizing every gate, edited in place on each run. Needs pull-requests: write; skipped with a warning on a fork's pull request. |
github-token | github.token | Token the summary comment is posted with. A later run edits only the comment this token's account posted. |
impacted-tests | false | Select the tests the change needs. See below. |
More inputs (coverage-min-coverable-lines, coverage-fail-under-risky,
coverage-fail-under-branches, python-version, working-directory,
impacted-tests-runner) are listed in the action's
action.yml.
Outputs coverage, doc-drift, security, health and risk hold each gate's exit
code. verdict combines them: fail when any gate failed or could not
evaluate, warn when any passed with warnings, else pass.
Summary. The job summary ends with a table of every gate and its result.
With comment: true the same table, plus each gate's findings, goes into one
pull-request comment per working directory, and each finding carries a hidden
fingerprint so a later push counts what it resolved. A failing health run
ends each annotation in a one-line fix hint and names the
repowise health check command that reproduces it locally.
Without the action, each gate is one step:
- run: pip install repowise
- run: repowise coverage check --report coverage/lcov.info --fail-under 80 --format github
- run: repowise doc-drift --check --format github
- run: repowise security check --format github
- run: repowise health check --format github
- run: repowise risk --fail-above-percentile 95 --format githubTo combine steps run this way, set REPOWISE_GATE_SUMMARY_DIR to a directory
for every gate and finish with
repowise ci-summary DIR --gate health=$HEALTH_EXIT --gate security=$SECURITY_EXIT.
It appends the verdict table to the job summary and prints pass, warn or
fail; --comment-file, --previous and --comment-key write the
pull-request comment body. A gate that left no result reads as could not
evaluate.
Make it a required check
The action reports through the job it runs in, not a check of its own, so the
check GitHub shows is the job's name: repowise in the workflow above, or
the job's name: when it sets one. In the repository's Settings, open
Rules > Rulesets (or Branches > Branch protection rules) for your
default branch, turn on Require status checks to pass, and add repowise.
The check appears in the list after the workflow has run once.
GitLab
Include the template and set the variables you need:
include:
- remote: https://raw.githubusercontent.com/repowise-dev/repowise/main/ci/gitlab/repowise.gitlab-ci.yml
variables:
REPOWISE_COVERAGE_REPORT: coverage/lcov.info
REPOWISE_COVERAGE_FAIL_UNDER: "80"
repowise-coverage:
needs: [test] # the job that writes the reportIt adds one job per gate on merge request pipelines. repowise-coverage
runs only when REPOWISE_COVERAGE_REPORT is set, and repowise-risk only
when REPOWISE_RISK_FAIL_ABOVE_PERCENTILE is set. repowise-health runs
when REPOWISE_HEALTH: "true" is set, with REPOWISE_HEALTH_FAIL_ON and
REPOWISE_HEALTH_FAIL_AT. Doc drift, security and health also write a Code
Quality report, so their findings show in the merge
request's Code Quality widget. The variable list is at the top of the
template.
Other CI systems
The gates are plain commands. Fetch the target branch, run them, and let the exit code fail the build:
git fetch --no-tags origin "+refs/heads/$CHANGE_TARGET:refs/remotes/origin/$CHANGE_TARGET"
pip install repowise
repowise coverage check "origin/$CHANGE_TARGET...HEAD" --report coverage/lcov.info --fail-under 80
repowise doc-drift --check
repowise security check "origin/$CHANGE_TARGET...HEAD"
repowise health check "origin/$CHANGE_TARGET...HEAD"Change policy
repowise check holds some changes to a higher bar than others. Rules
live in .repowise/policies.yaml, next to the code they govern:
version: 1
rules:
- name: payments-need-tests
match:
components: {tags: [pci]}
when:
reaching_tests: missing
action: require_tests
- name: no-silent-contract-breaks
when:
contract_break: true
action: fail
- name: no-new-secrets
gate: security
with: {fail_on: high}
action: failA rule selects components by name, tag or criticality, then tests
conditions over the change's evidence checklist: provenance, reaching
tests, contract breaks, decisions crossed and bus factor. A rule can
also wrap the security, coverage, doc_drift or health gate, which then runs
exactly as the command does on its own. A condition whose evidence is
not available, for example with no index, is reported as not evaluated
and never fires.
repowise check origin/main...HEAD
repowise check origin/main...HEAD --sarif-file policy.sarif --junit-file policy.xml| Output | Use |
|---|---|
table (default) | Readable in a job log |
json | Carries the change's evidence too, so teams that run OPA can pass it to Rego |
sarif, or --sarif-file | SARIF 2.1.0 for GitHub code scanning or reviewdog |
junit, or --junit-file | JUnit XML for any CI test view |
Actions are warn, require_owner_review, require_tests and fail.
Only fail exits 1; the others are reported in every format. Without
a revision, check reads the target branch from the CI's pull request
variables, like the gates. Conditions on reaching tests, owners and
contracts read the index, so run it in a job that restores
.repowise/ (see below). Every
condition is on Change policy.
What every gate shares
- Exit codes.
0passed or had nothing to judge.1the change failed.2the gate could not evaluate (unknown revision, no merge-base, unreadable report). A setup problem never reads as a pass. - The change. Pass a range such as
origin/main...HEAD(three dots: what the branch did since it forked). Without one, the gates read the target branch from the CI's pull request variables (GitHub, GitLab, Jenkins, Bitbucket), else the remote's default branch. Doc drift checks the whole tree and takes no range. - History. Use full history (
fetch-depth: 0on GitHub,GIT_DEPTH: "0"on GitLab). A shallow clone has no merge-base, and the gate exits2. - Formats.
table,json,markdown, andgithub(annotations on changed lines plus a job summary). Doc drift, security and health also writesarifandgitlab.
Adopting on an existing repository
Coverage, security, health and risk only judge what a change adds. Doc drift checks the whole tree, so a repository that already has drift fails on the first run. Record what is there once:
repowise doc-drift --check --write-baseline .doc-drift-baseline.json
git add .doc-drift-baseline.jsonThen pass --baseline .doc-drift-baseline.json (or doc-drift-baseline on
the action). Or gate only the drift the change is answerable for, with no
baseline: repowise doc-drift --check --since auto.
To accept a security finding on purpose, record it with
repowise security check --write-baseline .security-baseline.json, or put a
repowise-security-ignore comment on its line.
--fail-on-baseline-growth (the action's fail-on-baseline-growth input)
keeps a baseline shrink-only: doc drift or security fails when the baseline
lists a finding the same file at the change's base does not, so a change
cannot hide new findings by re-saving it. A baseline the change adds for the
first time passes.
The health gate reads .repowise/health-rules.json as the change's base has
it, so a rule a change adds or loosens applies from the next change on.
Run only the tests a change needs
repowise impacted-tests --format args prints one line of runner
arguments, or :all when any part of the answer is uncertain, with the
reasons on stderr. It needs an index, so cache one from the default branch:
# .github/workflows/repowise-index.yml
on: {push: {branches: [main]}}
jobs:
index:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: {fetch-depth: 0}
- uses: actions/cache/restore@v4
with: {path: .repowise, key: "repowise-${{ github.sha }}", restore-keys: "repowise-"}
- run: pip install repowise
- run: if [ -f .repowise/state.json ]; then repowise update --index-only; else repowise init --yes --no-prose; fi
- uses: actions/cache/save@v4
with: {path: .repowise, key: "repowise-${{ github.sha }}"}Then branch on the action's run-all-tests output in the pull request job:
- uses: actions/cache/restore@v4
with: {path: .repowise, key: "repowise-${{ github.event.pull_request.base.sha }}", restore-keys: "repowise-"}
- id: select
uses: repowise-dev/repowise@v0.60.0
with: {checks: "", impacted-tests: true, impacted-tests-runner: pytest}
- if: steps.select.outputs.run-all-tests != 'false'
run: pytest
- if: steps.select.outputs.run-all-tests == 'false' && steps.select.outputs.impacted-tests != ''
env: {TESTS: "${{ steps.select.outputs.impacted-tests }}"}
run: eval "pytest $TESTS"A skipped or failed step leaves run-all-tests empty, so != 'false' runs
everything. On GitLab, set REPOWISE_IMPACTED_TESTS: "true".
It runs the whole suite whenever selection is not certain: lockfiles, build
and CI config, test data, a changed file no test reaches, or an index that
is missing or out of date. Add your own rules in .repowise/config.yaml:
tests:
full_run_on: ["migrations/**"] # changes here run everything
always_run: ["tests/test_smoke.py"] # added to every selectionThe subset is only as small as the code is loosely coupled. On tightly
coupled code the graph alone picks most tests. A per-test coverage map
(repowise coverage add .coverage from pytest --cov=<package> --cov-context=test)
makes it more precise.
The full guide, with per-language coverage commands, monorepo reports and path-scoped gates, is Repowise in CI.