-
Notifications
You must be signed in to change notification settings - Fork 2
336 lines (276 loc) · 15.7 KB
/
2_validate_PR_and_merge.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
name: 2_validate_PR_and_merge
on:
workflow_dispatch:
pull_request_target:
types: [opened, synchronize, edited]
branches:
- master
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }}
# We need to make sure the whole program works as expected even when canceled at any point...
# Alternatively we *could* also check for old deployments and cancel them, which is more work.
# In reality this should basically never happen, as its only 2 people beeing able to trigger this
# workflow, the maintainer and the contributer via PR edit. It's quite unlikely that it collides.
# currently the worst outcome is a oprhaned tracker issue, which is not that bad.
# @todo maybe make this cancel all previous deployments instead :cancelInProgress
cancel-in-progress: true
jobs:
# Since only workflow files themselves are safe when using pull_request_target
# we have to write the validation logic in the workflow file itself
# After that its safe to checkout the PRs code and run the validation
# as we know for sure, that the .github folder was not modified
#
# WARNING: THE REST OF THE PR'S CODE IS STILL TO BE CONSIDERED UNSAFE
#
# The second part validates the PR structure to be single file or folder with first.jai
# This validation does not care about been canceled, as it only checks the PR structure
# and does not modify anything :cancelInProgress
pr-validation:
runs-on: [ubuntu-latest]
outputs:
validated_commit_sha: ${{ steps.pr_safety_check.outputs.validated_commit_sha }}
validation_passed: ${{ steps.validate_pr_structure.outputs.validation_passed }}
is_single_file: ${{ steps.validate_pr_structure.outputs.is_single_file }}
steps:
- name: Check if '.github' folder was modified
id: pr_safety_check
uses: actions/github-script@v7
with:
script: |
console.log('Concurrency Group', '${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }}');
console.log('eventType', context.eventName);
console.log('context.payload.sender', context.payload.sender)
// Get PR data. *This PR anchors the sha for the whole workflow*
const { data: pr } = await github.rest.pulls.get({
...context.repo,
pull_number: context.issue.number
});
// Make sure PR is still open
if (pr.state !== 'open') {
console.log('Issue/PR is not open ... skipping');
return;
}
// We put this here as an exception to safe processing time, as an early exit
const isSBOrBB = /^\[[SB]B\] /.test(pr.title);
if (!isSBOrBB) {
console.log('This PR is not a SB or BB, ignoring it.');
core.setOutput("run_validation", false);
return;
}
// THIS IS UNSAFE because we have a race condition from our
// sha of workflow trigger, to here! But it should support 3000
// Files via pagination.
// const { data } = await github.rest.pulls.listFiles({
// ...context.repo,
// pull_number: context.issue.number,
// per_page: 100
// });
// console.log('data', data);
// const filePathsBad = data.map(file => file.filename);
// This api seems to be limited to 300 files. Sadly we cant get more without splitting up the commits.
// Also although listFiles seems to support up to 3000, we cant use it as it does not support the
// option for a sha, to remove our race condition.
// https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#compare-two-commits
// Get the file list via compareCommits using the anchored sha of the PR
const { data: comparison } = await github.rest.repos.compareCommits({
...context.repo,
base: pr.base.sha, // Compare base branch
head: pr.head.sha, // Against head of PR
page: 1, // Fetch only the first page; file list is always here
per_page: 100,
});
const filePaths = comparison.files.map(file => file.filename);
console.log('filePaths', filePaths);
// https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#compare-two-commits
if (filePaths.length >= 300) {
await github.rest.issues.createComment({
...context.repo,
issue_number: context.issue.number,
body: `This PR has more than 300 files. Please reduce the number of files.`
})
throw new Error('This PR has more than 300 files.');
}
// // Make sure .github folder is not modified
if (filePaths.some(filePath => filePath.includes('.github'))) {
throw new Error('This PR has modified the .github folder, which is illegal!');
}
// Save the file paths instead of using outputs, because they could be to big
const fs = require('fs');
// Could get to big to pass around via outputs
console.log('saving pr_files.json');
fs.writeFileSync('pr_files.json', JSON.stringify(filePaths, null, 2));
// Save PR response, also could get to big to pass around via outputs
console.log('saving pr_response.json');
fs.writeFileSync('pr_response.json', JSON.stringify(pr, null, 2));
// Pass on the exact sha of the commit
const commitSha = pr.head.sha;
console.log(`Validated Commit SHA: ${commitSha}`);
core.setOutput('validated_commit_sha', commitSha);
core.setOutput('run_validation', true);
- name: Validate PR Structure
uses: actions/github-script@v7
id: validate_pr_structure
if: ${{ steps.pr_safety_check.outputs.run_validation == 'true' }}
with:
script: |
// Load the file paths instead of querying them again because of race conditions
const fs = require('fs');
const filePaths = JSON.parse(fs.readFileSync('pr_files.json', 'utf-8'));
console.log('loaded pr_files.json', filePaths);
// The file/folder must be named after the PR number
// <tracking_issue_number>_<PRNumber>[C|R]EC<exit_code>.jai @copyPasta
const validBugNameRegexTemplate = `^compiler_bugs/\\d+_${context.issue.number}_[CR]EC-?\\d+`; // @copyPasta
const singleFileValidBugNameRegex = new RegExp(`${validBugNameRegexTemplate}\\.jai$`);
const validFilePathRegex = new RegExp(`${validBugNameRegexTemplate}/`);
const validFirstJaiRegex = new RegExp(`${validBugNameRegexTemplate}/first\\.jai$`);
console.log('validBugNameRegexTemplate', validBugNameRegexTemplate);
console.log('singleFileValidBugNameRegex', singleFileValidBugNameRegex);
console.log('validFilePathRegex', validFilePathRegex);
console.log('validFirstJaiRegex', validFirstJaiRegex);
// Check if its a single file bug PR
const isSingleFile = filePaths.length === 1 && singleFileValidBugNameRegex.test(filePaths[0]);
// Check if its a single folder with first.jai file
const isSingleFolderWithFirstJaiFile =
filePaths.every((f) => validFilePathRegex.test(f)) && // All files are in one folder
filePaths.some((f) => validFirstJaiRegex.test(f)); // At least one file is first.jai
console.log('isSingleFile', isSingleFile);
console.log('isSingleFolderWithFirstJaiFile', isSingleFolderWithFirstJaiFile);
// Error, PR doesnt match needed structure
if (!isSingleFile && !isSingleFolderWithFirstJaiFile) {
throw new Error('This PR does not match the needed structure.');
}
// We passed the validation!
core.setOutput("validation_passed", true);
core.setOutput("is_single_file", isSingleFile);
# We need this for later validation as the approval could be given quite some time later
- name: Upload Captured Data as an artifact
if: ${{ steps.validate_pr_structure.outputs.validation_passed == 'true' }}
uses: actions/upload-artifact@v4
with:
name: pr_response
path: |
pr_response.json
pr_files.json
retention-days: 1 # we only need this for the next job
# WARNING: This could get called quite some time later, than the check above
validate-added-test-and-merge-pr:
runs-on: [self-hosted, linux] # @todo remove linux
needs: pr-validation
# Only run if validation passed
if: ${{ needs.pr-validation.outputs.validation_passed == 'true' }}
# Make sure this run is manually approved, because we merge untrusted code.
# The validation above, only makes sure the JS files are safe and that the
# commit has the correct structure. But the jai files themselves are still
# untrusted.
environment: test
steps:
# The JS in here *should* be safe, as we validated that the .github was not modified
- name: Checkout Validated PRs
uses: actions/checkout@v4
with:
ref: ${{ needs.pr-validation.outputs.validated_commit_sha }}
path: PR
# But, it is generally safer to use the base branch, so we kow for sure
# that the js code, that is run below, is safe.
- name: Checkout Base Branch
uses: actions/checkout@v4
with:
path: base
- name: Download Captured Data artifact
uses: actions/download-artifact@v4
with:
name: pr_response
- name: Get App Token
uses: actions/create-github-app-token@v1
id: app_token
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
# currently we can get canceled anywhere inside this code, so it should run
# in a way to not leave the system in a bad state. At least as much as possible...
# :cancelInProgress
# We need to make sure to only use the trusted data from the validation and
# ignore all new commits etc
- name: Create Tracking Issue and merge Pull Request
uses: actions/github-script@v7
with:
github-token: ${{ steps.app_token.outputs.token }} # we need to trigger the bugsuite on merge
script: |
console.log(`Manual approval was given for Pull Request #${context.issue.number}...`);
// If isSingleFile = false its garanted to be BB, because of validation above
const isSingleFile = ${{ needs.pr-validation.outputs.is_single_file == 'true' }};
const validatedCommitSha = '${{ needs.pr-validation.outputs.validated_commit_sha }}';
// Load the PR data
const fs = require('fs');
const originalPRData = JSON.parse(fs.readFileSync('pr_response.json', 'utf-8'));
console.log('loaded pr_response.json', originalPRData);
originalPRData.body = originalPRData.body.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
// Do some validations against the current version of PR, as a lot of time could have passed
// We do this not for security, but to avoid deleting commits in the force push later on
// DONT USE THIS DATA FOR ANYTHING THOUGH, AS IT HAS NOT BEEN VALIDATED!
{
const { data: untrusted_pr } = await github.rest.pulls.get({
...context.repo,
pull_number: context.issue.number
});
untrusted_pr.body = untrusted_pr.body.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
// Make sure PR is still open
if (untrusted_pr.state !== 'open') {
console.log('untrusted_pr.state', untrusted_pr.state);
throw new Error('The PR is not open anymore! Aborting ...');
}
// Make sure head sha is the same!
if (untrusted_pr.head.sha !== validatedCommitSha) {
console.log('new sha', untrusted_pr.head.sha);
console.log('old sha', validatedCommitSha);
throw new Error('The head sha has changed since validation! This could happen when changes happened since the time validation was requested, and manual approval was given. Aborting ...');
}
// Check if title has changed
if (untrusted_pr.title !== originalPRData.title) {
console.log('old title', originalPRData.title);
console.log('new title', untrusted_pr.title);
throw new Error('The PR title has changed since validation! Aborting ...');
}
// We could check if bodies are the same, im not sure if we want that though
if (untrusted_pr.body !== originalPRData.body) {
console.log('old body', originalPRData.body);
console.log('new body', untrusted_pr.body);
throw new Error('The PR body has changed since validation! Aborting ...');
}
}
//
// Ofc, in here we could also get new race conditions, from here on. To mitigate these, we use
// force commit while renaming, and make sure only the resulting sha is accepted for merge.
//
// Therefore if we get to here, no further changes will we accepted, and even will be overwritten.
// From here on out we finish the job, and merge the PR.
//
//
// THIS IS THE ONLY CRITICAL PART IN THE WHOLE WORKFLOW IN REGARD TO CANCLED WORKFLOWS
//
// Use the base branches js!
const { createTrackingIssueFromPR, renameAllFilesToMatchTracker } = require('./base/.github/workflows/2_create_tracking_issue_from_PR.js');
// Create or use existing tracking issue so the bug suite already can use it.
// If canceled right after this call, it could create orphans :cancelInProgress
const trackerIssueNumber = await createTrackingIssueFromPR({ github, context, originalPRData });
console.log('trackerIssueNumber', trackerIssueNumber);
// Rename all files to match the tracker issue number via force commit
// to make sure no unvalidated changes are accepted accidentally.
// The resulting sha is used for the merge later on.
//
// If canceled right after this, the issue will not have a leading 0 anymore,
// this means all code above must take this into account! :cancelInProgress
const newSafeSha = await renameAllFilesToMatchTracker({ github, context, originalPRData, validatedCommitSha, trackerIssueNumber });
console.log('newSafeSha', newSafeSha);
console.log('validatedCommitSha', validatedCommitSha);
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
await sleep(1500); // Wait for the force commit to be processed
// Only merge if sha is still the expected one!
const mergeResponse = await github.rest.pulls.merge({
...context.repo,
pull_number: context.issue.number,
merge_method: 'squash', // Use 'merge', 'squash', or 'rebase'
sha: newSafeSha, // make sure the head has not changed, from what we expected and validated!
});
console.log('Finished merging PR', mergeResponse);