forked from microsoft/testfx
-
Notifications
You must be signed in to change notification settings - Fork 0
344 lines (302 loc) · 14.4 KB
/
backport-base.yml
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
on:
workflow_call:
inputs:
pr_title_template:
description: 'The template used for the PR title. Special placeholder tokens that will be replaced with a value: %target_branch%, %source_pr_title%, %source_pr_number%, %cc_users%.'
required: false
type: string
default: '[%target_branch%] %source_pr_title%'
pr_description_template:
description: 'The template used for the PR description. Special placeholder tokens that will be replaced with a value: %target_branch%, %source_pr_title%, %source_pr_number%, %cc_users%.'
required: false
type: string
default: |
Backport of #%source_pr_number% to %target_branch%
/cc %cc_users%
repository_owners:
description: 'A comma-separated list of repository owners where the workflow will run. Defaults to "dotnet,microsoft".'
required: false
type: string
default: 'dotnet,microsoft'
jobs:
cleanup:
if: ${{ contains(format('{0},', inputs.repository_owners), format('{0},', github.repository_owner)) && github.event_name == 'schedule' }}
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Cleanup workflow runs
uses: actions/github-script@v7
with:
script: |
const repo_owner = context.payload.repository.owner.login;
const repo_name = context.payload.repository.name;
// look up workflow from current run
const currentWorkflowRun = await github.rest.actions.getWorkflowRun({
owner: repo_owner,
repo: repo_name,
run_id: context.runId
});
// get runs which are 'completed' (other candidate values of status field are e.g. 'queued' and 'in_progress')
for await (const response of github.paginate.iterator(
github.rest.actions.listWorkflowRuns, {
owner: repo_owner,
repo: repo_name,
workflow_id: currentWorkflowRun.data.workflow_id,
status: 'completed'
}
)) {
// delete each run
for (const run of response.data) {
console.log(`Deleting workflow run ${run.id}`);
await github.rest.actions.deleteWorkflowRun({
owner: repo_owner,
repo: repo_name,
run_id: run.id
});
}
}
run_backport:
if: ${{ contains(format('{0},', inputs.repository_owners), format('{0},', github.repository_owner)) && github.event.issue.pull_request != '' && contains(github.event.comment.body, '/backport to') }}
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
steps:
- name: Extract backport target branch
uses: actions/github-script@v7
id: target-branch-extractor
with:
result-encoding: string
script: |
if (context.eventName !== "issue_comment") throw "Error: This action only works on issue_comment events.";
// extract the target branch name from the trigger phrase containing these characters: a-z, A-Z, digits, forward slash, dot, hyphen, underscore
const regex = /^\/backport to ([a-zA-Z\d\/\.\-\_]+)/;
target_branch = regex.exec(context.payload.comment.body);
if (target_branch == null) throw "Error: No backport branch found in the trigger phrase.";
return target_branch[1];
- name: Extract PR id
uses: actions/github-script@v7
id: pr-id-extractor
with:
result-encoding: string
script: |
return context.issue.number;
- name: Calculate backport branch name
uses: actions/github-script@v7
id: backport-branch-name-extractor
with:
result-encoding: string
script: |
return `backport/${{ steps.pr-id-extractor.outputs.result }}/to/${{ steps.target-branch-extractor.outputs.result }}`;
<<<<<<< Updated upstream
=======
- name: Calculate backport PR title
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
id: backport-pr-title-extractor
env:
BACKPORT_PR_TITLE_TEMPLATE: ${{ inputs.pr_title_template }}
with:
result-encoding: string
script: |
// replace the special placeholder tokens with values
const { BACKPORT_PR_TITLE_TEMPLATE } = process.env
const target_branch = '${{ steps.target-branch-extractor.outputs.result }}';
const backport_pr_title = BACKPORT_PR_TITLE_TEMPLATE
.replace(/%target_branch%/g, target_branch)
.replace(/%source_pr_title%/g, context.payload.issue.title)
.replace(/%source_pr_number%/g, context.payload.issue.number)
return backport_pr_title;
- name: Calculate backport PR description
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
id: backport-pr-description-extractor
env:
BACKPORT_PR_DESCRIPTION_TEMPLATE: ${{ inputs.pr_description_template }}
with:
result-encoding: string
script: |
// get users to cc (append PR author if different from user who issued the backport command)
const comment_user = context.payload.comment.user.login;
let cc_users = `@${comment_user}`;
if (comment_user != context.payload.issue.user.login) cc_users += ` @${context.payload.issue.user.login}`;
// replace the special placeholder tokens with values
const { BACKPORT_PR_DESCRIPTION_TEMPLATE } = process.env
const target_branch = '${{ steps.target-branch-extractor.outputs.result }}';
const backport_pr_description = BACKPORT_PR_DESCRIPTION_TEMPLATE
.replace(/%target_branch%/g, target_branch)
.replace(/%source_pr_title%/g, context.payload.issue.title)
.replace(/%source_pr_number%/g, context.payload.issue.number)
.replace(/%cc_users%/g, cc_users);
return backport_pr_description;
>>>>>>> Stashed changes
- name: Unlock comments if PR is locked
uses: actions/github-script@v7
if: ${{ github.event.issue.locked == true }}
with:
script: |
console.log(`Unlocking locked PR #${context.issue.number}.`);
await github.rest.issues.unlock({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
});
- name: Post backport started comment to pull request
uses: actions/github-script@v7
with:
script: |
const target_branch = '${{ steps.target-branch-extractor.outputs.result }}';
const backport_start_body = `Started backporting to ${target_branch}: https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: backport_start_body
});
- name: Checkout repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run backport
uses: actions/github-script@v7
env:
BACKPORT_PR_TITLE_TEMPLATE: ${{ inputs.pr_title_template }}
BACKPORT_PR_DESCRIPTION_TEMPLATE: ${{ inputs.pr_description_template }}
with:
script: |
const target_branch = '${{ steps.target-branch-extractor.outputs.result }}';
const repo_owner = context.payload.repository.owner.login;
const repo_name = context.payload.repository.name;
const pr_number = context.payload.issue.number;
const comment_user = context.payload.comment.user.login;
try {
// verify the comment user is a repo collaborator
try {
await github.rest.repos.checkCollaborator({
owner: repo_owner,
repo: repo_name,
username: comment_user
});
console.log(`Verified ${comment_user} is a repo collaborator.`);
} catch (error) {
console.log(error);
throw new Error(`Error: @${comment_user} is not a repo collaborator, backporting is not allowed. If you're a collaborator please make sure your ${repo_owner} team membership visibility is set to Public on https://github.com/orgs/${repo_owner}/people?query=${comment_user}`);
}
try { await exec.exec(`git ls-remote --exit-code --heads origin ${target_branch}`) } catch { throw new Error(`Error: The specified backport target branch ${target_branch} wasn't found in the repo.`); }
console.log(`Backport target branch: ${target_branch}`);
console.log("Applying backport patch");
await exec.exec(`git checkout ${target_branch}`);
await exec.exec(`git clean -xdff`);
// download and apply patch
await exec.exec(`curl -sSL "${context.payload.issue.pull_request.patch_url}" --output changes.patch`);
// create temporary backport branch
// const temp_branch = `backport/pr-${pr_number}-to-${target_branch}`;
// await exec.exec(`git checkout -b ${temp_branch}`);
// configure git
await exec.exec(`git config user.name "github-actions"`);
await exec.exec(`git config user.email "[email protected]"`);
let git_am_command = "git am --3way --empty=keep --ignore-whitespace --keep-non-patch changes.patch";
let git_am_output = `$ ${git_am_command}\n\n`;
let git_am_failed = false;
try {
await exec.exec(git_am_command, [], {
listeners: {
stdout: function stdout(data) { git_am_output += data; },
stderr: function stderr(data) { git_am_output += data; }
}
});
} catch (error) {
git_am_output += error;
git_am_failed = true;
}
let failed_count = 0;
while (git_am_failed) {
failed_count++;
if (failed_count >= 5) {
await github.rest.issues.createComment({
owner: repo_owner,
repo: repo_name,
issue_number: pr_number,
body: "Potential infinite loop guard hit. Stopping"
});
return;
}
const git_am_failed_body = `@${context.payload.comment.user.login} backporting to ${target_branch} failed, the patch most likely resulted in conflicts:\n\n\`\`\`shell\n${git_am_output}\n\`\`\`\n\n**NOTE:** A PR will be created, but needs to be revised manually!**`;
await github.rest.issues.createComment({
owner: repo_owner,
repo: repo_name,
issue_number: pr_number,
body: git_am_failed_body
});
await exec.exec(`git add .`);
await exec.exec(`git restore --staged changes.patch`);
git_am_command = "git am --continue";
git_am_output = `$ ${git_am_command}\n\n`;
git_am_failed = false;
try {
await exec.exec(git_am_command, [], {
listeners: {
stdout: function stdout(data) { git_am_output += data; },
stderr: function stderr(data) { git_am_output += data; }
}
});
} catch (error) {
git_am_output += error;
git_am_failed = true;
}
}
await exec.exec(`rm changes.patch`);
// prepare the GitHub PR details
// get users to cc (append PR author if different from user who issued the backport command)
// let cc_users = `@${comment_user}`;
// if (comment_user != context.payload.issue.user.login) cc_users += ` @${context.payload.issue.user.login}`;
// replace the special placeholder tokens with values
// const { BACKPORT_PR_TITLE_TEMPLATE, BACKPORT_PR_DESCRIPTION_TEMPLATE } = process.env
// const backport_pr_title = BACKPORT_PR_TITLE_TEMPLATE
// .replace(/%target_branch%/g, target_branch)
// .replace(/%source_pr_title%/g, context.payload.issue.title)
// .replace(/%source_pr_number%/g, context.payload.issue.number)
// .replace(/%cc_users%/g, cc_users);
// const backport_pr_description = BACKPORT_PR_DESCRIPTION_TEMPLATE
// .replace(/%target_branch%/g, target_branch)
// .replace(/%source_pr_title%/g, context.payload.issue.title)
// .replace(/%source_pr_number%/g, context.payload.issue.number)
// .replace(/%cc_users%/g, cc_users);
// open the GitHub PR
// await github.rest.pulls.create({
// owner: repo_owner,
// repo: repo_name,
// title: backport_pr_title,
// body: backport_pr_description,
// head: temp_branch,
// base: target_branch
// });
// console.log("Successfully opened the GitHub PR.");
} catch (error) {
core.setFailed(error);
// post failure to GitHub comment
const unknown_error_body = `@${comment_user} an error occurred while backporting to ${target_branch}, please check the run log for details!\n\n${error.message}`;
await github.rest.issues.createComment({
owner: repo_owner,
repo: repo_name,
issue_number: pr_number,
body: unknown_error_body
});
}
- uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7
with:
token: ${{ secrets.BACKPORT_MACHINE_USER_PAT }}
push-to-fork: youssef-backport-bot/testfx
branch: ${{ steps.backport-branch-name-extractor.outputs.result }}
- name: Re-lock PR comments
uses: actions/github-script@v7
if: ${{ github.event.issue.locked == true && (success() || failure()) }}
with:
script: |
console.log(`Locking previously locked PR #${context.issue.number} again.`);
await github.rest.issues.lock({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
lock_reason: "resolved"
});