diff --git a/scripts/release/README.md b/scripts/release/README.md index 5e7dfda309097..7d7ab15a389cd 100644 --- a/scripts/release/README.md +++ b/scripts/release/README.md @@ -30,14 +30,10 @@ The sections below include meaningful `--tags` in the instructions. However, kee To prepare a build for a particular commit: 1. Choose a commit from [the commit log](https://github.com/facebook/react/commits/master). -2. Click the "“✓" icon and click the Circle CI "Details" link. -3. Select the `process_artifacts ` job (**not** the `process_artifacts_experimental`job; see the next section) - * If it's still pending, you'll need to wait for it to finish. 1 -4. Copy the build ID from the URL - * e.g. the build ID for commit [e5d06e34b](https://github.com/facebook/react/commit/e5d06e34b) is [**124756**](https://circleci.com/gh/facebook/react/124756) -5. Run the [`prepare-release-from-ci`](#prepare-release-from-ci) script with the build ID 2 you found: +2. Copy the SHA (by clicking the 📋 button) +5. Run the [`prepare-release-from-ci`](#prepare-release-from-ci) script with the SHA 1 you found: ```sh -scripts/release/prepare-release-from-ci.js --build=124756 +scripts/release/prepare-release-from-ci.js -r stable --commit=0e526bc ``` Once the build has been checked out and tested locally, you're ready to publish it: @@ -45,20 +41,16 @@ Once the build has been checked out and tested locally, you're ready to publish scripts/release/publish.js --tags next ``` -If the OTP code expires while publishing, re-run this command and answer "y" to the questions about whether it was expected for already published packages. - -1: This is the most awkward part of cutting a release right now. We have plans to improve it.
-2: You can omit the `build` param if you just want to release the latest commit as to "next". +1: You can omit the `commit` param if you just want to release the latest commit as to "next". ## Publishing an Experimental Release Experimental releases are special because they have additional features turned on. -The steps for publishing an experimental release are almost the same as for publishing a "next" release, except in step 3 you should choose the `process_artifacts_experimental ` job (instead of `process_artifacts`) 1 +The steps for publishing an experimental release are almost the same as for publishing a "next" release except for the release channel (`-r`) flag. -For example, the experimental build ID for commit [e5d06e34b](https://github.com/facebook/react/commit/e5d06e34b) is [**124763**](https://circleci.com/gh/facebook/react/124763): ```sh -scripts/release/prepare-release-from-ci.js --build=124763 +scripts/release/prepare-release-from-ci.js -r experimental --commit=0e526bc ``` Once the build has been checked out and tested locally, you're ready to publish it. When publishing an experimental release, use the `experimental` tag: @@ -67,10 +59,6 @@ Once the build has been checked out and tested locally, you're ready to publish scripts/release/publish.js --tags experimental ``` -If the OTP code expires while publishing, re-run this command and answer "y" to the questions about whether it was expected for already published packages. - -1: We have plans to make this less awkward. Ideally these releases will be published by a cron job. - ## Publishing a Stable Release Stable releases should always be created from the "next" channel. This encourages better testing of the actual release artifacts and reduces the chance of unintended changes accidentally being included in a stable release. @@ -92,8 +80,6 @@ scripts/release/publish.js --tags latest scripts/release/publish.js --tags latest next ``` -If the OTP code expires while publishing, re-run this command and answer "y" to the questions about whether it was expected for already published packages. - After successfully publishing the release, follow the on-screen instructions to ensure that all of the appropriate post-release steps are executed. 1: You can omit the `version` param if you just want to promote the latest "next" candidate to stable. @@ -145,9 +131,9 @@ Downloads build artifacts from Circle CI in preparation to be published to NPM a All artifacts built by Circle CI have already been unit-tested (both source and bundles) but these candidates should **always be manually tested** before being published. Upon completion, this script prints manual testing instructions. #### Example usage -To prepare the artifacts created by [Circle CI build 124756](https://circleci.com/gh/facebook/react/124756#artifacts/containers/0) you would run: +To prepare the artifacts created by Circle CI for commit [0e526bc](https://github.com/facebook/react/commit/0e526bc) you would run: ```sh -scripts/release/prepare-release-from-ci.js --build=124756 +scripts/release/prepare-release-from-ci.js --commit=0e526bc -r stable ``` ## `prepare-release-from-npm` diff --git a/scripts/release/download-experimental-build-commands/print-summary.js b/scripts/release/download-experimental-build-commands/print-summary.js index 2edf35f96fa1d..21e97c0dd504a 100644 --- a/scripts/release/download-experimental-build-commands/print-summary.js +++ b/scripts/release/download-experimental-build-commands/print-summary.js @@ -5,8 +5,9 @@ const clear = require('clear'); const {join, relative} = require('path'); const theme = require('../theme'); +const {getCommitFromCurrentBuild} = require('../utils'); -module.exports = ({build}) => { +module.exports = async () => { const commandPath = relative( process.env.PWD, join(__dirname, '../download-experimental-build.js') @@ -14,11 +15,13 @@ module.exports = ({build}) => { clear(); + const commit = await getCommitFromCurrentBuild(); + const message = theme` {caution An experimental build has been downloaded!} You can download this build again by running: - {path ${commandPath}} --build={build ${build}} + {path ${commandPath}} --commit={commit ${commit}} `; console.log(message.replace(/\n +/g, '\n').trim()); diff --git a/scripts/release/download-experimental-build.js b/scripts/release/download-experimental-build.js index 2bd77a67d1221..e722306d6039c 100755 --- a/scripts/release/download-experimental-build.js +++ b/scripts/release/download-experimental-build.js @@ -17,6 +17,9 @@ const printSummary = require('./download-experimental-build-commands/print-summa const run = async () => { try { addDefaultParamValue('-r', '--releaseChannel', 'experimental'); + + // Default to the latest commit in master. + // If this is a reproducible build (e.g. Firefox tester) a --commit will be specified. addDefaultParamValue(null, '--commit', 'master'); const params = await parseParams(); diff --git a/scripts/release/prepare-release-from-ci.js b/scripts/release/prepare-release-from-ci.js index 12b0b76409a7d..76885ff7af9ad 100755 --- a/scripts/release/prepare-release-from-ci.js +++ b/scripts/release/prepare-release-from-ci.js @@ -3,11 +3,10 @@ 'use strict'; const {join} = require('path'); -const {handleError} = require('./utils'); +const {addDefaultParamValue, handleError} = require('./utils'); const checkEnvironmentVariables = require('./shared-commands/check-environment-variables'); const downloadBuildArtifacts = require('./shared-commands/download-build-artifacts'); -const getLatestMasterBuildNumber = require('./shared-commands/get-latest-master-build-number'); const parseParams = require('./shared-commands/parse-params'); const printPrereleaseSummary = require('./shared-commands/print-prerelease-summary'); const testPackagingFixture = require('./shared-commands/test-packaging-fixture'); @@ -15,13 +14,11 @@ const testTracingFixture = require('./shared-commands/test-tracing-fixture'); const run = async () => { try { + addDefaultParamValue(null, '--commit', 'master'); + const params = await parseParams(); params.cwd = join(__dirname, '..', '..'); - if (!params.build) { - params.build = await getLatestMasterBuildNumber(false); - } - await checkEnvironmentVariables(params); await downloadBuildArtifacts(params); diff --git a/scripts/release/shared-commands/download-build-artifacts.js b/scripts/release/shared-commands/download-build-artifacts.js index 4d5259e24d1c7..cca62baabdefe 100644 --- a/scripts/release/shared-commands/download-build-artifacts.js +++ b/scripts/release/shared-commands/download-build-artifacts.js @@ -50,9 +50,9 @@ const run = async ({build, cwd, releaseChannel}) => { await exec(`cp -r ./build2/${sourceDir} ./build/node_modules`, {cwd}); }; -module.exports = async ({build, cwd, releaseChannel}) => { +module.exports = async ({build, commit, cwd, releaseChannel}) => { return logPromise( run({build, cwd, releaseChannel}), - theme`Downloading artifacts from Circle CI for build {build ${build}}` + theme`Downloading artifacts from Circle CI for commit {commit ${commit}} (build {build ${build}})` ); }; diff --git a/scripts/release/shared-commands/get-latest-master-build-number.js b/scripts/release/shared-commands/get-latest-master-build-number.js deleted file mode 100644 index d0bcd56e2e042..0000000000000 --- a/scripts/release/shared-commands/get-latest-master-build-number.js +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -const http = require('request-promise-json'); -const {logPromise} = require('../utils'); - -const run = async () => { - // https://circleci.com/docs/api/#recent-builds-for-a-project-branch - const metadataURL = `https://circleci.com/api/v1.1/project/github/facebook/react/tree/master`; - const metadata = await http.get(metadataURL, true); - const build = metadata.find( - entry => - entry.branch === 'master' && - entry.status === 'success' && - entry.workflows.job_name === 'process_artifacts_combined' - ).build_num; - - return build; -}; - -module.exports = async params => { - return logPromise( - run(params), - 'Determining latest Circle CI for the master branch' - ); -}; diff --git a/scripts/release/shared-commands/parse-params.js b/scripts/release/shared-commands/parse-params.js index 9af398cbc852c..5baec235b661b 100644 --- a/scripts/release/shared-commands/parse-params.js +++ b/scripts/release/shared-commands/parse-params.js @@ -4,15 +4,9 @@ const commandLineArgs = require('command-line-args'); const getBuildIdForCommit = require('../get-build-id-for-commit'); +const theme = require('../theme'); const paramDefinitions = [ - { - name: 'build', - type: Number, - description: - 'Circle CI build identifier (e.g. https://circleci.com/gh/facebook/react/)', - defaultValue: null, - }, { name: 'commit', type: String, @@ -37,29 +31,25 @@ const paramDefinitions = [ module.exports = async () => { const params = commandLineArgs(paramDefinitions); - if (params.build !== null) { - // TODO: Should we just remove the `build` param? Seems like `commit` is a - // sufficient replacement. - } else { - if (params.commit === null) { - console.error('Must provide either `build` or `commit`.'); - process.exit(1); - } - try { - params.build = await getBuildIdForCommit(params.commit); - } catch (error) { - console.error(error.message); - process.exit(1); - } - } - const channel = params.releaseChannel; if (channel !== 'experimental' && channel !== 'stable') { console.error( - `Invalid release channel (-r) "${channel}". Must be "stable" or "experimental".` + theme.error`Invalid release channel (-r) "${channel}". Must be "stable" or "experimental".` ); process.exit(1); } + if (params.commit === null) { + console.error(theme.error`No --commit param specified.`); + process.exit(1); + } + + try { + params.build = await getBuildIdForCommit(params.commit); + } catch (error) { + console.error(theme.error(error)); + process.exit(1); + } + return params; }; diff --git a/scripts/release/theme.js b/scripts/release/theme.js index 5c23185cba8ce..5e3ef374f962f 100644 --- a/scripts/release/theme.js +++ b/scripts/release/theme.js @@ -15,6 +15,7 @@ theme.package = theme.hex(colors.green); theme.version = theme.hex(colors.yellow); theme.tag = theme.hex(colors.yellow); theme.build = theme.hex(colors.yellow); +theme.commit = theme.hex(colors.yellow); theme.error = theme.hex(colors.red).bold; theme.dimmed = theme.hex(colors.gray); theme.caution = theme.hex(colors.red).bold; diff --git a/scripts/release/utils.js b/scripts/release/utils.js index 08ffdeb7c8c92..3aa81329c166c 100644 --- a/scripts/release/utils.js +++ b/scripts/release/utils.js @@ -3,7 +3,7 @@ const {exec} = require('child-process-promise'); const {createPatch} = require('diff'); const {hashElement} = require('folder-hash'); -const {readFileSync, writeFileSync} = require('fs'); +const {existsSync, readFileSync, writeFileSync} = require('fs'); const {readJson, writeJson} = require('fs-extra'); const http = require('request-promise-json'); const logUpdate = require('log-update'); @@ -47,6 +47,16 @@ const execRead = async (command, options) => { return stdout.trim(); }; +const extractCommitFromVersionNumber = version => { + // Support stable version format e.g. "0.0.0-0e526bcec" + // and experimental version format e.g. "0.0.0-experimental-0e526bcec" + const match = version.match(/0\.0\.0\-([a-z]+\-){0,1}(.+)/); + if (match === null) { + throw Error(`Could not extra commit from version "${version}"`); + } + return match[2]; +}; + const getArtifactsList = async buildID => { const buildMetadataURL = `https://circleci.com/api/v1.1/project/github/facebook/react/${buildID}?circle-token=${process.env.CIRCLE_CI_API_TOKEN}`; const buildMetadata = await http.get(buildMetadataURL, true); @@ -115,6 +125,35 @@ const getChecksumForCurrentRevision = async cwd => { return hashedPackages.hash.slice(0, 7); }; +const getCommitFromCurrentBuild = async () => { + const cwd = join(__dirname, '..', '..'); + + // If this build includes a build-info.json file, extract the commit from it. + // Otherwise fall back to parsing from the package version number. + // This is important to make the build reproducible (e.g. by Mozilla reviewers). + const buildInfoJSON = join( + cwd, + 'build2', + 'oss-experimental', + 'react', + 'build-info.json' + ); + if (existsSync(buildInfoJSON)) { + const buildInfo = await readJson(buildInfoJSON); + return buildInfo.commit; + } else { + const packageJSON = join( + cwd, + 'build2', + 'oss-experimental', + 'react', + 'package.json' + ); + const {version} = await readJson(packageJSON); + return extractCommitFromVersionNumber(version); + } +}; + const getPublicPackages = isExperimental => { if (isExperimental) { return [ @@ -270,6 +309,7 @@ module.exports = { getArtifactsList, getBuildInfo, getChecksumForCurrentRevision, + getCommitFromCurrentBuild, getPublicPackages, handleError, logPromise,