Skip to content

Commit 7943514

Browse files
committed
Add week and status filtering for bulk approval and merge handlers; enhance PR field retrieval
1 parent d0d703c commit 7943514

4 files changed

Lines changed: 134 additions & 11 deletions

File tree

handlers/approve_prs.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
parsePrActionPayload,
55
fetchOpenPullRequests,
66
filterTargetPrs,
7+
filterByWeekAndStatus,
78
getSkipReason,
89
formatResult,
910
safeJson,
@@ -25,10 +26,15 @@ export async function approvePrs(request, env) {
2526
return payload.response;
2627
}
2728

28-
const { repoOwner, repoName, excludes } = payload.data;
29+
const { repoOwner, repoName, week, excludes } = payload.data;
2930
const appToken = await generateGitHubAppToken(env);
3031
const openPrs = await fetchOpenPullRequests(repoOwner, repoName, appToken);
31-
const targetPrs = filterTargetPrs(openPrs, excludes);
32+
33+
// Week와 Status 필터링
34+
const { filtered: weekFilteredPrs, weekMismatched, solvingExcluded } =
35+
await filterByWeekAndStatus(openPrs, week, repoOwner, repoName, appToken);
36+
37+
const targetPrs = filterTargetPrs(weekFilteredPrs, excludes);
3238

