Skip to content

Commit 49a0c18

Browse files
authored
Add workflow to auto-merge PRs (#107)
* Add workflow to auto-merge PRs * Switch to reusable action + skip forks * Use core.getInput rather than env vars * Add harder runner step
1 parent 56fd584 commit 49a0c18

2 files changed

Lines changed: 144 additions & 0 deletions

File tree

.github/workflows/auto-merge.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Auto-merge PRs
2+
3+
on:
4+
schedule:
5+
- cron: '*/15 * * * *'
6+
workflow_dispatch:
7+
8+
permissions:
9+
pull-requests: write
10+
contents: write
11+
12+
jobs:
13+
auto-merge:
14+
if: github.repository == 'nodejs/web-team'
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- name: Harden Runner
19+
uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1
20+
with:
21+
egress-policy: audit
22+
23+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
24+
- uses: ./actions/auto-merge-prs

actions/auto-merge-prs/action.yml

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
name: Auto-merge PRs
2+
description: Automatically merge pull requests that meet specified criteria
3+
4+
inputs:
5+
label-name:
6+
description: 'The label name that PRs must have to be eligible for auto-merge'
7+
required: false
8+
default: 'auto-merge'
9+
hours-open:
10+
description: 'Number of hours a PR must be open before it can be auto-merged'
11+
required: false
12+
default: '48'
13+
merge-method:
14+
description: 'Merge method to use (merge, squash, or rebase)'
15+
required: false
16+
default: 'squash'
17+
github-token:
18+
description: 'GitHub token for authentication'
19+
required: false
20+
default: ${{ github.token }}
21+
22+
runs:
23+
using: composite
24+
steps:
25+
- name: Check and merge eligible PRs
26+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
27+
with:
28+
github-token: ${{ inputs.github-token }}
29+
script: |
30+
const { owner, repo } = context.repo;
31+
const labelName = core.getInput('label-name');
32+
const hoursOpen = parseFloat(core.getInput('hours-open'));
33+
const mergeMethod = core.getInput('merge-method');
34+
35+
// Get all open PRs (with pagination)
36+
const pullRequests = await github.paginate(github.rest.pulls.list, {
37+
owner,
38+
repo,
39+
state: 'open',
40+
per_page: 100
41+
});
42+
core.info(`Found ${pullRequests.length} open PRs for ${owner}/${repo}`);
43+
44+
// Process each PR
45+
for (const pr of pullRequests) {
46+
core.startGroup(`PR #${pr.number} (${pr.html_url}): ${pr.title}`);
47+
48+
try {
49+
// Check if PR is from the same repository (not a fork)
50+
if (pr.head.repo.full_name !== `${owner}/${repo}`) {
51+
core.info(`❌ PR is from a fork (${pr.head.repo.full_name})`);
52+
continue;
53+
}
54+
core.info(`✅ PR is from the base repository`);
55+
56+
// Skip draft PRs
57+
if (pr.draft) {
58+
core.info(`❌ PR is a draft`);
59+
continue;
60+
}
61+
core.info(`✅ PR is ready for review`);
62+
63+
// Check if PR has the required label
64+
const hasRequiredLabel = pr.labels.some(label => label.name === labelName);
65+
if (!hasRequiredLabel) {
66+
core.info(`❌ PR is missing '${labelName}' label`);
67+
continue;
68+
}
69+
core.info(`✅ PR has '${labelName}' label`);
70+
71+
// Check if PR has been open for at least the required number of hours
72+
const createdAt = new Date(pr.created_at);
73+
const now = new Date();
74+
const hoursSinceCreation = (now - createdAt) / (1000 * 60 * 60);
75+
76+
if (hoursSinceCreation < hoursOpen) {
77+
core.info(`❌ PR opened ${hoursSinceCreation.toFixed(2)} hours ago (needs ${hoursOpen}+ hours)`);
78+
continue;
79+
}
80+
core.info(`✅ PR opened ${hoursSinceCreation.toFixed(2)} hours ago`);
81+
82+
// Check if the PR has a known valid merge commit SHA
83+
if (!pr.merge_commit_sha) {
84+
core.info(`❌ PR does not have a merge commit SHA (not mergeable)`);
85+
continue;
86+
}
87+
core.info(`✅ PR has a merge commit SHA (${pr.merge_commit_sha})`);
88+
89+
// Get full PR details to check mergeability
90+
const { data: prDetails } = await github.rest.pulls.get({
91+
owner,
92+
repo,
93+
pull_number: pr.number
94+
});
95+
96+
// Check for clean mergeable_state (indicates all checks and requirements are met)
97+
if (prDetails.mergeable_state !== 'clean') {
98+
core.info(`❌ PR mergeable_state is '${prDetails.mergeable_state}' (not clean)`);
99+
continue;
100+
}
101+
core.info(`✅ PR is mergeable (${prDetails.mergeable_state})`);
102+
103+
// All conditions met - merge the PR
104+
try {
105+
await github.rest.pulls.merge({
106+
owner,
107+
repo,
108+
pull_number: pr.number,
109+
merge_method: mergeMethod
110+
});
111+
core.notice(`🚀 Successfully merged PR #${pr.number} (${pr.html_url}): ${pr.title}`);
112+
} catch (error) {
113+
core.error(`❌ Failed to merge PR #${pr.number} (${pr.html_url}): ${error.message}`);
114+
}
115+
} finally {
116+
core.endGroup();
117+
}
118+
}
119+
120+
core.info('Auto-merge check complete');

0 commit comments

Comments
 (0)