-
Notifications
You must be signed in to change notification settings - Fork 11
196 lines (162 loc) · 6.65 KB
/
pr-workflow.yml
File metadata and controls
196 lines (162 loc) · 6.65 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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
name: PR workflow
on:
pull_request:
types: [opened, synchronize, ready_for_review] # include ready_for_review for draft PRs
jobs:
pr-automation:
runs-on: ubuntu-latest
steps:
# Step 1: Get changed files
- name: Get changed files
id: files
uses: tj-actions/changed-files@v44
# Step 2: Assign, label, request reviewers, and enforce approvals
- name: Assign, label, request reviewers, enforce approvals
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = context.payload.pull_request.number;
const author = context.payload.pull_request.user.login;
if (context.payload.pull_request.draft) {
console.log("PR is a draft — skipping workflow steps.");
return;
}
const changedFiles = "${{ steps.files.outputs.all_changed_files }}".split(" ").filter(f => f);
// -------------------
// CONFIGURATION
// -------------------
const rules = {
frontend: { path: "frontend/", label: "frontend", lead: ["MaddieWright"], team: [] },
backend: { path: "backend/", label: "backend", lead: ["HeisSteve"], team: [] },
infra: { path: ".github/", label: "infra", lead: ["HeisSteve", "MaddieWright" ], team: [] }
};
const labelsToAdd = new Set();
const reviewersToAdd = new Set();
const leadsToCheck = new Set();
const teamToCheck = new Set();
// Determine applicable rules
for (const file of changedFiles) {
for (const rule of Object.values(rules)) {
if (!file.startsWith(rule.path)) continue;
labelsToAdd.add(rule.label);
// Normalize leads to an array
const leads = Array.isArray(rule.lead) ? rule.lead : [rule.lead];
// Add leads for approval & reviewers
leads.forEach(u => {
leadsToCheck.add(u);
reviewersToAdd.add(u);
});
// Add team members for approval & reviewers
rule.team.forEach(u => {
teamToCheck.add(u);
reviewersToAdd.add(u);
});
}
}
// Assign PR author
await github.rest.issues.addAssignees({
owner,
repo,
issue_number: prNumber,
assignees: [author]
});
// Get existing labels
const existingLabels = await github.rest.issues.listLabelsOnIssue({
owner,
repo,
issue_number: prNumber
});
const managedLabels = Object.values(rules).map(r => r.label);
const existingManagedLabels = existingLabels.data
.map(l => l.name)
.filter(l => managedLabels.includes(l));
// Remove labels that no longer apply
for (const label of existingManagedLabels) {
if (!labelsToAdd.has(label)) {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: prNumber,
name: label
});
}
}
// Add labels
if (labelsToAdd.size > 0) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: [...labelsToAdd]
});
}
// Remove PR author from reviewers
if (!leadsToCheck.has(author)) {
reviewersToAdd.delete(author);
}
// Request reviewers
if (reviewersToAdd.size > 0) {
await github.rest.pulls.requestReviewers({
owner,
repo,
pull_number: prNumber,
reviewers: [...reviewersToAdd]
});
}
// -------------------
// CUSTOM APPROVAL CHECK WITH SINGLE COMMENT
// -------------------
const { data: reviews } = await github.rest.pulls.listReviews({
owner,
repo,
pull_number: prNumber
});
// Set of users who approved
const approvedUsers = new Set(
reviews
.filter(r => r.state === "APPROVED")
.map(r => r.user.login)
);
// Check if at least one lead approved (include author if they are a lead)
const leadApproved = [...leadsToCheck].some(u => approvedUsers.has(u));
// Count how many team members approved (exclude PR author)
let teamApprovals = 0;
for (const member of teamToCheck) {
if (approvedUsers.has(member) && member !== author) teamApprovals++;
}
// Determine if approval rules are satisfied
const approvalSatisfied = leadApproved || teamApprovals >= 2;
// Prepare the comment body with a timestamp and identifier
const now = new Date().toISOString();
const commentBody = approvalSatisfied
? `✅ PR approval conditions satisfied (1 lead OR 2 team members approved).\n[Last checked: ${now}] <!-- PR_APPROVAL_CHECK -->`
: `❌ PR approval conditions NOT satisfied. At least 1 team lead OR 2 team members must approve before merging.\n[Last checked: ${now}] <!-- PR_APPROVAL_CHECK -->`;
const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number: prNumber
});
const existingComment = comments.find(c => c.body.includes("<!-- PR_APPROVAL_CHECK -->"));
if (existingComment) {
// Update the existing comment
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existingComment.id,
body: commentBody
});
} else {
// Create a new comment
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: commentBody
});
}
// Fail workflow if approvals are not satisfied
if (!approvalSatisfied) {
process.exit(1);
}