Skip to content

Commit 0d2df95

Browse files
authored
Merge pull request #10422 from The-OpenROAD-Project-staging/codeowners-sync-reviewers
ci: request CODEOWNERS reviewers on staging-sync PRs
2 parents cc8b927 + 6cdf883 commit 0d2df95

1 file changed

Lines changed: 83 additions & 0 deletions

File tree

.github/workflows/github-actions-on-label-create.yml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,86 @@ jobs:
8888
env:
8989
GITHUB_TOKEN: ${{ github.token }}
9090
UPSTREAM_PR: ${{ steps.send_pr.outputs.pr }}
91+
92+
- name: Request CODEOWNERS reviewers on upstream PR
93+
if: steps.send_pr.outputs.pr != ''
94+
env:
95+
GH_TOKEN: ${{ steps.resolve_token.outputs.token }}
96+
PR: ${{ steps.send_pr.outputs.pr }}
97+
UPSTREAM: ${{ env.UPSTREAM_OWNER }}/${{ env.UPSTREAM_REPO }}
98+
run: |
99+
set -euo pipefail
100+
# System Python on the runner is PEP 668 externally-managed; use a venv.
101+
python3 -m venv /tmp/codeowners-venv
102+
/tmp/codeowners-venv/bin/pip install --quiet pathspec
103+
/tmp/codeowners-venv/bin/python <<'PY'
104+
import base64, json, os, subprocess, sys
105+
from pathspec import GitIgnoreSpec
106+
107+
pr = os.environ["PR"]
108+
upstream = os.environ["UPSTREAM"]
109+
110+
pr_json = subprocess.check_output(
111+
["gh", "api", f"repos/{upstream}/pulls/{pr}"], text=True)
112+
pr_data = json.loads(pr_json)
113+
author = pr_data["user"]["login"]
114+
base_ref = pr_data["base"]["ref"]
115+
116+
# Authoritative CODEOWNERS is the one on the PR base branch.
117+
raw = subprocess.check_output(
118+
["gh", "api", "--method", "GET",
119+
f"repos/{upstream}/contents/.github/CODEOWNERS",
120+
"-f", f"ref={base_ref}", "--jq", ".content"], text=True).strip()
121+
codeowners = base64.b64decode(raw).decode()
122+
123+
rules = []
124+
for line in codeowners.splitlines():
125+
line = line.split("#", 1)[0].strip()
126+
if not line:
127+
continue
128+
pattern, *rule_owners = line.split()
129+
rules.append((GitIgnoreSpec.from_lines([pattern]), rule_owners))
130+
131+
# GitHub caps /pulls/{n}/files at 3000 even with --paginate; truly
132+
# enormous PRs will under-request owners for the overflow.
133+
files = subprocess.check_output(
134+
["gh", "api", f"repos/{upstream}/pulls/{pr}/files", "--paginate",
135+
"--jq", ".[].filename"], text=True).splitlines()
136+
137+
owners = set()
138+
for path in files:
139+
matched = None
140+
for spec, rule_owners in rules:
141+
if spec.match_file(path):
142+
matched = rule_owners # last match wins
143+
if matched:
144+
owners.update(o.lstrip("@") for o in matched)
145+
146+
# CODEOWNERS lists teams as "org/slug"; the REST endpoint wants the
147+
# bare slug in team_reviewers.
148+
team_slugs = sorted(t.split("/", 1)[1] for t in owners if "/" in t)
149+
users = sorted(o for o in owners
150+
if "/" not in o and o.lower() != author.lower())
151+
152+
if not (team_slugs or users):
153+
print("No CODEOWNERS-matched reviewers.")
154+
sys.exit(0)
155+
156+
# Use the REST POST endpoint directly: `gh pr edit --add-reviewer`
157+
# runs a GraphQL query that needs read:org, which our tokens don't
158+
# have. POST /pulls/{n}/requested_reviewers only writes, so the
159+
# existing repo / pull-requests:write scope is enough. Each array
160+
# is capped at 15 per call.
161+
def request(body):
162+
print("Requesting:", body)
163+
subprocess.run(
164+
["gh", "api", "--method", "POST",
165+
f"repos/{upstream}/pulls/{pr}/requested_reviewers",
166+
"--input", "-"],
167+
input=json.dumps(body), text=True, check=True)
168+
169+
for i in range(0, len(team_slugs), 15):
170+
request({"team_reviewers": team_slugs[i:i + 15]})
171+
for i in range(0, len(users), 15):
172+
request({"reviewers": users[i:i + 15]})
173+
PY

0 commit comments

Comments
 (0)