PR-time governance gates in CI
Governance that only lives in a runbook gets skipped. Kirimana pushes it into the pull request: a set of CLI checks run in GitHub Actions on every contract change, and a failing check blocks the merge. The same commands run locally, so a developer sees the same result before pushing that CI will see after. This page covers what each gate checks, what blocks, and a workflow you can drop in.
The commands that gate a PR
Four commands do the work. All of them are safe to run locally and exit non-zero on a finding, which is what fails the CI job.
kiri contract lint— PR-time governance checks over the contracts touched in the diff (classification presence, owner validity, AI-policy coherence, column-lineage resolution).kiri contract validate— structural and semantic validation of a contract (well-formed ODCS, closed-menu enum values, schema coherence).kiri contract approval-count— resolves how many approvals this specific PR requires, so the workflow can enforce it.kiri contract codeowners --check— fails if the committed CODEOWNERS file has drifted fromkiri.ymldomain owners.
For projects on the per-(domain, layer) shape, kiri layer validate
is the structural gate over the layer files themselves — it returns
zero findings on a well-formed project and hard-fails on the invariant
violations described in hub-and-spoke
governance.
What blocks a merge
kiri contract lint emits typed findings, and an error-severity
finding fails the job. The rules that matter most at the gate:
- classification-missing — a contract with no
kiri.classificationproperty. Every asset must declare its data class; an unclassified contract cannot ship. - owner-placeholder — an owner field that is a placeholder
(
owner@example.com,TODO,FIXME) or a malformed email. The domain must resolve to a real accountable party. - ai-policy-coherence — a classification / AI-policy mismatch, the
classic “raised classification to
confidentialbut left inference allowed” slip. Aconfidentialorrestrictedcontract that still permits inference is flagged. - column-lineage-unresolvable — a silver/gold column declares an upstream column URN that does not resolve to a real upstream. A declared lineage edge that points at nothing is a broken contract.
- column-lineage-missing — a silver/gold property with no
kiri.lineage.from_columns. This is a warning during the migration window rather than a hard block, so a large estate can adopt lineage incrementally.
kiri contract validate blocks on anything structurally wrong: a
malformed ODCS document, a value outside a closed-menu enum, an
inconsistent schema. And kiri contract codeowners --check blocks on
docs drift between the generated CODEOWNERS file and current domain
owners — a hand-edit to routing fails the job until it is regenerated.
Resolving the approval count
The required number of approvals is not a fixed repo setting — it
rises with the sensitivity of the change. kiri contract approval-count computes it per PR from three inputs the workflow
derives with git diff:
- changed paths — the per-domain base approval count for every domain the PR touches.
- restricted-raises — paths whose diff introduced
value: restricted. Raising a classification torestricteddemands elevated approval. - cross-domain edits — paths that introduce a new cross-domain consumer. A new dependency between domains is a two-team decision.
The command prints a single integer on stdout — the number the
workflow feeds into its required-approvals check. Because the count is
computed from the diff, a routine change inside one domain needs the
domain’s base count, while a change that raises restricted or adds a
cross-domain edge automatically needs more.
Example workflow
A minimal .github/workflows/contract-governance.yml:
name: contract-governance
on:
pull_request:
paths:
- "01-config/**"
- "contracts/**"
- "kiri.yml"
jobs:
govern:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # need the base ref for the diff
- name: Install kiri
run: pip install kiri-cli
# Structural + semantic validation.
- name: Validate contracts
run: kiri contract validate
# Governance findings (classification, owner, AI policy, lineage).
- name: Lint contracts
run: kiri contract lint
# CODEOWNERS must match kiri.yml — no hand-edits.
- name: Check CODEOWNERS drift
run: kiri contract codeowners --output .github/CODEOWNERS --check
# Compute the required approver count for this PR.
- name: Compute changed paths
run: |
git diff --name-only origin/${{ github.base_ref }}...HEAD \
> changed.txt
git diff origin/${{ github.base_ref }}...HEAD \
| grep -l 'value: restricted' changed.txt > raises.txt || true
- name: Required approvals
id: approvals
run: |
echo "count=$(kiri contract approval-count \
--changed-files-from changed.txt \
--restricted-raises-from raises.txt)" >> "$GITHUB_OUTPUT"
- name: Enforce approvals
run: |
echo "This PR requires ${{ steps.approvals.outputs.count }} approval(s)."
# Wire the count into your branch-protection / merge-gate check.
Adapt the last step to your merge-gate mechanism — the point is that
approval-count gives you the number, and your branch protection
enforces it. CODEOWNERS (from the previous page) has already routed
the review to the right people; this makes sure enough of them sign
off before the change lands.
Why the gate lives in the PR
Every one of these checks runs identically on a laptop and in CI, so
the feedback loop is tight: kiri contract lint before you push tells
you exactly what the merge gate will say. Nothing is enforced by
convention or by a reviewer remembering to look — an unclassified
contract, a placeholder owner, a dangling lineage edge, or a drifted
CODEOWNERS file simply cannot merge. Governance becomes a property of
the pipeline rather than a discipline you have to sustain by hand.
Once a change clears the gate but still needs a recorded human decision, that moves into the contract-approval review.