Skip to content

Commit f63ab98

Browse files
Migrate to Codeowners Plus for AND-based ownership enforcement (#12729)
1 parent b5b91c9 commit f63ab98

File tree

4 files changed

+275
-0
lines changed

4 files changed

+275
-0
lines changed

.codeowners

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# ============================================================
2+
# Codeowners Plus - Code Ownership Rules
3+
# ============================================================
4+
# This file defines code ownership for the workers-sdk monorepo,
5+
# enforced by Codeowners Plus (https://github.com/multimediallc/codeowners-plus).
6+
#
7+
# See CODEOWNERS.md for full documentation on how ownership works.
8+
#
9+
# Syntax:
10+
# (no prefix) = primary owner (highest-priority match wins)
11+
# & prefix = AND rule (additional required reviewer)
12+
# ? prefix = optional/CC reviewer (non-blocking)
13+
# Multiple teams on one line = OR (either can satisfy)
14+
#
15+
# Rules are relative to this file's directory (repo root).
16+
# Unlike GitHub CODEOWNERS, `*.js` only matches in this directory;
17+
# use `**/*.js` for recursive matching.
18+
#
19+
# Paths not matching any specific rule fall through to the default
20+
# primary owner (currently @cloudflare/wrangler).
21+
# ============================================================
22+
23+
# Default owner - ANT/Wrangler team owns everything
24+
* @cloudflare/wrangler
25+
26+
# ----------------------------------------------------------
27+
# D&C ownership (AND: requires wrangler + deploy-config)
28+
# ----------------------------------------------------------
29+
& packages/workers-shared/** @cloudflare/deploy-config
30+
31+
# ----------------------------------------------------------
32+
# D1 ownership (AND: requires wrangler + d1)
33+
# ----------------------------------------------------------
34+
& packages/wrangler/src/api/d1/** @cloudflare/d1
35+
& packages/wrangler/src/d1/** @cloudflare/d1
36+
& packages/wrangler/src/__tests__/d1/** @cloudflare/d1
37+
38+
# ----------------------------------------------------------
39+
# Cloudchamber ownership (AND: requires wrangler + cloudchamber)
40+
# ----------------------------------------------------------
41+
& packages/wrangler/src/cloudchamber/** @cloudflare/cloudchamber
42+
& packages/wrangler/src/containers/** @cloudflare/cloudchamber
43+
& packages/containers-shared/** @cloudflare/cloudchamber
44+
45+
# ----------------------------------------------------------
46+
# Workers KV ownership (AND: requires wrangler + workers-kv)
47+
# ----------------------------------------------------------
48+
& packages/wrangler/src/kv/** @cloudflare/workers-kv
49+
& packages/wrangler/src/__tests__/kv/** @cloudflare/workers-kv
50+
& packages/miniflare/src/workers/kv/** @cloudflare/workers-kv
51+
& packages/miniflare/test/plugins/kv/** @cloudflare/workers-kv
52+
53+
# ----------------------------------------------------------
54+
# Workflows ownership (AND: requires wrangler + workflows)
55+
# ----------------------------------------------------------
56+
& packages/workflows-shared/** @cloudflare/workflows
57+
58+
# ----------------------------------------------------------
59+
# Adding a new product team
60+
# ----------------------------------------------------------
61+
# Copy this template and fill in <feature> and <team>:
62+
#
63+
# # Product: <Name> (AND: requires wrangler + <team>)
64+
# & packages/wrangler/src/<feature>/** @cloudflare/<team>
65+
# & packages/wrangler/src/__tests__/<feature>/** @cloudflare/<team>
66+
# & packages/miniflare/src/plugins/<feature>/** @cloudflare/<team>
67+
# & packages/miniflare/src/workers/<feature>/** @cloudflare/<team>
68+
# & packages/miniflare/test/plugins/<feature>/** @cloudflare/<team>

.github/workflows/codeowners.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: "Code Owners"
2+
3+
# Re-evaluate when PRs are opened/updated, and when reviews are submitted/dismissed.
4+
# Using pull_request_target (not pull_request) so the workflow has access to secrets
5+
# for fork PRs. This is safe because:
6+
# - The checkout is the BASE branch (ownership rules come from the protected branch)
7+
# - PR head commits are fetched as git objects only (never checked out or executed)
8+
# - The action only reads config files and calls the GitHub API
9+
on:
10+
pull_request_target:
11+
types: [opened, reopened, synchronize, ready_for_review, labeled, unlabeled]
12+
pull_request_review:
13+
types: [submitted, dismissed]
14+
15+
concurrency:
16+
group: codeowners-${{ github.event.pull_request.number }}
17+
cancel-in-progress: true
18+
19+
permissions:
20+
contents: read
21+
issues: write
22+
pull-requests: write
23+
24+
jobs:
25+
codeowners:
26+
name: "Run Codeowners Plus"
27+
runs-on: ubuntu-latest
28+
steps:
29+
- name: "Checkout Base Branch"
30+
uses: actions/checkout@v4
31+
with:
32+
fetch-depth: 0
33+
34+
- name: "Fetch PR Head (for diff computation)"
35+
run: git fetch origin +refs/pull/${{ github.event.pull_request.number }}/head
36+
env:
37+
GITHUB_TOKEN: "${{ secrets.READ_ONLY_ORG_GITHUB_TOKEN }}"
38+
39+
- name: "Codeowners Plus"
40+
uses: multimediallc/codeowners-plus@ff02aa993a92e8efe01642916d0877beb9439e9f # v1.9.0
41+
with:
42+
github-token: "${{ secrets.READ_ONLY_ORG_GITHUB_TOKEN }}"
43+
pr: "${{ github.event.pull_request.number }}"
44+
verbose: true
45+
quiet: ${{ github.event.pull_request.draft }}

CODEOWNERS.md

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Code Ownership
2+
3+
This repository uses [Codeowners Plus](https://github.com/multimediallc/codeowners-plus) to enforce code ownership and review requirements. This replaces GitHub's native CODEOWNERS with more fine-grained control — specifically, the ability to require approval from **multiple teams** (AND rules) before a PR can merge.
4+
5+
## How It Works
6+
7+
### Overview
8+
9+
When a PR is opened, updated, or reviewed, the Codeowners Plus GitHub Action runs. It reads `.codeowners` and `codeowners.toml` from the **base branch** (not the PR), evaluates the ownership rules, and:
10+
11+
- Posts a PR comment listing which teams need to approve
12+
- Requests reviews from those teams
13+
- Sets a **required status check** ("Run Codeowners Plus") that passes only when all ownership rules are satisfied
14+
15+
The native GitHub `CODEOWNERS` file is not involved in enforcement.
16+
17+
### Key Difference from Native CODEOWNERS
18+
19+
| Feature | Native GitHub CODEOWNERS | Codeowners Plus |
20+
| ------------------------ | --------------------------------- | ------------------------------------------------------------------------------- |
21+
| Multiple teams on a path | **OR** — any one team can approve | **AND** via `&` prefix — all listed teams must approve |
22+
| Path matching for `*.js` | Matches anywhere in repo | Matches only in the `.codeowners` file's directory; use `**/*.js` for recursive |
23+
| Per-directory config | Single file only | `.codeowners` file in any directory (rules are relative to that directory) |
24+
| Stale review dismissal | All-or-nothing | Smart — only dismisses when reviewer's owned files change |
25+
| Optional reviewers | Not supported | `?` prefix — CC without blocking |
26+
27+
## Configuration Files
28+
29+
### `.codeowners` — Ownership Rules
30+
31+
Located at the repo root. Defines who owns what using path patterns and team handles. See the comments in the file itself for syntax details and a template for adding new product teams.
32+
33+
### `codeowners.toml` — Advanced Configuration
34+
35+
Located at the repo root. Controls enforcement behavior, ignored paths, and admin bypass.
36+
37+
Key settings:
38+
39+
| Setting | Purpose |
40+
| -------------------------- | -------------------------------------------------------------------------- |
41+
| `ignore` | Directories excluded from ownership checks (e.g. `.changeset`, `fixtures`) |
42+
| `detailed_reviewers` | Show per-file owner breakdown in PR comments |
43+
| `suppress_unowned_warning` | Don't warn about files with no owner |
44+
| `enforcement.fail_check` | When `true`, the GHA check fails if rules aren't satisfied |
45+
| `admin_bypass.enabled` | Allow admins to bypass by approving with "Codeowners Bypass" text |
46+
47+
### `CODEOWNERS` — Native GitHub File
48+
49+
The native GitHub `CODEOWNERS` file is kept for reference but is **not the enforcement mechanism**. Enforcement is handled by the Codeowners Plus required status check. All ownership logic lives in `.codeowners`.
50+
51+
### `.github/workflows/codeowners.yml` — GitHub Actions Workflow
52+
53+
A single workflow handles all events:
54+
55+
- `pull_request_target` — PR opened, updated, marked ready, labeled
56+
- `pull_request_review` — review submitted or dismissed
57+
58+
Using `pull_request_target` (not `pull_request`) ensures the workflow has access to secrets for **fork PRs**. The checkout is always the base branch, so PR authors cannot modify ownership rules.
59+
60+
## Common Scenarios
61+
62+
### PR touches only wrangler-team-owned code
63+
64+
Example: changes to `packages/create-cloudflare/` or `packages/vite-plugin-cloudflare/`.
65+
66+
Only `@cloudflare/wrangler` approval is required.
67+
68+
### PR touches product-team-owned code
69+
70+
Example: changes to `packages/wrangler/src/d1/`.
71+
72+
**Both** `@cloudflare/wrangler` AND `@cloudflare/d1` must approve. Codeowners Plus will post a comment listing who still needs to approve and request reviews from both teams.
73+
74+
### PR touches multiple product areas
75+
76+
Example: changes to both `packages/wrangler/src/d1/` and `packages/wrangler/src/kv/`.
77+
78+
All three teams must approve: `@cloudflare/wrangler` + `@cloudflare/d1` + `@cloudflare/workers-kv`.
79+
80+
### PR touches ignored paths only
81+
82+
Example: changes only in `.changeset/` or `fixtures/`.
83+
84+
No ownership checks apply. The codeowners-plus check passes automatically.
85+
86+
### Draft PRs
87+
88+
The workflow runs in **quiet mode** for draft PRs:
89+
90+
- No PR comments posted
91+
- No review requests sent
92+
- The status check still runs for visibility
93+
94+
### Emergency bypass
95+
96+
Repository admins can bypass all requirements by submitting an **approval review** with the text "Codeowners Bypass" (case-insensitive). This creates an audit trail.
97+
98+
### Fork PRs
99+
100+
Fork PRs are fully supported. The workflow uses `pull_request_target` to run in the base repo context with access to secrets. The base branch is checked out (so ownership rules come from the protected branch), and the PR head is fetched as git objects only for diff computation. No fork code is executed.
101+
102+
## Adding a New Product Team
103+
104+
To add ownership for a new product team, add AND rules to `.codeowners`:
105+
106+
```bash
107+
# Product: <Name> (AND: requires wrangler + <team>)
108+
& packages/wrangler/src/<feature>/** @cloudflare/<team>
109+
& packages/wrangler/src/__tests__/<feature>/** @cloudflare/<team>
110+
& packages/miniflare/src/plugins/<feature>/** @cloudflare/<team>
111+
& packages/miniflare/src/workers/<feature>/** @cloudflare/<team>
112+
& packages/miniflare/test/plugins/<feature>/** @cloudflare/<team>
113+
```
114+
115+
For example, to add R2 ownership:
116+
117+
```bash
118+
# Product: R2 (AND: requires wrangler + r2)
119+
& packages/wrangler/src/r2/** @cloudflare/r2
120+
& packages/wrangler/src/__tests__/r2/** @cloudflare/r2
121+
& packages/miniflare/src/plugins/r2/** @cloudflare/r2
122+
& packages/miniflare/src/workers/r2/** @cloudflare/r2
123+
& packages/miniflare/test/plugins/r2/** @cloudflare/r2
124+
```
125+
126+
**Teams ready to add** (have source paths but no ownership entries yet):
127+
R2, Queues, AI, Hyperdrive, Vectorize, Pipelines, SSL/Secrets Store, WVPC.
128+
129+
## Stale Review Handling
130+
131+
Codeowners Plus uses **smart dismissal**: when new commits are pushed to a PR, it only dismisses an approval if the files owned by that reviewer were changed. This avoids the frustration of GitHub's all-or-nothing stale review dismissal.
132+
133+
For this to work, the branch protection setting **"Dismiss stale pull request approvals when new commits are pushed"** must be **disabled**. Codeowners Plus handles dismissal itself.
134+
135+
## References
136+
137+
- [Codeowners Plus documentation](https://github.com/multimediallc/codeowners-plus)
138+
- [Codeowners Plus action on GitHub Marketplace](https://github.com/marketplace/actions/codeowners-plus)
139+
- [GitHub branch protection docs](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches)

codeowners.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Codeowners Plus - Advanced Configuration
2+
# See: https://github.com/multimediallc/codeowners-plus
3+
# See: CODEOWNERS.md for full documentation
4+
5+
# Directories to ignore (no ownership checks, reduces review noise)
6+
ignore = [".changeset", "fixtures"]
7+
8+
# Show detailed file-to-owner mapping in PR comments
9+
detailed_reviewers = true
10+
11+
# Suppress warnings for intentionally unowned files (e.g. pnpm-lock.yaml)
12+
suppress_unowned_warning = true
13+
14+
[enforcement]
15+
# Enforcement is via a required GitHub status check ("Run Codeowners Plus").
16+
# The check fails when ownership rules are not satisfied, blocking merge.
17+
# No bot approval is used — the status check is the sole enforcement mechanism.
18+
fail_check = true
19+
20+
[admin_bypass]
21+
# Allow repo admins to bypass codeowner requirements in emergencies
22+
# by submitting an approval review containing "Codeowners Bypass" text
23+
enabled = true

0 commit comments

Comments
 (0)