From 11b0eb205aae1155715e842b37acf9dc37f600b1 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Fri, 3 Oct 2025 00:04:57 +0200 Subject: [PATCH] Simplify release-from-npm workflow --- .../check-out-packages.js | 33 +++--- .../confirm-stable-version-numbers.js | 71 ------------ .../get-latest-next-version.js | 15 --- .../guess-stable-version-numbers.js | 63 ----------- .../parse-params.js | 16 +-- .../update-stable-version-numbers.js | 101 +++++++++--------- scripts/release/prepare-release-from-npm.js | 33 ++---- 7 files changed, 76 insertions(+), 256 deletions(-) delete mode 100644 scripts/release/prepare-release-from-npm-commands/confirm-stable-version-numbers.js delete mode 100644 scripts/release/prepare-release-from-npm-commands/get-latest-next-version.js delete mode 100644 scripts/release/prepare-release-from-npm-commands/guess-stable-version-numbers.js diff --git a/scripts/release/prepare-release-from-npm-commands/check-out-packages.js b/scripts/release/prepare-release-from-npm-commands/check-out-packages.js index ea7d1252a06e7..927e7f7f4c30f 100644 --- a/scripts/release/prepare-release-from-npm-commands/check-out-packages.js +++ b/scripts/release/prepare-release-from-npm-commands/check-out-packages.js @@ -5,19 +5,10 @@ const {exec} = require('child-process-promise'); const {existsSync} = require('fs'); const {join} = require('path'); -const {execRead, logPromise} = require('../utils'); +const {execRead} = require('../utils'); const theme = require('../theme'); -const run = async ({cwd, local, packages, version}) => { - if (local) { - // Sanity test - if (!existsSync(join(cwd, 'build', 'node_modules', 'react'))) { - console.error(theme.error`No local build exists.`); - process.exit(1); - } - return; - } - +const run = async (packages, versionsMap, {cwd, prerelease}) => { if (!existsSync(join(cwd, 'build'))) { await exec(`mkdir ./build`, {cwd}); } @@ -31,6 +22,14 @@ const run = async ({cwd, local, packages, version}) => { // Checkout "next" release from NPM for all local packages for (let i = 0; i < packages.length; i++) { const packageName = packages[i]; + if (!(packageName in versionsMap)) { + throw Error( + `Package "${packageName}" has no version specified. Only ${JSON.stringify( + Object.keys(versionsMap) + )} are supported are valid package names.` + ); + } + const version = versionsMap[packageName] + '-canary-' + prerelease; // We previously used `npm install` for this, // but in addition to checking out a lot of transient dependencies that we don't care about– @@ -49,12 +48,12 @@ const run = async ({cwd, local, packages, version}) => { await exec(`tar -xvzf ${filePath} -C ${nodeModulesPath}`, {cwd}); await exec(`mv ${tempPackagePath} ${packagePath}`, {cwd}); await exec(`rm ${filePath}`, {cwd}); + + console.log( + theme`{green ✔} NPM checkout {package ${packageName}}@{version ${version}}` + ); } }; -module.exports = async params => { - return logPromise( - run(params), - theme`Checking out "next" from NPM {version ${params.version}}` - ); -}; +// Run this directly because logPromise would interfere with printing package dependencies. +module.exports = run; diff --git a/scripts/release/prepare-release-from-npm-commands/confirm-stable-version-numbers.js b/scripts/release/prepare-release-from-npm-commands/confirm-stable-version-numbers.js deleted file mode 100644 index 6bf49132149a8..0000000000000 --- a/scripts/release/prepare-release-from-npm-commands/confirm-stable-version-numbers.js +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -const prompt = require('prompt-promise'); -const semver = require('semver'); -const theme = require('../theme'); -const {confirm} = require('../utils'); - -const run = async ({ci, skipPackages}, versionsMap) => { - const groupedVersionsMap = new Map(); - - // Group packages with the same source versions. - // We want these to stay lock-synced anyway. - // This will require less redundant input from the user later, - // and reduce the likelihood of human error (entering the wrong version). - versionsMap.forEach((version, packageName) => { - if (!groupedVersionsMap.has(version)) { - groupedVersionsMap.set(version, [packageName]); - } else { - groupedVersionsMap.get(version).push(packageName); - } - }); - - if (ci !== true) { - // Prompt user to confirm or override each version group if not running in CI. - const entries = [...groupedVersionsMap.entries()]; - for (let i = 0; i < entries.length; i++) { - const [bestGuessVersion, packages] = entries[i]; - const packageNames = packages.map(name => theme.package(name)).join(', '); - - let version = bestGuessVersion; - if ( - skipPackages.some(skipPackageName => packages.includes(skipPackageName)) - ) { - await confirm( - theme`{spinnerSuccess ✓} Version for ${packageNames} will remain {version ${bestGuessVersion}}` - ); - } else { - const defaultVersion = bestGuessVersion - ? theme.version(` (default ${bestGuessVersion})`) - : ''; - version = - (await prompt( - theme`{spinnerSuccess ✓} Version for ${packageNames}${defaultVersion}: ` - )) || bestGuessVersion; - prompt.done(); - } - - // Verify a valid version has been supplied. - try { - semver(version); - - packages.forEach(packageName => { - versionsMap.set(packageName, version); - }); - } catch (error) { - console.log( - theme`{spinnerError ✘} Version {version ${version}} is invalid.` - ); - - // Prompt again - i--; - } - } - } -}; - -// Run this directly because it's fast, -// and logPromise would interfere with console prompting. -module.exports = run; diff --git a/scripts/release/prepare-release-from-npm-commands/get-latest-next-version.js b/scripts/release/prepare-release-from-npm-commands/get-latest-next-version.js deleted file mode 100644 index f40730d119aed..0000000000000 --- a/scripts/release/prepare-release-from-npm-commands/get-latest-next-version.js +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -const {execRead, logPromise} = require('../utils'); - -const run = async () => { - const version = await execRead('npm info react@canary version'); - - return version; -}; - -module.exports = async params => { - return logPromise(run(params), 'Determining latest "canary" release version'); -}; diff --git a/scripts/release/prepare-release-from-npm-commands/guess-stable-version-numbers.js b/scripts/release/prepare-release-from-npm-commands/guess-stable-version-numbers.js deleted file mode 100644 index c0072b6637177..0000000000000 --- a/scripts/release/prepare-release-from-npm-commands/guess-stable-version-numbers.js +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -const semver = require('semver'); -const {execRead, logPromise} = require('../utils'); - -const run = async ( - {cwd, packages, skipPackages, ci, publishVersion}, - versionsMap -) => { - const branch = await execRead('git branch | grep \\* | cut -d " " -f2', { - cwd, - }); - - for (let i = 0; i < packages.length; i++) { - const packageName = packages[i]; - - if (ci === true) { - if (publishVersion != null) { - versionsMap.set(packageName, publishVersion); - } else { - console.error( - 'When running in CI mode, a publishVersion must be supplied' - ); - process.exit(1); - } - } else { - try { - // In case local package JSONs are outdated, - // guess the next version based on the latest NPM release. - const version = await execRead(`npm show ${packageName} version`); - - if (skipPackages.includes(packageName)) { - versionsMap.set(packageName, version); - } else { - const {major, minor, patch} = semver(version); - - // Guess the next version by incrementing patch. - // The script will confirm this later. - // By default, new releases from mains should increment the minor version number, - // and patch releases should be done from branches. - if (branch === 'main') { - versionsMap.set(packageName, `${major}.${minor + 1}.0`); - } else { - versionsMap.set(packageName, `${major}.${minor}.${patch + 1}`); - } - } - } catch (error) { - // If the package has not yet been published, - // we'll require a version number to be entered later. - versionsMap.set(packageName, null); - } - } - } -}; - -module.exports = async (params, versionsMap) => { - return logPromise( - run(params, versionsMap), - 'Guessing stable version numbers' - ); -}; diff --git a/scripts/release/prepare-release-from-npm-commands/parse-params.js b/scripts/release/prepare-release-from-npm-commands/parse-params.js index 10dbfb4e51f50..2246aa40e4d65 100644 --- a/scripts/release/prepare-release-from-npm-commands/parse-params.js +++ b/scripts/release/prepare-release-from-npm-commands/parse-params.js @@ -6,13 +6,6 @@ const commandLineArgs = require('command-line-args'); const {splitCommaParams} = require('../utils'); const paramDefinitions = [ - { - name: 'local', - type: Boolean, - description: - 'Skip NPM and use the build already present in "build/node_modules".', - defaultValue: false, - }, { name: 'onlyPackages', type: String, @@ -34,15 +27,10 @@ const paramDefinitions = [ defaultValue: false, }, { - name: 'version', + name: 'prerelease', type: String, description: - 'Version of published "next" release (e.g. 0.0.0-0e526bcec-20210202)', - }, - { - name: 'publishVersion', - type: String, - description: 'Version to publish', + 'prerelease to publish (e.g. version 19.2.0-canary-86181134-20251001 has prerelease "86181134-20251001")', }, { name: 'ci', diff --git a/scripts/release/prepare-release-from-npm-commands/update-stable-version-numbers.js b/scripts/release/prepare-release-from-npm-commands/update-stable-version-numbers.js index eef68255e2ba2..1908322fc4594 100644 --- a/scripts/release/prepare-release-from-npm-commands/update-stable-version-numbers.js +++ b/scripts/release/prepare-release-from-npm-commands/update-stable-version-numbers.js @@ -9,7 +9,7 @@ const {join, relative} = require('path'); const {confirm, execRead, printDiff} = require('../utils'); const theme = require('../theme'); -const run = async ({cwd, packages, version, ci}, versionsMap) => { +const run = async (packages, versions, {cwd, prerelease, ci}) => { const nodeModulesPath = join(cwd, 'build/node_modules'); // Cache all package JSONs for easy lookup below. @@ -52,10 +52,16 @@ const run = async ({cwd, packages, version, ci}, versionsMap) => { sourceDependencyVersion === sourceDependencyConstraint.replace(/^[\^\~]/, '') ) { + if (!(dependencyName in versions)) { + throw new Error( + `No version found for ${dependencyName} in the release.` + ); + } + targetDependencies[dependencyName] = sourceDependencyConstraint.replace( sourceDependencyVersion, - versionsMap.get(dependencyName) + versions[dependencyName] ); } else { targetDependencies[dependencyName] = sourceDependencyConstraint; @@ -73,7 +79,10 @@ const run = async ({cwd, packages, version, ci}, versionsMap) => { const packageName = packages[i]; const packageJSONPath = join(nodeModulesPath, packageName, 'package.json'); const packageJSON = await readJson(packageJSONPath); - packageJSON.version = versionsMap.get(packageName); + if (!(packageName in versions)) { + throw new Error(`No version found for ${packageName} in the release.`); + } + packageJSON.version = versions[packageName]; await updateDependencies(packageJSON, 'dependencies'); await updateDependencies(packageJSON, 'peerDependencies'); @@ -100,9 +109,7 @@ const run = async ({cwd, packages, version, ci}, versionsMap) => { const packageJSONPath = join(nodeModulesPath, packageName, 'package.json'); const packageJSON = await readJson(packageJSONPath); console.log( - theme`\n{package ${packageName}} {version ${versionsMap.get( - packageName - )}}` + theme`\n{package ${packageName}} {version ${versions[packageName]}}` ); printDependencies(packageJSON.dependencies, 'dependency'); printDependencies(packageJSON.peerDependencies, 'peer'); @@ -113,51 +120,47 @@ const run = async ({cwd, packages, version, ci}, versionsMap) => { clear(); - if (packages.includes('react')) { - // We print the diff to the console for review, - // but it can be large so let's also write it to disk. - const diffPath = join(cwd, 'build', 'temp.diff'); - let diff = ''; - let numFilesModified = 0; - - // Find-and-replace hardcoded version (in built JS) for renderers. - for (let i = 0; i < packages.length; i++) { - const packageName = packages[i]; - const packagePath = join(nodeModulesPath, packageName); - - let files = await execRead( - `find ${packagePath} -name '*.js' -exec echo {} \\;`, - {cwd} - ); - files = files.split('\n'); - files.forEach(path => { - const newStableVersion = versionsMap.get(packageName); - const beforeContents = readFileSync(path, 'utf8', {cwd}); - let afterContents = beforeContents; - // Replace all "next" version numbers (e.g. header @license). - while (afterContents.indexOf(version) >= 0) { - afterContents = afterContents.replace(version, newStableVersion); - } - if (beforeContents !== afterContents) { - numFilesModified++; - // Using a relative path for diff helps with the snapshot test - diff += printDiff(relative(cwd, path), beforeContents, afterContents); - writeFileSync(path, afterContents, {cwd}); - } - }); - } - writeFileSync(diffPath, diff, {cwd}); - console.log(theme.header(`\n${numFilesModified} files have been updated.`)); - console.log( - theme`A full diff is available at {path ${relative(cwd, diffPath)}}.` - ); - if (ci !== true) { - await confirm('Do the changes above look correct?'); + // We print the diff to the console for review, + // but it can be large so let's also write it to disk. + const diffPath = join(cwd, 'build', 'temp.diff'); + let diff = ''; + let numFilesModified = 0; + + // Find-and-replace hardcoded version (in built JS) for renderers. + for (let i = 0; i < packages.length; i++) { + const packageName = packages[i]; + if (!(packageName in versions)) { + throw new Error(`No version found for ${packageName} in the release.`); } - } else { - console.log( - theme`Skipping React renderer version update because React is not included in the release.` + const newStableVersion = versions[packageName]; + // Replace all "next" version numbers (e.g. header @license). + const canaryVersion = `${newStableVersion}-canary-${prerelease}`; + const packagePath = join(nodeModulesPath, packageName); + + let files = await execRead( + `find ${packagePath} -name '*.js' -exec echo {} \\;`, + {cwd} ); + files = files.split('\n'); + files.forEach(path => { + const beforeContents = readFileSync(path, 'utf8', {cwd}); + let afterContents = beforeContents; + afterContents = afterContents.replaceAll(canaryVersion, newStableVersion); + if (beforeContents !== afterContents) { + numFilesModified++; + // Using a relative path for diff helps with the snapshot test + diff += printDiff(relative(cwd, path), beforeContents, afterContents); + writeFileSync(path, afterContents, {cwd}); + } + }); + } + writeFileSync(diffPath, diff, {cwd}); + console.log(theme.header(`\n${numFilesModified} files have been updated.`)); + console.log( + theme`A full diff is available at {path ${relative(cwd, diffPath)}}.` + ); + if (ci !== true) { + await confirm('Do the changes above look correct?'); } clear(); diff --git a/scripts/release/prepare-release-from-npm.js b/scripts/release/prepare-release-from-npm.js index bb67fddd37803..de4bcca261e13 100755 --- a/scripts/release/prepare-release-from-npm.js +++ b/scripts/release/prepare-release-from-npm.js @@ -6,26 +6,17 @@ const {join} = require('path'); const {getPublicPackages, handleError} = require('./utils'); const checkOutPackages = require('./prepare-release-from-npm-commands/check-out-packages'); -const confirmStableVersionNumbers = require('./prepare-release-from-npm-commands/confirm-stable-version-numbers'); -const getLatestNextVersion = require('./prepare-release-from-npm-commands/get-latest-next-version'); -const guessStableVersionNumbers = require('./prepare-release-from-npm-commands/guess-stable-version-numbers'); const parseParams = require('./prepare-release-from-npm-commands/parse-params'); const printPrereleaseSummary = require('./shared-commands/print-prerelease-summary'); const testPackagingFixture = require('./shared-commands/test-packaging-fixture'); const updateStableVersionNumbers = require('./prepare-release-from-npm-commands/update-stable-version-numbers'); -const theme = require('./theme'); +const {stablePackages} = require('../../ReactVersions'); const run = async () => { try { const params = parseParams(); params.cwd = join(__dirname, '..', '..'); - const isExperimental = params.version.includes('experimental'); - - if (!params.version) { - params.version = await getLatestNextVersion(); - } - if (params.onlyPackages.length > 0 && params.skipPackages.length > 0) { console.error( '--onlyPackages and --skipPackages cannot be used together' @@ -33,30 +24,18 @@ const run = async () => { process.exit(1); } - params.packages = await getPublicPackages(isExperimental); - params.packages = params.packages.filter(packageName => { + let packages = getPublicPackages(); + packages = packages.filter(packageName => { if (params.onlyPackages.length > 0) { return params.onlyPackages.includes(packageName); } return !params.skipPackages.includes(packageName); }); - // Map of package name to upcoming stable version. - // This Map is initially populated with guesses based on local versions. - // The developer running the release later confirms or overrides each version. - const versionsMap = new Map(); - - if (isExperimental) { - console.error( - theme.error`Cannot promote an experimental build to stable.` - ); - process.exit(1); - } + const versions = stablePackages; - await checkOutPackages(params); - await guessStableVersionNumbers(params, versionsMap); - await confirmStableVersionNumbers(params, versionsMap); - await updateStableVersionNumbers(params, versionsMap); + await checkOutPackages(packages, versions, params); + await updateStableVersionNumbers(packages, versions, params); if (!params.skipTests) { await testPackagingFixture(params);