265 lines
8.7 KiB
JavaScript
265 lines
8.7 KiB
JavaScript
'use strict';
|
|
|
|
const {exec} = require('child-process-promise');
|
|
const {createPatch} = require('diff');
|
|
const {hashElement} = require('folder-hash');
|
|
const {readdirSync, readFileSync, statSync, writeFileSync} = require('fs');
|
|
const {readJson, writeJson} = require('fs-extra');
|
|
const http = require('request-promise-json');
|
|
const logUpdate = require('log-update');
|
|
const {join} = require('path');
|
|
const createLogger = require('progress-estimator');
|
|
const prompt = require('prompt-promise');
|
|
const theme = require('./theme');
|
|
|
|
// The following packages are published to NPM but not by this script.
|
|
// They are released through a separate process.
|
|
const RELEASE_SCRIPT_PACKAGE_SKIPLIST = [
|
|
'react-devtools',
|
|
'react-devtools-core',
|
|
'react-devtools-inline',
|
|
];
|
|
|
|
// https://www.npmjs.com/package/progress-estimator#configuration
|
|
const logger = createLogger({
|
|
storagePath: join(__dirname, '.progress-estimator'),
|
|
});
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
const execRead = async (command, options) => {
|
|
const {stdout} = await exec(command, options);
|
|
|
|
return stdout.trim();
|
|
};
|
|
|
|
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);
|
|
if (!buildMetadata.workflows || !buildMetadata.workflows.workflow_id) {
|
|
console.log(
|
|
theme`{error Could not find workflow info for build ${buildID}.}`
|
|
);
|
|
process.exit(1);
|
|
}
|
|
const artifactsJobName = buildMetadata.workflows.job_name.endsWith(
|
|
'_experimental'
|
|
)
|
|
? 'process_artifacts_experimental'
|
|
: 'process_artifacts';
|
|
const workflowID = buildMetadata.workflows.workflow_id;
|
|
const workflowMetadataURL = `https://circleci.com/api/v2/workflow/${workflowID}/job?circle-token=${process.env.CIRCLE_CI_API_TOKEN}`;
|
|
const workflowMetadata = await http.get(workflowMetadataURL, true);
|
|
const job = workflowMetadata.items.find(
|
|
({name}) => name === artifactsJobName
|
|
);
|
|
if (!job || !job.job_number) {
|
|
console.log(
|
|
theme`{error Could not find "${artifactsJobName}" job for workflow ${workflowID}.}`
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const jobArtifactsURL = `https://circleci.com/api/v1.1/project/github/facebook/react/${job.job_number}/artifacts?circle-token=${process.env.CIRCLE_CI_API_TOKEN}`;
|
|
const jobArtifacts = await http.get(jobArtifactsURL, true);
|
|
|
|
return jobArtifacts;
|
|
};
|
|
|
|
const getBuildInfo = async () => {
|
|
const cwd = join(__dirname, '..', '..');
|
|
|
|
const isExperimental = process.env.RELEASE_CHANNEL === 'experimental';
|
|
|
|
const branch = await execRead('git branch | grep \\* | cut -d " " -f2', {
|
|
cwd,
|
|
});
|
|
const commit = await execRead('git show -s --format=%h', {cwd});
|
|
const checksum = await getChecksumForCurrentRevision(cwd);
|
|
const version = isExperimental
|
|
? `0.0.0-experimental-${commit}`
|
|
: `0.0.0-${commit}`;
|
|
|
|
// 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.
|
|
// See updateVersionsForNext() below for more info.
|
|
const packageJSON = await readJson(
|
|
join(cwd, 'packages', 'react', 'package.json')
|
|
);
|
|
const reactVersion = isExperimental
|
|
? `${packageJSON.version}-experimental-${commit}`
|
|
: `${packageJSON.version}-${commit}`;
|
|
|
|
return {branch, buildNumber, checksum, commit, reactVersion, version};
|
|
};
|
|
|
|
const getChecksumForCurrentRevision = async cwd => {
|
|
const packagesDir = join(cwd, 'packages');
|
|
const hashedPackages = await hashElement(packagesDir, {
|
|
encoding: 'hex',
|
|
files: {exclude: ['.DS_Store']},
|
|
});
|
|
return hashedPackages.hash.slice(0, 7);
|
|
};
|
|
|
|
const getPublicPackages = () => {
|
|
const packagesRoot = join(__dirname, '..', '..', 'packages');
|
|
|
|
return readdirSync(packagesRoot).filter(dir => {
|
|
if (RELEASE_SCRIPT_PACKAGE_SKIPLIST.includes(dir)) {
|
|
return false;
|
|
}
|
|
|
|
const packagePath = join(packagesRoot, dir, 'package.json');
|
|
|
|
if (dir.charAt(0) !== '.') {
|
|
let stat;
|
|
try {
|
|
stat = statSync(packagePath);
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
if (stat.isFile()) {
|
|
const packageJSON = JSON.parse(readFileSync(packagePath));
|
|
return packageJSON.private !== true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
});
|
|
};
|
|
|
|
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);
|
|
};
|
|
|
|
const logPromise = async (promise, text, estimate) =>
|
|
logger(promise, text, {estimate});
|
|
|
|
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;
|
|
};
|
|
|
|
// 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(','));
|
|
}
|
|
}
|
|
};
|
|
|
|
// 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".
|
|
// Canaries version numbers use the format of 0.0.0-<sha> to be easily recognized (e.g. 0.0.0-01974a867).
|
|
// 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.
|
|
// It is based on the version of React in the local package.json (e.g. 16.12.0-01974a867).
|
|
// Both numbers will be replaced if the "next" release is promoted to a stable release.
|
|
const updateVersionsForNext = async (cwd, reactVersion, version) => {
|
|
const packages = getPublicPackages(join(cwd, 'packages'));
|
|
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'
|
|
).replace(/export default '[^']+';/, `export default '${reactVersion}';`);
|
|
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.
|
|
// Next releases always have exact version matches.
|
|
// 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});
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
confirm,
|
|
execRead,
|
|
getArtifactsList,
|
|
getBuildInfo,
|
|
getChecksumForCurrentRevision,
|
|
getPublicPackages,
|
|
handleError,
|
|
logPromise,
|
|
printDiff,
|
|
splitCommaParams,
|
|
theme,
|
|
updateVersionsForNext,
|
|
};
|