Files

355 lines
12 KiB
JavaScript
Raw Permalink Normal View History

const chalk = require('chalk');
const {execSync} = require('child_process');
const minimist = require('minimist');
2019-01-28 12:15:05 -06:00
const path = require('path');
const {readJsonSync} = require('fs-extra');
const args = minimist(process.argv.slice(2));
2019-01-28 12:15:05 -06:00
const DEFAULT_BUMP = null;
const bump = args._[0] || DEFAULT_BUMP;
2019-01-28 12:15:05 -06:00
const DEFAULT_COMMIT_MSG = `chore(release): %v`;
const commitMsg = args['commit-message'] || DEFAULT_COMMIT_MSG;
const DEFAULT_CONVENTIONAL_COMMITS = true;
let cCommits;
// with --no-conventional-commits cli option `args['conventional-commits']` will be `false`
// there is never a need to use --conventional-commits cli option because the
// default behavior is `true`
if (args['conventional-commits'] === false) {
cCommits = false;
} else {
cCommits = DEFAULT_CONVENTIONAL_COMMITS;
}
const DEFAULT_CONVENTIONAL_GRADUATE = false;
const cGraduate = args['conventional-graduate'] || DEFAULT_CONVENTIONAL_GRADUATE;
const DEFAULT_CONVENTIONAL_PRERELEASE = false;
const cPrerelease = args['conventional-prerelease'] || DEFAULT_CONVENTIONAL_PRERELEASE;
// if not using --no-create-release or --no-push cli option then an environment
// variable named GH_TOKEN must be defined with a properly scoped GitHub
// personal access token; for local releases, e.g. with verdaccio,
// --no-create-release should be used (unless using --no-push, which implies
// --no-create-release)
// See: https://github.com/lerna/lerna/tree/master/commands/version#--create-release-type
const DEFAULT_CREATE_RELEASE = true;
let createRelease;
// with --no-create-release cli option `args['create-release']` will be `false`
// there is never a need to use --create-release cli option because the default
// is `true`
if (args['create-release'] === false) {
createRelease = false;
} else {
createRelease = DEFAULT_CREATE_RELEASE;
}
2019-01-28 12:15:05 -06:00
const DEFAULT_DIST_TAG = `latest`;
const distTag = args['dist-tag'] || DEFAULT_DIST_TAG;
const DEFAULT_FORCE_PUBLISH = false;
const forcePublish = args['force-publish'] || DEFAULT_FORCE_PUBLISH;
2019-01-28 12:15:05 -06:00
const DEFAULT_GIT_REMOTE = `origin`;
const remote = args['git-remote'] || DEFAULT_GIT_REMOTE;
const DEFAULT_NO_PUSH = false;
let noPush;
// with --no-push cli option `args['push']` will be `false`
// there is never a need to use --push cli option because the default behavior
// is to push
if (args['push'] === false) {
noPush = true;
} else {
noPush = DEFAULT_NO_PUSH;
}
2019-01-28 12:15:05 -06:00
const DEFAULT_PRE_ID = null;
const preId = args.preid || DEFAULT_PRE_ID;
const DEFAULT_RELEASE_BRANCH = `master`;
const branch = args['release-branch'] || DEFAULT_RELEASE_BRANCH;
const DEFAULT_SIGN = false;
const sign = args.sign || DEFAULT_SIGN;
const DEFAULT_SKIP_QA = false;
const skipQa = args['skip-qa'] || DEFAULT_SKIP_QA;
const DEFAULT_VERSION_ONLY = false;
const versionOnly = args['version-only'] || DEFAULT_VERSION_ONLY;
2019-01-28 12:15:05 -06:00
const cyan = (str) => chalk.cyan(str);
const execSyncInherit = (cmd) => execSync(cmd, {stdio: 'inherit'});
2019-01-28 12:15:05 -06:00
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);
2019-01-28 12:15:05 -06:00
const failMsg = `${chalk.red(`RELEASE FAILED!`)} Stopping right here.`;
const reportSetting = (desc, val, def) => {
logInfo(`${desc} is set to ${cyan(val)}${val === def ? ` (default).`: `.`}`);
};
2019-01-28 12:15:05 -06:00
const runCommand = (cmd, inherit = true, display) => {
logInfo(`Running command ${cyan(display || cmd)}.`);
let out;
if (inherit) {
execSyncInherit(cmd);
} else {
out = execSync(cmd);
}
2019-01-28 12:15:05 -06:00
return out;
};
// eslint-disable-next-line complexity
(async () => {
if (!noPush && createRelease && !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,`,
`or else the ${cyan('--no-create-release')} or ${cyan('--no-push')}`,
`option should be used. Always use ${cyan('--no-create-release')} when`,
`running a local release, e.g. with ${cyan('verdaccio')} (unless using`,
`${cyan('--no-push')}, which implies ${cyan('--no-create-release')}).`
);
logError(failMsg);
process.exit(1);
}
const lernaJsonPath = path.join(__dirname, '../lerna.json');
logInfo(`Reading ${cyan(lernaJsonPath)}...`);
let lernaJson;
try {
lernaJson = readJsonSync(lernaJsonPath);
} catch (e) {
console.error(e.stack || e);
logError(
`Could not read ${cyan(lernaJsonPath)}. Please check the error above.`
);
logError(failMsg);
process.exit(1);
}
let DEFAULT_REGISTRY, registry;
if (!versionOnly) {
try {
2019-01-28 12:15:05 -06:00
DEFAULT_REGISTRY = lernaJson.command.publish.registry;
if (!DEFAULT_REGISTRY) throw new Error('missing registry in lerna.json');
registry = args.registry || DEFAULT_REGISTRY;
} 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(`Checking the working tree...`);
try {
runCommand(`npm run --silent cwtree`, true, `npm run cwtree`);
logSuccess(`Working tree is clean.`);
} catch (e) {
logError(
`Working tree is dirty or has untracked files.`,
`Please make necessary changes or commits before rerunning this script.`
);
logError(failMsg);
process.exit(1);
}
reportSetting(`Release branch`, branch, DEFAULT_RELEASE_BRANCH);
logInfo(`Determining the current branch...`);
2019-01-28 12:15:05 -06:00
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);
}
2019-01-28 12:15:05 -06:00
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)}. Please checkout the release branch before`,
`rerunning this script or rerun with`,
`${cyan(`--release-branch ${currentBranch}`)}.`
);
logError(failMsg);
process.exit(1);
}
2019-01-28 12:15:05 -06:00
let localRef;
if (!noPush) {
2019-01-28 12:15:05 -06:00
reportSetting(`Git remote`, remote, DEFAULT_GIT_REMOTE);
logInfo(
`Fetching commits from ${cyan(remote)} to compare local and remote`,
`branches...`
2019-01-28 12:15:05 -06:00
);
try {
runCommand(`git fetch ${remote}`, false);
} catch (e) {
logError(`Could not fetch latest commits. Please check the error above.`);
logError(failMsg);
process.exit(1);
2019-01-28 12:15:05 -06:00
}
let remoteRef;
2019-01-28 12:15:05 -06:00
try {
localRef = runCommand(`git rev-parse ${branch}`, false).toString().trim();
remoteRef = runCommand(`git rev-parse ${remote}/${branch}`, false)
.toString()
.trim();
2019-01-28 12:15:05 -06:00
} catch (e) {
logError(`A problem occured. Please check the error above.`);
logError(failMsg);
process.exit(1);
2019-01-28 12:15:05 -06:00
}
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}`)}.`,
`Please sync branches before rerunning this script.`
);
logError(failMsg);
process.exit(1);
2019-01-28 12:15:05 -06:00
}
} else {
try {
localRef = runCommand(`git rev-parse ${branch}`, false).toString().trim();
} catch (e) {
logError(`A problem occured. Please check the error above.`);
logError(failMsg);
process.exit(1);
}
}
2019-01-28 12:15:05 -06:00
if (skipQa) {
logWarning(
`Skipping the QA suite. You already built the packages, right?`
);
} else {
2019-01-28 12:15:05 -06:00
logInfo(
`It's time to run the QA suite, this will take awhile...`
);
try {
runCommand(`npm run qa:full`);
2019-01-28 12:15:05 -06:00
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);
2019-01-28 12:15:05 -06:00
}
}
2019-01-28 12:15:05 -06:00
logInfo(`${versionOnly ? 'Versioning' : 'Publishing'} with Lerna...`);
if (bump) reportSetting(`Version bump`, bump, DEFAULT_BUMP);
reportSetting(`Conventional commits option`, cCommits, DEFAULT_CONVENTIONAL_COMMITS);
reportSetting(`Conventional graduate option`, cGraduate, DEFAULT_CONVENTIONAL_GRADUATE);
reportSetting(`Conventional prerelease option`, cPrerelease, DEFAULT_CONVENTIONAL_PRERELEASE);
if (!noPush) reportSetting(`Create GitHub release option`, createRelease, DEFAULT_CREATE_RELEASE);
if (!versionOnly) reportSetting(`NPM dist-tag`, distTag, DEFAULT_DIST_TAG);
reportSetting(`Force publish option`, forcePublish, DEFAULT_FORCE_PUBLISH);
reportSetting(`Commit message format`, commitMsg, DEFAULT_COMMIT_MSG);
reportSetting(`No push option`, noPush, DEFAULT_NO_PUSH);
if (preId) reportSetting(`Prerelease identifier`, preId, DEFAULT_PRE_ID);
if(!versionOnly) reportSetting(`Package registry`, registry, DEFAULT_REGISTRY);
reportSetting(`Signature option`, sign, DEFAULT_SIGN);
2019-01-28 12:15:05 -06:00
const lernaCmd = [
`lerna`,
(versionOnly && `version`) || `publish`,
bump || ``,
(cCommits && `--conventional-commits`) || ``,
(cCommits && cGraduate && `--conventional-graduate${cGraduate === true ? '' : `=${cGraduate}`}`) || ``,
(cCommits && cPrerelease && `--conventional-prerelease${cPrerelease === true ? '' : `=${cPrerelease}`}`) || ``,
(!noPush && createRelease && `--create-release github`) || ``,
(!versionOnly && `--dist-tag ${distTag}`) || ``,
(forcePublish && `--force-publish${forcePublish === true ? '' : `=${forcePublish}`}`) || ``,
(!noPush && `--git-remote ${remote}`) || ``,
`--message "${commitMsg}"`,
(noPush && `--no-push`) || ``,
(preId && `--preid ${preId}`) || ``,
(!versionOnly && `--registry ${registry}`) || ``,
(sign && `--sign-git-commit`) || ``,
(sign && `--sign-git-tag`) || ``
].filter(str => !!str).join(` `);
2019-01-28 12:15:05 -06:00
try {
runCommand(lernaCmd);
if (bump !== 'from-package' && localRef ===
runCommand(`git rev-parse ${branch}`, false).toString().trim()) {
let action, cmd, noPubMsg;
if (versionOnly) {
action = 'version creation';
cmd = 'version';
noPubMsg = '';
} else {
action = 'publication';
cmd = 'publish';
noPubMsg = `No packages were published. `;
2019-01-28 12:15:05 -06:00
}
logWarning(
chalk.yellow(`RELEASE STOPPED!`),
`No commit or tag was created. ${noPubMsg}Please check the output`,
`above if you did not stop ${action} via a prompt from the`,
`${cyan(`lerna ${cmd}`)} command.`
);
process.exit(0);
2019-01-28 12:15:05 -06:00
}
} catch (e) {
logError(`A problem occured. Please check the error above.`);
let checkPkgMsg;
if (versionOnly) {
checkPkgMsg = '';
} else {
const lernaDocsUrl = 'https://github.com/lerna/lerna/tree/master/commands/publish#bump-from-package';
checkPkgMsg = [
` 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}`
].join(' ');
}
logError(
2019-01-28 12:15:05 -06:00
failMsg,
`Make sure to clean up the working tree and local/remote commits, tags,`,
`and releases as necessary.${checkPkgMsg}`
);
process.exit(1);
}
logSuccess(`${chalk.green(`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);
});