diff --git a/.github/actions/prepare_release/action.yml b/.github/actions/prepare_release/action.yml index 86c27c327b8fd1..fe0eaeac27a360 100644 --- a/.github/actions/prepare_release/action.yml +++ b/.github/actions/prepare_release/action.yml @@ -6,6 +6,9 @@ runs: - name: Yarn install shell: bash run: yarn install --non-interactive + - name: Validate version + shell: bash + run: node ./scripts/releases/validate-version.js --build-type release --version "${{ inputs.version }}" - name: Versioning workspace packages shell: bash run: | @@ -17,7 +20,14 @@ runs: - name: Creating release commit shell: bash run: | - git commit -a -m "Release ${{ inputs.version }}" -m "#publish-packages-to-npm&${{ inputs.tag }}" + BRANCH="$(git branch --show-current)" + NPM_TAG="$(node ./scripts/releases/compute-npm-tag.js --version ${{ inputs.version }} --branch $BRANCH)" + + if [[ "${{ inputs.is-latest-on-npm }}" ]]; then + NPM_TAG="$(node ./scripts/releases/compute-npm-tag.js --is_latest_on_npm --version ${{ inputs.version }} --branch $BRANCH)" + fi + + git commit -a -m "Release ${{ inputs.version }}" -m "#publish-packages-to-npm&$NPM_TAG" git tag -a "v${{ inputs.version }}" -m "v${{ inputs.version }}" GIT_PAGER=cat git show HEAD - name: Update "latest" tag if needed diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index e0af0de2241d75..b976638a681eff 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -7,10 +7,11 @@ on: description: 'The version of React Native we want to release' required: true type: string - tag: - description: 'The tag name that will be associated with the published npm packages. A tag of "latest" will also be written as a Git tag.' + is-latest-on-npm: + description: 'Whether we want to tag this release as latest on NPM' required: true - type: string + type: boolean + default: false dry_run: description: 'Whether the job should be executed in dry-run mode or not' type: boolean @@ -33,6 +34,15 @@ jobs: fi - name: Print output run: echo "ON_STABLE_BRANCH ${{steps.check_stable_branch.outputs.ON_STABLE_BRANCH}}" + - name: Check if tag already exists + id: check_if_tag_exists + run: | + TAG="v${{ inputs.version }}" + TAG_EXISTS=$(git tag -l "$TAG") + if [[ -n "$TAG_EXISTS" ]]; then + echo "Version tag already exists!" + echo "TAG_EXISTS=true" >> $GITHUB_OUTPUT + fi - name: Execute Prepare Release - if: ${{ steps.check_stable_branch.outputs.ON_STABLE_BRANCH }} + if: ${{ steps.check_stable_branch.outputs.ON_STABLE_BRANCH && !steps.check_if_tag_exists.outputs.TAG_EXISTS }} uses: ./.github/actions/prepare_release diff --git a/scripts/releases-local/README.md b/scripts/releases-local/README.md deleted file mode 100644 index 38b818e601cf8f..00000000000000 --- a/scripts/releases-local/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# scripts/releases-local - -Local-only release scripts. - -## Commands - -For information on command arguments, run `node --help`. - -### `trigger-react-native-release.js` - -Trigger the external release publishing workflow on CircleCI. diff --git a/scripts/releases-local/trigger-react-native-release.js b/scripts/releases-local/trigger-react-native-release.js deleted file mode 100644 index 3d426f1a33c459..00000000000000 --- a/scripts/releases-local/trigger-react-native-release.js +++ /dev/null @@ -1,223 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - * @format - * @oncall react_native - */ - -'use strict'; - -const checkForGitChanges = require('../monorepo/check-for-git-changes'); -const {failIfTagExists} = require('../releases/utils/release-utils'); -const { - isReleaseBranch, - parseVersion, -} = require('../releases/utils/version-utils'); -const {exitIfNotOnGit, getBranchName} = require('../scm-utils'); -const {getPackages} = require('../utils/monorepo'); -const chalk = require('chalk'); -const inquirer = require('inquirer'); -const request = require('request'); -const {echo, exit} = require('shelljs'); -const yargs = require('yargs'); - -/** - * This script walks a releaser through bumping the version for a release - * It will commit the appropriate tags to trigger the CircleCI jobs. - */ - -let argv = yargs - .option('r', { - alias: 'remote', - default: 'origin', - }) - .option('t', { - alias: 'token', - describe: - 'Your GitHub personal API token. See https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens.', - required: true, - }) - .option('v', { - alias: 'to-version', - describe: 'Version you aim to release, ex. 0.67.0-rc.1, 0.66.3', - required: true, - }) - .option('dry-run', { - type: 'boolean', - default: false, - }) - .check(() => { - const branch = exitIfNotOnGit( - () => getBranchName(), - "Not in git. You can't invoke trigger-react-native-release from outside a git repo.", - ); - exitIfNotOnReleaseBranch(branch); - return true; - }).argv; - -function exitIfNotOnReleaseBranch(branch /*: string */) { - if (!isReleaseBranch(branch)) { - console.log( - 'This script must be run in a react-native git repository checkout and on a release branch', - ); - exit(1); - } -} - -/** - * Get the next version that all workspace packages will be set to. - * - * This approach is specific to the 0.74 release. For 0.75, the `--to-version` - * value will be used instead, setting all packages to a single version. - */ -async function getNextMonorepoPackagesVersion() /*: Promise */ { - // Based on last publish before this strategy - const _0_74_MIN_PATCH = 75; - - const packages = await getPackages({ - includeReactNative: false, - }); - - let patchVersion = _0_74_MIN_PATCH; - - for (const pkg of Object.values(packages)) { - const {version} = pkg.packageJson; - - if (!version.startsWith('0.74.') || version.endsWith('-main')) { - return null; - } - - const {patch} = parseVersion(version, 'release'); - patchVersion = Math.max(patchVersion, parseInt(patch, 10) + 1); - } - - return '0.74.' + patchVersion; -} - -function triggerReleaseWorkflow(options /*: $FlowFixMe */) { - return new Promise((resolve, reject) => { - request(options, function (error, response, body) { - if (error) { - reject(error); - } else { - resolve(body); - } - }); - }); -} - -async function main() { - const branch = exitIfNotOnGit( - () => getBranchName(), - "Not in git. You can't invoke trigger-react-native-release from outside a git repo.", - ); - - // check for uncommitted changes - if (checkForGitChanges()) { - echo( - chalk.red( - 'Found uncommitted changes. Please commit or stash them before running this script', - ), - ); - exit(1); - } - - // $FlowFixMe[prop-missing] - const token = argv.token; - // $FlowFixMe[prop-missing] - const releaseVersion = argv.toVersion; - failIfTagExists(releaseVersion, 'release'); - - const {pushed} = await inquirer.prompt({ - type: 'confirm', - name: 'pushed', - message: `This script will trigger a release with whatever changes are on the remote branch: ${branch}. \nMake sure you have pushed any updates remotely.`, - }); - - if (!pushed) { - // $FlowFixMe[prop-missing] - console.log(`Please run 'git push ${argv.remote} ${branch}'`); - exit(1); - return; - } - - let latest = false; - const {version, prerelease} = parseVersion(releaseVersion, 'release'); - if (!prerelease) { - const {setLatest} = await inquirer.prompt({ - type: 'confirm', - name: 'setLatest', - message: `Do you want to set ${version} as "latest" release on npm?`, - }); - latest = setLatest; - } - - const npmTag = latest ? 'latest' : !prerelease ? branch : 'next'; - const {confirmRelease} = await inquirer.prompt({ - type: 'confirm', - name: 'confirmRelease', - message: `Releasing version "${version}" with npm tag "${npmTag}". Is this correct?`, - }); - - if (!confirmRelease) { - console.log('Aborting.'); - return; - } - - let nextMonorepoPackagesVersion = await getNextMonorepoPackagesVersion(); - - if (nextMonorepoPackagesVersion == null) { - // TODO(T182538198): Once this warning is hit, we can remove the - // `release_monorepo_packages_version` logic from here and the CI jobs, - // see other TODOs. - console.warn( - 'Warning: No longer on the 0.74-stable branch, meaning we will ' + - 'write all package versions identically. Please double-check the ' + - 'generated diff to see if this is correct.', - ); - nextMonorepoPackagesVersion = version; - } - - const parameters = { - version: version, - tag: npmTag, - // $FlowFixMe[prop-missing] - dry_run: argv.dryRun, - }; - - const options = { - method: 'POST', - url: 'https://api.github.com/repos/facebook/react-native/actions/workflows/prepare-release/dispatches', - headers: { - Authorization: `Bearer ${token}`, - Accept: 'application/vnd.github+json', - 'X-GitHub-Api-Version': '2022-11-28', - }, - body: { - ref: branch, - inputs: parameters, - }, - json: true, - }; - - // See response: https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#create-a-workflow-dispatch-event - await triggerReleaseWorkflow(options); - console.log( - // $FlowFixMe[incompatible-use] - 'Monitor your release workflow: https://github.com/facebook/react-native/actions/workflows/prepare-release.yml', - ); - - // TODO - // - Output the release changelog to paste into Github releases - // - Link to release discussions to update - // - Verify RN-diff publish is through -} - -// $FlowFixMe[unused-promise] -main().then(() => { - exit(0); -}); diff --git a/scripts/releases/README.md b/scripts/releases/README.md index 2c9f4ce06f8928..98e663a665c2d5 100644 --- a/scripts/releases/README.md +++ b/scripts/releases/README.md @@ -21,3 +21,12 @@ Updates relevant files in the `react-native` package and template to materialize ### `update-template-package` Updates workspace dependencies in the template app`package.json` + +### `validate-version` + +Takes a version and a build type and validates whether the version passed is valid for the given build type +It is intended to use in CI + +### `compute-npm-tag` + +Takes a version, a branch and whether we want to explicitly tag the release as "latest" and outputs the most appropriate NPM tag for the release. diff --git a/scripts/releases/compute-npm-tag.js b/scripts/releases/compute-npm-tag.js new file mode 100644 index 00000000000000..79616005b0d760 --- /dev/null +++ b/scripts/releases/compute-npm-tag.js @@ -0,0 +1,64 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + * @oncall react_native + */ + +const {parseVersion} = require('./utils/version-utils'); +const {parseArgs} = require('@pkgjs/parseargs'); + +const config = { + options: { + is_latest_on_npm: { + type: 'boolean', + short: 'l', + default: false, + }, + version: { + type: 'string', + short: 'v', + }, + branch: { + type: 'string', + short: 'b', + }, + help: {type: 'boolean'}, + }, +}; + +async function main() { + const { + values: {help, is_latest_on_npm: latest, version: version, branch: branch}, + } = parseArgs(config); + + if (help) { + console.log(` + Usage: node ./scripts/releases/compute-npm-tag.js [OPTIONS] + + Validates a version string for a given build type. + + Options: + --is-latest-on-npm Whether we need to publish this as latest on NPM or not. Defaults to false. + --version The new version string. + --branch The branch name. + `); + return; + } + + const {prerelease} = parseVersion(version, 'release'); + + const npmTag = latest ? 'latest' : !prerelease ? branch : 'next'; + + console.log(npmTag); + return; +} + +if (require.main === module) { + // eslint-disable-next-line no-void + void main(); +} diff --git a/scripts/releases/validate-version.js b/scripts/releases/validate-version.js new file mode 100644 index 00000000000000..5c29f5a3c4dde0 --- /dev/null +++ b/scripts/releases/validate-version.js @@ -0,0 +1,60 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + * @oncall react_native + */ + +const {parseVersion, validateBuildType} = require('./utils/version-utils'); +const {parseArgs} = require('@pkgjs/parseargs'); + +const config = { + options: { + 'build-type': { + type: 'string', + short: 'b', + }, + version: { + type: 'string', + short: 'v', + }, + help: {type: 'boolean'}, + }, +}; + +async function main() { + const { + values: {help, 'build-type': buildType, version: version}, + } = parseArgs(config); + + if (help) { + console.log(` + Usage: node ./scripts/releases/validate-version.js [OPTIONS] + + Validates a version string for a given build type. + + Options: + --build-type One of ['dry-run', 'nightly', 'release', 'prealpha']. + --version The new version string. + `); + return; + } + + if (!validateBuildType(buildType)) { + throw new Error(`Unsupported build type: ${buildType}`); + } + + parseVersion(version, 'release'); + + console.log(`Version ${version} is valid for ${buildType} build.`); + return; +} + +if (require.main === module) { + // eslint-disable-next-line no-void + void main(); +}