@node-core/utils 6.2.0 → 6.3.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/bin/ncu-ci.js CHANGED
@@ -119,6 +119,11 @@ const args = yargs(hideBin(process.argv))
119
119
  'If not provided, the command will use the SHA of the last approved commit.',
120
120
  type: 'string'
121
121
  })
122
+ .option('check-for-duplicates', {
123
+ describe: 'When set, NCU will query Jenkins recent builds to ensure ' +
124
+ 'there is not an existing job for the same commit.',
125
+ type: 'boolean'
126
+ })
122
127
  .option('owner', {
123
128
  default: '',
124
129
  describe: 'GitHub repository owner'
@@ -298,7 +303,10 @@ class RunPRJobCommand {
298
303
  this.cli.setExitCode(1);
299
304
  return;
300
305
  }
301
- const jobRunner = new RunPRJob(cli, request, owner, repo, prid, this.argv.certifySafe);
306
+ const { certifySafe, checkForDuplicates } = this.argv;
307
+ const jobRunner = new RunPRJob(
308
+ cli, request, owner, repo, prid,
309
+ certifySafe, checkForDuplicates);
302
310
  if (!(await jobRunner.start())) {
303
311
  this.cli.setExitCode(1);
304
312
  process.exitCode = 1;
@@ -168,7 +168,7 @@ async function main(state, argv, cli, dir) {
168
168
  github: true
169
169
  });
170
170
  const req = new Request(credentials);
171
- let session = new LandingSession(cli, req, dir);
171
+ let session = new LandingSession(cli, req, dir, argv);
172
172
 
173
173
  if (state !== AMEND &&
174
174
  state !== CONTINUE &&
@@ -189,6 +189,7 @@ async function main(state, argv, cli, dir) {
189
189
  cli.warn(
190
190
  'Failed to detect previous session. ' +
191
191
  'please run `git node land --abort`');
192
+ cli.setErrorCode(1);
192
193
  return;
193
194
  }
194
195
 
@@ -197,17 +198,12 @@ async function main(state, argv, cli, dir) {
197
198
  cli.warn(
198
199
  'Previous `git node land` session for ' +
199
200
  `${session.pullName} in progress.`);
200
- cli.log('run `git node land --abort` before starting a new session');
201
- return;
201
+ const response = await cli.prompt('Do you want to abort it?', { defaultAnswer: false });
202
+ if (!response) throw new Error('Please run `git node land --abort`');
203
+ await session.abort();
204
+ session = new LandingSession(cli, req, dir, argv);
202
205
  }
203
- session = new LandingSession(cli, req, dir, argv);
204
206
  const metadata = await getMetadata(session.argv, argv.skipRefs, cli);
205
- if (argv.backport) {
206
- const split = metadata.metadata.split('\n')[0];
207
- if (split === 'PR-URL: ') {
208
- cli.error('Commit message is missing PR-URL');
209
- }
210
- }
211
207
  return session.start(metadata);
212
208
  } else if (state === APPLY) {
213
209
  return session.apply();
@@ -92,110 +92,93 @@ export function builder(yargs) {
92
92
  }
93
93
 
94
94
  export function handler(argv) {
95
+ const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
96
+ const cli = new CLI(logStream);
97
+
95
98
  if (argv.start) {
96
- return startSecurityRelease(argv);
99
+ return startSecurityRelease(cli, argv);
97
100
  }
98
101
  if (argv.sync) {
99
- return syncSecurityRelease(argv);
102
+ return syncSecurityRelease(cli, argv);
100
103
  }
101
104
  if (argv['update-date']) {
102
- return updateReleaseDate(argv);
105
+ return updateReleaseDate(cli, argv);
103
106
  }
104
107
  if (argv['pre-release']) {
105
- return createPreRelease(argv);
108
+ return createPreRelease(cli, argv);
106
109
  }
107
110
  if (argv['add-report']) {
108
- return addReport(argv);
111
+ return addReport(cli, argv);
109
112
  }
110
113
  if (argv['remove-report']) {
111
- return removeReport(argv);
114
+ return removeReport(cli, argv);
112
115
  }
113
116
  if (argv['notify-pre-release']) {
114
- return notifyPreRelease(argv);
117
+ return notifyPreRelease(cli, argv);
115
118
  }
116
119
  if (argv['request-cve']) {
117
- return requestCVEs(argv);
120
+ return requestCVEs(cli, argv);
118
121
  }
119
122
  if (argv['post-release']) {
120
- return createPostRelease(argv);
123
+ return createPostRelease(cli, argv);
121
124
  }
122
125
  if (argv.cleanup) {
123
- return cleanupSecurityRelease(argv);
126
+ return cleanupSecurityRelease(cli, argv);
124
127
  }
125
128
  yargsInstance.showHelp();
126
129
  }
127
130
 
128
- async function removeReport(argv) {
131
+ async function removeReport(cli, argv) {
129
132
  const reportId = argv['remove-report'];
130
- const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
131
- const cli = new CLI(logStream);
132
133
  const update = new UpdateSecurityRelease(cli);
133
134
  return update.removeReport(reportId);
134
135
  }
135
136
 
136
- async function addReport(argv) {
137
+ async function addReport(cli, argv) {
137
138
  const reportId = argv['add-report'];
138
- const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
139
- const cli = new CLI(logStream);
140
139
  const update = new UpdateSecurityRelease(cli);
141
140
  return update.addReport(reportId);
142
141
  }
143
142
 
144
- async function updateReleaseDate(argv) {
143
+ async function updateReleaseDate(cli, argv) {
145
144
  const releaseDate = argv['update-date'];
146
- const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
147
- const cli = new CLI(logStream);
148
145
  const update = new UpdateSecurityRelease(cli);
149
146
  return update.updateReleaseDate(releaseDate);
150
147
  }
151
148
 
152
- async function createPreRelease(argv) {
149
+ async function createPreRelease(cli, argv) {
153
150
  const nodejsOrgFolder = argv['pre-release'];
154
- const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
155
- const cli = new CLI(logStream);
156
151
  const preRelease = new SecurityBlog(cli);
157
152
  return preRelease.createPreRelease(nodejsOrgFolder);
158
153
  }
159
154
 
160
- async function requestCVEs() {
161
- const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
162
- const cli = new CLI(logStream);
155
+ async function requestCVEs(cli) {
163
156
  const hackerOneCve = new UpdateSecurityRelease(cli);
164
157
  return hackerOneCve.requestCVEs();
165
158
  }
166
159
 
167
- async function createPostRelease(argv) {
160
+ async function createPostRelease(cli, argv) {
168
161
  const nodejsOrgFolder = argv['post-release'];
169
- const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
170
- const cli = new CLI(logStream);
171
162
  const blog = new SecurityBlog(cli);
172
163
  return blog.createPostRelease(nodejsOrgFolder);
173
164
  }
174
165
 
175
- async function startSecurityRelease() {
176
- const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
177
- const cli = new CLI(logStream);
166
+ async function startSecurityRelease(cli) {
178
167
  const release = new PrepareSecurityRelease(cli);
179
168
  return release.start();
180
169
  }
181
170
 
182
- async function cleanupSecurityRelease() {
183
- const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
184
- const cli = new CLI(logStream);
171
+ async function cleanupSecurityRelease(cli) {
185
172
  const release = new PrepareSecurityRelease(cli);
186
173
  return release.cleanup();
187
174
  }
188
175
 
189
- async function syncSecurityRelease(argv) {
190
- const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
191
- const cli = new CLI(logStream);
176
+ async function syncSecurityRelease(cli) {
192
177
  const release = new UpdateSecurityRelease(cli);
193
178
  return release.sync();
194
179
  }
195
180
 
196
- async function notifyPreRelease() {
197
- const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
198
- const cli = new CLI(logStream);
181
+ async function notifyPreRelease(cli) {
199
182
  const preRelease = new SecurityAnnouncement(cli);
200
183
  return preRelease.notifyPreRelease();
201
184
  }
package/lib/auth.js CHANGED
@@ -7,26 +7,21 @@ import { clearCachedConfig, encryptValue, getMergedConfig, getNcurcPath } from '
7
7
 
8
8
  export default lazy(auth);
9
9
 
10
- function errorExit(message) {
11
- process.stderr.write(`${message}\n`);
12
- process.exit(1);
13
- }
14
-
15
10
  function check(username, token, format = /^[A-Za-z0-9_]+$/) {
16
11
  if (typeof username !== 'string') {
17
- errorExit(`username must be a string, received ${typeof username}`);
12
+ throw new Error(`username must be a string, received ${typeof username}`);
18
13
  }
19
14
  if (!/^[a-zA-Z0-9-]+$/.test(username)) {
20
- errorExit(
15
+ throw new Error(
21
16
  'username may only contain alphanumeric characters or hyphens, ' +
22
17
  `received ${username}`
23
18
  );
24
19
  }
25
20
  if (typeof token !== 'string') {
26
- errorExit(`token must be a string, received ${typeof token}`);
21
+ throw new Error(`token must be a string, received ${typeof token}`);
27
22
  }
28
23
  if (!format.test(token)) {
29
- errorExit(`token is misformatted: ${token}`);
24
+ throw new Error(`token is misformatted: ${token}`);
30
25
  }
31
26
  }
32
27
 
@@ -51,7 +46,7 @@ async function tryCreateGitHubToken(githubAuth) {
51
46
  noDeviceFlow: true
52
47
  });
53
48
  } catch (e) {
54
- errorExit(`Could not get token: ${e.message}`);
49
+ throw new Error(`Could not get token: ${e.message}`, { cause: e });
55
50
  }
56
51
  return credentials;
57
52
  }
@@ -93,7 +88,7 @@ async function auth(
93
88
  get jenkins() {
94
89
  const { username, jenkins_token } = getMergedConfig();
95
90
  if (!username || !jenkins_token) {
96
- errorExit(
91
+ throw new Error(
97
92
  'Get your Jenkins API token in https://ci.nodejs.org/me/security ' +
98
93
  'and run the following command to add it to your ncu config: ' +
99
94
  'ncu-config --global set -x jenkins_token'
@@ -15,9 +15,8 @@ const {
15
15
  } = CIFailureParser;
16
16
 
17
17
  export class PRBuild extends TestBuild {
18
- constructor(cli, request, id, skipMoreThan) {
18
+ constructor(cli, request, id, skipMoreThan, tree = PR_TREE) {
19
19
  const path = `job/node-test-pull-request/${id}/`;
20
- const tree = PR_TREE;
21
20
  super(cli, request, path, tree);
22
21
  this.skipMoreThan = skipMoreThan;
23
22
  this.commitBuild = null;
package/lib/ci/run_ci.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { FormData } from 'undici';
2
2
 
3
3
  import {
4
+ JobParser,
4
5
  CI_DOMAIN,
5
6
  CI_TYPES,
6
7
  CI_TYPES_KEYS
@@ -8,6 +9,7 @@ import {
8
9
  import PRData from '../pr_data.js';
9
10
  import { debuglog } from '../verbosity.js';
10
11
  import PRChecker from '../pr_checker.js';
12
+ import { PRBuild } from './build-types/pr_build.js';
11
13
 
12
14
  export const CI_CRUMB_URL = `https://${CI_DOMAIN}/crumbIssuer/api/json`;
13
15
  const CI_PR_NAME = CI_TYPES.get(CI_TYPES_KEYS.PR).jobName;
@@ -17,7 +19,7 @@ const CI_V8_NAME = CI_TYPES.get(CI_TYPES_KEYS.V8).jobName;
17
19
  export const CI_V8_URL = `https://${CI_DOMAIN}/job/${CI_V8_NAME}/build`;
18
20
 
19
21
  export class RunPRJob {
20
- constructor(cli, request, owner, repo, prid, certifySafe) {
22
+ constructor(cli, request, owner, repo, prid, certifySafe, checkForDuplicates) {
21
23
  this.cli = cli;
22
24
  this.request = request;
23
25
  this.owner = owner;
@@ -29,6 +31,7 @@ export class RunPRJob {
29
31
  Promise.all([this.prData.getReviews(), this.prData.getCommits()]).then(() =>
30
32
  (this.certifySafe = new PRChecker(cli, this.prData, request, {}).getApprovedTipOfHead())
31
33
  );
34
+ this.checkForDuplicates = checkForDuplicates;
32
35
  }
33
36
 
34
37
  async getCrumb() {
@@ -70,7 +73,7 @@ export class RunPRJob {
70
73
  }
71
74
 
72
75
  async start() {
73
- const { cli, certifySafe } = this;
76
+ const { cli, request, certifySafe, checkForDuplicates } = this;
74
77
 
75
78
  if (!(await certifySafe)) {
76
79
  cli.error('Refusing to run CI on potentially unsafe PR');
@@ -87,9 +90,25 @@ export class RunPRJob {
87
90
  }
88
91
  cli.stopSpinner('Jenkins credentials valid');
89
92
 
93
+ if (checkForDuplicates) {
94
+ await this.prData.getComments();
95
+ const { jobid, link } = new JobParser(this.prData.comments).parse().get('PR') ?? {};
96
+ const { actions } = jobid
97
+ ? (await new PRBuild(cli, request, jobid, undefined, 'actions[parameters[name,value]]')
98
+ .getBuildData())
99
+ : {};
100
+ const { parameters } = actions?.find(a => 'parameters' in a) ?? {};
101
+ if (parameters?.find(c => c.name === 'COMMIT_SHA_CHECK')?.value === certifySafe) {
102
+ cli.info('Existing CI run found: ' + link);
103
+ cli.error('Refusing to start a potentially duplicate CI job. Use the ' +
104
+ '"Resume build" button in the Jenkins UI, or start a new CI manually.');
105
+ return false;
106
+ }
107
+ }
108
+
90
109
  try {
91
110
  cli.startSpinner('Starting PR CI job');
92
- const response = await this.request.fetch(CI_PR_URL, {
111
+ const response = await request.fetch(CI_PR_URL, {
93
112
  method: 'POST',
94
113
  headers: {
95
114
  'Jenkins-Crumb': crumb
@@ -106,10 +125,10 @@ export class RunPRJob {
106
125
 
107
126
  // check if the job need a v8 build and trigger it
108
127
  await this.prData.getPR();
109
- const labels = this.prData.pr.labels;
110
- if (labels.nodes.map(i => i.name).includes('v8 engine')) {
128
+ const { labels } = this.prData.pr;
129
+ if (labels?.nodes.some(i => i.name === 'v8 engine')) {
111
130
  cli.startSpinner('Starting V8 CI job');
112
- const response = await this.request.fetch(CI_V8_URL, {
131
+ const response = await request.fetch(CI_V8_URL, {
113
132
  method: 'POST',
114
133
  headers: {
115
134
  'Jenkins-Crumb': crumb
package/lib/config.js CHANGED
@@ -25,9 +25,21 @@ export function getMergedConfig(dir, home, additional) {
25
25
  const globalConfig = getConfig(GLOBAL_CONFIG, home);
26
26
  const projectConfig = getConfig(PROJECT_CONFIG, dir);
27
27
  const localConfig = getConfig(LOCAL_CONFIG, dir);
28
- mergedConfig = Object.assign(globalConfig, projectConfig, localConfig, additional);
28
+
29
+ // Using property descriptors to avoid calling any setter if e.g. a
30
+ // localConfig value overrides a globalConfig key.
31
+ mergedConfig = Object.create(null, {
32
+ ...Object.getOwnPropertyDescriptors(globalConfig),
33
+ ...Object.getOwnPropertyDescriptors(projectConfig),
34
+ ...Object.getOwnPropertyDescriptors(localConfig),
35
+ });
29
36
  }
30
- return mergedConfig;
37
+ return additional
38
+ ? Object.create(null, {
39
+ ...Object.getOwnPropertyDescriptors(mergedConfig),
40
+ ...Object.getOwnPropertyDescriptors(additional),
41
+ })
42
+ : mergedConfig;
31
43
  };
32
44
  export function clearCachedConfig() {
33
45
  mergedConfig = null;
@@ -82,12 +82,38 @@ export default class LandingSession extends Session {
82
82
  }
83
83
 
84
84
  async downloadAndPatch() {
85
- const { cli, upstream, prid, expectedCommitShas } = this;
85
+ const { cli, upstream, prid, expectedCommitShas, crossRepoPR } = this;
86
+
87
+ if (crossRepoPR) {
88
+ const { owner, repo } = this;
89
+ cli.warn(`The configured remote "${upstream}" does not point to the ` +
90
+ 'Pull Request repository.');
91
+ const isHeadAMergeCommit = () => {
92
+ try {
93
+ return runSync('git', ['rev-parse', 'FETCH_HEAD^2']).trim();
94
+ } catch {}
95
+ };
96
+ do {
97
+ cli.warn('Fetching commits from a different upstream might require user intervention.');
98
+
99
+ cli.info('Run the following command to fetch the commit(s):');
100
+ cli.info(`git fetch git@github.com:${owner}/${repo}.git refs/pull/${prid}/merge`);
101
+ const response = await cli.prompt('Should NCU run the command for you?',
102
+ { defaultAnswer: false });
103
+ if (response) {
104
+ cli.startSpinner(`Downloading patch for ${prid}`);
105
+ await forceRunAsync('git',
106
+ ['fetch', `git@github.com:${owner}/${repo}.git`, `refs/pull/${prid}/merge`],
107
+ { ignoreFailure: false });
108
+ }
109
+ } while (!isHeadAMergeCommit());
110
+ } else {
111
+ cli.startSpinner(`Downloading patch for ${prid}`);
112
+ await runAsync('git', [
113
+ 'fetch', upstream,
114
+ `refs/pull/${prid}/merge`]);
115
+ }
86
116
 
87
- cli.startSpinner(`Downloading patch for ${prid}`);
88
- await runAsync('git', [
89
- 'fetch', upstream,
90
- `refs/pull/${prid}/merge`]);
91
117
  // We fetched the commit that would result if we used `git merge`.
92
118
  // ^1 and ^2 refer to the PR base and the PR head, respectively.
93
119
  const [base, head] = await runAsync('git',
@@ -286,6 +312,12 @@ export default class LandingSession extends Session {
286
312
  cli.warn('Not yet ready to amend, run `git node land --abort`');
287
313
  return;
288
314
  }
315
+
316
+ const BACKPORT_RE = /^BACKPORT-PR-URL\s*:\s*(\S+)$/i;
317
+ const PR_RE = /^PR-URL\s*:\s*(\S+)$/i;
318
+ const REVIEW_RE = /^Reviewed-By\s*:\s*(\S+)$/i;
319
+ const CVE_RE = /^CVE-ID\s*:\s*(\S+)$/i;
320
+
289
321
  this.startAmending();
290
322
 
291
323
  const rev = this.getCurrentRev();
@@ -303,7 +335,12 @@ export default class LandingSession extends Session {
303
335
  }).trim().length !== 0;
304
336
 
305
337
  const metadata = this.metadata.trim().split('\n');
306
- const amended = original.split('\n');
338
+ // Filtering out metadata that we will add ourselves:
339
+ const isFiltered = line =>
340
+ (!PR_RE.test(line) || this.backport) &&
341
+ !BACKPORT_RE.test(line) &&
342
+ !REVIEW_RE.test(line);
343
+ const amended = original.split('\n').filter(isFiltered);
307
344
 
308
345
  // If the original commit message already contains trailers (such as
309
346
  // "Co-authored-by"), we simply add our own metadata after those. Otherwise,
@@ -313,35 +350,29 @@ export default class LandingSession extends Session {
313
350
  amended.push('');
314
351
  }
315
352
 
316
- const BACKPORT_RE = /BACKPORT-PR-URL\s*:\s*(\S+)/i;
317
- const PR_RE = /PR-URL\s*:\s*(\S+)/i;
318
- const REVIEW_RE = /Reviewed-By\s*:\s*(\S+)/i;
319
- const CVE_RE = /CVE-ID\s*:\s*(\S+)/i;
320
-
321
353
  let containCVETrailer = false;
322
354
  for (const line of metadata) {
323
- if (line.length !== 0 && original.includes(line)) {
324
- if (line.match(CVE_RE)) {
325
- containCVETrailer = true;
326
- }
355
+ if (line.length !== 0 && original.includes(line) && !isFiltered(line)) {
356
+ containCVETrailer ||= CVE_RE.test(line);
327
357
  if (originalHasTrailers) {
328
358
  cli.warn(`Found ${line}, skipping..`);
359
+ continue;
329
360
  } else {
330
361
  cli.error('Git found no trailers in the original commit message, ' +
331
362
  `but '${line}' is present and should be a trailer.`);
332
363
  process.exit(1); // make it work with git rebase -x
333
364
  }
334
- } else {
335
- if (line.match(BACKPORT_RE)) {
336
- let prIndex = amended.findIndex(datum => datum.match(PR_RE));
337
- if (prIndex === -1) {
338
- prIndex = amended.findIndex(datum => datum.match(REVIEW_RE)) - 1;
339
- }
340
- amended.splice(prIndex + 1, 0, line);
341
- } else {
342
- amended.push(line);
365
+ }
366
+ if (BACKPORT_RE.test(line)) {
367
+ const prIndex =
368
+ (amended.findIndex(datum => PR_RE.test(datum)) + 1) ||
369
+ amended.findIndex(datum => REVIEW_RE.test(datum));
370
+ if (prIndex !== -1) {
371
+ amended.splice(prIndex, 0, line);
372
+ continue;
343
373
  }
344
374
  }
375
+ amended.push(line);
345
376
  }
346
377
 
347
378
  if (!containCVETrailer && this.includeCVE) {
@@ -472,7 +503,7 @@ export default class LandingSession extends Session {
472
503
  }
473
504
 
474
505
  cli.separator();
475
- cli.log('The following commits are ready to be pushed to ' +
506
+ cli.log('The following commits are landable on ' +
476
507
  `${upstream}/${branch}`);
477
508
  cli.log(`- ${strayVerbose.join('\n- ')}`);
478
509
  cli.separator();
@@ -483,8 +514,15 @@ export default class LandingSession extends Session {
483
514
  willBeLanded = `${head}...${willBeLanded}`;
484
515
  }
485
516
 
517
+ cli.startSpinner('Removing temporary files');
486
518
  this.cleanFiles();
487
- cli.log('Temporary files removed.');
519
+ cli.stopSpinner('Temporary files removed.');
520
+ if (this.crossRepoPR) {
521
+ cli.warn(`The configured remote "${upstream}" does not point to the ` +
522
+ 'Pull Request repository.');
523
+ cli.info(`${willBeLanded} should likely not be pushed to "${upstream}"`);
524
+ return;
525
+ }
488
526
  cli.log('To finish landing:');
489
527
  cli.log('1. Run: ');
490
528
  cli.log(` git push ${upstream} ${branch}`);
@@ -31,7 +31,6 @@ export default class MetadataGenerator {
31
31
  const parser = new LinkParser(owner, repo, op);
32
32
  const fixes = parser.getFixes();
33
33
  const refs = parser.getRefs().filter(f => f !== prUrl);
34
- const altPrUrl = parser.getAltPrUrl();
35
34
 
36
35
  const meta = [];
37
36
 
@@ -44,17 +43,10 @@ export default class MetadataGenerator {
44
43
  meta.push(...refs.map((ref) => `Refs: ${ref}`));
45
44
  }
46
45
  const backport = this.argv ? this.argv.backport : undefined;
47
- if (backport) {
48
- meta.unshift(`Backport-PR-URL: ${prUrl}`);
49
- meta.unshift(`PR-URL: ${altPrUrl}`);
50
- } else {
51
- // Reviews are only added here as backports should not contain reviews
52
- // for the backport itself in the metadata
53
- meta.unshift(`PR-URL: ${prUrl}`);
54
- meta.push(
55
- ...reviewedBy.map((r) => `Reviewed-By: ${r.reviewer.getContact()}`)
56
- );
57
- }
46
+ meta.unshift(`${backport ? 'Backport-' : ''}PR-URL: ${prUrl}`);
47
+ meta.push(
48
+ ...reviewedBy.map((r) => `Reviewed-By: ${r.reviewer.getContact()}`)
49
+ );
58
50
  meta.push(''); // creates final EOL
59
51
  return meta.join('\n');
60
52
  }
@@ -133,88 +133,12 @@ export default class ReleasePreparation extends Session {
133
133
  return true;
134
134
  }
135
135
 
136
- async prepareSecurity() {
137
- const {
138
- cli,
139
- newVersion,
140
- versionComponents,
141
- releaseBranch,
142
- filterLabels
143
- } = this;
144
-
145
- // Create new proposal branch.
146
- cli.startSpinner(`Switching to proposal branch for ${newVersion}`);
147
- const proposalBranch = await this.createProposalBranch(releaseBranch);
148
- cli.stopSpinner(`Switched to proposal branch for ${newVersion}`);
149
-
150
- const success = await this.cherryPickSecurityPRs(filterLabels);
151
- if (!success) {
152
- cli.error('Aborting security release preparation. ' +
153
- 'Remember to exclude the proposal branch.' +
154
- 'git branch -D ' + proposalBranch);
155
- return;
156
- }
157
- // Update version and release info in src/node_version.h.
158
- cli.startSpinner(`Updating 'src/node_version.h' for ${newVersion}`);
159
- await this.updateNodeVersion();
160
- cli.stopSpinner(`Updated 'src/node_version.h' for ${newVersion}`);
161
-
162
- // Update any REPLACEME tags in the docs.
163
- cli.startSpinner('Updating REPLACEME items in docs');
164
- await this.updateREPLACEMEs();
165
- cli.stopSpinner('Updated REPLACEME items in docs');
166
-
167
- // Fetch date to use in release commit & changelogs.
168
- const todayDate = new Date().toISOString().split('T')[0];
169
- this.date = await cli.prompt('Enter release date in YYYY-MM-DD format:',
170
- { questionType: 'input', defaultAnswer: todayDate });
171
-
172
- cli.startSpinner('Updating CHANGELOG.md');
173
- await this.updateMainChangelog();
174
- cli.stopSpinner('Updated CHANGELOG.md');
175
-
176
- cli.startSpinner(`Updating CHANGELOG_V${versionComponents.major}.md`);
177
- await this.updateMajorChangelog();
178
- cli.stopSpinner(`Updated CHANGELOG_V${versionComponents.major}.md`);
179
-
180
- // Create release commit.
181
- const shouldCreateReleaseCommit = await cli.prompt(
182
- 'Create release commit?');
183
- if (!shouldCreateReleaseCommit) {
184
- cli.warn(`Aborting \`git node release\` for version ${newVersion}`);
185
- return;
186
- }
187
-
188
- // Proceed with release only after the releaser has amended
189
- // it to their liking.
190
- const createDefaultCommit = await this.createReleaseCommit();
191
- if (!createDefaultCommit) {
192
- const lastCommitSha = runSync('git', ['rev-parse', '--short', 'HEAD']);
193
- cli.warn(`Please manually edit commit ${lastCommitSha} by running ` +
194
- '`git commit --amend` before proceeding.');
195
- }
196
-
197
- cli.separator();
198
- cli.ok(`Release preparation for ${newVersion} complete.\n`);
199
- cli.info(
200
- 'To finish the release proposal, run: \n' +
201
- ` $ git push -u ${this.upstream} v${newVersion}-proposal\n` +
202
- 'Finally, proceed to Jenkins and begin the following CI jobs:\n' +
203
- ' * https://ci.nodejs.org/job/node-test-pull-request/\n' +
204
- ' * https://ci.nodejs.org/job/citgm-smoker/');
205
- cli.info(
206
- 'If this release has deps/v8 changes, you\'ll also need to run:\n' +
207
- ' * https://ci.nodejs.org/job/node-test-commit-v8-linux/'
208
- );
209
- }
210
-
211
136
  async prepare() {
212
137
  const { cli, newVersion, versionComponents, isSecurityRelease } = this;
213
138
 
214
139
  if (isSecurityRelease) {
215
140
  this.config.owner = 'nodejs-private';
216
141
  this.config.repo = 'node-private';
217
- return this.prepareSecurity();
218
142
  }
219
143
 
220
144
  const runBranchDiff = await cli.prompt(
@@ -222,30 +146,38 @@ export default class ReleasePreparation extends Session {
222
146
  '(recommended except for Maintenance releases)?',
223
147
  { defaultAnswer: this.runBranchDiff });
224
148
  if (runBranchDiff) {
225
- // TODO: UPDATE re-use
226
- // Check the branch diff to determine if the releaser
227
- // wants to backport any more commits before proceeding.
228
- cli.startSpinner('Fetching branch-diff');
229
- const raw = await this.getBranchDiff({
230
- onlyNotableChanges: false,
231
- comparisonBranch: newVersion
232
- });
233
-
234
- const diff = raw.split('*');
235
- cli.stopSpinner('Got branch diff');
236
-
237
- const outstandingCommits = diff.length - 1;
238
- if (outstandingCommits !== 0) {
239
- const proceed = await cli.prompt(
149
+ if (isSecurityRelease) {
150
+ const success = await this.cherryPickSecurityPRs(this.filterLabels);
151
+ if (!success) {
152
+ cli.error('Aborting security release preparation.');
153
+ return;
154
+ }
155
+ } else {
156
+ // TODO: UPDATE re-use
157
+ // Check the branch diff to determine if the releaser
158
+ // wants to backport any more commits before proceeding.
159
+ cli.startSpinner('Fetching branch-diff');
160
+ const raw = await this.getBranchDiff({
161
+ onlyNotableChanges: false,
162
+ comparisonBranch: newVersion
163
+ });
164
+
165
+ const diff = raw.split('*');
166
+ cli.stopSpinner('Got branch diff');
167
+
168
+ const outstandingCommits = diff.length - 1;
169
+ if (outstandingCommits !== 0) {
170
+ const proceed = await cli.prompt(
240
171
  `There are ${outstandingCommits} commits that may be ` +
241
172
  `backported to ${this.stagingBranch} - do you still want to proceed?`,
242
173
  { defaultAnswer: false });
243
174
 
244
- if (!proceed) {
245
- const seeDiff = await cli.prompt(
246
- 'Do you want to see the branch diff?');
247
- if (seeDiff) cli.log(raw);
248
- return;
175
+ if (!proceed) {
176
+ const seeDiff = await cli.prompt(
177
+ 'Do you want to see the branch diff?');
178
+ if (seeDiff) cli.log(raw);
179
+ return;
180
+ }
249
181
  }
250
182
  }
251
183
  }
@@ -203,23 +203,19 @@ export default class PrepareSecurityRelease extends SecurityRelease {
203
203
  }
204
204
 
205
205
  async createVulnerabilitiesJSON(reports, dependencies, releaseDate) {
206
- this.cli.separator('Creating vulnerabilities.json...');
207
- const file = JSON.stringify({
206
+ this.cli.startSpinner('Creating vulnerabilities.json...');
207
+ const fileContent = JSON.stringify({
208
208
  releaseDate,
209
209
  reports,
210
210
  dependencies
211
- }, null, 2);
211
+ }, null, 2) + '\n';
212
212
 
213
- const folderPath = path.join(process.cwd(), NEXT_SECURITY_RELEASE_FOLDER);
214
- try {
215
- fs.accessSync(folderPath);
216
- } catch (error) {
217
- fs.mkdirSync(folderPath, { recursive: true });
218
- }
213
+ const folderPath = path.resolve(NEXT_SECURITY_RELEASE_FOLDER);
214
+ await fs.promises.mkdir(folderPath, { recursive: true });
219
215
 
220
216
  const fullPath = path.join(folderPath, 'vulnerabilities.json');
221
- fs.writeFileSync(fullPath, file);
222
- this.cli.ok(`Created ${fullPath} `);
217
+ await fs.promises.writeFile(fullPath, fileContent);
218
+ this.cli.stopSpinner(`Created ${fullPath}`);
223
219
 
224
220
  return fullPath;
225
221
  }
package/lib/request.js CHANGED
@@ -32,6 +32,14 @@ export default class Request {
32
32
  if (url.startsWith(`https://${CI_DOMAIN}`)) {
33
33
  options.headers = options.headers || {};
34
34
  Object.assign(options.headers, this.getJenkinsHeaders());
35
+ } else if (!url.startsWith('https://')) {
36
+ url = new URL(url, 'https://api.github.com/').href;
37
+ options.headers = {
38
+ Authorization: `Basic ${this.credentials.github}`,
39
+ 'User-Agent': 'node-core-utils',
40
+ Accept: 'application/vnd.github+json',
41
+ ...options.headers
42
+ };
35
43
  }
36
44
  return wrappedFetch(url, options);
37
45
  }
@@ -65,14 +73,9 @@ export default class Request {
65
73
  }
66
74
 
67
75
  async createIssue(title, body, { owner, repo }) {
68
- const url = `https://api.github.com/repos/${owner}/${repo}/issues`;
76
+ const url = `/repos/${owner}/${repo}/issues`;
69
77
  const options = {
70
78
  method: 'POST',
71
- headers: {
72
- Authorization: `Basic ${this.credentials.github}`,
73
- 'User-Agent': 'node-core-utils',
74
- Accept: 'application/vnd.github+json'
75
- },
76
79
  body: JSON.stringify({
77
80
  title,
78
81
  body
@@ -82,15 +85,9 @@ export default class Request {
82
85
  }
83
86
 
84
87
  async commentIssue(fullUrl, comment) {
85
- const commentUrl = fullUrl.replace('https://github.com/', 'https://api.github.com/repos/') +
86
- '/comments';
88
+ const commentUrl = fullUrl.replace('https://github.com/', '/repos/') + '/comments';
87
89
  const options = {
88
90
  method: 'POST',
89
- headers: {
90
- Authorization: `Basic ${this.credentials.github}`,
91
- 'User-Agent': 'node-core-utils',
92
- Accept: 'application/vnd.github+json'
93
- },
94
91
  body: JSON.stringify({
95
92
  body: comment,
96
93
  })
@@ -99,27 +96,14 @@ export default class Request {
99
96
  }
100
97
 
101
98
  async getPullRequest(fullUrl) {
102
- const prUrl = fullUrl.replace('https://github.com/', 'https://api.github.com/repos/').replace('pull', 'pulls');
103
- const options = {
104
- method: 'GET',
105
- headers: {
106
- Authorization: `Basic ${this.credentials.github}`,
107
- 'User-Agent': 'node-core-utils',
108
- Accept: 'application/vnd.github+json'
109
- }
110
- };
111
- return this.json(prUrl, options);
99
+ const prUrl = fullUrl.replace('https://github.com/', '/repos/').replace('pull', 'pulls');
100
+ return this.json(prUrl);
112
101
  }
113
102
 
114
103
  async createPullRequest(title, body, { owner, repo, head, base }) {
115
- const url = `https://api.github.com/repos/${owner}/${repo}/pulls`;
104
+ const url = `/repos/${owner}/${repo}/pulls`;
116
105
  const options = {
117
106
  method: 'POST',
118
- headers: {
119
- Authorization: `Basic ${this.credentials.github}`,
120
- 'User-Agent': 'node-core-utils',
121
- Accept: 'application/vnd.github+json'
122
- },
123
107
  body: JSON.stringify({
124
108
  title,
125
109
  body,
@@ -131,13 +115,10 @@ export default class Request {
131
115
  }
132
116
 
133
117
  async closePullRequest(id, { owner, repo }) {
134
- const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${id}`;
118
+ const url = `/repos/${owner}/${repo}/pulls/${id}`;
135
119
  const options = {
136
120
  method: 'POST',
137
121
  headers: {
138
- Authorization: `Basic ${this.credentials.github}`,
139
- 'User-Agent': 'node-core-utils',
140
- Accept: 'application/vnd.github+json',
141
122
  'Content-Type': 'application/json'
142
123
  },
143
124
  body: JSON.stringify({
@@ -299,13 +280,11 @@ export default class Request {
299
280
  throw new Error('The request has not been ' +
300
281
  'authenticated with a GitHub token');
301
282
  }
302
- const url = 'https://api.github.com/graphql';
283
+ const url = '/graphql';
303
284
  const options = {
304
285
  agent: this.proxyAgent,
305
286
  method: 'POST',
306
287
  headers: {
307
- Authorization: `Basic ${githubCredentials}`,
308
- 'User-Agent': 'node-core-utils',
309
288
  Accept: 'application/vnd.github.antiope-preview+json'
310
289
  },
311
290
  body: JSON.stringify({
package/lib/session.js CHANGED
@@ -33,10 +33,11 @@ export default class Session {
33
33
  const upstreamHref = runSync('git', [
34
34
  'config', '--get',
35
35
  `remote.${upstream}.url`]).trim();
36
- if (!new RegExp(`${owner}/${repo}(?:.git)?$`).test(upstreamHref)) {
36
+ if (!upstreamHref.endsWith(`${owner}/${repo}.git`) &&
37
+ !upstreamHref.endsWith(`${owner}/${repo}`)) {
37
38
  cli.warn('Remote repository URL does not point to the expected ' +
38
39
  `repository ${owner}/${repo}`);
39
- cli.setExitCode(1);
40
+ this.crossRepoPR = true;
40
41
  }
41
42
  }
42
43
  }
@@ -72,14 +72,10 @@ export default class VotingSession extends Session {
72
72
  const subPath = `${prInfo.headRef.name}/vote.yml`;
73
73
  this.cli.startSpinner('Downloading vote file from remote...');
74
74
  const yamlString = await this.req.text(
75
- `https://api.github.com/repos/${this.owner}/${this.repo}/contents/${encodeURIComponent(subPath)}?ref=${prInfo.commits.nodes[0].commit.oid}`, {
76
- agent: this.req.proxyAgent,
77
- headers: {
78
- Authorization: `Basic ${this.req.credentials.github}`,
79
- 'User-Agent': 'node-core-utils',
80
- Accept: 'application/vnd.github.raw'
81
- }
82
- });
75
+ `/repos/${this.owner}/${this.repo}/contents/${
76
+ encodeURIComponent(subPath)}?ref=${prInfo.commits.nodes[0].commit.oid}`,
77
+ { agent: this.req.proxyAgent }
78
+ );
83
79
  this.cli.stopSpinner('Download complete');
84
80
 
85
81
  const { shares } = yaml.load(yamlString);
@@ -114,16 +110,15 @@ export default class VotingSession extends Session {
114
110
  const body = 'I would like to close this vote, and for this effect, I\'m revealing my ' +
115
111
  `key part:\n\n${'```'}\n${keyPart}\n${'```'}\n`;
116
112
  if (this.postComment) {
117
- const { message, html_url } = await this.req.json(`https://api.github.com/repos/${this.owner}/${this.repo}/issues/${this.prid}/comments`, {
118
- agent: this.req.proxyAgent,
119
- method: 'POST',
120
- headers: {
121
- Authorization: `Basic ${this.req.credentials.github}`,
122
- 'User-Agent': 'node-core-utils',
123
- Accept: 'application/vnd.github.antiope-preview+json'
124
- },
125
- body: JSON.stringify({ body })
126
- });
113
+ const { message, html_url } = await this.req.json(
114
+ `/repos/${this.owner}/${this.repo}/issues/${this.prid}/comments`,
115
+ {
116
+ agent: this.req.proxyAgent,
117
+ method: 'POST',
118
+ headers: { Accept: 'application/vnd.github.antiope-preview+json' },
119
+ body: JSON.stringify({ body })
120
+ }
121
+ );
127
122
  if (html_url) {
128
123
  this.cli.log(`Comment posted at: ${html_url}`);
129
124
  return;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@node-core/utils",
3
- "version": "6.2.0",
3
+ "version": "6.3.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@node-core/utils",
9
- "version": "6.2.0",
9
+ "version": "6.3.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@inquirer/prompts": "^7.4.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node-core/utils",
3
- "version": "6.2.0",
3
+ "version": "6.3.0",
4
4
  "description": "Utilities for Node.js core collaborators",
5
5
  "type": "module",
6
6
  "engines": {