name: Auto-Merge PRs
on:
pull_request_review:
types: [submitted]
check_suite:
types: [completed]
status: {}
pull_request:
types: [opened, reopened, synchronize]
permissions:
contents: write
pull-requests: write
checks: read
jobs:
auto-merge:
timeout-minutes: 5
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: write
checks: read
steps:
- name: Check if PR is ready for merge
id: check
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
// Handle different event types that might not have pull_request
let pr = context.payload.pull_request;
// For check_suite events, we need to find PRs associated with the commit
if (!pr && context.payload.check_suite) {
const checkSuite = context.payload.check_suite;
const { data: pulls } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
head: `${checkSuite.head_branch}`,
state: 'open'
});
if (pulls.length > 0) {
pr = pulls[0];
}
}
// Skip if PR is already merged or is a draft
if (!pr || pr.merged || pr.draft) {
console.log('PR is not eligible for auto-merge');
return false;
}
// Get PR details
const { data: pullRequest } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number
});
// Check if PR has auto-merge label
const hasAutoMergeLabel = pullRequest.labels.some(
label => label.name === 'auto-merge'
);
// Check target branch (only auto-merge to develop or main)
const validTargetBranches = ['develop', 'main'];
const isValidTarget = validTargetBranches.includes(pullRequest.base.ref);
// Get reviews
const { data: reviews } = await github.rest.pulls.listReviews({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number
});
// Check for approved reviews
const approvedReviews = reviews.filter(
review => review.state === 'APPROVED'
);
const hasApproval = approvedReviews.length > 0;
// Get check runs
const { data: checkRuns } = await github.rest.checks.listForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: pullRequest.head.sha
});
// Check if all required checks passed
const requiredChecks = ['test', 'lint', 'build'];
const allChecksPassed = requiredChecks.every(checkName => {
const check = checkRuns.check_runs.find(
run => run.name.toLowerCase().includes(checkName)
);
return check && check.conclusion === 'success';
});
const canMerge = hasAutoMergeLabel && isValidTarget && hasApproval && allChecksPassed;
console.log(`Auto-merge eligibility: ${canMerge}`);
console.log(`- Has auto-merge label: ${hasAutoMergeLabel}`);
console.log(`- Valid target branch: ${isValidTarget}`);
console.log(`- Has approval: ${hasApproval}`);
console.log(`- All checks passed: ${allChecksPassed}`);
return canMerge;
- name: Enable auto-merge
if: steps.check.outputs.result == 'true'
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const pr = context.payload.pull_request;
try {
// Enable auto-merge using the correct API
await github.rest.pulls.enableAutoMerge({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
merge_method: "squash"
});
console.log(`✅ Auto-merge enabled for PR #${pr.number}`);
// Remove auto-merge label
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
name: 'auto-merge'
});
} catch (labelError) {
console.log('Label auto-merge not found or already removed');
}
} catch (error) {
console.error(`❌ Failed to enable auto-merge: ${error.message}`);
// Fallback: Use gh CLI for auto-merge
const { exec } = require('child_process');
exec(`gh pr merge ${pr.number} --auto --squash`, (err, stdout, stderr) => {
if (err) {
console.error(`CLI fallback failed: ${err}`);
throw err;
}
console.log(`✅ Auto-merge enabled via CLI for PR #${pr.number}`);
});
}
dependabot-auto-merge:
timeout-minutes: 10
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'
permissions:
pull-requests: write
contents: write
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v2
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Auto-approve Dependabot PRs
if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor'
run: gh pr review --approve "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Wait for CI checks to complete
if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor'
run: |
echo "Waiting for CI checks to complete..."
# Wait up to 5 minutes for checks to complete
for i in {1..30}; do
STATUS=$(gh pr checks "$PR_URL" --json state --jq '.[] | select(.state != "SUCCESS" and .state != "SKIPPED" and .state != "PENDING") | .state' 2>/dev/null || echo "PENDING")
PENDING=$(gh pr checks "$PR_URL" --json state --jq '[.[] | select(.state == "PENDING")] | length' 2>/dev/null || echo "1")
if [ "$PENDING" = "0" ]; then
echo "All checks completed"
break
fi
echo "Checks still pending ($PENDING remaining), waiting 10s..."
sleep 10
done
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Merge Dependabot PRs
if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor'
run: |
# Check if all required checks passed
FAILED=$(gh pr checks "$PR_URL" --json state --jq '[.[] | select(.state == "FAILURE")] | length' 2>/dev/null || echo "0")
if [ "$FAILED" != "0" ]; then
echo "Some checks failed, skipping merge"
exit 0
fi
# Try to merge directly (squash)
gh pr merge --squash --delete-branch "$PR_URL" || {
echo "Direct merge failed, PR may need manual review"
exit 0
}
echo "Successfully merged PR"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}