Skip to content

Commit 7c1c84b

Browse files
authored
Merge branch 'main' into websocket-wait
2 parents 728aa7c + f4440bd commit 7c1c84b

File tree

20 files changed

+1439
-500
lines changed

20 files changed

+1439
-500
lines changed

.DS_Store

10 KB
Binary file not shown.

.github/CODEOWNERS

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Frontend lead
2+
/frontend/ @MaddieWright
3+
4+
# Backend lead
5+
/backend/ @HeisSteve
6+
7+
# Infra
8+
/.github/ @HeisSteve @MaddieWright

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Pull Request Template
2+
3+
## What?
4+
5+
**Add a concise description** of the change made.
6+
7+
**Example:** "Implemented backend API for dataset retrieval."
8+
9+
---
10+
11+
## How?
12+
13+
**Mention a brief technical explanation** of the change.
14+
15+
**Example:** "Refactored the API service to handle dataset retrieval with
16+
caching to improve performance."
17+
18+
---
19+
20+
## Testing
21+
22+
List ways you have tested the code.
23+
24+
**Example:** "Used console.log() to ensure function ran, UI changes color when
25+
hovering over"
26+
27+
Attach images or videos if possible.
28+
---
29+
30+
### Notes:
31+
32+
- This template is intended to **guide** your PR submission, but feel free to
33+
modify sections as needed.
34+
- For the PR title, just reference the appropriate Github Issue/Notion Task
35+
- eg. `steve/[Bug Fix] Changed color of background`

