embark/scripts/stable-release.js

336 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* global __dirname process require */
const chalk = require('chalk');
const {execSync} = require('child_process');
const minimist = require('minimist');
const path = require('path');
const {readJsonSync} = require('fs-extra');
const semver = require('semver');
const args = minimist(process.argv.slice(2));
const DEFAULT_SKIP_QA = false;
const skipQa = args['skip-qa'] || DEFAULT_SKIP_QA;
const branch = `master`;
const commitMsg = `chore(release): %v`;
const distTag = `latest`;
const remote = `origin`;
const cyan = (str) => chalk.cyan(str);
const execSyncInherit = (cmd) => execSync(cmd, {stdio: 'inherit'});
const log = (mark, str, which = 'log') => console[which](
mark, str.filter(s => !!s).join(` `)
);
const logError = (...str) => log(chalk.red(``), str, 'error');
const logInfo = (...str) => log(chalk.blue(``), str);
const logSuccess = (...str) => log(chalk.green(``), str);
const logWarning = (...str) => log(chalk.yellow('‼︎'), str);
const failMsg = `${chalk.red(`STABLE RELEASE FAILED!`)} Stopping right here.`;
const runCommand = (cmd, inherit = true, display) => {
logInfo(`Running command ${cyan(display || cmd)}.`);
let out;
if (inherit) {
execSyncInherit(cmd);
} else {
out = execSync(cmd);
}
return out;
};
(async () => {
if (!process.env.GH_TOKEN) {
logError(
`Environment variable ${cyan('GH_TOKEN')} was not defined or falsy. It`,
`must be defined with a properly scoped GitHub personal access token.`
);
logError(failMsg);
process.exit(1);
}
const lernaJsonPath = path.join(__dirname, '../lerna.json');
logInfo(`Reading ${cyan(lernaJsonPath)}...`);
let currentVersion, lernaJson, registry;
try {
lernaJson = readJsonSync(lernaJsonPath);
currentVersion = lernaJson.version;
if (!currentVersion) throw new Error('missing version in lerna.json');
registry = lernaJson.command.publish.registry;
if (!registry) throw new Error('missing registry in lerna.json');
} catch (e) {
console.error(e.stack || e);
logError(
`Could not read values from ${cyan(lernaJsonPath)}. Please check the`,
`error above.`
);
logError(failMsg);
process.exit(1);
}
try {
if (!semver(currentVersion).prerelease.length) {
logError(
`Current version in ${cyan('lerna.json')} is not a prerelease. This`,
`script is intended only for graduating a prerelease to a stable`,
`release.`
);
logError(failMsg);
process.exit(1);
}
logSuccess(
`Current version in ${cyan('lerna.json')} is ${cyan(currentVersion)}.`
);
} catch (e) {
logError(`A problem occured. Please check the error above.`);
logError(failMsg);
process.exit(1);
}
logInfo(`Determining the latest stable version...`);
let latestStableVersion;
try {
const stableReleaseTags = runCommand(`git tag --list`, false)
.toString()
.trim()
.split('\n')
.filter(tag => tag.startsWith('v') && !tag.includes('-'))
.map(tag => tag.slice(1));
latestStableVersion = semver.rsort(stableReleaseTags)[0];
if (!latestStableVersion) {
logError(`Unable to determine the latest stable version.`);
logError(failMsg);
process.exit(1);
}
logSuccess(`Latest stable version is ${cyan(latestStableVersion)}.`);
} catch (e) {
logError(`A problem occured. Please check the error above.`);
logError(failMsg);
process.exit(1);
}
let forcePublish;
try {
const currentMajor = semver(currentVersion).major;
const stableMajor = semver(latestStableVersion).major;
if (currentMajor > stableMajor) {
forcePublish = true;
} else if (currentMajor < stableMajor) {
logError(
`Major version of the current version in ${cyan('lerna.json')} is less`,
`than the major version of the latest stable version. wat.`
);
logError(failMsg);
process.exit(1);
}
} catch (e) {
logError(`A problem occured. Please check the error above.`);
logError(failMsg);
process.exit(1);
}
logInfo(`Determining the current branch...`);
let currentBranch;
try {
currentBranch = runCommand(`git rev-parse --abbrev-ref HEAD`, false)
.toString()
.trim();
} catch (e) {
logError(`Could not determine the branch. Please check the error above.`);
logError(failMsg);
process.exit(1);
}
if (currentBranch === branch) {
logSuccess(`Current branch and release branch are the same.`);
} else {
logError(
`Current branch ${cyan(currentBranch)} is not the same as release branch`,
`${cyan(branch)}.`
);
logError(failMsg);
process.exit(1);
}
logInfo(
`Fetching commits from ${cyan(remote)} to compare local and remote`,
`branches...`
);
try {
runCommand(`git fetch ${remote}`, false);
} catch (e) {
logError(`Could not fetch latest commits. Please check the error above.`);
logError(failMsg);
process.exit(1);
}
let localRef, remoteRef;
try {
localRef = runCommand(`git rev-parse ${branch}`, false).toString().trim();
remoteRef = runCommand(`git rev-parse ${remote}/${branch}`, false)
.toString()
.trim();
} catch (e) {
logError(`A problem occured. Please check the error above.`);
logError(failMsg);
process.exit(1);
}
if (localRef === remoteRef) {
logSuccess(`Local branch is in sync with remote branch.`);
} else {
logError(
`Local branch ${cyan(branch)} is not in sync with`,
`${cyan(`${remote}/${branch}`)}.`
);
logError(failMsg);
process.exit(1);
}
if (skipQa) {
logWarning(
`Skipping the QA suite. You already built the packages, right?`
);
} else {
logInfo(
`It's time to run the QA suite, this will take awhile...`
);
try {
runCommand(`npm run qa:full`);
logSuccess(`All steps succeeded in the QA suite.`);
} catch (e) {
logError(`A step failed in the QA suite. Please check the error above.`);
logError(failMsg);
process.exit(1);
}
}
// The `--all` option is not supplied below because `lerna changed` is being
// used to calculate which packages should be tagged via:
// `npm dist-tag add [pkg]@[newStableVersion] nightly`
// Private packages are never tagged because dist-tags only pertain to
// packages published to the NPM registry. By tagging every newly released
// stable version of public packages as `nightly` (in addition to tagging as
// `latest`) we avoid version drift in nightly releases
const lernaChanged = [
`lerna`,
`changed`,
(!forcePublish && `--conventional-graduate`) || ``,
(forcePublish && `--force-publish`) || ``,
`--json`
].filter(str => !!str).join(` `);
let pkgsToTag;
try {
pkgsToTag = JSON.parse(
runCommand(`${lernaChanged} 2>/dev/null || true`, false, lernaChanged)
.toString()
.trim() || '[]'
);
} catch (e) {
logError(`A problem occured. Please check the error above.`);
logError(failMsg);
process.exit(1);
}
const lernaPublish = [
`lerna`,
`publish`,
`--conventional-commits`,
(!forcePublish && `--conventional-graduate`) || ``,
`--create-release github`,
`--dist-tag ${distTag}`,
(forcePublish && `--force-publish`) || ``,
`--git-remote ${remote}`,
`--message "${commitMsg}"`,
`--registry ${registry}`
].filter(str => !!str).join(` `);
try {
runCommand(lernaPublish);
if (localRef ===
runCommand(`git rev-parse ${branch}`, false).toString().trim()) {
logWarning(
chalk.yellow(`STABLE RELEASE STOPPED!`),
`No commit or tag was created. No packages were published. This is`,
`most likely due to no qualifying changes having been made to the`,
`${cyan(branch)} branch since the previous release. Please check the`,
`output above.`
);
process.exit(0);
}
} catch (e) {
logError(`A problem occured. Please check the error above.`);
const lernaDocsUrl = 'https://github.com/lerna/lerna/tree/master/commands/publish#bump-from-package';
logError(
failMsg,
`Make sure to clean up commits, tags, and releases as necessary. Check`,
`the package registry to verify if any packages were published. If so`,
`they may need to be flagged as deprecated since the`,
`${cyan('lerna publish')} command exited with error. In some cases it`,
`may be possible to salvage an imcomplete release by using the`,
`${cyan('from-package')} keyword with the ${cyan('lerna publish')}`,
`command. See: ${lernaDocsUrl}`
);
process.exit(1);
}
if (pkgsToTag.length) {
logInfo(`Reading ${cyan(lernaJsonPath)}...`);
let updatedCurrentVersion;
try {
lernaJson = readJsonSync(lernaJsonPath);
updatedCurrentVersion = lernaJson.version;
if (!updatedCurrentVersion) throw new Error('missing version in lerna.json');
logSuccess(`Updated current version is ${updatedCurrentVersion}`);
} catch (e) {
console.error(e.stack || e);
logError(
`Could not read values from ${cyan(lernaJsonPath)}. Please check the`,
`error above.`
);
logError(failMsg);
process.exit(1);
}
logInfo(
`Updating ${cyan('nightly')} dist-tags to point to the new stable`,
`version...`
);
logInfo('Packages to tag:', pkgsToTag.map(({name}) => cyan(name)).join(', '));
const _pkgsToTag = pkgsToTag.slice();
try {
for (const {name} of pkgsToTag) {
runCommand(`npm dist-tag add ${name}@${updatedCurrentVersion} nightly`);
_pkgsToTag.shift();
}
} catch (e) {
logError(`A problem occured. Please check the error above.`);
const packages = _pkgsToTag.map(({name}) => cyan(name)).join(', ');
logError(
`NPM dist-tag ${cyan('nightly')} was not updated for the following`,
`packages: ${packages}. Make sure to complete the updates manually.`
);
logError(failMsg);
process.exit(1);
}
}
logSuccess(`${chalk.green(`STABLE RELEASE SUCCEEDED!`)} Woohoo! Done.`);
})().then(() => {
process.exit(0);
}).catch(e => {
console.error(e.stack || e);
logError(`A problem occured. Please check the error above.`);
logError(failMsg);
process.exit(1);
});