From 2672510165184038fee003e9647dc41bda87dbf7 Mon Sep 17 00:00:00 2001 From: eps1lon Date: Tue, 27 Aug 2024 12:16:07 +0200 Subject: [PATCH 1/2] Remove obsolete `.react-version` file This file was only added to automatically add GH labels. However, we now set this label in the automated GH workflow. It was also problematic since the labeler only ever adds labels. Sometimes the label was only added during merge or intermediate steps but not on the final PR making the label not useful as a single signal. --- .github/.react-version | 1 - .github/labeler.json | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 .github/.react-version diff --git a/.github/.react-version b/.github/.react-version deleted file mode 100644 index 4c946f78dd695..0000000000000 --- a/.github/.react-version +++ /dev/null @@ -1 +0,0 @@ -19.0.0-rc-eb3ad065-20240822 \ No newline at end of file diff --git a/.github/labeler.json b/.github/labeler.json index c0c57e7820161..0f1575da620bb 100644 --- a/.github/labeler.json +++ b/.github/labeler.json @@ -71,7 +71,6 @@ "packages/next-swc/**", "packages/next/**", "packages/react-refresh-utils/**" - ], - "type: react-sync": [".github/.react-version"] + ] } } From 81c1b323deb50ad988d90eb7e77f0eda4e48cf60 Mon Sep 17 00:00:00 2001 From: eps1lon Date: Mon, 26 Aug 2024 19:32:17 +0200 Subject: [PATCH 2/2] Create React sync automatically --- .github/workflows/update_react.yml | 15 ++- scripts/sync-react.js | 174 +++++++++++++++++++++++++---- 2 files changed, 169 insertions(+), 20 deletions(-) diff --git a/.github/workflows/update_react.yml b/.github/workflows/update_react.yml index 46b645be93668..f935de8e6cd34 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: @@ -37,4 +43,11 @@ jobs: - name: Install dependencies shell: bash - run: pnpm i + # Just need scripts/ but those dependencies are listed in the workspace root. + run: pnpm install --filter . + + - name: Create Pull Request + shell: bash + run: pnpm sync-react --actor "${{ github.actor }}" --commit --create-pull --version "${{ inputs.version }}" + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN_PULL_REQUESTS }} diff --git a/scripts/sync-react.js b/scripts/sync-react.js index f6c203f653712..2e4473a08b5bc 100644 --- a/scripts/sync-react.js +++ b/scripts/sync-react.js @@ -4,11 +4,17 @@ const path = require('path') const fsp = require('fs/promises') const process = require('process') const execa = require('execa') +const { Octokit } = require('octokit') 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,12 +161,50 @@ 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: false, + type: 'boolean', + description: + 'Creates commits for each intermediate step. Useful to create better diffs for GitHub.', + }) .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 commitEverything(message) { + 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) { + if ( + newVersionStr === null || + // TODO: Fork arguments in GitHub workflow to ensure `--version ""` is considered a mistake + newVersionStr === '' + ) { const { stdout, stderr } = await execa( 'npm', ['view', 'react@canary', 'version'], @@ -174,6 +218,9 @@ async function main() { throw new Error('Failed to read latest React canary version from npm.') } newVersionStr = stdout.trim() + console.log( + `--version was not provided. Using react@canary: ${newVersionStr}` + ) } const newVersionInfo = extractInfoFromReactVersion(newVersionStr) @@ -188,6 +235,37 @@ Or, run this command with no arguments to use the most recently published versio ) } const { sha: newSha, dateString: newDateString } = newVersionInfo + + const branchName = `update/react/${newSha}-${newDateString}` + if (createPull) { + const { exitCode, all, command } = await execa( + 'git', + [ + 'ls-remote', + '--exit-code', + '--heads', + 'origin', + `refs/heads/${branchName}`, + ], + { reject: false } + ) + + if (exitCode === 2) { + console.log( + `No sync in progress in branch '${branchName}' according to '${command}'. Starting a new one.` + ) + } else if (exitCode === 0) { + console.log( + `An existing sync already exists in branch '${branchName}'. Delete the branch to start a new sync.` + ) + return + } else { + throw new Error( + `Failed to check if the branch already existed:\n${command}: ${all}` + ) + } + } + const rootManifest = JSON.parse( await fsp.readFile(path.join(cwd, 'package.json'), 'utf-8') ) @@ -203,6 +281,9 @@ Or, run this command with no arguments to use the most recently published versio noInstall: !install, channel: 'experimental', }) + if (commit) { + await commitEverything('Update `react@experimental`') + } await sync({ newDateString, newSha, @@ -210,6 +291,9 @@ Or, run this command with no arguments to use the most recently published versio noInstall: !install, channel: 'rc', }) + if (commit) { + await commitEverything('Update `react@rc`') + } const baseVersionInfo = extractInfoFromReactVersion(baseVersionStr) if (!baseVersionInfo) { @@ -269,13 +353,22 @@ Or, run this command with no arguments to use the most recently published versio ) } + if (commit) { + await commitEverything('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 +379,10 @@ Or, run this command with no arguments to use the most recently published versio throw new Error('Failed to install updated dependencies.') } + if (commit) { + await commitEverything('Update lockfile') + } + console.log('Building vendored React files...\n') const nccSubprocess = execa('pnpm', ['ncc-compiled'], { cwd: path.join(cwd, 'packages', 'next'), @@ -300,34 +397,29 @@ Or, run this command with no arguments to use the most recently published versio throw new Error('Failed to run ncc.') } + if (commit) { + await commitEverything('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 +435,57 @@ 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 octokit = new Octokit({ auth: githubToken }) + const prTitle = `Upgrade React from \`${baseSha}-${baseDateString}\` to \`${newSha}-${newDateString}\`` + + await execa('git', ['checkout', '-b', branchName]) + // We didn't commit intermediate steps yet so now we need to commit to create a PR. + if (!commit) { + commitEverything(prTitle) + } + await execa('git', ['push', 'origin', branchName]) + const pullRequest = await octokit.rest.pulls.create({ + owner: repoOwner, + repo: repoName, + head: branchName, + base: 'canary', + draft: false, + title: prTitle, + body: prDescription, + }) + console.log('Created pull request %s', pullRequest.data.html_url) + + 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}\`` )