Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions .github/workflows/auto-merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
name: Auto-merge PRs

on:
schedule:
- cron: '*/15 * * * *'
workflow_dispatch:
workflow_call:
Comment thread
MattIPv4 marked this conversation as resolved.
Outdated

permissions:
pull-requests: write
contents: write

jobs:
auto-merge:
Comment thread
MattIPv4 marked this conversation as resolved.
runs-on: ubuntu-latest

steps:
Comment thread
MattIPv4 marked this conversation as resolved.
- name: Check and merge eligible PRs
uses: actions/github-script@v7
Comment thread
MattIPv4 marked this conversation as resolved.
Outdated
Comment thread
MattIPv4 marked this conversation as resolved.
Outdated
with:
script: |
const { owner, repo } = context.repo;

// Get the default branch
const { data: repository } = await github.rest.repos.get({ owner, repo });
const defaultBranch = repository.default_branch;
Comment thread
MattIPv4 marked this conversation as resolved.
Outdated

core.info(`Checking PRs against ${owner}/${repo}:${defaultBranch}`);

// Get all open PRs against the default branch
const { data: pullRequests } = await github.rest.pulls.list({
owner,
repo,
state: 'open',
base: defaultBranch,
Comment thread
MattIPv4 marked this conversation as resolved.
Outdated
per_page: 100
});
Comment thread
MattIPv4 marked this conversation as resolved.
Outdated

core.info(`Found ${pullRequests.length} open PRs`);

// Process each PR
for (const pr of pullRequests) {
core.startGroup(`PR #${pr.number} (${pr.html_url}): ${pr.title}`);

try {
// Check if PR has 'auto-merge' label
const hasAutoMergeLabel = pr.labels.some(label => label.name === 'auto-merge');
if (!hasAutoMergeLabel) {
core.info(`❌ Missing 'auto-merge' label`);
continue;
}
core.info(`✅ Has 'auto-merge' label`);
Comment thread
MattIPv4 marked this conversation as resolved.
Outdated

// Check if PR has been open for at least 2 days
const createdAt = new Date(pr.created_at);
const now = new Date();
const daysSinceCreation = (now - createdAt) / (1000 * 60 * 60 * 24);
Comment thread
MattIPv4 marked this conversation as resolved.
Outdated

if (daysSinceCreation < 2) {
core.info(`❌ PR opened ${daysSinceCreation.toFixed(2)} days ago (needs 2+ days)`);
continue;
}
core.info(`✅ PR opened ${daysSinceCreation.toFixed(2)} days ago`);

// Get the most recent commit
const { data: commits } = await github.rest.pulls.listCommits({
owner,
repo,
pull_number: pr.number,
per_page: 100
Comment thread
MattIPv4 marked this conversation as resolved.
Outdated
});

if (commits.length === 0) {
core.info(`❌ No commits found`);
continue;
}

const latestCommit = commits[commits.length - 1];
Comment thread
MattIPv4 marked this conversation as resolved.
Outdated
const latestCommitDate = new Date(latestCommit.commit.committer.date);
Comment thread
MattIPv4 marked this conversation as resolved.
Outdated
core.info(`Latest commit: ${latestCommit.sha.substring(0, 7)} at ${latestCommitDate.toISOString()}`);

// Get reviews for this PR
const { data: reviews } = await github.rest.pulls.listReviews({
owner,
repo,
pull_number: pr.number,
per_page: 100
});

const latestApproval = reviews
.filter(review => review.state === 'APPROVED')
.sort((a, b) => new Date(b.submitted_at) - new Date(a.submitted_at))[0];

if (!latestApproval) {
core.info(`❌ No approvals found`);
continue;
}

const latestApprovalDate = new Date(latestApproval.submitted_at);
core.info(`Latest approval: ${latestApproval.user.login} at ${latestApprovalDate.toISOString()}`);

if (latestApprovalDate < latestCommitDate) {
core.info(`❌ Latest approval is older than the latest commit`);
continue;
}
Comment thread
MattIPv4 marked this conversation as resolved.
Outdated
core.info(`✅ Has valid approval after latest commit`);

// Check if PR is mergeable
// We need to fetch the PR again to get the mergeable state
const { data: prDetails } = await github.rest.pulls.get({
Comment thread
MattIPv4 marked this conversation as resolved.
Outdated
owner,
repo,
pull_number: pr.number
});

if (prDetails.mergeable === false) {
core.info(`❌ PR has merge conflicts`);
continue;
}

if (prDetails.mergeable_state === 'blocked') {
core.info(`❌ PR is blocked (required checks may not have passed)`);
continue;
}

core.info(`✅ PR is mergeable (state: ${prDetails.mergeable_state})`);

Comment thread
MattIPv4 marked this conversation as resolved.
Outdated
// All conditions met - merge the PR
try {
await github.rest.pulls.merge({
owner,
repo,
pull_number: pr.number,
merge_method: 'squash'
});
core.notice(`🚀 Successfully merged PR #${pr.number} (${pr.html_url})`);
} catch (error) {
core.error(`❌ Failed to merge PR #${pr.number} (${pr.html_url}): ${error.message}`);
}
} finally {
core.endGroup();
}
}

core.info('Auto-merge check complete');
Loading