Block merges in CI
vidi verify exits non-zero when the gate fails. That exit code is the
enforcement. CI needs nothing else from vidi.
The job
# .github/workflows/vidi.yml
name: Review coverage
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
verify:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
with:
# The gate reads .vidi/*.jsonl out of the checkout. A shallow clone is
# fine, the ledger is committed files, not git history.
fetch-depth: 1
- name: Install Rust (stable)
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
- name: Install vidi
run: cargo install --git https://github.com/graze-ai/vidi vidi-cli --locked
- name: Verify review coverage
run: vidi verify
Nothing here needs a token, a login, or a network call. verify is offline and
read-only: it scans the working tree, reads the committed ledger, and compares.
Getting the binary
vidi-cli is not published to crates.io and there are no prebuilt release
binaries yet, so CI has to build it from source. Which source depends on where
the workflow lives:
| Where the workflow runs | Install step |
|---|---|
| Another repository, gating its own code | cargo install --git https://github.com/graze-ai/vidi vidi-cli --locked |
| This repository, gating itself | cargo install --path crates/vidi-cli --locked |
vidi-cli is the package name; the binary it installs is vidi. Both forms
put it on PATH, which a bare cargo build would not.
Pin the version you gate on — --git … --tag v0.1.0 — rather than tracking the
default branch, or a change upstream can turn a green build red without a commit
of yours. --locked uses the lockfile committed to vidi rather than resolving
fresh dependencies, so the build is reproducible.
Expect this step to dominate the run: it compiles the tree-sitter grammars from
C, which takes minutes. actions-rust-lang/setup-rust-toolchain caches the
cargo registry and build artifacts between runs, so only the first one pays full
price.
Exit codes
| Exit | Verdict | Meaning |
|---|---|---|
0 | PASS | a policy exists and every requirement is met |
0 | NEUTRAL | no .vidi/policy.toml, and nothing broken |
1 | FAILURE | something is stale, orphaned, or unmet — or a ledger line would not decode |
2 | — | the command refused: a broken policy, bad arguments, a failed scan |
141 | — | stdout closed early (128+SIGPIPE, e.g. vidi queue | head) |
Only 1 is a gate failure. A 2 means vidi never got as far as forming a
verdict, so nothing was checked. Do not read it as a review backlog.
NEUTRAL passes: an unconfigured repository does not fail its own builds. See
Turn on the gate.
Use verify, not status
vidi status prints the same verdict word and always exits 0. A CI step
running vidi status prints FAILURE on every build and passes anyway.
Explaining a failure in the log
verify names the failing units but not why they fail. Running vidi explain
on failure turns the log into something a contributor can act on without
reproducing anything locally:
- name: Verify review coverage
run: vidi verify
- name: Explain the failure
if: failure()
run: vidi explain
explain is read-only, and renders each failure as category → what failed → why
→ what to do.
Annotating the diff
vidi report --sarif emits SARIF 2.1.0, which GitHub renders as inline
annotations on the changed lines:
- name: Review-coverage annotations
if: always()
run: vidi report --sarif > vidi.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: vidi.sarif
Use if: always() on both steps, so the annotations still appear when verify
failed, which is exactly when they are useful.
Three rules can appear: vidi/requirement-unmet, vidi/stale, and
vidi/orphan. A passing repository emits a valid document with an empty
results array, which is the healthy case, not a broken one:
$ vidi report --sarif
{"$schema":"https://json.schemastore.org/sarif-2.1.0.json","runs":[{"results":[],"tool":{"driver":{"informationUri":"https://vidivouch.com","name":"vidi","version":"0.1.2"}}}],"version":"2.1.0"}
There is no vidi/rejected-ledger-line result. A partially-decoded ledger
refuses before any report is assembled, so it exits 1 with no SARIF artifact at
all. The gate still fails closed; it just fails earlier and without
annotations.
Machine-readable results
For a custom check-run or a dashboard, vidi verify --json emits the gate
object directly:
$ vidi verify --json
{ "vidi": "0.1.2", "schema": "vidi.gate/v1", "gate": { "checkRun": "failure", "exit": 1, "policyPresent": true, "stale": 1, "orphan": 0, "requirementFailures": 0, "defaultShortfalls": 0, "rejectedLedgerLines": 0, "reviewed": 0, "total": 5456 } }
checkRun is success / failure / neutral — the GitHub check-run
vocabulary, so it can be forwarded to the Checks API unchanged.
This is a different schema from vidi json (the full report,
vidi.json/v1.0). The gate object carries its own schema id precisely so a
consumer never validates one against the other’s schema.
Check the exit code before reading the stream. A ledger line that will not
decode refuses before any report is assembled, so nothing is emitted: --json
exits 1 with empty stdout and the rejected lines on stderr, and --sarif
produces no artifact. The gate still fails closed, just earlier and without
output.
Branch protection
Make the verify job a required status check in your branch-protection rules.
Because an unconfigured repo returns NEUTRAL and exit 0, you can require the
check before writing a policy and it will simply pass — then start gating by
adding .vidi/policy.toml, with no CI change.
Deleting or corrupting the ledger does not quietly disable the gate, every ambiguity resolves toward failure. See Fail-closed by default.
Other CI systems
Nothing above is GitHub-specific except the SARIF upload. The whole gate is one command:
vidi verify
vidi push reads $GITHUB_REPOSITORY, $GITHUB_SHA, $GITHUB_REF_NAME and
$CI_COMMIT_REF_NAME when they are set, and falls back to the git remote and
HEAD otherwise, so the hosted lane works anywhere too. See Share reviews across
a team.