Skip to content

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

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

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

Conversation

@simonfaltum
Copy link
Copy Markdown
Member

@simonfaltum simonfaltum commented Apr 8, 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

  • 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
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