@node-core/utils 5.13.0 → 5.14.1

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 CHANGED
@@ -98,7 +98,7 @@ these commands.
98
98
  To obtain the Jenkins API token
99
99
 
100
100
  1. Open
101
- `https://ci.nodejs.org/user/<your-github-username>/configure` (replace
101
+ `https://ci.nodejs.org/user/<your-github-username>/security` (replace
102
102
  \<your-github-username\> with your own GitHub username).
103
103
  2. Click on the `ADD NEW TOKEN` button in the `API Token` section.
104
104
  3. Enter an identifiable name (for example, `node-core-utils`) for this
package/bin/ncu-ci.js CHANGED
@@ -114,7 +114,7 @@ const args = yargs(hideBin(process.argv))
114
114
  describe: 'ID of the PR',
115
115
  type: 'number'
116
116
  })
117
- .positional('certify-safe', {
117
+ .option('certify-safe', {
118
118
  describe: 'SHA of the commit that is expected to be at the tip of the PR head. ' +
119
119
  'If not provided, the command will use the SHA of the last approved commit.',
120
120
  type: 'string'
@@ -6,7 +6,7 @@ import TeamInfo from '../../lib/team_info.js';
6
6
  import Request from '../../lib/request.js';
7
7
  import { runPromise } from '../../lib/run.js';
8
8
 
9
- export const command = 'release [prid|options]';
9
+ export const command = 'release [prid..]';
10
10
  export const describe = 'Manage an in-progress release or start a new one.';
11
11
 
12
12
  const PREPARE = 'prepare';
@@ -34,8 +34,13 @@ const releaseOptions = {
34
34
  describe: 'Promote new release of Node.js',
35
35
  type: 'boolean'
36
36
  },
37
+ fetchFrom: {
38
+ describe: 'Remote to fetch the release proposal(s) from, if different from the one where to' +
39
+ 'push the tags and commits.',
40
+ type: 'string',
41
+ },
37
42
  releaseDate: {
38
- describe: 'Default relase date when --prepare is used. It must be YYYY-MM-DD',
43
+ describe: 'Default release date when --prepare is used. It must be YYYY-MM-DD',
39
44
  type: 'string'
40
45
  },
41
46
  run: {
@@ -112,11 +117,6 @@ function release(state, argv) {
112
117
  }
113
118
 
114
119
  async function main(state, argv, cli, dir) {
115
- const prID = /^(?:https:\/\/github\.com\/nodejs(-private)?\/node\1\/pull\/)?(\d+)$/.exec(argv.prid);
116
- if (prID) {
117
- if (prID[1]) argv.security = true;
118
- argv.prid = Number(prID[2]);
119
- }
120
120
  if (state === PREPARE) {
121
121
  const release = new ReleasePreparation(argv, cli, dir);
122
122
 
@@ -160,6 +160,24 @@ async function main(state, argv, cli, dir) {
160
160
  cli.stopSpinner(`${release.username} is a Releaser`);
161
161
  }
162
162
 
163
- return release.promote();
163
+ const releases = [];
164
+ for (const pr of argv.prid) {
165
+ const match = /^(?:https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/)?(\d+)(?:#.*)?$/.exec(pr);
166
+ if (!match) throw new Error('Invalid PR ID or URL', { cause: pr });
167
+ const [,owner, repo, prid] = match;
168
+
169
+ if (
170
+ owner &&
171
+ (owner !== release.owner || repo !== release.repo) &&
172
+ !argv.fetchFrom
173
+ ) {
174
+ console.warn('The configured owner/repo does not match the PR URL.');
175
+ console.info('You should either pass `--fetch-from` flag or check your configuration');
176
+ console.info(`E.g. --fetch-from=git@github.com:${owner}/${repo}.git`);
177
+ throw new Error('You need to tell what remote use to fetch security release proposal.');
178
+ }
179
+ releases.push(await release.preparePromotion({ owner, repo, prid: Number(prid) }));
180
+ }
181
+ return release.promote(releases);
164
182
  }
165
183
  }
@@ -47,7 +47,8 @@ async function main(argv) {
47
47
  const statusFolder = path.join(nodedir, 'test', 'wpt', 'status');
48
48
  let supported = [
49
49
  'dom',
50
- 'html'
50
+ 'html',
51
+ 'webcrypto'
51
52
  ];
52
53
  if (fs.existsSync(statusFolder)) {
53
54
  const jsons = fs.readdirSync(statusFolder);
@@ -11,12 +11,16 @@ export default class CherryPick {
11
11
  constructor(prid, dir, cli, {
12
12
  owner,
13
13
  repo,
14
+ upstream,
15
+ gpgSign,
14
16
  lint,
15
17
  includeCVE
16
18
  } = {}) {
17
19
  this.prid = prid;
18
20
  this.cli = cli;
19
21
  this.dir = dir;
22
+ this.upstream = upstream;
23
+ this.gpgSign = gpgSign;
20
24
  this.options = { owner, repo, lint, includeCVE };
21
25
  }
22
26
 
@@ -88,14 +92,15 @@ export default class CherryPick {
88
92
  } else if (cleanLint === LINT_RESULTS.SUCCESS) {
89
93
  cli.ok('Lint passed cleanly');
90
94
  }
91
- return this.amend(metadata.metadata, commitInfo);
95
+ this.metadata = metadata.metadata;
96
+ return this.amend(commitInfo);
92
97
  } catch (e) {
93
98
  cli.error(e.message);
94
99
  return false;
95
100
  }
96
101
  }
97
102
 
98
- async amend(metadata, commitInfo) {
103
+ async amend(commitInfo) {
99
104
  const { cli } = this;
100
105
  const subjects = await runAsync('git',
101
106
  ['log', '--pretty=format:%s', `${commitInfo.base}..${commitInfo.head}`],
@@ -116,7 +121,7 @@ export default class CherryPick {
116
121
  await runAsync('git', ['commit', '--amend', '--no-edit']);
117
122
  }
118
123
 
119
- return LandingSession.prototype.amend.call(this, metadata);
124
+ return LandingSession.prototype.amend.call(this);
120
125
  }
121
126
 
122
127
  readyToAmend() {
package/lib/ci/run_ci.js CHANGED
@@ -62,7 +62,8 @@ export class RunPRJob {
62
62
  parameter: [
63
63
  { name: 'GITHUB_ORG', value: this.owner },
64
64
  { name: 'REPO_NAME', value: this.repo },
65
- { name: 'GIT_REMOTE_REF', value: `refs/pull/${this.prid}/head` }
65
+ { name: 'GIT_REMOTE_REF', value: `refs/pull/${this.prid}/head` },
66
+ { name: 'COMMIT_SHA_CHECK', value: this.certifySafe }
66
67
  ]
67
68
  }));
68
69
  return payload;
@@ -7,6 +7,7 @@ import Session from './session.js';
7
7
  import {
8
8
  shortSha, isGhAvailable, getEditor
9
9
  } from './utils.js';
10
+ import { debuglog, isDebugVerbosity } from './verbosity.js';
10
11
 
11
12
  const isWindows = process.platform === 'win32';
12
13
 
@@ -27,9 +28,6 @@ export default class LandingSession extends Session {
27
28
  this.lint = lint;
28
29
  this.autorebase = autorebase;
29
30
  this.fixupAll = fixupAll;
30
- this.gpgSign = argv?.['gpg-sign']
31
- ? (argv['gpg-sign'] === true ? ['-S'] : ['-S', argv['gpg-sign']])
32
- : [];
33
31
  this.oneCommitMax = oneCommitMax;
34
32
  this.expectedCommitShas = [];
35
33
  this.checkCI = !!checkCI;
@@ -120,6 +118,9 @@ export default class LandingSession extends Session {
120
118
  ['cherry-pick', '--allow-empty', ...this.gpgSign, `${base}..${head}`],
121
119
  { ignoreFailure: false });
122
120
  } catch (ex) {
121
+ if (isDebugVerbosity()) {
122
+ debuglog('[LandingSession] Got error', ex);
123
+ }
123
124
  cli.error('Failed to apply patches');
124
125
  process.exit(1);
125
126
  }
@@ -70,6 +70,8 @@ export default class ReleasePreparation extends Session {
70
70
  const cp = new CherryPick(pr.number, this.dir, cli, {
71
71
  owner: this.owner,
72
72
  repo: this.repo,
73
+ gpgSign: this.gpgSign,
74
+ upstream: this.isSecurityRelease ? `https://${this.username}:${this.config.token}@github.com/${this.owner}/${this.repo}.git` : this.upstream,
73
75
  lint: false,
74
76
  includeCVE: true
75
77
  });
@@ -70,7 +70,7 @@ export default class PrepareSecurityRelease extends SecurityRelease {
70
70
  { cli: this.cli, repository: this.repository }
71
71
  );
72
72
  }
73
- this.cli.info(`Merge pull request with:
73
+ this.cli.info(`If the PR is ready (CI is passing): merge pull request with:
74
74
  - git checkout main
75
75
  - git merge ${NEXT_SECURITY_RELEASE_BRANCH} --no-ff -m "chore: add latest security release"
76
76
  - git push origin main`);
@@ -17,19 +17,10 @@ const dryRunMessage = 'You are running in dry-run mode, meaning NCU will not run
17
17
 
18
18
  export default class ReleasePromotion extends Session {
19
19
  constructor(argv, req, cli, dir) {
20
- super(cli, dir, argv.prid);
20
+ super(cli, dir, null, argv);
21
21
  this.req = req;
22
- if (argv.security) {
23
- this.config.owner = 'nodejs-private';
24
- this.config.repo = 'node-private';
25
- }
26
22
  this.dryRun = !argv.run;
27
- this.isLTS = false;
28
- this.ltsCodename = '';
29
- this.date = '';
30
- this.gpgSign = argv?.['gpg-sign']
31
- ? (argv['gpg-sign'] === true ? ['-S'] : ['-S', argv['gpg-sign']])
32
- : [];
23
+ this.proposalUpstreamRemote = argv.fetchFrom ?? this.upstream;
33
24
  }
34
25
 
35
26
  get branch() {
@@ -43,8 +34,8 @@ export default class ReleasePromotion extends Session {
43
34
  return defaultBranchRef.name;
44
35
  }
45
36
 
46
- async promote() {
47
- const { prid, cli } = this;
37
+ async preparePromotion({ prid, owner, repo }) {
38
+ const { cli, proposalUpstreamRemote } = this;
48
39
 
49
40
  // In the promotion stage, we can pull most relevant data
50
41
  // from the release commit created in the preparation stage.
@@ -54,9 +45,7 @@ export default class ReleasePromotion extends Session {
54
45
  isApproved,
55
46
  jenkinsReady,
56
47
  releaseCommitSha
57
- } = await this.verifyPRAttributes();
58
-
59
- this.releaseCommitSha = releaseCommitSha;
48
+ } = await this.verifyPRAttributes({ prid, owner, repo });
60
49
 
61
50
  let localCloneIsClean = true;
62
51
  const currentHEAD = await forceRunAsync('git', ['rev-parse', 'HEAD'],
@@ -74,7 +63,7 @@ export default class ReleasePromotion extends Session {
74
63
  if (!localCloneIsClean) {
75
64
  if (await cli.prompt('Should we reset the local HEAD to be the release proposal?')) {
76
65
  cli.startSpinner('Fetching the proposal upstream...');
77
- await forceRunAsync('git', ['fetch', this.upstream, releaseCommitSha],
66
+ await forceRunAsync('git', ['fetch', proposalUpstreamRemote, releaseCommitSha],
78
67
  { ignoreFailure: false });
79
68
  await forceRunAsync('git', ['reset', releaseCommitSha, '--hard'], { ignoreFailure: false });
80
69
  cli.stopSpinner('Local HEAD is now in sync with the proposal');
@@ -84,9 +73,9 @@ export default class ReleasePromotion extends Session {
84
73
  }
85
74
  }
86
75
 
87
- await this.parseDataFromReleaseCommit();
76
+ const releaseData = await this.parseDataFromReleaseCommit(releaseCommitSha);
88
77
 
89
- const { version } = this;
78
+ const { version, isLTS, ltsCodename, date, versionComponents } = releaseData;
90
79
  cli.startSpinner('Verifying Jenkins CI status');
91
80
  if (!jenkinsReady) {
92
81
  cli.stopSpinner(
@@ -134,102 +123,79 @@ export default class ReleasePromotion extends Session {
134
123
  cli.warn(`Aborting release promotion for version ${version}`);
135
124
  throw new Error('Aborted');
136
125
  }
137
- await this.secureTagRelease();
138
- await this.verifyTagSignature();
126
+ await this.secureTagRelease({ version, isLTS, ltsCodename, releaseCommitSha, date });
127
+ await this.verifyTagSignature(version);
139
128
 
140
129
  // Set up for next release.
141
130
  cli.startSpinner('Setting up for next release');
142
- const workingOnNewReleaseCommit = await this.setupForNextRelease();
131
+ const workingOnNewReleaseCommit =
132
+ await this.setupForNextRelease({ prid, owner, repo, versionComponents });
143
133
  cli.stopSpinner('Successfully set up for next release');
144
134
 
135
+ const shouldRebaseStagingBranch = await cli.prompt(
136
+ 'Rebase staging branch on top of the release commit?', { defaultAnswer: true });
137
+ const tipOfStagingBranch = shouldRebaseStagingBranch
138
+ ? await this.rebaseRemoteBranch(
139
+ `v${versionComponents.major}.x-staging`,
140
+ workingOnNewReleaseCommit)
141
+ : workingOnNewReleaseCommit;
142
+
143
+ return { releaseCommitSha, workingOnNewReleaseCommit, tipOfStagingBranch, ...releaseData };
144
+ }
145
+
146
+ async promote(releases) {
147
+ const { cli } = this;
148
+
145
149
  // Cherry pick release commit to master.
146
150
  const shouldCherryPick = await cli.prompt(
147
- 'Cherry-pick release commit to the default branch?', { defaultAnswer: true });
151
+ 'Cherry-pick release commit(s) to the default branch?', { defaultAnswer: true });
148
152
  if (!shouldCherryPick) {
149
- cli.warn(`Aborting release promotion for version ${version}`);
150
153
  throw new Error('Aborted');
151
154
  }
152
- const appliedCleanly = await this.cherryPickToDefaultBranch();
153
-
154
- // Ensure `node_version.h`'s `NODE_VERSION_IS_RELEASE` bit is not updated
155
- await forceRunAsync('git', ['checkout',
156
- appliedCleanly
157
- ? 'HEAD^' // In the absence of conflict, the top of the remote branch is the commit before.
158
- : 'HEAD', // In case of conflict, HEAD is still the top of the remove branch.
159
- '--', 'src/node_version.h'],
160
- { ignoreFailure: false });
161
-
162
- if (appliedCleanly) {
163
- // There were no conflicts, we have to amend the commit to revert the
164
- // `node_version.h` changes.
165
- await forceRunAsync('git', ['commit', ...this.gpgSign, '--amend', '--no-edit', '-n'],
166
- { ignoreFailure: false });
167
- } else {
168
- // There will be remaining cherry-pick conflicts the Releaser will
169
- // need to resolve, so confirm they've been resolved before
170
- // proceeding with next steps.
171
- cli.separator();
172
- cli.info('Resolve the conflicts and commit the result');
173
- cli.separator();
174
- const didResolveConflicts = await cli.prompt(
175
- 'Finished resolving cherry-pick conflicts?', { defaultAnswer: true });
176
- if (!didResolveConflicts) {
177
- cli.warn(`Aborting release promotion for version ${version}`);
178
- throw new Error('Aborted');
179
- }
180
- }
155
+ const defaultBranch = await this.checkoutDefaultBranch();
181
156
 
182
- if (existsSync('.git/CHERRY_PICK_HEAD')) {
183
- cli.info('Cherry-pick is still in progress, attempting to continue it.');
184
- await forceRunAsync('git', ['cherry-pick', ...this.gpgSign, '--continue'],
185
- { ignoreFailure: false });
186
- }
187
-
188
- // Validate release commit on the default branch
189
- const releaseCommitOnDefaultBranch =
190
- await forceRunAsync('git', ['show', 'HEAD', '--name-only', '--pretty=format:%s'],
191
- { captureStdout: true, ignoreFailure: false });
192
- const [commitTitle, ...modifiedFiles] = releaseCommitOnDefaultBranch.trim().split('\n');
193
- await this.validateReleaseCommit(commitTitle);
194
- if (modifiedFiles.some(file => !file.endsWith('.md'))) {
195
- cli.warn('Some modified files are not markdown, that\'s unusual.');
196
- cli.info(`The list of modified files: ${modifiedFiles.map(f => `- ${f}`).join('\n')}`);
197
- if (!await cli.prompt('Do you want to proceed anyway?', { defaultAnswer: false })) {
198
- throw new Error('Aborted');
199
- }
157
+ for (const { releaseCommitSha } of releases) {
158
+ await this.cherryPickReleaseCommit(releaseCommitSha);
200
159
  }
201
160
 
202
- // Push to the remote the release tag, and default, release, and staging branch.
203
- await this.pushToRemote(workingOnNewReleaseCommit);
161
+ // Push to the remote the release tag(s), and default, release, and staging branches.
162
+ await this.pushToRemote(this.upstream, defaultBranch, ...releases.flatMap(
163
+ ({ version, versionComponents, workingOnNewReleaseCommit, tipOfStagingBranch }) => [
164
+ `v${version}`,
165
+ `${workingOnNewReleaseCommit}:refs/heads/v${versionComponents.major}.x`,
166
+ `+${tipOfStagingBranch}:refs/heads/v${versionComponents.major}.x-staging`,
167
+ ]));
204
168
 
205
169
  // Promote and sign the release builds.
206
170
  await this.promoteAndSignRelease();
207
171
 
208
172
  cli.separator();
209
- cli.ok(`Release promotion for ${version} complete.\n`);
173
+ cli.ok('Release promotion(s) complete.\n');
210
174
  cli.info(
211
175
  'To finish this release, you\'ll need to: \n' +
212
- ` 1. Check the release at: https://nodejs.org/dist/v${version}\n` +
213
- ' 2. Create the blog post for nodejs.org.\n' +
214
- ' 3. Create the release on GitHub.\n' +
215
- ' 4. Optionally, announce the release on your social networks.\n' +
176
+ ' 1. Check the release(s) at: https://nodejs.org/dist/v{version}\n' +
177
+ ' 2. Create the blog post(s) for nodejs.org.\n' +
178
+ ' 3. Create the release(s) on GitHub.\n' +
179
+ ' 4. Optionally, announce the release(s) on your social networks.\n' +
216
180
  ' 5. Tag @nodejs-social-team on #nodejs-release Slack channel.\n');
217
181
 
218
182
  cli.separator();
219
- cli.info('Use the following command to create the GitHub release:');
183
+ cli.info('Use the following command(s) to create the GitHub release(s):');
220
184
  cli.separator();
221
- cli.info(
222
- 'awk \'' +
223
- `/^## ${this.date}, Version ${this.version.replaceAll('.', '\\.')} /,` +
224
- '/^<a id="[0-9]+\\.[0-9]+\\.[0-9]+"><\\x2fa>$/{' +
225
- 'print buf; if(firstLine == "") firstLine = $0; else buf = $0' +
226
- `}' doc/changelogs/CHANGELOG_V${
227
- this.versionComponents.major}.md | gh release create v${this.version} --verify-tag --latest${
228
- this.isLTS ? '=false' : ''} --title=${JSON.stringify(this.releaseTitle)} --notes-file -`);
185
+ for (const { date, version, versionComponents, isLTS, releaseTitle } of releases) {
186
+ cli.info(
187
+ 'awk \'' +
188
+ `/^## ${date}, Version ${version.replaceAll('.', '\\.')} /,` +
189
+ '/^<a id="[0-9]+\\.[0-9]+\\.[0-9]+"><\\x2fa>$/{' +
190
+ 'print buf; if(firstLine == "") firstLine = $0; else buf = $0' +
191
+ `}' doc/changelogs/CHANGELOG_V${
192
+ versionComponents.major}.md | gh release create v${version} --verify-tag --latest${
193
+ isLTS ? '=false' : ''} --title=${JSON.stringify(releaseTitle)} --notes-file -`);
194
+ }
229
195
  }
230
196
 
231
- async verifyTagSignature() {
232
- const { cli, version } = this;
197
+ async verifyTagSignature(version) {
198
+ const { cli } = this;
233
199
  const verifyTagPattern = /gpg:[^\n]+\ngpg:\s+using RSA key ([^\n]+)\ngpg:\s+issuer "([^"]+)"\ngpg:\s+Good signature from "([^<]+) <\2>"/;
234
200
  const [verifyTagOutput, haystack] = await Promise.all([forceRunAsync(
235
201
  'git', ['--no-pager',
@@ -258,8 +224,8 @@ export default class ReleasePromotion extends Session {
258
224
  }
259
225
  }
260
226
 
261
- async verifyPRAttributes() {
262
- const { cli, prid, owner, repo, req } = this;
227
+ async verifyPRAttributes({ prid, owner, repo }) {
228
+ const { cli, req } = this;
263
229
 
264
230
  const data = new PRData({ prid, owner, repo }, cli, req);
265
231
  await data.getAll();
@@ -325,8 +291,8 @@ export default class ReleasePromotion extends Session {
325
291
  return data;
326
292
  }
327
293
 
328
- async parseDataFromReleaseCommit() {
329
- const { cli, releaseCommitSha } = this;
294
+ async parseDataFromReleaseCommit(releaseCommitSha) {
295
+ const { cli } = this;
330
296
 
331
297
  const releaseCommitMessage = await forceRunAsync('git', [
332
298
  '--no-pager', 'log', '-1',
@@ -338,26 +304,19 @@ export default class ReleasePromotion extends Session {
338
304
 
339
305
  const releaseCommitData = await this.validateReleaseCommit(releaseCommitMessage);
340
306
 
341
- this.date = releaseCommitData.date;
342
- this.version = releaseCommitData.version;
343
- this.stagingBranch = releaseCommitData.stagingBranch;
344
- this.versionComponents = releaseCommitData.versionComponents;
345
- this.isLTS = releaseCommitData.isLTS;
346
- this.ltsCodename = releaseCommitData.ltsCodename;
347
-
348
307
  // Check if CHANGELOG show the correct releaser for the current release
349
308
  const changeLogDiff = await forceRunAsync('git', [
350
309
  '--no-pager', 'diff',
351
- `${this.releaseCommitSha}^..${this.releaseCommitSha}`,
310
+ `${releaseCommitSha}^..${releaseCommitSha}`,
352
311
  '--',
353
- `doc/changelogs/CHANGELOG_V${this.versionComponents.major}.md`
312
+ `doc/changelogs/CHANGELOG_V${releaseCommitData.versionComponents.major}.md`
354
313
  ], { captureStdout: true, ignoreFailure: false });
355
314
  const headingLine = /^\+## \d{4}-\d{2}-\d{2}, Version \d.+$/m.exec(changeLogDiff);
356
315
  if (headingLine == null) {
357
316
  cli.error('Cannot find section for the new release in CHANGELOG');
358
317
  throw new Error('Aborted');
359
318
  }
360
- this.releaseTitle = headingLine[0].slice(4);
319
+ releaseCommitData.releaseTitle = headingLine[0].slice(4);
361
320
  const expectedLine = `+## ${releaseCommitMessage}, @${this.username}`;
362
321
  if (headingLine[0] !== expectedLine &&
363
322
  !headingLine[0].startsWith(`${expectedLine} prepared by @`)) {
@@ -370,11 +329,11 @@ export default class ReleasePromotion extends Session {
370
329
  throw new Error('Aborted');
371
330
  }
372
331
  }
373
- }
374
332
 
375
- async secureTagRelease() {
376
- const { version, isLTS, ltsCodename, releaseCommitSha } = this;
333
+ return releaseCommitData;
334
+ }
377
335
 
336
+ async secureTagRelease({ version, isLTS, ltsCodename, releaseCommitSha, date }) {
378
337
  const releaseInfo = isLTS ? `${ltsCodename} (LTS)` : '(Current)';
379
338
 
380
339
  try {
@@ -382,7 +341,7 @@ export default class ReleasePromotion extends Session {
382
341
  const api = new gst.API(process.cwd());
383
342
  api.sign(`v${version}`, releaseCommitSha, {
384
343
  insecure: false,
385
- m: `${this.date} Node.js v${version} ${releaseInfo} Release`
344
+ m: `${date} Node.js v${version} ${releaseInfo} Release`
386
345
  }, (err) => err ? reject(err) : resolve());
387
346
  });
388
347
  } catch (err) {
@@ -401,9 +360,7 @@ export default class ReleasePromotion extends Session {
401
360
 
402
361
  // Set up the branch so that nightly builds are produced with the next
403
362
  // version number and a pre-release tag.
404
- async setupForNextRelease() {
405
- const { versionComponents, prid, owner, repo } = this;
406
-
363
+ async setupForNextRelease({ prid, owner, repo, versionComponents }) {
407
364
  // Update node_version.h for next patch release.
408
365
  const filePath = path.resolve('src', 'node_version.h');
409
366
  const nodeVersionFile = await fs.open(filePath, 'r+');
@@ -440,22 +397,16 @@ export default class ReleasePromotion extends Session {
440
397
  return workingOnNewReleaseCommit.trim();
441
398
  }
442
399
 
443
- async pushToRemote(workingOnNewReleaseCommit) {
444
- const { cli, dryRun, version, versionComponents, stagingBranch } = this;
445
- const releaseBranch = `v${versionComponents.major}.x`;
446
- const tagVersion = `v${version}`;
400
+ async pushToRemote(upstream, ...refs) {
401
+ const { cli, dryRun } = this;
447
402
 
448
403
  this.defaultBranch ??= await this.getDefaultBranch();
449
404
 
450
- let prompt = `Push release tag and commits to ${this.upstream}?`;
405
+ let prompt = `Push release tag and commits to ${upstream}?`;
451
406
  if (dryRun) {
452
407
  cli.info(dryRunMessage);
453
408
  cli.info('Run the following command to push to remote:');
454
- cli.info(`git push ${this.upstream} ${
455
- this.defaultBranch} ${
456
- tagVersion} ${
457
- workingOnNewReleaseCommit}:refs/heads/${releaseBranch} ${
458
- workingOnNewReleaseCommit}:refs/heads/${stagingBranch}`);
409
+ cli.info(`git push ${upstream} ${refs.join(' ')}`);
459
410
  cli.warn('Once pushed, you must not delete the local tag');
460
411
  prompt = 'Ready to continue?';
461
412
  }
@@ -469,12 +420,8 @@ export default class ReleasePromotion extends Session {
469
420
  }
470
421
 
471
422
  cli.startSpinner('Pushing to remote');
472
- await forceRunAsync('git', ['push', this.upstream, this.defaultBranch, tagVersion,
473
- `${workingOnNewReleaseCommit}:refs/heads/${releaseBranch}`,
474
- `${workingOnNewReleaseCommit}:refs/heads/${stagingBranch}`],
475
- { ignoreFailure: false });
476
- cli.stopSpinner(`Pushed ${tagVersion}, ${this.defaultBranch}, ${
477
- releaseBranch}, and ${stagingBranch} to remote`);
423
+ await forceRunAsync('git', ['push', upstream, ...refs], { ignoreFailure: false });
424
+ cli.stopSpinner(`Pushed ${JSON.stringify(refs)} to remote`);
478
425
  cli.warn('Now that it has been pushed, you must not delete the local tag');
479
426
  }
480
427
 
@@ -507,21 +454,91 @@ export default class ReleasePromotion extends Session {
507
454
  cli.stopSpinner('Release has been signed and promoted');
508
455
  }
509
456
 
510
- async cherryPickToDefaultBranch() {
457
+ async rebaseRemoteBranch(branchName, workingOnNewReleaseCommit) {
458
+ const { cli, upstream } = this;
459
+ cli.startSpinner('Fetch staging branch');
460
+ await forceRunAsync('git', ['fetch', upstream, branchName], { ignoreFailure: false });
461
+ cli.updateSpinner('Reset and rebase');
462
+ await forceRunAsync('git', ['reset', 'FETCH_HEAD', '--hard'], { ignoreFailure: false });
463
+ await forceRunAsync('git',
464
+ ['rebase', workingOnNewReleaseCommit, ...this.gpgSign], { ignoreFailure: false });
465
+ const tipOfStagingBranch = await forceRunAsync('git', ['rev-parse', 'HEAD'],
466
+ { ignoreFailure: false, captureStdout: true });
467
+ cli.stopSpinner('Rebased successfully');
468
+
469
+ return tipOfStagingBranch.trim();
470
+ }
471
+
472
+ async checkoutDefaultBranch() {
511
473
  this.defaultBranch ??= await this.getDefaultBranch();
512
- const releaseCommitSha = this.releaseCommitSha;
513
474
  await forceRunAsync('git', ['checkout', this.defaultBranch], { ignoreFailure: false });
514
475
 
515
476
  await this.tryResetBranch();
516
477
 
478
+ return this.defaultBranch;
479
+ }
480
+
481
+ async cherryPick(commit) {
517
482
  // There might be conflicts, we do not want to treat this as a hard failure,
518
483
  // but we want to retain that information.
519
484
  try {
520
- await forceRunAsync('git', ['cherry-pick', ...this.gpgSign, releaseCommitSha],
485
+ await forceRunAsync('git', ['cherry-pick', ...this.gpgSign, commit],
521
486
  { ignoreFailure: false });
522
487
  return true;
523
488
  } catch {
524
489
  return false;
525
490
  }
526
491
  }
492
+
493
+ async cherryPickReleaseCommit(releaseCommitSha) {
494
+ const { cli } = this;
495
+
496
+ const appliedCleanly = await this.cherryPick(releaseCommitSha);
497
+ // Ensure `node_version.h`'s `NODE_VERSION_IS_RELEASE` bit is not updated
498
+ await forceRunAsync('git', ['checkout',
499
+ appliedCleanly
500
+ ? 'HEAD^' // In the absence of conflict, the top of the remote branch is the commit before.
501
+ : 'HEAD', // In case of conflict, HEAD is still the top of the remove branch.
502
+ '--', 'src/node_version.h'],
503
+ { ignoreFailure: false });
504
+
505
+ if (appliedCleanly) {
506
+ // There were no conflicts, we have to amend the commit to revert the
507
+ // `node_version.h` changes.
508
+ await forceRunAsync('git', ['commit', ...this.gpgSign, '--amend', '--no-edit', '-n'],
509
+ { ignoreFailure: false });
510
+ } else {
511
+ // There will be remaining cherry-pick conflicts the Releaser will
512
+ // need to resolve, so confirm they've been resolved before
513
+ // proceeding with next steps.
514
+ cli.separator();
515
+ cli.info('Resolve the conflicts and commit the result');
516
+ cli.separator();
517
+ const didResolveConflicts = await cli.prompt(
518
+ 'Finished resolving cherry-pick conflicts?', { defaultAnswer: true });
519
+ if (!didResolveConflicts) {
520
+ throw new Error('Aborted');
521
+ }
522
+ }
523
+
524
+ if (existsSync('.git/CHERRY_PICK_HEAD')) {
525
+ cli.info('Cherry-pick is still in progress, attempting to continue it.');
526
+ await forceRunAsync('git', ['cherry-pick', ...this.gpgSign, '--continue'],
527
+ { ignoreFailure: false });
528
+ }
529
+
530
+ // Validate release commit on the default branch
531
+ const releaseCommitOnDefaultBranch =
532
+ await forceRunAsync('git', ['show', 'HEAD', '--name-only', '--pretty=format:%s'],
533
+ { captureStdout: true, ignoreFailure: false });
534
+ const [commitTitle, ...modifiedFiles] = releaseCommitOnDefaultBranch.trim().split('\n');
535
+ await this.validateReleaseCommit(commitTitle);
536
+ if (modifiedFiles.some(file => !file.endsWith('.md'))) {
537
+ cli.warn('Some modified files are not markdown, that\'s unusual.');
538
+ cli.info(`The list of modified files: ${modifiedFiles.map(f => `- ${f}`).join('\n')}`);
539
+ if (!await cli.prompt('Do you want to proceed anyway?', { defaultAnswer: false })) {
540
+ throw new Error('Aborted');
541
+ }
542
+ }
543
+ }
527
544
  }
package/lib/request.js CHANGED
@@ -43,7 +43,11 @@ export default class Request {
43
43
  }
44
44
 
45
45
  async text(url, options = {}) {
46
- return this.fetch(url, options).then(res => res.text());
46
+ const res = await this.fetch(url, options);
47
+ if (isDebugVerbosity()) {
48
+ debuglog('[Request] Got response from', url, ':\n', res.status, ' ', res.statusText);
49
+ }
50
+ return res.text();
47
51
  }
48
52
 
49
53
  async json(url, options = {}) {
package/lib/session.js CHANGED
@@ -20,6 +20,9 @@ export default class Session {
20
20
  this.dir = dir;
21
21
  this.prid = prid;
22
22
  this.config = { ...getMergedConfig(this.dir), ...argv };
23
+ this.gpgSign = argv?.['gpg-sign']
24
+ ? (argv['gpg-sign'] === true ? ['-S'] : ['-S', argv['gpg-sign']])
25
+ : [];
23
26
 
24
27
  if (warnForMissing) {
25
28
  const { upstream, owner, repo } = this;
@@ -46,6 +46,10 @@ export default class UpdateSecurityRelease extends SecurityRelease {
46
46
  }
47
47
  const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
48
48
  fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2));
49
+
50
+ const commitMessage = 'chore: git node security --sync';
51
+ commitAndPushVulnerabilitiesJSON([vulnerabilitiesJSONPath],
52
+ commitMessage, { cli: this.cli, repository: this.repository });
49
53
  this.cli.ok('Synced vulnerabilities.json with HackerOne');
50
54
  }
51
55
 
@@ -29,7 +29,6 @@ export default class VotingSession extends Session {
29
29
  this.abstain = abstain;
30
30
  this.closeVote = argv['decrypt-key-part'];
31
31
  this.postComment = argv['post-comment'];
32
- this.gpgSign = argv['gpg-sign'];
33
32
  }
34
33
 
35
34
  get argv() {
@@ -111,7 +110,7 @@ export default class VotingSession extends Session {
111
110
  out.toString('base64') +
112
111
  '\n-----END SHAMIR KEY PART-----';
113
112
  this.cli.log('Your key part is:');
114
- this.cli.log(keyPart);
113
+ console.log(keyPart); // Using `console.log` so this gets output to stdout, not stderr.
115
114
  const body = 'I would like to close this vote, and for this effect, I\'m revealing my ' +
116
115
  `key part:\n\n${'```'}\n${keyPart}\n${'```'}\n`;
117
116
  if (this.postComment) {
package/lib/wpt/index.js CHANGED
@@ -79,9 +79,13 @@ export class WPTUpdater {
79
79
  await removeDirectory(this.fixtures(this.path));
80
80
 
81
81
  this.cli.startSpinner('Pulling assets...');
82
- await Promise.all(assets.map(
83
- (asset) => this.pullTextFile(fixtures, asset.name)
84
- ));
82
+ // See https://github.com/nodejs/node-core-utils/issues/810
83
+ for (let i = 0; i < assets.length; i += 10) {
84
+ const chunk = assets.slice(i, i + 10);
85
+ await Promise.all(chunk.map(
86
+ (asset) => this.pullTextFile(fixtures, asset.name)
87
+ ));
88
+ }
85
89
  this.cli.stopSpinner(`Downloaded ${assets.length} assets.`);
86
90
 
87
91
  return assets;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node-core/utils",
3
- "version": "5.13.0",
3
+ "version": "5.14.1",
4
4
  "description": "Utilities for Node.js core collaborators",
5
5
  "type": "module",
6
6
  "engines": {