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:
		
							parent
							
								
									5bec59b26b
								
							
						
					
					
						commit
						0c67ba517f
					
				| @ -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` | ||||
|  | ||||
							
								
								
									
										3
									
								
								cli.js
									
									
									
									
									
								
							
							
						
						
									
										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
 | ||||
|  | ||||
							
								
								
									
										30
									
								
								index.js
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								index.js
									
									
									
									
									
								
							| @ -1,32 +1,40 @@ | ||||
| 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 => { | ||||
|   const config = await getConfig(opts, logger); | ||||
|   const {plugins, env, options, pkg} = config; | ||||
|   if (!await isGitRepo()) { | ||||
|     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) { | ||||
|     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'); | ||||
|   } | ||||
| @ -34,9 +42,9 @@ module.exports = async opts => { | ||||
|   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); | ||||
|   } | ||||
|  | ||||
| @ -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; | ||||
| } | ||||
|  | ||||
							
								
								
									
										18
									
								
								lib/git.js
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								lib/git.js
									
									
									
									
									
								
							| @ -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}; | ||||
|  | ||||
							
								
								
									
										21
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								package.json
									
									
									
									
									
								
							| @ -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 | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| import test from 'ava'; | ||||
| import {writeJson} from 'fs-extra'; | ||||
| import {writeFile, writeJson} from 'fs-extra'; | ||||
| import proxyquire from 'proxyquire'; | ||||
| import {stub} from 'sinon'; | ||||
| import normalizeData from 'normalize-package-data'; | ||||
| import {gitRepo} from './helpers/git-utils'; | ||||
| import yaml from 'js-yaml'; | ||||
| import {gitRepo, gitCommits, gitShallowClone, gitAddConfig} from './helpers/git-utils'; | ||||
| 
 | ||||
| test.beforeEach(t => { | ||||
|   // Save the current process.env
 | ||||
| @ -21,85 +21,175 @@ test.afterEach.always(t => { | ||||
|   process.env = Object.assign({}, t.context.env); | ||||
| }); | ||||
| 
 | ||||
| test.serial('Default values', async t => { | ||||
|   const pkg = {name: 'package_name', release: {}}; | ||||
| test.serial('Default values, reading repositoryUrl from package.json', async t => { | ||||
|   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
 | ||||
|   await gitRepo(); | ||||
|   // Create package.json in repository root
 | ||||
|   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
 | ||||
|   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 = { | ||||
|     analyzeCommits: 'analyzeCommits', | ||||
|     generateNotes: 'generateNotes', | ||||
|     getLastRelease: { | ||||
|       path: 'getLastRelease', | ||||
|       param: 'getLastRelease_param', | ||||
|     }, | ||||
|     getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_param'}, | ||||
|     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
 | ||||
|   await gitRepo(); | ||||
|   // 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
 | ||||
|   t.is(result.options.analyzeCommits, release.analyzeCommits); | ||||
|   t.is(result.options.generateNotes, release.generateNotes); | ||||
|   t.deepEqual(result.options.getLastRelease, release.getLastRelease); | ||||
|   t.is(result.options.branch, release.branch); | ||||
| 
 | ||||
|   t.deepEqual(options, release); | ||||
|   // 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.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); | ||||
|   t.deepEqual(t.context.plugins.firstCall.args[0], release); | ||||
| }); | ||||
| 
 | ||||
| test.serial('Prioritise cli parameters over package.json configuration', async t => { | ||||
| test.serial('Read options from .releaserc.yml', async t => { | ||||
|   const release = { | ||||
|     analyzeCommits: 'analyzeCommits', | ||||
|     generateNotes: 'generateNotes', | ||||
|     getLastRelease: { | ||||
|       path: 'getLastRelease', | ||||
|       param: 'getLastRelease_pkg', | ||||
|     }, | ||||
|     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.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', | ||||
|   }; | ||||
|   const options = { | ||||
|     getLastRelease: { | ||||
|       path: 'getLastRelease', | ||||
|       param: 'getLastRelease_cli', | ||||
|     }, | ||||
|     getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_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
 | ||||
|   await gitRepo(); | ||||
|   const repo = await gitRepo(); | ||||
|   await gitCommits(['First']); | ||||
|   // Create a clone
 | ||||
|   await gitShallowClone(repo); | ||||
|   // Create package.json in repository root
 | ||||
|   await writeJson('./package.json', pkg); | ||||
| 
 | ||||
|   const result = await t.context.getConfig(options); | ||||
| 
 | ||||
|   // Verify the options contains the plugin config from cli
 | ||||
|   t.deepEqual(result.options.getLastRelease, options.getLastRelease); | ||||
|   t.is(result.options.branch, options.branch); | ||||
| 
 | ||||
|   t.deepEqual(result.options, options); | ||||
|   // Verify the plugins module is called with the plugin options from cli
 | ||||
|   t.deepEqual(t.context.plugins.firstCall.args[0].getLastRelease, options.getLastRelease); | ||||
|   t.is(t.context.plugins.firstCall.args[0].branch, options.branch); | ||||
|   t.deepEqual(t.context.plugins.firstCall.args[0], options); | ||||
| }); | ||||
|  | ||||
| @ -1,6 +1,15 @@ | ||||
| import test from 'ava'; | ||||
| import {gitTagHead, gitCommitTag, isCommitInHistory, unshallow, gitHead} from '../lib/git'; | ||||
| import {gitRepo, gitCommits, gitCheckout, gitTagVersion, gitShallowClone, gitLog} from './helpers/git-utils'; | ||||
| import fileUrl from 'file-url'; | ||||
| 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 => { | ||||
|   // 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.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); | ||||
| }); | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| import tempy from 'tempy'; | ||||
| import execa from 'execa'; | ||||
| import pMapSeries from 'p-map-series'; | ||||
| import fileUrl from 'file-url'; | ||||
| import pReduce from 'p-reduce'; | ||||
| 
 | ||||
| /** | ||||
|  * 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). | ||||
|  */ | ||||
| export async function gitCommits(messages) { | ||||
|   return (await pMapSeries(messages, async msg => { | ||||
|   return (await pReduce( | ||||
|     messages, | ||||
|     async (commits, msg) => { | ||||
|       const {stdout} = await execa('git', ['commit', '-m', msg, '--allow-empty', '--no-gpg-sign']); | ||||
|       const [, branch, hash, message] = /^\[(\w+)\(?.*?\)?(\w+)\] (.+)$/.exec(stdout); | ||||
|     return {branch, hash, message}; | ||||
|   })).reverse(); | ||||
|       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(); | ||||
| 
 | ||||
|   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; | ||||
| } | ||||
| 
 | ||||
| @ -137,3 +143,13 @@ export async function gitDetachedHead(origin, head) { | ||||
| export async function gitPackRefs() { | ||||
|   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]); | ||||
| } | ||||
|  | ||||
| @ -1,8 +1,8 @@ | ||||
| import test from 'ava'; | ||||
| import {writeJson} from 'fs-extra'; | ||||
| import proxyquire from 'proxyquire'; | ||||
| 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 {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
 | ||||
|   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 nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'}; | ||||
|   const notes = 'Release notes'; | ||||
| 
 | ||||
|   const verifyConditions1 = stub().resolves(); | ||||
|   const verifyConditions2 = stub().resolves(); | ||||
|   const getLastRelease = stub().resolves(lastRelease); | ||||
| @ -56,6 +54,7 @@ test.serial('Plugins are called with expected values', async t => { | ||||
| 
 | ||||
|   const options = { | ||||
|     branch: 'master', | ||||
|     repositoryUrl: 'git@hostname.com:owner/module.git', | ||||
|     verifyConditions: [verifyConditions1, verifyConditions2], | ||||
|     getLastRelease, | ||||
|     analyzeCommits, | ||||
| @ -63,34 +62,26 @@ test.serial('Plugins are called with expected values', async t => { | ||||
|     generateNotes, | ||||
|     publish, | ||||
|   }; | ||||
|   const pkg = {name, version: '0.0.0-dev'}; | ||||
|   normalizeData(pkg); | ||||
| 
 | ||||
|   await writeJson('./package.json', pkg); | ||||
| 
 | ||||
|   await t.context.semanticRelease(options); | ||||
| 
 | ||||
|   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.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.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.deepEqual(analyzeCommits.firstCall.args[1].env, process.env); | ||||
|   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].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].message, commits[0].message); | ||||
| 
 | ||||
|   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].pkg, pkg); | ||||
|   t.deepEqual(verifyRelease.firstCall.args[1].logger, t.context.logger); | ||||
|   t.deepEqual(verifyRelease.firstCall.args[1].lastRelease, lastRelease); | ||||
|   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.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].pkg, pkg); | ||||
|   t.deepEqual(generateNotes.firstCall.args[1].logger, t.context.logger); | ||||
|   t.deepEqual(generateNotes.firstCall.args[1].lastRelease, lastRelease); | ||||
|   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.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].lastRelease, lastRelease); | ||||
|   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 = { | ||||
|     branch: 'master', | ||||
|     repositoryUrl: 'git@hostname.com:owner/module.git', | ||||
|     verifyConditions: stub().resolves(), | ||||
|     getLastRelease: stub().resolves(lastRelease), | ||||
|     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], | ||||
|   }; | ||||
| 
 | ||||
|   await writeJson('./package.json', {}); | ||||
|   await t.context.semanticRelease(options); | ||||
| 
 | ||||
|   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
 | ||||
|   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 nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'}; | ||||
|   const notes = 'Release notes'; | ||||
| @ -187,6 +174,7 @@ test.serial('Dry-run skips verifyConditions and publish', async t => { | ||||
|   const options = { | ||||
|     dryRun: true, | ||||
|     branch: 'master', | ||||
|     repositoryUrl: 'git@hostname.com:owner/module.git', | ||||
|     verifyConditions, | ||||
|     getLastRelease, | ||||
|     analyzeCommits, | ||||
| @ -194,10 +182,6 @@ test.serial('Dry-run skips verifyConditions and publish', async t => { | ||||
|     generateNotes, | ||||
|     publish, | ||||
|   }; | ||||
|   const pkg = {name, version: '0.0.0-dev'}; | ||||
|   normalizeData(pkg); | ||||
| 
 | ||||
|   await writeJson('./package.json', pkg); | ||||
| 
 | ||||
|   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(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); | ||||
| }); | ||||
|  | ||||
| @ -7,7 +7,7 @@ import registry from './helpers/registry'; | ||||
| import mockServer from './helpers/mockserver'; | ||||
| import semanticRelease from '..'; | ||||
| 
 | ||||
| /* eslint-disable camelcase */ | ||||
| /* eslint camelcase: ["error", {properties: "never"}] */ | ||||
| 
 | ||||
| // Environment variables used with cli
 | ||||
| const env = { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user