Repository navigation
Expand file tree
/
Copy pathdependency-approval-command.yml
More file actions
160 lines (143 loc) · 7.17 KB
/
Copy pathdependency-approval-command.yml
File metadata and controls
160 lines (143 loc) · 7.17 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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# Dependency Approval via Comment Command
# Allows maintainers to approve dependency changes by commenting /approve-dependencies
#
# Besides adding the `dependencies-reviewed` label, this workflow also flips the
# "Dependency Security Review" commit status to success. Events created with the
# default GITHUB_TOKEN (like this label add) cannot trigger other workflows, so
# the dependency-security workflows will NOT re-run — the status must be updated
# here directly. Only a 'failure' (awaiting review) status is flipped; 'error'
# states (failed analysis, high-risk packages, known vulnerabilities) cannot be
# overridden by this command.
name: Dependency Approval Command
on:
issue_comment:
types: [created]
permissions:
issues: write
pull-requests: write
statuses: write
jobs:
approve-via-command:
runs-on: ubuntu-latest
# Only run on PRs, not issues
if: github.event.issue.pull_request
steps:
- name: Check for approval command
uses: actions/github-script@v9
with:
script: |
const comment = context.payload.comment;
const commentBody = comment.body.trim();
// Check if comment contains the approval command
if (!commentBody.match(/^\/approve-dependencies$/m)) {
core.info('Not an approval command, skipping');
return;
}
// Check if the comment creator is a maintainer/admin
const userPermission = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: comment.user.login
});
const hasPermission = ['admin', 'write'].includes(userPermission.data.permission);
if (!hasPermission) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `❌ @${comment.user.login} does not have permission to approve dependencies. Only maintainers with write access can approve.`
});
return;
}
// Check if already approved (label add is skipped below, but the
// status flip still runs so the command can repair a stuck status)
const labels = context.payload.issue.labels.map(l => l.name);
const alreadyApproved = labels.includes('dependencies-reviewed');
if (!alreadyApproved) {
// Add the dependencies-reviewed label
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['dependencies-reviewed']
});
}
// Flip the "Dependency Security Review" commit status. The label add
// above uses GITHUB_TOKEN and therefore cannot re-trigger the
// dependency-security workflows, so the status is updated here. This
// runs even when the label was already present, so the command can
// repair a stuck failing (or missing) status.
// Only 'failure' (awaiting review) or missing statuses may be
// flipped; 'error' (failed analysis / high risk / vulnerabilities)
// must be resolved by a re-run or a fix and cannot be approved away.
const statusContext = 'Dependency Security Review';
const pr = (await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
})).data;
// The commit status is per-SHA: if other open PRs share this head
// commit, flipping it here would greenlight them too. Only flip when
// every such PR carries the review label.
const assoc = await github.paginate(
github.rest.repos.listPullRequestsAssociatedWithCommit,
{
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: pr.head.sha,
per_page: 100,
},
);
const unapprovedSibling = assoc.find(p =>
p.number !== pr.number &&
p.state === 'open' &&
p.head.sha === pr.head.sha &&
!p.labels.map(l => l.name).includes('dependencies-reviewed')
);
const statuses = await github.paginate(github.rest.repos.listCommitStatusesForRef, {
owner: context.repo.owner,
repo: context.repo.repo,
ref: pr.head.sha,
per_page: 100,
});
// Statuses are newest-first; the first match is the current state.
const current = statuses.find(s => s.context === statusContext);
let statusNote = '';
if (current && current.state === 'error') {
statusNote = `⚠️ The security check reported an error ("${current.description}") which cannot be overridden by this command. Resolve it or re-run the Dependency Security Check workflow.`;
} else if (current && current.state === 'pending') {
statusNote = 'The dependency security analysis is still running; the status will reflect the approval once it completes.';
} else if (current && current.state === 'success') {
statusNote = 'The security check is already passing.';
} else if (unapprovedSibling) {
statusNote = `⚠️ The commit status was not updated: PR #${unapprovedSibling.number} shares the same head commit and has not been approved yet. Approve it as well, or re-run the Dependency Security Check workflow.`;
} else {
// 'failure' (awaiting review) or no status yet: publish approval
await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: pr.head.sha,
state: 'success',
context: statusContext,
description: `Dependency changes reviewed (approved by @${comment.user.login})`,
});
statusNote = 'Security checks will now pass and this PR can be merged.';
}
// Add a confirmation comment
const labelNote = alreadyApproved
? 'The `dependencies-reviewed` label was already present.'
: 'The `dependencies-reviewed` label has been added.';
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `✅ **Dependencies approved** by @${comment.user.login}\n\n${labelNote} ${statusNote}`
});
// Add a reaction to the command comment
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: comment.id,
content: '+1'
});
core.info('✅ Dependencies approved and label added');