Skip to content

Commit 5397fe8

Browse files
chore: sync docs-review-workflows (#25682)
1 parent 24bd060 commit 5397fe8

3 files changed

Lines changed: 112 additions & 45 deletions

File tree

.github/workflows/add_docs_review_label.yml

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
1-
# Adds a label to PRs that touch documentation paths until a maintainer approves.
1+
# Docs review hold — workflow 1/3
2+
#
3+
# Purpose: avoid pulling the docs team into a PR review before the code is
4+
# directionally approved by a maintainer. This workflow adds
5+
# "docs review on hold" to any PR touching documentation paths, and the label is
6+
# cleared automatically once a MEMBER or OWNER approves (see check_docs_review.yml).
7+
#
8+
# Design is intentionally best-effort. Edge cases such as an approval being later
9+
# dismissed without a new commit are accepted — the label corrects itself on the
10+
# next push. add and check/remove use separate concurrency groups (pr-labels-add-*
11+
# vs pr-labels-check-*) so a new push cannot cancel a pending approval-check run.
12+
# Each workflow independently checks state before acting, so concurrent runs
13+
# across groups are safe.
14+
#
215
# Currently dedicated to the docs review flow. If a second use case appears, promote
316
# this into a reusable workflow (`workflow_call`) with the label as an input.
417
name: Add Docs Review Label
518

6-
concurrency:
7-
group: pr-labels-${{ github.event.pull_request.number }}
8-
cancel-in-progress: false
9-
1019
permissions:
1120
pull-requests: write
1221

@@ -21,6 +30,10 @@ on:
2130
- "website/content/**"
2231
- "website/cue/reference/**"
2332

33+
concurrency:
34+
group: pr-labels-add-${{ github.event.pull_request.number }}
35+
cancel-in-progress: false
36+
2437
env:
2538
LABEL_NAME: "docs review on hold"
2639

@@ -29,34 +42,21 @@ jobs:
2942
runs-on: ubuntu-24.04
3043
timeout-minutes: 5
3144
steps:
32-
- name: Check member review state
45+
- name: Check member approval
3346
id: check
3447
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
3548
with:
3649
script: |
37-
const { data: reviews } = await github.rest.pulls.listReviews({
50+
const reviews = await github.paginate(github.rest.pulls.listReviews, {
3851
owner: context.repo.owner,
3952
repo: context.repo.repo,
4053
pull_number: context.payload.pull_request.number,
4154
per_page: 100,
4255
});
43-
4456
const allowed = ['MEMBER', 'OWNER'];
45-
const latestByUser = new Map();
46-
for (const r of reviews) {
47-
if (!allowed.includes(r.author_association)) continue;
48-
if (r.state === 'COMMENTED') continue;
49-
latestByUser.set(r.user.id, r.state);
50-
}
51-
52-
const states = [...latestByUser.values()];
53-
const hasApproved = states.includes('APPROVED');
54-
const hasChangesRequested = states.includes('CHANGES_REQUESTED');
55-
const skip = hasApproved && !hasChangesRequested;
56-
57-
core.info(`Latest member review states: ${states.join(', ') || '(none)'}`);
58-
core.info(`skip=${skip}`);
59-
core.setOutput('skip', skip);
57+
const approved = reviews.some(r => allowed.includes(r.author_association) && r.state === 'APPROVED');
58+
core.info(`Member approval found: ${approved}`);
59+
core.setOutput('skip', String(approved));
6060
- if: steps.check.outputs.skip != 'true'
6161
uses: actions-ecosystem/action-add-labels@18f1af5e3544586314bbe15c0273249c770b2daf # v1.1.3
6262
with:

.github/workflows/check_docs_review.yml

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,59 @@
1+
# Docs review hold — workflow 2/3
2+
#
3+
# Fires on every review submission or dismissal. If at least one MEMBER/OWNER
4+
# has an APPROVED review and the hold label is present, writes the PR number to
5+
# an artifact for remove_docs_review_label.yml (3/3) to act on.
6+
#
7+
# The workflow_run indirection exists for a security reason: pull_request_review
8+
# events from fork PRs run with a read-only token, so label writes must happen in
9+
# a separate workflow that runs in the base-repo context with a write token.
10+
#
11+
# Review state check is intentionally simple: any APPROVED review from a
12+
# MEMBER/OWNER is sufficient, regardless of later CHANGES_REQUESTED from the
13+
# same reviewer. Best-effort is good enough here.
114
name: Check Docs Review
215

316
on:
417
pull_request_review:
5-
types: [submitted]
18+
types: [submitted, dismissed]
619

720
permissions:
821
contents: read
922
pull-requests: read
1023

24+
concurrency:
25+
group: pr-labels-check-${{ github.event.pull_request.number }}
26+
cancel-in-progress: false
27+
1128
env:
1229
LABEL_NAME: "docs review on hold"
1330

1431
jobs:
1532
check:
16-
if: github.event.review.state == 'approved'
1733
runs-on: ubuntu-24.04
1834
timeout-minutes: 5
1935
steps:
20-
- name: Check association and label
36+
- name: Check approval and label
2137
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
2238
with:
2339
script: |
24-
const association = context.payload.review.author_association;
25-
core.info(`Author association: ${association}`);
26-
2740
const allowed = ['MEMBER', 'OWNER'];
28-
if (!allowed.includes(association)) {
29-
core.info(`Association "${association}" not in allowed list, skipping`);
30-
return;
31-
}
41+
const reviews = await github.paginate(github.rest.pulls.listReviews, {
42+
owner: context.repo.owner,
43+
repo: context.repo.repo,
44+
pull_number: context.payload.pull_request.number,
45+
per_page: 100,
46+
});
47+
const approved = reviews.some(r => allowed.includes(r.author_association) && r.state === 'APPROVED');
48+
core.info(`Member approval found: ${approved}`);
49+
if (!approved) return;
3250
3351
const labelName = process.env.LABEL_NAME;
34-
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
52+
const labels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
3553
owner: context.repo.owner,
3654
repo: context.repo.repo,
3755
issue_number: context.payload.pull_request.number,
56+
per_page: 100,
3857
});
3958
if (!labels.some(l => l.name === labelName)) {
4059
core.info(`Label "${labelName}" not found, skipping`);
Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# Docs review hold — workflow 3/3
2+
#
3+
# Triggered by check_docs_review.yml (2/3) completing successfully. Runs in the
4+
# base-repo context so it has a write token, which is required to modify labels
5+
# on fork PRs. Re-checks review state immediately before removal to reduce (but
6+
# not eliminate) the chance of removing the label while a CHANGES_REQUESTED was
7+
# submitted in the small window between check and removal. Best-effort is fine.
18
name: Remove Docs Review Label
29

310
on:
@@ -13,14 +20,14 @@ env:
1320
LABEL_NAME: "docs review on hold"
1421

1522
jobs:
16-
remove_label:
23+
download:
1724
if: github.event.workflow_run.conclusion == 'success'
1825
runs-on: ubuntu-24.04
1926
timeout-minutes: 5
2027
permissions:
2128
actions: read
22-
issues: write
23-
pull-requests: write
29+
outputs:
30+
pr_number: ${{ steps.read.outputs.pr_number }}
2431
steps:
2532
- name: Download PR number
2633
id: download
@@ -31,23 +38,64 @@ jobs:
3138
run-id: ${{ github.event.workflow_run.id }}
3239
github-token: ${{ github.token }}
3340

34-
- name: Remove label
41+
- name: Read PR number
42+
id: read
3543
if: steps.download.outcome == 'success'
44+
run: |
45+
pr_number=$(cat pr-number | tr -d '[:space:]')
46+
echo "pr_number=${pr_number}" >> $GITHUB_OUTPUT
47+
48+
remove_label:
49+
needs: download
50+
if: needs.download.outputs.pr_number != ''
51+
runs-on: ubuntu-24.04
52+
timeout-minutes: 5
53+
concurrency:
54+
group: pr-labels-${{ needs.download.outputs.pr_number }}
55+
cancel-in-progress: false
56+
permissions:
57+
issues: write
58+
pull-requests: write
59+
steps:
60+
- name: Remove label
3661
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
62+
env:
63+
PR_NUMBER: ${{ needs.download.outputs.pr_number }}
3764
with:
3865
script: |
39-
const fs = require('fs');
40-
const prNumber = parseInt(fs.readFileSync('pr-number', 'utf8').trim(), 10);
66+
const prNumber = parseInt(process.env.PR_NUMBER, 10);
4167
if (isNaN(prNumber)) {
4268
core.setFailed('Invalid PR number');
4369
return;
4470
}
4571
46-
const labelName = process.env.LABEL_NAME;
47-
await github.rest.issues.removeLabel({
72+
const allowed = ['MEMBER', 'OWNER'];
73+
const reviews = await github.paginate(github.rest.pulls.listReviews, {
4874
owner: context.repo.owner,
4975
repo: context.repo.repo,
50-
issue_number: prNumber,
51-
name: labelName,
76+
pull_number: prNumber,
77+
per_page: 100,
5278
});
53-
core.info(`Removed "${labelName}" label from PR #${prNumber}`);
79+
const approved = reviews.some(r => allowed.includes(r.author_association) && r.state === 'APPROVED');
80+
core.info(`Member approval found: ${approved}`);
81+
if (!approved) {
82+
core.info('No member approval, skipping removal');
83+
return;
84+
}
85+
86+
const labelName = process.env.LABEL_NAME;
87+
try {
88+
await github.rest.issues.removeLabel({
89+
owner: context.repo.owner,
90+
repo: context.repo.repo,
91+
issue_number: prNumber,
92+
name: labelName,
93+
});
94+
core.info(`Removed "${labelName}" label from PR #${prNumber}`);
95+
} catch (err) {
96+
if (err.status === 404) {
97+
core.info(`Label "${labelName}" already absent from PR #${prNumber}, skipping`);
98+
} else {
99+
throw err;
100+
}
101+
}

0 commit comments

Comments
 (0)