Skip to content

PR merge on time v3.3 by crong #4

PR merge on time v3.3 by crong

PR merge on time v3.3 by crong #4

Workflow file for this run

name: "PR merge on time v3.3 by crong"
on:
schedule:
- cron: "10 15 * * 0"
workflow_dispatch:
permissions:
contents: write
pull-requests: write
issues: write
jobs:
merge:
name: "Auto Merge on time"
runs-on: "ubuntu-latest"
steps:
- name: "Merge pull request"
uses: "actions/github-script@v7"
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const query = `query($owner:String!, $name:String!) {
repository(owner: $owner, name: $name) {
pullRequests(last: 100, states: OPEN) {
edges {
node {
number
headRefName
baseRefName
author { login }
repository { name }
mergeable
labels(first: 10) { nodes { name } }
reviews(last: 1) { nodes { state } }
}
}
}
}
}`
const repo = { owner: context.repo.owner, repo: context.repo.repo };
// ---------- low-level API helpers ----------
async function comment(pr, body) {
try {
await github.rest.issues.createComment({ ...repo, issue_number: pr.number, body });
console.log(`PR #${pr.number} 코멘트 추가: "${body}"`);
} catch (e) {
console.log(`PR #${pr.number} 코멘트 실패:`, e);
}
}
async function closePR(pr) {
await github.rest.pulls.update({ ...repo, pull_number: pr.number, state: "closed" });
}
async function mergePR(pr) {
await github.rest.pulls.merge({ ...repo, pull_number: pr.number, merge_method: "merge" });
}
// ---------- predicates ----------
const isTargetingMain = (pr) => pr.baseRefName === "main";
const hasReviewLabel = (pr) => pr.labels.nodes.some(l => l.name.toLowerCase() === "review");
const isChangesRequested = (pr) => pr.reviews?.nodes?.[0]?.state === "CHANGES_REQUESTED";
const isConflicting = (pr) => pr.mergeable === "CONFLICTING";
// ---------- actions ----------
async function skipMain(pr) {
await comment(pr, "main 브랜치로 병합 시도");
}
async function skipReviewLabel(pr) {
// 코멘트 없이 로그만 (기존 동작 유지)
}
async function deferChangesRequested(pr) {
await comment(pr, "변경 요청 중인 브랜치의 머지를 연기합니다.");
}
async function closeOnConflict(pr) {
try {
await closePR(pr);
await comment(pr, "충돌로 인해 자동 병합이 불가능합니다.");
} catch (e) {
console.log(`PR #${pr.number} 충돌 close 처리 실패:`, e);
}
}
async function mergeIfReady(pr) {
try {
await mergePR(pr);
console.log(`PR #${pr.number} 병합 완료`);
} catch (e) {
console.log(`PR #${pr.number} 병합 실패:`, e);
}
}
// ---------- rules (배열 순서 = 우선순위) ----------
const rules = [
{ name: "main 브랜치 타겟 → 스킵", match: isTargetingMain, run: skipMain },
{ name: "review 라벨 → 스킵", match: hasReviewLabel, run: skipReviewLabel },
{ name: "변경 요청 상태 → 연기", match: isChangesRequested, run: deferChangesRequested },
{ name: "충돌 → close", match: isConflicting, run: closeOnConflict },
];
const defaultRule = { name: "조건 통과 → 머지", match: () => true, run: mergeIfReady };
// ---------- main ----------
const { repository: { pullRequests: { edges: list } } } = await github.graphql(query, {
owner: repo.owner,
name: repo.repo,
});
for (const { node: pr } of list) {
const rule = rules.find(r => r.match(pr)) ?? defaultRule;
console.log(`\n---- PR #${pr.number} → ${rule.name}`);
await rule.run(pr);
}