> ## Documentation Index
> Fetch the complete documentation index at: https://ctrlrun.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify in CI

> Run ctrlrun verify against your policy on every push with the CTRLRun GitHub Action, read the two shapes of report, understand the N/A line.

`ctrlrun verify` runs the kernel's own failure scenarios against your policy, in a scratch
store, with fake executors and no network, and reports what passed, what failed, and what it
could not check. In CI that is one step, and the badge it writes means one thing: the declared
guarantees pass.

**Prerequisites:** a repository with a `ctrlrun.yaml`, and GitHub Actions.

<Steps>
  <Step title="Run it locally first">
    ```yaml runnable theme={null}
    schema: ctrlrun.policy/v2

    actions:
      stripe.refund:
        effect: "refund:{payment_id}"
        rules:
          - when: { amount_gte: 0, amount_lte: 50000 }
            decision: allow
          - decision: approve
      k8s.delete_namespace:
        effect: "namespace:{cluster}:{name}"
        decision: approve
    ```

    ```bash runnable theme={null}
    ctrlrun verify
    ```

    ```text theme={null}
    k8s.delete_namespace: denied: no principal is available
    CTRLRun verify — ctrlrun 0.6.0, catalogue ctrlrun.guarantees/v2
    policy     /home/you/agent/ctrlrun.yaml (ctrlrun.policy/v2, mode: enforce)
    authority  none
    store      sqlite, scratch (created and destroyed for this run)

    G1   mutated approval refused         PASS  k8s.delete_namespace
    G2   replayed approval refused        PASS  k8s.delete_namespace
    G3   duplicate effect refused         PASS  k8s.delete_namespace
    G4   one winner under concurrency     PASS  k8s.delete_namespace (8 processes)
    G5   ambiguous blocks a blind retry   PASS  k8s.delete_namespace
    G6   unknown action refused           PASS
    G7   no principal refused             PASS  k8s.delete_namespace
    G8   expired authority refused        N/A   no authority section
    G9   delegation cannot escalate       N/A   no authority section
    G10  unknown exception is ambiguous   PASS  k8s.delete_namespace
    G11  an altered receipt is detected   PASS  k8s.delete_namespace

    9/9 declared guarantees pass. 2 not applicable: G8, G9.
    ```

    Two things about that output. The first line is on **stderr**, from G7's own scenario: it
    drives an action with no principal and the refusal logs, which is the guarantee passing and
    not a problem. And every row names `k8s.delete_namespace` rather than the refund — verify
    exercises one action per guarantee and takes the **first that fits, in alphabetical order**,
    so which of your actions appears is not a judgement about it.

    Two guarantees are not applicable: this policy has no `authority:` section, so nothing
    about grants can be exercised. They are reported with the reason, excluded from the
    denominator and listed separately. Never `11/11`, and no flag folds them in.
  </Step>

  <Step title="Add the action">
    ```yaml theme={null}
    name: CTRLRun verify

    on: [push, pull_request]

    jobs:
      verify:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: CTRLRun/ctrlrun@main
            with:
              policy: ctrlrun.yaml
    ```

    The action installs `ctrlrun`, runs `ctrlrun verify --json --junit`, renders the job summary
    and the badge JSON from that one report, and uploads the three as an artifact. It fails the
    job when a guarantee failed or the configuration was refused, and succeeds when guarantees
    are N/A: green means nothing that could be checked was wrong.
  </Step>

  <Step title="Read the outputs">
    | Input        | Default                  |                                                              |
    | ------------ | ------------------------ | ------------------------------------------------------------ |
    | `policy`     | `ctrlrun.yaml`           | the document to verify                                       |
    | `authority`  | the policy's own section | a standalone authority document                              |
    | `only`       | all                      | comma-separated guarantee ids; a partial run writes no badge |
    | `install`    | `ctrlrun`                | the pip requirement; `.` verifies with the checkout          |
    | `badge-path` | `verify-badge.json`      | where the Shields endpoint JSON goes                         |

    Outputs: `passed`, `failed`, `applicable`, `not-applicable`, `badge-message`, `report-path`.
    Pin the action to a release tag or a commit rather than `@main` once you rely on it.
  </Step>

  <Step title="Publish the badge, if you want it">
    The action writes the badge JSON and never publishes it, because publishing needs
    `contents: write` and asking every user for that is a bad trade for a tool about least
    privilege. Publish it yourself from a job that runs only on pushes to your default branch,
    with `contents: write` on that job alone, and point Shields' endpoint badge at the raw file.
    [Get the badge](/verify/get-the-badge) has the copy-paste workflow.
  </Step>
</Steps>

## What verify cannot see

Your executors, your `reconcile` hooks, where you put the decorator, your deployment, and
whether your policy is the right policy. An executor that raises `NotExecuted` after the remote
acted is invisible to it. The badge does not mean secure, safe, compliant, certified or audited.

## If it didn't work

* Exit 2, `mode: observe`: verify refuses an observed configuration, because observe mode
  executes what enforce mode would refuse and nothing can be proved about it.
* Exit 2, *nothing was checked and nothing is claimed*: **no** guarantee was applicable, so
  `0/0` — which is never a pass. In practice that means an `actions:` map with nothing in it. A
  policy with one `allow` action and no `approve` rule, no `effect:` template and no grants is
  not this case: it exits 0 with `4/4` and seven not applicable, because the fail-closed and
  unknown-outcome guarantees still have something to exercise.
* Exit 1: a guarantee failed. The report names the scenario and the action; that is a bug
  report, not a configuration problem.

## Next

* [Exit codes](/reference/exit-codes).
* [What the badge means](/verify): the guarantee catalogue in full.
* [Get started](/get-started/quickstart) · [Why](/why).


## Related topics

- [What verify guarantees](/security/verify-guarantees.md)
- [Run verify in GitHub Actions](/cookbook/verify-in-github-actions.md)
- [Get the verified badge](/verify/get-the-badge.md)
- [Exit codes](/reference/exit-codes.md)
- [ctrlrun verify](/verify.md)
