mergegate/.github/workflows/auto-merge.yml
林 駿甫 (Shunsuke Hayashi) 2d500c3654 feat(ci): add Copilot full automation pipeline
- copilot-assign.yml: GraphQL agentAssignment
- ai-review.yml: Claude Opus auto-review
- auto-merge.yml: CI + APPROVE squash merge
- decompose.yml: Issue decomposition + sub-issues

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 21:48:22 +09:00

178 lines
6.9 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}