-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathmain.js
354 lines (316 loc) · 11 KB
/
main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
import * as core from '@actions/core'
import * as github from '@actions/github'
import {context} from '@actions/github'
import {checkLabels} from './functions/check-labels'
import {checkStatus} from './functions/check-status'
const repoName = 'github/combine-prs'
const repoUrl = 'https://github.com/github/combine-prs'
export async function run() {
// Get configuration inputs
const branchPrefix = core.getInput('branch_prefix')
const branchRegex = core.getInput('branch_regex')
const mustBeGreen = core.getInput('ci_required') === 'true'
const mustBeApproved = core.getInput('review_required') === 'true'
const combineBranchName = core.getInput('combine_branch_name')
const ignoreLabel = core.getInput('ignore_label')
const selectLabel = core.getInput('select_label')
const labels = core.getInput('labels').trim()
const assignees = core.getInput('assignees').trim()
const token = core.getInput('github_token', {required: true})
const prTitle = core.getInput('pr_title', {required: true})
const prBodyHeader = core.getInput('pr_body_header', {required: true})
const minCombineNumber = parseInt(
core.getInput('min_combine_number', {required: true})
)
const autoclose = core.getInput('autoclose') === 'true'
const updateBranch = core.getBooleanInput('update_branch')
const createFromScratch = core.getBooleanInput('create_from_scratch')
// check for either prefix or regex
if (branchPrefix === '' && branchRegex === '') {
core.setFailed('Must specify either branch_prefix or branch_regex')
return 'Must specify either branch_prefix or branch_regex'
}
// check valid label config
if (ignoreLabel && selectLabel && ignoreLabel == selectLabel) {
core.setFailed('ignore_label and select_label cannot have the same value')
return 'ignore_label and select_label cannot have the same value'
}
// Create a octokit GitHub client
const octokit = github.getOctokit(token)
// Get all open pull requests in the repository
const pulls = await octokit.paginate('GET /repos/:owner/:repo/pulls', {
owner: context.repo.owner,
repo: context.repo.repo
})
// Filter the pull requests by branch prefix and CI status
let branchesAndPRStrings = []
let baseBranch = null
let baseBranchSHA = null
for (const pull of pulls) {
const branch = pull['head']['ref']
core.info('Pull for branch: ' + branch)
// Check branch with branch_regex
if (branchRegex !== '') {
const regex = new RegExp(branchRegex)
if (regex.test(branch)) {
core.info('Branch matched regex: ' + branch)
} else {
core.info('Branch did not match regex: ' + branch)
continue
}
} else {
// If no regex, check branch with branch_prefix
if (branch.startsWith(branchPrefix)) {
core.info('Branch matched prefix: ' + branch)
} else {
continue
}
}
// Check labels
let statusOK = await checkLabels(pull, branch, selectLabel, ignoreLabel)
// Check CI status or review status if required
statusOK =
statusOK &&
(await checkStatus(pull, branch, octokit, mustBeGreen, mustBeApproved))
if (statusOK) {
core.info('Adding branch to array: ' + branch)
const prString = `${autoclose ? 'Closes ' : ''}#${pull['number']} ${
pull['title']
}`
branchesAndPRStrings.push({branch, prString})
baseBranch = pull['base']['ref']
baseBranchSHA = pull['base']['sha']
}
}
// If no branches match, exit
if (branchesAndPRStrings.length === 0) {
core.info('No PRs/branches matched criteria')
return 'No PRs/branches matched criteria'
}
// If not enough branches match given min_combine_number, exit
if (branchesAndPRStrings.length < minCombineNumber) {
core.info(
`Not enough PRs/branches matched criteria to create a combined PR - matched ${branchesAndPRStrings.length} branches/PRs but need ${minCombineNumber} branches/PRs`
)
return 'not enough PRs/branches matched criteria to create a combined PR'
}
let workingRef = combineBranchName
if (createFromScratch) {
workingRef = combineBranchName + '-working'
// delete any pre-existing working branch
try {
await octokit.rest.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: 'heads/' + workingRef
})
} catch (error) {
// If the branch doesn't exist, that's fine
// istanbul ignore next
core.debug(`branch ${workingRef} not found - OK`)
}
// create our working branch
try {
await octokit.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: 'refs/heads/' + workingRef,
sha: baseBranchSHA
})
} catch (error) {
// Otherwise, fail the Action
core.error(error)
core.setFailed('Failed to create working branch')
return 'Failed to create working branch'
}
}
// Create a new branch
try {
await octokit.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: 'refs/heads/' + combineBranchName,
sha: baseBranchSHA
})
} catch (error) {
// If the branch already exists, we'll try to merge into it
if (error.status == 422) {
core.warning('Branch already exists - will try to merge into it')
} else {
// Otherwise, fail the Action
core.error(error)
core.setFailed('Failed to create combined branch')
return 'Failed to create combined branch'
}
}
// Merge all branches into the new branch
let combinedPRs = []
let mergeFailedPRs = []
for (const {branch, prString} of branchesAndPRStrings) {
try {
await octokit.rest.repos.merge({
owner: context.repo.owner,
repo: context.repo.repo,
base: workingRef,
head: branch
})
core.info('Merged branch ' + branch)
combinedPRs.push(prString)
} catch (error) {
core.warning('Failed to merge branch ' + branch)
mergeFailedPRs.push(prString.replace('Closes ', ''))
}
}
if (createFromScratch) {
// Get the updated ref of the working branch
const {
data: {object: workingRefObject}
} = await octokit.rest.git.getRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: 'heads/' + workingRef
})
// Update the PR branch to the latest commit of the working branch
await octokit.rest.git.updateRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: 'heads/' + combineBranchName,
sha: workingRefObject.sha,
force: true
})
// Delete the temp working branch
await octokit.rest.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: 'heads/' + workingRef
})
}
// Create a new PR with the combined branch
core.info('Creating combined PR')
const combinedPRsString = `- ${combinedPRs.join('\n- ')}`
let body = `${prBodyHeader}\n\n✅ The following pull requests have been successfully combined on this PR:\n${combinedPRsString}`
if (mergeFailedPRs.length > 0) {
const mergeFailedPRsString = `- ${mergeFailedPRs.join('\n- ')}`
body +=
'\n\n⚠️ The following PRs were left out due to merge conflicts:\n' +
mergeFailedPRsString
}
body += `\n\n> This PR was created by the [\`${repoName}\`](${repoUrl}) action`
core.debug('PR body: ' + body)
var pullRequest
try {
pullRequest = await octokit.rest.pulls.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: prTitle,
head: combineBranchName,
base: baseBranch,
body: body
})
} catch (error) {
if (error?.status === 422) {
core.warning('Combined PR already exists')
// update the PR body
const prs = await octokit.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
head: context.repo.owner + ':' + combineBranchName,
base: baseBranch,
state: 'open'
})
const pr = prs.data[0]
core.info('Updating PR body')
await octokit.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
body: body
})
pullRequest = {data: pr}
} else {
if (
error?.message?.includes(
'GitHub Actions is not permitted to create or approve pull requests'
)
) {
core.warning(
'https://github.blog/changelog/2022-05-03-github-actions-prevent-github-actions-from-creating-and-approving-pull-requests/'
)
}
core.setFailed(`Failed to create combined PR - ${error}`)
return 'failure'
}
}
// check the combined PR's state to see if it is closed
const combinedPRState = pullRequest.data.state
if (combinedPRState === 'closed') {
core.info('Combined PR is closed - attempting to reopen')
await octokit.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pullRequest.data.number,
state: 'open'
})
}
if (labels !== '') {
// split and trim labels
const labelsArray = labels.split(',').map(label => label.trim())
// add labels to the combined PR if specified
if (labelsArray.length > 0) {
core.info(`Adding labels to combined PR: ${labelsArray}`)
await octokit.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.data.number,
labels: labelsArray
})
}
}
if (assignees !== '') {
// split and trim assignees
const assigneesArray = assignees.split(',').map(assignee => assignee.trim())
// add assignees to the combined PR if specified
if (assigneesArray.length > 0) {
core.info(`Adding assignees to combined PR: ${assigneesArray}`)
await octokit.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequest.data.number,
assignees: assigneesArray
})
}
}
// lastly, if the pull request's branch can be updated cleanly, update it
if (updateBranch === true) {
core.info('Attempting to update branch')
try {
const result = await octokit.rest.pulls.updateBranch({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pullRequest.data.number
})
// If the result is not a 202, return an error message and exit
if (result.status !== 202) {
throw new Error(
`Failed to update combined pr branch with the base branch - ${result}`
)
}
core.info('Branch updated')
} catch (error) {
core.warning('Failed to update combined pr branch with the base branch')
core.warning(error)
}
}
// output pull request url
core.info('Combined PR url: ' + pullRequest.data.html_url)
core.setOutput('pr_url', pullRequest.data.html_url)
// output pull request number
core.info('Combined PR number: ' + pullRequest.data.number)
core.setOutput('pr_number', pullRequest.data.number)
return 'success'
}
// Do not run if this is a test
if (process.env.COMBINE_PRS_TEST !== 'true') {
/* istanbul ignore next */
run()
}