@@ -4,11 +4,17 @@ const path = require('path')
4
4
const fsp = require ( 'fs/promises' )
5
5
const process = require ( 'process' )
6
6
const execa = require ( 'execa' )
7
+ const { Octokit } = require ( 'octokit' )
7
8
const yargs = require ( 'yargs' )
8
9
9
10
/** @type {any } */
10
11
const fetch = require ( 'node-fetch' )
11
12
13
+ const repoOwner = 'vercel'
14
+ const repoName = 'next.js'
15
+ const pullRequestLabels = [ 'type: react-sync' ]
16
+ const pullRequestReviewers = [ 'eps1lon' ]
17
+
12
18
const filesReferencingReactPeerDependencyVersion = [
13
19
'run-tests.js' ,
14
20
'packages/create-next-app/templates/index.ts' ,
@@ -155,12 +161,50 @@ async function main() {
155
161
const errors = [ ]
156
162
const argv = await yargs ( process . argv . slice ( 2 ) )
157
163
. version ( false )
164
+ . options ( 'actor' , {
165
+ type : 'string' ,
166
+ description :
167
+ 'Required with `--create-pull`. The actor (GitHub username) that runs this script. Will be used for notifications but not commit attribution.' ,
168
+ } )
169
+ . options ( 'create-pull' , {
170
+ default : false ,
171
+ type : 'boolean' ,
172
+ description : 'Create a Pull Request in vercel/next.js' ,
173
+ } )
174
+ . options ( 'commit' , {
175
+ default : false ,
176
+ type : 'boolean' ,
177
+ description :
178
+ 'Creates commits for each intermediate step. Useful to create better diffs for GitHub.' ,
179
+ } )
158
180
. options ( 'install' , { default : true , type : 'boolean' } )
159
181
. options ( 'version' , { default : null , type : 'string' } ) . argv
160
- const { install, version } = argv
182
+ const { actor, createPull, commit, install, version } = argv
183
+
184
+ async function commitEverything ( message ) {
185
+ await execa ( 'git' , [ 'add' , '-A' ] )
186
+ await execa ( 'git' , [ 'commit' , '--message' , message , '--no-verify' ] )
187
+ }
188
+
189
+ if ( createPull && ! actor ) {
190
+ throw new Error (
191
+ `Pull Request cannot be created without a GitHub actor (received '${ String ( actor ) } '). ` +
192
+ 'Pass an actor via `--actor "some-actor"`.'
193
+ )
194
+ }
195
+ const githubToken = process . env . GITHUB_TOKEN
196
+ if ( createPull && ! githubToken ) {
197
+ throw new Error (
198
+ `Environment variable 'GITHUB_TOKEN' not specified but required when --create-pull is specified.`
199
+ )
200
+ }
161
201
162
202
let newVersionStr = version
163
- if ( newVersionStr === null ) {
203
+ if (
204
+ newVersionStr === null ||
205
+ // TODO: Fork arguments in GitHub workflow to ensure `--version ""` is considered a mistake
206
+ newVersionStr === ''
207
+ ) {
164
208
const { stdout, stderr } = await execa (
165
209
'npm' ,
166
210
[ 'view' , 'react@canary' , 'version' ] ,
@@ -174,6 +218,9 @@ async function main() {
174
218
throw new Error ( 'Failed to read latest React canary version from npm.' )
175
219
}
176
220
newVersionStr = stdout . trim ( )
221
+ console . log (
222
+ `--version was not provided. Using react@canary: ${ newVersionStr } `
223
+ )
177
224
}
178
225
179
226
const newVersionInfo = extractInfoFromReactVersion ( newVersionStr )
@@ -188,6 +235,37 @@ Or, run this command with no arguments to use the most recently published versio
188
235
)
189
236
}
190
237
const { sha : newSha , dateString : newDateString } = newVersionInfo
238
+
239
+ const branchName = `update/react/${ newSha } -${ newDateString } `
240
+ if ( createPull ) {
241
+ const { exitCode, all, command } = await execa (
242
+ 'git' ,
243
+ [
244
+ 'ls-remote' ,
245
+ '--exit-code' ,
246
+ '--heads' ,
247
+ 'origin' ,
248
+ `refs/heads/${ branchName } ` ,
249
+ ] ,
250
+ { reject : false }
251
+ )
252
+
253
+ if ( exitCode === 2 ) {
254
+ console . log (
255
+ `No sync in progress in branch '${ branchName } ' according to '${ command } '. Starting a new one.`
256
+ )
257
+ } else if ( exitCode === 0 ) {
258
+ console . log (
259
+ `An existing sync already exists in branch '${ branchName } '. Delete the branch to start a new sync.`
260
+ )
261
+ return
262
+ } else {
263
+ throw new Error (
264
+ `Failed to check if the branch already existed:\n${ command } : ${ all } `
265
+ )
266
+ }
267
+ }
268
+
191
269
const rootManifest = JSON . parse (
192
270
await fsp . readFile ( path . join ( cwd , 'package.json' ) , 'utf-8' )
193
271
)
@@ -203,13 +281,19 @@ Or, run this command with no arguments to use the most recently published versio
203
281
noInstall : ! install ,
204
282
channel : 'experimental' ,
205
283
} )
284
+ if ( commit ) {
285
+ await commitEverything ( 'Update `react@experimental`' )
286
+ }
206
287
await sync ( {
207
288
newDateString,
208
289
newSha,
209
290
newVersionStr,
210
291
noInstall : ! install ,
211
292
channel : 'rc' ,
212
293
} )
294
+ if ( commit ) {
295
+ await commitEverything ( 'Update `react@rc`' )
296
+ }
213
297
214
298
const baseVersionInfo = extractInfoFromReactVersion ( baseVersionStr )
215
299
if ( ! baseVersionInfo ) {
@@ -269,13 +353,22 @@ Or, run this command with no arguments to use the most recently published versio
269
353
)
270
354
}
271
355
356
+ if ( commit ) {
357
+ await commitEverything ( 'Updated peer dependency references' )
358
+ }
359
+
272
360
// Install the updated dependencies and build the vendored React files.
273
361
if ( ! install ) {
274
362
console . log ( 'Skipping install step because --no-install flag was passed.\n' )
275
363
} else {
276
364
console . log ( 'Installing dependencies...\n' )
277
365
278
- const installSubprocess = execa ( 'pnpm' , [ 'install' ] )
366
+ const installSubprocess = execa ( 'pnpm' , [
367
+ 'install' ,
368
+ // Pnpm freezes the lockfile by default in CI.
369
+ // However, we just changed versions so the lockfile is expected to be changed.
370
+ '--no-frozen-lockfile' ,
371
+ ] )
279
372
if ( installSubprocess . stdout ) {
280
373
installSubprocess . stdout . pipe ( process . stdout )
281
374
}
@@ -286,6 +379,10 @@ Or, run this command with no arguments to use the most recently published versio
286
379
throw new Error ( 'Failed to install updated dependencies.' )
287
380
}
288
381
382
+ if ( commit ) {
383
+ await commitEverything ( 'Update lockfile' )
384
+ }
385
+
289
386
console . log ( 'Building vendored React files...\n' )
290
387
const nccSubprocess = execa ( 'pnpm' , [ 'ncc-compiled' ] , {
291
388
cwd : path . join ( cwd , 'packages' , 'next' ) ,
@@ -300,34 +397,29 @@ Or, run this command with no arguments to use the most recently published versio
300
397
throw new Error ( 'Failed to run ncc.' )
301
398
}
302
399
400
+ if ( commit ) {
401
+ await commitEverything ( 'ncc-compiled' )
402
+ }
403
+
303
404
// Print extra newline after ncc output
304
405
console . log ( )
305
406
}
306
407
307
- console . log (
308
- `**breaking change for canary users: Bumps peer dependency of React from \`${ baseVersionStr } \` to \`${ newVersionStr } \`**`
309
- )
408
+ let prDescription = `**breaking change for canary users: Bumps peer dependency of React from \`${ baseVersionStr } \` to \`${ newVersionStr } \`**\n\n`
310
409
311
410
// Fetch the changelog from GitHub and print it to the console.
312
- console . log (
313
- `[diff facebook/react@${ baseSha } ...${ newSha } ](https://github.com/facebook/react/compare/${ baseSha } ...${ newSha } )`
314
- )
411
+ prDescription += `[diff facebook/react@${ baseSha } ...${ newSha } ](https://github.com/facebook/react/compare/${ baseSha } ...${ newSha } )\n\n`
315
412
try {
316
413
const changelog = await getChangelogFromGitHub ( baseSha , newSha )
317
414
if ( changelog === null ) {
318
- console . log (
319
- `GitHub reported no changes between ${ baseSha } and ${ newSha } .`
320
- )
415
+ prDescription += `GitHub reported no changes between ${ baseSha } and ${ newSha } .`
321
416
} else {
322
- console . log (
323
- `<details>\n<summary>React upstream changes</summary>\n\n${ changelog } \n\n</details>`
324
- )
417
+ prDescription += `<details>\n<summary>React upstream changes</summary>\n\n${ changelog } \n\n</details>`
325
418
}
326
419
} catch ( error ) {
327
420
console . error ( error )
328
- console . log (
421
+ prDescription +=
329
422
'\nFailed to fetch changelog from GitHub. Changes were applied, anyway.\n'
330
- )
331
423
}
332
424
333
425
if ( ! install ) {
@@ -343,13 +435,57 @@ Or run this command again without the --no-install flag to do both automatically
343
435
)
344
436
}
345
437
346
- await fsp . writeFile ( path . join ( cwd , '.github/.react-version' ) , newVersionStr )
347
-
348
438
if ( errors . length ) {
349
439
// eslint-disable-next-line no-undef -- Defined in Node.js
350
440
throw new AggregateError ( errors )
351
441
}
352
442
443
+ if ( createPull ) {
444
+ const octokit = new Octokit ( { auth : githubToken } )
445
+ const prTitle = `Upgrade React from \`${ baseSha } -${ baseDateString } \` to \`${ newSha } -${ newDateString } \``
446
+
447
+ await execa ( 'git' , [ 'checkout' , '-b' , branchName ] )
448
+ // We didn't commit intermediate steps yet so now we need to commit to create a PR.
449
+ if ( ! commit ) {
450
+ commitEverything ( prTitle )
451
+ }
452
+ await execa ( 'git' , [ 'push' , 'origin' , branchName ] )
453
+ const pullRequest = await octokit . rest . pulls . create ( {
454
+ owner : repoOwner ,
455
+ repo : repoName ,
456
+ head : branchName ,
457
+ base : 'canary' ,
458
+ draft : false ,
459
+ title : prTitle ,
460
+ body : prDescription ,
461
+ } )
462
+ console . log ( 'Created pull request %s' , pullRequest . data . html_url )
463
+
464
+ await Promise . all ( [
465
+ actor
466
+ ? octokit . rest . issues . addAssignees ( {
467
+ owner : repoOwner ,
468
+ repo : repoName ,
469
+ issue_number : pullRequest . data . number ,
470
+ assignees : [ actor ] ,
471
+ } )
472
+ : Promise . resolve ( ) ,
473
+ octokit . rest . pulls . requestReviewers ( {
474
+ owner : repoOwner ,
475
+ repo : repoName ,
476
+ pull_number : pullRequest . data . number ,
477
+ reviewers : pullRequestReviewers ,
478
+ } ) ,
479
+ octokit . rest . issues . addLabels ( {
480
+ owner : repoOwner ,
481
+ repo : repoName ,
482
+ issue_number : pullRequest . data . number ,
483
+ labels : pullRequestLabels ,
484
+ } ) ,
485
+ ] )
486
+ }
487
+
488
+ console . log ( prDescription )
353
489
console . log (
354
490
`Successfully updated React from \`${ baseSha } -${ baseDateString } \` to \`${ newSha } -${ newDateString } \``
355
491
)
0 commit comments