Per-path approval and per-group reviewer suggestions#4918
Merged
Conversation
Replace the author-based isExempted check with per-path ownership group approval. Each OWNERS pattern group now needs at least one approval from its owners (or a team member for org/team refs). - Add getOwnershipGroups to owners.js (groups files by last-match-wins OWNERS pattern) - Add checkPerPathApproval to maintainer-approval.js with team membership resolution via GitHub Teams API - Wildcard-only files require a maintainer - Failure messages list uncovered groups with their owners - PR author is excluded from approver list (self-approval defense) Co-authored-by: Isaac
When a PR touches files across multiple ownership areas (as defined in OWNERS), suggest-reviewers.js now shows a structured "Reviewers by ownership area" comment with per-group sections instead of a flat list. Each section shows the matched files, team refs, and individual owners ranked by git history score. Single-domain PRs keep the existing flat format. Co-authored-by: Isaac
Use createCommitStatus instead of core.setFailed so that PRs waiting for approval show a yellow pending dot, not a red failure X. The workflow itself always succeeds; the commit status context "maintainer-approval" is what branch rulesets check. Co-authored-by: Isaac
…ad args - isTeamMember now distinguishes 404 (not a member) from permission errors (403/other), logging a core.warning when team resolution fails due to insufficient token scope - Wildcard reviewer suggestions now filter to maintainers only, since non-maintainers cannot approve wildcard files - checkPerPathApproval accepts rulesWithTeams directly instead of re-parsing OWNERS internally; removed unused findOwners import and dead rules parameter - Added YAML comment about org:read token requirement for team membership Co-authored-by: Isaac
If the PR author is a maintainer, any single approval (from anyone) is sufficient. Maintainers already have the trust to approve anything, so the review gate only serves as a "second pair of eyes" check. Co-authored-by: Isaac
Tests for owners.js (24 cases) covering ownersMatch, parseOwnersFile, findOwners, getMaintainers, and getOwnershipGroups. Tests for maintainer-approval.js (12 cases) covering maintainer approval, maintainer-authored PRs, per-path approval, team membership, wildcard files, self-approval exclusion, and missing OWNERS rules. Uses node:test runner with zero external dependencies. Co-authored-by: Isaac
mihaimitrea-db
approved these changes
Apr 8, 2026
denik
pushed a commit
that referenced
this pull request
May 20, 2026
## Why
Cross-domain PRs aren't handled correctly by the current
maintainer-approval workflow. If a PR touches `/cmd/pipelines/` and
`/cmd/apps/`, a single approval from either team covers the whole PR.
Each domain team should approve their own area independently.
The `suggest-reviewers` comment also doesn't surface which ownership
areas need review. It just shows a flat list.
On top of that, the workflow uses `core.setFailed()` to block PRs
missing approval. This shows a red X, which is misleading: "waiting for
approval" isn't an error, it's a normal pending state.
## Changes
**Before:** One exemption check (`isExempted`) that only worked when the
PR author owned all changed files. Cross-domain PRs fell through to
requiring a maintainer. `suggest-reviewers` showed a flat list. Missing
approval = red X.
**Now:** Per-path approval where each ownership group needs at least one
approval from its owners. `suggest-reviewers` shows per-group sections
for cross-domain PRs. Missing approval = yellow pending dot via Commit
Status API.
### `owners.js`
Added `getOwnershipGroups(filenames, rules)` that groups files by their
last-match-wins OWNERS pattern. Returns `Map<pattern, { owners, files
}>`. This is the shared building block used by both the approval check
and reviewer suggestions.
### `maintainer-approval.js`
Replaced `isExempted` with `checkPerPathApproval`. The new flow:
1. Any maintainer approved? -> `success` (unchanged)
2. PR author is a maintainer and has any approval? -> `success` (new,
any second pair of eyes suffices)
3. Group changed files by ownership. Any files matching only `*`? ->
`pending` (needs maintainer)
4. For each ownership group, has at least one owner approved? All
covered -> `success`. Some uncovered -> `pending` (lists which groups
need approval)
Team refs (`@org/team`) are resolved via `getMembershipForUserInOrg`.
The resolution distinguishes 404 (not a member) from permission errors
(logs a warning, falls back to requiring maintainer).
Switched from `core.setFailed()` to `createCommitStatus()` with
`pending`/`success` states, so "waiting for approval" is a yellow dot
instead of a red X. The ruleset should require the `maintainer-approval`
commit status context.
### `suggest-reviewers.js`
When a PR crosses 2+ ownership groups, shows "Reviewers by ownership
area" with per-group sections. Each section lists the matched files,
team refs, and individually ranked owners (by git history score).
Single-domain PRs keep the existing flat format. The wildcard section
(`*` files) only suggests maintainers, since only they can approve those
files.
### `OWNERS`
- Added `/bundle/`, `/acceptance/bundle/`, and `/acceptance/labs/` rules
mirroring their source counterparts
- Added section comments for readability
- Grouped rules by domain
### `maintainer-approval.yml`
- Added `statuses: write` permission (needed for `createCommitStatus`)
## Test plan
- [x] 36 unit tests using Node's built-in `node:test` runner (zero
dependencies)
- 24 tests for `owners.js`: `ownersMatch`, `parseOwnersFile`,
`findOwners`, `getMaintainers`, `getOwnershipGroups`
- 12 tests for `maintainer-approval.js` with mocked GitHub API:
maintainer approval, maintainer-authored PR self-approval, single
domain, cross-domain (both covered and partially covered), wildcard
files, team member approval, non-team-member rejection,
`CHANGES_REQUESTED` exclusion, self-approval exclusion, missing `*` rule
error
- Run: `node --test .github/scripts/owners.test.js
.github/workflows/maintainer-approval.test.js`
- [ ] Verify on a single-domain PR (only `/cmd/pipelines/` files):
pending until pipelines owner approves
- [ ] Verify on a cross-domain PR (`/cmd/pipelines/` + `/cmd/apps/`):
pending until both groups approved
- [ ] Verify on a PR touching only `*` files: pending, message says
"needs maintainer"
- [ ] Verify maintainer approval short-circuits: success status
immediately
- [ ] Update branch ruleset to require `maintainer-approval` commit
status context
### Edge case coverage (from unit tests)
| Scenario | Expected | Tested |
|----------|----------|--------|
| PR touches only `*` files | Maintainer required, pending | yes |
| PR touches one domain only | One domain owner approval suffices | yes
|
| PR touches two domains | Both groups need approval | yes |
| PR touches domain + `*` files | Maintainer required (wildcard forces
it) | yes |
| Maintainer approves | Always success | yes |
| PR author is maintainer + any approval | Success (second pair of eyes)
| yes |
| Team member approves team-owned path | Success (via Teams API) | yes |
| `CHANGES_REQUESTED` review | Does not count as approval | yes |
| PR author self-approval | Excluded from approver list | yes |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Cross-domain PRs aren't handled correctly by the current maintainer-approval workflow. If a PR touches
/cmd/pipelines/and/cmd/apps/, a single approval from either team covers the whole PR. Each domain team should approve their own area independently.The
suggest-reviewerscomment also doesn't surface which ownership areas need review. It just shows a flat list.On top of that, the workflow uses
core.setFailed()to block PRs missing approval. This shows a red X, which is misleading: "waiting for approval" isn't an error, it's a normal pending state.Changes
Before: One exemption check (
isExempted) that only worked when the PR author owned all changed files. Cross-domain PRs fell through to requiring a maintainer.suggest-reviewersshowed a flat list. Missing approval = red X.Now: Per-path approval where each ownership group needs at least one approval from its owners.
suggest-reviewersshows per-group sections for cross-domain PRs. Missing approval = yellow pending dot via Commit Status API.owners.jsAdded
getOwnershipGroups(filenames, rules)that groups files by their last-match-wins OWNERS pattern. ReturnsMap<pattern, { owners, files }>. This is the shared building block used by both the approval check and reviewer suggestions.maintainer-approval.jsReplaced
isExemptedwithcheckPerPathApproval. The new flow:success(unchanged)success(new, any second pair of eyes suffices)*? ->pending(needs maintainer)success. Some uncovered ->pending(lists which groups need approval)Team refs (
@org/team) are resolved viagetMembershipForUserInOrg. The resolution distinguishes 404 (not a member) from permission errors (logs a warning, falls back to requiring maintainer).Switched from
core.setFailed()tocreateCommitStatus()withpending/successstates, so "waiting for approval" is a yellow dot instead of a red X. The ruleset should require themaintainer-approvalcommit status context.suggest-reviewers.jsWhen a PR crosses 2+ ownership groups, shows "Reviewers by ownership area" with per-group sections. Each section lists the matched files, team refs, and individually ranked owners (by git history score). Single-domain PRs keep the existing flat format. The wildcard section (
*files) only suggests maintainers, since only they can approve those files.OWNERS/bundle/,/acceptance/bundle/, and/acceptance/labs/rules mirroring their source counterpartsmaintainer-approval.ymlstatuses: writepermission (needed forcreateCommitStatus)Test plan
node:testrunner (zero dependencies)owners.js:ownersMatch,parseOwnersFile,findOwners,getMaintainers,getOwnershipGroupsmaintainer-approval.jswith mocked GitHub API: maintainer approval, maintainer-authored PR self-approval, single domain, cross-domain (both covered and partially covered), wildcard files, team member approval, non-team-member rejection,CHANGES_REQUESTEDexclusion, self-approval exclusion, missing*rule errornode --test .github/scripts/owners.test.js .github/workflows/maintainer-approval.test.js/cmd/pipelines/files): pending until pipelines owner approves/cmd/pipelines/+/cmd/apps/): pending until both groups approved*files: pending, message says "needs maintainer"maintainer-approvalcommit status contextEdge case coverage (from unit tests)
*files*filesCHANGES_REQUESTEDreview