PR merge on time v3.3 by crong #10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | |
| } |