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