Skip to content

Commit e2b9147

Browse files
committed
Switch to reusable action + skip forks
1 parent d00527f commit e2b9147

File tree

2 files changed

+133
-129
lines changed

2 files changed

+133
-129
lines changed

.github/workflows/auto-merge.yml

Lines changed: 3 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -4,142 +4,16 @@ on:
44
schedule:
55
- cron: '*/15 * * * *'
66
workflow_dispatch:
7-
workflow_call:
87

98
permissions:
109
pull-requests: write
1110
contents: write
1211

1312
jobs:
1413
auto-merge:
14+
if: github.repository == 'nodejs/web-team'
1515
runs-on: ubuntu-latest
1616

1717
steps:
18-
- name: Check and merge eligible PRs
19-
uses: actions/github-script@v7
20-
with:
21-
script: |
22-
const { owner, repo } = context.repo;
23-
24-
// Get the default branch
25-
const { data: repository } = await github.rest.repos.get({ owner, repo });
26-
const defaultBranch = repository.default_branch;
27-
28-
core.info(`Checking PRs against ${owner}/${repo}:${defaultBranch}`);
29-
30-
// Get all open PRs against the default branch
31-
const { data: pullRequests } = await github.rest.pulls.list({
32-
owner,
33-
repo,
34-
state: 'open',
35-
base: defaultBranch,
36-
per_page: 100
37-
});
38-
39-
core.info(`Found ${pullRequests.length} open PRs`);
40-
41-
// Process each PR
42-
for (const pr of pullRequests) {
43-
core.startGroup(`PR #${pr.number} (${pr.html_url}): ${pr.title}`);
44-
45-
try {
46-
// Check if PR has 'auto-merge' label
47-
const hasAutoMergeLabel = pr.labels.some(label => label.name === 'auto-merge');
48-
if (!hasAutoMergeLabel) {
49-
core.info(`❌ Missing 'auto-merge' label`);
50-
continue;
51-
}
52-
core.info(`✅ Has 'auto-merge' label`);
53-
54-
// Check if PR has been open for at least 2 days
55-
const createdAt = new Date(pr.created_at);
56-
const now = new Date();
57-
const daysSinceCreation = (now - createdAt) / (1000 * 60 * 60 * 24);
58-
59-
if (daysSinceCreation < 2) {
60-
core.info(`❌ PR opened ${daysSinceCreation.toFixed(2)} days ago (needs 2+ days)`);
61-
continue;
62-
}
63-
core.info(`✅ PR opened ${daysSinceCreation.toFixed(2)} days ago`);
64-
65-
// Get the most recent commit
66-
const { data: commits } = await github.rest.pulls.listCommits({
67-
owner,
68-
repo,
69-
pull_number: pr.number,
70-
per_page: 100
71-
});
72-
73-
if (commits.length === 0) {
74-
core.info(`❌ No commits found`);
75-
continue;
76-
}
77-
78-
const latestCommit = commits[commits.length - 1];
79-
const latestCommitDate = new Date(latestCommit.commit.committer.date);
80-
core.info(`Latest commit: ${latestCommit.sha.substring(0, 7)} at ${latestCommitDate.toISOString()}`);
81-
82-
// Get reviews for this PR
83-
const { data: reviews } = await github.rest.pulls.listReviews({
84-
owner,
85-
repo,
86-
pull_number: pr.number,
87-
per_page: 100
88-
});
89-
90-
const latestApproval = reviews
91-
.filter(review => review.state === 'APPROVED')
92-
.sort((a, b) => new Date(b.submitted_at) - new Date(a.submitted_at))[0];
93-
94-
if (!latestApproval) {
95-
core.info(`❌ No approvals found`);
96-
continue;
97-
}
98-
99-
const latestApprovalDate = new Date(latestApproval.submitted_at);
100-
core.info(`Latest approval: ${latestApproval.user.login} at ${latestApprovalDate.toISOString()}`);
101-
102-
if (latestApprovalDate < latestCommitDate) {
103-
core.info(`❌ Latest approval is older than the latest commit`);
104-
continue;
105-
}
106-
core.info(`✅ Has valid approval after latest commit`);
107-
108-
// Check if PR is mergeable
109-
// We need to fetch the PR again to get the mergeable state
110-
const { data: prDetails } = await github.rest.pulls.get({
111-
owner,
112-
repo,
113-
pull_number: pr.number
114-
});
115-
116-
if (prDetails.mergeable === false) {
117-
core.info(`❌ PR has merge conflicts`);
118-
continue;
119-
}
120-
121-
if (prDetails.mergeable_state === 'blocked') {
122-
core.info(`❌ PR is blocked (required checks may not have passed)`);
123-
continue;
124-
}
125-
126-
core.info(`✅ PR is mergeable (state: ${prDetails.mergeable_state})`);
127-
128-
// All conditions met - merge the PR
129-
try {
130-
await github.rest.pulls.merge({
131-
owner,
132-
repo,
133-
pull_number: pr.number,
134-
merge_method: 'squash'
135-
});
136-
core.notice(`🚀 Successfully merged PR #${pr.number} (${pr.html_url})`);
137-
} catch (error) {
138-
core.error(`❌ Failed to merge PR #${pr.number} (${pr.html_url}): ${error.message}`);
139-
}
140-
} finally {
141-
core.endGroup();
142-
}
143-
}
144-
145-
core.info('Auto-merge check complete');
18+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
19+
- uses: ./actions/auto-merge-prs

