From 662ab4dcdceadcae1bf06959874c394ece01e165 Mon Sep 17 00:00:00 2001 From: Lauren Tan Date: Thu, 27 Feb 2025 15:03:41 -0500 Subject: [PATCH] [ci] Prepare publish workflow Fixes up a few things in the script and workflow to make it possible to run in CI without interactive prompts. --- .../runtime_releases_from_npm_manual.yml | 56 +++++++++++---- .../confirm-stable-version-numbers.js | 72 ++++++++++--------- .../guess-stable-version-numbers.js | 54 ++++++++------ .../parse-params.js | 11 +++ .../update-stable-version-numbers.js | 10 ++- scripts/release/prepare-release-from-npm.js | 7 ++ .../confirm-version-and-tags.js | 3 + .../publish-commands/publish-to-npm.js | 4 +- scripts/release/publish.js | 7 ++ 9 files changed, 153 insertions(+), 71 deletions(-) diff --git a/.github/workflows/runtime_releases_from_npm_manual.yml b/.github/workflows/runtime_releases_from_npm_manual.yml index 0851b38ff9..f076573230 100644 --- a/.github/workflows/runtime_releases_from_npm_manual.yml +++ b/.github/workflows/runtime_releases_from_npm_manual.yml @@ -12,22 +12,22 @@ on: description: Version to publish for the specified packages type: string only_packages: - description: Space separated list of packages to publish on NPM. Use this OR skip_packages, not together. + description: Packages to publish (space separated) type: string skip_packages: - description: Space separated list of packages to NOT publish on NPM. Use this OR only_packages, not together. + description: Packages to NOT publish (space separated) type: string tags: - description: Space separated list of tags to tag the release with on NPM + description: NPM tags (space separated) type: string - default: "['untagged']" + default: untagged dry: required: true - description: Don't actually publish, just run a dry run + description: Dry run instead of publish? type: boolean default: true force_notify: - description: Force a Discord notification + description: Force a Discord notification? type: boolean default: false @@ -52,8 +52,8 @@ jobs: embed-author-icon-url: ${{ github.event.sender.avatar_url }} embed-title: '⚠️ Publishing release from NPM' embed-description: | - ``` - inputs: ${{ toJson(inputs) }} + ```json + ${{ toJson(inputs) }} ``` embed-url: https://github.com/facebook/react/actions/runs/${{ github.run_id }} @@ -80,12 +80,44 @@ jobs: working-directory: scripts/release - run: cp ./scripts/release/ci-npmrc ~/.npmrc - if: '${{ inputs.only_packages }}' + name: 'Prepare and publish ${{ inputs.only_packages }}' run: | - scripts/release/prepare-release-from-npm.js --skipTests --version=${{ inputs.version_to_promote }} --onlyPackages=${{ inputs.only_packages }} + echo -e "===== Preparing release from NPM =====\n" + scripts/release/prepare-release-from-npm.js \ + --ci \ + --skipTests \ + --version=${{ inputs.version_to_promote }} \ + --publishVersion=${{ inputs.version_to_publish }} \ + --onlyPackages=${{ inputs.only_packages }} + + echo -e "\n\n===== Check prepared files =====\n" ls -R build/node_modules - # scripts/release/publish.js --ci --tags=${{ inputs.tags }} --publishVersion=${{ inputs.version_to_publish }} --onlyPackages=${{ inputs.only_packages }} --dry=${{ inputs.dry || 'false' }} + + echo -e "\n\n===== Publishing to NPM =====\n" + scripts/release/publish.js \ + --ci \ + --tags=${{ inputs.tags }} \ + --publishVersion=${{ inputs.version_to_publish }} \ + --onlyPackages=${{ inputs.only_packages }} \ + --dry=${{ inputs.dry }} - if: '${{ inputs.skip_packages }}' + name: 'Prepare and publish alll packages EXCEPT ${{ inputs.skip_packages }}' run: | - scripts/release/prepare-release-from-npm.js --skipTests --version=${{ inputs.version_to_promote }} --skipPackages=${{ inputs.skip_packages }} + echo -e "===== Preparing release from NPM =====\n" + scripts/release/prepare-release-from-npm.js \ + --ci \ + --skipTests \ + --version=${{ inputs.version_to_promote }} \ + --publishVersion=${{ inputs.version_to_publish }} \ + --skipPackages=${{ inputs.skip_packages }} + + echo -e "\n\n===== Check prepared files =====\n" ls -R build/node_modules - # scripts/release/publish.js --ci --tags=${{ inputs.tags }} --publishVersion=${{ inputs.version_to_publish }} --skipPackages=${{ inputs.skip_packages }} --dry=${{ inputs.dry || 'false' }} + + echo -e "\n\n===== Publishing to NPM =====\n" + scripts/release/publish.js \ + --ci \ + --tags=${{ inputs.tags }} \ + --publishVersion=${{ inputs.version_to_publish }} \ + --skipPackages=${{ inputs.skip_packages }} \ + --dry=${{ inputs.dry }} 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 index 0b93fba2fc..6bf4913214 100644 --- 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 @@ -7,7 +7,7 @@ const semver = require('semver'); const theme = require('../theme'); const {confirm} = require('../utils'); -const run = async ({skipPackages}, versionsMap) => { +const run = async ({ci, skipPackages}, versionsMap) => { const groupedVersionsMap = new Map(); // Group packages with the same source versions. @@ -22,44 +22,46 @@ const run = async ({skipPackages}, versionsMap) => { } }); - // Prompt user to confirm or override each version group. - 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(', '); + 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(); - } + 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); + // 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.` - ); + packages.forEach(packageName => { + versionsMap.set(packageName, version); + }); + } catch (error) { + console.log( + theme`{spinnerError ✘} Version {version ${version}} is invalid.` + ); - // Prompt again - i--; + // Prompt again + i--; + } } } }; 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 index a3cde5d1c4..c0072b6637 100644 --- 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 @@ -5,7 +5,10 @@ const semver = require('semver'); const {execRead, logPromise} = require('../utils'); -const run = async ({cwd, packages, skipPackages}, versionsMap) => { +const run = async ( + {cwd, packages, skipPackages, ci, publishVersion}, + versionsMap +) => { const branch = await execRead('git branch | grep \\* | cut -d " " -f2', { cwd, }); @@ -13,30 +16,41 @@ const run = async ({cwd, packages, skipPackages}, versionsMap) => { for (let i = 0; i < packages.length; i++) { const packageName = packages[i]; - 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); + if (ci === true) { + if (publishVersion != null) { + versionsMap.set(packageName, publishVersion); } else { - const {major, minor, patch} = semver(version); + 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`); - // 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`); + if (skipPackages.includes(packageName)) { + versionsMap.set(packageName, version); } else { - versionsMap.set(packageName, `${major}.${minor}.${patch + 1}`); + 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); } - } catch (error) { - // If the package has not yet been published, - // we'll require a version number to be entered later. - versionsMap.set(packageName, null); } } }; 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 b08d81e89d..ef9c4979b2 100644 --- a/scripts/release/prepare-release-from-npm-commands/parse-params.js +++ b/scripts/release/prepare-release-from-npm-commands/parse-params.js @@ -39,6 +39,17 @@ const paramDefinitions = [ description: 'Version of published "next" release (e.g. 0.0.0-0e526bcec-20210202)', }, + { + name: 'publishVersion', + type: String, + description: 'Version to publish', + }, + { + name: 'ci', + type: Boolean, + description: 'Run in automated environment, without interactive prompts.', + defaultValue: false, + }, ]; module.exports = () => { 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 a41d83ac69..f201118890 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}, versionsMap) => { +const run = async ({cwd, packages, version, ci}, versionsMap) => { const nodeModulesPath = join(cwd, 'build/node_modules'); // Cache all package JSONs for easy lookup below. @@ -107,7 +107,9 @@ const run = async ({cwd, packages, version}, versionsMap) => { printDependencies(packageJSON.dependencies, 'dependency'); printDependencies(packageJSON.peerDependencies, 'peer'); } - await confirm('Do the versions above look correct?'); + if (ci !== true) { + await confirm('Do the versions above look correct?'); + } clear(); @@ -167,7 +169,9 @@ const run = async ({cwd, packages, version}, versionsMap) => { console.log( theme`A full diff is available at {path ${relative(cwd, diffPath)}}.` ); - await confirm('Do the changes above look correct?'); + if (ci !== true) { + await confirm('Do the changes above look correct?'); + } } else { console.log( theme`Skipping React renderer version update because React is not included in the release.` diff --git a/scripts/release/prepare-release-from-npm.js b/scripts/release/prepare-release-from-npm.js index 8220fda220..bb67fddd37 100755 --- a/scripts/release/prepare-release-from-npm.js +++ b/scripts/release/prepare-release-from-npm.js @@ -26,6 +26,13 @@ const run = async () => { params.version = await getLatestNextVersion(); } + if (params.onlyPackages.length > 0 && params.skipPackages.length > 0) { + console.error( + '--onlyPackages and --skipPackages cannot be used together' + ); + process.exit(1); + } + params.packages = await getPublicPackages(isExperimental); params.packages = params.packages.filter(packageName => { if (params.onlyPackages.length > 0) { diff --git a/scripts/release/publish-commands/confirm-version-and-tags.js b/scripts/release/publish-commands/confirm-version-and-tags.js index 6900878da0..2c6fed58d1 100644 --- a/scripts/release/publish-commands/confirm-version-and-tags.js +++ b/scripts/release/publish-commands/confirm-version-and-tags.js @@ -38,6 +38,9 @@ const run = async ({cwd, packages, tags, ci}) => { console.log( theme`• {package ${packageName}} {version ${packageJSON.version}}` ); + if (ci) { + console.log(packageJSON); + } } if (!ci) { diff --git a/scripts/release/publish-commands/publish-to-npm.js b/scripts/release/publish-commands/publish-to-npm.js index d9e3b19023..f1e62657c0 100644 --- a/scripts/release/publish-commands/publish-to-npm.js +++ b/scripts/release/publish-commands/publish-to-npm.js @@ -27,7 +27,9 @@ const run = async ({cwd, dry, tags, ci}, packageName, otp) => { await confirm('Is this expected?'); } } else { - console.log(theme`{spinnerSuccess ✓} Publishing {package ${packageName}}`); + console.log( + theme`{spinnerSuccess ✓} Publishing {package ${packageName}}${dry ? ' (dry-run)' : ''}` + ); // Publish the package and tag it. if (!dry) { diff --git a/scripts/release/publish.js b/scripts/release/publish.js index 87b9940636..f9e450b559 100755 --- a/scripts/release/publish.js +++ b/scripts/release/publish.js @@ -31,6 +31,13 @@ const run = async () => { params.cwd = join(__dirname, '..', '..'); params.packages = await getPublicPackages(isExperimental); + if (params.onlyPackages.length > 0 && params.skipPackages.length > 0) { + console.error( + '--onlyPackages and --skipPackages cannot be used together' + ); + process.exit(1); + } + if (params.onlyPackages.length > 0) { params.packages = params.packages.filter(packageName => { return params.onlyPackages.includes(packageName);