diff --git a/.github/workflows/update_react.yml b/.github/workflows/update_react.yml
index 46b645be93668..966ff168b9fba 100644
--- a/.github/workflows/update_react.yml
+++ b/.github/workflows/update_react.yml
@@ -15,6 +15,7 @@ on:
env:
NODE_LTS_VERSION: 20
+ PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
jobs:
create-pull-request:
@@ -27,6 +28,11 @@ jobs:
# See: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow
token: ${{ secrets.RELEASE_BOT_GITHUB_TOKEN }}
+ - name: Set Git author
+ run: |
+ git config user.name "vercel-release-bot"
+ git config user.email "infra+release@vercel.com"
+
- name: Setup node
uses: actions/setup-node@v4
with:
@@ -38,3 +44,9 @@ jobs:
- name: Install dependencies
shell: bash
run: pnpm i
+
+ - name: Create Pull Request
+ shell: bash
+ run: pnpm sync-react --actor "${{ github.actor }}" --version "${{ inputs.version }}" --create-pull
+ env:
+ GITHUB_TOKEN: ${{ secrets.RELEASE_BOT_GITHUB_TOKEN }}
diff --git a/scripts/sync-react.js b/scripts/sync-react.js
index f6c203f653712..3031b67161509 100644
--- a/scripts/sync-react.js
+++ b/scripts/sync-react.js
@@ -9,6 +9,11 @@ const yargs = require('yargs')
/** @type {any} */
const fetch = require('node-fetch')
+const repoOwner = 'vercel'
+const repoName = 'next.js'
+const pullRequestLabels = ['type: react-sync']
+const pullRequestReviewers = ['eps1lon']
+
const filesReferencingReactPeerDependencyVersion = [
'run-tests.js',
'packages/create-next-app/templates/index.ts',
@@ -155,9 +160,44 @@ async function main() {
const errors = []
const argv = await yargs(process.argv.slice(2))
.version(false)
+ .options('actor', {
+ type: 'string',
+ description:
+ 'Required with `--create-pull`. The actor (GitHub username) that runs this script. Will be used for notifications but not commit attribution.',
+ })
+ .options('create-pull', {
+ default: false,
+ type: 'boolean',
+ description: 'Create a Pull Request in vercel/next.js',
+ })
+ .options('commit', {
+ default: true,
+ type: 'boolean',
+ description: 'Will not create any commit',
+ })
.options('install', { default: true, type: 'boolean' })
.options('version', { default: null, type: 'string' }).argv
- const { install, version } = argv
+ const { actor, createPull, commit, install, version } = argv
+
+ async function commitIfEnabled(message) {
+ if (commit) {
+ await execa('git', ['add', '-A'])
+ await execa('git', ['commit', '--message', message, '--no-verify'])
+ }
+ }
+
+ if (createPull && !actor) {
+ throw new Error(
+ `Pull Request cannot be created without a GitHub actor (received '${String(actor)}'). ` +
+ 'Pass an actor via `--actor "some-actor"`.'
+ )
+ }
+ const githubToken = process.env.GITHUB_TOKEN
+ if (createPull && !githubToken) {
+ throw new Error(
+ `Environment variable 'GITHUB_TOKEN' not specified but required when --create-pull is specified.`
+ )
+ }
let newVersionStr = version
if (newVersionStr === null) {
@@ -203,6 +243,7 @@ Or, run this command with no arguments to use the most recently published versio
noInstall: !install,
channel: 'experimental',
})
+ await commitIfEnabled('Update `react@experimental`')
await sync({
newDateString,
newSha,
@@ -210,6 +251,7 @@ Or, run this command with no arguments to use the most recently published versio
noInstall: !install,
channel: 'rc',
})
+ await commitIfEnabled('Update `react@rc`')
const baseVersionInfo = extractInfoFromReactVersion(baseVersionStr)
if (!baseVersionInfo) {
@@ -269,13 +311,20 @@ Or, run this command with no arguments to use the most recently published versio
)
}
+ await commitIfEnabled('Updated peer dependency references')
+
// Install the updated dependencies and build the vendored React files.
if (!install) {
console.log('Skipping install step because --no-install flag was passed.\n')
} else {
console.log('Installing dependencies...\n')
- const installSubprocess = execa('pnpm', ['install'])
+ const installSubprocess = execa('pnpm', [
+ 'install',
+ // Pnpm freezes the lockfile by default in CI.
+ // However, we just changed versions so the lockfile is expected to be changed.
+ '--no-frozen-lockfile',
+ ])
if (installSubprocess.stdout) {
installSubprocess.stdout.pipe(process.stdout)
}
@@ -286,6 +335,8 @@ Or, run this command with no arguments to use the most recently published versio
throw new Error('Failed to install updated dependencies.')
}
+ await commitIfEnabled('Update lockfile')
+
console.log('Building vendored React files...\n')
const nccSubprocess = execa('pnpm', ['ncc-compiled'], {
cwd: path.join(cwd, 'packages', 'next'),
@@ -300,34 +351,27 @@ Or, run this command with no arguments to use the most recently published versio
throw new Error('Failed to run ncc.')
}
+ await commitIfEnabled('ncc-compiled')
+
// Print extra newline after ncc output
console.log()
}
- console.log(
- `**breaking change for canary users: Bumps peer dependency of React from \`${baseVersionStr}\` to \`${newVersionStr}\`**`
- )
+ let prDescription = `**breaking change for canary users: Bumps peer dependency of React from \`${baseVersionStr}\` to \`${newVersionStr}\`**\n\n`
// Fetch the changelog from GitHub and print it to the console.
- console.log(
- `[diff facebook/react@${baseSha}...${newSha}](https://github.com/facebook/react/compare/${baseSha}...${newSha})`
- )
+ prDescription += `[diff facebook/react@${baseSha}...${newSha}](https://github.com/facebook/react/compare/${baseSha}...${newSha})\n\n`
try {
const changelog = await getChangelogFromGitHub(baseSha, newSha)
if (changelog === null) {
- console.log(
- `GitHub reported no changes between ${baseSha} and ${newSha}.`
- )
+ prDescription += `GitHub reported no changes between ${baseSha} and ${newSha}.`
} else {
- console.log(
- `\nReact upstream changes
\n\n${changelog}\n\n `
- )
+ prDescription += `\nReact upstream changes
\n\n${changelog}\n\n `
}
} catch (error) {
console.error(error)
- console.log(
+ prDescription +=
'\nFailed to fetch changelog from GitHub. Changes were applied, anyway.\n'
- )
}
if (!install) {
@@ -343,13 +387,51 @@ Or run this command again without the --no-install flag to do both automatically
)
}
- await fsp.writeFile(path.join(cwd, '.github/.react-version'), newVersionStr)
-
if (errors.length) {
// eslint-disable-next-line no-undef -- Defined in Node.js
throw new AggregateError(errors)
}
+ if (createPull) {
+ const github = await import('@actions/github')
+ const octokit = github.getOctokit(githubToken)
+ const branchName = `update/react/${newSha}-${newDateString}`
+
+ const pullRequest = await octokit.rest.pulls.create({
+ owner: repoOwner,
+ repo: repoName,
+ head: branchName,
+ base: 'main',
+ draft: false,
+ title: `Upgrade React from \`${baseSha}-${baseDateString}\` to \`${newSha}-${newDateString}\``,
+ body: prDescription,
+ })
+
+ await Promise.all([
+ actor
+ ? octokit.rest.issues.addAssignees({
+ owner: repoOwner,
+ repo: repoName,
+ issue_number: pullRequest.data.number,
+ assignees: [actor],
+ })
+ : Promise.resolve(),
+ octokit.rest.pulls.requestReviewers({
+ owner: repoOwner,
+ repo: repoName,
+ pull_number: pullRequest.data.number,
+ reviewers: pullRequestReviewers,
+ }),
+ octokit.rest.issues.addLabels({
+ owner: repoOwner,
+ repo: repoName,
+ issue_number: pullRequest.data.number,
+ labels: pullRequestLabels,
+ }),
+ ])
+ }
+
+ console.log(prDescription)
console.log(
`Successfully updated React from \`${baseSha}-${baseDateString}\` to \`${newSha}-${newDateString}\``
)