Skip to content

feat(repo): make review a code-owner gate, not a blanket approval - #63

Merged
justinmerrell merged 1 commit into
mainfrom
feat/selective-code-owner-review
Aug 25, 2026
Merged

feat(repo): make review a code-owner gate, not a blanket approval#63
justinmerrell merged 1 commit into
mainfrom
feat/selective-code-owner-review

Conversation

@justinmerrell

Copy link
Copy Markdown
Contributor

What changes

No contract change. This replaces the blanket "one approving review on every
pull request" rule with the selective code-owner gate musher-dev/platform
already runs: .github/rulesets/main-branch.json pairs
required_approving_review_count: 0 with require_code_owner_review: true, and
.github/CODEOWNERS drops its * catch-all down to the two paths that define
the gate itself. A pull request touching no owned path merges on green CI; one
touching .github/CODEOWNERS or .github/rulesets/ still needs its owner.
tools/src/rulesets.ts (task check:rulesets, RUL-01..RUL-09) keeps the two
halves in step, a new codeowners-notice workflow makes self-owned edits
visible, and ADR 0015 plus three GOVERNANCE.md passages record what is now
enforced versus asked for.

The live rulesets have already been updated — see "Applied live" below.

Why

The old rule was never satisfied. @justinmerrell is the only owner, GitHub
cannot request a review from a pull request's author, and the ruleset grants
OrganizationAdmin an always-bypass. So every pull request they opened arrived
blocked and was merged by spending the bypass: #42, #45, #47, #48, #52, #53, #55
and #57 all carry reviewDecision: REVIEW_REQUIRED. A control exercised only by
bypassing it enforces nothing, trains the bypass as a normal step, and still
charges every other contributor the wait.

ADR 0015
records the decision, the alternatives, and the cost — including that owning
specifications/ becomes worth revisiting the moment there is a second
maintainer.

Two drifts reconciled, both in the file's favour

The committed JSON had not been re-applied since 2026-08-08:

  1. main-branch.json declared a fourth required check, Signed off; live had
    only three. The DCO gate that dco.yml, CONTRIBUTING.md and the pull request
    template all promise was not actually required. It is now.
  2. release-tags.json declared bypass_actors: [], with a paragraph in
    RULESETS.md arguing that an escape hatch nobody may legitimately use is one
    an attacker inherits. Live granted OrganizationAdmin bypass anyway. Removed.

RUL-09 goes beyond the upstream version

Platform documents in prose that a required context must exist and must not live
in a paths:-filtered workflow, and enforces neither. RUL-09 enforces both —
they are the two ways a required check hangs a pull request forever. It is also
why the validator runs inside the existing Lint job rather than as its own
workflow: a path-filtered validator would be the bug it exists to catch.

Applied live

Both rulesets were PUT from the exact JSON committed here, so the files and
GitHub are in step as of this pull request:

gh api -X PUT repos/musher-dev/spec/rulesets/20585885 --input .github/rulesets/main-branch.json
gh api -X PUT repos/musher-dev/spec/rulesets/20585889 --input .github/rulesets/release-tags.json

Verified against the aggregate view, which is the only one that accounts for
org-level and classic rules folding in — gh api repos/musher-dev/spec/rules/branches/main
reports a single pull_request rule, sourced from musher-dev/spec, with
required_approving_review_count: 0, require_code_owner_review: true,
require_last_push_approval: false. Classic branch protection on main returns
404, and spec is deliberately absent from the org pr-workflow ruleset.

Note that CODEOWNERS on main still carries the * catch-all until this merges,
so every path stays owned until then.

Compatibility

  • No schema change (docs, tooling, or CI only)

Checklist

  • task check passes locally (including the new check:rulesets)
  • schemas/dist/ untouched — no schema change
  • No behavioural change to the specification, so no conformance fixtures
  • No spec.md affected
  • Commit is Conventional and scoped repo (releases nothing)
  • Commit is DCO signed off

Each of RUL-01..RUL-09 was verified to fire by deliberately breaking it: a *
catch-all, a stale CODEOWNERS path, a malformed owner, the approval count back
at 1, require_last_push_approval: true, a leaked server-side id, a typo'd
required context, and a required context pointing at the path-filtered
ShellCheck job.

