feat: Make semantic-release language agnostic

- Do not rely on `package.json` anymore
- Use `cosmiconfig` to load the configation. `semantic-release` can be configured:
  - via CLI options (including plugin names but not plugin options)
  - in the `release` property of `package.json` (as before)
  - in a `.releaserc.yml` or `.releaserc.js` or `.releaserc.js` or `release.config.js` file
  - in a `.releaserc` file containing `json`, `yaml` or `javascript` module
- Add the `repositoryUrl` options (used across `semantic-release` and plugins). The value is determined from CLi option, or option configuration, or package.json or the git remote url
- Verifies that `semantic-release` runs from a git repository
- `pkg` and `env` are not passed to plugin anymore
- `semantic-release` can be run both locally and globally. If ran globally with non default plugins, the plugins can be installed both globally or locally.

BREAKING CHANGE: `pkg` and `env` are not passed to plugin anymore.
Plugins relying on a `package.json` must verify the presence of a valid `package.json` and load it.
Plugins can use `process.env` instead of `env`.
This commit is contained in:
Pierre Vanduynslager 2017-11-23 17:38:33 -05:00
parent 5bec59b26b
commit 0c67ba517f
11 changed files with 292 additions and 110 deletions

View File

@ -167,6 +167,7 @@ semantic-release
These options are currently available: These options are currently available:
- `branch`: The branch on which releases should happen. Default: `'master'` - `branch`: The branch on which releases should happen. Default: `'master'`
- `repositoryUrl`: The git repository URL. Default: `repository` property in `package.json` or git origin url. Any valid git url format is supported (See [Git protocols](https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols)). If the [Github plugin](https://github.com/semantic-release/github) is used the URL must be a valid Github URL that include the `owner`, the `repository` name and the `host`. The Github shorthand URL is not supported.
- `dry-run`: Dry-run mode, skipping verifyConditions, publishing and release, printing next version and release notes - `dry-run`: Dry-run mode, skipping verifyConditions, publishing and release, printing next version and release notes
- `debug`: Output debugging information - `debug`: Output debugging information
@ -206,9 +207,7 @@ module.exports = function (pluginConfig, config, callback) {}
- `pluginConfig`: If the user of your plugin specifies additional plugin config in the `package.json` (see the `verifyConditions` example above) then its this object. - `pluginConfig`: If the user of your plugin specifies additional plugin config in the `package.json` (see the `verifyConditions` example above) then its this object.
- `config`: A config object containing a lot of information to act upon. - `config`: A config object containing a lot of information to act upon.
- `env`: All environment variables
- `options`: `semantic-release` options like `debug`, or `branch` - `options`: `semantic-release` options like `debug`, or `branch`
- `pkg`: Parsed `package.json`
- For certain plugins the `config` object contains even more information. See below. - For certain plugins the `config` object contains even more information. See below.
### `analyzeCommits` ### `analyzeCommits`

3
cli.js
View File

@ -10,6 +10,7 @@ module.exports = async () => {
.name('semantic-release') .name('semantic-release')
.description('Run automated package publishing') .description('Run automated package publishing')
.option('-b, --branch <branch>', 'Branch to release from') .option('-b, --branch <branch>', 'Branch to release from')
.option('-r, --repositoryUrl <repositoryUrl>', 'Git repository URL')
.option( .option(
'--verify-conditions <paths>', '--verify-conditions <paths>',
'Comma separated list of paths or packages name for the verifyConditions plugin(s)', 'Comma separated list of paths or packages name for the verifyConditions plugin(s)',
@ -41,7 +42,7 @@ module.exports = async () => {
program.outputHelp(); program.outputHelp();
process.exitCode = 1; process.exitCode = 1;
} else { } else {
await require('./index')(program.opts()); await require('.')(program.opts());
} }
} catch (err) { } 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 // 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

View File

@ -1,32 +1,40 @@
const marked = require('marked'); const marked = require('marked');
const TerminalRenderer = require('marked-terminal'); const TerminalRenderer = require('marked-terminal');
const SemanticReleaseError = require('@semantic-release/error'); const SemanticReleaseError = require('@semantic-release/error');
const {gitHead: getGitHead} = require('./lib/git');
const getConfig = require('./lib/get-config'); const getConfig = require('./lib/get-config');
const getNextVersion = require('./lib/get-next-version'); const getNextVersion = require('./lib/get-next-version');
const getCommits = require('./lib/get-commits'); const getCommits = require('./lib/get-commits');
const logger = require('./lib/logger'); const logger = require('./lib/logger');
const {gitHead: getGitHead, isGitRepo} = require('./lib/git');
module.exports = async opts => { module.exports = async opts => {
const config = await getConfig(opts, logger); if (!await isGitRepo()) {
const {plugins, env, options, pkg} = config; throw new SemanticReleaseError('Semantic-release must run from a git repository', 'ENOGITREPO');
}
logger.log('Run automated release for branch %s', options.branch); const config = await getConfig(opts, logger);
const {plugins, options} = config;
if (!options.repositoryUrl) {
throw new SemanticReleaseError('The repositoryUrl option is required', 'ENOREPOURL');
}
logger.log('Run automated release from branch %s', options.name, options.branch);
if (!options.dryRun) { if (!options.dryRun) {
logger.log('Call plugin %s', 'verify-conditions'); logger.log('Call plugin %s', 'verify-conditions');
await plugins.verifyConditions({env, options, pkg, logger}); await plugins.verifyConditions({options, logger});
} }
logger.log('Call plugin %s', 'get-last-release'); logger.log('Call plugin %s', 'get-last-release');
const {commits, lastRelease} = await getCommits( const {commits, lastRelease} = await getCommits(
await plugins.getLastRelease({env, options, pkg, logger}), await plugins.getLastRelease({options, logger}),
options.branch, options.branch,
logger logger
); );
logger.log('Call plugin %s', 'analyze-commits'); logger.log('Call plugin %s', 'analyze-commits');
const type = await plugins.analyzeCommits({env, options, pkg, logger, lastRelease, commits}); const type = await plugins.analyzeCommits({options, logger, lastRelease, commits});
if (!type) { if (!type) {
throw new SemanticReleaseError('There are no relevant changes, so no new version is released.', 'ENOCHANGE'); throw new SemanticReleaseError('There are no relevant changes, so no new version is released.', 'ENOCHANGE');
} }
@ -34,9 +42,9 @@ module.exports = async opts => {
const nextRelease = {type, version, gitHead: await getGitHead(), gitTag: `v${version}`}; const nextRelease = {type, version, gitHead: await getGitHead(), gitTag: `v${version}`};
logger.log('Call plugin %s', 'verify-release'); logger.log('Call plugin %s', 'verify-release');
await plugins.verifyRelease({env, options, pkg, logger, lastRelease, commits, nextRelease}); await plugins.verifyRelease({options, logger, lastRelease, commits, nextRelease});
const generateNotesParam = {env, options, pkg, logger, lastRelease, commits, nextRelease}; const generateNotesParam = {options, logger, lastRelease, commits, nextRelease};
if (options.dryRun) { if (options.dryRun) {
logger.log('Call plugin %s', 'generate-notes'); logger.log('Call plugin %s', 'generate-notes');
@ -49,7 +57,7 @@ module.exports = async opts => {
nextRelease.notes = await plugins.generateNotes(generateNotesParam); nextRelease.notes = await plugins.generateNotes(generateNotesParam);
logger.log('Call plugin %s', 'publish'); logger.log('Call plugin %s', 'publish');
await plugins.publish({options, pkg, logger, lastRelease, commits, nextRelease}, async prevInput => { await plugins.publish({options, logger, lastRelease, commits, nextRelease}, async prevInput => {
const newGitHead = await getGitHead(); const newGitHead = await getGitHead();
// If previous publish plugin has created a commit (gitHead changed) // If previous publish plugin has created a commit (gitHead changed)
if (prevInput.nextRelease.gitHead !== newGitHead) { if (prevInput.nextRelease.gitHead !== newGitHead) {
@ -59,7 +67,7 @@ module.exports = async opts => {
nextRelease.notes = await plugins.generateNotes(generateNotesParam); nextRelease.notes = await plugins.generateNotes(generateNotesParam);
} }
// Call the next publish plugin with the updated `nextRelease` // Call the next publish plugin with the updated `nextRelease`
return {options, pkg, logger, lastRelease, commits, nextRelease}; return {options, logger, lastRelease, commits, nextRelease};
}); });
logger.log('Published release: %s', nextRelease.version); logger.log('Published release: %s', nextRelease.version);
} }

View File

@ -1,19 +1,27 @@
const {readJson} = require('fs-extra'); const readPkgUp = require('read-pkg-up');
const {defaults} = require('lodash'); const {defaults} = require('lodash');
const normalizeData = require('normalize-package-data'); const cosmiconfig = require('cosmiconfig');
const debug = require('debug')('semantic-release:config'); const debug = require('debug')('semantic-release:config');
const {repoUrl} = require('./git');
const plugins = require('./plugins'); const plugins = require('./plugins');
module.exports = async (opts, logger) => { module.exports = async (opts, logger) => {
const pkg = await readJson('./package.json'); const {config} = (await cosmiconfig('release', {rcExtensions: true}).load(process.cwd())) || {};
normalizeData(pkg); const options = defaults(opts, config, {branch: 'master', repositoryUrl: (await pkgRepoUrl()) || (await repoUrl())});
const options = defaults(opts, pkg.release, {branch: 'master'});
debug('name: %O', options.name);
debug('branch: %O', options.branch); debug('branch: %O', options.branch);
debug('repositoryUrl: %O', options.repositoryUrl);
debug('analyzeCommits: %O', options.analyzeCommits); debug('analyzeCommits: %O', options.analyzeCommits);
debug('generateNotes: %O', options.generateNotes); debug('generateNotes: %O', options.generateNotes);
debug('verifyConditions: %O', options.verifyConditions); debug('verifyConditions: %O', options.verifyConditions);
debug('verifyRelease: %O', options.verifyRelease); debug('verifyRelease: %O', options.verifyRelease);
debug('publish: %O', options.publish); debug('publish: %O', options.publish);
return {env: process.env, pkg, options, plugins: await plugins(options, logger), logger}; return {options, plugins: await plugins(options, logger)};
}; };
async function pkgRepoUrl() {
const {pkg} = await readPkgUp();
return pkg && pkg.repository ? pkg.repository.url : null;
}

View File

@ -72,4 +72,20 @@ async function gitHead() {
} }
} }
module.exports = {gitTagHead, gitCommitTag, isCommitInHistory, unshallow, gitHead}; /**
* @return {string|null} The value of the remote git URL.
*/
async function repoUrl() {
return (await execa.stdout('git', ['remote', 'get-url', 'origin'], {reject: false})) || null;
}
/**
* @return {Boolean} `true` if the current working directory is in a git repository, `false` otherwise.
*/
async function isGitRepo() {
const shell = await execa('git', ['rev-parse', '--git-dir'], {reject: false});
debugShell('Check if the current working directory is a git repository', shell, debug);
return shell.code === 0;
}
module.exports = {gitTagHead, gitCommitTag, isCommitInHistory, unshallow, gitHead, repoUrl, isGitRepo};

View File

@ -15,25 +15,25 @@
} }
}, },
"dependencies": { "dependencies": {
"@semantic-release/commit-analyzer": "^4.0.0", "@semantic-release/commit-analyzer": "^5.0.0",
"@semantic-release/condition-travis": "^6.0.0", "@semantic-release/condition-travis": "^7.0.0",
"@semantic-release/error": "^2.1.0", "@semantic-release/error": "^2.1.0",
"@semantic-release/github": "^1.0.0", "@semantic-release/github": "^2.0.0",
"@semantic-release/npm": "^1.0.0", "@semantic-release/npm": "^2.0.0",
"@semantic-release/release-notes-generator": "^5.0.0", "@semantic-release/release-notes-generator": "^6.0.0",
"chalk": "^2.3.0", "chalk": "^2.3.0",
"commander": "^2.11.0", "commander": "^2.11.0",
"cosmiconfig": "^3.1.0",
"debug": "^3.1.0", "debug": "^3.1.0",
"execa": "^0.8.0", "execa": "^0.8.0",
"fs-extra": "^4.0.2",
"get-stream": "^3.0.0", "get-stream": "^3.0.0",
"git-log-parser": "^1.2.0", "git-log-parser": "^1.2.0",
"import-from": "^2.1.0", "import-from": "^2.1.0",
"lodash": "^4.0.0", "lodash": "^4.0.0",
"marked": "^0.3.6", "marked": "^0.3.6",
"marked-terminal": "^2.0.0", "marked-terminal": "^2.0.0",
"normalize-package-data": "^2.3.4",
"p-reduce": "^1.0.0", "p-reduce": "^1.0.0",
"read-pkg-up": "^3.0.0",
"semver": "^5.4.1" "semver": "^5.4.1"
}, },
"devDependencies": { "devDependencies": {
@ -44,11 +44,13 @@
"dockerode": "^2.5.2", "dockerode": "^2.5.2",
"eslint-config-prettier": "^2.5.0", "eslint-config-prettier": "^2.5.0",
"eslint-plugin-prettier": "^2.3.0", "eslint-plugin-prettier": "^2.3.0",
"file-url": "^2.0.2",
"fs-extra": "^4.0.2",
"js-yaml": "^3.10.0",
"mockserver-client": "^2.0.0", "mockserver-client": "^2.0.0",
"nock": "^9.0.2", "nock": "^9.0.2",
"npm-registry-couchapp": "^2.6.12", "npm-registry-couchapp": "^2.6.12",
"nyc": "^11.2.1", "nyc": "^11.2.1",
"p-map-series": "^1.0.0",
"prettier": "~1.8.0", "prettier": "~1.8.0",
"proxyquire": "^1.8.0", "proxyquire": "^1.8.0",
"sinon": "^4.0.0", "sinon": "^4.0.0",
@ -124,7 +126,8 @@
"prettier" "prettier"
], ],
"rules": { "rules": {
"prettier/prettier": 2 "prettier/prettier": 2,
"no-duplicate-imports": 2
} }
} }
} }

