Permalink
Browse files

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`.
  • Loading branch information...
pvdlg committed Nov 23, 2017
1 parent 5bec59b commit 0c67ba517fd6dd42959ee263ad50a6c7c30d57af
Showing with 290 additions and 108 deletions.
  1. +1 −2 README.md
  2. +2 −1 cli.js
  3. +18 −10 index.js
  4. +14 −6 lib/get-config.js
  5. +17 −1 lib/git.js
  6. +12 −9 package.json
  7. +135 −45 test/get-config.test.js
  8. +37 −2 test/git.test.js
  9. +23 −7 test/helpers/git-utils.js
  10. +30 −24 test/index.test.js
  11. +1 −1 test/integration.test.js
View
@@ -167,6 +167,7 @@ semantic-release
These options are currently available:
- `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
- `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 it’s this object.
- `config`: A config object containing a lot of information to act upon.
- `env`: All environment variables
- `options`: `semantic-release` options like `debug`, or `branch`
- `pkg`: Parsed `package.json`
- For certain plugins the `config` object contains even more information. See below.
### `analyzeCommits`
View
3 cli.js
@@ -10,6 +10,7 @@ module.exports = async () => {
.name('semantic-release')
.description('Run automated package publishing')
.option('-b, --branch <branch>', 'Branch to release from')
.option('-r, --repositoryUrl <repositoryUrl>', 'Git repository URL')
.option(
'--verify-conditions <paths>',
'Comma separated list of paths or packages name for the verifyConditions plugin(s)',
@@ -41,7 +42,7 @@ module.exports = async () => {
program.outputHelp();
process.exitCode = 1;
} else {
await require('./index')(program.opts());
await require('.')(program.opts());
}
} 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
View
@@ -1,42 +1,50 @@
const marked = require('marked');
const TerminalRenderer = require('marked-terminal');
const SemanticReleaseError = require('@semantic-release/error');
const {gitHead: getGitHead} = require('./lib/git');
const getConfig = require('./lib/get-config');
const getNextVersion = require('./lib/get-next-version');
const getCommits = require('./lib/get-commits');
const logger = require('./lib/logger');
const {gitHead: getGitHead, isGitRepo} = require('./lib/git');
module.exports = async opts => {
if (!await isGitRepo()) {
throw new SemanticReleaseError('Semantic-release must run from a git repository', 'ENOGITREPO');
}
const config = await getConfig(opts, logger);
const {plugins, env, options, pkg} = config;
const {plugins, options} = config;
if (!options.repositoryUrl) {
throw new SemanticReleaseError('The repositoryUrl option is required', 'ENOREPOURL');
}
logger.log('Run automated release for branch %s', options.branch);
logger.log('Run automated release from branch %s', options.name, options.branch);
if (!options.dryRun) {
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');
const {commits, lastRelease} = await getCommits(
await plugins.getLastRelease({env, options, pkg, logger}),
await plugins.getLastRelease({options, logger}),
options.branch,
logger
);
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) {
throw new SemanticReleaseError('There are no relevant changes, so no new version is released.', 'ENOCHANGE');
}
const version = getNextVersion(type, lastRelease, logger);
const nextRelease = {type, version, gitHead: await getGitHead(), gitTag: `v${version}`};
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) {
logger.log('Call plugin %s', 'generate-notes');
@@ -49,7 +57,7 @@ module.exports = async opts => {
nextRelease.notes = await plugins.generateNotes(generateNotesParam);
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();
// If previous publish plugin has created a commit (gitHead changed)
if (prevInput.nextRelease.gitHead !== newGitHead) {
@@ -59,7 +67,7 @@ module.exports = async opts => {
nextRelease.notes = await plugins.generateNotes(generateNotesParam);
}
// 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);
}
View
@@ -1,19 +1,27 @@
const {readJson} = require('fs-extra');
const readPkgUp = require('read-pkg-up');
const {defaults} = require('lodash');
const normalizeData = require('normalize-package-data');
const cosmiconfig = require('cosmiconfig');
const debug = require('debug')('semantic-release:config');
const {repoUrl} = require('./git');
const plugins = require('./plugins');
module.exports = async (opts, logger) => {
const pkg = await readJson('./package.json');
normalizeData(pkg);
const options = defaults(opts, pkg.release, {branch: 'master'});
const {config} = (await cosmiconfig('release', {rcExtensions: true}).load(process.cwd())) || {};
const options = defaults(opts, config, {branch: 'master', repositoryUrl: (await pkgRepoUrl()) || (await repoUrl())});
debug('name: %O', options.name);
debug('branch: %O', options.branch);
debug('repositoryUrl: %O', options.repositoryUrl);
debug('analyzeCommits: %O', options.analyzeCommits);
debug('generateNotes: %O', options.generateNotes);
debug('verifyConditions: %O', options.verifyConditions);
debug('verifyRelease: %O', options.verifyRelease);
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
@@ -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
@@ -15,25 +15,25 @@
}
},
"dependencies": {
"@semantic-release/commit-analyzer": "^4.0.0",
"@semantic-release/condition-travis": "^6.0.0",
"@semantic-release/commit-analyzer": "^5.0.0",
"@semantic-release/condition-travis": "^7.0.0",
"@semantic-release/error": "^2.1.0",
"@semantic-release/github": "^1.0.0",
"@semantic-release/npm": "^1.0.0",
"@semantic-release/release-notes-generator": "^5.0.0",
"@semantic-release/github": "^2.0.0",
"@semantic-release/npm": "^2.0.0",
"@semantic-release/release-notes-generator": "^6.0.0",
"chalk": "^2.3.0",
"commander": "^2.11.0",
"cosmiconfig": "^3.1.0",
"debug": "^3.1.0",
"execa": "^0.8.0",
"fs-extra": "^4.0.2",
"get-stream": "^3.0.0",
"git-log-parser": "^1.2.0",
"import-from": "^2.1.0",
"lodash": "^4.0.0",
"marked": "^0.3.6",
"marked-terminal": "^2.0.0",
"normalize-package-data": "^2.3.4",
"p-reduce": "^1.0.0",
"read-pkg-up": "^3.0.0",
"semver": "^5.4.1"
},
"devDependencies": {
@@ -44,11 +44,13 @@
"dockerode": "^2.5.2",
"eslint-config-prettier": "^2.5.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",
"nock": "^9.0.2",
"npm-registry-couchapp": "^2.6.12",
"nyc": "^11.2.1",
"p-map-series": "^1.0.0",
"prettier": "~1.8.0",
"proxyquire": "^1.8.0",
"sinon": "^4.0.0",
@@ -124,7 +126,8 @@
"prettier"
],
"rules": {
"prettier/prettier": 2
"prettier/prettier": 2,
"no-duplicate-imports": 2
}
}
}
Oops, something went wrong.

3 comments on commit 0c67ba5

@felixfbecker

This comment has been minimized.

Show comment
Hide comment
@felixfbecker

felixfbecker Nov 25, 2017

Contributor

Could we get a JSON schema on https://github.com/SchemaStore/schemastore?

Contributor

felixfbecker replied Nov 25, 2017

Could we get a JSON schema on https://github.com/SchemaStore/schemastore?

@pvdlg

This comment has been minimized.

Show comment
Hide comment
@pvdlg

pvdlg Nov 25, 2017

Member

Sure. Feel free to propose a PR for the releaserc format.
The problem is the format change based on the plugin configured. Is it something that can be achieved on shcemastore?

Member

pvdlg replied Nov 25, 2017

Sure. Feel free to propose a PR for the releaserc format.
The problem is the format change based on the plugin configured. Is it something that can be achieved on shcemastore?

@felixfbecker

This comment has been minimized.

Show comment
Hide comment
@felixfbecker

felixfbecker Nov 25, 2017

Contributor

No, I don't think so. But we can specify the top-level keys and could also add the types for the official @semantic-release plugins.

Contributor

felixfbecker replied Nov 25, 2017

No, I don't think so. But we can specify the top-level keys and could also add the types for the official @semantic-release plugins.

Please sign in to comment.