@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 +1 -1
- package/bin/ncu-ci.js +1 -1
- package/components/git/release.js +26 -8
- package/components/git/wpt.js +2 -1
- package/lib/cherry_pick.js +8 -3
- package/lib/ci/run_ci.js +2 -1
- package/lib/landing_session.js +4 -3
- package/lib/prepare_release.js +2 -0
- package/lib/prepare_security.js +1 -1
- package/lib/promote_release.js +146 -129
- package/lib/request.js +5 -1
- package/lib/session.js +3 -0
- package/lib/update_security_release.js +4 -0
- package/lib/voting_session.js +1 -2
- package/lib/wpt/index.js +7 -3
- package/package.json +1 -1
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>/
|
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
|
-
.
|
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
|
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
|
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
|
-
|
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
|
}
|
package/components/git/wpt.js
CHANGED
package/lib/cherry_pick.js
CHANGED
@@ -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
|
-
|
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(
|
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
|
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;
|
package/lib/landing_session.js
CHANGED
@@ -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
|
}
|
package/lib/prepare_release.js
CHANGED
@@ -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
|
});
|
package/lib/prepare_security.js
CHANGED
@@ -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(`
|
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`);
|
package/lib/promote_release.js
CHANGED
@@ -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
|
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.
|
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
|
47
|
-
const {
|
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',
|
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 } =
|
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 =
|
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
|
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
|
-
|
183
|
-
|
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
|
203
|
-
await this.pushToRemote(
|
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(
|
173
|
+
cli.ok('Release promotion(s) complete.\n');
|
210
174
|
cli.info(
|
211
175
|
'To finish this release, you\'ll need to: \n' +
|
212
|
-
|
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
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
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
|
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,
|
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
|
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
|
-
`${
|
310
|
+
`${releaseCommitSha}^..${releaseCommitSha}`,
|
352
311
|
'--',
|
353
|
-
`doc/changelogs/CHANGELOG_V${
|
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
|
-
|
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
|
-
|
376
|
-
|
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: `${
|
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(
|
444
|
-
const { cli, dryRun
|
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 ${
|
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 ${
|
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',
|
473
|
-
|
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
|
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,
|
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
|
-
|
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
|
|
package/lib/voting_session.js
CHANGED
@@ -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
|
-
|
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
|
-
|
83
|
-
|
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;
|