@@ -5,76 +5,292 @@ const { promises: fs } = require('fs');
5
5
const semver = require ( 'semver' ) ;
6
6
7
7
const { getMergedConfig } = require ( './config' ) ;
8
- const { runAsync, runSync } = require ( './run' ) ;
9
- const { writeJson, readJson } = require ( './file' ) ;
8
+ const { runSync } = require ( './run' ) ;
9
+ const auth = require ( '../lib/auth' ) ;
10
+ const PRData = require ( '../lib/pr_data' ) ;
11
+ const PRChecker = require ( '../lib/pr_checker' ) ;
12
+ const Request = require ( '../../lib/request' ) ;
10
13
11
14
const isWindows = process . platform === 'win32' ;
12
15
13
- class ReleasePreparation {
16
+ class ReleasePromotion {
14
17
constructor ( argv , cli , dir ) {
15
18
this . cli = cli ;
16
19
this . dir = dir ;
17
- this . isSecurityRelease = argv . security ;
18
- this . isLTS = await this . checkIsLTS ;
20
+ this . isLTS = false ;
21
+ this . prid = argv . prid ;
19
22
this . ltsCodename = '' ;
20
23
this . date = '' ;
21
24
this . config = getMergedConfig ( this . dir ) ;
22
25
}
23
26
24
27
async promote ( ) {
25
- // Verify that CI is green
28
+ const { version , prid , cli } = this ;
26
29
27
- // Verify that the PR has at least one approval from a Releaser
30
+ // In the promotion stage, we can pull most relevant data
31
+ // from the release commit created in the preparation stage.
32
+ await this . parseDataFromReleaseCommit ( ) ;
28
33
29
- //
30
- }
34
+ // Verify that PR is ready to promote.
35
+ cli . startSpinner ( 'Verifying PR promotion readiness' ) ;
36
+ const {
37
+ jenkinsReady,
38
+ githubCIReady,
39
+ isApproved
40
+ } = await this . verifyPRAttributes ( ) ;
41
+ if ( ! jenkinsReady ) {
42
+ cli . stopSpinner ( `Jenkins CI is failing for #${ prid } ` ) ;
43
+ const proceed = await cli . prompt ( 'Do you want to proceed?' ) ;
44
+ if ( ! proceed ) {
45
+ cli . warn ( `Aborting release promotion for version ${ version } ` ) ;
46
+ return ;
47
+ }
48
+ } else if ( ! githubCIReady ) {
49
+ cli . stopSpinner ( `GitHub CI is failing for #${ prid } ` ) ;
50
+ const proceed = await cli . prompt ( 'Do you want to proceed?' ) ;
51
+ if ( ! proceed ) {
52
+ cli . warn ( `Aborting release promotion for version ${ version } ` ) ;
53
+ return ;
54
+ }
55
+ } else if ( ! isApproved ) {
56
+ cli . stopSpinner ( `#${ prid } does not have sufficient approvals` ) ;
57
+ const proceed = await cli . prompt ( 'Do you want to proceed?' ) ;
58
+ if ( ! proceed ) {
59
+ cli . warn ( `Aborting release promotion for version ${ version } ` ) ;
60
+ return ;
61
+ }
62
+ }
63
+ cli . stopSpinner ( `The release PR for ${ version } is ready to promote!` ) ;
31
64
32
- async checkIsLTS ( ) {
33
- const filePath = path . resolve ( 'src' , 'node_version.h' ) ;
34
- const data = await fs . readFile ( filePath , 'utf8' ) ;
35
- const arr = data . split ( '\n' ) ;
65
+ // Create and sign the release tag.
66
+ const shouldTagAndSignRelease = await cli . prompt (
67
+ 'Tag and sign the release?' ) ;
68
+ if ( ! shouldTagAndSignRelease ) {
69
+ cli . warn ( `Aborting release promotion for version ${ version } ` ) ;
70
+ return ;
71
+ }
72
+ this . secureTagRelease ( ) ;
36
73
37
- for ( let idx = 0 ; idx < arr . length ; idx ++ ) {
38
- const line = arr [ idx ] ;
39
- if ( line . includes ( '#define NODE_VERSION_IS_LTS' ) ) {
40
- return line . split ( ' ' ) [ 2 ] === '1' ;
41
- }
74
+ // Set up for next release.
75
+ cli . startSpinner ( 'Setting up for next release' ) ;
76
+ await this . setupForNextRelease ( ) ;
77
+ cli . startSpinner ( 'Successfully set up for next release' ) ;
78
+
79
+ const shouldMergeProposalBranch = await cli . prompt (
80
+ 'Merge proposal branch into staging branch?' ) ;
81
+ if ( ! shouldMergeProposalBranch ) {
82
+ cli . warn ( `Aborting release promotion for version ${ version } ` ) ;
83
+ return ;
84
+ }
85
+
86
+ // Merge vX.Y.Z-proposal into vX.x.
87
+ cli . startSpinner ( 'Merging proposal branch' ) ;
88
+ await this . mergeProposalBranch ( ) ;
89
+ cli . startSpinner ( 'Merged proposal branch' ) ;
90
+
91
+ // Cherry pick release commit to master.
92
+ const shouldCherryPick = await cli . prompt (
93
+ 'Cherry-pick release commit to master?' , { defaultAnswer : true } ) ;
94
+ if ( ! shouldCherryPick ) {
95
+ cli . warn ( `Aborting release promotion for version ${ version } ` ) ;
96
+ return ;
97
+ }
98
+ await this . cherryPickToMaster ( ) ;
99
+
100
+ // Push release tag.
101
+ const shouldPushTag = await cli . prompt ( 'Push release tag?' ,
102
+ { defaultAnswer : true } ) ;
103
+ if ( ! shouldPushTag ) {
104
+ cli . warn ( `Aborting release promotion for version ${ version } ` ) ;
105
+ return ;
42
106
}
107
+ this . pushReleaseTag ( ) ;
108
+
109
+ // Promote and sign the release builds.
110
+ const shouldPromote = await cli . prompt ( 'Promote and sign release builds?' ,
111
+ { defaultAnswer : true } ) ;
112
+ if ( ! shouldPromote ) {
113
+ cli . warn ( `Aborting release promotion for version ${ version } ` ) ;
114
+ return ;
115
+ }
116
+
117
+ const defaultKeyPath = '~/.ssh/node_id_rsa' ;
118
+ const keyPath = await cli . prompt (
119
+ `Please enter the path to your ssh key (Default ${ defaultKeyPath } ): ` ,
120
+ { questionType : 'input' , defaultAnswer : defaultKeyPath } ) ;
121
+ this . promoteAndSignRelease ( keyPath ) ;
122
+
123
+ cli . separator ( ) ;
124
+ cli . ok ( `Release promotion for ${ version } complete.\n` ) ;
125
+ cli . info (
126
+ 'To finish this release, you\'ll need to: \n' +
127
+ ` 1) Check the release at: https://nodejs.org/dist/v${ version } \n` +
128
+ ' 2) Create the blog post for nodejs.org\n' +
129
+ ' 3) Create the release on GitHub\n' +
130
+ 'Finally, proceed to Twitter and announce the new release!' ) ;
43
131
}
44
132
45
- // Returns the LTS codename for the Release line; e.g 'Erbium' for 12.x.
46
- async checkReleaseCodename ( ) {
47
- const filePath = path . resolve ( 'src' , 'node_version.h' ) ;
48
- const data = await fs . readFile ( filePath , 'utf8' ) ;
49
- const arr = data . split ( '\n' ) ;
133
+ async verifyPRAttributes ( ) {
134
+ const { cli, prid, owner, repo } = this ;
50
135
51
- for ( let idx = 0 ; idx < arr . length ; idx ++ ) {
52
- const line = arr [ idx ] ;
53
- if ( line . includes ( '#define NODE_VERSION_LTS_CODENAME' ) ) {
54
- return line . split ( ' ' ) [ 2 ] ;
55
- }
136
+ const credentials = await auth ( { github : true } ) ;
137
+ const request = new Request ( credentials ) ;
138
+
139
+ const data = new PRData ( { prid, owner, repo } , cli , request ) ;
140
+ await data . getAll ( ) ;
141
+
142
+ const checker = new PRChecker ( cli , data , { prid, owner, repo } ) ;
143
+ const jenkinsReady = checker . checkJenkinsCI ( ) ;
144
+ const githubCIReady = checker . checkGitHubCI ( ) ;
145
+ const isApproved = checker . checkReviewsAndWait ( false /* checkComments */ ) ;
146
+
147
+ return {
148
+ jenkinsReady,
149
+ githubCIReady,
150
+ isApproved
151
+ } ;
152
+ }
153
+
154
+ async parseDataFromReleaseCommit ( ) {
155
+ const { cli } = this ;
156
+
157
+ const releaseCommitMessage = runSync (
158
+ 'git' , [ 'log' , '-n' , '1' , '--pretty=format:\'%s\'' ] ) . trim ( ) ;
159
+
160
+ const components = releaseCommitMessage . split ( ' ' ) ;
161
+
162
+ // Parse out release date.
163
+ if ( ! / \d { 4 } - \d { 2 } - \d { 2 } / . match ( components [ 0 ] ) ) {
164
+ cli . error ( `Release commit contains invalid date: ${ components [ 0 ] } ` ) ;
165
+ return ;
166
+ }
167
+ this . date = components [ 0 ] ;
168
+
169
+ // Parse out release version.
170
+ const version = semver . clean ( components [ 2 ] ) ;
171
+ if ( ! semver . valid ( version ) ) {
172
+ cli . error ( `Release commit contains invalid semantic version: ${ version } ` ) ;
173
+ return ;
174
+ }
175
+
176
+ this . version = version ;
177
+ this . stagingBranch = `v${ semver . major ( version ) } .x-staging` ;
178
+ this . versionComponents = {
179
+ major : semver . major ( version ) ,
180
+ minor : semver . minor ( version ) ,
181
+ patch : semver . patch ( version )
182
+ } ;
183
+
184
+ // Parse out LTS status and codename.
185
+ if ( components . length === 5 ) {
186
+ this . isLTS = true ;
187
+ this . ltsCodename = components [ 3 ] ;
56
188
}
189
+ }
190
+
191
+ getCommitSha ( position = 0 ) {
192
+ return runSync ( 'git' , [ 'rev-parse' , `HEAD~${ position } ` ] ) ;
193
+ }
194
+
195
+ get owner ( ) {
196
+ return this . config . owner || 'nodejs' ;
197
+ }
198
+
199
+ get repo ( ) {
200
+ return this . config . repo || 'node' ;
201
+ }
202
+
203
+ secureTagRelease ( ) {
204
+ const { version, isLTS, ltsCodename } = this ;
205
+
206
+ const secureTag = path . join (
207
+ __dirname ,
208
+ '../node_modules/.bin/git-secure-tag' + ( isWindows ? '.cmd' : '' )
209
+ ) ;
210
+
211
+ const releaseInfo = isLTS ? `'${ ltsCodename } ' (LTS)` : '(Current)' ;
212
+ const secureTagOptions = [
213
+ `v${ version } ` ,
214
+ this . getCommitSha ( ) ,
215
+ '-sm' ,
216
+ `"${ this . date } Node.js v${ version } ${ releaseInfo } Release"`
217
+ ] ;
57
218
58
-
219
+ return runSync ( secureTag , secureTagOptions ) ;
59
220
}
60
221
61
222
// Set up the branch so that nightly builds are produced with the next
62
223
// version number and a pre-release tag.
63
224
async setupForNextRelease ( ) {
64
- const { versionComponents } = this ;
225
+ const { versionComponents, prid } = this ;
65
226
227
+ // Update node_version.h for next patch release.
66
228
const filePath = path . resolve ( 'src' , 'node_version.h' ) ;
67
229
const data = await fs . readFile ( filePath , 'utf8' ) ;
68
230
const arr = data . split ( '\n' ) ;
69
231
232
+ const patchVersion = versionComponents . patch + 1 ;
70
233
arr . forEach ( ( line , idx ) => {
71
234
if ( line . includes ( '#define NODE_PATCH_VERSION' ) ) {
72
- arr [ idx ] = `#define NODE_PATCH_VERSION ${ versionComponents . patch + 1 } ` ;
235
+ arr [ idx ] = `#define NODE_PATCH_VERSION ${ patchVersion } ` ;
73
236
} else if ( line . includes ( '#define NODE_VERSION_IS_RELEASE' ) ) {
74
237
arr [ idx ] = '#define NODE_VERSION_IS_RELEASE 0' ;
75
238
}
76
239
} ) ;
77
240
78
241
await fs . writeFile ( filePath , arr . join ( '\n' ) ) ;
242
+
243
+ const workingOnVersion =
244
+ `${ versionComponents . major } .${ versionComponents . minor } .${ patchVersion } ` ;
245
+
246
+ // Create 'Working On' commit.
247
+ runSync ( 'git' , [ 'add' , filePath ] ) ;
248
+ return runSync ( 'git' , [
249
+ 'commit' ,
250
+ '-m' ,
251
+ `Working on ${ workingOnVersion } ` ,
252
+ '-m' ,
253
+ `PR-URL: https://github.com/nodejs/node/pull/${ prid } `
254
+ ] ) ;
255
+ }
256
+
257
+ async mergeProposalBranch ( ) {
258
+ const { stagingBranch, versionComponents, version } = this ;
259
+
260
+ const releaseBranch = `v${ versionComponents . major } .x` ;
261
+ const proposalBranch = `v${ version } -proposal` ;
262
+
263
+ runSync ( 'git' , [ 'checkout' , releaseBranch ] ) ;
264
+ runSync ( 'git' , [ 'merge' , '--ff-only' , proposalBranch ] ) ;
265
+ runSync ( 'git' , [ 'push' , 'upstream' , releaseBranch ] ) ;
266
+ runSync ( 'git' , [ 'checkout' , stagingBranch ] ) ;
267
+ runSync ( 'git' , [ 'rebase' , releaseBranch ] ) ;
268
+ runSync ( 'git' , [ 'push' , 'upstream' , stagingBranch ] ) ;
269
+ }
270
+
271
+ pushReleaseTag ( ) {
272
+ const { version } = this ;
273
+
274
+ const tagVersion = `v${ version } ` ;
275
+ return runSync ( 'git' , [ 'push' , 'upstream' , tagVersion ] ) ;
276
+ }
277
+
278
+ promoteAndSignRelease ( keyPath ) {
279
+ return runSync ( './tools/release.sh' , [ '-i' , keyPath ] ) ;
280
+ }
281
+
282
+ async cherryPickToMaster ( ) {
283
+ // Since we've committed the Working On commit,
284
+ // the release commit will be 1 removed from
285
+ // tip-of-tree (e.g HEAD~1).
286
+ const releaseCommitSha = this . getCommitSha ( 1 ) ;
287
+ runSync ( 'git' , [ 'checkout' , 'master' ] ) ;
288
+
289
+ // There will be conflicts.
290
+ runSync ( 'git' , [ 'cherry-pick' , releaseCommitSha ] ) ;
291
+ // TODO(codebytere): gracefully handle conflicts and
292
+ // wait for the releaser to resolve.
79
293
}
80
294
}
295
+
296
+ module . exports = ReleasePromotion ;
0 commit comments