From cfc90969e6569b5ce2f9f6f203b120ee7cd11f58 Mon Sep 17 00:00:00 2001 From: Luna Wei Date: Fri, 21 Jan 2022 00:12:44 -0800 Subject: [PATCH] Use CircleCI API to trigger releases --- .circleci/config.yml | 52 ++++++++--- scripts/__tests__/version-utils-test.js | 38 -------- scripts/bump-oss-version.js | 117 ++++++++++++++---------- scripts/prepare-package-for-release.js | 61 +++++------- scripts/version-utils.js | 19 ---- 5 files changed, 132 insertions(+), 155 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4d8caab23bef10..120ad25cd5008a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -733,6 +733,12 @@ jobs: # JOBS: Releases # ------------------------- prepare_package_for_release: + parameters: + version: + type: string + latest: + type: boolean + default: false executor: reactnativeios steps: - checkout @@ -743,7 +749,7 @@ jobs: - run: name: "Set new react-native version and commit changes" command: | - node ./scripts/prepare-package-for-release.js + node ./scripts/prepare-package-for-release.js -v << parameters.version >> -l << parameters.latest >> build_npm_package: parameters: @@ -827,13 +833,35 @@ jobs: command: | echo "Nightly build run" + +# ------------------------- +# PIPELINE PARAMETERS +# ------------------------- +parameters: + run_package_release_workflow_only: + default: false + type: boolean + + release_latest: + default: false + type: boolean + + release_version: + default: "9999" + type: string + # ------------------------- # WORK FLOWS +# +# When creating a new workflow, make sure to include condition `unless: << pipeline.parameters.run_package_release_workflow_only >>` +# It's setup this way so we can trigger a release via a POST +# See limitations: https://support.circleci.com/hc/en-us/articles/360050351292-How-to-trigger-a-workflow-via-CircleCI-API-v2 # ------------------------- workflows: version: 2 tests: + unless: << pipeline.parameters.run_package_release_workflow_only >> jobs: - build_npm_package: # Build a release package on every untagged commit, but do not publish to npm. @@ -899,20 +927,20 @@ workflows: ignore: gh-pages run_disabled_tests: false - releases: + # This workflow should only be triggered by release script + package_release: + when: << pipeline.parameters.run_package_release_workflow_only >> jobs: - # This job will trigger on relevant release branch pushes. - # It prepares the package and triggers `build_npm_package` for release + # This job will trigger publish_release workflow - prepare_package_for_release: name: prepare_package_for_release - # Since CircleCI does not support branch AND tag filters, we manually check in job - # and no-op if there is no `publish-v{version}` tag set. - filters: - branches: - only: - - /^(\d+)\.(\d+)-stable$/ + version: << pipeline.parameters.release_version >> + latest : << pipeline.parameters.release_latest >> - # This job will trigger when a version tag is pushed (by prepare_package_for_release) + publish_release: + unless: << pipeline.parameters.run_package_release_workflow_only >> + jobs: + # This job will trigger when a version tag is pushed (by package_release) - build_npm_package: name: build_and_publish_npm_package context: react-native-bot @@ -930,6 +958,7 @@ workflows: only: /v[0-9]+(\.[0-9]+)*(\-rc(\.[0-9]+)?)?/ analysis: + unless: << pipeline.parameters.run_package_release_workflow_only >> jobs: # Run lints on every commit other than those to the gh-pages branch - analyze_code: @@ -950,6 +979,7 @@ workflows: ignore: gh-pages nightly: + unless: << pipeline.parameters.run_package_release_workflow_only >> triggers: - schedule: cron: "0 20 * * *" diff --git a/scripts/__tests__/version-utils-test.js b/scripts/__tests__/version-utils-test.js index e15a4d1778ffb8..d8bf22f400a958 100644 --- a/scripts/__tests__/version-utils-test.js +++ b/scripts/__tests__/version-utils-test.js @@ -10,9 +10,7 @@ const { parseVersion, isTaggedLatest, - getPublishVersion, isReleaseBranch, - getPublishTag, } = require('../version-utils'); let execResult = null; @@ -49,42 +47,6 @@ describe('version-utils', () => { }); }); - describe('getPublishTag', () => { - it('Should return null no tags are returned', () => { - execResult = '\n'; - expect(getPublishTag()).toBe(null); - }); - it('Should return tag', () => { - execResult = 'publish-v999.0.0-rc.0\n'; - expect(getPublishTag()).toBe('publish-v999.0.0-rc.0'); - }); - }); - - describe('getPublishVersion', () => { - it('Should return null if invalid tag provided', () => { - expect(getPublishVersion('')).toBe(null); - expect(getPublishVersion('something')).toBe(null); - }); - it('should throw error if invalid tag version provided', () => { - function testInvalidVersion() { - getPublishVersion('publish-'); - } - expect(testInvalidVersion).toThrowErrorMatchingInlineSnapshot( - `"You must pass a correctly formatted version; couldn't parse "`, - ); - }); - it('Should return version for tag', () => { - const {version, major, minor, patch, prerelease} = getPublishVersion( - 'publish-v0.67.0-rc.6', - ); - expect(version).toBe('0.67.0-rc.6'); - expect(major).toBe('0'); - expect(minor).toBe('67'); - expect(patch).toBe('0'); - expect(prerelease).toBe('rc.6'); - }); - }); - describe('parseVersion', () => { it('should throw error if invalid match', () => { function testInvalidVersion() { diff --git a/scripts/bump-oss-version.js b/scripts/bump-oss-version.js index f5c485f0f8b8ba..2cfd230fd73ede 100755 --- a/scripts/bump-oss-version.js +++ b/scripts/bump-oss-version.js @@ -17,6 +17,8 @@ const {exec, exit} = require('shelljs'); const yargs = require('yargs'); const inquirer = require('inquirer'); +const request = require('request'); + const { parseVersion, isReleaseBranch, @@ -28,6 +30,17 @@ let argv = yargs alias: 'remote', default: 'origin', }) + .option('t', { + alias: 'token', + describe: + 'Your CircleCI personal API token. See https://circleci.com/docs/2.0/managing-api-tokens/#creating-a-personal-api-token to set one', + required: true, + }) + .option('v', { + alias: 'to-version', + describe: 'Version you aim to release, ex. 0.67.0-rc.1, 0.66.3', + required: true, + }) .check(() => { const branch = getBranchName(); exitIfNotOnReleaseBranch(branch); @@ -56,47 +69,47 @@ function getLatestTag(versionPrefix) { return null; } +function triggerReleaseWorkflow(options) { + return new Promise((resolve, reject) => { + request(options, function (error, response, body) { + if (error) { + reject(error); + } else { + resolve(body); + } + }); + }); +} + async function main() { const branch = getBranchName(); + const token = argv.token; + const releaseVersion = argv.toVersion; - const {pulled} = await inquirer.prompt({ + const {pushed} = await inquirer.prompt({ type: 'confirm', - name: 'pulled', - message: `You are currently on branch: ${branch}. Have you run "git pull ${argv.remote} ${branch} --tags"?`, + 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 (!pulled) { - console.log(`Please run 'git pull ${argv.remote} ${branch} --tags'`); + if (!pushed) { + console.log(`Please run 'git push ${argv.remote} ${branch}'`); exit(1); return; } - const lastVersionTag = getLatestTag(branch.replace('-stable', '')); - const lastVersion = lastVersionTag - ? parseVersion(lastVersionTag).version - : null; - const lastVersionMessage = lastVersion - ? `Last version tagged is ${lastVersion}.\n` - : ''; - - const {releaseVersion} = await inquirer.prompt({ - type: 'input', - name: 'releaseVersion', - message: `What version are you releasing? (Ex. 0.66.0-rc.4)\n${lastVersionMessage}`, - }); - - let setLatest = false; - + let latest = false; const {version, prerelease} = parseVersion(releaseVersion); if (!prerelease) { - const {latest} = await inquirer.prompt({ + const {setLatest} = await inquirer.prompt({ type: 'confirm', - name: 'latest', - message: 'Set this version as "latest" on npm?', + name: 'setLatest', + message: `Do you want to set ${version} as "latest" release on npm?`, }); - setLatest = latest; + latest = setLatest; } - const npmTag = setLatest ? 'latest' : !prerelease ? branch : 'next'; + + const npmTag = latest ? 'latest' : !prerelease ? branch : 'next'; const {confirmRelease} = await inquirer.prompt({ type: 'confirm', name: 'confirmRelease', @@ -108,32 +121,36 @@ async function main() { return; } - if ( - exec(`git tag -a publish-v${version} -m "publish version ${version}"`).code - ) { - console.error(`Failed to tag publish-v${version}`); - exit(1); - return; - } - - if (setLatest) { - exec('git tag -d latest'); - exec(`git push ${argv.remote} :latest`); - exec('git tag -a latest -m "latest"'); - } - - if (exec(`git push ${argv.remote} ${branch} --follow-tags`).code) { - console.error(`Failed to push tag publish-v${version}`); - exit(1); - return; - } + const parameters = { + release_version: version, + release_latest: latest, + run_package_release_workflow_only: true, + }; + + const options = { + method: 'POST', + url: 'https://circleci.com/api/v2/project/github/facebook/react-native/pipeline', + headers: { + 'Circle-Token': token, + 'content-type': 'application/json', + }, + body: { + branch, + parameters, + }, + json: true, + }; + + // See response: https://circleci.com/docs/api/v2/#operation/triggerPipeline + const body = await triggerReleaseWorkflow(options); + console.log( + `Monitor your release workflow: https://app.circleci.com/pipelines/github/facebook/react-native/${body.number}`, + ); // TODO - // 1. Link to CircleCI job to watch - // 2. Output the release changelog to paste into Github releases - // 3. Link to release discussions to update - // 4. Verify RN-diff publish is through - // 5. General changelog update on PR? + // - Output the release changelog to paste into Github releases + // - Link to release discussions to update + // - Verify RN-diff publish is through } main().then(() => { diff --git a/scripts/prepare-package-for-release.js b/scripts/prepare-package-for-release.js index ae80c4831d93b6..6018ee816f95ad 100755 --- a/scripts/prepare-package-for-release.js +++ b/scripts/prepare-package-for-release.js @@ -11,7 +11,7 @@ /** * This script prepares a release package to be pushed to npm - * It is run by CircleCI on a push to a release branch + * It is triggered to run on CircleCI * It will: * * It updates the version in json/gradle files and makes sure they are consistent between each other (set-rn-version) * * Updates podfile for RNTester @@ -20,51 +20,41 @@ */ const {echo, exec, exit} = require('shelljs'); const yargs = require('yargs'); -const { - isReleaseBranch, - isTaggedLatest, - getPublishVersion, - getPublishTag, -} = require('./version-utils'); - -const argv = yargs.option('r', { - alias: 'remote', - default: 'origin', -}).argv; +const {isReleaseBranch, parseVersion} = require('./version-utils'); + +const argv = yargs + .option('r', { + alias: 'remote', + default: 'origin', + }) + .option('v', { + alias: 'to-version', + type: 'string', + required: true, + }) + .option('l', { + alias: 'latest', + type: 'boolean', + default: false, + }).argv; const currentCommit = process.env.CIRCLE_SHA1; const branch = process.env.CIRCLE_BRANCH; const remote = argv.remote; - -const tag = getPublishTag(); -if (tag == null) { - console.log( - 'No publish tag set. Not publishing this release.\nCircleCI cannot filter workflows on both branch and tag so we do this check in prepare-package-for-release', - ); - exit(0); -} +const releaseVersion = argv.toVersion; +const isLatest = argv.latest; if (!isReleaseBranch(branch)) { console.error(`This needs to be on a release branch. On branch: ${branch}`); exit(1); } -// Get the version we're publishing from the publish tag -// Tag of the form `publish-v{versionStr}` -const versionInfo = getPublishVersion(tag); -if (versionInfo == null) { - console.error( - `Invalid tag provided: ${tag}, needs to be of form 'publish-v{major}.{minor}.{patch}'`, - ); +const {version} = parseVersion(releaseVersion); +if (version == null) { + console.error(`Invalid version provided: ${releaseVersion}`); exit(1); } -// Clean up tag now that we're publishing the release. -exec(`git tag -d ${tag}`); -exec(`git push ${remote} :${tag}`); - -const {version} = versionInfo; - if (exec(`node scripts/set-rn-version.js --to-version ${version}`).code) { echo(`Failed to set React Native version to ${version}`); exit(1); @@ -94,10 +84,7 @@ if (exec(`git tag -a v${version} -m "v${version}"`).code) { exit(1); } -// See if `latest` was set on the commit that triggered this script -// If yes, move the tag to commit we just made -// This tag will also update npm release as `latest` -const isLatest = isTaggedLatest(currentCommit); +// If `isLatest`, this git tag will also set npm release as `latest` if (isLatest) { exec('git tag -d latest'); exec(`git push ${remote} :latest`); diff --git a/scripts/version-utils.js b/scripts/version-utils.js index 94c005cb8c6ffc..25eddd5c907249 100644 --- a/scripts/version-utils.js +++ b/scripts/version-utils.js @@ -38,15 +38,6 @@ function getBranchName() { }).stdout.trim(); } -function getPublishVersion(tag) { - if (!tag.startsWith('publish-')) { - return null; - } - - const versionStr = tag.replace('publish-', ''); - return parseVersion(versionStr); -} - function isTaggedLatest(commitSha) { return ( exec(`git rev-list -1 latest | grep ${commitSha}`, { @@ -55,19 +46,9 @@ function isTaggedLatest(commitSha) { ); } -function getPublishTag() { - // Assumes we only ever have one tag with the prefix `publish-v` - const tag = exec("git tag --points-at HEAD | grep 'publish-v'", { - silent: true, - }).stdout.trim(); - return tag ? tag : null; -} - module.exports = { getBranchName, isTaggedLatest, - getPublishTag, - getPublishVersion, parseVersion, isReleaseBranch, };