semantic-release/index.js
2021-07-30 13:57:35 -07:00

276 lines
9.4 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.

import lodash from 'lodash'
const {pick} = lodash
import marked from 'marked';
import TerminalRenderer from 'marked-terminal';
import envCi from 'env-ci';
import hookStd from 'hook-std';
import semver from 'semver';
import AggregateError from 'aggregate-error';
import pkg from './package.json';
import hideSensitive from './lib/hide-sensitive';
import getConfig from './lib/get-config';
import verify from './lib/verify';
import getNextVersion from './lib/get-next-version';
import getCommits from './lib/get-commits';
import getLastRelease from './lib/get-last-release';
import getReleaseToAdd from './lib/get-release-to-add';
import {extractErrors, makeTag} from './lib/utils';
import getGitAuthUrl from './lib/get-git-auth-url';
import getBranches from './lib/branches';
import getLogger from './lib/get-logger';
import {verifyAuth, isBranchUpToDate, getGitHead, tag, push, pushNotes, getTagHead, addNote} from './lib/git';
import getError from './lib/get-error';
import {COMMIT_NAME, COMMIT_EMAIL} from './lib/definitions/constants';
marked.setOptions({renderer: new TerminalRenderer()});
/* eslint complexity: off */
async function run(context, plugins) {
const {cwd, env, options, logger} = context;
const {isCi, branch, prBranch, isPr} = context.envCi;
const ciBranch = isPr ? prBranch : branch;
if (!isCi && !options.dryRun && !options.noCi) {
logger.warn('This run was not triggered in a known CI environment, running in dry-run mode.');
options.dryRun = true;
} else {
// When running on CI, set the commits author and commiter info and prevent the `git` CLI to prompt for username/password. See #703.
Object.assign(env, {
GIT_AUTHOR_NAME: COMMIT_NAME,
GIT_AUTHOR_EMAIL: COMMIT_EMAIL,
GIT_COMMITTER_NAME: COMMIT_NAME,
GIT_COMMITTER_EMAIL: COMMIT_EMAIL,
...env,
GIT_ASKPASS: 'echo',
GIT_TERMINAL_PROMPT: 0,
});
}
if (isCi && isPr && !options.noCi) {
logger.log("This run was triggered by a pull request and therefore a new version won't be published.");
return false;
}
// Verify config
await verify(context);
options.repositoryUrl = await getGitAuthUrl({...context, branch: {name: ciBranch}});
context.branches = await getBranches(options.repositoryUrl, ciBranch, context);
context.branch = context.branches.find(({name}) => name === ciBranch);
if (!context.branch) {
logger.log(
`This test run was triggered on the branch ${ciBranch}, while semantic-release is configured to only publish from ${context.branches
.map(({name}) => name)
.join(', ')}, therefore a new version wont be published.`
);
return false;
}
logger[options.dryRun ? 'warn' : 'success'](
`Run automated release from branch ${ciBranch} on repository ${options.repositoryUrl}${
options.dryRun ? ' in dry-run mode' : ''
}`
);
try {
try {
await verifyAuth(options.repositoryUrl, context.branch.name, {cwd, env});
} catch (error) {
if (!(await isBranchUpToDate(options.repositoryUrl, context.branch.name, {cwd, env}))) {
logger.log(
`The local branch ${context.branch.name} is behind the remote one, therefore a new version won't be published.`
);
return false;
}
throw error;
}
} catch (error) {
logger.error(`The command "${error.command}" failed with the error message ${error.stderr}.`);
throw getError('EGITNOPERMISSION', context);
}
logger.success(`Allowed to push to the Git repository`);
await plugins.verifyConditions(context);
const errors = [];
context.releases = [];
const releaseToAdd = getReleaseToAdd(context);
if (releaseToAdd) {
const {lastRelease, currentRelease, nextRelease} = releaseToAdd;
nextRelease.gitHead = await getTagHead(nextRelease.gitHead, {cwd, env});
currentRelease.gitHead = await getTagHead(currentRelease.gitHead, {cwd, env});
if (context.branch.mergeRange && !semver.satisfies(nextRelease.version, context.branch.mergeRange)) {
errors.push(getError('EINVALIDMAINTENANCEMERGE', {...context, nextRelease}));
} else {
const commits = await getCommits({...context, lastRelease, nextRelease});
nextRelease.notes = await plugins.generateNotes({...context, commits, lastRelease, nextRelease});
if (options.dryRun) {
logger.warn(`Skip ${nextRelease.gitTag} tag creation in dry-run mode`);
} else {
await addNote({channels: [...currentRelease.channels, nextRelease.channel]}, nextRelease.gitHead, {cwd, env});
await push(options.repositoryUrl, {cwd, env});
await pushNotes(options.repositoryUrl, {cwd, env});
logger.success(
`Add ${nextRelease.channel ? `channel ${nextRelease.channel}` : 'default channel'} to tag ${
nextRelease.gitTag
}`
);
}
context.branch.tags.push({
version: nextRelease.version,
channel: nextRelease.channel,
gitTag: nextRelease.gitTag,
gitHead: nextRelease.gitHead,
});
const releases = await plugins.addChannel({...context, commits, lastRelease, currentRelease, nextRelease});
context.releases.push(...releases);
await plugins.success({...context, lastRelease, commits, nextRelease, releases});
}
}
if (errors.length > 0) {
throw new AggregateError(errors);
}
context.lastRelease = getLastRelease(context);
if (context.lastRelease.gitHead) {
context.lastRelease.gitHead = await getTagHead(context.lastRelease.gitHead, {cwd, env});
}
if (context.lastRelease.gitTag) {
logger.log(
`Found git tag ${context.lastRelease.gitTag} associated with version ${context.lastRelease.version} on branch ${context.branch.name}`
);
} else {
logger.log(`No git tag version found on branch ${context.branch.name}`);
}
context.commits = await getCommits(context);
const nextRelease = {
type: await plugins.analyzeCommits(context),
channel: context.branch.channel || null,
gitHead: await getGitHead({cwd, env}),
};
if (!nextRelease.type) {
logger.log('There are no relevant changes, so no new version is released.');
return context.releases.length > 0 ? {releases: context.releases} : false;
}
context.nextRelease = nextRelease;
nextRelease.version = getNextVersion(context);
nextRelease.gitTag = makeTag(options.tagFormat, nextRelease.version);
nextRelease.name = nextRelease.gitTag;
if (context.branch.type !== 'prerelease' && !semver.satisfies(nextRelease.version, context.branch.range)) {
throw getError('EINVALIDNEXTVERSION', {
...context,
validBranches: context.branches.filter(
({type, accept}) => type !== 'prerelease' && accept.includes(nextRelease.type)
),
});
}
await plugins.verifyRelease(context);
nextRelease.notes = await plugins.generateNotes(context);
await plugins.prepare(context);
if (options.dryRun) {
logger.warn(`Skip ${nextRelease.gitTag} tag creation in dry-run mode`);
} else {
// Create the tag before calling the publish plugins as some require the tag to exists
await tag(nextRelease.gitTag, nextRelease.gitHead, {cwd, env});
await addNote({channels: [nextRelease.channel]}, nextRelease.gitHead, {cwd, env});
await push(options.repositoryUrl, {cwd, env});
await pushNotes(options.repositoryUrl, {cwd, env});
logger.success(`Created tag ${nextRelease.gitTag}`);
}
const releases = await plugins.publish(context);
context.releases.push(...releases);
await plugins.success({...context, releases});
logger.success(
`Published release ${nextRelease.version} on ${nextRelease.channel ? nextRelease.channel : 'default'} channel`
);
if (options.dryRun) {
logger.log(`Release note for version ${nextRelease.version}:`);
if (nextRelease.notes) {
context.stdout.write(marked(nextRelease.notes));
}
}
return pick(context, ['lastRelease', 'commits', 'nextRelease', 'releases']);
}
function logErrors({logger, stderr}, err) {
const errors = extractErrors(err).sort((error) => (error.semanticRelease ? -1 : 0));
for (const error of errors) {
if (error.semanticRelease) {
logger.error(`${error.code} ${error.message}`);
if (error.details) {
stderr.write(marked(error.details));
}
} else {
logger.error('An error occurred while running semantic-release: %O', error);
}
}
}
async function callFail(context, plugins, err) {
const errors = extractErrors(err).filter((err) => err.semanticRelease);
if (errors.length > 0) {
try {
await plugins.fail({...context, errors});
} catch (error) {
logErrors(context, error);
}
}
}
export default async (cliOptions = {}, {cwd = process.cwd(), env = process.env, stdout, stderr} = {}) => {
const {unhook} = hookStd(
{silent: false, streams: [process.stdout, process.stderr, stdout, stderr].filter(Boolean)},
hideSensitive(env)
);
const context = {
cwd,
env,
stdout: stdout || process.stdout,
stderr: stderr || process.stderr,
envCi: envCi({env, cwd}),
};
context.logger = getLogger(context);
context.logger.log(`Running ${pkg.name} version ${pkg.version}`);
try {
const {plugins, options} = await getConfig(context, cliOptions);
context.options = options;
try {
const result = await run(context, plugins);
unhook();
return result;
} catch (error) {
await callFail(context, plugins, error);
throw error;
}
} catch (error) {
logErrors(context, error);
unhook();
throw error;
}
};