Skip to content

Per-path approval and per-group reviewer suggestions#4918

Merged
simonfaltum merged 9 commits into
mainfrom
simonfaltum/per-path-approval
Apr 8, 2026
Merged

Per-path approval and per-group reviewer suggestions#4918
simonfaltum merged 9 commits into
mainfrom
simonfaltum/per-path-approval

Conversation

@simonfaltum

@simonfaltum simonfaltum commented Apr 8, 2026

Copy link
Copy Markdown
Member

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

  • 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

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
simonfaltum and others added 4 commits April 8, 2026 17:50
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
@simonfaltum simonfaltum added this pull request to the merge queue Apr 8, 2026
Merged via the queue into main with commit d3ba591 Apr 8, 2026
19 of 20 checks passed
@simonfaltum simonfaltum deleted the simonfaltum/per-path-approval branch April 8, 2026 17:10
github-merge-queue Bot pushed a commit that referenced this pull request Apr 21, 2026
## Changes

Update the `CODEOWNERS` for the `labs` subcommand to include @asnare.

2026-04-21: Updated to modify `OWNERS`, since `CODEOWNERS` was migrated
into that file as part of #4918.
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 |
denik pushed a commit that referenced this pull request May 20, 2026
## Changes

Update the `CODEOWNERS` for the `labs` subcommand to include @asnare.

2026-04-21: Updated to modify `OWNERS`, since `CODEOWNERS` was migrated
into that file as part of #4918.
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.

2 participants