# Gates in CI

Gate pull requests in CI on patch coverage, new doc drift, new security findings, new code-health findings and change risk, enforce a change policy, and run only the tests needed.

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](https://docs.repowise.dev/cli/gates). What each gate
measures is on [Test intelligence](https://docs.repowise.dev/intelligence/test-intelligence),
[Documentation drift](https://docs.repowise.dev/intelligence/doc-drift),
[Security signals](https://docs.repowise.dev/intelligence/security-signals),
[Code health](https://docs.repowise.dev/intelligence/code-health) and
[`repowise risk`](https://docs.repowise.dev/cli/health#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.

```yaml
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](https://docs.repowise.dev/ci#run-only-the-tests-a-change-needs). |

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`](https://github.com/repowise-dev/repowise/blob/main/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:

```yaml
- 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 github
```

To 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:

```yaml
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 report
```

It 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](https://github.com/repowise-dev/repowise/blob/main/ci/gitlab/repowise.gitlab-ci.yml).

## Other CI systems

The gates are plain commands. Fetch the target branch, run them, and let
the exit code fail the build:

```bash
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:

```yaml
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: fail
```

A 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.

```bash
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](https://docs.repowise.dev/ci#run-only-the-tests-a-change-needs)). Every
condition is on [Change policy](https://docs.repowise.dev/intelligence/change-policy).

## What every gate shares

- **Exit codes.** `0` passed or had nothing to judge. `1` the change failed.
  `2` the 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: 0` on GitHub,
  `GIT_DEPTH: "0"` on GitLab). A shallow clone has no merge-base, and the
  gate exits `2`.
- **Formats.** `table`, `json`, `markdown`, and `github` (annotations on
  changed lines plus a job summary). Doc drift, security and health also
  write `sarif` and `gitlab`.

## 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:

```bash
repowise doc-drift --check --write-baseline .doc-drift-baseline.json
git add .doc-drift-baseline.json
```

Then 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:

```yaml
# .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:

```yaml
- 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`:

```yaml
tests:
  full_run_on: ["migrations/**"]        # changes here run everything
  always_run: ["tests/test_smoke.py"]   # added to every selection
```

> **Note**
>
> The 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](https://github.com/repowise-dev/repowise/blob/main/docs/start/CI.md).

On Hosted, the PR bot can post a GitHub check that branch protection can require, gating merges on code health with no CI step to maintain.

[The PR bot merge gate](https://docs.repowise.dev/hosted/bot#the-merge-gate)

[Try hosted](https://repowise.dev/login?mode=signup)
