/
dangerfile.js
184 lines (165 loc) · 6.68 KB
/
dangerfile.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
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
'use strict';
/* eslint-disable lint/sort-imports */
// The 'danger' package seems to have some side effects that make it unsafe
// to reorder.
const {danger, fail, /*message,*/ warn} = require('danger');
const includes = require('lodash.includes');
const fetch = require('node-fetch');
const {validate: validateChangelog} =
require('@rnx-kit/rn-changelog-generator').default;
const isFromPhabricator =
danger.github.pr.body &&
danger.github.pr.body.toLowerCase().includes('differential revision:');
// Provides advice if a summary section is missing, or body is too short
const includesSummary =
danger.github.pr.body &&
danger.github.pr.body.toLowerCase().includes('## summary');
if (!danger.github.pr.body || danger.github.pr.body.length < 50) {
fail(':grey_question: This pull request needs a description.');
} else if (!includesSummary && !isFromPhabricator) {
// PRs from Phabricator always includes the Summary by default.
const title = ':clipboard: Missing Summary';
const idea =
'Can you add a Summary? ' +
'To do so, add a "## Summary" section to your PR description. ' +
'This is a good place to explain the motivation for making this change.';
warn(`${title} - <i>${idea}</i>`);
}
// Warns if there are changes to package.json, and tags the team.
const packageChanged = includes(danger.git.modified_files, 'package.json');
if (packageChanged) {
const title = ':lock: package.json';
const idea =
'Changes were made to package.json. ' +
'This will require a manual import by a Facebook employee.';
warn(`${title} - <i>${idea}</i>`);
}
// Provides advice if a test plan is missing.
const includesTestPlan =
danger.github.pr.body &&
danger.github.pr.body.toLowerCase().includes('## test plan');
if (!includesTestPlan && !isFromPhabricator) {
// PRs from Phabricator never exports the Test Plan so let's disable this check.
const title = ':clipboard: Missing Test Plan';
const idea =
'Can you add a Test Plan? ' +
'To do so, add a "## Test Plan" section to your PR description. ' +
'A Test Plan lets us know how these changes were tested.';
warn(`${title} - <i>${idea}</i>`);
}
// Check if there is a changelog and validate it
if (!isFromPhabricator) {
const status = validateChangelog(danger.github.pr.body);
const changelogInstructions =
'See <a target="_blank" href="https://reactnative.dev/contributing/changelogs-in-pull-requests">Changelog format</a>';
if (status === 'missing') {
// Provides advice if a changelog is missing
const title = ':clipboard: Missing Changelog';
const idea =
'Please add a Changelog to your PR description. ' + changelogInstructions;
fail(`${title} - <i>${idea}</i>`);
} else if (status === 'invalid') {
const title = ':clipboard: Verify Changelog Format';
const idea = changelogInstructions;
fail(`${title} - <i>${idea}</i>`);
}
}
// Warns if the PR is opened against stable, as commits need to be cherry picked and tagged by a release maintainer.
// Fails if the PR is opened against anything other than `main` or `-stable`.
const isMergeRefMain = danger.github.pr.base.ref === 'main';
const isMergeRefStable = danger.github.pr.base.ref.endsWith('-stable');
if (!isMergeRefMain && !isMergeRefStable) {
const title = ':exclamation: Base Branch';
const idea =
'The base branch for this PR is something other than `main` or a `-stable` branch. [Are you sure you want to target something other than the `main` branch?](https://reactnative.dev/docs/contributing#pull-requests)';
fail(`${title} - <i>${idea}</i>`);
}
// If the PR is opened against stable should add `Pick Request` label
if (isMergeRefStable) {
danger.github.api.issues.addLabels({
owner: danger.github.pr.base.repo.owner.login,
repo: danger.github.pr.base.repo.name,
issue_number: danger.github.pr.number,
labels: ['Pick Request'],
});
}
// Wait for statuses and post a message if there are failures.
async function handleStatuses() {
const regex = /Test Suites: \d+ failed/;
let startChecking = Date.now();
let done = false;
while (!done) {
let now = Date.now();
if (now - startChecking > 90 * 60 * 1000) {
warn(
"One hour and a half have passed and the E2E jobs haven't finished yet.",
);
done = true;
continue;
}
const githubBaseURL = `https://api.github.com/repos/${danger.github.pr.base.repo.owner.login}/${danger.github.pr.base.repo.name}`;
const statusesURL = `${githubBaseURL}/commits/${danger.github.pr.head.sha}/statuses?per_page=100`;
const response = await fetch(statusesURL, {
headers: {
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
Authorization: `Bearer ${process.env.DANGER_GITHUB_API_TOKEN}`,
},
});
const data = await response.json();
const e2e_jobs = data.filter(job => {
return (
job.context === 'ci/circleci: test_e2e_ios'
// test_e2e_android does not currently tun
// || job.context === 'ci/circleci: test_e2e_android'
);
});
if (e2e_jobs.length <= 0) {
console.log('No e2e jobs found yet, retrying in 5 minutes.');
await new Promise(resolve => setTimeout(resolve, 5 * 60 * 1000));
continue;
}
const jobFinished = e2e_jobs.every(job => job.state !== 'pending');
if (!jobFinished) {
console.log("E2E jobs haven't finished yet, retrying in 5 minutes.");
await new Promise(resolve => setTimeout(resolve, 5 * 60 * 1000));
continue;
}
e2e_jobs.forEach(async job => {
const url = job.target_url;
const components = url.split('/');
const jobId = components[components.length - 1];
const jobUrl = `https://circleci.com/api/v2/project/gh/facebook/react-native/${jobId}`;
const artifactUrl = `${jobUrl}/artifacts`;
const artifactResponse = await fetch(artifactUrl);
const artifactData = await artifactResponse.json();
const testLogs = artifactData.items.filter(
item => item.path === 'tmp/test_log',
);
if (testLogs.length !== 1) {
warn(
`Can't find the E2E test log for ${job.context}. <a href=${jobUrl}>Job link</a>`,
);
return;
}
const logUrl = testLogs[0].url;
const logResponseText = await fetch(logUrl);
const logText = await logResponseText.text();
if (regex.test(logText)) {
warn(
`E2E tests for ${job.context} failed with errors. See the <a href="${logUrl}">logs for details<a/>`,
);
}
});
done = true;
}
}
// handleStatuses();