name: Auto-merge Copilot PRs # Copilot Coding Agent が作成した PR が # 1) CI 通過(Type Check + Build Check)かつ # 2) AI レビュー (ai-review.yml) で APPROVE された # 場合に自動マージする。 on: pull_request_review: types: [submitted] pull_request: types: [opened, synchronize, ready_for_review] check_run: types: [completed] jobs: auto-merge: name: Auto-merge if CI + AI Review passed runs-on: ubuntu-latest if: | github.actor == 'Copilot' || github.event.pull_request.user.login == 'Copilot' || github.event.review.user.login == 'github-actions[bot]' || github.event_name == 'check_run' permissions: pull-requests: write contents: write steps: - name: Check CI status and AI review approval uses: actions/github-script@v7 with: script: | let prNumber; // イベント種別に応じて PR 番号を取得 if (context.eventName === 'check_run') { const runName = context.payload.check_run.name; if (runName === 'Auto-merge if CI + AI Review passed') { console.log('Skipping self-triggered check_run'); return; } const prs = context.payload.check_run.pull_requests; if (!prs || prs.length === 0) { console.log('check_run: no associated PRs, skipping'); return; } prNumber = prs[0].number; console.log(`check_run "${runName}" completed: checking PR #${prNumber}`); } else { prNumber = context.payload.pull_request?.number || context.payload.review?.pull_request_url?.match(/\/pulls\/(\d+)$/)?.[1]; } if (!prNumber) { console.log('No PR number found, skipping'); return; } const pr = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, pull_number: Number(prNumber), }); // すでに closed/merged の PR はスキップ if (pr.data.state !== 'open') { console.log(`PR #${prNumber} is ${pr.data.state}, skipping`); return; } // Draft PR はスキップ if (pr.data.draft) { console.log(`PR #${prNumber} is still Draft, skipping`); return; } // Copilot 作成でなければスキップ if (pr.data.user.login !== 'Copilot') { console.log(`PR #${prNumber} is not from Copilot (${pr.data.user.login}), skipping`); return; } // ── CI チェックの状態を確認 ────────────────────────────── const checks = await github.rest.checks.listForRef({ owner: context.repo.owner, repo: context.repo.repo, ref: pr.data.head.sha, per_page: 100, }); // 同名の check_run が複数ある場合(re-run)は最新のものだけを使う const latestByName = {}; for (const c of checks.data.check_runs) { if (c.name === 'Type Check' || c.name === 'Build Check') { const prev = latestByName[c.name]; if (!prev || new Date(c.started_at) > new Date(prev.started_at)) { latestByName[c.name] = c; } } } const ciChecks = Object.values(latestByName); const ciPassed = ciChecks.length >= 2 && ciChecks.every(c => c.status === 'completed' && c.conclusion === 'success'); const ciStatus = ciChecks.map(c => `${c.name}:${c.conclusion ?? c.status}`).join(', '); console.log(`PR #${prNumber} CI (latest runs): ${ciStatus || 'no checks found'}`); if (!ciPassed) { console.log(`PR #${prNumber}: CI not fully passed yet, skipping`); return; } // ── AI レビューの APPROVE を確認 ───────────────────────── const reviews = await github.rest.pulls.listReviews({ owner: context.repo.owner, repo: context.repo.repo, pull_number: Number(prNumber), per_page: 100, }); const latestByReviewer = {}; for (const r of reviews.data) { if (r.state !== 'COMMENTED') { latestByReviewer[r.user.login] = r.state; } } const hasApproval = Object.values(latestByReviewer).includes('APPROVED'); const hasChangesRequested = Object.values(latestByReviewer).includes('CHANGES_REQUESTED'); console.log(`PR #${prNumber} reviews: ${JSON.stringify(latestByReviewer)}`); if (hasChangesRequested) { console.log(`PR #${prNumber}: AI review requested changes, skipping merge`); return; } if (!hasApproval) { console.log(`PR #${prNumber}: No APPROVE review yet, waiting for AI review`); return; } console.log(`PR #${prNumber}: CI passed + AI review approved. Proceeding to merge.`); // ── マージ実行(squash) ────────────────────────────────── try { await github.rest.pulls.merge({ owner: context.repo.owner, repo: context.repo.repo, pull_number: Number(prNumber), merge_method: 'squash', }); console.log(`✅ Merged PR #${prNumber}`); } catch (err) { if (err.status === 405) { // Branch protection: GitHub native auto-merge を有効化 try { await github.graphql(` mutation EnableAutoMerge($prId: ID!) { enablePullRequestAutoMerge(input: { pullRequestId: $prId mergeMethod: SQUASH }) { pullRequest { number state } } } `, { prId: pr.data.node_id }); console.log(`Enabled GitHub auto-merge for PR #${prNumber}`); } catch (graphqlErr) { console.log(`Auto-merge enable failed: ${graphqlErr.message}`); } } else if (err.status === 404 || err.status === 422) { console.log(`PR #${prNumber} not mergeable (${err.status}): ${err.message}`); } else { throw err; } }