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}\`` )