feat: Refactor CLI to run with one command, improve logs, modularize, add tests

- Run with one command and do not rely on error exit codes to stop the process when a release is not necessary
- Break `index.js` in smaller modules in order to improve testability and simplify the code
- Add several missing unit and integration tests to reach 100% coverage
- Integration tests now test end to end, including publishing to Github (with http://www.mock-server.com on Docker)
- Use `tj/commander.js` to print an help message, verify and parse CLI arguments
- Semantic-release can now be called via Javascript API: `require('semantic-release')(options)`
- Remove npmlog dependency and add more log messages
- Logger is now passed to plugins
- Add debug logs with `visionmedia/debug`. `debug` is enabled for both semantic-release and plugins with `--debug`
- Use `kevva/npm-conf` in place of the deprecated `npm/npmconf`
- Pass lastRelease, nextRelease and commits to generate-notes plugin
- In dry-run mode, print the release note instead of publishing it to Github as draft, and skip the CI verifications
- The dry-run mode does not require npm and Github TOKEN to be set anymore and can be run locally

BREAKING CHANGE: Semantic-Release must now be executed with `semantic-release` instead of `semantic-release pre && npm publish && semantic-release post`.
BREAKING CHANGE: The `semantic-release` command now returns with exit code 0 on expected exception (no release has to be done, running on a PR, gitHead not found, other CI job failed etc...). It only returns with 1 when there is an unexpected error (code error in a plugin, plugin not found, git command cannot be run etc..).
BREAKING CHANGE: Calling the `semantic-release` command with unexpected argument(s) now exit with 1 and print an help message.
BREAKING CHANGE: Semantic-Release does not rely on `npmlog` anymore and the log level cannot be configured. Debug logs can be activated with CLI option `--debug` or with environment variable `DEBUG=semantic-release:*`
BREAKING CHANGE: The CLI options `--debug` doesn't enable the dry-run mode anymore but activate the debugs. The dry run mode is now set with the CLI command `--dry-run` or `-d`.
This commit is contained in:
Pierre-Denis Vanduynslager
2017-10-27 18:07:13 -07:00
committed by Gregor Martynus
parent 1bd095d26c
commit e2a8a5cd32
39 changed files with 1857 additions and 1041 deletions
Executable
+59
View File
@@ -0,0 +1,59 @@
const program = require('commander');
const SemanticReleaseError = require('@semantic-release/error');
const logger = require('./lib/logger');
function list(values) {
return values.split(',').map(value => value.trim());
}
module.exports = async () => {
program
.name('semantic-release')
.description('Run automated package publishing')
.option('-b, --branch <branch>', 'Branch to release from')
.option('--github-token <token>', 'Token to authenticate with Github API')
.option('--github-url <url>', 'GitHub Enterprise endpoint')
.option('--github-api-path-prefix <prefix>', 'Prefix of the GitHub Enterprise endpoint')
.option(
'--verify-conditions <paths>',
'Comma separated list of paths or packages name for the verifyConditions plugin',
list
)
.option('--get-last-release <path>', 'Path or package name for the getLastRelease plugin')
.option('--analyze-commits <path>', 'Path or package name for the analyzeCommits plugin')
.option(
'--verify-release <paths>',
'Comma separated list of paths or packages name for the verifyRelease plugin',
list
)
.option('--generate-notes <path>', 'Path or package name for the generateNotes plugin')
.option('--debug', 'Output debugging information')
.option(
'-d, --dry-run',
'Dry-run mode, skipping verifyConditions, publishing and release, printing next version and release notes'
)
.parse(process.argv);
if (program.debug) {
// Debug must be enabled before other requires in order to work
require('debug').enable('semantic-release:*');
}
try {
if (program.args.length > 0) {
program.outputHelp();
process.exitCode = 1;
} else {
await require('./index')(program);
}
} catch (err) {
// If error is a SemanticReleaseError then it's an expected exception case (no release to be done, running on a PR etc..) and the cli will return with 0
// Otherwise it's an unexpected error (configuration issue, code issue, plugin issue etc...) and the cli will return 1
if (err instanceof SemanticReleaseError) {
logger.log(`%s ${err.message}`, err.code);
} else {
process.exitCode = 1;
logger.error(err);
}
}
};
+48 -141
View File
@@ -1,153 +1,60 @@
const path = require('path');
const {promisify} = require('util');
const url = require('url');
const {readJson, writeJson} = require('fs-extra');
const {cloneDeep, defaults, mapKeys, camelCase, assign} = require('lodash');
const log = require('npmlog');
const nopt = require('nopt');
const npmconf = require('npmconf');
const normalizeData = require('normalize-package-data');
const marked = require('marked');
const TerminalRenderer = require('marked-terminal');
const SemanticReleaseError = require('@semantic-release/error');
const getConfig = require('./lib/get-config');
const getNextVersion = require('./lib/get-next-version');
const verifyPkg = require('./lib/verify-pkg');
const verifyAuth = require('./lib/verify-auth');
const getCommits = require('./lib/get-commits');
const publishNpm = require('./lib/publish-npm');
const githubRelease = require('./lib/github-release');
const logger = require('./lib/logger');
module.exports = async () => {
log.heading = 'semantic-release';
const env = process.env;
const pkg = await readJson('./package.json');
const originalPkg = cloneDeep(pkg);
normalizeData(pkg);
const knownOptions = {
branch: String,
debug: Boolean,
'github-token': String,
'github-url': String,
'analyze-commits': [path, String],
'generate-notes': [path, String],
'verify-conditions': [path, String],
'verify-release': [path, String],
};
const options = defaults(
mapKeys(nopt(knownOptions), (value, key) => {
return camelCase(key);
}),
pkg.release,
{
branch: 'master',
fallbackTags: {next: 'latest'},
debug: !env.CI,
githubToken: env.GH_TOKEN || env.GITHUB_TOKEN,
githubUrl: env.GH_URL,
}
);
const plugins = require('../src/lib/plugins')(options);
let conf;
try {
conf = await promisify(npmconf.load)({});
} catch (err) {
log.error('init', 'Failed to load npm config.', err);
process.exit(1);
module.exports = async opts => {
const config = await getConfig(opts);
const {plugins, env, options, pkg, npm} = config;
logger.log('Run automated release for %s on branch %s', pkg.name, options.branch);
verifyPkg(pkg);
if (!options.dryRun) {
verifyAuth(options, env);
}
const npm = {
auth: {token: env.NPM_TOKEN},
cafile: conf.get('cafile'),
loglevel: conf.get('loglevel'),
registry: require('../src/lib/get-registry')(pkg, conf),
tag: (pkg.publishConfig || {}).tag || conf.get('tag') || 'latest',
};
if (!options.dryRun) {
logger.log('Call plugin %s', 'verify-conditions');
await plugins.verifyConditions({env, options, pkg, npm, logger});
}
// normalize trailing slash
npm.registry = url.format(url.parse(npm.registry));
log.level = npm.loglevel;
logger.log('Call plugin %s', 'get-last-release');
const {commits, lastRelease} = await getCommits(
await plugins.getLastRelease({env, options, pkg, npm, logger}),
options.branch
);
const config = {env: env, pkg: pkg, options: options, plugins: plugins, npm: npm};
const hide = {};
if (options.githubToken) hide.githubToken = '***';
logger.log('Call plugin %s', 'analyze-commits');
const type = await plugins.analyzeCommits({env, options, pkg, npm, logger, lastRelease, commits});
if (!type) {
throw new SemanticReleaseError('There are no relevant changes, so no new version is released.', 'ENOCHANGE');
}
const nextRelease = {type, version: getNextVersion(type, lastRelease)};
log.verbose('init', 'options:', assign({}, options, hide));
log.verbose('init', 'Verifying config.');
logger.log('Call plugin %s', 'verify-release');
await plugins.verifyRelease({env, options, pkg, npm, logger, lastRelease, commits, nextRelease});
const errors = require('../src/lib/verify')(config);
errors.forEach(err => {
log.error('init', err.message + ' ' + err.code);
});
if (errors.length) process.exit(1);
if (!options.dryRun) {
await publishNpm(pkg, npm, nextRelease);
}
if (options.argv.remain[0] === 'pre') {
log.verbose('pre', 'Running pre-script.');
log.verbose('pre', 'Veriying conditions.');
try {
await plugins.verifyConditions(config);
} catch (err) {
log[options.debug ? 'warn' : 'error']('pre', err.message);
if (!options.debug) process.exit(1);
}
logger.log('Call plugin %s', 'generate-notes');
const notes = await plugins.generateNotes({env, options, pkg, npm, logger, lastRelease, commits, nextRelease});
const nerfDart = require('nerf-dart')(npm.registry);
let wroteNpmRc = false;
if (env.NPM_OLD_TOKEN && env.NPM_EMAIL) {
// Using the old auth token format is not considered part of the public API
// This might go away anytime (i.e. once we have a better testing strategy)
conf.set('_auth', '${NPM_OLD_TOKEN}', 'project'); // eslint-disable-line no-template-curly-in-string
conf.set('email', '${NPM_EMAIL}', 'project'); // eslint-disable-line no-template-curly-in-string
wroteNpmRc = true;
} else if (env.NPM_TOKEN) {
conf.set(nerfDart + ':_authToken', '${NPM_TOKEN}', 'project'); // eslint-disable-line no-template-curly-in-string
wroteNpmRc = true;
}
try {
await promisify(conf.save.bind(conf))('project');
} catch (err) {
return log.error('pre', 'Failed to save npm config.', err);
}
if (wroteNpmRc) log.verbose('pre', 'Wrote authToken to .npmrc.');
let release;
try {
release = await require('../src/pre')(config);
} catch (err) {
log.error('pre', 'Failed to determine new version.');
const args = ['pre', (err.code ? err.code + ' ' : '') + err.message];
if (err.stack) args.push(err.stack);
log.error.apply(log, args);
process.exit(1);
}
const message = 'Determined version ' + release.version + ' as "' + npm.tag + '".';
log.verbose('pre', message);
if (options.debug) {
log.error('pre', message + ' Not publishing in debug mode.', release);
process.exit(1);
}
try {
const shrinkwrap = await readJson('./npm-shrinkwrap.json');
shrinkwrap.version = release.version;
await writeJson('./npm-shrinkwrap.json', shrinkwrap);
log.verbose('pre', 'Wrote version ' + release.version + 'to npm-shrinkwrap.json.');
} catch (e) {
log.silly('pre', "Couldn't find npm-shrinkwrap.json.");
}
await writeJson('./package.json', assign(originalPkg, {version: release.version}));
log.verbose('pre', 'Wrote version ' + release.version + ' to package.json.');
} else if (options.argv.remain[0] === 'post') {
log.verbose('post', 'Running post-script.');
let published, release;
try {
({published, release} = await require('../src/post')(config));
log.verbose('post', (published ? 'Published' : 'Generated') + ' release notes.', release);
} catch (err) {
log.error('post', 'Failed to publish release notes.', err);
process.exit(1);
}
if (options.dryRun) {
marked.setOptions({renderer: new TerminalRenderer()});
logger.log('Release note for version %s:\n', nextRelease.version);
console.log(marked(notes));
} else {
log.error('post', 'Command "' + options.argv.remain[0] + '" not recognized. Use either "pre" or "post"');
const releaseUrl = await githubRelease(pkg, notes, nextRelease.version, options);
logger.log('Published Github release: %s', releaseUrl);
}
};
+9
View File
@@ -0,0 +1,9 @@
function debugShell(message, shell, debug) {
debug(message);
debug('cmd: %O', shell.cmd);
debug('stdout: %O', shell.stdout);
debug('stderr: %O', shell.stderr);
debug('code: %O', shell.code);
}
module.exports = {debugShell};
+46 -21
View File
@@ -1,6 +1,8 @@
const execa = require('execa');
const log = require('npmlog');
const debug = require('debug')('semantic-release:get-commits');
const getVersionHead = require('./get-version-head');
const {debugShell} = require('./debug');
const logger = require('./logger');
/**
* Commit message.
@@ -10,62 +12,85 @@ const getVersionHead = require('./get-version-head');
* @property {string} message The commit message.
*/
/**
* Last release.
*
* @typedef {Object} LastRelease
* @property {string} version The version number of the last release.
* @property {string} [gitHead] The commit sha used to make the last release.
*/
/**
* Result object.
*
* @typedef {Object} Result
* @property {Array<Commit>} commits The list of commits since the last release.
* @property {LastRelease} lastRelease The updated lastRelease.
*/
/**
* Retrieve the list of commits on the current branch since the last released version, or all the commits of the current branch if there is no last released version.
*
* The commit correspoding to the last released version is determined as follow:
* - Use `lastRelease.gitHead` if defined and present in `config.options.branch` history.
* - If `lastRelease.gitHead` is not in the `config.options.branch` history, unshallow the repository and try again.
* - If `lastRelease.gitHead` is still not in the `config.options.branch` history, search for a tag named `v<version>` or `<version>` and verify if it's associated commit sha is present in `config.options.branch` history.
* - Use `lastRelease.gitHead` if defined and present in `branch` history.
* - If `lastRelease.gitHead` is not in the `branch` history, unshallow the repository and try again.
* - If `lastRelease.gitHead` is still not in the `branch` history, search for a tag named `v<version>` or `<version>` and verify if it's associated commit sha is present in `branch` history.
*
* @param {Object} config
* @param {Object} config.lastRelease The lastRelease object obtained from the getLastRelease plugin.
* @param {string} [config.lastRelease.version] The version number of the last release.
* @param {string} [config.lastRelease.gitHead] The commit sha used to make the last release.
* @param {Object} config.options The semantic-relese options.
* @param {string} config.options.branch The branch to release from.
* @param {LastRelease} lastRelease The lastRelease object obtained from the getLastRelease plugin.
* @param {string} branch The branch to release from.
* @param {Object} logger Global logger.
*
* @return {Promise<Array<Commit>>} The list of commits on the branch `config.options.branch` since the last release.
* @return {Promise<Result>} The list of commits on the branch `branch` since the last release and the updated lastRelease with the gitHead used to retrieve the commits.
*
* @throws {SemanticReleaseError} with code `ENOTINHISTORY` if `config.lastRelease.gitHead` or the commit sha derived from `config.lastRelease.version` is not in the direct history of `config.options.branch`.
* @throws {SemanticReleaseError} with code `ENOGITHEAD` if `config.lastRelease.gitHead` is undefined and no commit sha can be found for the `config.lastRelease.version`.
* @throws {SemanticReleaseError} with code `ENOTINHISTORY` if `lastRelease.gitHead` or the commit sha derived from `config.lastRelease.version` is not in the direct history of `branch`.
* @throws {SemanticReleaseError} with code `ENOGITHEAD` if `lastRelease.gitHead` is undefined and no commit sha can be found for the `config.lastRelease.version`.
*/
module.exports = async ({lastRelease: {version, gitHead}, options: {branch}}) => {
module.exports = async ({version, gitHead}, branch) => {
if (gitHead || version) {
try {
gitHead = await getVersionHead(gitHead, version, branch);
} catch (err) {
if (err.code === 'ENOTINHISTORY') {
log.error('commits', notInHistoryMessage(err.gitHead, branch, version));
logger.error(notInHistoryMessage(err.gitHead, branch, version));
} else {
log.error('commits', noGitHeadMessage(branch, version));
logger.error(noGitHeadMessage(branch, version));
}
throw err;
}
logger.log('Retrieving commits since %s, corresponding to version %s', gitHead, version);
} else {
logger.log('No previous release found, retrieving all commits');
// If there is no gitHead nor a version, there is no previous release. Unshallow the repo in order to retrieve all commits
await execa('git', ['fetch', '--unshallow', '--tags'], {reject: false});
const shell = await execa('git', ['fetch', '--unshallow', '--tags'], {reject: false});
debugShell('Unshallow repo', shell, debug);
}
try {
return (await execa('git', [
const shell = await execa('git', [
'log',
'--format=format:%H==SPLIT==%B==END==',
`${gitHead ? gitHead + '..' : ''}HEAD`,
])).stdout
]);
debugShell('Get commits', shell, debug);
const commits = shell.stdout
.split('==END==')
.filter(raw => !!raw.trim())
.map(raw => {
const [hash, message] = raw.trim().split('==SPLIT==');
return {hash, message};
});
logger.log('Found %s commits since last release', commits.length);
debug('Parsed commits: %o', commits);
return {commits, lastRelease: {version, gitHead}};
} catch (err) {
return [];
debug(err);
logger.log('Found no commit since last release');
return {commits: [], lastRelease: {version, gitHead}};
}
};
function noGitHeadMessage(branch, version) {
return `The commit the last release of this package was derived from cannot be determined from the release metadata not from the repository tags.
return `The commit the last release of this package was derived from cannot be determined from the release metadata nor from the repository tags.
This means semantic-release can not extract the commits between now and then.
This is usually caused by releasing from outside the repository directory or with innaccessible git metadata.
+43
View File
@@ -0,0 +1,43 @@
const url = require('url');
const {readJson} = require('fs-extra');
const {defaults} = require('lodash');
const npmConf = require('npm-conf');
const normalizeData = require('normalize-package-data');
const debug = require('debug')('semantic-release:config');
const logger = require('./logger');
const getPlugins = require('./plugins');
const getRegistry = require('./get-registry');
module.exports = async opts => {
const pkg = await readJson('./package.json');
const {GH_TOKEN, GITHUB_TOKEN, GH_URL} = process.env;
normalizeData(pkg);
const options = defaults(opts, pkg.release, {
branch: 'master',
fallbackTags: {next: 'latest'},
githubToken: GH_TOKEN || GITHUB_TOKEN,
githubUrl: GH_URL,
});
debug('branch: %O', options.branch);
debug('fallbackTags: %O', options.fallbackTags);
debug('analyzeCommits: %O', options.analyzeCommits);
debug('generateNotes: %O', options.generateNotes);
debug('verifyConditions: %O', options.verifyConditions);
debug('verifyRelease: %O', options.verifyRelease);
const plugins = await getPlugins(options);
const conf = npmConf();
const npm = {
auth: {token: process.env.NPM_TOKEN},
registry: getRegistry(pkg, conf),
tag: (pkg.publishConfig || {}).tag || conf.get('tag'),
conf,
};
// normalize trailing slash
npm.registry = url.format(url.parse(npm.registry));
debug('npm registry: %O', npm.registry);
debug('npm tag: %O', npm.tag);
return {env: process.env, pkg, options, plugins, npm, logger};
};
+19
View File
@@ -0,0 +1,19 @@
const semver = require('semver');
const SemanticReleaseError = require('@semantic-release/error');
const logger = require('./logger');
module.exports = (type, lastRelease) => {
let version;
if (!lastRelease.version) {
version = '1.0.0';
logger.log('There is no previous release, the next release version is %s', version);
} else {
version = semver.inc(lastRelease.version, type);
if (!version) {
throw new SemanticReleaseError(`Invalid release type ${type}`, 'EINVALIDTYPE');
}
logger.log('The next release version is %s', version);
}
return version;
};
-14
View File
@@ -1,14 +0,0 @@
const {promisify} = require('util');
const SemanticReleaseError = require('@semantic-release/error');
module.exports = async config => {
const {plugins, lastRelease} = config;
const type = await promisify(plugins.analyzeCommits)(config);
if (!type) {
throw new SemanticReleaseError('There are no relevant changes, so no new version is released.', 'ENOCHANGE');
}
if (!lastRelease.version) return 'initial';
return type;
};
+15 -4
View File
@@ -1,5 +1,7 @@
const SemanticReleaseError = require('@semantic-release/error');
const execa = require('execa');
const debug = require('debug')('semantic-release:get-version-head');
const SemanticReleaseError = require('@semantic-release/error');
const {debugShell} = require('./debug');
/**
* Get the commit sha for a given tag.
@@ -10,8 +12,11 @@ const execa = require('execa');
*/
async function gitTagHead(tagName) {
try {
return (await execa('git', ['rev-list', '-1', '--tags', tagName])).stdout;
const shell = await execa('git', ['rev-list', '-1', '--tags', tagName]);
debugShell('Get git tag head', shell, debug);
return shell.stdout;
} catch (err) {
debug(err);
return null;
}
}
@@ -24,7 +29,9 @@ async function gitTagHead(tagName) {
* @return {boolean} `true` if the commit `sha` is in the history of the current branch, `false` otherwise.
*/
async function isCommitInHistory(sha) {
return (await execa('git', ['merge-base', '--is-ancestor', sha, 'HEAD'], {reject: false})).code === 0;
const shell = await execa('git', ['merge-base', '--is-ancestor', sha, 'HEAD'], {reject: false});
debugShell('Check if commit is in history', shell, debug);
return shell.code === 0;
}
/**
@@ -41,14 +48,17 @@ async function isCommitInHistory(sha) {
module.exports = async (gitHead, version) => {
// Check if gitHead is defined and exists in release branch
if (gitHead && (await isCommitInHistory(gitHead))) {
debug('Use gitHead: %s', gitHead);
return gitHead;
}
// Ushallow the repository
await execa('git', ['fetch', '--unshallow', '--tags'], {reject: false});
const shell = await execa('git', ['fetch', '--unshallow', '--tags'], {reject: false});
debugShell('Unshallow repo', shell, debug);
// Check if gitHead is defined and exists in release branch again
if (gitHead && (await isCommitInHistory(gitHead))) {
debug('Use gitHead: %s', gitHead);
return gitHead;
}
@@ -59,6 +69,7 @@ module.exports = async (gitHead, version) => {
// Check if tagHead is found and exists in release branch again
if (tagHead && (await isCommitInHistory(tagHead))) {
debug('Use tagHead: %s', tagHead);
return tagHead;
}
}
+36
View File
@@ -0,0 +1,36 @@
const {promisify} = require('util');
const url = require('url');
const gitHead = require('git-head');
const GitHubApi = require('github');
const parseSlug = require('parse-github-repo-url');
const debug = require('debug')('semantic-release:github-release');
module.exports = async (pkg, notes, version, {branch, githubUrl, githubToken, githubApiPathPrefix}) => {
const [owner, repo] = parseSlug(pkg.repository.url);
let {port, protocol, hostname: host} = githubUrl ? url.parse(githubUrl) : {};
protocol = (protocol || '').split(':')[0] || null;
const pathPrefix = githubApiPathPrefix || null;
const github = new GitHubApi({port, protocol, host, pathPrefix});
debug('Github host: %o', host);
debug('Github port: %o', port);
debug('Github protocol: %o', protocol);
debug('Github pathPrefix: %o', pathPrefix);
github.authenticate({type: 'token', token: githubToken});
const name = `v${version}`;
const release = {owner, repo, tag_name: name, name, target_commitish: branch, body: notes};
debug('release owner: %o', owner);
debug('release repo: %o', repo);
debug('release name: %o', name);
debug('release branch: %o', branch);
const sha = await promisify(gitHead)();
const ref = `refs/tags/${name}`;
debug('Create git tag %o with commit %o', ref, sha);
await github.gitdata.createReference({owner, repo, ref, sha});
const {data: {html_url: releaseUrl}} = await github.repos.createRelease(release);
return releaseUrl;
};
+18
View File
@@ -0,0 +1,18 @@
const chalk = require('chalk');
/**
* Logger with `log` and `error` function.
*/
module.exports = {
log(...args) {
const [format, ...rest] = args;
console.log(`${chalk.grey('[Semantic release]:')} ${format}`, ...rest.map(arg => chalk.magenta(arg)));
},
error(...args) {
const [format, ...rest] = args;
console.error(
`${chalk.grey('[Semantic release]:')} ${chalk.red(format instanceof Error ? format.stack : format)}`,
...rest.map(arg => chalk.red(arg instanceof Error ? arg.stack : arg))
);
},
};
+13 -11
View File
@@ -1,6 +1,7 @@
const {promisify} = require('util');
const relative = require('require-relative');
const pSeries = require('p-series');
const logger = require('./logger');
module.exports = options => {
const plugins = {
@@ -10,17 +11,15 @@ module.exports = options => {
};
['verifyConditions', 'verifyRelease'].forEach(plugin => {
if (!Array.isArray(options[plugin])) {
plugins[plugin] = promisify(
normalize(
options[plugin],
plugin === 'verifyConditions' ? '@semantic-release/condition-travis' : './plugin-noop'
)
plugins[plugin] = normalize(
options[plugin],
plugin === 'verifyConditions' ? '@semantic-release/condition-travis' : './plugin-noop'
);
} else {
plugins[plugin] = async pluginOptions => {
return pSeries(
options[plugin].map(step => {
return () => promisify(normalize(step, './plugin-noop'))(pluginOptions);
return () => normalize(step, './plugin-noop')(pluginOptions);
})
);
};
@@ -31,13 +30,16 @@ module.exports = options => {
};
const normalize = (pluginConfig, fallback) => {
if (typeof pluginConfig === 'string') return relative(pluginConfig).bind(null, {});
if (pluginConfig && typeof pluginConfig.path === 'string') {
return relative(pluginConfig.path).bind(null, pluginConfig);
if (typeof pluginConfig === 'string') {
logger.log('Load plugin %s', pluginConfig);
return promisify(relative(pluginConfig).bind(null, {}));
}
return require(fallback).bind(null, pluginConfig || {});
if (pluginConfig && typeof pluginConfig.path === 'string') {
logger.log('Load plugin %s', pluginConfig.path);
return promisify(relative(pluginConfig.path).bind(null, pluginConfig));
}
return promisify(require(fallback).bind(null, pluginConfig || {}));
};
module.exports.normalize = normalize;
+35
View File
@@ -0,0 +1,35 @@
const {appendFile, readJson, writeJson, pathExists} = require('fs-extra');
const execa = require('execa');
const nerfDart = require('nerf-dart');
const debug = require('debug')('semantic-release:publish-npm');
const {debugShell} = require('./debug');
const logger = require('./logger');
module.exports = async (pkg, {conf, registry, auth}, {version}) => {
const pkgFile = await readJson('./package.json');
if (await pathExists('./npm-shrinkwrap.json')) {
const shrinkwrap = await readJson('./npm-shrinkwrap.json');
shrinkwrap.version = version;
await writeJson('./npm-shrinkwrap.json', shrinkwrap);
logger.log('Wrote version %s to npm-shrinkwrap.json', version);
}
await writeJson('./package.json', Object.assign(pkgFile, {version}));
logger.log('Wrote version %s to package.json', version);
if (process.env.NPM_OLD_TOKEN && process.env.NPM_EMAIL) {
// Using the old auth token format is not considered part of the public API
// This might go away anytime (i.e. once we have a better testing strategy)
await appendFile('./.npmrc', `_auth = \${NPM_OLD_TOKEN}\nemail = \${NPM_EMAIL}`);
logger.log('Wrote NPM_OLD_TOKEN and NPM_EMAIL to .npmrc.');
} else {
await appendFile('./.npmrc', `${nerfDart(registry)}:_authToken = \${NPM_TOKEN}`);
logger.log('Wrote NPM_TOKEN to .npmrc.');
}
logger.log('Publishing version %s to npm registry %s', version, registry);
const shell = await execa('npm', ['publish']);
console.log(shell.stdout);
debugShell('Publishing on npm', shell, debug);
};
+11
View File
@@ -0,0 +1,11 @@
const SemanticReleaseError = require('@semantic-release/error');
module.exports = (options, env) => {
if (!options.githubToken) {
throw new SemanticReleaseError('No github token specified.', 'ENOGHTOKEN');
}
if (!(env.NPM_TOKEN || (env.NPM_OLD_TOKEN && env.NPM_EMAIL))) {
throw new SemanticReleaseError('No npm token specified.', 'ENONPMTOKEN');
}
};
+11
View File
@@ -0,0 +1,11 @@
const SemanticReleaseError = require('@semantic-release/error');
module.exports = pkg => {
if (!pkg.name) {
throw new SemanticReleaseError('No "name" found in package.json.', 'ENOPKGNAME');
}
if (!pkg.repository || !pkg.repository.url) {
throw new SemanticReleaseError('No "repository" found in package.json.', 'ENOPKGREPO');
}
};
-25
View File
@@ -1,25 +0,0 @@
const SemanticReleaseError = require('@semantic-release/error');
module.exports = ({pkg, options, env}) => {
const errors = [];
if (!pkg.name) {
errors.push(new SemanticReleaseError('No "name" found in package.json.', 'ENOPKGNAME'));
}
if (!pkg.repository || !pkg.repository.url) {
errors.push(new SemanticReleaseError('No "repository" found in package.json.', 'ENOPKGREPO'));
}
if (!options.debug) {
if (!options.githubToken) {
errors.push(new SemanticReleaseError('No github token specified.', 'ENOGHTOKEN'));
}
if (!(env.NPM_TOKEN || (env.NPM_OLD_TOKEN && env.NPM_EMAIL))) {
errors.push(new SemanticReleaseError('No npm token specified.', 'ENONPMTOKEN'));
}
}
return errors;
};
-38
View File
@@ -1,38 +0,0 @@
const {promisify} = require('util');
const url = require('url');
const gitHead = require('git-head');
const GitHubApi = require('github');
const parseSlug = require('parse-github-repo-url');
module.exports = async config => {
const {pkg, options: {branch, debug, githubUrl, githubToken, githubApiPathPrefix}, plugins} = config;
const [owner, repo] = parseSlug(pkg.repository.url);
const name = `v${pkg.version}`;
const tag = {owner, repo, ref: `refs/tags/${name}`, sha: await promisify(gitHead)()};
const body = await promisify(plugins.generateNotes)(config);
const release = {owner, repo, tag_name: name, name, target_commitish: branch, draft: !!debug, body};
if (debug && !githubToken) {
return {published: false, release};
}
const {port, protocol, hostname} = githubUrl ? url.parse(githubUrl) : {};
const github = new GitHubApi({
port,
protocol: (protocol || '').split(':')[0] || null,
host: hostname,
pathPrefix: githubApiPathPrefix || null,
});
github.authenticate({type: 'token', token: githubToken});
if (debug) {
await github.repos.createRelease(release);
return {published: true, release};
}
await github.gitdata.createReference(tag);
await github.repos.createRelease(release);
return {published: true, release};
};
-23
View File
@@ -1,23 +0,0 @@
const {promisify} = require('util');
const {assign} = require('lodash');
const semver = require('semver');
const getCommits = require('./lib/get-commits');
const getReleaseType = require('./lib/get-release-type');
module.exports = async config => {
const {getLastRelease, verifyRelease} = config.plugins;
const lastRelease = await promisify(getLastRelease)(config);
const commits = await getCommits(assign({lastRelease}, config));
const type = await getReleaseType(assign({commits, lastRelease}, config));
const nextRelease = {
type: type,
version: type === 'initial' ? '1.0.0' : semver.inc(lastRelease.version, type),
};
await verifyRelease(assign({commits, lastRelease, nextRelease}, config));
return nextRelease;
};