Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ GitHub Organization webhook 수신용 엔드포인트

#### `POST /merge-prs`

열려있는 PR을 일괄 병합합니다. 기본 병합 방식은 `merge`이며 `merge_method` 값으로 `merge | squash | rebase` 중 선택할 수 있습니다. `excludes`로 특정 PR을 제외할 수 있습니다. 승인 리뷰가 없거나 `maintenance` 라벨이 붙은 PR, Draft PR, GitHub `mergeable_state !== "clean"` PR은 스킵되며 `unknown`/`behind` 상태는 최대 1초 후 한 번 더 확인합니다.
열려있는 PR을 일괄 병합합니다. 기본 병합 방식은 `squash`이며 `merge_method` 값으로 `merge | squash | rebase` 중 선택할 수 있습니다. `excludes`로 특정 PR을 제외할 수 있습니다. 승인 리뷰가 없거나 `maintenance` 라벨이 붙은 PR, Draft PR, GitHub `mergeable_state !== "clean"` PR은 스킵되며 `unknown`/`behind` 상태는 최대 1초 후 한 번 더 확인합니다.

**Request:**

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ https://github.dalestudy.com

### `POST /merge-prs`

열려있는 PR을 일괄 병합합니다. 기본 병합 방식은 `merge`이며, `merge_method`로 `merge | squash | rebase` 중 선택할 수 있습니다. `excludes` 배열로 특정 PR을 제외할 수 있습니다. 최소 1개의 승인 리뷰가 없거나 Draft/`maintenance` 라벨이 붙은 PR은 스킵되며, GitHub에서 `mergeable_state === "clean"`인 PR만 병합됩니다(`behind`, `dirty`, `unknown` 등은 스킵). `unknown`/`behind` 상태는 최대 1초 후 한 차례 재확인합니다.
열려있는 PR을 일괄 병합합니다. 기본 병합 방식은 `squash`이며, `merge_method`로 `merge | squash | rebase` 중 선택할 수 있습니다. `excludes` 배열로 특정 PR을 제외할 수 있습니다. 최소 1개의 승인 리뷰가 없거나 Draft/`maintenance` 라벨이 붙은 PR은 스킵되며, GitHub에서 `mergeable_state === "clean"`인 PR만 병합됩니다(`behind`, `dirty`, `unknown` 등은 스킵). `unknown`/`behind` 상태는 최대 1초 후 한 차례 재확인합니다.

**Request:**

Expand Down
85 changes: 14 additions & 71 deletions handlers/merge_prs.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export async function mergePrs(request, env) {
}

const { repoOwner, repoName, week, excludes, rawPayload } = payload.data;
const mergeMethod = (rawPayload.merge_method || "merge").toLowerCase();
const mergeMethod = (rawPayload.merge_method || "squash").toLowerCase();

if (!ALLOWED_MERGE_METHODS.has(mergeMethod)) {
return errorResponse(
Expand Down Expand Up @@ -150,90 +150,33 @@ export async function mergePrs(request, env) {
}

async function mergePullRequest(owner, repo, prNumber, mergeMethod, token, sha) {
// 1. PR의 GraphQL node ID 조회
const nodeId = await getPullRequestNodeId(owner, repo, prNumber, token);
if (!nodeId) {
return {
merged: false,
error: "Failed to get PR node ID",
};
}

// 2. Merge method 매핑 (REST → GraphQL)
const graphqlMergeMethod = {
merge: "MERGE",
squash: "SQUASH",
rebase: "REBASE",
}[mergeMethod] || "MERGE";

// 3. Auto-merge 활성화 (Merge Queue 사용)
const mutation = `
mutation {
enablePullRequestAutoMerge(input: {
pullRequestId: "${nodeId}"
mergeMethod: ${graphqlMergeMethod}
}) {
pullRequest {
id
number
autoMergeRequest {
enabledAt
mergeMethod
}
}
}
const response = await fetch(
`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/merge`,
{
method: "PUT",
headers: {
...getGitHubHeaders(token),
"Content-Type": "application/json",
},
body: JSON.stringify({ merge_method: mergeMethod, sha }),
}
`;

const response = await fetch("https://api.github.com/graphql", {
method: "POST",
headers: {
...getGitHubHeaders(token),
"Content-Type": "application/json",
},
body: JSON.stringify({ query: mutation }),
});
);

const result = await safeJson(response);

if (response.ok && result.data?.enablePullRequestAutoMerge?.pullRequest) {
if (response.ok && result.merged) {
return {
merged: true,
autoMergeEnabled: true,
sha: sha,
sha: result.sha,
};
}

return {
merged: false,
error: result.errors?.[0]?.message || "Auto-merge failed",
error: result.message || "Merge failed",
};
}

async function getPullRequestNodeId(owner, repo, prNumber, token) {
const query = `
query {
repository(owner: "${owner}", name: "${repo}") {
pullRequest(number: ${prNumber}) {
id
}
}
}
`;

const response = await fetch("https://api.github.com/graphql", {
method: "POST",
headers: {
...getGitHubHeaders(token),
"Content-Type": "application/json",
},
body: JSON.stringify({ query }),
});

const result = await safeJson(response);
return result.data?.repository?.pullRequest?.id || null;
}

async function getMergeableState(owner, repo, prNumber, token) {
const prDetails = await getPullRequestDetails(owner, repo, prNumber, token);

Expand Down