actions/auto-merge-prs/action.yml

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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+
env:
28+
LABEL_NAME: ${{ inputs.label-name }}
29+
HOURS_OPEN: ${{ inputs.hours-open }}
30+
MERGE_METHOD: ${{ inputs.merge-method }}
31+
with:
32+
github-token: ${{ inputs.github-token }}
33+
script: |
34+
const { owner, repo } = context.repo;
35+
const labelName = process.env.LABEL_NAME;
36+
const hoursOpen = parseFloat(process.env.HOURS_OPEN);
37+
const mergeMethod = process.env.MERGE_METHOD;
38+
39+
core.info(`Checking PRs for ${owner}/${repo}`);
40+
core.info(`Required label: "${labelName}"`);
41+
core.info(`Required hours open: ${hoursOpen}`);
42+
core.info(`Merge method: ${mergeMethod}`);
43+
44+
// Get all open PRs (with pagination)
45+
const pullRequests = await github.paginate(github.rest.pulls.list, {
46+
owner,
47+
repo,
48+
state: 'open',
49+
per_page: 100
50+
});
51+
52+
core.info(`Found ${pullRequests.length} open PRs`);
53+
54+
// Process each PR
55+
for (const pr of pullRequests) {
56+
core.startGroup(`PR #${pr.number} (${pr.html_url}): ${pr.title}`);
57+
58+
try {
59+
// Check if PR is from the same repository (not a fork)
60+
if (pr.head.repo.full_name !== `${owner}/${repo}`) {
61+
core.info(`❌ PR is from a fork (${pr.head.repo.full_name})`);
62+
continue;
63+
}
64+
core.info(`✅ PR is from the base repository`);
65+
66+
// Skip draft PRs
67+
if (pr.draft) {
68+
core.info(`❌ PR is a draft`);
69+
continue;
70+
}
71+
core.info(`✅ PR is ready for review`);
72+
73+
// Check if PR has the required label
74+
const hasRequiredLabel = pr.labels.some(label => label.name === labelName);
75+
if (!hasRequiredLabel) {
76+
core.info(`❌ PR is missing '${labelName}' label`);
77+
continue;
78+
}
79+
core.info(`✅ PR has '${labelName}' label`);
80+
81+
// Check if PR has been open for at least the required number of hours
82+
const createdAt = new Date(pr.created_at);
83+
const now = new Date();
84+
const hoursSinceCreation = (now - createdAt) / (1000 * 60 * 60);
85+
86+
if (hoursSinceCreation < hoursOpen) {
87+
core.info(`❌ PR opened ${hoursSinceCreation.toFixed(2)} hours ago (needs ${hoursOpen}+ hours)`);
88+
continue;
89+
}
90+
core.info(`✅ PR opened ${hoursSinceCreation.toFixed(2)} hours ago`);
91+
92+
// Check if the PR has a known valid merge commit SHA
93+
if (!pr.merge_commit_sha) {
94+
core.info(`❌ PR does not have a merge commit SHA (not mergeable)`);
95+
continue;
96+
}
97+
core.info(`✅ PR has a merge commit SHA (${pr.merge_commit_sha})`);
98+
99+
// Get full PR details to check mergeability
100+
const { data: prDetails } = await github.rest.pulls.get({
101+
owner,
102+
repo,
103+
pull_number: pr.number
104+
});
105+
106+
// Check for clean mergeable_state (indicates all checks and requirements are met)
107+
if (prDetails.mergeable_state !== 'clean') {
108+
core.info(`❌ PR mergeable_state is '${prDetails.mergeable_state}' (not clean)`);
109+
continue;
110+
}
111+
core.info(`✅ PR is mergeable (${prDetails.mergeable_state})`);
112+
113+
// All conditions met - merge the PR
114+
try {
115+
await github.rest.pulls.merge({
116+
owner,
117+
repo,
118+
pull_number: pr.number,
119+
merge_method: mergeMethod
120+
});
121+
core.notice(`🚀 Successfully merged PR #${pr.number} (${pr.html_url}): ${pr.title}`);
122+
} catch (error) {
123+
core.error(`❌ Failed to merge PR #${pr.number} (${pr.html_url}): ${error.message}`);
124+
}
125+
} finally {
126+
core.endGroup();
127+
}
128+
}
129+
130+
core.info('Auto-merge check complete');

0 commit comments

Comments
 (0)