.github/workflows/pr-workflow.yml

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
name: PR workflow
2+
3+
permissions:
4+
contents: read # for git operations
5+
pull-requests: write # to request reviewers
6+
issues: write # to create/update PR comments
7+
8+
on:
9+
pull_request:
10+
types: [opened, synchronize, ready_for_review] # include ready_for_review for draft PRs
11+
12+
jobs:
13+
pr-automation:
14+
name: "PR Check"
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
# Step 1: Get changed files
19+
- name: Get changed files
20+
id: files
21+
uses: tj-actions/changed-files@v44
22+
23+
# Step 2: Assign, label, request reviewers, and enforce approvals
24+
- name: Assign, label, request reviewers, enforce approvals
25+
uses: actions/github-script@v7
26+
with:
27+
script: |
28+
const owner = context.repo.owner;
29+
const repo = context.repo.repo;
30+
const prNumber = context.payload.pull_request.number;
31+
const author = context.payload.pull_request.user.login;
32+
33+
if (context.payload.pull_request.draft) {
34+
console.log("PR is a draft — skipping workflow steps.");
35+
return;
36+
}
37+
38+
const changedFiles = "${{ steps.files.outputs.all_changed_files }}".split(" ").filter(f => f);
39+
40+
// -------------------
41+
// CONFIGURATION
42+
// -------------------
43+
const rules = {
44+
frontend: { path: "frontend/", label: "frontend", lead: ["MaddieWright"], team: [] },
45+
backend: { path: "backend/", label: "backend", lead: ["HeisSteve"], team: [] },
46+
infra: { path: ".github/", label: "infra", lead: ["HeisSteve", "MaddieWright" ], team: [] }
47+
};
48+
49+
const labelsToAdd = new Set();
50+
const reviewersToAdd = new Set();
51+
52+
const leadsToCheck = new Set();
53+
const teamToCheck = new Set();
54+
55+
56+
// Determine applicable rules
57+
for (const file of changedFiles) {
58+
for (const rule of Object.values(rules)) {
59+
if (!file.startsWith(rule.path)) continue;
60+
61+
labelsToAdd.add(rule.label);
62+
63+
// Normalize leads to an array
64+
const leads = Array.isArray(rule.lead) ? rule.lead : [rule.lead];
65+
66+
// Add leads for approval & reviewers
67+
leads.forEach(u => {
68+
leadsToCheck.add(u);
69+
reviewersToAdd.add(u);
70+
});
71+
72+
// Add team members for approval & reviewers
73+
rule.team.forEach(u => {
74+
teamToCheck.add(u);
75+
reviewersToAdd.add(u);
76+
});
77+
}
78+
}
79+
80+
// Assign PR author
81+
await github.rest.issues.addAssignees({
82+
owner,
83+
repo,
84+
issue_number: prNumber,
85+
assignees: [author]
86+
});
87+
88+
// Get existing labels
89+
const existingLabels = await github.rest.issues.listLabelsOnIssue({
90+
owner,
91+
repo,
92+
issue_number: prNumber
93+
});
94+
95+
const managedLabels = Object.values(rules).map(r => r.label);
96+
const existingManagedLabels = existingLabels.data
97+
.map(l => l.name)
98+
.filter(l => managedLabels.includes(l));
99+
100+
// Remove labels that no longer apply
101+
for (const label of existingManagedLabels) {
102+
if (!labelsToAdd.has(label)) {
103+
await github.rest.issues.removeLabel({
104+
owner,
105+
repo,
106+
issue_number: prNumber,
107+
name: label
108+
});
109+
}
110+
}
111+
112+
// Add labels
113+
if (labelsToAdd.size > 0) {
114+
await github.rest.issues.addLabels({
115+
owner,
116+
repo,
117+
issue_number: prNumber,
118+
labels: [...labelsToAdd]
119+
});
120+
}
121+
122+
// Remove PR author from reviewers
123+
if (!leadsToCheck.has(author)) {
124+
reviewersToAdd.delete(author);
125+
}
126+
127+
// Request reviewers
128+
if (reviewersToAdd.size > 0) {
129+
await github.rest.pulls.requestReviewers({
130+
owner,
131+
repo,
132+
pull_number: prNumber,
133+
reviewers: [...reviewersToAdd]
134+
});
135+
}
136+
137+
138+
// -------------------
139+
// CUSTOM APPROVAL CHECK WITH SINGLE COMMENT
140+
// -------------------
141+
const { data: reviews } = await github.rest.pulls.listReviews({
142+
owner,
143+
repo,
144+
pull_number: prNumber
145+
});
146+
147+
// Set of users who approved
148+
const approvedUsers = new Set(
149+
reviews
150+
.filter(r => r.state === "APPROVED")
151+
.map(r => r.user.login)
152+
);
153+
154+
// Check if at least one lead approved (include author if they are a lead)
155+
const leadApproved = [...leadsToCheck].some(u => approvedUsers.has(u));
156+
157+
// Count how many team members approved (exclude PR author)
158+
let teamApprovals = 0;
159+
for (const member of teamToCheck) {
160+
if (approvedUsers.has(member) && member !== author) teamApprovals++;
161+
}
162+
163+
// Determine if approval rules are satisfied
164+
const approvalSatisfied = leadApproved || teamApprovals >= 2;
165+
166+
167+
// Prepare the comment body with a timestamp and identifier
168+
const now = new Date().toISOString();
169+
const commentBody = approvalSatisfied
170+
? `✅ PR approval conditions satisfied (1 lead OR 2 team members approved).\n[Last checked: ${now}] <!-- PR_APPROVAL_CHECK -->`
171+
: `❌ 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 -->`;
172+
173+
const { data: comments } = await github.rest.issues.listComments({
174+
owner,
175+
repo,
176+
issue_number: prNumber
177+
});
178+
179+
const existingComment = comments.find(c => c.body.includes("<!-- PR_APPROVAL_CHECK -->"));
180+
181+
if (existingComment) {
182+
// Update the existing comment
183+
await github.rest.issues.updateComment({
184+
owner,
185+
repo,
186+
comment_id: existingComment.id,
187+
body: commentBody
188+
});
189+
} else {
190+
// Create a new comment
191+
await github.rest.issues.createComment({
192+
owner,
193+
repo,
194+
issue_number: prNumber,
195+
body: commentBody
196+
});
197+
}
198+
199+
// Fail workflow if approvals are not satisfied
200+
if (!approvalSatisfied) {
201+
process.exit(1);
202+
}

backend/.DS_Store

6 KB
Binary file not shown.

0 commit comments

Comments
 (0)