Review note

This pull request touches both owned paths, and its author owns both — GitHub
will waive the code-owner requirement. That is exactly the case
codeowners-notice.yml exists to make visible, and it should post its sticky
comment on this pull request. A second pair of eyes is worth requesting manually
here, since the diff changes who is allowed to change it.

The main-branch ruleset required one approving review on every pull
request, and CODEOWNERS opened with a `*` catch-all naming the only
maintainer. Since GitHub cannot request a review from a pull request's
author, that combination blocked every pull request the maintainer
opened, and every one was merged by spending the OrganizationAdmin
bypass — #42, #45, #47, #48, #52, #53, #55 and #57 all carry
`reviewDecision: REVIEW_REQUIRED`. A control satisfied on no pull
request and stepped over on all of them enforced nothing, while still
charging every contributor the wait.

Adopt the mechanism musher-dev/platform already uses, so that moving
between the two repositories does not mean relearning when review is
required: pair `required_approving_review_count: 0` with
`require_code_owner_review: true`, and reduce CODEOWNERS to the two
paths that define the gate itself. A pull request touching no owned
path now merges on green CI; one touching `.github/CODEOWNERS` or
`.github/rulesets/` still needs its owner.

Both halves break silently — a catch-all restores the blanket gate from
one side, a count of 1 from the other — so `tools/src/rulesets.ts`
(RUL-01..RUL-09) keeps them in step from inside the existing Lint job.
RUL-09 has no counterpart upstream: it rejects a required status check
that no workflow publishes, or that a `paths:`-filtered workflow
publishes, which are the two ways a required context hangs a pull
request forever. It is also why the check runs inside Lint rather than
as a workflow of its own.

Reconcile two drifts the committed JSON had accumulated since
2026-08-08, both in the file's favour: `Signed off` is now genuinely a
required check rather than only a declared one, and release-tags no
longer grants the OrganizationAdmin bypass its own documentation says
it does not have.

GitHub waives the code-owner requirement for a pull request's author,
which with a single owner is the whole gate on the paths that matter
most. codeowners-notice.yml posts a sticky comment on self-owned edits
so the waiver is visible; it is a notifier and must never become a
required check.

GOVERNANCE.md advertised approval as mechanical in three places and
CONTRIBUTING.md in one; they now say plainly which obligations are
enforced and which are asked for. ADR 0015 records the trade, including
that owning specifications/ becomes worth revisiting the moment there
is a second maintainer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Merrell <merrelljustin@gmail.com>
@github-actions

Copy link
Copy Markdown

You own some of the paths this pull request changes

@justinmerrell, this pull request touches paths you are a code owner for. Your authorship waives the code-owner review requirement — GitHub cannot request a review from a pull request's author — so nobody is automatically notified about these changes:

  • .config/lefthook.yml — you are the sole owner; no review will be requested
  • .github/CODEOWNERS — you are the sole owner; no review will be requested
  • .github/CONTRIBUTING.md — you are the sole owner; no review will be requested
  • .github/rulesets/RULESETS.md — you are the sole owner; no review will be requested
  • .github/rulesets/main-branch.json — you are the sole owner; no review will be requested
  • .github/workflows/codeowners-notice.yml — you are the sole owner; no review will be requested
  • CLAUDE.md — you are the sole owner; no review will be requested
  • GOVERNANCE.md — you are the sole owner; no review will be requested
  • Taskfile.yml — you are the sole owner; no review will be requested
  • docs/adr/0015-selective-code-owner-review.md — you are the sole owner; no review will be requested
  • taskfiles/check.Taskfile.yml — you are the sole owner; no review will be requested
  • taskfiles/ci.Taskfile.yml — you are the sole owner; no review will be requested
  • tools/src/rulesets.ts — you are the sole owner; no review will be requested

Double-check they are intentional. If you want a second pair of eyes, request a review manually. (Automated notice from the codeowners-notice workflow — ADR 0015.)

@justinmerrell
justinmerrell merged commit 6d9160a into main Aug 25, 2026
7 checks passed
@justinmerrell
justinmerrell deleted the feat/selective-code-owner-review branch August 25, 2026 16:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant