mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-09 01:35:18 +02:00
codeowners: yml source of truth + generator + drift CI (#88)
* codeowners: generator + drift CI + initial roles
Source-of-truth approach to CODEOWNERS: yml is hand-edited, CODEOWNERS
is generated and CI-enforced. Every role change is a reviewable PR
with a permanent in-repo audit trail. No GitHub UI clicks, no shadow
state.
Initial roles:
engineering @aaltshuler owns crates/** + default (.github/,
scripts/, Cargo.*, openapi.json,
everything else not docs)
docs @aaltshuler @ragnorc owns docs/**, README.md, AGENTS.md,
CLAUDE.md, SECURITY.md
Per GitHub semantics, multiple owners on a CODEOWNERS line means "any
one satisfies the review" — for docs, either named member can approve.
Strict "N distinct approvers" would need a CI workaround (not wired
today; tracked for future hardening).
Components:
- .github/codeowners-roles.yml — source of truth. Edit this.
- .github/scripts/render-codeowners.py — generator (PyYAML; ~100 LoC).
- .github/CODEOWNERS — generated. CI rejects hand-edits.
- .github/workflows/codeowners.yml — two checks:
* drift: re-render and assert CODEOWNERS matches.
* noedit: reject PRs that edit CODEOWNERS without editing the yml.
- docs/codeowners.md — explains the source-of-truth pattern, how to
change roles, how to add new roles.
- AGENTS.md topic-index row.
What's NOT in this PR:
- Branch protection on main (separate PR; needs `gh api` call against
the org).
- Required-reviewer enforcement (depends on branch protection landing).
- Required CI status checks (depends on branch protection landing).
- Scheduled rotation (the schedule: block in the yml + a weekly
workflow). Today's roles are stable; rotation isn't needed yet.
- Linear-as-source-of-truth integration (Approach 4 from the design
discussion; deferred).
Verified:
- Generator output is deterministic (idempotent re-runs).
- scripts/check-agents-md.sh OK (28 links, 28 docs).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* codeowners: fix catch-all ordering (Devin review #88)
Devin caught a real bug: GitHub CODEOWNERS uses "last match wins"
semantics, but the generator emitted the catch-all `*` AFTER specific
patterns. Net effect: `*` won for every file, silently nullifying the
docs role and never routing reviews to @ragnorc.
Fix is one-line — emit the default `*` line before iterating the
specific paths. Also:
- Added a regression assertion in the generator: after rendering, the
first non-comment line must start with `*` if a default is
configured. Generator exits non-zero otherwise. Catches the same
class of mistake in any future refactor.
- Rewrote the yml header comment, which incorrectly stated "keep
more-specific paths after broader patterns" (correct for GitHub
semantics but the generator was doing the opposite — so the comment
read as a description of behavior when it was actually a contradicted
intention).
Verified by re-rendering: `*` is now line 12, `crates/**` is line 14,
`docs/**` is line 15, etc. README.md matches both `*` and `README.md`;
`README.md` is later → wins → @aaltshuler + @ragnorc both assigned.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c142dafdf3
commit
730712b73f
6 changed files with 313 additions and 0 deletions
66
.github/workflows/codeowners.yml
vendored
Normal file
66
.github/workflows/codeowners.yml
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
name: CODEOWNERS
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/codeowners-roles.yml'
|
||||
- '.github/CODEOWNERS'
|
||||
- '.github/scripts/render-codeowners.py'
|
||||
- '.github/workflows/codeowners.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
# Read-only; we never push from this workflow.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
drift:
|
||||
name: CODEOWNERS matches source
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5.0.1
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.4.0
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
- name: Install PyYAML
|
||||
run: pip install pyyaml
|
||||
|
||||
- name: Re-render CODEOWNERS
|
||||
run: python3 .github/scripts/render-codeowners.py
|
||||
|
||||
- name: Reject drift
|
||||
run: |
|
||||
if ! git diff --quiet .github/CODEOWNERS; then
|
||||
echo "::error::.github/CODEOWNERS is out of sync with .github/codeowners-roles.yml."
|
||||
echo "::error::Run \`python3 .github/scripts/render-codeowners.py\` locally and commit the result."
|
||||
echo "--- diff ---"
|
||||
git --no-pager diff .github/CODEOWNERS
|
||||
exit 1
|
||||
fi
|
||||
echo "CODEOWNERS is in sync with its source."
|
||||
|
||||
noedit:
|
||||
name: CODEOWNERS not hand-edited
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5.0.1
|
||||
with:
|
||||
# Need history so we can diff against the PR base.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Reject hand-edits to generated file
|
||||
run: |
|
||||
base="origin/${{ github.base_ref }}"
|
||||
git fetch origin "${{ github.base_ref }}" --quiet
|
||||
changed=$(git diff --name-only "$base" HEAD)
|
||||
edited_generated=$(echo "$changed" | grep -E '^\.github/CODEOWNERS$' || true)
|
||||
edited_source=$(echo "$changed" | grep -E '^\.github/codeowners-roles\.yml$' || true)
|
||||
if [ -n "$edited_generated" ] && [ -z "$edited_source" ]; then
|
||||
echo "::error::This PR edits .github/CODEOWNERS but not its source .github/codeowners-roles.yml."
|
||||
echo "::error::Edit the yml and regenerate via \`python3 .github/scripts/render-codeowners.py\`."
|
||||
exit 1
|
||||
fi
|
||||
echo "CODEOWNERS edits accompany source edits (or no CODEOWNERS edits in this PR)."
|
||||
Loading…
Add table
Add a link
Reference in a new issue