View File

@ -1,9 +1,9 @@
import test from 'ava'; import test from 'ava';
import {writeJson} from 'fs-extra'; import {writeFile, writeJson} from 'fs-extra';
import proxyquire from 'proxyquire'; import proxyquire from 'proxyquire';
import {stub} from 'sinon'; import {stub} from 'sinon';
import normalizeData from 'normalize-package-data'; import yaml from 'js-yaml';
import {gitRepo} from './helpers/git-utils'; import {gitRepo, gitCommits, gitShallowClone, gitAddConfig} from './helpers/git-utils';
test.beforeEach(t => { test.beforeEach(t => {
// Save the current process.env // Save the current process.env
@ -21,85 +21,175 @@ test.afterEach.always(t => {
process.env = Object.assign({}, t.context.env); process.env = Object.assign({}, t.context.env);
}); });
test.serial('Default values', async t => { test.serial('Default values, reading repositoryUrl from package.json', async t => {
const pkg = {name: 'package_name', release: {}}; const pkg = {repository: 'git@package.com:owner/module.git'};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
await gitCommits(['First']);
// Add remote.origin.url config
await gitAddConfig('remote.origin.url', 'git@repo.com:owner/module.git');
// Create package.json in repository root
await writeJson('./package.json', pkg);
const {options} = await t.context.getConfig();
// Verify the default options are set
t.is(options.branch, 'master');
t.is(options.repositoryUrl, 'git@package.com:owner/module.git');
});
test.serial('Default values, reading repositoryUrl from repo if not set in package.json', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Add remote.origin.url config
await gitAddConfig('remote.origin.url', 'git@repo.com:owner/module.git');
const {options} = await t.context.getConfig();
// Verify the default options are set
t.is(options.branch, 'master');
t.is(options.repositoryUrl, 'git@repo.com:owner/module.git');
});
test.serial('Default values, reading repositoryUrl (http url) from package.json if not set in repo', async t => {
const pkg = {repository: 'git+https://hostname.com/owner/module.git'};
// Create a git repository, set the current working directory at the root of the repo // Create a git repository, set the current working directory at the root of the repo
await gitRepo(); await gitRepo();
// Create package.json in repository root // Create package.json in repository root
await writeJson('./package.json', pkg); await writeJson('./package.json', pkg);
const result = await t.context.getConfig(); const {options} = await t.context.getConfig();
// Verify the normalized package is returned
normalizeData(pkg);
t.deepEqual(result.pkg, pkg);
// Verify the default options are set // Verify the default options are set
t.is(result.options.branch, 'master'); t.is(options.branch, 'master');
t.is(options.repositoryUrl, pkg.repository);
}); });
test.serial('Read package.json configuration', async t => { test.serial('Read options from package.json', async t => {
const release = { const release = {
analyzeCommits: 'analyzeCommits', analyzeCommits: 'analyzeCommits',
generateNotes: 'generateNotes', generateNotes: 'generateNotes',
getLastRelease: { getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_param'},
path: 'getLastRelease',
param: 'getLastRelease_param',
},
branch: 'test_branch', branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
}; };
const pkg = {name: 'package_name', release};
// Create a git repository, set the current working directory at the root of the repo // Create a git repository, set the current working directory at the root of the repo
await gitRepo(); await gitRepo();
// Create package.json in repository root // Create package.json in repository root
await writeJson('./package.json', pkg); await writeJson('./package.json', {release});
const result = await t.context.getConfig(); const {options} = await t.context.getConfig();
// Verify the options contains the plugin config from package.json // Verify the options contains the plugin config from package.json
t.is(result.options.analyzeCommits, release.analyzeCommits); t.deepEqual(options, release);
t.is(result.options.generateNotes, release.generateNotes);
t.deepEqual(result.options.getLastRelease, release.getLastRelease);
t.is(result.options.branch, release.branch);
// Verify the plugins module is called with the plugin options from package.json // Verify the plugins module is called with the plugin options from package.json
t.is(t.context.plugins.firstCall.args[0].analyzeCommits, release.analyzeCommits); t.deepEqual(t.context.plugins.firstCall.args[0], release);
t.is(t.context.plugins.firstCall.args[0].generateNotes, release.generateNotes);
t.deepEqual(t.context.plugins.firstCall.args[0].getLastRelease, release.getLastRelease);
t.is(t.context.plugins.firstCall.args[0].branch, release.branch);
}); });
test.serial('Prioritise cli parameters over package.json configuration', async t => { test.serial('Read options from .releaserc.yml', async t => {
const release = { const release = {
analyzeCommits: 'analyzeCommits', getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_param'},
generateNotes: 'generateNotes', branch: 'test_branch',
getLastRelease: { repositoryUrl: 'git+https://hostname.com/owner/module.git',
path: 'getLastRelease', };
param: 'getLastRelease_pkg',
}, // Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json in repository root
await writeFile('.releaserc.yml', yaml.safeDump(release));
const {options} = await t.context.getConfig();
// Verify the options contains the plugin config from package.json
t.deepEqual(options, release);
// Verify the plugins module is called with the plugin options from package.json
t.deepEqual(t.context.plugins.firstCall.args[0], release);
});
test.serial('Read options from .releaserc.json', async t => {
const release = {
getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_param'},
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json in repository root
await writeJson('.releaserc.json', release);
const {options} = await t.context.getConfig();
// Verify the options contains the plugin config from package.json
t.deepEqual(options, release);
// Verify the plugins module is called with the plugin options from package.json
t.deepEqual(t.context.plugins.firstCall.args[0], release);
});
test.serial('Read options from .releaserc.js', async t => {
const release = {
getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_param'},
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json in repository root
await writeFile('.releaserc.js', `module.exports = ${JSON.stringify(release)}`);
const {options} = await t.context.getConfig();
// Verify the options contains the plugin config from package.json
t.deepEqual(options, release);
// Verify the plugins module is called with the plugin options from package.json
t.deepEqual(t.context.plugins.firstCall.args[0], release);
});
test.serial('Read options from release.config.js', async t => {
const release = {
getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_param'},
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json in repository root
await writeFile('release.config.js', `module.exports = ${JSON.stringify(release)}`);
const {options} = await t.context.getConfig();
// Verify the options contains the plugin config from package.json
t.deepEqual(options, release);
// Verify the plugins module is called with the plugin options from package.json
t.deepEqual(t.context.plugins.firstCall.args[0], release);
});
test.serial('Prioritise cli parameters over file configuration and git repo', async t => {
const release = {
getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_pkg'},
branch: 'branch_pkg', branch: 'branch_pkg',
}; };
const options = { const options = {
getLastRelease: { getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_cli'},
path: 'getLastRelease',
param: 'getLastRelease_cli',
},
branch: 'branch_cli', branch: 'branch_cli',
repositoryUrl: 'http://cli-url.com/owner/package',
}; };
const pkg = {name: 'package_name', release}; const pkg = {release, repository: 'git@hostname.com:owner/module.git'};
// Create a git repository, set the current working directory at the root of the repo // Create a git repository, set the current working directory at the root of the repo
await gitRepo(); const repo = await gitRepo();
await gitCommits(['First']);
// Create a clone
await gitShallowClone(repo);
// Create package.json in repository root // Create package.json in repository root
await writeJson('./package.json', pkg); await writeJson('./package.json', pkg);
const result = await t.context.getConfig(options); const result = await t.context.getConfig(options);
// Verify the options contains the plugin config from cli // Verify the options contains the plugin config from cli
t.deepEqual(result.options.getLastRelease, options.getLastRelease); t.deepEqual(result.options, options);
t.is(result.options.branch, options.branch);
// Verify the plugins module is called with the plugin options from cli // Verify the plugins module is called with the plugin options from cli
t.deepEqual(t.context.plugins.firstCall.args[0].getLastRelease, options.getLastRelease); t.deepEqual(t.context.plugins.firstCall.args[0], options);
t.is(t.context.plugins.firstCall.args[0].branch, options.branch);
}); });

View File

@ -1,6 +1,15 @@
import test from 'ava'; import test from 'ava';
import {gitTagHead, gitCommitTag, isCommitInHistory, unshallow, gitHead} from '../lib/git'; import fileUrl from 'file-url';
import {gitRepo, gitCommits, gitCheckout, gitTagVersion, gitShallowClone, gitLog} from './helpers/git-utils'; import {gitTagHead, gitCommitTag, isCommitInHistory, unshallow, gitHead, repoUrl} from '../lib/git';
import {
gitRepo,
gitCommits,
gitCheckout,
gitTagVersion,
gitShallowClone,
gitLog,
gitAddConfig,
} from './helpers/git-utils';
test.beforeEach(t => { test.beforeEach(t => {
// Save the current working diretory // Save the current working diretory
@ -93,3 +102,29 @@ test.serial('Get the commit sha for a given tag or "null" if the tag does not ex
t.is((await gitTagHead('v1.0.0')).substring(0, 7), commits[0].hash); t.is((await gitTagHead('v1.0.0')).substring(0, 7), commits[0].hash);
t.falsy(await gitTagHead('missing_tag')); t.falsy(await gitTagHead('missing_tag'));
}); });
test.serial('Return git remote repository url from config', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Add remote.origin.url config
await gitAddConfig('remote.origin.url', 'git@hostname.com:owner/package.git');
t.is(await repoUrl(), 'git@hostname.com:owner/package.git');
});
test.serial('Return git remote repository url set while cloning', async t => {
// Create a git repository, set the current working directory at the root of the repo
const repo = await gitRepo();
await gitCommits(['First']);
// Create a clone
await gitShallowClone(repo);
t.is(await repoUrl(), fileUrl(repo));
});
test.serial('Return "null" if git repository url is not set', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
t.is(await repoUrl(), null);
});

View File

@ -1,6 +1,7 @@
import tempy from 'tempy'; import tempy from 'tempy';
import execa from 'execa'; import execa from 'execa';
import pMapSeries from 'p-map-series'; import fileUrl from 'file-url';
import pReduce from 'p-reduce';
/** /**
* Commit message informations. * Commit message informations.
@ -33,11 +34,16 @@ export async function gitRepo() {
* @returns {Array<Commit>} The created commits, in reverse order (to match `git log` order). * @returns {Array<Commit>} The created commits, in reverse order (to match `git log` order).
*/ */
export async function gitCommits(messages) { export async function gitCommits(messages) {
return (await pMapSeries(messages, async msg => { return (await pReduce(
const {stdout} = await execa('git', ['commit', '-m', msg, '--allow-empty', '--no-gpg-sign']); messages,
const [, branch, hash, message] = /^\[(\w+)\(?.*?\)?(\w+)\] (.+)$/.exec(stdout); async (commits, msg) => {
return {branch, hash, message}; const {stdout} = await execa('git', ['commit', '-m', msg, '--allow-empty', '--no-gpg-sign']);
})).reverse(); const [, branch, hash, message] = /^\[(\w+)\(?.*?\)?(\w+)\] (.+)$/.exec(stdout);
commits.push({branch, hash, message});
return commits;
},
[]
)).reverse();
} }
/** /**
@ -109,7 +115,7 @@ export async function gitShallowClone(origin, branch = 'master', depth = 1) {
const dir = tempy.directory(); const dir = tempy.directory();
process.chdir(dir); process.chdir(dir);
await execa('git', ['clone', '--no-hardlinks', '--no-tags', '-b', branch, '--depth', depth, `file://${origin}`, dir]); await execa('git', ['clone', '--no-hardlinks', '--no-tags', '-b', branch, '--depth', depth, fileUrl(origin), dir]);
return dir; return dir;
} }
@ -137,3 +143,13 @@ export async function gitDetachedHead(origin, head) {
export async function gitPackRefs() { export async function gitPackRefs() {
await execa('git', ['pack-refs', '--all']); await execa('git', ['pack-refs', '--all']);
} }
/**
* Add a new Git configuration.
*
* @param {string} name Config name.
* @param {string} value Config value.
*/
export async function gitAddConfig(name, value) {
await execa('git', ['config', '--add', name, value]);
}

View File

@ -1,8 +1,8 @@
import test from 'ava'; import test from 'ava';
import {writeJson} from 'fs-extra';
import proxyquire from 'proxyquire'; import proxyquire from 'proxyquire';
import {stub} from 'sinon'; import {stub} from 'sinon';
import normalizeData from 'normalize-package-data'; import tempy from 'tempy';
import SemanticReleaseError from '@semantic-release/error';
import {gitHead as getGitHead} from '../lib/git'; import {gitHead as getGitHead} from '../lib/git';
import {gitRepo, gitCommits, gitTagVersion} from './helpers/git-utils'; import {gitRepo, gitCommits, gitTagVersion} from './helpers/git-utils';
@ -41,11 +41,9 @@ test.serial('Plugins are called with expected values', async t => {
// Add new commits to the master branch // Add new commits to the master branch
commits = (await gitCommits(['Second'])).concat(commits); commits = (await gitCommits(['Second'])).concat(commits);
const name = 'package-name';
const lastRelease = {version: '1.0.0', gitHead: commits[commits.length - 1].hash, gitTag: 'v1.0.0'}; const lastRelease = {version: '1.0.0', gitHead: commits[commits.length - 1].hash, gitTag: 'v1.0.0'};
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'}; const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
const notes = 'Release notes'; const notes = 'Release notes';
const verifyConditions1 = stub().resolves(); const verifyConditions1 = stub().resolves();
const verifyConditions2 = stub().resolves(); const verifyConditions2 = stub().resolves();
const getLastRelease = stub().resolves(lastRelease); const getLastRelease = stub().resolves(lastRelease);
@ -56,6 +54,7 @@ test.serial('Plugins are called with expected values', async t => {
const options = { const options = {
branch: 'master', branch: 'master',
repositoryUrl: 'git@hostname.com:owner/module.git',
verifyConditions: [verifyConditions1, verifyConditions2], verifyConditions: [verifyConditions1, verifyConditions2],
getLastRelease, getLastRelease,
analyzeCommits, analyzeCommits,
@ -63,34 +62,26 @@ test.serial('Plugins are called with expected values', async t => {
generateNotes, generateNotes,
publish, publish,
}; };
const pkg = {name, version: '0.0.0-dev'};
normalizeData(pkg);
await writeJson('./package.json', pkg);
await t.context.semanticRelease(options); await t.context.semanticRelease(options);
t.true(verifyConditions1.calledOnce); t.true(verifyConditions1.calledOnce);
t.deepEqual(verifyConditions1.firstCall.args[1], {env: process.env, options, pkg, logger: t.context.logger}); t.deepEqual(verifyConditions1.firstCall.args[1], {options, logger: t.context.logger});
t.true(verifyConditions2.calledOnce); t.true(verifyConditions2.calledOnce);
t.deepEqual(verifyConditions2.firstCall.args[1], {env: process.env, options, pkg, logger: t.context.logger}); t.deepEqual(verifyConditions2.firstCall.args[1], {options, logger: t.context.logger});
t.true(getLastRelease.calledOnce); t.true(getLastRelease.calledOnce);
t.deepEqual(getLastRelease.firstCall.args[1], {env: process.env, options, pkg, logger: t.context.logger}); t.deepEqual(getLastRelease.firstCall.args[1], {options, logger: t.context.logger});
t.true(analyzeCommits.calledOnce); t.true(analyzeCommits.calledOnce);
t.deepEqual(analyzeCommits.firstCall.args[1].env, process.env);
t.deepEqual(analyzeCommits.firstCall.args[1].options, options); t.deepEqual(analyzeCommits.firstCall.args[1].options, options);
t.deepEqual(analyzeCommits.firstCall.args[1].pkg, pkg);
t.deepEqual(analyzeCommits.firstCall.args[1].logger, t.context.logger); t.deepEqual(analyzeCommits.firstCall.args[1].logger, t.context.logger);
t.deepEqual(analyzeCommits.firstCall.args[1].lastRelease, lastRelease); t.deepEqual(analyzeCommits.firstCall.args[1].lastRelease, lastRelease);
t.deepEqual(analyzeCommits.firstCall.args[1].commits[0].hash.substring(0, 7), commits[0].hash); t.deepEqual(analyzeCommits.firstCall.args[1].commits[0].hash.substring(0, 7), commits[0].hash);
t.deepEqual(analyzeCommits.firstCall.args[1].commits[0].message, commits[0].message); t.deepEqual(analyzeCommits.firstCall.args[1].commits[0].message, commits[0].message);
t.true(verifyRelease.calledOnce); t.true(verifyRelease.calledOnce);
t.deepEqual(verifyRelease.firstCall.args[1].env, process.env);
t.deepEqual(verifyRelease.firstCall.args[1].options, options); t.deepEqual(verifyRelease.firstCall.args[1].options, options);
t.deepEqual(verifyRelease.firstCall.args[1].pkg, pkg);
t.deepEqual(verifyRelease.firstCall.args[1].logger, t.context.logger); t.deepEqual(verifyRelease.firstCall.args[1].logger, t.context.logger);
t.deepEqual(verifyRelease.firstCall.args[1].lastRelease, lastRelease); t.deepEqual(verifyRelease.firstCall.args[1].lastRelease, lastRelease);
t.deepEqual(verifyRelease.firstCall.args[1].commits[0].hash.substring(0, 7), commits[0].hash); t.deepEqual(verifyRelease.firstCall.args[1].commits[0].hash.substring(0, 7), commits[0].hash);
@ -98,9 +89,7 @@ test.serial('Plugins are called with expected values', async t => {
t.deepEqual(verifyRelease.firstCall.args[1].nextRelease, nextRelease); t.deepEqual(verifyRelease.firstCall.args[1].nextRelease, nextRelease);
t.true(generateNotes.calledOnce); t.true(generateNotes.calledOnce);
t.deepEqual(generateNotes.firstCall.args[1].env, process.env);
t.deepEqual(generateNotes.firstCall.args[1].options, options); t.deepEqual(generateNotes.firstCall.args[1].options, options);
t.deepEqual(generateNotes.firstCall.args[1].pkg, pkg);
t.deepEqual(generateNotes.firstCall.args[1].logger, t.context.logger); t.deepEqual(generateNotes.firstCall.args[1].logger, t.context.logger);
t.deepEqual(generateNotes.firstCall.args[1].lastRelease, lastRelease); t.deepEqual(generateNotes.firstCall.args[1].lastRelease, lastRelease);
t.deepEqual(generateNotes.firstCall.args[1].commits[0].hash.substring(0, 7), commits[0].hash); t.deepEqual(generateNotes.firstCall.args[1].commits[0].hash.substring(0, 7), commits[0].hash);
@ -109,7 +98,6 @@ test.serial('Plugins are called with expected values', async t => {
t.true(publish.calledOnce); t.true(publish.calledOnce);
t.deepEqual(publish.firstCall.args[1].options, options); t.deepEqual(publish.firstCall.args[1].options, options);
t.deepEqual(publish.firstCall.args[1].pkg, pkg);
t.deepEqual(publish.firstCall.args[1].logger, t.context.logger); t.deepEqual(publish.firstCall.args[1].logger, t.context.logger);
t.deepEqual(publish.firstCall.args[1].lastRelease, lastRelease); t.deepEqual(publish.firstCall.args[1].lastRelease, lastRelease);
t.deepEqual(publish.firstCall.args[1].commits[0].hash.substring(0, 7), commits[0].hash); t.deepEqual(publish.firstCall.args[1].commits[0].hash.substring(0, 7), commits[0].hash);
@ -139,6 +127,7 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre
const options = { const options = {
branch: 'master', branch: 'master',
repositoryUrl: 'git@hostname.com:owner/module.git',
verifyConditions: stub().resolves(), verifyConditions: stub().resolves(),
getLastRelease: stub().resolves(lastRelease), getLastRelease: stub().resolves(lastRelease),
analyzeCommits: stub().resolves(nextRelease.type), analyzeCommits: stub().resolves(nextRelease.type),
@ -147,7 +136,6 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre
publish: [publish1, publish2], publish: [publish1, publish2],
}; };
await writeJson('./package.json', {});
await t.context.semanticRelease(options); await t.context.semanticRelease(options);
t.true(generateNotes.calledTwice); t.true(generateNotes.calledTwice);
@ -172,7 +160,6 @@ test.serial('Dry-run skips verifyConditions and publish', async t => {
// Add new commits to the master branch // Add new commits to the master branch
commits = (await gitCommits(['Second'])).concat(commits); commits = (await gitCommits(['Second'])).concat(commits);
const name = 'package-name';
const lastRelease = {version: '1.0.0', gitHead: commits[commits.length - 1].hash, gitTag: 'v1.0.0'}; const lastRelease = {version: '1.0.0', gitHead: commits[commits.length - 1].hash, gitTag: 'v1.0.0'};
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'}; const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
const notes = 'Release notes'; const notes = 'Release notes';
@ -187,6 +174,7 @@ test.serial('Dry-run skips verifyConditions and publish', async t => {
const options = { const options = {
dryRun: true, dryRun: true,
branch: 'master', branch: 'master',
repositoryUrl: 'git@hostname.com:owner/module.git',
verifyConditions, verifyConditions,
getLastRelease, getLastRelease,
analyzeCommits, analyzeCommits,
@ -194,10 +182,6 @@ test.serial('Dry-run skips verifyConditions and publish', async t => {
generateNotes, generateNotes,
publish, publish,
}; };
const pkg = {name, version: '0.0.0-dev'};
normalizeData(pkg);
await writeJson('./package.json', pkg);
await t.context.semanticRelease(options); await t.context.semanticRelease(options);
@ -208,3 +192,25 @@ test.serial('Dry-run skips verifyConditions and publish', async t => {
t.true(generateNotes.calledOnce); t.true(generateNotes.calledOnce);
t.true(publish.notCalled); t.true(publish.notCalled);
}); });
test.serial('Throw SemanticReleaseError if not running from a git repository', async t => {
// Set the current working directory to a temp directory
process.chdir(tempy.directory());
const error = await t.throws(t.context.semanticRelease());
// Verify error code and type
t.is(error.code, 'ENOGITREPO');
t.true(error instanceof SemanticReleaseError);
});
test.serial('Throw SemanticReleaseError if repositoryUrl is not set and canot be found', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const error = await t.throws(t.context.semanticRelease());
// Verify error code and type
t.is(error.code, 'ENOREPOURL');
t.true(error instanceof SemanticReleaseError);
});

View File

@ -7,7 +7,7 @@ import registry from './helpers/registry';
import mockServer from './helpers/mockserver'; import mockServer from './helpers/mockserver';
import semanticRelease from '..'; import semanticRelease from '..';
/* eslint-disable camelcase */ /* eslint camelcase: ["error", {properties: "never"}] */
// Environment variables used with cli // Environment variables used with cli
const env = { const env = {