Repository navigation
109 lines (100 loc) · 4.71 KB
/
Copy pathauto-merge-docs-tests.yml
File metadata and controls
109 lines (100 loc) · 4.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
name: auto-merge-docs-tests
on:
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review, converted_to_draft, edited]
workflow_dispatch:
inputs:
pull_request:
description: Pull request number
required: true
type: number
permissions:
contents: write
pull-requests: write
concurrency:
group: auto-merge-docs-tests-${{ github.event.pull_request.number || inputs.pull_request }}
cancel-in-progress: false
jobs:
auto-merge:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
# Inspect API metadata only: never check out or execute the PR's code.
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_TOKEN: ${{ github.token }}
with:
script: |
const number = context.payload.pull_request?.number
?? Number(context.payload.inputs.pull_request);
const params = { ...context.repo, pull_number: number };
const { data: pr } = await github.rest.pulls.get(params);
if (pr.state !== 'open') return;
const allowed = path => /^(docs|tests)\//.test(path);
const files = await github.paginate(github.rest.pulls.listFiles, {
...params, per_page: 100,
});
const eligible = pr.base.ref === 'master' && !pr.draft
&& files.length > 0 && files.length === pr.changed_files
&& files.every(file => allowed(file.filename)
&& (!file.previous_filename || allowed(file.previous_filename)));
// A push or retarget during pagination invalidates this decision.
const { data: current } = await github.rest.pulls.get(params);
if (current.state !== 'open'
|| current.head.sha !== pr.head.sha
|| current.base.sha !== pr.base.sha
|| current.base.ref !== pr.base.ref
|| current.draft !== pr.draft
|| current.changed_files !== pr.changed_files) {
core.info('PR changed during inspection; leave it to the next run.');
return;
}
const repo = `${context.repo.owner}/${context.repo.repo}`;
const approvalBody = 'Automatically approved: changes are limited to docs/ and tests/.';
const reviews = await github.paginate(github.rest.pulls.listReviews, {
...params, per_page: 100,
});
const approvals = reviews.filter(review =>
review.user?.login === 'github-actions[bot]'
&& review.body === approvalBody && review.state === 'APPROVED');
if (!eligible) {
for (const review of approvals) {
await github.rest.pulls.dismissReview({
...params, review_id: review.id,
message: 'The PR no longer qualifies for docs/tests auto-approval.',
});
}
// Preserve auto-merge requests explicitly enabled by a maintainer.
if (current.auto_merge?.enabled_by?.login === 'github-actions[bot]') {
await exec.exec('gh', [
'pr', 'merge', String(number), '--repo', repo, '--disable-auto',
]);
}
core.info('Only non-draft PRs confined to docs/ and tests/ qualify.');
return;
}
// Discarding stale reviews prevents approval from carrying over to code changes.
const { data: rules } = await github.request(
'GET /repos/{owner}/{repo}/rules/branches/{branch}',
{ ...context.repo, branch: 'master' },
);
if (!rules.some(rule => rule.type === 'pull_request'
&& rule.parameters.dismiss_stale_reviews_on_push)) {
throw new Error('Auto-approval requires dismissal of stale reviews.');
}
if (current.user?.login === 'github-actions[bot]') {
core.info('GitHub Actions cannot approve its own PR; a maintainer must review it.');
} else if (!approvals.some(review => review.commit_id === current.head.sha)) {
await github.rest.pulls.createReview({
...params, commit_id: current.head.sha,
event: 'APPROVE', body: approvalBody,
});
}
if (current.auto_merge) return;
const emoji = files.some(file => file.filename.startsWith('tests/'))
? '✅' : '📝';
await exec.exec('gh', [
'pr', 'merge', String(number), '--repo', repo,
'--auto', '--squash', '--match-head-commit', current.head.sha,
'--subject', `${emoji} ${current.title} (#${number})`,
]);