@node-core/utils 6.4.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -5
- package/bin/ncu-ci.js +3 -19
- package/components/git/benchmark.js +112 -0
- package/components/git/land.js +3 -3
- package/components/git/metadata.js +34 -1
- package/components/git/release.js +3 -5
- package/components/git/security.js +258 -27
- package/components/git/v8.js +6 -3
- package/components/metadata.js +75 -7
- package/lib/auth.js +3 -4
- package/lib/backport_session.js +3 -3
- package/lib/benchmark.js +243 -0
- package/lib/cherry_pick.js +32 -11
- package/lib/ci/failure_aggregator.js +16 -20
- package/lib/ci/run_ci.js +1 -1
- package/lib/cli.js +25 -1
- package/lib/config.js +7 -7
- package/lib/file.js +5 -5
- package/lib/landing_session.js +65 -24
- package/lib/links.js +4 -4
- package/lib/pr_checker.js +163 -31
- package/lib/pr_data.js +1 -1
- package/lib/pr_summary.js +1 -1
- package/lib/prepare_release.js +270 -85
- package/lib/prepare_security.js +312 -46
- package/lib/promote_release.js +19 -16
- package/lib/queries/ListSecurityReleasePRs.gql +19 -0
- package/lib/queries/PR.gql +4 -1
- package/lib/request.js +41 -0
- package/lib/run.js +5 -5
- package/lib/security-announcement.js +10 -5
- package/lib/security-release/security-release.js +160 -20
- package/lib/security_blog.js +130 -65
- package/lib/session.js +4 -3
- package/lib/team_info.js +1 -1
- package/lib/update-v8/applyNodeChanges.js +1 -3
- package/lib/update-v8/backport.js +6 -6
- package/lib/update-v8/commitUpdate.js +3 -6
- package/lib/update-v8/common.js +30 -6
- package/lib/update-v8/constants.js +5 -0
- package/lib/update-v8/deps.js +3 -5
- package/lib/update-v8/index.js +14 -5
- package/lib/update-v8/majorUpdate.js +5 -4
- package/lib/update-v8/minorUpdate.js +4 -4
- package/lib/update-v8/updateCrates.js +192 -0
- package/lib/update-v8/updateV8Clone.js +1 -1
- package/lib/update-v8/updateVersionNumbers.js +3 -6
- package/lib/update-v8/util.js +3 -3
- package/lib/update_security_release.js +347 -105
- package/lib/utils.js +5 -5
- package/lib/verbosity.js +3 -3
- package/lib/wpt/index.js +1 -23
- package/package.json +18 -22
- package/bin/get-metadata.js +0 -11
- package/lib/wpt/templates/README.md +0 -16
- package/npm-shrinkwrap.json +0 -20380
package/components/metadata.js
CHANGED
|
@@ -4,9 +4,71 @@ import Request from '../lib/request.js';
|
|
|
4
4
|
import auth from '../lib/auth.js';
|
|
5
5
|
import PRData from '../lib/pr_data.js';
|
|
6
6
|
import PRSummary from '../lib/pr_summary.js';
|
|
7
|
-
import PRChecker
|
|
7
|
+
import PRChecker, {
|
|
8
|
+
PR_CHECK_REASON_CODES
|
|
9
|
+
} from '../lib/pr_checker.js';
|
|
8
10
|
import MetadataGenerator from '../lib/metadata_gen.js';
|
|
9
11
|
|
|
12
|
+
export const METADATA_READINESS = Object.freeze({
|
|
13
|
+
READY: 'ready',
|
|
14
|
+
DEFERRABLE: 'deferrable',
|
|
15
|
+
FAILED: 'failed'
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const METADATA_EXIT_CODES = Object.freeze({
|
|
19
|
+
READY: 0,
|
|
20
|
+
DEFERRABLE: 20,
|
|
21
|
+
FAILED: 40
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const DEFERRABLE_REASON_CODES = new Set([
|
|
25
|
+
PR_CHECK_REASON_CODES.WAIT_TIME
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export function classifyMetadataReadiness(ready, reasonCodes) {
|
|
29
|
+
if (ready) {
|
|
30
|
+
return METADATA_READINESS.READY;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (reasonCodes.length > 0 &&
|
|
34
|
+
reasonCodes.every((code) => DEFERRABLE_REASON_CODES.has(code))) {
|
|
35
|
+
return METADATA_READINESS.DEFERRABLE;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return METADATA_READINESS.FAILED;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getMetadataExitCode(readiness) {
|
|
42
|
+
switch (readiness) {
|
|
43
|
+
case METADATA_READINESS.READY:
|
|
44
|
+
return METADATA_EXIT_CODES.READY;
|
|
45
|
+
case METADATA_READINESS.DEFERRABLE:
|
|
46
|
+
return METADATA_EXIT_CODES.DEFERRABLE;
|
|
47
|
+
default:
|
|
48
|
+
return METADATA_EXIT_CODES.FAILED;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function formatMetadataResult({ status, data, metadata, checker }) {
|
|
53
|
+
const reasonCodes = [...new Set(checker.reasons.map(({ code }) => code))];
|
|
54
|
+
const readiness = classifyMetadataReadiness(status, reasonCodes);
|
|
55
|
+
const exitCode = getMetadataExitCode(readiness);
|
|
56
|
+
return {
|
|
57
|
+
ready: status,
|
|
58
|
+
readiness,
|
|
59
|
+
exitCode,
|
|
60
|
+
pullRequest: {
|
|
61
|
+
owner: data.owner,
|
|
62
|
+
repo: data.repo,
|
|
63
|
+
number: data.prid,
|
|
64
|
+
url: data.pr.url
|
|
65
|
+
},
|
|
66
|
+
metadata,
|
|
67
|
+
reasonCodes,
|
|
68
|
+
reasons: checker.reasons
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
10
72
|
export async function getMetadata(argv, skipRefs, cli) {
|
|
11
73
|
const credentials = await auth({
|
|
12
74
|
github: true,
|
|
@@ -22,7 +84,7 @@ export async function getMetadata(argv, skipRefs, cli) {
|
|
|
22
84
|
summary.display();
|
|
23
85
|
|
|
24
86
|
const metadata = new MetadataGenerator({ skipRefs, ...data }).getMetadata();
|
|
25
|
-
if (!process.stdout.isTTY) {
|
|
87
|
+
if (!argv.json && !process.stdout.isTTY) {
|
|
26
88
|
process.stdout.write(metadata);
|
|
27
89
|
}
|
|
28
90
|
|
|
@@ -33,17 +95,23 @@ export async function getMetadata(argv, skipRefs, cli) {
|
|
|
33
95
|
cli.stopSpinner(`Done writing metadata to ${argv.file}`);
|
|
34
96
|
}
|
|
35
97
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
98
|
+
if (!argv.json) {
|
|
99
|
+
cli.separator('Generated metadata');
|
|
100
|
+
cli.write(metadata);
|
|
101
|
+
cli.separator();
|
|
102
|
+
}
|
|
39
103
|
|
|
40
104
|
const checker = new PRChecker(cli, data, request, argv);
|
|
41
105
|
const status = await checker.checkAll(argv.checkComments, argv.checkCI);
|
|
42
|
-
|
|
106
|
+
const result = {
|
|
43
107
|
status,
|
|
44
108
|
request,
|
|
45
109
|
data,
|
|
46
110
|
metadata,
|
|
47
111
|
checker
|
|
48
112
|
};
|
|
49
|
-
|
|
113
|
+
return {
|
|
114
|
+
...result,
|
|
115
|
+
json: formatMetadataResult(result)
|
|
116
|
+
};
|
|
117
|
+
}
|
package/lib/auth.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { ClientRequest } from 'node:http';
|
|
3
3
|
|
|
4
|
-
// eslint-disable-next-line import/no-unresolved
|
|
5
4
|
import ghauth from 'ghauth';
|
|
6
5
|
|
|
7
6
|
import { clearCachedConfig, encryptValue, getMergedConfig, getNcurcPath } from './config.js';
|
|
@@ -76,7 +75,7 @@ async function auth(
|
|
|
76
75
|
let token;
|
|
77
76
|
try {
|
|
78
77
|
({ username, token } = getMergedConfig());
|
|
79
|
-
} catch
|
|
78
|
+
} catch {
|
|
80
79
|
// Ignore error and prompt
|
|
81
80
|
}
|
|
82
81
|
|
|
@@ -94,7 +93,7 @@ async function auth(
|
|
|
94
93
|
'and run the following command to add it to your ncu config: ' +
|
|
95
94
|
'ncu-config --global set -x jenkins_token'
|
|
96
95
|
);
|
|
97
|
-
}
|
|
96
|
+
}
|
|
98
97
|
check(username, jenkins_token);
|
|
99
98
|
const jenkins = encode(username, jenkins_token);
|
|
100
99
|
setOwnProperty(result, 'jenkins', jenkins);
|
|
@@ -127,7 +126,7 @@ async function auth(
|
|
|
127
126
|
let token;
|
|
128
127
|
try {
|
|
129
128
|
token = await encryptValue(credentials.token);
|
|
130
|
-
} catch
|
|
129
|
+
} catch {
|
|
131
130
|
console.warn('Failed encrypt token, storing unencrypted instead');
|
|
132
131
|
token = credentials.token;
|
|
133
132
|
}
|
package/lib/backport_session.js
CHANGED
|
@@ -27,7 +27,7 @@ export default class BackportSession extends Session {
|
|
|
27
27
|
logs = runSync('git',
|
|
28
28
|
['log', `-${num}`, '--format=%h', rev, '--', file]
|
|
29
29
|
).trim();
|
|
30
|
-
} catch
|
|
30
|
+
} catch {
|
|
31
31
|
return null;
|
|
32
32
|
}
|
|
33
33
|
if (!logs) {
|
|
@@ -273,7 +273,7 @@ export default class BackportSession extends Session {
|
|
|
273
273
|
const upstreamCommit = this.getBranchCommit(`${this.upstream}/${branch}`);
|
|
274
274
|
|
|
275
275
|
return localCommit === upstreamCommit;
|
|
276
|
-
}
|
|
276
|
+
}
|
|
277
277
|
|
|
278
278
|
isLocalBranchExists(branch) {
|
|
279
279
|
try {
|
|
@@ -282,7 +282,7 @@ export default class BackportSession extends Session {
|
|
|
282
282
|
['rev-parse', '--verify', '--quiet', branch]
|
|
283
283
|
);
|
|
284
284
|
return true;
|
|
285
|
-
} catch
|
|
285
|
+
} catch {
|
|
286
286
|
return false;
|
|
287
287
|
}
|
|
288
288
|
}
|
package/lib/benchmark.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { getPrURL } from './links.js';
|
|
4
|
+
import { IGNORE } from './run.js';
|
|
5
|
+
|
|
6
|
+
const BENCHMARK_DIR = 'benchmark';
|
|
7
|
+
// Entries in the benchmark folder that are not benchmark categories.
|
|
8
|
+
const NON_CATEGORY_DIRS = new Set(['fixtures']);
|
|
9
|
+
|
|
10
|
+
export default class BenchmarkSession {
|
|
11
|
+
constructor(cli, request, argv) {
|
|
12
|
+
this.cli = cli;
|
|
13
|
+
this.request = request;
|
|
14
|
+
this.argv = argv;
|
|
15
|
+
this.workflow = argv.workflow || 'benchmark.yml';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get prUrl() {
|
|
19
|
+
const { owner, repo, prid } = this.argv;
|
|
20
|
+
return getPrURL({ owner, repo, prid });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async getPullRequest() {
|
|
24
|
+
const { cli } = this;
|
|
25
|
+
cli.startSpinner(`Fetching data for ${this.prUrl}`);
|
|
26
|
+
const pr = await this.request.getPullRequest(this.prUrl);
|
|
27
|
+
if (!pr || !pr.head) {
|
|
28
|
+
cli.stopSpinner(
|
|
29
|
+
`Could not find pull request ${this.prUrl}`,
|
|
30
|
+
cli.SPINNER_STATUS.FAILED);
|
|
31
|
+
throw new Error(IGNORE);
|
|
32
|
+
}
|
|
33
|
+
cli.stopSpinner(`Fetched data for ${this.prUrl}`);
|
|
34
|
+
this.pr = pr;
|
|
35
|
+
return pr;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async getCategories() {
|
|
39
|
+
const { cli, pr } = this;
|
|
40
|
+
const head = pr.head;
|
|
41
|
+
// Prefer listing the benchmark folder on the PR head, falling back to the
|
|
42
|
+
// base branch if the head repository is unavailable (e.g. deleted fork).
|
|
43
|
+
const source = head.repo
|
|
44
|
+
? { fullName: head.repo.full_name, ref: head.sha }
|
|
45
|
+
: { fullName: pr.base.repo.full_name, ref: pr.base.ref };
|
|
46
|
+
const [owner, repo] = source.fullName.split('/');
|
|
47
|
+
|
|
48
|
+
cli.startSpinner('Fetching available benchmark categories');
|
|
49
|
+
let entries;
|
|
50
|
+
try {
|
|
51
|
+
entries = await this.request.listDirectory({
|
|
52
|
+
owner,
|
|
53
|
+
repo,
|
|
54
|
+
path: BENCHMARK_DIR,
|
|
55
|
+
ref: source.ref
|
|
56
|
+
});
|
|
57
|
+
} catch (e) {
|
|
58
|
+
cli.stopSpinner(
|
|
59
|
+
'Could not fetch benchmark categories',
|
|
60
|
+
cli.SPINNER_STATUS.FAILED);
|
|
61
|
+
throw e;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const categories = entries
|
|
65
|
+
.filter((entry) => entry.type === 'dir')
|
|
66
|
+
.map((entry) => entry.name)
|
|
67
|
+
.filter((name) => !NON_CATEGORY_DIRS.has(name))
|
|
68
|
+
.sort();
|
|
69
|
+
|
|
70
|
+
if (categories.length === 0) {
|
|
71
|
+
cli.stopSpinner(
|
|
72
|
+
`No benchmark categories found in ${source.fullName}/${BENCHMARK_DIR}`,
|
|
73
|
+
cli.SPINNER_STATUS.FAILED);
|
|
74
|
+
throw new Error(IGNORE);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
cli.stopSpinner(`Found ${categories.length} benchmark categories`);
|
|
78
|
+
this.categories = categories;
|
|
79
|
+
return categories;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async getTouchedBenchmarks() {
|
|
83
|
+
const { cli, argv } = this;
|
|
84
|
+
const touched = [];
|
|
85
|
+
try {
|
|
86
|
+
const files = this.request.getPullRequestFiles({
|
|
87
|
+
owner: argv.owner,
|
|
88
|
+
repo: argv.repo,
|
|
89
|
+
prid: argv.prid
|
|
90
|
+
});
|
|
91
|
+
for await (const file of files) {
|
|
92
|
+
const match = /^benchmark\/([^/]+)\/(.+\.js)$/.exec(file.filename);
|
|
93
|
+
if (match && this.categories.includes(match[1])) {
|
|
94
|
+
touched.push({
|
|
95
|
+
category: match[1],
|
|
96
|
+
name: basename(match[2], '.js')
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// Not being able to fetch the changed files is not fatal: fall back to
|
|
102
|
+
// an empty pre-selection.
|
|
103
|
+
cli.warn('Could not determine which benchmark files the PR touches.');
|
|
104
|
+
}
|
|
105
|
+
this.touched = touched;
|
|
106
|
+
return touched;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Guess relevant categories from the PR subsystem labels, matching a label
|
|
110
|
+
// to a category name up to a trailing plural "s" (e.g. `buffer` -> `buffers`).
|
|
111
|
+
categoriesFromLabels() {
|
|
112
|
+
const normalize = (name) => name.toLowerCase().replace(/s$| /, '');
|
|
113
|
+
const labels = (this.pr.labels ?? [])
|
|
114
|
+
.map((label) => (typeof label === 'string' ? label : label.name))
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.map(normalize);
|
|
117
|
+
return new Set(
|
|
118
|
+
this.categories.filter((category) =>
|
|
119
|
+
labels.includes(normalize(category))));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async promptCategories() {
|
|
123
|
+
const { cli, categories } = this;
|
|
124
|
+
// Fall back to the PR labels when the PR does not touch any benchmark file.
|
|
125
|
+
const preselected = this.touched.length
|
|
126
|
+
? new Set(this.touched.map((f) => f.category))
|
|
127
|
+
: this.categoriesFromLabels();
|
|
128
|
+
|
|
129
|
+
if (preselected.size) {
|
|
130
|
+
cli.info(`Based on the PR ${
|
|
131
|
+
this.touched.length ? 'changed files' : 'labels'
|
|
132
|
+
}, this/these benchmark(s) seem(s) relevant:`);
|
|
133
|
+
cli.info([...preselected].join(', '));
|
|
134
|
+
cli.info('If not relevant, do not forget to unselect it/them.');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const selected = await cli.promptCheckbox(
|
|
138
|
+
'Select the benchmark categories to run:',
|
|
139
|
+
categories.map((name) => ({
|
|
140
|
+
name,
|
|
141
|
+
value: name,
|
|
142
|
+
checked: preselected.has(name)
|
|
143
|
+
})));
|
|
144
|
+
|
|
145
|
+
if (!selected || selected.length === 0) {
|
|
146
|
+
cli.warn('No benchmark category selected, aborting.');
|
|
147
|
+
throw new Error(IGNORE);
|
|
148
|
+
}
|
|
149
|
+
return selected;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async promptFilter(selected) {
|
|
153
|
+
const { cli, touched } = this;
|
|
154
|
+
|
|
155
|
+
// When a single benchmark file was touched and only its category is
|
|
156
|
+
// selected, pre-fill the filter with that file so it runs on its own.
|
|
157
|
+
const defaultAnswer =
|
|
158
|
+
touched.length === 1 && selected.length === 1 &&
|
|
159
|
+
selected[0] === touched[0].category
|
|
160
|
+
? touched[0].name
|
|
161
|
+
: '';
|
|
162
|
+
|
|
163
|
+
const filter = await cli.prompt(
|
|
164
|
+
'Substring to filter which benchmarks run (leave empty to run all):',
|
|
165
|
+
{ questionType: cli.QUESTION_TYPE.INPUT, defaultAnswer });
|
|
166
|
+
return filter?.trim() || undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async start() {
|
|
170
|
+
const { cli, argv } = this;
|
|
171
|
+
|
|
172
|
+
await this.getPullRequest();
|
|
173
|
+
await this.getCategories();
|
|
174
|
+
cli.startSpinner('Checking for benchmark files listed in the PR changes...');
|
|
175
|
+
await this.getTouchedBenchmarks();
|
|
176
|
+
cli.stopSpinner(`Found ${this.touched.length} benchmark file(s) in the PR changes`);
|
|
177
|
+
const selected = await this.promptCategories();
|
|
178
|
+
const filter = await this.promptFilter(selected);
|
|
179
|
+
|
|
180
|
+
const workflowOwner = argv.workflowOwner || argv.owner;
|
|
181
|
+
const workflowRepo = argv.workflowRepo || argv.repo;
|
|
182
|
+
const prRepo = `${argv.owner}/${argv.repo}`;
|
|
183
|
+
// The workflow token can only comment when it runs in the PR repository;
|
|
184
|
+
// default to not commenting when the workflow lives elsewhere.
|
|
185
|
+
const sameRepo = `${workflowOwner}/${workflowRepo}` === prRepo;
|
|
186
|
+
const postComment = await cli.prompt(
|
|
187
|
+
'Should the workflow post the results as a comment on the PR?',
|
|
188
|
+
{ defaultAnswer: sameRepo });
|
|
189
|
+
|
|
190
|
+
const category = selected.join(' ');
|
|
191
|
+
// Benchmark the commit provided on the command line, defaulting to the
|
|
192
|
+
// current PR head.
|
|
193
|
+
const commit = argv.commit || this.pr.head.sha;
|
|
194
|
+
|
|
195
|
+
const inputs = {
|
|
196
|
+
pr_id: `${argv.prid}`,
|
|
197
|
+
commit,
|
|
198
|
+
category
|
|
199
|
+
};
|
|
200
|
+
// The benchmark workflow checks out `inputs.repo || github.repository`; only
|
|
201
|
+
// set it when the workflow lives in a different repository than the PR.
|
|
202
|
+
if (!sameRepo) {
|
|
203
|
+
inputs.repo = prRepo;
|
|
204
|
+
}
|
|
205
|
+
if (filter) {
|
|
206
|
+
inputs.filter = filter;
|
|
207
|
+
}
|
|
208
|
+
if (argv.runs != null) {
|
|
209
|
+
inputs.runs = `${argv.runs}`;
|
|
210
|
+
}
|
|
211
|
+
inputs['post-comment'] = `${postComment}`;
|
|
212
|
+
|
|
213
|
+
cli.separator('Benchmark');
|
|
214
|
+
cli.table('PR:', this.prUrl);
|
|
215
|
+
cli.table('Commit:', commit);
|
|
216
|
+
cli.table('Categories:', category);
|
|
217
|
+
if (inputs.filter) cli.table('Filter:', inputs.filter);
|
|
218
|
+
if (inputs.runs) cli.table('Runs:', inputs.runs);
|
|
219
|
+
cli.table('Comment on PR:', postComment ? 'yes' : 'no');
|
|
220
|
+
cli.table('Workflow:',
|
|
221
|
+
`${workflowOwner}/${workflowRepo} (${this.workflow}@${argv.ref})`);
|
|
222
|
+
|
|
223
|
+
const confirmed = await cli.prompt(
|
|
224
|
+
'Trigger the benchmark workflow with these settings?',
|
|
225
|
+
{ defaultAnswer: true });
|
|
226
|
+
if (!confirmed) {
|
|
227
|
+
cli.warn('Aborted, no workflow was triggered.');
|
|
228
|
+
throw new Error(IGNORE);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
cli.startSpinner('Dispatching benchmark workflow');
|
|
232
|
+
await this.request.dispatchWorkflow(this.workflow, {
|
|
233
|
+
owner: workflowOwner,
|
|
234
|
+
repo: workflowRepo,
|
|
235
|
+
ref: argv.ref,
|
|
236
|
+
inputs
|
|
237
|
+
});
|
|
238
|
+
cli.stopSpinner('Benchmark workflow dispatched');
|
|
239
|
+
cli.info('Follow the run at ' +
|
|
240
|
+
`https://github.com/${workflowOwner}/${workflowRepo}/actions/` +
|
|
241
|
+
`workflows/${this.workflow}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
package/lib/cherry_pick.js
CHANGED
|
@@ -16,20 +16,31 @@ export default class CherryPick {
|
|
|
16
16
|
lint,
|
|
17
17
|
includeCVE,
|
|
18
18
|
cveIds,
|
|
19
|
-
vulnCveMap
|
|
19
|
+
vulnCveMap,
|
|
20
|
+
promptAmend,
|
|
21
|
+
skipMessagePrompt
|
|
20
22
|
} = {}) {
|
|
21
23
|
this.prid = prid;
|
|
22
24
|
this.cli = cli;
|
|
23
25
|
this.dir = dir;
|
|
24
26
|
this.upstream = upstream;
|
|
25
27
|
this.gpgSign = gpgSign;
|
|
26
|
-
this.
|
|
28
|
+
this.skipMessagePrompt = skipMessagePrompt ?? false;
|
|
29
|
+
this.options = {
|
|
30
|
+
owner, repo, lint, includeCVE, cveIds, vulnCveMap, promptAmend
|
|
31
|
+
};
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
get includeCVE() {
|
|
30
35
|
return this.options.includeCVE ?? false;
|
|
31
36
|
}
|
|
32
37
|
|
|
38
|
+
// Whether to ask before amending the PR to the proposal. Callers that already
|
|
39
|
+
// asked opt out of it.
|
|
40
|
+
get promptAmend() {
|
|
41
|
+
return this.options.promptAmend ?? true;
|
|
42
|
+
}
|
|
43
|
+
|
|
33
44
|
get cveIds() {
|
|
34
45
|
return this.options.cveIds ?? null;
|
|
35
46
|
}
|
|
@@ -82,14 +93,22 @@ export default class CherryPick {
|
|
|
82
93
|
}, false, cli);
|
|
83
94
|
this.expectedCommitShas =
|
|
84
95
|
metadata.data.commits.map(({ commit }) => commit.oid);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
// In security releases the fix may have already landed publicly, so the
|
|
97
|
+
// PR is merged and refs/pull/<prid>/merge no longer exists - pick the
|
|
98
|
+
// commits as they landed on the base branch instead.
|
|
99
|
+
this.landedCommitSha = metadata.data.pr.merged
|
|
100
|
+
? metadata.data.pr.mergeCommit?.oid
|
|
101
|
+
: undefined;
|
|
102
|
+
|
|
103
|
+
if (this.promptAmend) {
|
|
104
|
+
const amend = await cli.prompt(
|
|
105
|
+
'Would you like to amend this PR to the proposal?',
|
|
106
|
+
{ default: true }
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (!amend) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
93
112
|
}
|
|
94
113
|
|
|
95
114
|
try {
|
|
@@ -117,7 +136,7 @@ export default class CherryPick {
|
|
|
117
136
|
{ captureStdout: 'lines' });
|
|
118
137
|
|
|
119
138
|
if (commitInfo.shas.length !== 1) {
|
|
120
|
-
const fixupAll = await cli.prompt(
|
|
139
|
+
const fixupAll = this.skipMessagePrompt || await cli.prompt(
|
|
121
140
|
`${subjects.length} commits from the original PR are going to be` +
|
|
122
141
|
'squashed into a single commit. OK to proceed?', {
|
|
123
142
|
defaultAnswer: true
|
|
@@ -151,3 +170,5 @@ CherryPick.prototype.downloadAndPatch = LandingSession.prototype.downloadAndPatc
|
|
|
151
170
|
CherryPick.prototype.validateLint = LandingSession.prototype.validateLint;
|
|
152
171
|
CherryPick.prototype.getMessagePath = LandingSession.prototype.getMessagePath;
|
|
153
172
|
CherryPick.prototype.saveMessage = LandingSession.prototype.saveMessage;
|
|
173
|
+
CherryPick.prototype.generateAmendedMessage =
|
|
174
|
+
LandingSession.prototype.generateAmendedMessage;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import _ from 'lodash';
|
|
2
1
|
import chalk from 'chalk';
|
|
3
2
|
|
|
4
3
|
import { getMachineUrl, parsePRFromURL } from '../links.js';
|
|
@@ -15,6 +14,11 @@ import {
|
|
|
15
14
|
|
|
16
15
|
const { FAILURE_TYPES_NAME } = CIFailureParser;
|
|
17
16
|
|
|
17
|
+
function uniqBy(array, key) {
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
return array.filter((item) => !seen.has(item[key]) && seen.add(item[key]));
|
|
20
|
+
}
|
|
21
|
+
|
|
18
22
|
export class FailureAggregator {
|
|
19
23
|
constructor(cli, data) {
|
|
20
24
|
this.cli = cli;
|
|
@@ -24,37 +28,29 @@ export class FailureAggregator {
|
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
aggregate() {
|
|
27
|
-
const
|
|
28
|
-
const groupedByReason = _.chain(failures)
|
|
29
|
-
.groupBy(getHighlight)
|
|
30
|
-
.toPairs()
|
|
31
|
-
.sortBy(0)
|
|
32
|
-
.value();
|
|
31
|
+
const groupedByReason = Object.groupBy(this.failures, getHighlight);
|
|
33
32
|
const data = [];
|
|
34
|
-
for (const
|
|
35
|
-
const
|
|
33
|
+
for (const reason of Object.keys(groupedByReason).sort()) {
|
|
34
|
+
const failures = groupedByReason[reason];
|
|
36
35
|
// Uncomment this and redirect stderr away to see matched highlights
|
|
37
36
|
// console.log('HIGHLIGHT', reason);
|
|
38
37
|
|
|
39
38
|
// If multiple sub builds of one PR are failed by the same reason,
|
|
40
39
|
// we'll only take one of those builds, as that might be a genuine failure
|
|
41
|
-
const prs =
|
|
42
|
-
.
|
|
43
|
-
.
|
|
44
|
-
|
|
45
|
-
.value();
|
|
46
|
-
const machines = _.uniqBy(
|
|
40
|
+
const prs = uniqBy(failures, 'source')
|
|
41
|
+
.map(({ source, upstream }) => ({ source, upstream, _id: parseJobFromURL(upstream).jobid }))
|
|
42
|
+
.sort((a, b) => a._id - b._id);
|
|
43
|
+
const machines = uniqBy(
|
|
47
44
|
failures.map(f => ({ hostname: f.builtOn, url: f.url })),
|
|
48
45
|
'hostname');
|
|
49
46
|
data.push({
|
|
50
47
|
reason, type: failures[0].type, failures, prs, machines
|
|
51
48
|
});
|
|
52
|
-
}
|
|
49
|
+
}
|
|
53
50
|
|
|
54
|
-
const groupedByType =
|
|
55
|
-
for (const
|
|
56
|
-
|
|
57
|
-
_.sortBy(groupedByType[type], r => 0 - (r.prs.length));
|
|
51
|
+
const groupedByType = Object.groupBy(data, ({ type }) => type);
|
|
52
|
+
for (const group of Object.values(groupedByType)) {
|
|
53
|
+
group.sort((a, b) => b.prs.length - a.prs.length);
|
|
58
54
|
}
|
|
59
55
|
this.aggregates = groupedByType;
|
|
60
56
|
return groupedByType;
|
package/lib/ci/run_ci.js
CHANGED
package/lib/cli.js
CHANGED
|
@@ -94,6 +94,16 @@ export default class CLI {
|
|
|
94
94
|
return answer;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
async promptRadio(message, choices, opts = undefined) {
|
|
98
|
+
if (Object.hasOwn(opts, 'defaultAnswer')) {
|
|
99
|
+
opts.default = opts.defaultAnswer;
|
|
100
|
+
}
|
|
101
|
+
return inquirer.select({
|
|
102
|
+
...opts,
|
|
103
|
+
message, choices,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
97
107
|
async promptCheckbox(message, choices) {
|
|
98
108
|
if (this.assumeYes) {
|
|
99
109
|
return choices.filter((c) => c.checked).map((c) => c.value);
|
|
@@ -102,6 +112,20 @@ export default class CLI {
|
|
|
102
112
|
return inquirer.checkbox({ message, choices });
|
|
103
113
|
}
|
|
104
114
|
|
|
115
|
+
async promptSelect(message, choices, opts = {}) {
|
|
116
|
+
const defaultAnswer = opts.defaultAnswer ?? choices[0]?.value;
|
|
117
|
+
|
|
118
|
+
if (this.assumeYes) {
|
|
119
|
+
return defaultAnswer;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return inquirer.select({
|
|
123
|
+
message,
|
|
124
|
+
choices,
|
|
125
|
+
default: defaultAnswer
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
105
129
|
setAssumeYes() {
|
|
106
130
|
this.assumeYes = true;
|
|
107
131
|
}
|
|
@@ -193,7 +217,7 @@ export default class CLI {
|
|
|
193
217
|
setExitCode(statusCode) {
|
|
194
218
|
process.exitCode = statusCode;
|
|
195
219
|
}
|
|
196
|
-
}
|
|
220
|
+
}
|
|
197
221
|
|
|
198
222
|
CLI.SPINNER_STATUS = SPINNER_STATUS;
|
|
199
223
|
CLI.QUESTION_TYPE = QUESTION_TYPE;
|
package/lib/config.js
CHANGED
|
@@ -40,7 +40,7 @@ export function getMergedConfig(dir, home, additional) {
|
|
|
40
40
|
...Object.getOwnPropertyDescriptors(additional),
|
|
41
41
|
})
|
|
42
42
|
: mergedConfig;
|
|
43
|
-
}
|
|
43
|
+
}
|
|
44
44
|
export function clearCachedConfig() {
|
|
45
45
|
mergedConfig = null;
|
|
46
46
|
}
|
|
@@ -117,7 +117,7 @@ export function getConfig(configType, dir, raw = false) {
|
|
|
117
117
|
} catch (cause) {
|
|
118
118
|
throw new Error('Unable to parse config file ' + configPath, { cause });
|
|
119
119
|
}
|
|
120
|
-
}
|
|
120
|
+
}
|
|
121
121
|
|
|
122
122
|
function getConfigPath(configType, dir) {
|
|
123
123
|
switch (configType) {
|
|
@@ -135,7 +135,7 @@ function getConfigPath(configType, dir) {
|
|
|
135
135
|
default:
|
|
136
136
|
throw Error('Invalid configType');
|
|
137
137
|
}
|
|
138
|
-
}
|
|
138
|
+
}
|
|
139
139
|
|
|
140
140
|
function writeConfig(configType, obj, dir) {
|
|
141
141
|
const configPath = getConfigPath(configType, dir);
|
|
@@ -158,20 +158,20 @@ function writeConfig(configType, obj, dir) {
|
|
|
158
158
|
}
|
|
159
159
|
writeJson(configPath, obj);
|
|
160
160
|
return configPath;
|
|
161
|
-
}
|
|
161
|
+
}
|
|
162
162
|
|
|
163
163
|
export function updateConfig(configType, obj, dir) {
|
|
164
164
|
const config = getConfig(configType, dir, true);
|
|
165
165
|
writeConfig(configType, Object.assign(config, obj), dir);
|
|
166
|
-
}
|
|
166
|
+
}
|
|
167
167
|
|
|
168
168
|
export function getHomeDir(home) {
|
|
169
169
|
if (process.env.XDG_CONFIG_HOME) {
|
|
170
170
|
return process.env.XDG_CONFIG_HOME;
|
|
171
171
|
}
|
|
172
172
|
return home || os.homedir();
|
|
173
|
-
}
|
|
173
|
+
}
|
|
174
174
|
|
|
175
175
|
export function getNcuDir(dir) {
|
|
176
176
|
return path.join(dir || process.cwd(), '.ncu');
|
|
177
|
-
}
|
|
177
|
+
}
|