-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Expand file tree
/
Copy pathrun-bitrise-e2e-check.ts
More file actions
403 lines (340 loc) · 14.2 KB
/
run-bitrise-e2e-check.ts
File metadata and controls
403 lines (340 loc) · 14.2 KB
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
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
import * as core from '@actions/core';
import { context } from '@actions/github';
import {printTime, determineE2ERunFlags, getOctokitInstance, shouldRunBitriseE2E, getCommitHash, isMergeQueue, getMergeQueueCommitHash, getBitriseCommentForCommit} from './bitrise-utils';
import {
CompletedConclusionType,
PullRequestTriggerType,
StatusCheckStatusType,
} from '../scripts.types';
import axios from 'axios';
let owner: string;
let repo: string;
main().catch((error: Error): void => {
console.error(error);
process.exit(1);
});
async function upsertStatusCheck(
statusCheckName: string,
commitHash: string,
status: StatusCheckStatusType,
conclusion: CompletedConclusionType | undefined,
summary: string
): Promise<void> {
//Deprecated Approach
console.log(`Upserting status check: ${statusCheckName} with status ${status} for commit ${commitHash}`);
}
async function main(): Promise<void> {
const githubToken = process.env.GITHUB_TOKEN;
const e2eLabel = process.env.E2E_LABEL;
const antiLabel = process.env.NO_E2E_LABEL;
const e2ePipeline = process.env.E2E_PIPELINE;
const workflowName = process.env.WORKFLOW_NAME;
const triggerAction = context.payload.action as PullRequestTriggerType;
// Assuming context.issue comes populated with owner and repo, as typical with GitHub Actions
const { owner: contextOwner, repo: contextRepo, number: pullRequestNumber } = context.issue;
owner = contextOwner;
repo = contextRepo;
const removeAndApplyInstructions = `Remove and re-apply the "${e2eLabel}" label to trigger a E2E smoke test on Bitrise.`;
const mergeFromMainCommitMessagePrefix = `Merge branch 'main' into`;
const pullRequestLink = `https://github.com/MetaMask/metamask-mobile/pull/${pullRequestNumber}`;
const statusCheckName = process.env.STATUS_CHECK_NAME || 'Bitrise E2E Status';
const statusCheckTitle = 'Bitrise E2E Smoke Test Run';
// Define Bitrise comment tags
const bitriseTag = '<!-- BITRISE_TAG -->';
const bitrisePendingTag = '<!-- BITRISE_PENDING_TAG -->';
const bitriseSuccessTag = '<!-- BITRISE_SUCCESS_TAG -->';
const bitriseFailTag = '<!-- BITRISE_FAIL_TAG -->';
if (!githubToken) {
core.setFailed('GITHUB_TOKEN not found');
process.exit(1);
}
if (!e2eLabel) {
core.setFailed('E2E_LABEL not found');
process.exit(1);
}
if (!antiLabel) {
core.setFailed('NO_E2E_LABEL not found');
process.exit(1);
}
printTime()
// Logging for Pipeline debugging
console.log(`Trigger action: ${triggerAction}`);
console.log(`event: ${context.eventName}`);
console.log(`pullRequestNumber: ${pullRequestNumber}`);
const mergeQueue = isMergeQueue();
const octokit = getOctokitInstance();
const { data: prData } = await octokit.rest.pulls.get({
owner,
repo,
pull_number: pullRequestNumber,
});
// Determine the latest commit hash depending if it's a PR or MQ
const latestCommitHash = await getCommitHash()
const flags = await determineE2ERunFlags();
console.log(`Docs: ${flags.isDocs}`);
console.log(`Fork: ${flags.isFork}`);
console.log(`Merge Queue: ${flags.isMQ}`);
console.log(`Has smoke test label: ${flags.hasSmokeTestLabel}`);
console.log(`Anti label: ${flags.hasAntiLabel}`);
const [shouldRun, reason] = shouldRunBitriseE2E(flags);
console.log(`Should run: ${shouldRun}, Reason: ${reason}`);
// One of these two labels must exist for pull_request type (unless it's a custom Flask workflow)
const isFlaskWorkflow = process.env.STATUS_CHECK_NAME && process.env.STATUS_CHECK_NAME !== 'Bitrise E2E Status';
if (!mergeQueue && !flags.hasSmokeTestLabel && !flags.hasAntiLabel && !isFlaskWorkflow) {
// Fail Status due to missing labels
await upsertStatusCheck(statusCheckName, latestCommitHash, StatusCheckStatusType.Completed,
CompletedConclusionType.Failure, `Failed due to missing labels. Please apply either ${e2eLabel} or ${antiLabel}.`);
return
}
if (!shouldRun) {
console.log(
`Skipping Bitrise status check. due to the following reason: ${reason}`,
);
await upsertStatusCheck(statusCheckName, latestCommitHash, StatusCheckStatusType.Completed, CompletedConclusionType.Success,
`Skip run since ${reason}`);
return;
}
// Kick off E2E smoke tests if E2E smoke label is applied
if (
triggerAction === PullRequestTriggerType.Labeled &&
context.payload?.label?.name === e2eLabel
) {
console.log(`Starting Bitrise build for commit ${latestCommitHash}`);
// Configure Bitrise configuration for API call
const data = {
build_params: {
branch: process.env.GITHUB_HEAD_REF,
pipeline_id: e2ePipeline,
environments: [
{
mapped_to: 'GITHUB_PR_NUMBER',
value: `${pullRequestNumber}`,
is_expand: true,
},
{
mapped_to: 'TRIGGERED_BY_PR_LABEL',
value: `true`,
is_expand: true,
},
{
mapped_to: 'GITHUB_PR_HASH',
value: `${latestCommitHash}`,
is_expand: true,
},
],
commit_message: `Triggered by (${workflowName}) workflow in ${pullRequestLink}`,
},
hook_info: {
type: 'bitrise',
build_trigger_token: process.env.BITRISE_BUILD_TRIGGER_TOKEN,
},
triggered_by: workflowName,
};
const bitriseProjectUrl = `https://app.bitrise.io/app/${process.env.BITRISE_APP_ID}`;
const bitriseBuildStartUrl = `${bitriseProjectUrl}/build/start.json`;
// Start Bitrise build.
const bitriseBuildResponse = await axios.post(bitriseBuildStartUrl, data, {
headers: {
'Content-Type': 'application/json',
},
});
if (!bitriseBuildResponse.data.build_slug) {
core.setFailed(`Bitrise build slug not found`);
process.exit(1);
}
const latestCommitTag = `<!-- ${latestCommitHash} -->`;
const buildLink = `${bitriseProjectUrl}/pipelines/${bitriseBuildResponse.data.build_slug}`;
const message = `## [<img alt="https://bitrise.io/" src="https://assets-global.website-files.com/5db35de024bb983af1b4e151/5e6f9ccc3e129dfd8a205e4e_Bitrise%20Logo%20-%20Eggplant%20Bg.png" height="20">](${buildLink}) **Bitrise**\n\n🔄🔄🔄 \`${e2ePipeline}\` started on Bitrise...🔄🔄🔄\n\nCommit hash: ${latestCommitHash}\nBuild link: ${buildLink}\n\n>[!NOTE]\n>- This comment will auto-update when build completes\n>- You can kick off another \`${e2ePipeline}\` on Bitrise by removing and re-applying the \`${e2eLabel}\` label on the pull request\n${bitriseTag}\n${bitrisePendingTag}\n\n${latestCommitTag}`;
if (bitriseBuildResponse.status === 201) {
console.log(
`Started Bitrise build for commit ${latestCommitHash} at ${buildLink}`,
);
//TODO Remove
console.log(`Response headers from bitrise call ${bitriseBuildResponse.headers}`)
console.log(`Response data from bitrise call ${JSON.stringify(bitriseBuildResponse.data)}`)
} else {
core.setFailed(
`Bitrise build request returned with status code ${bitriseBuildResponse.status}`,
);
process.exit(1);
}
const bitriseComment = await getBitriseCommentForCommit(latestCommitHash);
// Reopen conversation in case it's locked
const unlockConvoResponse = await octokit.rest.issues.unlock({
owner,
repo,
issue_number: pullRequestNumber,
});
if (unlockConvoResponse.status === 204) {
console.log(`Unlocked conversation for PR ${pullRequestLink}`);
} else {
core.setFailed(
`Unlock conversation request returned with status code ${unlockConvoResponse.status}`,
);
process.exit(1);
}
if (bitriseComment) {
// Existing comment exists for commit hash. Update comment with pending status.
const updateCommentResponse = await octokit.rest.issues.updateComment({
owner,
repo,
issue_number: pullRequestNumber,
body: message,
comment_id: bitriseComment.id,
});
if (updateCommentResponse.status === 200) {
console.log(`Updating comment in pull request ${pullRequestLink}`);
} else {
core.setFailed(
`Update comment request returned with status code ${updateCommentResponse.status}`,
);
process.exit(1);
}
} else {
// Post new Bitrise pending comment in PR.
const postCommentResponse = await octokit.rest.issues.createComment({
owner,
repo,
issue_number: pullRequestNumber,
body: message,
});
if (postCommentResponse.status === 201) {
console.log(`Posting comment in pull request ${pullRequestLink}`);
} else {
core.setFailed(
`Post comment request returned with status code ${postCommentResponse.status}`,
);
process.exit(1);
}
}
// Post pending status
console.log(`Posting pending status for commit ${latestCommitHash}`);
await upsertStatusCheck( statusCheckName, latestCommitHash, StatusCheckStatusType.InProgress, undefined, `Test runs in progress... You can view them at ${buildLink}`);
return;
}
// Code below updates Bitrise status check by comparing the latest Bitrise comment against the latest commits
// Get at least the last 30 comments
const numberOfTotalComments = prData.comments;
const numberOfCommentsToCheck = 30;
const lastCommentPage = Math.ceil(
numberOfTotalComments / numberOfCommentsToCheck,
);
const { data: latestCommentBatch } = await octokit.rest.issues.listComments({
owner,
repo,
issue_number: pullRequestNumber,
page: lastCommentPage,
per_page: numberOfCommentsToCheck,
});
let comments = [...latestCommentBatch];
if (
numberOfTotalComments % numberOfCommentsToCheck !== 0 &&
lastCommentPage > 1
) {
// Last page's comments will be less than 30, fetch second last page as well to ensure there is at least 30 comments.
const { data: previousCommentBatch } =
await octokit.rest.issues.listComments({
owner,
repo,
issue_number: pullRequestNumber,
page: lastCommentPage - 1,
per_page: numberOfCommentsToCheck,
});
comments = [...previousCommentBatch, ...comments];
}
// Get latest Bitrise comment
const bitriseComment = comments
.reverse()
.find(({ body }) => body?.includes(bitriseTag));
// Bitrise comment doesn't exist, post fail status
if (!bitriseComment) {
console.log(`Bitrise comment not detected for commit ${latestCommitHash}`);
await upsertStatusCheck(statusCheckName, latestCommitHash, StatusCheckStatusType.Completed,
CompletedConclusionType.Failure,
`No Bitrise comment found for commit ${latestCommitHash}. Try re-applying the '${e2eLabel}' label.`);
return;
}
// Bitrise comment does exist, update status check based on Bitrise comment status
// This regex matches a 40-character hexadecimal string enclosed within <!-- and -->
let bitriseCommentBody = bitriseComment.body || '';
const commitTagRegex = /<!--\s*([0-9a-f]{40})\s*-->/i;
const hashMatch = bitriseCommentBody.match(commitTagRegex);
let bitriseCommentCommitHash = hashMatch && hashMatch[1] ? hashMatch[1] : '';
// Get at least the last 10 commits
const numberOfTotalCommits = prData.commits;
const numberOfCommitsToCheck = 10;
const lastCommitPage = Math.ceil(
numberOfTotalCommits / numberOfCommitsToCheck,
);
const { data: latestCommitBatch } = await octokit.rest.pulls.listCommits({
owner,
repo,
pull_number: pullRequestNumber,
page: lastCommitPage,
per_page: numberOfCommitsToCheck,
});
let commits = [...latestCommitBatch];
if (
numberOfTotalCommits % numberOfCommitsToCheck !== 0 &&
lastCommitPage > 1
) {
// Last page's commits will be less than 10, fetch second last page as well to ensure there is at least 10 commits.
const { data: previousCommitBatch } = await octokit.rest.pulls.listCommits({
owner,
repo,
pull_number: pullRequestNumber,
page: lastCommitPage - 1,
per_page: numberOfCommitsToCheck,
});
commits = [...previousCommitBatch, ...commits];
}
// Relevant hashes include both merge from main commits and the last non-merge from main commit (the commits that you manually push)
const relevantCommitHashes: string[] = [];
for (const commit of commits.reverse()) {
const commitMessage = commit.commit.message;
relevantCommitHashes.push(commit.sha);
if (!commitMessage.includes(mergeFromMainCommitMessagePrefix)) {
break;
}
}
let checkStatus: {
status: StatusCheckStatusType;
conclusion?: CompletedConclusionType;
} = {
status: StatusCheckStatusType.Completed,
};
let statusMessage = '';
// Check if Bitrise comment hash matches any of the relevant commit hashes
if (relevantCommitHashes.includes(bitriseCommentCommitHash)) {
// Check Bitrise build status from comment
const bitriseCommentPrefix = `Bitrise build status comment for commit ${bitriseCommentCommitHash}`;
if (bitriseCommentBody.includes(bitrisePendingTag)) {
checkStatus.status = StatusCheckStatusType.InProgress;
statusMessage = `${bitriseCommentPrefix} is pending.`;
} else if (bitriseCommentBody.includes(bitriseFailTag)) {
checkStatus = {
status: StatusCheckStatusType.Completed,
conclusion: CompletedConclusionType.Failure,
};
statusMessage = `${bitriseCommentPrefix} has failed.`;
} else if (bitriseCommentBody.includes(bitriseSuccessTag)) {
checkStatus.conclusion = CompletedConclusionType.Success;
statusMessage = `${bitriseCommentPrefix} has passed.`;
} else {
checkStatus = {
status: StatusCheckStatusType.Completed,
conclusion: CompletedConclusionType.Failure,
};
statusMessage = `${bitriseCommentPrefix} does not contain any build status. Please verify that the build status tag exists in the comment body.`;
}
} else {
// No build comment found for relevant commits
checkStatus = {
status: StatusCheckStatusType.Completed,
conclusion: CompletedConclusionType.Failure,
};
statusMessage = `No Bitrise build comment exists for latest commits. ${removeAndApplyInstructions}`;
}
// Post status check
await upsertStatusCheck(statusCheckName, latestCommitHash, checkStatus.status, checkStatus.conclusion, statusMessage);
}