3339
const results = [];
3440
let approved = 0;
@@ -74,7 +80,11 @@ export async function approvePrs(request, env) {
7480
success: true,
7581
action: "approve",
7682
repo: `${repoOwner}/${repoName}`,
83+
week_filter: week,
7784
total_open_prs: openPrs.length,
85+
week_matched: weekFilteredPrs.length,
86+
week_mismatched: weekMismatched,
87+
solving_excluded: solvingExcluded,
7888
processed: targetPrs.length,
7989
approved,
8090
skipped,

handlers/merge_prs.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
parsePrActionPayload,
55
fetchOpenPullRequests,
66
filterTargetPrs,
7+
filterByWeekAndStatus,
78
getSkipReason,
89
formatResult,
910
safeJson,
@@ -26,7 +27,7 @@ export async function mergePrs(request, env) {
2627
return payload.response;
2728
}
2829

29-
const { repoOwner, repoName, excludes, rawPayload } = payload.data;
30+
const { repoOwner, repoName, week, excludes, rawPayload } = payload.data;
3031
const mergeMethod = (rawPayload.merge_method || "merge").toLowerCase();
3132

3233
if (!ALLOWED_MERGE_METHODS.has(mergeMethod)) {
@@ -40,7 +41,12 @@ export async function mergePrs(request, env) {
4041

4142
const appToken = await generateGitHubAppToken(env);
4243
const openPrs = await fetchOpenPullRequests(repoOwner, repoName, appToken);
43-
const targetPrs = filterTargetPrs(openPrs, excludes);
44+
45+
// Week와 Status 필터링
46+
const { filtered: weekFilteredPrs, weekMismatched, solvingExcluded } =
47+
await filterByWeekAndStatus(openPrs, week, repoOwner, repoName, appToken);
48+
49+
const targetPrs = filterTargetPrs(weekFilteredPrs, excludes);
4450

4551
const results = [];
4652
let merged = 0;
@@ -120,7 +126,11 @@ export async function mergePrs(request, env) {
120126
success: true,
121127
action: "merge",
122128
repo: `${repoOwner}/${repoName}`,
129+
week_filter: week,
123130
total_open_prs: openPrs.length,
131+
week_matched: weekFilteredPrs.length,
132+
week_mismatched: weekMismatched,
133+
solving_excluded: solvingExcluded,
124134
processed: targetPrs.length,
125135
merged,
126136
skipped,

utils/prActions.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import { errorResponse } from "./cors.js";
66
import { getGitHubHeaders } from "./github.js";
77
import { validateOrganization, hasMaintenanceLabel } from "./validation.js";
8+
import { getProjectFields } from "./prWeeks.js";
89

910
/**
1011
* 공통 payload 파서
@@ -14,6 +15,7 @@ export async function parsePrActionPayload(request) {
1415
const data = await request.json();
1516
const repoOwner = data.repo_owner || "DaleStudy";
1617
const repoName = data.repo_name;
18+
const week = data.week;
1719

1820
if (!repoName) {
1921
return {
@@ -25,6 +27,17 @@ export async function parsePrActionPayload(request) {
2527
};
2628
}
2729

30+
// week 필수 검증
31+
if (!week) {
32+
return {
33+
valid: false,
34+
response: errorResponse(
35+
"Missing required field: week (e.g., 'Week 8')",
36+
400
37+
),
38+
};
39+
}
40+
2841
if (!validateOrganization(repoOwner)) {
2942
return {
3043
valid: false,
@@ -39,6 +52,7 @@ export async function parsePrActionPayload(request) {
3952
data: {
4053
repoOwner,
4154
repoName,
55+
week,
4256
excludes,
4357
rawPayload: data,
4458
},
@@ -110,6 +124,8 @@ export function formatResult(pr, result) {
110124
return {
111125
pr: pr.number,
112126
title: pr.title,
127+
week: pr.week || null,
128+
status: pr.status || null,
113129
...result,
114130
};
115131
}
@@ -158,3 +174,61 @@ function parseNumberArray(value) {
158174
.map((n) => parseInt(n, 10))
159175
.filter((n) => Number.isInteger(n));
160176
}
177+
178+
/**
179+
* Week 값 매칭 (Week 8 == Week 8(current))
180+
*/
181+
export function matchesWeek(actualWeek, expectedWeek) {
182+
if (!actualWeek) return false;
183+
184+
// 정확히 일치
185+
if (actualWeek === expectedWeek) return true;
186+
187+
// "Week 8(current)" 형태도 매칭
188+
if (actualWeek.startsWith(expectedWeek + "(")) return true;
189+
if (expectedWeek.startsWith(actualWeek.replace(/\(current\)$/, "").trim())) return true;
190+
191+
return false;
192+
}
193+
194+
/**
195+
* Week와 Status로 PR 필터링
196+
* - Week 필터링: 지정된 Week만
197+
* - Status 필터링: "Solving" 상태 제외
198+
*
199+
* @returns {Promise<{filtered: Array, weekMismatched: number, solvingExcluded: number}>}
200+
*/
201+
export async function filterByWeekAndStatus(pullRequests, weekFilter, repoOwner, repoName, appToken) {
202+
const filtered = [];
203+
let weekMismatched = 0;
204+
let solvingExcluded = 0;
205+
206+
for (const pr of pullRequests) {
207+
// 프로젝트 필드 조회 (Week, Status)
208+
const fields = await getProjectFields(repoOwner, repoName, pr.number, appToken);
209+
210+
// PR 객체에 Week와 Status 메타데이터 추가
211+
pr.week = fields.week;
212+
pr.status = fields.status;
213+
214+
// Week 필터링
215+
if (!matchesWeek(fields.week, weekFilter)) {
216+
weekMismatched++;
217+
continue;
218+
}
219+
220+
// Status "Solving" 제외
221+
if (fields.status === "Solving") {
222+
solvingExcluded++;
223+
continue;
224+
}
225+
226+
filtered.push(pr);
227+
}
228+
229+
return {
230+
filtered,
231+
weekMismatched,
232+
solvingExcluded,
233+
};
234+
}

utils/prWeeks.js

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
import { generateGitHubAppToken, getGitHubHeaders } from "./github.js";
66

77
/**
8-
* PR의 Week 값 조회 (GraphQL)
8+
* PR의 프로젝트 필드 값 조회 (Week, Status 등)
9+
* @returns {Promise<{week: string|null, status: string|null}>}
910
*/
10-
export async function getWeekValue(repoOwner, repoName, prNumber, appToken) {
11-
const weekQuery = `
11+
export async function getProjectFields(repoOwner, repoName, prNumber, appToken) {
12+
const query = `
1213
query {
1314
repository(owner: "${repoOwner}", name: "${repoName}") {
1415
pullRequest(number: ${prNumber}) {
@@ -25,6 +26,14 @@ export async function getWeekValue(repoOwner, repoName, prNumber, appToken) {
2526
}
2627
}
2728
}
29+
... on ProjectV2ItemFieldSingleSelectValue {
30+
name
31+
field {
32+
... on ProjectV2FieldCommon {
33+
name
34+
}
35+
}
36+
}
2837
}
2938
}
3039
}
@@ -37,26 +46,45 @@ export async function getWeekValue(repoOwner, repoName, prNumber, appToken) {
3746
const response = await fetch("https://api.github.com/graphql", {
3847
method: "POST",
3948
headers: { ...getGitHubHeaders(appToken), "Content-Type": "application/json" },
40-
body: JSON.stringify({ query: weekQuery }),
49+
body: JSON.stringify({ query }),
4150
});
4251

4352
const data = await response.json();
4453
const projectItems =
4554
data.data?.repository?.pullRequest?.projectItems?.nodes || [];
4655

56+
const result = { week: null, status: null };
57+
4758
for (const item of projectItems) {
4859
const fieldValues = item.fieldValues?.nodes || [];
4960
for (const field of fieldValues) {
61+
// Week 필드 (Iteration 타입)
5062
if (
5163
field.__typename === "ProjectV2ItemFieldIterationValue" &&
5264
field.field?.name === "Week"
5365
) {
54-
return field.title;
66+
result.week = field.title;
67+
}
68+
// Status 필드 (SingleSelect 타입)
69+
if (
70+
field.__typename === "ProjectV2ItemFieldSingleSelectValue" &&
71+
field.field?.name === "Status"
72+
) {
73+
result.status = field.name;
5574
}
5675
}
5776
}
5877

59-
return null;
78+
return result;
79+
}
80+
81+
/**
82+
* PR의 Week 값 조회 (GraphQL)
83+
* @deprecated Use getProjectFields() for better performance
84+
*/
85+
export async function getWeekValue(repoOwner, repoName, prNumber, appToken) {
86+
const fields = await getProjectFields(repoOwner, repoName, prNumber, appToken);
87+
return fields.week;
6088
}
6189

6290
/**
@@ -172,7 +200,8 @@ export async function removeWarningComment(repoOwner, repoName, prNumber, env) {
172200
* @returns {Promise<string|null>} Week 값 또는 null
173201
*/
174202
export async function handleWeekComment(repoOwner, repoName, prNumber, env, appToken) {
175-
const weekValue = await getWeekValue(repoOwner, repoName, prNumber, appToken);
203+
const fields = await getProjectFields(repoOwner, repoName, prNumber, appToken);
204+
const weekValue = fields.week;
176205

177206
if (!weekValue) {
178207
await ensureWarningComment(repoOwner, repoName, prNumber, env);

0 commit comments

Comments
 (0)