2017-10-17 06:01:14 +08:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
const {exec} = require('child-process-promise');
|
2018-11-24 04:37:18 +08:00
|
|
|
const {createPatch} = require('diff');
|
|
|
|
|
const {hashElement} = require('folder-hash');
|
2021-02-04 00:11:56 +08:00
|
|
|
const {existsSync, readFileSync, writeFileSync} = require('fs');
|
2018-11-24 04:37:18 +08:00
|
|
|
const {readJson, writeJson} = require('fs-extra');
|
2023-03-18 04:04:20 +08:00
|
|
|
const fetch = require('node-fetch');
|
2017-10-17 06:01:14 +08:00
|
|
|
const logUpdate = require('log-update');
|
2017-11-10 00:29:51 +08:00
|
|
|
const {join} = require('path');
|
2018-11-27 01:28:37 +08:00
|
|
|
const createLogger = require('progress-estimator');
|
2018-11-24 04:37:18 +08:00
|
|
|
const prompt = require('prompt-promise');
|
|
|
|
|
const theme = require('./theme');
|
2021-06-03 23:45:10 +08:00
|
|
|
const {stablePackages, experimentalPackages} = require('../../ReactVersions');
|
2018-11-24 04:37:18 +08:00
|
|
|
|
2018-11-27 01:28:37 +08:00
|
|
|
// https://www.npmjs.com/package/progress-estimator#configuration
|
|
|
|
|
const logger = createLogger({
|
|
|
|
|
storagePath: join(__dirname, '.progress-estimator'),
|
|
|
|
|
});
|
|
|
|
|
|
2021-02-03 05:40:31 +08:00
|
|
|
const addDefaultParamValue = (optionalShortName, longName, defaultValue) => {
|
2021-01-27 04:17:56 +08:00
|
|
|
let found = false;
|
|
|
|
|
for (let i = 0; i < process.argv.length; i++) {
|
|
|
|
|
const current = process.argv[i];
|
2021-02-03 05:40:31 +08:00
|
|
|
if (current === optionalShortName || current.startsWith(`${longName}=`)) {
|
2021-01-27 04:17:56 +08:00
|
|
|
found = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!found) {
|
2021-02-03 05:40:31 +08:00
|
|
|
process.argv.push(`${longName}=${defaultValue}`);
|
2021-01-27 04:17:56 +08:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2018-11-24 04:37:18 +08:00
|
|
|
const confirm = async message => {
|
|
|
|
|
const confirmation = await prompt(theme`\n{caution ${message}} (y/N) `);
|
|
|
|
|
prompt.done();
|
|
|
|
|
if (confirmation !== 'y' && confirmation !== 'Y') {
|
|
|
|
|
console.log(theme`\n{caution Release cancelled.}`);
|
|
|
|
|
process.exit(0);
|
|
|
|
|
}
|
|
|
|
|
};
|
2017-10-17 06:01:14 +08:00
|
|
|
|
|
|
|
|
const execRead = async (command, options) => {
|
|
|
|
|
const {stdout} = await exec(command, options);
|
|
|
|
|
|
|
|
|
|
return stdout.trim();
|
|
|
|
|
};
|
|
|
|
|
|
2021-02-04 00:11:56 +08:00
|
|
|
const extractCommitFromVersionNumber = version => {
|
2021-06-24 01:50:09 +08:00
|
|
|
// Support stable version format e.g. "0.0.0-0e526bcec-20210202"
|
|
|
|
|
// and experimental version format e.g. "0.0.0-experimental-0e526bcec-20210202"
|
|
|
|
|
const match = version.match(/0\.0\.0\-([a-z]+\-){0,1}([^-]+).+/);
|
2021-02-04 00:11:56 +08:00
|
|
|
if (match === null) {
|
|
|
|
|
throw Error(`Could not extra commit from version "${version}"`);
|
|
|
|
|
}
|
|
|
|
|
return match[2];
|
|
|
|
|
};
|
|
|
|
|
|
2019-06-05 04:28:41 +08:00
|
|
|
const getArtifactsList = async buildID => {
|
2023-03-18 04:04:20 +08:00
|
|
|
const {CIRCLE_CI_API_TOKEN} = process.env;
|
|
|
|
|
if (CIRCLE_CI_API_TOKEN == null) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Expected a CircleCI token to download artifacts, got ${CIRCLE_CI_API_TOKEN}`
|
|
|
|
|
);
|
|
|
|
|
}
|
2021-02-04 00:29:51 +08:00
|
|
|
const jobArtifactsURL = `https://circleci.com/api/v1.1/project/github/facebook/react/${buildID}/artifacts`;
|
2023-03-18 04:04:20 +08:00
|
|
|
const jobArtifacts = await fetch(jobArtifactsURL, {
|
|
|
|
|
headers: {
|
|
|
|
|
'Circle-Token': CIRCLE_CI_API_TOKEN,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
return jobArtifacts.json();
|
2019-06-05 04:28:41 +08:00
|
|
|
};
|
|
|
|
|
|
2018-11-24 04:37:18 +08:00
|
|
|
const getBuildInfo = async () => {
|
|
|
|
|
const cwd = join(__dirname, '..', '..');
|
2017-10-17 06:01:14 +08:00
|
|
|
|
Set up experimental builds (#17071)
* Don't bother including `unstable_` in error
The method names don't get stripped out of the production bundles
because they are passed as arguments to the error decoder.
Let's just always use the unprefixed APIs in the messages.
* Set up experimental builds
The experimental builds are packaged exactly like builds in the stable
release channel: same file structure, entry points, and npm package
names. The goal is to match what will eventually be released in stable
as closely as possible, but with additional features turned on.
Versioning and Releasing
------------------------
The experimental builds will be published to the same registry and
package names as the stable ones. However, they will be versioned using
a separate scheme. Instead of semver versions, experimental releases
will receive arbitrary version strings based on their content hashes.
The motivation is to thwart attempts to use a version range to match
against future experimental releases. The only way to install or depend
on an experimental release is to refer to the specific version number.
Building
--------
I did not use the existing feature flag infra to configure the
experimental builds. The reason is because feature flags are designed
to configure a single package. They're not designed to generate multiple
forks of the same package; for each set of feature flags, you must
create a separate package configuration.
Instead, I've added a new build dimension called the **release
channel**. By default, builds use the **stable** channel. There's
also an **experimental** release channel. We have the option to add more
in the future.
There are now two dimensions per artifact: build type (production,
development, or profiling), and release channel (stable or
experimental). These are separate dimensions because they are
combinatorial: there are stable and experimental production builds,
stable and experimental developmenet builds, and so on.
You can add something to an experimental build by gating on
`__EXPERIMENTAL__`, similar to how we use `__DEV__`. Anything inside
these branches will be excluded from the stable builds.
This gives us a low effort way to add experimental behavior in any
package without setting up feature flags or configuring a new package.
2019-10-15 01:46:42 +08:00
|
|
|
const isExperimental = process.env.RELEASE_CHANNEL === 'experimental';
|
|
|
|
|
|
2018-11-24 04:37:18 +08:00
|
|
|
const branch = await execRead('git branch | grep \\* | cut -d " " -f2', {
|
|
|
|
|
cwd,
|
|
|
|
|
});
|
2022-01-12 01:14:08 +08:00
|
|
|
const commit = await execRead('git show -s --no-show-signature --format=%h', {
|
|
|
|
|
cwd,
|
|
|
|
|
});
|
2018-11-24 04:37:18 +08:00
|
|
|
const checksum = await getChecksumForCurrentRevision(cwd);
|
2021-06-24 01:50:09 +08:00
|
|
|
const dateString = await getDateStringForCommit(commit);
|
Set up experimental builds (#17071)
* Don't bother including `unstable_` in error
The method names don't get stripped out of the production bundles
because they are passed as arguments to the error decoder.
Let's just always use the unprefixed APIs in the messages.
* Set up experimental builds
The experimental builds are packaged exactly like builds in the stable
release channel: same file structure, entry points, and npm package
names. The goal is to match what will eventually be released in stable
as closely as possible, but with additional features turned on.
Versioning and Releasing
------------------------
The experimental builds will be published to the same registry and
package names as the stable ones. However, they will be versioned using
a separate scheme. Instead of semver versions, experimental releases
will receive arbitrary version strings based on their content hashes.
The motivation is to thwart attempts to use a version range to match
against future experimental releases. The only way to install or depend
on an experimental release is to refer to the specific version number.
Building
--------
I did not use the existing feature flag infra to configure the
experimental builds. The reason is because feature flags are designed
to configure a single package. They're not designed to generate multiple
forks of the same package; for each set of feature flags, you must
create a separate package configuration.
Instead, I've added a new build dimension called the **release
channel**. By default, builds use the **stable** channel. There's
also an **experimental** release channel. We have the option to add more
in the future.
There are now two dimensions per artifact: build type (production,
development, or profiling), and release channel (stable or
experimental). These are separate dimensions because they are
combinatorial: there are stable and experimental production builds,
stable and experimental developmenet builds, and so on.
You can add something to an experimental build by gating on
`__EXPERIMENTAL__`, similar to how we use `__DEV__`. Anything inside
these branches will be excluded from the stable builds.
This gives us a low effort way to add experimental behavior in any
package without setting up feature flags or configuring a new package.
2019-10-15 01:46:42 +08:00
|
|
|
const version = isExperimental
|
2021-06-24 01:50:09 +08:00
|
|
|
? `0.0.0-experimental-${commit}-${dateString}`
|
|
|
|
|
: `0.0.0-${commit}-${dateString}`;
|
2018-11-24 04:37:18 +08:00
|
|
|
|
|
|
|
|
// Only available for Circle CI builds.
|
|
|
|
|
// https://circleci.com/docs/2.0/env-vars/
|
|
|
|
|
const buildNumber = process.env.CIRCLE_BUILD_NUM;
|
|
|
|
|
|
|
|
|
|
// React version is stored explicitly, separately for DevTools support.
|
2020-02-06 00:52:31 +08:00
|
|
|
// See updateVersionsForNext() below for more info.
|
2018-11-24 04:37:18 +08:00
|
|
|
const packageJSON = await readJson(
|
|
|
|
|
join(cwd, 'packages', 'react', 'package.json')
|
|
|
|
|
);
|
Set up experimental builds (#17071)
* Don't bother including `unstable_` in error
The method names don't get stripped out of the production bundles
because they are passed as arguments to the error decoder.
Let's just always use the unprefixed APIs in the messages.
* Set up experimental builds
The experimental builds are packaged exactly like builds in the stable
release channel: same file structure, entry points, and npm package
names. The goal is to match what will eventually be released in stable
as closely as possible, but with additional features turned on.
Versioning and Releasing
------------------------
The experimental builds will be published to the same registry and
package names as the stable ones. However, they will be versioned using
a separate scheme. Instead of semver versions, experimental releases
will receive arbitrary version strings based on their content hashes.
The motivation is to thwart attempts to use a version range to match
against future experimental releases. The only way to install or depend
on an experimental release is to refer to the specific version number.
Building
--------
I did not use the existing feature flag infra to configure the
experimental builds. The reason is because feature flags are designed
to configure a single package. They're not designed to generate multiple
forks of the same package; for each set of feature flags, you must
create a separate package configuration.
Instead, I've added a new build dimension called the **release
channel**. By default, builds use the **stable** channel. There's
also an **experimental** release channel. We have the option to add more
in the future.
There are now two dimensions per artifact: build type (production,
development, or profiling), and release channel (stable or
experimental). These are separate dimensions because they are
combinatorial: there are stable and experimental production builds,
stable and experimental developmenet builds, and so on.
You can add something to an experimental build by gating on
`__EXPERIMENTAL__`, similar to how we use `__DEV__`. Anything inside
these branches will be excluded from the stable builds.
This gives us a low effort way to add experimental behavior in any
package without setting up feature flags or configuring a new package.
2019-10-15 01:46:42 +08:00
|
|
|
const reactVersion = isExperimental
|
2021-06-24 01:50:09 +08:00
|
|
|
? `${packageJSON.version}-experimental-${commit}-${dateString}`
|
|
|
|
|
: `${packageJSON.version}-${commit}-${dateString}`;
|
2018-11-24 04:37:18 +08:00
|
|
|
|
|
|
|
|
return {branch, buildNumber, checksum, commit, reactVersion, version};
|
2017-10-17 06:01:14 +08:00
|
|
|
};
|
|
|
|
|
|
2018-11-24 04:37:18 +08:00
|
|
|
const getChecksumForCurrentRevision = async cwd => {
|
|
|
|
|
const packagesDir = join(cwd, 'packages');
|
|
|
|
|
const hashedPackages = await hashElement(packagesDir, {
|
|
|
|
|
encoding: 'hex',
|
|
|
|
|
files: {exclude: ['.DS_Store']},
|
2018-09-11 04:14:34 +08:00
|
|
|
});
|
2018-11-24 04:37:18 +08:00
|
|
|
return hashedPackages.hash.slice(0, 7);
|
2018-09-11 04:14:34 +08:00
|
|
|
};
|
|
|
|
|
|
2021-06-24 01:50:09 +08:00
|
|
|
const getDateStringForCommit = async commit => {
|
|
|
|
|
let dateString = await execRead(
|
2022-01-12 01:14:08 +08:00
|
|
|
`git show -s --no-show-signature --format=%cd --date=format:%Y%m%d ${commit}`
|
2021-06-24 01:50:09 +08:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// On CI environment, this string is wrapped with quotes '...'s
|
|
|
|
|
if (dateString.startsWith("'")) {
|
|
|
|
|
dateString = dateString.substr(1, 8);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return dateString;
|
|
|
|
|
};
|
|
|
|
|
|
2021-02-04 00:11:56 +08:00
|
|
|
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,
|
2021-09-22 03:12:52 +08:00
|
|
|
'build',
|
2021-02-04 00:11:56 +08:00
|
|
|
'oss-experimental',
|
|
|
|
|
'react',
|
|
|
|
|
'build-info.json'
|
|
|
|
|
);
|
|
|
|
|
if (existsSync(buildInfoJSON)) {
|
|
|
|
|
const buildInfo = await readJson(buildInfoJSON);
|
|
|
|
|
return buildInfo.commit;
|
|
|
|
|
} else {
|
|
|
|
|
const packageJSON = join(
|
|
|
|
|
cwd,
|
2021-09-22 03:12:52 +08:00
|
|
|
'build',
|
2021-02-04 00:11:56 +08:00
|
|
|
'oss-experimental',
|
|
|
|
|
'react',
|
|
|
|
|
'package.json'
|
|
|
|
|
);
|
|
|
|
|
const {version} = await readJson(packageJSON);
|
|
|
|
|
return extractCommitFromVersionNumber(version);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2020-12-19 06:20:37 +08:00
|
|
|
const getPublicPackages = isExperimental => {
|
2021-06-03 23:45:10 +08:00
|
|
|
const packageNames = Object.keys(stablePackages);
|
2020-12-19 06:20:37 +08:00
|
|
|
if (isExperimental) {
|
2021-06-03 23:45:10 +08:00
|
|
|
packageNames.push(...experimentalPackages);
|
2020-12-19 06:20:37 +08:00
|
|
|
}
|
2021-06-03 23:45:10 +08:00
|
|
|
return packageNames;
|
2017-11-10 00:29:51 +08:00
|
|
|
};
|
|
|
|
|
|
2018-11-24 04:37:18 +08:00
|
|
|
const handleError = error => {
|
|
|
|
|
logUpdate.clear();
|
|
|
|
|
|
|
|
|
|
const message = error.message.trim().replace(/\n +/g, '\n');
|
|
|
|
|
const stack = error.stack.replace(error.message, '');
|
|
|
|
|
|
|
|
|
|
console.log(theme`{error ${message}}\n\n{path ${stack}}`);
|
|
|
|
|
process.exit(1);
|
2017-10-17 06:01:14 +08:00
|
|
|
};
|
|
|
|
|
|
2018-11-27 01:28:37 +08:00
|
|
|
const logPromise = async (promise, text, estimate) =>
|
|
|
|
|
logger(promise, text, {estimate});
|
2017-10-17 06:01:14 +08:00
|
|
|
|
2018-11-24 04:37:18 +08:00
|
|
|
const printDiff = (path, beforeContents, afterContents) => {
|
|
|
|
|
const patch = createPatch(path, beforeContents, afterContents);
|
|
|
|
|
const coloredLines = patch
|
|
|
|
|
.split('\n')
|
|
|
|
|
.slice(2) // Trim index file
|
|
|
|
|
.map((line, index) => {
|
|
|
|
|
if (index <= 1) {
|
|
|
|
|
return theme.diffHeader(line);
|
|
|
|
|
}
|
|
|
|
|
switch (line[0]) {
|
|
|
|
|
case '+':
|
|
|
|
|
return theme.diffAdded(line);
|
|
|
|
|
case '-':
|
|
|
|
|
return theme.diffRemoved(line);
|
|
|
|
|
case ' ':
|
|
|
|
|
return line;
|
|
|
|
|
case '@':
|
|
|
|
|
return null;
|
|
|
|
|
case '\\':
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.filter(line => line);
|
|
|
|
|
console.log(coloredLines.join('\n'));
|
|
|
|
|
return patch;
|
|
|
|
|
};
|
2017-11-27 00:47:20 +08:00
|
|
|
|
2019-08-10 04:12:00 +08:00
|
|
|
// Convert an array param (expected format "--foo bar baz")
|
|
|
|
|
// to also accept comma input (e.g. "--foo bar,baz")
|
|
|
|
|
const splitCommaParams = array => {
|
|
|
|
|
for (let i = array.length - 1; i >= 0; i--) {
|
|
|
|
|
const param = array[i];
|
|
|
|
|
if (param.includes(',')) {
|
|
|
|
|
array.splice(i, 1, ...param.split(','));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2018-11-24 04:37:18 +08:00
|
|
|
// This method is used by both local Node release scripts and Circle CI bash scripts.
|
|
|
|
|
// It updates version numbers in package JSONs (both the version field and dependencies),
|
|
|
|
|
// As well as the embedded renderer version in "packages/shared/ReactVersion".
|
2021-06-24 01:50:09 +08:00
|
|
|
// Canaries version numbers use the format of 0.0.0-<sha>-<date> to be easily recognized (e.g. 0.0.0-01974a867-20200129).
|
2018-11-24 04:37:18 +08:00
|
|
|
// A separate "React version" is used for the embedded renderer version to support DevTools,
|
|
|
|
|
// since it needs to distinguish between different version ranges of React.
|
2021-06-24 01:50:09 +08:00
|
|
|
// It is based on the version of React in the local package.json (e.g. 16.12.0-01974a867-20200129).
|
2020-02-06 00:52:31 +08:00
|
|
|
// Both numbers will be replaced if the "next" release is promoted to a stable release.
|
|
|
|
|
const updateVersionsForNext = async (cwd, reactVersion, version) => {
|
2020-12-19 06:20:37 +08:00
|
|
|
const isExperimental = reactVersion.includes('experimental');
|
|
|
|
|
const packages = getPublicPackages(isExperimental);
|
2018-11-24 04:37:18 +08:00
|
|
|
const packagesDir = join(cwd, 'packages');
|
|
|
|
|
|
|
|
|
|
// Update the shared React version source file.
|
|
|
|
|
// This is bundled into built renderers.
|
|
|
|
|
// The promote script will replace this with a final version later.
|
|
|
|
|
const sourceReactVersionPath = join(cwd, 'packages/shared/ReactVersion.js');
|
|
|
|
|
const sourceReactVersion = readFileSync(
|
|
|
|
|
sourceReactVersionPath,
|
|
|
|
|
'utf8'
|
2020-02-29 05:09:02 +08:00
|
|
|
).replace(/export default '[^']+';/, `export default '${reactVersion}';`);
|
2018-11-24 04:37:18 +08:00
|
|
|
writeFileSync(sourceReactVersionPath, sourceReactVersion);
|
|
|
|
|
|
|
|
|
|
// Update the root package.json.
|
|
|
|
|
// This is required to pass a later version check script.
|
|
|
|
|
{
|
|
|
|
|
const packageJSONPath = join(cwd, 'package.json');
|
|
|
|
|
const packageJSON = await readJson(packageJSONPath);
|
|
|
|
|
packageJSON.version = version;
|
|
|
|
|
await writeJson(packageJSONPath, packageJSON, {spaces: 2});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < packages.length; i++) {
|
|
|
|
|
const packageName = packages[i];
|
|
|
|
|
const packagePath = join(packagesDir, packageName);
|
|
|
|
|
|
|
|
|
|
// Update version numbers in package JSONs
|
|
|
|
|
const packageJSONPath = join(packagePath, 'package.json');
|
|
|
|
|
const packageJSON = await readJson(packageJSONPath);
|
|
|
|
|
packageJSON.version = version;
|
|
|
|
|
|
|
|
|
|
// Also update inter-package dependencies.
|
2020-02-06 00:52:31 +08:00
|
|
|
// Next releases always have exact version matches.
|
2018-11-24 04:37:18 +08:00
|
|
|
// The promote script may later relax these (e.g. "^x.x.x") based on source package JSONs.
|
|
|
|
|
const {dependencies, peerDependencies} = packageJSON;
|
|
|
|
|
for (let j = 0; j < packages.length; j++) {
|
|
|
|
|
const dependencyName = packages[j];
|
|
|
|
|
if (dependencies && dependencies[dependencyName]) {
|
|
|
|
|
dependencies[dependencyName] = version;
|
|
|
|
|
}
|
|
|
|
|
if (peerDependencies && peerDependencies[dependencyName]) {
|
|
|
|
|
peerDependencies[dependencyName] = version;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await writeJson(packageJSONPath, packageJSON, {spaces: 2});
|
2017-11-27 00:47:20 +08:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2017-10-17 06:01:14 +08:00
|
|
|
module.exports = {
|
2021-01-27 04:17:56 +08:00
|
|
|
addDefaultParamValue,
|
2018-11-24 04:37:18 +08:00
|
|
|
confirm,
|
2017-10-17 06:01:14 +08:00
|
|
|
execRead,
|
2019-06-05 04:28:41 +08:00
|
|
|
getArtifactsList,
|
2018-11-24 04:37:18 +08:00
|
|
|
getBuildInfo,
|
|
|
|
|
getChecksumForCurrentRevision,
|
2021-02-04 00:11:56 +08:00
|
|
|
getCommitFromCurrentBuild,
|
2021-06-24 01:50:09 +08:00
|
|
|
getDateStringForCommit,
|
2017-11-10 00:29:51 +08:00
|
|
|
getPublicPackages,
|
2018-11-24 04:37:18 +08:00
|
|
|
handleError,
|
2017-10-17 06:01:14 +08:00
|
|
|
logPromise,
|
2018-11-24 04:37:18 +08:00
|
|
|
printDiff,
|
2019-08-10 04:12:00 +08:00
|
|
|
splitCommaParams,
|
2018-11-24 04:37:18 +08:00
|
|
|
theme,
|
2020-02-06 00:52:31 +08:00
|
|
|
updateVersionsForNext,
|
2017-10-17 06:01:14 +08:00
|
|
|
};
|