Domain owners + CODEOWNERS
Ownership is not optional in Kirimana. Every domain declares at least
one owner, and that single fact drives two mechanisms: GitHub
auto-requests review from the right people when a contract PR lands,
and the PR-time lint refuses to merge a domain whose owner is a
placeholder. You maintain ownership in one place — kiri.yml — and
both mechanisms read from it.
Ownership lives in kiri.yml
Owners are declared per domain, alongside the domain’s other policy:
domains:
sales:
owners: [sales-team, alice@acme.com]
hr:
owners: [hr-team]
finance:
owners: [finance-team, dana@acme.com]
Owners can be GitHub team handles (sales-team), bare emails
(alice@acme.com), or handles already spelled with a leading @.
A domain must carry at least one owner — this is the anchor for
everything downstream. Keeping ownership in kiri.yml rather than in
a hand-edited CODEOWNERS file means the platform team reviews
ownership changes through the same PR flow as every other governance
edit, and there is exactly one source of truth to keep honest.
Auto-generating CODEOWNERS
GitHub’s CODEOWNERS file drives review-request routing: when a PR touches a file under a matched path, GitHub automatically requests review from the owners that path declares. Kirimana renders this file from your domain owners so you never hand-maintain it:
# Print to stdout (inspect before writing):
kiri contract codeowners
# Write it into place:
kiri contract codeowners --output .github/CODEOWNERS
The output is one line per domain, sorted deterministically, mapping the domain’s contract path to its owners:
# Auto-generated by `kiri contract codeowners`.
# Do not edit by hand — edit kiri.yml domain owners and regenerate.
contracts/finance/** @finance-team dana@acme.com
contracts/hr/** @hr-team
contracts/sales/** @sales-team alice@acme.com
Team handles get a leading @ automatically; bare emails pass
through verbatim (GitHub accepts them when the user has a verified
email); entries already prefixed with @ are left alone. Because the
render is byte-deterministic, the file is safe to commit and diff —
a change to it is always a real change to ownership, never formatting
noise.
CODEOWNERS only controls who gets pinged. It does not set how many approvals are required — that is the per-domain approval count, resolved separately at PR time (see PR-time governance gates). The two compose: CODEOWNERS routes the review to the right people, the approval count decides how many of them must sign off.
Keeping the file honest in CI
A hand-edited CODEOWNERS file would silently bypass kiri.yml and
break the single-source-of-truth guarantee. To prevent that, run the
command in --check mode in CI:
kiri contract codeowners --output .github/CODEOWNERS --check
--check re-renders from kiri.yml and compares against the
committed file. It exits non-zero — failing the workflow — if the
file is missing, or if it drifts from what the current domain owners
would produce. The fix is always the same: regenerate and commit.
This is the same generated-artifact-drift pattern Kirimana uses to
fence other emitted files, applied to review routing.
Owner validation at lint
Routing a PR to an owner only helps if the owner is real. The PR-time contract lint carries an owner-placeholder rule that fails on owner fields that are obviously not a real accountable party:
- Known placeholders —
owner@example.com,TODO,FIXME,example.com-style stand-ins. - Malformed emails — a value containing
@that is not a well-formed address.
Team-handle-style owners (ops, analytics-team) pass, since not
every owner is an email. The point of the rule is narrow and
deliberate: a contract cannot ship with ownership that resolves to
nobody. Discovery and scaffolding leave placeholder owners behind by
design — the lint is what forces a human to replace them before the
contract is governed for real.
Run the lint locally the same way CI does:
kiri contract lint
An owner-placeholder finding blocks the merge until you set a genuine
owner in kiri.yml and regenerate CODEOWNERS.
The full ownership loop
Putting it together, ownership flows in one direction and self-checks at every hop:
- A platform-admin sets or changes a domain’s owner in
kiri.yml. kiri contract codeowners --output .github/CODEOWNERSrenders the routing file; it is committed in the same PR.- CI runs
kiri contract codeowners --check— the committed file must match the render, or the workflow fails. - CI runs
kiri contract lint— placeholder or malformed owners fail the workflow. - On the next contract PR for that domain, GitHub auto-requests
review from exactly the owners
kiri.ymldeclares.
You never touch .github/CODEOWNERS by hand, you never route a PR to
the wrong team, and you never merge a contract that nobody owns. Each
of those guarantees comes from the same declared fact: the domain’s
owner in kiri.yml.
For how many approvals those owners must give — and how the count
rises for sensitive changes — see PR-time governance
gates. For who is allowed to edit the
kiri.yml owner list in the first place, see RBAC roles +
capabilities.