@@ -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,36 @@ 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 (
241
+ 'git' ,
242
+ [
243
+ 'ls-remote' ,
244
+ '--exit-code' ,
245
+ '--heads' ,
246
+ 'origin' ,
247
+ `refs/heads/${ branchName } ` ,
248
+ ] ,
249
+ { reject : false }
250
+ )
251
+
252
+ if ( exitCode === 2 ) {
253
+ console . log (
254
+ `No sync in progress in branch '${ branchName } ' according to '${ command } '. Starting a new one.`
255
+ )
256
+ } else if ( exitCode === 0 ) {
257
+ throw new Error (
258
+ `An existing sync already exists in branch '${ branchName } '. Delete the branch to start a new sync.`
259
+ )
260
+ } else {
261
+ throw new Error (
262
+ `Failed to check if the branch already existed:\n${ command } : ${ all } `
263
+ )
264
+ }
265
+ }
266
+
191
267
const rootManifest = JSON . parse (
192
268
await fsp . readFile ( path . join ( cwd , 'package.json' ) , 'utf-8' )
193
269
)
@@ -203,13 +279,19 @@ Or, run this command with no arguments to use the most recently published versio
203
279
noInstall : ! install ,
204
280
channel : 'experimental' ,
205
281
} )
282
+ if ( commit ) {
283
+ await commitEverything ( 'Update `react@experimental`' )
284
+ }
206
285
await sync ( {
207
286
newDateString,
208
287
newSha,
209
288
newVersionStr,
210
289
noInstall : ! install ,
211
290
channel : 'rc' ,
212
291
} )
292
+ if ( commit ) {
293
+ await commitEverything ( 'Update `react@rc`' )
294
+ }
213
295
214
296
const baseVersionInfo = extractInfoFromReactVersion ( baseVersionStr )
215
297
if ( ! baseVersionInfo ) {
@@ -269,13 +351,22 @@ Or, run this command with no arguments to use the most recently published versio
269
351
)
270
352
}
271
353
354
+ if ( commit ) {
355
+ await commitEverything ( 'Updated peer dependency references' )
356
+ }
357
+
272
358
// Install the updated dependencies and build the vendored React files.
273
359
if ( ! install ) {
274
360
console . log ( 'Skipping install step because --no-install flag was passed.\n' )
275
361
} else {
276
362
console . log ( 'Installing dependencies...\n' )
277
363
278
- const installSubprocess = execa ( 'pnpm' , [ 'install' ] )
364
+ const installSubprocess = execa ( 'pnpm' , [
365
+ 'install' ,
366
+ // Pnpm freezes the lockfile by default in CI.
367
+ // However, we just changed versions so the lockfile is expected to be changed.
368
+ '--no-frozen-lockfile' ,
369
+ ] )
279
370
if ( installSubprocess . stdout ) {
280
371
installSubprocess . stdout . pipe ( process . stdout )
281
372
}
@@ -286,6 +377,10 @@ Or, run this command with no arguments to use the most recently published versio
286
377
throw new Error ( 'Failed to install updated dependencies.' )
287
378
}
288
379
380
+ if ( commit ) {
381
+ await commitEverything ( 'Update lockfile' )
382
+ }
383
+
289
384
console . log ( 'Building vendored React files...\n' )
290
385
const nccSubprocess = execa ( 'pnpm' , [ 'ncc-compiled' ] , {
291
386
cwd : path . join ( cwd , 'packages' , 'next' ) ,
@@ -300,34 +395,29 @@ Or, run this command with no arguments to use the most recently published versio
300
395
throw new Error ( 'Failed to run ncc.' )
301
396
}
302
397
398
+ if ( commit ) {
399
+ await commitEverything ( 'ncc-compiled' )
400
+ }
401
+
303
402
// Print extra newline after ncc output
304
403
console . log ( )
305
404
}
306
405
307
- console . log (
308
- `**breaking change for canary users: Bumps peer dependency of React from \`${ baseVersionStr } \` to \`${ newVersionStr } \`**`
309
- )
406
+ let prDescription = `**breaking change for canary users: Bumps peer dependency of React from \`${ baseVersionStr } \` to \`${ newVersionStr } \`**\n\n`
310
407
311
408
// 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
- )
409
+ prDescription += `[diff facebook/react@${ baseSha } ...${ newSha } ](https://github.com/facebook/react/compare/${ baseSha } ...${ newSha } )\n\n`
315
410
try {
316
411
const changelog = await getChangelogFromGitHub ( baseSha , newSha )
317
412
if ( changelog === null ) {
318
- console . log (
319
- `GitHub reported no changes between ${ baseSha } and ${ newSha } .`
320
- )
413
+ prDescription += `GitHub reported no changes between ${ baseSha } and ${ newSha } .`
321
414
} else {
322
- console . log (
323
- `<details>\n<summary>React upstream changes</summary>\n\n${ changelog } \n\n</details>`
324
- )
415
+ prDescription += `<details>\n<summary>React upstream changes</summary>\n\n${ changelog } \n\n</details>`
325
416
}
326
417
} catch ( error ) {
327
418
console . error ( error )
328
- console . log (
419
+ prDescription +=
329
420
'\nFailed to fetch changelog from GitHub. Changes were applied, anyway.\n'
330
- )
331
421
}
332
422
333
423
if ( ! install ) {
@@ -343,13 +433,57 @@ Or run this command again without the --no-install flag to do both automatically
343
433
)
344
434
}
345
435
346
- await fsp . writeFile ( path . join ( cwd , '.github/.react-version' ) , newVersionStr )
347
-
348
436
if ( errors . length ) {
349
437
// eslint-disable-next-line no-undef -- Defined in Node.js
350
438
throw new AggregateError ( errors )
351
439
}
352
440
441
+ if ( createPull ) {
442
+ const octokit = new Octokit ( { auth : githubToken } )
443
+ const prTitle = `Upgrade React from \`${ baseSha } -${ baseDateString } \` to \`${ newSha } -${ newDateString } \``
444
+
445
+ await execa ( 'git' , [ 'checkout' , '-b' , branchName ] )
446
+ // We didn't commit intermediate steps yet so now we need to commit to create a PR.
447
+ if ( ! commit ) {
448
+ commitEverything ( prTitle )
449
+ }
450
+ await execa ( 'git' , [ 'push' , 'origin' , branchName ] )
451
+ const pullRequest = await octokit . rest . pulls . create ( {
452
+ owner : repoOwner ,
453
+ repo : repoName ,
454
+ head : branchName ,
455
+ base : 'canary' ,
456
+ draft : false ,
457
+ title : prTitle ,
458
+ body : prDescription ,
459
+ } )
460
+ console . log ( 'Created pull request %s' , pullRequest . data . html_url )
461
+
462
+ await Promise . all ( [
463
+ actor
464
+ ? octokit . rest . issues . addAssignees ( {
465
+ owner : repoOwner ,
466
+ repo : repoName ,
467
+ issue_number : pullRequest . data . number ,
468
+ assignees : [ actor ] ,
469
+ } )
470
+ : Promise . resolve ( ) ,
471
+ octokit . rest . pulls . requestReviewers ( {
472
+ owner : repoOwner ,
473
+ repo : repoName ,
474
+ pull_number : pullRequest . data . number ,
475
+ reviewers : pullRequestReviewers ,
476
+ } ) ,
477
+ octokit . rest . issues . addLabels ( {
478
+ owner : repoOwner ,
479
+ repo : repoName ,
480
+ issue_number : pullRequest . data . number ,
481
+ labels : pullRequestLabels ,
482
+ } ) ,
483
+ ] )
484
+ }
485
+
486
+ console . log ( prDescription )
353
487
console . log (
354
488
`Successfully updated React from \`${ baseSha } -${ baseDateString } \` to \`${ newSha } -${ newDateString } \``
355
489
)
0 commit comments