Compare commits

..
152 Commits
Author SHA1 Message Date
greenkeeper[bot]andPierre Vanduynslager b7aeabad93 fix(package): update marked to version 0.6.0 2019-01-10 16:14:04 -05:00
greenkeeper[bot]andPierre Vanduynslager 3819ca56a9 chore(package): update ava to version 1.0.1 2018-12-27 14:43:11 -05:00
greenkeeper[bot]andPierre Vanduynslager 1aefd98d31 fix(package): update aggregate-error to version 2.0.0 2018-12-26 13:22:26 -05:00
Pierre Vanduynslager 1243f79064 fix: correctly handle skipped releases 2018-12-16 16:28:52 -05:00
Pierre Vanduynslager 70c68ef3ed feat: allow publish plugins to return false in order to signify no release was done 2018-12-14 16:16:27 -05:00
Pierre VanduynslagerandGregor Martynus 048292baa5 docs: fix broken link in CircleCI recipe 2018-12-14 10:48:58 -08:00
Pierre VanduynslagerandGregor Martynus 0f642ffe4d docs: remove mentions of travis-deploy-once 2018-12-14 10:48:58 -08:00
greenkeeper[bot]andPierre Vanduynslager 89ad3a0b61 chore(package): update p-retry to version 3.0.0 2018-12-12 11:59:37 -05:00
Pierre Vanduynslager 649b53087a docs: mention that debug option is CLI only 2018-12-11 13:33:06 -05:00
Pierre Vanduynslager 862ec4c087 fix: allow to set ci option via API and config file 2018-12-11 13:33:06 -05:00
Pierre Vanduynslager 6b110b6e9e docs: switch to spectrum.chat 2018-12-03 17:48:15 -05:00
Pierre Vanduynslager e4c6649714 docs: syntax fixes in plugins list 2018-11-30 03:25:05 -05:00
Pierre Vanduynslager 6220641aba docs: add @semantic-release/apm to plugins list 2018-11-30 03:25:05 -05:00
Evan SirokyandPierre Vanduynslager a45273e921 docs: add maven-semantic-release to list of community plugins 2018-11-30 03:12:00 -05:00
Pierre Vanduynslager d1091133b9 chore(package): update nyc and sinon 2018-11-30 02:10:04 -05:00
Pierre Vanduynslager cd69583746 test: delete unused test helper file 2018-11-30 01:24:16 -05:00
Pierre Vanduynslager ffe1062830 fix: remove unnecessary branch parameter from push function 2018-11-29 20:08:25 -05:00
Pierre Vanduynslager 7a9922a492 fix: rename default branch 2018-11-28 17:32:05 -05:00
Andy EdwardsandPierre Vanduynslager d6d1bc954e docs: grammar corrections 2018-11-27 16:04:50 -05:00
Pierre Vanduynslager eafbb343dd fix: correctly resolve plugins installed globally with npx
This bug affects only plugins defined with the `plugins` option and wrapped in an Array to add a configuration
2018-11-26 18:39:28 -05:00
Pierre Vanduynslager 2b082acc73 fix: push only tags to remote repo 2018-11-21 12:32:01 -05:00
Pierre Vanduynslager aa022e0ac2 test: clarify variables name 2018-11-21 12:32:01 -05:00
Pierre Vanduynslager f2ede5bdde docs: typo in JS docs 2018-11-19 18:34:16 -05:00
Pierre Vanduynslager 9742712957 test: verify errors have message and details 2018-11-19 18:34:16 -05:00
Pierre Vanduynslager 5937f13bdf test: fix typo 2018-11-19 18:34:16 -05:00
Pierre Vanduynslager 43dbd10046 refactor: prefer filter shorter form 2018-11-19 18:34:16 -05:00
Pierre Vanduynslager 9f5645cfa0 refactor: harmonize git utils function names 2018-11-19 18:34:16 -05:00
Pierre Vanduynslager e594638a96 test: prefer array spread 2018-11-19 18:34:16 -05:00
Kostis (Codefresh)andPierre Vanduynslager 244f014a91 docs: add Codefresh in list of CIs with pipelines 2018-11-18 14:59:12 -05:00
Waldir PimentaandPierre Vanduynslager 218d830af0 docs: fix quote marks (#986) 2018-11-16 13:06:48 -05:00
Pierre Vanduynslager 90da6a0bc1 feat: add logs about prepare, publish, success and fail plugins skipped in dry-run mode 2018-11-14 11:27:06 -05:00
Pierre Vanduynslager 90b497464e test: test that dry-run also skips prepare step 2018-11-14 11:27:06 -05:00
Pierre Vanduynslager c86518a050 fix: do not underline log messages 2018-11-14 11:27:06 -05:00
Pierre Vanduynslager e92c84ec08 fix: indicate in logs if running in dry-run mode 2018-11-14 11:27:06 -05:00
Pierre Vanduynslager 5180001ae6 feat: support multiple plugins for the analyzeCommits step
In case multiple plugins with a `analyzeCommits` step are configured, all of them will be executed and the highest release type (`major` > `minor`, `patch`) will be used.
2018-11-12 15:06:46 -05:00
Pierre Vanduynslager 728ea34dda fix: remove redundant log 2018-11-12 15:06:46 -05:00
Pierre Vanduynslager 83af7acf69 docs: mention default analyzeCommits plugin 2018-11-12 15:06:46 -05:00
Pierre Vanduynslager 8e564ebc9d docs: update FAQ to reflect new plugins option 2018-11-09 17:08:49 -05:00
Pierre Vanduynslager dff1f1056f docs: mention postversion npm script hook to run build scripts 2018-11-09 17:08:49 -05:00
Pierre Vanduynslager ae4995c867 style: fix prettier errors 2018-11-07 12:19:19 -05:00
greenkeeper[bot]andPierre Vanduynslager 669c9ed300 fix(package): update find-versions to version 3.0.0 2018-11-06 12:59:37 -05:00
Pierre Vanduynslager 3c8177aed8 fix: add debug logs for config file path 2018-11-02 15:00:12 -04:00
Pierre Vanduynslager 6d0bd8c9b1 fix: add debug logs for plugins resolved options 2018-11-02 15:00:12 -04:00
Pierre Vanduynslager a99355effa fix: remove dependency to git-url-parse 2018-10-27 09:35:45 -04:00
Pierre Vanduynslager cc06d8999a test: fix errors ordering in assertions 2018-10-27 09:35:45 -04:00
Lucas CorreiaandPierre Vanduynslager 3411520ea7 fix: include release notes in JS API result for dry mode 2018-10-20 12:37:11 -04:00
Pierre Vanduynslager 3d5db1f8b0 fix: remove debugging console.error 2018-10-20 04:24:21 -04:00
Cedric van PuttenandPierre Vanduynslager cfc10039f3 docs: add semantic-release-expo to plugins list 2018-10-18 11:10:52 -04:00
Pierre Vanduynslager 0578c8b91c fix: do not log outated branch error for missing permission cases 2018-10-17 15:33:16 -04:00
Pierre Vanduynslager e29110103b docs: add section existing tags in configuration docs 2018-10-09 22:02:34 -04:00
Pierre Vanduynslager 88539222f4 docs: add troubleshooting section for reference already exists Git error 2018-10-09 22:02:34 -04:00
Pierre Vanduynslager d45861b989 docs: clarify the "npm missing permission" troubleshooting section 2018-10-09 22:02:34 -04:00
Pierre Vanduynslager 2ba0b81f39 docs: remove troubleshooting section related to legacy error messages 2018-10-09 22:02:34 -04:00
Pierre Vanduynslager e93a663490 docs: fix markdown link in configuration docs 2018-10-09 22:02:34 -04:00
Pierre Vanduynslager 58c25beeb6 fix: fix logs for plugins loaded with plugins option 2018-10-09 16:33:37 -04:00
Pierre Vanduynslager ff275a5cd4 fix: use module name in logs for plugins loaded with plugins option 2018-10-09 15:29:04 -04:00
Jamie BartonandPierre Vanduynslager 3e8216ab35 docs: typo in configuration docs 2018-10-09 12:20:35 -04:00
Pierre Vanduynslager aa9d5c6efe docs: add a Getting started section and clarify config steps 2018-10-08 13:24:51 -04:00
Pierre Vanduynslager 5ba5010c80 feat: add new plugins option 2018-10-08 13:24:51 -04:00
Pierre Vanduynslager 9930dac69e fix: add cwd to ENOGITREPO error message 2018-10-04 01:49:44 -04:00
Pierre Vanduynslager bc8551c025 fix: typo in EPLUGINCONF error message 2018-10-04 01:49:44 -04:00
Pierre Vanduynslager 234a9105f6 docs: add Waffle.io README badge 2018-10-04 01:41:37 -04:00
greenkeeper[bot]andPierre Vanduynslager 68fce9616e chore(package): update commitizen to version 3.0.0 2018-10-01 13:41:09 -04:00
Mateusz NikielandPierre Vanduynslager fb6e2d80a8 docs: add semantic-release-gerrit to plugins list 2018-09-25 16:40:24 -04:00
greenkeeper[bot]andPierre Vanduynslager b9ae7d25ea fix(package): update env-ci to version 3.0.0 2018-09-19 17:00:16 -04:00
greenkeeper[bot]andPierre Vanduynslager c27e18ea3a chore(package): update nock to version 10.0.0 2018-09-14 16:59:05 -04:00
greenkeeper[bot]andPierre Vanduynslager 7b8cd9918b fix(package): update debug to version 4.0.0 2018-09-11 12:42:39 -04:00
Pierre Vanduynslager 540d4fa764 fix: pass custom env and cwd to env-ci 2018-09-07 17:21:21 -04:00
Pierre Vanduynslager 4f75cb3376 fix: support multiple generate-notes plugin in CLI arg 2018-09-07 10:46:04 -04:00
Patrick MowrerandPierre Vanduynslager 1348985b6c docs: analyzeCommits does not accept an Array
[Fixes #924]
2018-09-07 10:17:06 -04:00
Pierre Vanduynslager dfbb9f2429 docs: clarify the URL encoding of GIT_CREDENTIALS 2018-09-05 14:05:08 -04:00
greenkeeper[bot]andPierre Vanduynslager fa62d427b6 chore(package): update xo to version 0.23.0 2018-09-03 16:54:31 -04:00
greenkeeper[bot]andPierre Vanduynslager 42e9d1192f chore(package): update delay to version 4.0.0 2018-09-03 15:28:41 -04:00
Pierre Vanduynslager 468f8a8bda docs: clarify the --ci / --no-ci CLI option 2018-09-03 14:21:55 -04:00
Pierre Vanduynslager dffe148e33 fix: hide sensitive data in relesae notes and fail/success plugin params 2018-08-27 16:20:36 -04:00
greenkeeper[bot]andPierre Vanduynslager 1aed97e577 fix(package): update execa to version 1.0.0 2018-08-26 22:46:48 +02:00
greenkeeper[bot]andPierre Vanduynslager edd8bc2889 fix(package): update execa to version 0.11.0 2018-08-24 10:43:59 +02:00
greenkeeper[bot]andPierre Vanduynslager 2f4befed75 fix(package): update marked to version 0.5.0 2018-08-18 10:57:38 -04:00
Pierre Vanduynslager cfeaf49233 test: use real writable stream
This way plugins can pipe a stream to `stdout` and `stderr` in integration tests.
2018-08-15 16:27:55 -04:00
greenkeeper[bot]andPierre Vanduynslager e8820960ce fix(package): update get-stream to version 4.0.0 2018-08-10 15:02:13 -04:00
Pierre Vanduynslager 93377eb636 revert: "fix: do not convert ssh repositoryUrl to https"
This reverts commit b89523105c.
2018-08-10 13:16:29 -04:00
Pierre Vanduynslager b89523105c fix: do not convert ssh repositoryUrl to https 2018-08-08 12:02:02 -04:00
Felix BeckerandPierre Vanduynslager 8b8e40c91e docs(plugins): add semantic-release-firefox 2018-08-06 12:09:46 -04:00
greenkeeper[bot]andPierre Vanduynslager 06695a1af8 chore(package): update xo to version 0.22.0 2018-08-06 11:48:44 -04:00
Pierre Vanduynslager 63d422ed5c fix: do not clone stdout/stderr passed to pugins 2018-08-04 20:33:26 -04:00
greenkeeper[bot]andPierre Vanduynslager efb40000b4 fix(package): update @semantic-release/npm to version 5.0.1
Closes #882
2018-08-04 20:03:19 -04:00
greenkeeper[bot]andPierre Vanduynslager d8fa18272f chore(package): update got to version 9.0.0 2018-08-04 13:03:27 -04:00
Pierre Vanduynslager b082a2eb38 fix: do not hide env variable value if shorter than 5 2018-07-31 16:57:29 -04:00
Pierre Vanduynslager 43d0646150 fix: also hide sensitive info when loggin from cli.js 2018-07-30 14:06:51 -04:00
Pierre Vanduynslager b2d82c2ccb docs: specify for each step if one or more plugins are required/allowed 2018-07-29 23:50:17 -04:00
Pierre Vanduynslager 31ec1eb5de docs: fix configuration doc syntax 2018-07-29 23:50:17 -04:00
Pierre Vanduynslager d8c84a0e0b fix: clarify EPLUGINCONF error message
The message now specify if the step is required and if it allows to configure multiple plugins.
2018-07-29 23:50:17 -04:00
Pierre Vanduynslager 3cc62f0318 docs: add JS API documentation 2018-07-29 21:56:21 -04:00
Pierre Vanduynslager 417779e221 feat: return lastRelease, commits, nextRelease, releases from JS API 2018-07-29 21:56:21 -04:00
Pierre Vanduynslager c57443500f refactor: pass complete context to lib/plugins/normalize.js 2018-07-29 21:56:21 -04:00
Pierre Vanduynslager 0626d57116 feat: log with signale and allow to customize stdin and stdout 2018-07-29 21:56:21 -04:00
Pierre Vanduynslager f64046f1d9 docs: fix link to resources page 2018-07-29 21:56:21 -04:00
Pierre Vanduynslager 1d4f67e5e8 test: use semanticrelease/npm-registry-docker Docker image for tests 2018-07-29 21:48:35 -04:00
Carlos CubasandPierre Vanduynslager b9af5c42c9 docs: add new semantic-release-gcr plugin link
Update docs to add a new community plugin semantic-release-gcr. This plugin publishes a docker image to google cloud registry.
2018-07-20 14:01:32 -04:00
Pierre Vanduynslager 3fb3fa8378 fix: do not override env variable with default if defined 2018-07-18 19:37:11 -04:00
Pierre Vanduynslager a94e08de9a feat: pass cwd and env context to plugins
- Allow to run semantic-release (via API) from anywhere passing the current working directory.
- Allows to simplify the tests and to run them in parallel in both the core and plugins.
2018-07-17 00:42:04 -04:00
Pierre Vanduynslager 12e4155cd3 refactor: pass argv via proxyquire for cli tests 2018-07-17 00:42:04 -04:00
greenkeeper[bot]andPierre Vanduynslager 264472c998 chore(package): update fs-extra to version 7.0.0 2018-07-16 22:44:56 -04:00
greenkeeper[bot]andPierre Vanduynslager d3c7232b70 fix(package): update git-url-parse to version 10.0.1 2018-07-16 00:02:59 -04:00
Pierre Vanduynslager 89e584713d docs: migration to new GitBook version 2018-07-14 18:00:06 -04:00
greenkeeper[bot]andPierre Vanduynslager ed6a3818dc chore(package): update clear-module to version 3.0.0 2018-07-11 14:20:05 -04:00
Pierre Vanduynslager d8e59cce53 fix: set default path to generateNotes object config 2018-07-10 15:50:24 -04:00
Pierre Vanduynslager 24ce560065 refactor: build plugin pipeline parameters at initialization
In addition, factorize the pipeline config function to avoid code duplication.
2018-07-10 13:18:58 -04:00
Pierre Vanduynslager eb26254b00 refactor: use Object.entries rather than Object.keys 2018-07-10 13:18:58 -04:00
Pierre Vanduynslager 50061bb267 refactor: remove unnecessary object destructuring 2018-07-10 13:18:58 -04:00
Pierre Vanduynslager 5989989452 feat: allow to define multiple generateNotes plugins
Each `generateNotes` plugin will be called in the order defined and will receive the concatenation of the previous one in `nextRelease.notes`.
That gives each plugin the ability to test if there is a notes part that will precede it's own.
Each plugin is expected to return it's own part of the release notes only. **semantic-release** will take care of concatenating all the notes parts.
2018-07-10 13:18:58 -04:00
Pierre Vanduynslager 576eb6027f refactor: simplify plugin validation 2018-07-10 13:18:58 -04:00
Pierre Vanduynslager f7f4aabe9e refactor: use the lastInput arg to compute the prepare pipeline next input
Use the `getNextInput`'s provided argument rather than relying on the closure.
2018-07-10 13:18:58 -04:00
Pierre Vanduynslager 12de6282dd refactor: fix incorrect comments in lib/plugins/pipeline.js 2018-07-10 13:18:58 -04:00
Pierre Vanduynslager d3032868d8 docs: fix default value for analyzeCommits plugin 2018-07-10 13:18:58 -04:00
Pierre Vanduynslager ed9c456f5e refactor: always return an Array of results/errors from a plugin pipeline
Always return an `Array` from a pipeline simplify the function utilization as it's more deterministic. Previously, it would return/throw a single value/error when called with a single input and an `Array` of results/errors when called with an `Array` of input.
2018-07-10 13:18:58 -04:00
Pierre Vanduynslager cac48823f1 docs: clarify verifyRelease plugin description 2018-07-10 13:18:58 -04:00
Pierre Vanduynslager 09348f1b8a style: disable max-params warning for lib/plugins/normalize.js 2018-07-10 13:18:58 -04:00
Pierre Vanduynslager f93eeb799b fix: do not set path to plugin config defined as a Function or an Array
If a plugin hook was defined as a `Function` or an `Array` the `path` property would be set to the default value.
Even if this bug had no actual negative impact, it should be fixed so the code would perform as intended.
2018-07-10 13:18:58 -04:00
Pierre Vanduynslager 071dccea4b fix: use unauthenticated URL to check if branch is up to date
In case the authentication token provided is unauthorized the call to `isBranchUpToDate` will fail due to lack of read permission if that URL is used. As a result the error about outdated local branch will be reported instead of the one about missing permission.
By using the original (unauthenticated) URL `isBranchUpToDate` shouldn't fail due to permission as it requires only read permissions, that are necessarly present as the CI wass able to clone the repo.
2018-07-10 13:07:00 -04:00
Pierre Vanduynslager eb22f9998d docs: FAQ for reverting a release 2018-07-10 11:52:39 -04:00
Pierre Vanduynslager 5847514fcc fix: allow empty release notes in dry-run mode 2018-07-10 11:42:22 -04:00
Pierre Vanduynslager a39ccb8f6c docs: add missing link in GitBook summary 2018-07-08 01:16:21 -04:00
Pierre VanduynslagerandGitHub 0862480cf0 fix(package): update hosted-git-info to version 2.7.1 2018-07-06 21:47:50 -04:00
Pierre Vanduynslager 45eee4acdd fix: fetch all tags even if the repo is not shallow 2018-07-02 18:38:24 -04:00
Pierre Vanduynslager 2d3a5e53e9 test: harmonize git-utils functions name 2018-07-02 18:38:24 -04:00
Pierre Vanduynslager 4abda31f83 fix: add debug log for git fetch command 2018-07-02 16:50:41 -04:00
Pierre Vanduynslager 3602716c0b docs: update semantic-release and travis-deploy-once versions in examples 2018-07-02 11:23:37 -04:00
Pierre Vanduynslager d4f68a5680 fix(package): update yargs to version 12.0.0 2018-06-26 15:46:19 -04:00
Pierre Vanduynslager 4454d57e02 feat: allow to disable the publish plugin hook 2018-06-19 15:03:43 -04:00
greenkeeper[bot]andPierre Vanduynslager 9137f8537b fix(package): update read-pkg-up to version 4.0.0 2018-06-18 11:15:16 -04:00
Trevor RichardsonandPierre Vanduynslager 7615fdc9bc Fix documented explanation in plugins.md
`verifyConditions` explanation was wrong previously.
2018-06-18 10:52:27 -04:00
Pierre Vanduynslager 2b6378f26f fix: use git ls-remote to verify if the remote branch is ahead 2018-06-15 16:16:55 -04:00
Pierre Vanduynslager 24a8052038 refactor: simply EGITNOPERMISSION error parameters 2018-06-15 16:16:55 -04:00
greenkeeper[bot]andPierre Vanduynslager 0ab0426075 fix(package): update p-locate to version 3.0.0 2018-06-15 12:22:47 -04:00
Pierre Vanduynslager f9d9144e3d docs: Add a troubleshooting section about squashed commits 2018-06-11 11:52:23 -04:00
greenkeeper[bot]andPierre Vanduynslager 11cef46c48 chore(package): update sinon to version 6.0.0 2018-06-11 11:33:14 -04:00
greenkeeper[bot]andPierre Vanduynslager 29e7ebfe0b fix(package): update hook-std to version 1.0.0 2018-06-11 11:21:16 -04:00
Matt TraviandGregor Martynus 6a36832398 fix(plugin-load): clarify load message
added quotes around plugin name to set it apart from the message. without the quotes, some consumers
were missunderstanding the successful loading of the `fail` plugin as a load failure and assuming
something was broken

resolves #811
2018-06-05 22:27:05 -07:00
Pierre Vanduynslager 4d47b20831 docs: clarify config file format 2018-06-04 15:12:46 -04:00
greenkeeper[bot]andGregor Martynus ddcf29acf7 chore(package): update nyc to version 12.0.1 2018-06-02 10:24:04 -07:00
Felix BeckerandGregor Martynus 4c157f3bfd docs(plugins): add semantic-release-vsce 2018-05-30 16:35:46 -07:00
Felix BeckerandGregor Martynus c6636abfd2 docs(plugins): remove warning
semantic-release-docker is compatible with latest semantic-release now,
see peerDependency: https://david-dm.org/felixfbecker/semantic-release-docker?type=peer
2018-05-30 16:34:59 -07:00
greenkeeper[bot]andPierre Vanduynslager 44fd7fe3f6 chore(package): update delay to version 3.0.0 2018-05-25 14:38:20 -04:00
Pierre Vanduynslager db1cc60c41 feat: verify minimum required git version is installed 2018-05-21 17:52:52 -04:00
Pierre Vanduynslager 47c73eb672 feat: add support for git version 2.0.0 2018-05-21 17:52:52 -04:00
greenkeeper[bot]andPierre Vanduynslager cd8ff5df43 fix(package): update marked-terminal to version 3.0.0 2018-05-21 15:59:09 -04:00
greenkeeper[bot]andPierre Vanduynslager a387c045a7 fix(package): update marked to version 0.4.0 2018-05-21 18:02:52 +03:00
Pierre VanduynslagerandGregor Martynus 228451b7c9 docs: fix typo 2018-05-20 15:16:55 -07:00
Pierre Vanduynslager 3503407bf4 docs: clarify which commit types trigger a release 2018-05-20 23:55:47 +03:00
Pierre Vanduynslager 5025fcab55 docs: fix FAQ heading level 2018-05-20 23:55:47 +03:00
Pierre Vanduynslager 6693080b51 style: fix table indentation 2018-05-20 23:55:47 +03:00
75 changed files with 3689 additions and 2659 deletions
+2 -2
View File
@@ -7,11 +7,11 @@ node_js:
- 10
- 8
# Trigger a push build on caribou and greenkeeper branches + PRs build on every branches
# Trigger a push build on master and greenkeeper branches + PRs build on every branches
# Avoid double build on PRs (See https://github.com/travis-ci/travis-ci/issues/1147)
branches:
only:
- caribou
- master
- /^greenkeeper.*$/
# Retry install on fail to avoid failing a build on network/disk/external errors
+27 -23
View File
@@ -1,14 +1,14 @@
<h1 align="center" style="border-bottom: none;">📦🚀 semantic-release</h1>
<h3 align="center">Fully automated version management and package publishing</h3>
<p align="center">
<a href="https://gitter.im/semantic-release/semantic-release">
<img alt="Gitter" src="https://badges.gitter.im/semantic-release/semantic-release.svg">
<a href="https://spectrum.chat/semantic-release">
<img alt="Join the community on Spectrum" src="https://withspectrum.github.io/badge/badge.svg">
</a>
<a href="https://travis-ci.org/semantic-release/semantic-release">
<img alt="Travis" src="https://img.shields.io/travis/semantic-release/semantic-release/caribou.svg">
<img alt="Travis" src="https://img.shields.io/travis/semantic-release/semantic-release/master.svg">
</a>
<a href="https://codecov.io/gh/semantic-release/semantic-release">
<img alt="Codecov" src="https://img.shields.io/codecov/c/github/semantic-release/semantic-release/caribou.svg">
<img alt="Codecov" src="https://img.shields.io/codecov/c/github/semantic-release/semantic-release/master.svg">
</a>
<a href="https://greenkeeper.io">
<img alt="Greenkeeper" src="https://badges.greenkeeper.io/semantic-release/semantic-release.svg">
@@ -17,6 +17,11 @@
<img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
</a>
</p>
<p align="center">
<a href="https://waffle.io/semantic-release/semantic-release">
<img alt="Waffle.io" src="https://badge.waffle.io/semantic-release/semantic-release.svg?columns=all">
</a>
</p>
<p align="center">
<a href="https://www.npmjs.com/package/semantic-release">
<img alt="npm latest version" src="https://img.shields.io/npm/v/semantic-release/latest.svg">
@@ -68,31 +73,32 @@ Here is an example of the release type that will be done based on a commit messa
### Triggering a release
When pushing new commits to the release branch (i.e. `master`) with `git push` or by merging a pull request or merging from another branch, a CI build is triggered and runs the `semantic-release` command to make a release if there are relevant codebase changes since the last release.
For each new commits added to the release branch (i.e. `master`) with `git push` or by merging a pull request or merging from another branch, a CI build is triggered and runs the `semantic-release` command to make a release if there are codebase changes since the last release that affect the package functionalities.
By default a release will be done for each push to the release branch that contains relevant code changes. If you need more control over the timing of releases you have a couple of options:
If you need more control over the timing of releases you have a couple of options:
- Publish releases on a distribution channel (for example npms [dist-tags](https://docs.npmjs.com/cli/dist-tag)). This way you can keep control over what your users end up using by default, and you can decide when to make an automatically released version available to the stable channel, and promote it.
- Develop on a `dev` branch and merge it to the release branch (i.e. `master`) once you are ready to publish. **semantic-release** will run only on pushes to the release branch.
### Release steps
After running the tests the command `semantic-release` will execute the following steps:
After running the tests, the command `semantic-release` will execute the following steps:
| Step | Description |
|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Verify Conditions | Verify all the conditions to proceed with the release with the [verify conditions plugins](docs/usage/plugins.md#verifyconditions-plugin). |
| Get last release | Obtain the commit corresponding to the last release by analyzing [Git tags](https://git-scm.com/book/en/v2/Git-Basics-Tagging). |
| Analyze commits | Determine the type of release with the [analyze commits plugin](docs/usage/plugins.md#analyzecommits-plugin) based on the commits added since the last release. |
| Verify release | Verify the release conformity with the [verify release plugins](docs/usage/plugins.md#verifyrelease-plugin). |
| Generate notes | Generate release notes with the [generate notes plugin](docs/usage/plugins.md#generatenotes-plugin) for the commits added since the last release. |
| Create Git tag | Create a Git tag corresponding to the new release version |
| Prepare | Prepare the release with the [prepare plugins](docs/usage/plugins.md#prepare-plugin). |
| Publish | Publish the release with the [publish plugins](docs/usage/plugins.md#publish-plugin). |
| Notify | Notify of new releases or errors with the [success](docs/usage/plugins.md#success-plugin) and [fail](docs/usage/plugins.md#fail-plugin) plugins. |
| Step | Description |
|-------------------|---------------------------------------------------------------------------------------------------------------------------------|
| Verify Conditions | Verify all the conditions to proceed with the release. |
| Get last release | Obtain the commit corresponding to the last release by analyzing [Git tags](https://git-scm.com/book/en/v2/Git-Basics-Tagging). |
| Analyze commits | Determine the type of release based on the commits added since the last release. |
| Verify release | Verify the release conformity. |
| Generate notes | Generate release notes for the commits added since the last release. |
| Create Git tag | Create a Git tag corresponding to the new release version. |
| Prepare | Prepare the release. |
| Publish | Publish the release. |
| Notify | Notify of new releases or errors. |
## Documentation
- Usage
- [Getting started](docs/usage/getting-started.md#getting-started)
- [Installation](docs/usage/installation.md#installation)
- [CI Configuration](docs/usage/ci-configuration.md#ci-configuration)
- [Configuration](docs/usage/configuration.md#configuration)
@@ -105,13 +111,11 @@ After running the tests the command `semantic-release` will execute the followin
- [CI configurations](docs/recipes/README.md)
- [Package managers and languages](docs/recipes/README.md)
- Developer guide
- [JavaScript API](docs/developer-guide/js-api.md)
- [Plugins](docs/developer-guide/plugin.md)
- [Shareable configuration](docs/developer-guide/shareable-configuration.md)
- Resources
- [Videos](docs/resources.md#videos)
- [Articles](docs/resources.md#articles)
- [Tutorials](docs/resources.md#tutorials)
- Support
- [Resources](docs/support/resources.md)
- [Frequently Asked Questions](docs/support/FAQ.md)
- [Troubleshooting](docs/support/troubleshooting.md)
- [Node version requirement](docs/support/node-version.md)
@@ -120,7 +124,7 @@ After running the tests the command `semantic-release` will execute the followin
## Get help
- [Stack Overflow](https://stackoverflow.com/questions/tagged/semantic-release)
- [Gitter chat](https://gitter.im/semantic-release/semantic-release)
- [Spectrum community](https://spectrum.chat/semantic-release)
- [Twitter](https://twitter.com/SemanticRelease)
## Badge
+4 -9
View File
@@ -1,9 +1,5 @@
# Summary
## About
- [Highlights](README.md#highlights)
- [How does it work?](README.md#how-does-it-work)
## Usage
- [Installation](docs/usage/installation.md#installation)
- [CI Configuration](docs/usage/ci-configuration.md#ci-configuration)
@@ -21,18 +17,17 @@
- [Travis CI](docs/recipes/travis.md)
- [Travis CI with build stages](docs/recipes/travis-build-stages.md)
- [GitLab CI](docs/recipes/gitlab-ci.md)
- [Git hosted services](docs/recipes/README.md)
- [Git authentication with SSH keys](docs/recipes/git-auth-ssh-keys.md)
- [Package managers and languages](docs/recipes/README.md)
## Developer guide
- [JavaScript API](docs/developer-guide/js-api.md)
- [Plugin](docs/developer-guide/plugin.md)
- [Shareable configuration](docs/developer-guide/shareable-configuration.md)
## Resources
- [Videos](docs/resources.md#videos)
- [Articles](docs/resources.md#articles)
- [Tutorials](docs/resources.md#tutorials)
## Support
- [Resources](docs/support/resources.md)
- [Frequently Asked Questions](docs/support/FAQ.md)
- [Troubleshooting](docs/support/troubleshooting.md)
- [Node version requirement](docs/support/node-version.md)
+28 -5
View File
@@ -3,21 +3,44 @@
// Bad news: We have to write plain ES5 in this file
// Good news: It's the only file of the entire project
/* eslint-disable no-var */
/* eslint-disable no-var, promise/prefer-await-to-then, prefer-destructuring */
var semver = require('semver');
var execa = require('execa');
var findVersions = require('find-versions');
var pkg = require('../package.json');
var MIN_GIT_VERSION = '2.0.0';
if (!semver.satisfies(process.version, pkg.engines.node)) {
console.error(
`[semantic-release]: node version ${pkg.engines.node} is required. Found ${process.version}.
See https://github.com/semantic-release/semantic-release/blob/caribou/docs/support/node-version.md for more details and solutions.`
See https://github.com/semantic-release/semantic-release/blob/master/docs/support/node-version.md for more details and solutions.`
);
process.exit(1);
}
execa
.stdout('git', ['--version'])
.then(stdout => {
var gitVersion = findVersions(stdout)[0];
if (semver.lt(gitVersion, MIN_GIT_VERSION)) {
console.error(`[semantic-release]: Git version ${MIN_GIT_VERSION} is required. Found ${gitVersion}.`);
process.exit(1);
}
})
.catch(error => {
console.error(`[semantic-release]: Git version ${MIN_GIT_VERSION} is required. No git binary found.`);
console.error(error);
process.exit(1);
});
// Node 8+ from this point on
require('../cli')().catch(() => {
process.exitCode = 1;
});
require('../cli')()
.then(exitCode => {
process.exitCode = exitCode;
})
.catch(() => {
process.exitCode = 1;
});
-8
View File
@@ -1,8 +0,0 @@
{
"plugins" : ["github", "anchors"],
"pluginsConfig": {
"github": {
"url": "https://github.com/semantic-release/semantic-release"
}
}
}
+18 -22
View File
@@ -1,4 +1,6 @@
const {pickBy, isUndefined} = require('lodash');
const {argv, env, stderr} = require('process');
const util = require('util');
const hideSensitive = require('./lib/hide-sensitive');
const stringList = {
type: 'string',
@@ -13,54 +15,48 @@ module.exports = async () => {
const cli = require('yargs')
.command('$0', 'Run automated package publishing', yargs => {
yargs.demandCommand(0, 0).usage(`Run automated package publishing
Usage:
semantic-release [options] [plugins]`);
})
.option('b', {alias: 'branch', describe: 'Git branch to release from', type: 'string', group: 'Options'})
.option('r', {alias: 'repository-url', describe: 'Git repository URL', type: 'string', group: 'Options'})
.option('t', {alias: 'tag-format', describe: 'Git tag format', type: 'string', group: 'Options'})
.option('p', {alias: 'plugins', describe: 'Plugins', ...stringList, group: 'Options'})
.option('e', {alias: 'extends', describe: 'Shareable configurations', ...stringList, group: 'Options'})
.option('ci', {describe: 'Toggle CI verifications', default: undefined, type: 'boolean', group: 'Options'})
.option('ci', {describe: 'Toggle CI verifications', type: 'boolean', group: 'Options'})
.option('verify-conditions', {...stringList, group: 'Plugins'})
.option('analyze-commits', {type: 'string', group: 'Plugins'})
.option('verify-release', {...stringList, group: 'Plugins'})
.option('generate-notes', {type: 'string', group: 'Plugins'})
.option('generate-notes', {...stringList, group: 'Plugins'})
.option('prepare', {...stringList, group: 'Plugins'})
.option('publish', {...stringList, group: 'Plugins'})
.option('success', {...stringList, group: 'Plugins'})
.option('fail', {...stringList, group: 'Plugins'})
.option('debug', {describe: 'Output debugging information', default: undefined, type: 'boolean', group: 'Options'})
.option('d', {alias: 'dry-run', describe: 'Skip publishing', default: undefined, type: 'boolean', group: 'Options'})
.option('h', {alias: 'help', default: undefined, group: 'Options'})
.option('v', {alias: 'version', default: undefined, group: 'Options'})
.option('debug', {describe: 'Output debugging information', type: 'boolean', group: 'Options'})
.option('d', {alias: 'dry-run', describe: 'Skip publishing', type: 'boolean', group: 'Options'})
.option('h', {alias: 'help', group: 'Options'})
.option('v', {alias: 'version', group: 'Options'})
.strict(false)
.exitProcess(false);
try {
// Remove option with undefined values, as yargs sets non defined options as `undefined`
const {help, version, ...opts} = pickBy(cli.argv, value => !isUndefined(value));
const {help, version, ...opts} = cli.parse(argv.slice(2));
if (Boolean(help) || Boolean(version)) {
process.exitCode = 0;
return;
}
// Set the `noCi` options as yargs sets the `ci` options instead (because arg starts with `--no`)
if (opts.ci === false) {
opts.noCi = true;
return 0;
}
if (opts.debug) {
// Debug must be enabled before other requires in order to work
require('debug').enable('semantic-release:*');
}
await require('.')(opts);
process.exitCode = 0;
} catch (err) {
if (err.name !== 'YError') {
console.error(err);
return 0;
} catch (error) {
if (error.name !== 'YError') {
stderr.write(hideSensitive(env)(util.inspect(error, {colors: true})));
}
process.exitCode = 1;
return 1;
}
};
-2
View File
@@ -1,2 +0,0 @@
codecov:
branch: caribou
-1
View File
@@ -4,5 +4,4 @@
- [Extending](extending/README.md)- Extending **semantic-release** with plugins and shareable configurations
- [Recipes](recipes/README.md) - Community written recipes for common **semantic-release** use-cases
- [Developer Guide](developer-guide/README.md) - The essentials of writing a **semantic-release** plugin or shareable configurations
- [Resources](resources.md) - Videos, articles and tutorials
- [Support](support/README.md) - FAQ and troubleshooting
+1
View File
@@ -1,4 +1,5 @@
# Developer guide
- [JavaScript API](js-api.md)
- [Plugins](plugin.md)
- [Shareable configuration](shareable-configuration.md)
+270
View File
@@ -0,0 +1,270 @@
# JavaScript API
## Usage
```js
const semanticRelease = require('semantic-release');
const {WritableStreamBuffer} = require('stream-buffers');
const stdoutBuffer = WritableStreamBuffer();
const stderrBuffer = WritableStreamBuffer();
try {
const result = await semanticRelease({
// Core options
branch: 'master',
repositoryUrl: 'https://github.com/me/my-package.git',
// Shareable config
extends: 'my-shareable-config',
// Plugin options
githubUrl: 'https://my-ghe.com',
githubApiPathPrefix: '/api-prefix'
}, {
// Run semantic-release from `/path/to/git/repo/root` without having to change local process `cwd` with `process.chdir()`
cwd: '/path/to/git/repo/root',
// Pass the variable `MY_ENV_VAR` to semantic-release without having to modify the local `process.env`
env: {...process.env, MY_ENV_VAR: 'MY_ENV_VAR_VALUE'},
// Store stdout and stderr to use later instead of writing to `process.stdout` and `process.stderr`
stdout: stdoutBuffer,
stderr: stderrBuffer
});
if (result) {
const {lastRelease, commits, nextRelease, releases} = result;
console.log(`Published ${nextRelease.type} release version ${nextRelease.version} containing ${commits.length} commits.`);
if (lastRelease.version) {
console.log(`The last release was "${lastRelease.version}".`);
}
for (const release of releases) {
console.log(`The release was published with plugin "${pluginName}".`);
}
} else {
console.log('No release published.');
}
// Get stdout and stderr content
const logs = stdoutBuffer.getContentsAsString('utf8');
const errors = stderrBuffer.getContentsAsString('utf8');
} catch (err) {
console.error('The automated release failed with %O', err)
}
```
## API
### semanticRelease([options], [config]) => Promise<Result>
Run **semantic-release** and returns a `Promise` that resolves to a [Result](#result) object.
#### options
Type: `Object`
**semantic-release** options.
Can be used to set any [core option](../usage/configuration.md#configuration) or [plugin options](../usage/plugins.md#configuration).
Each option, will take precedence over options configured in the [configuration file](../usage/configuration.md#configuration) and [shareable configurations](../usage/configuration.md#extends).
#### config
Type: `Object`
**semantic-release** configuration specific for API usage.
##### cwd
Type: `String`<br>
Default: `process.cwd()`
The current working directory to use. It should be configured to the root of the Git repository to release from.
It allows to run **semantic-release** from a specific path without having to change the local process `cwd` with `process.chdir()`.
##### env
Type: `Object`<br>
Default: `process.env`
The environment variables to use.
It allows to run **semantic-release** with specific environment variables without having to modify the local `process.env`.
##### stdout
Type: [`stream.Writable`](https://nodejs.org/api/stream.html#stream_writable_streams)<br>
Default: `process.stdout`
The [writable stream](https://nodejs.org/api/stream.html#stream_writable_streams) used to log information.
It allows to configure **semantic-release** to write logs to a specific stream rather than the local `process.stdout`.
##### stderr
Type: [`stream.Writable`](https://nodejs.org/api/stream.html#stream_writable_streams)<br>
Default: `process.stderr`
The [writable stream](https://nodejs.org/api/stream.html#stream_writable_streams) used to log errors.
It allows to configure **semantic-release** to write errors to a specific stream rather than the local `process.stderr`.
### Result
Type: `Object` `Boolean`<br>
And object with [`lastRelease`](#lastrelease), [`nextRelease`](#nextrelease), [`commits`](#commits) and [`releases`](#releases) if a release is published or `false` if no release was published.
#### lastRelease
Type: `Object`
Information related to the last release found:
| Name | Type | Description |
|---------|----------|----------------------------------------------------------------------------------------------------|
| version | `String` | The version of the last release. |
| gitHead | `String` | The sha of the last commit being part of the last release. |
| gitTag | `String` | The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) associated with the last release. |
**Notes**: If no previous release is found, `lastRelease` will be an empty `Object`.
Example:
```js
{
gitHead: 'da39a3ee5e6b4b0d3255bfef95601890afd80709',
version: '1.0.0',
gitTag: 'v1.0.0',
}
```
#### commits
Type: `Array<Object>`
The list of commit included in the new release.<br>
Each commit object has the following properties:
| Name | Type | Description |
|-----------------|----------|-------------------------------------------------|
| commit | `Object` | The commit abbreviated and full hash. |
| commit.long | `String` | The commit hash. |
| commit.short | `String` | The commit abbreviated hash. |
| tree | `Object` | The commit abbreviated and full tree hash. |
| tree.long | `String` | The commit tree hash. |
| tree.short | `String` | The commit abbreviated tree hash. |
| author | `Object` | The commit author information. |
| author.name | `String` | The commit author name. |
| author.email | `String` | The commit author email. |
| author.short | `String` | The commit author date. |
| committer | `Object` | The committer information. |
| committer.name | `String` | The committer name. |
| committer.email | `String` | The committer email. |
| committer.short | `String` | The committer date. |
| subject | `String` | The commit subject. |
| body | `String` | The commit body. |
| message | `String` | The commit full message (`subject` and `body`). |
| hash | `String` | The commit hash. |
| committerDate | `String` | The committer date. |
Example:
```js
[
{
commit: {
long: '68eb2c4d778050b0701136ca129f837d7ed494d2',
short: '68eb2c4'
},
tree: {
long: '7ab515d12bd2cf431745511ac4ee13fed15ab578',
short: '7ab515d'
},
author: {
name: 'Me',
email: 'me@email.com',
date: 2018-07-22T20:52:44.000Z
},
committer: {
name: 'Me',
email: 'me@email.com',
date: 2018-07-22T20:52:44.000Z
},
subject: 'feat: a new feature',
body: 'Description of the new feature',
hash: '68eb2c4d778050b0701136ca129f837d7ed494d2',
message: 'feat: a new feature\n\nDescription of the new feature',
committerDate: 2018-07-22T20:52:44.000Z
}
]
```
#### nextRelease
Type: `Object`
Information related to the newly published release:
| Name | Type | Description |
|---------|----------|---------------------------------------------------------------------------------------------------|
| type | `String` | The [semver](https://semver.org) type of the release (`patch`, `minor` or `major`). |
| version | `String` | The version of the new release. |
| gitHead | `String` | The sha of the last commit being part of the new release. |
| gitTag | `String` | The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) associated with the new release. |
| notes | `String` | The release notes for the new release. |
Example:
```js
{
type: 'minor',
gitHead: '68eb2c4d778050b0701136ca129f837d7ed494d2',
version: '1.1.0',
gitTag: 'v1.1.0',
notes: 'Release notes for version 1.1.0...',
}
```
#### releases
Type: `Array<Object>`
The list of releases published, one release per [publish plugin](../usage/plugins.md#publish-plugin).<br>
Each release object has the following properties:
| Name | Type | Description |
|------------|----------|-----------------------------------------------------------------------------------------------|
| name | `String` | **Optional.** The release name, only if set by the corresponding `publish` plugin. |
| url | `String` | **Optional.** The release URL, only if set by the corresponding `publish` plugin. |
| type | `String` | The [semver](https://semver.org) type of the release (`patch`, `minor` or `major`). |
| version | `String` | The version of the release. |
| gitHead | `String` | The sha of the last commit being part of the release. |
| gitTag | `String` | The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) associated with the release. |
| notes | `String` | The release notes for the release. |
| pluginName | `String` | The name of the plugin that published the release. |
Example:
```js
[
{
name: 'GitHub release',
url: 'https://github.com/me/my-package/releases/tag/v1.1.0',
type: 'minor',
gitHead: '68eb2c4d778050b0701136ca129f837d7ed494d2',
version: '1.1.0',
gitTag: 'v1.1.0',
notes: 'Release notes for version 1.1.0...',
pluginName: '@semantic-release/github'
},
{
name: 'npm package (@latest dist-tag)',
url: 'https://www.npmjs.com/package/my-package',
type: 'minor',
gitHead: '68eb2c4d778050b0701136ca129f837d7ed494d2',
version: '1.1.0',
gitTag: 'v1.1.0',
notes: 'Release notes for version 1.1.0...',
pluginName: '@semantic-release/npm'
}
]
```
+62 -37
View File
@@ -1,48 +1,73 @@
# Plugins list
## Default plugins
- [@semantic-release/github](https://github.com/semantic-release/github)
- [verifyConditions](https://github.com/semantic-release/github#verifyconditions): Verify the presence and the validity of the GitHub authentication and release configuration
- [publish](https://github.com/semantic-release/github#publish): Publish a [GitHub release](https://help.github.com/articles/about-releases)
- [success](https://github.com/semantic-release/github#success): Add a comment to GitHub issues and pull requests resolved in the release
- [fail](https://github.com/semantic-release/github#fail): Open a GitHub issue when a release fails
- [@semantic-release/npm](https://github.com/semantic-release/npm)
- [verifyConditions](https://github.com/semantic-release/npm#verifyconditions): Verify the presence and the validity of the npm authentication and release configuration
- [prepare](https://github.com/semantic-release/npm#prepare): Update the package.json version and create the npm package tarball
- [publish](https://github.com/semantic-release/npm#publish): Publish the package on the npm registry
## Official plugins
- [@semantic-release/commit-analyzer](https://github.com/semantic-release/commit-analyzer)
- `analyzeCommits`: Determine the type of release by analyzing commits with [conventional-changelog](https://github.com/conventional-changelog/conventional-changelog)
- [@semantic-release/release-notes-generator](https://github.com/semantic-release/release-notes-generator)
- `generateNotes`: Generate release notes for the commits added since the last release with [conventional-changelog](https://github.com/conventional-changelog/conventional-changelog)
- [@semantic-release/github](https://github.com/semantic-release/github)
- `verifyConditions`: Verify the presence and the validity of the GitHub authentication and release configuration
- `publish`: Publish a [GitHub release](https://help.github.com/articles/about-releases)
- `success`: Add a comment to GitHub issues and pull requests resolved in the release
- `fail`: Open a GitHub issue when a release fails
- [@semantic-release/npm](https://github.com/semantic-release/npm)
- `verifyConditions`: Verify the presence and the validity of the npm authentication and release configuration
- `prepare`: Update the package.json version and create the npm package tarball
- `publish`: Publish the package on the npm registry
- [@semantic-release/gitlab](https://github.com/semantic-release/gitlab)
- [verifyConditions](https://github.com/semantic-release/gitlab#verifyconditions): Verify the presence and the validity of the GitLab authentication and release configuration
- [publish](https://github.com/semantic-release/gitlab#publish): Publish a [GitLab release](https://docs.gitlab.com/ce/workflow/releases.html)
- `verifyConditions`: Verify the presence and the validity of the GitLab authentication and release configuration
- `publish`: Publish a [GitLab release](https://docs.gitlab.com/ce/workflow/releases.html)
- [@semantic-release/git](https://github.com/semantic-release/git)
- [verifyConditions](https://github.com/semantic-release/git#verifyconditions): Verify the presence and the validity of the Git authentication and release configuration
- [prepare](https://github.com/semantic-release/git#prepare): Push a release commit and tag, including configurable files
- `verifyConditions`: Verify the presence and the validity of the Git authentication and release configuration
- `prepare`: Push a release commit and tag, including configurable files
- [@semantic-release/changelog](https://github.com/semantic-release/changelog)
- [verifyConditions](https://github.com/semantic-release/changelog#verifyconditions): Verify the presence and the validity of the configuration
- [prepare](https://github.com/semantic-release/changelog#prepare): Create or update the changelog file in the local project repository
- `verifyConditions`: Verify the presence and the validity of the configuration
- `prepare`: Create or update the changelog file in the local project repository
- [@semantic-release/exec](https://github.com/semantic-release/exec)
- [verifyConditions](https://github.com/semantic-release/exec#verifyconditions): Execute a shell command to verify if the release should happen
- [analyzeCommits](https://github.com/semantic-release/exec#analyzecommits): Execute a shell command to determine the type of release
- [verifyRelease](https://github.com/semantic-release/exec#verifyrelease): Execute a shell command to verifying a release that was determined before and is about to be published.
- [generateNotes](https://github.com/semantic-release/exec#analyzecommits): Execute a shell command to generate the release note
- [prepare](https://github.com/semantic-release/exec#prepare): Execute a shell command to prepare the release
- [publish](https://github.com/semantic-release/exec#publish): Execute a shell command to publish the release
- [success](https://github.com/semantic-release/exec#success): Execute a shell command to notify of a new release
- [fail](https://github.com/semantic-release/exec#fail): Execute a shell command to notify of a failed release
- `verifyConditions`: Execute a shell command to verify if the release should happen
- `analyzeCommits`: Execute a shell command to determine the type of release
- `verifyRelease`: Execute a shell command to verifying a release that was determined before and is about to be published
- `generateNotes`: Execute a shell command to generate the release note
- `prepare`: Execute a shell command to prepare the release
- `publish`: Execute a shell command to publish the release
- `success`: Execute a shell command to notify of a new release
- `fail`: Execute a shell command to notify of a failed release
- [@semantic-release/apm](https://github.com/semantic-release/apm)
- `verifyConditions`: Verify the presence of the `ATOM_ACCESS_TOKEN` environment variable and the [`apm`](https://github.com/atom/apm) CLI
- `prepare`: Update the `package.json` version with [`npm version`](https://docs.npmjs.com/cli/version)
- `publish`: Publish the [Atom package](https://flight-manual.atom.io/hacking-atom/sections/publishing)
## Community plugins
[Open a Pull Request](https://github.com/semantic-release/semantic-release/blob/caribou/CONTRIBUTING.md#submitting-a-pull-request) to add your plugin to the list.
[Open a Pull Request](https://github.com/semantic-release/semantic-release/blob/master/CONTRIBUTING.md#submitting-a-pull-request) to add your plugin to the list.
- [semantic-release-docker](https://github.com/felixfbecker/semantic-release-docker) Set of semantic-release plugins for publishing a docker image to Docker Hub (tested on semantic-release **^11.0.0**)
- [verifyConditions](https://github.com/felixfbecker/semantic-release-docker#verifyconditions) Verify that all needed configuration is present and login to the Docker registry.
- [publish](https://github.com/felixfbecker/semantic-release-docker#publish) Tag the image specified by `name` with the new version, push it to Docker Hub and update the latest tag.
- [semantic-release-verify-deps](https://github.com/piercus/semantic-release-verify-deps)
- [verifyConditions](https://github.com/piercus/semantic-release-verify-deps) Check the dependencies format against a regexp before a release
- [semantic-release-chrome](https://github.com/GabrielDuarteM/semantic-release-chrome) Set of semantic-release plugins for publishing a Chrome extension release.
- [verifyConditions](https://github.com/GabrielDuarteM/semantic-release-chrome#verifyconditions) Verify the presence of the authentication (set via environment variables).
- [prepare](https://github.com/GabrielDuarteM/semantic-release-chrome#prepare) Write the correct version to the manifest.json and creates a zip file of the whole dist folder.
- [publish](https://github.com/GabrielDuarteM/semantic-release-chrome#publish) Uploads the generated zip file to the webstore, and publish the item.
- [semantic-release-docker](https://github.com/felixfbecker/semantic-release-docker)
- `verifyConditions`: Verify that all needed configuration is present and login to the Docker registry.
- `publish`: Tag the image specified by `name` with the new version, push it to Docker Hub and update the latest tag
- [semantic-release-gcr](https://github.com/carlos-cubas/semantic-release-gcr)
- `verifyConditions`: Verify that all needed configuration is present and login to the Docker registry
- `publish`: Tag the image specified by `name` with the new version, push it to Docker Hub and update the latest tag
- [semantic-release-vsce](https://github.com/raix/semantic-release-vsce)
- `verifyConditions`: Verify the presence and the validity of the vsce authentication and release configuration
- `prepare`: Create a `.vsix` for distribution
- `publish`: Publish the package to the Visual Studio Code marketplace
- [semantic-release-verify-deps](https://github.com/piercus/semantic-release-verify-deps)
- `verifyConditions`: Check the dependencies format against a regexp before a release
- [semantic-release-chrome](https://github.com/GabrielDuarteM/semantic-release-chrome)
- `verifyConditions`: Verify the presence of the authentication (set via environment variables)
- `prepare`: Write the correct version to the `manifest.json` and creates a zip file of the whole dist folder
- `publish`: Uploads the generated zip file to the webstore, and publish the item
- [semantic-release-firefox](https://github.com/felixfbecker/semantic-release-firefox)
- `verifyConditions`: Verify the presence of the authentication (set via environment variables)
- `prepare`: Write the correct version to the `manifest.json`, creates a `xpi` file of the dist folder and a zip of the sources
- `publish`: Submit the generated archives to the webstore for review, and publish the item including release notes
- [semantic-release-gerrit](https://github.com/pascalMN/semantic-release-gerrit)
- `generateNotes`: Generate release notes with Gerrit reviews URL
- [semantic-release-expo](https://github.com/bycedric/semantic-release-expo)
- `verifyConditions`: Verify Expo manifest(s) are readable and valid.
- `prepare`: Update version, ios build number and android version code in the Expo manifest(s).
- [maven-semantic-release](https://github.com/conveyal/maven-semantic-release)
- `verifyConditions`: Verifies that the `pom.xml` file and other files exist and are setup to allow releases
- `verifyRelease`: Checks and warns (does not error by default) if the version numbers found on maven central and within the Git project differ by quite a bit
- `prepare`: Changes the version number in the `pom.xml` (or all `pom.xml` files in maven projects with multiple `pom.xml` files) and optionally creates a commit with this version number and pushes it to `master`
- `publish`: Runs `mvn deploy` to deploy to maven central and optionally will update to next snapshot version and merge changes to development branch
-1
View File
@@ -3,7 +3,6 @@
## CI configurations
- [CircleCI 2.0 workflows](circleci-workflows.md)
- [Travis CI](travis.md)
- [Travis CI with build stages](travis-build-stages.md)
- [GitLab CI](gitlab-ci.md)
## Git hosted services
+2 -2
View File
@@ -4,7 +4,7 @@
The [Authentication](../usage/ci-configuration.md#authentication) environment variables can be configured in [CircleCi Project Settings](https://circleci.com/docs/2.0/env-vars/#adding-environment-variables-in-the-app)..
Alternatively, the default `NPM_TOKEN` and `GH_TOKEN` can be easily [setup with semantic-release-cli](../usage/ci-configuration.md#automatic-setup-with-semantic-release-cli).
Alternatively, the default `NPM_TOKEN` and `GH_TOKEN` can be easily [setup with semantic-release-cli](../usage/getting-started.md#getting-started).
## Multiple Node jobs configuration
@@ -59,7 +59,7 @@ A `package.json` is required only for [local](../usage/installation.md#local-ins
```json
{
"devDependencies": {
"semantic-release": "^12.0.0"
"semantic-release": "^15.0.0"
}
}
```
+1 -1
View File
@@ -51,7 +51,7 @@ A `package.json` is required only for [local](../usage/installation.md#local-ins
```json
{
"devDependencies": {
"semantic-release": "^12.0.0"
"semantic-release": "^15.0.0"
}
}
```
-95
View File
@@ -1,95 +0,0 @@
# Using semantic-release with [Travis CI build stages](https://docs.travis-ci.com/user/build-stages)
## Environment variables
The [Authentication](../usage/ci-configuration.md#authentication) environment variables can be configured in [Travis Repository Settings](https://docs.travis-ci.com/user/environment-variables/#defining-variables-in-repository-Settings) or with the [travis env set CLI](https://github.com/travis-ci/travis.rb#env).
Alternatively, the default `NPM_TOKEN` and `GH_TOKEN` can be easily [setup with semantic-release-cli](../usage/ci-configuration.md#automatic-setup-with-semantic-release-cli).
## Multiple Node jobs configuration
### `.travis.yml` configuration for multiple Node jobs
This example is a minimal configuration for **semantic-release** with a build running Node 6 and 8. See [Travis - Customizing the Build](https://docs.travis-ci.com/user/customizing-the-build) for additional configuration options.
This example creates a `release` [build stage](https://docs.travis-ci.com/user/build-stages) that [runs `semantic-release` only after all test jobs are successful](../usage/ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded).
It's recommended to run the `semantic-release` command in the [Travis `deploy` step](https://docs.travis-ci.com/user/customizing-the-build/#The-Build-Lifecycle) so if an error occurs the build will fail and Travis will send a notification.
**Note**: It's not recommended to run the `semantic-release` command in the Travis `script` step as each script in this step will be executed regardless of the outcome of the previous one. See [travis-ci/travis-ci#1066](https://github.com/travis-ci/travis-ci/issues/1066).
**Advanced configuration**: Running the tests in the `script` step of the `release` stage is not necessary as the previous stage(s) already ran them. To increase speed, the `script` step of the `release` stage can be overwritten to skip the tests. Note that other commands such as build or compilation might still be required.
```yaml
language: node_js
node_js:
- 8
- 6
jobs:
include:
# Define the release stage that runs semantic-release
- stage: release
node_js: lts/*
# Advanced: optionally overwrite your default `script` step to skip the tests
# script: skip
deploy:
provider: script
skip_cleanup: true
script:
- npx semantic-release
```
### `package.json` configuration for multiple Node jobs
A `package.json` is required only for [local](../usage/installation.md#local-installation) **semantic-release** installation.
```json
{
"devDependencies": {
"semantic-release": "^12.0.0"
}
}
```
## Non-JavaScript projects configuration
For projects that require to be tested with one or multiple version of a Non-JavaScript [language](https://docs.travis-ci.com/user/languages), optionally on multiple [Operating Systems](https://docs.travis-ci.com/user/multi-os).
This recipe cover the Travis specifics only. See [Non JavaScript projects recipe](../support/FAQ.md#can-i-use-semantic-release-to-publish-non-javascript-packages) for more information on the **semantic-release** configuration.
### `.travis.yml` configuration for non-JavaScript projects
This example is a minimal configuration for **semantic-release** with a build running [Go 1.6 and 1.7](https://docs.travis-ci.com/user/languages/go). See [Travis - Customizing the Build](https://docs.travis-ci.com/user/customizing-the-build) for additional configuration options.
This example creates a `release` [build stage](https://docs.travis-ci.com/user/build-stages) that [runs `semantic-release` only after all test jobs are successful](../usage/ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded).
It's recommended to run the `semantic-release` command in the [Travis `deploy` step](https://docs.travis-ci.com/user/customizing-the-build/#The-Build-Lifecycle) so if an error occurs the build will fail and Travis will send a notification.
**Note**: It's not recommended to run the `semantic-release` command in the Travis `script` step as each script in this step will be executed regardless of the outcome of the previous one. See [travis-ci/travis-ci#1066](https://github.com/travis-ci/travis-ci/issues/1066).
**Advanced configuration**: Running the tests in the `script` step of the `release` stage is not necessary as the previous stage(s) already ran them. To increase speed, the `script` step of the `release` stage can be overwritten to skip the tests. Note that other commands such as build or compilation might still be required.
```yaml
language: go
go:
- 1.6
- 1.7
jobs:
include:
# Define the release stage that runs semantic-release
- stage: release
# Advanced: optionally overwrite your default `script` step to skip the tests
# script:
# - make
deploy:
provider: script
skip_cleanup: true
script:
# Use nvm to install and use the Node LTS version (nvm is installed on all Travis images)
- nvm install lts/*
- npx semantic-release
```
+38 -72
View File
@@ -4,62 +4,22 @@
The [Authentication](../usage/ci-configuration.md#authentication) environment variables can be configured in [Travis Repository Settings](https://docs.travis-ci.com/user/environment-variables/#defining-variables-in-repository-Settings) or with the [travis env set CLI](https://github.com/travis-ci/travis.rb#env).
Alternatively, the default `NPM_TOKEN` and `GH_TOKEN` can be easily [setup with semantic-release-cli](../usage/ci-configuration.md#automatic-setup-with-semantic-release-cli).
Alternatively, the default `NPM_TOKEN` and `GH_TOKEN` can be easily [setup with semantic-release-cli](../usage/getting-started.md#getting-started).
## Single Node job configuration
## Node.js projects configuration
For projects that require to be tested only with a single [Node version](https://docs.travis-ci.com/user/getting-started/#Selecting-a-different-programming-language) on [one Operating System](https://docs.travis-ci.com/user/getting-started/#Selecting-infrastructure-(optional)).
**Note**: [Node 8 is the minimal version required](../support/FAQ.md#why-does-semantic-release-require-node-version--83).
### `.travis.yml` configuration for single Node job
This example is a minimal configuration for semantic-release with a build running Node 8 on Linux. See [Travis - Customizing the Build](https://docs.travis-ci.com/user/customizing-the-build) for additional configuration options.
It's recommended to run the `semantic-release` command in the [Travis `deploy` step](https://docs.travis-ci.com/user/customizing-the-build/#The-Build-Lifecycle) so if an error occurs the build will fail and Travis will send a notification.
**Note**: It's not recommended to run the `semantic-release` command in the Travis `script` step as each script in this step will be executed regardless of the outcome of the previous one. See [travis-ci/travis-ci#1066](https://github.com/travis-ci/travis-ci/issues/1066).
```yaml
language: node_js
node_js: 8
deploy:
provider: script
skip_cleanup: true
script:
- npx semantic-release
```
### `package.json` configuration for single Node job
A `package.json` is required only for [local](../usage/installation.md#local-installation) **semantic-release** installation.
```json
{
"devDependencies": {
"semantic-release": "^11.0.0"
}
}
```
## Multiple Node jobs configuration
For projects that require to be tested with multiple [Node versions](https://docs.travis-ci.com/user/languages/javascript-with-nodejs/#Specifying-Node.js-versions) and/or on multiple [Operating Systems](https://docs.travis-ci.com/user/multi-os).
**Note**: At least one job must run a [Node >= 8 version](../support/FAQ.md#why-does-semantic-release-require-node-version--83).
### `.travis.yml` configuration for multiple Node jobs
### `.travis.yml` configuration for multiple Node.js jobs
This example is a minimal configuration for **semantic-release** with a build running Node 6 and 8. See [Travis - Customizing the Build](https://docs.travis-ci.com/user/customizing-the-build) for additional configuration options.
This example uses [`travis-deploy-once`](https://github.com/semantic-release/travis-deploy-once) in order to [Run `semantic-release` only after all tests succeeded](../usage/ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded). Alternatively you can use [Travis CI Build Stages recipe](travis-build-stages.md).
This example creates a `release` [build stage](https://docs.travis-ci.com/user/build-stages) that [runs `semantic-release` only after all test jobs are successful](../usage/ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded).
It's recommended to run the `semantic-release` command in the [Travis `deploy` step](https://docs.travis-ci.com/user/customizing-the-build/#The-Build-Lifecycle) so if an error occurs the build will fail and Travis will send a notification.
**Note**: It's not recommended to run the `semantic-release` command in the Travis `script` step as each script in this step will be executed regardless of the outcome of the previous one. See [travis-ci/travis-ci#1066](https://github.com/travis-ci/travis-ci/issues/1066).
**Advanced configuration**: Running the tests in the `script` step of the `release` stage is not necessary as the previous stage(s) already ran them. To increase speed, the `script` step of the `release` stage can be overwritten to skip the tests. Note that other commands such as build or compilation might still be required.
```yaml
language: node_js
@@ -67,15 +27,20 @@ node_js:
- 8
- 6
deploy:
provider: script
skip_cleanup: true
script:
- npx travis-deploy-once "npx semantic-release"
jobs:
include:
# Define the release stage that runs semantic-release
- stage: release
node_js: lts/*
# Advanced: optionally overwrite your default `script` step to skip the tests
# script: skip
deploy:
provider: script
skip_cleanup: true
script:
- npx semantic-release
```
**Note**: See the `travis-deploy-once` [`pro`](https://github.com/semantic-release/travis-deploy-once#-p---pro) and [`travis-url`](https://github.com/semantic-release/travis-deploy-once#-u---travis-url) options for using with [Travis Pro](https://docs.travis-ci.com/user/travis-pro) and [Travis Enterprise](https://enterprise.travis-ci.com).
### `package.json` configuration for multiple Node jobs
A `package.json` is required only for [local](../usage/installation.md#local-installation) **semantic-release** installation.
@@ -83,13 +48,12 @@ A `package.json` is required only for [local](../usage/installation.md#local-ins
```json
{
"devDependencies": {
"semantic-release": "^12.0.0",
"travis-deploy-once": "^4.0.0"
"semantic-release": "^15.0.0"
}
}
```
## Non-JavaScript projects configuration
## Non-Node.js projects configuration
For projects that require to be tested with one or multiple version of a Non-JavaScript [language](https://docs.travis-ci.com/user/languages), optionally on multiple [Operating Systems](https://docs.travis-ci.com/user/multi-os).
@@ -97,14 +61,16 @@ This recipe cover the Travis specifics only. See [Non JavaScript projects recipe
### `.travis.yml` configuration for non-JavaScript projects
This example is a minimal configuration for **semantic-release** with a build running [Go 1.6 and 1.7](https://docs.travis-ci.com/user/languages/go) on Linux and OSX. See [Travis - Customizing the Build](https://docs.travis-ci.com/user/customizing-the-build) for additional configuration options.
This example is a minimal configuration for **semantic-release** with a build running [Go 1.6 and 1.7](https://docs.travis-ci.com/user/languages/go). See [Travis - Customizing the Build](https://docs.travis-ci.com/user/customizing-the-build) for additional configuration options.
This example uses [`travis-deploy-once`](https://github.com/semantic-release/travis-deploy-once) in order to [run `semantic-release` only after all tests succeeded](../usage/ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded). Alternatively you can use [Travis CI Build Stages recipe](travis-build-stages.md).
This example creates a `release` [build stage](https://docs.travis-ci.com/user/build-stages) that [runs `semantic-release` only after all test jobs are successful](../usage/ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded).
It's recommended to run the `semantic-release` command in the [Travis `deploy` step](https://docs.travis-ci.com/user/customizing-the-build/#The-Build-Lifecycle) so if an error occurs the build will fail and Travis will send a notification.
**Note**: It's not recommended to run the `semantic-release` command in the Travis `script` step as each script in this step will be executed regardless of the outcome of the previous one. See [travis-ci/travis-ci#1066](https://github.com/travis-ci/travis-ci/issues/1066).
**Advanced configuration**: Running the tests in the `script` step of the `release` stage is not necessary as the previous stage(s) already ran them. To increase speed, the `script` step of the `release` stage can be overwritten to skip the tests. Note that other commands such as build or compilation might still be required.
```yaml
language: go
@@ -112,18 +78,18 @@ go:
- 1.6
- 1.7
os:
- linux
- osx
deploy:
provider: script
skip_cleanup: true
script:
# Use nvm to install and use the Node LTS version (nvm is installed on all Travis images)
- nvm install lts/*
# Run semantic-release only on one job, after all other are successful
- npx travis-deploy-once "npx semantic-release"
jobs:
include:
# Define the release stage that runs semantic-release
- stage: release
# Advanced: optionally overwrite your default `script` step to skip the tests
# script:
# - make
deploy:
provider: script
skip_cleanup: true
script:
# Use nvm to install and use the Node LTS version (nvm is installed on all Travis images)
- nvm install lts/*
- npx semantic-release
```
**Note**: See the `travis-deploy-once` [`pro`](https://github.com/semantic-release/travis-deploy-once#-p---pro) and [`travis-url`](https://github.com/semantic-release/travis-deploy-once#-u---travis-url) options for using with [Travis Pro](https://docs.travis-ci.com/user/travis-pro) and [Travis Enterprise](https://enterprise.travis-ci.com).
-20
View File
@@ -1,20 +0,0 @@
# Resources
## Videos
- ["Introducing Reliable Dependency and Release Management for npm Packages" - Gregor Martynus](https://www.youtube.com/watch?v=R2RJWLcfzwc)
- ["Kill all humans" - Jan Lehnardt](https://www.youtube.com/watch?v=ZXyx_1kN1L8&t=2s)
- [Publishing JavaScript Packages" - JavaScript Air](https://javascriptair.com/episodes/2016-07-20)
- ["Managing Dependencies like a boss 😎" - JavaScript Air](https://javascriptair.com/episodes/2016-08-17)
- ["Dependency Hell Just Froze Over" - Stephan Bönnemann](https://www.youtube.com/watch?v=PA139CERNbc)
- [“semantic-release Q&A with Kent C. Dodds”](https://www.youtube.com/watch?v=g6y3DnhkjrI)
- [“We fail to follow SemVer and why it neednt matter” - Stephan Bönnemann](https://www.youtube.com/watch?v=tc2UgG5L7WM)
## Articles
- [“Introduction to SemVer” - Irina Gebauer](https://blog.greenkeeper.io/introduction-to-semver-d272990c44f2)
- ["Introduction to Semantic Release" - liv](https://blog.greenkeeper.io/introduction-to-semantic-release-33f73b117c8)
## Tutorials
- [“How to Write a JavaScript Library - Automating Releases with semantic-release” egghead.io](https://egghead.io/lessons/javascript-automating-releases-with-semantic-release)
+65 -21
View File
@@ -12,7 +12,25 @@ However, the [`@semantic-release/git`](https://github.com/semantic-release/git)
The `package.json`s version will be updated by the `semantic-release` command just before publishing to [npm](https://www.npmjs.com), therefore it won't be available for scripts ran before the `semantic-release` command.
As semantic-release uses the [npm CLI](https://docs.npmjs.com/cli/npm) to publish, all [npm hook scripts](https://docs.npmjs.com/misc/scripts#hook-scripts) will be executed. Therefore you can run your build script in the [`prepublishOnly`](https://docs.npmjs.com/misc/scripts#prepublish-and-prepare) hook. It will be executed after the `semantic-release` command update the `package.json`s version and before publishing.
As the [`@semantic-release/npm`](https://github.com/semantic-release/npm) plugin uses the [npm CLI](https://docs.npmjs.com/cli/npm) to update the `package.json` version and publish the package, all [npm hook scripts](https://docs.npmjs.com/misc/scripts#description) will be executed.
You can run your build script in:
- the `prepublishOnly` or `prepack` hook so it will be executed during the `publish` step of `@semantic-release/npm`
- the `postversion` hook so it will be executed during the `prepare` step of `@semantic-release/npm`, which allow for example to update files before committing them with the [`@semantic-release/git`](https://github.com/semantic-release/git) plugin
If using npm hook scripts is not possible, and alternative solution is to [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugin to run your script in the `prepare` step:
```json
{
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/npm",
["@semantic-release/exec", {
"prepareCmd": "./my-build-script.sh ${nextRelease.version}",
}],
]
}
```
## Is there a way to preview which version would currently get published?
@@ -59,28 +77,23 @@ To publish a non-Node package (without a `package.json`) you would need to:
See the [CI configuration recipes](../recipes/README.md#ci-configurations) for more details on specific CI environments.
In addition you will need to configure the **semantic-release** [plugins](../usage/plugins.md#plugins) to:
- Disable the [`@semantic-release/npm`](https://github.com/semantic-release/npm) plugin
- Define plugins for the [verifyConditions](../usage/plugins.md#verifyconditions-plugin), [prepare](../usage/plugins.md#prepare-plugin) and [publish](../usage/plugins.md#publish-plugin) steps to release on your package registry. The [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugin is recommended for situation where a release can be done with a shell command.
In addition you will need to configure the **semantic-release** [plugins](../usage/plugins.md#plugins) to disable the [`@semantic-release/npm`](https://github.com/semantic-release/npm) plugin which is used by default and use a plugin for your project type.
Here is a basic example to create [GitHub releases](https://help.github.com/articles/about-releases) and use shell command to publish:
If there is no specific plugin for your project type you can use the [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugin to publish the release with a shell command.
Here is a basic example to create [GitHub releases](https://help.github.com/articles/about-releases) and use a shell command to publish:
```json
{
"verifyConditions": ["@semantic-release/github"],
"prepare": [
{
"path": "@semantic-release/exec",
"cmd": "set-version ${nextRelease.version}"
}
],
"publish": [
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/github",
{
"path": "@semantic-release/exec",
"cmd": "publish-package"
}
],
["@semantic-release/exec", {
"prepareCmd" : "set-version ${nextRelease.version}",
"publishCmd" : "publish-package"
}]
]
}
```
@@ -98,7 +111,7 @@ See the [CI configuration recipes](../recipes/README.md#ci-configurations) for m
## Can I run semantic-release on my local machine rather than on a CI server?
Yes, you can by explicitly setting the [`--no-ci` CLI option](../usage/configuration.md#options) option. You will also have to set the required [authentication](../usage/ci-configuration.md#authentication) via environment variables on your local machine, for example:
Yes, you can by explicitly setting the [`--no-ci` CLI option](../usage/configuration.md#ci) option. You will also have to set the required [authentication](../usage/ci-configuration.md#authentication) via environment variables on your local machine, for example:
```bash
$ NPM_TOKEN=<your_npm_token> GH_TOKEN=<your_github_token> npx semantic-release --no-ci
@@ -114,7 +127,7 @@ See the [GitLab CI recipes](../recipes/gitlab-ci.md#using-semantic-release-with-
## Can I use semantic-release with any Git hosted environment?
By default **semantic-release** uses the [`@semantic-release/github`](https://github.com/semantic-release/github) plugin to publish a [GitHub release](https://help.github.com/articles/about-releases). For other Git hosted environment the [`@semantic-release/git`](https://github.com/semantic-release/git) and [`@semantic-release/changelog`](https://github.com/semantic-release/changelog) plugins can be used via [plugins configuration](../usage/plugins.md#configuration).
By default **semantic-release** uses the [`@semantic-release/github`](https://github.com/semantic-release/github) plugin to publish a [GitHub release](https://help.github.com/articles/about-releases). For other Git hosted environment the [`@semantic-release/git`](https://github.com/semantic-release/git) and [`@semantic-release/changelog`](https://github.com/semantic-release/changelog) plugins can be used via [plugins configuration](../usage/plugins.md).
See the [`@semantic-release/git`](https://github.com/semantic-release/git#semantic-releasegit) [`@semantic-release/changelog`](https://github.com/semantic-release/changelog#semantic-releasechangelog) plugins documentation for more details.
@@ -124,6 +137,20 @@ Yes, the publishing to the npm registry can be disabled with the [`npmPublish`](
See the [`@semantic-release/npm`](https://github.com/semantic-release/npm#semantic-releasenpm) plugin documentation for more details.
## How can I revert a release?
If you have introduced a breaking bug in a release you have 2 options:
- If you have a fix immediately ready, commit and push it (or merge it via a pull request) to the release branch
- Otherwise [revert the commit](https://git-scm.com/docs/git-revert) that introduced the bug and push the revert commit (or merge it via a pull request) to the release branch
In both cases **semantic-release** will publish a new release, so your package users' will get the fixed/reverted version.
Depending on the package manager you are using, you might be able to un-publish or deprecate a release, in order to prevent users to download it by accident. For example npm allows you to [un-publish](https://docs.npmjs.com/cli/unpublish) in [next 72 hours](https://www.npmjs.com/policies/unpublish) after releasing or to [deprecate](https://docs.npmjs.com/cli/deprecate) a release.
In any case **do not remove the Git tag associated with the buggy version**, otherwise **semantic-release** will later try to republish that version. Publishing a version after un-publishing is not supported by most package managers.
**Note**: If you are using the default [Angular Commit Message Conventions](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines) be aware that it uses a different revert commit format than the standard one created by [git revert](https://git-scm.com/docs/git-revert), contrary to what is [claimed in the convention](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#revert). Therefore, if you revert a commit with [`git revert`](https://git-scm.com/docs/git-revert), use the [`--edit` option](https://git-scm.com/docs/git-revert#git-revert---edit) to format the message according to the [Angular revert commit message format](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#revert). See [conventional-changelog/conventional-changelog#348](https://github.com/conventional-changelog/conventional-changelog/issues/348) for more details.
## Can I use `.npmrc` options?
Yes, all the [npm configuration options](https://docs.npmjs.com/misc/config) are supported via the [`.npmrc`](https://docs.npmjs.com/files/npmrc) file at the root of your repository.
@@ -164,6 +191,21 @@ You can trigger a release by pushing to your Git repository. You deliberately ca
Yes, every commits that contains `[skip release]` or `[release skip]` in their message will be excluded from the commit analysis and won't participate in the release type determination.
## How can I change the type of commits that trigger a release?
By default **semantic-release** uses the [Angular Commit Message Conventions](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines) and triggers releases based on the following rules:
| Commit | Release type |
|-----------------------------|----------------------------|
| Commit with breaking change | ~~Major~~ Breaking release |
| Commit with type `feat` | ~~Minor~~ Feature release |
| Commit with type `fix` | Patch release |
| Commit with type `perf` | Patch release |
See the [`@semantic-release/npm`](https://github.com/semantic-release/npm#npm-configuration) plugin documentation for more details.
This is fully customizable with the [`@semantic-release/commit-analyzer`](https://github.com/semantic-release/commit-analyzer) plugin's [`release-rules` option](https://github.com/semantic-release/commit-analyzer#release-rules).
## Is it *really* a good idea to release on every push?
It is indeed a great idea because it *forces* you to follow best practices. If you dont feel comfortable releasing every feature or fix on your `master` you might not treat your `master` branch as intended.
@@ -174,6 +216,8 @@ From [Understanding the GitHub Flow](https://guides.github.com/introduction/flow
If you need more control over the timing of releases, see [Triggering a release](../../README.md#triggering-a-release) for different options.
**Note**: Only the codebase changes altering the published package will trigger a release (for example new features, bug fixes or performance improvements would trigger a release while refactoring or changing code style would not). See [How can I change the type of commits that trigger a release?](#how-can-i-change-the-type-of-commits-that-trigger-a-release) for more details.
## Can I set the initial release version of my package to `0.0.1`?
This is not supported by **semantic-release** as it's not considered a good practice, mostly because [Semantic Versioning](https://semver.org) rules applies differently to major version zero.
@@ -194,7 +238,7 @@ In addition the [verify conditions step](../../README.md#release-steps) verifies
See [Node version requirement](../support/node-version.md#node-version-requirement) for more details and solutions.
# What is npx?
## What is npx?
[`npx`](https://www.npmjs.com/package/npx) short for "npm exec" is a CLI to find and execute npm binaries within the local `node_modules` folder or in the $PATH. If a binary can't be located npx will download the required package and execute it from its cache location.
The tool is bundled with [npm](https://www.npmjs.com/package/npm) >= 5.2, or can be installed via `npm install -g npx`.
+1
View File
@@ -1,5 +1,6 @@
# Support
- [Resources](resources.md) - Videos, articles and tutorials
- [Frequently Asked Questions](FAQ.md)
- [Troubleshooting](troubleshooting.md)
- [Node version requirement](node-version.md)
+20
View File
@@ -0,0 +1,20 @@
# Resources
## Videos
- ["Introducing Reliable Dependency and Release Management for npm Packages" - Gregor Martynus](https://www.youtube.com/watch?v=R2RJWLcfzwc)
- ["Kill all humans" - Jan Lehnardt](https://www.youtube.com/watch?v=ZXyx_1kN1L8&t=2s)
- ["Publishing JavaScript Packages" - JavaScript Air](https://javascriptair.com/episodes/2016-07-20)
- ["Managing Dependencies like a boss 😎" - JavaScript Air](https://javascriptair.com/episodes/2016-08-17)
- ["Dependency Hell Just Froze Over" - Stephan Bönnemann](https://www.youtube.com/watch?v=PA139CERNbc)
- ["semantic-release Q&A with Kent C. Dodds"](https://www.youtube.com/watch?v=g6y3DnhkjrI)
- ["We fail to follow SemVer and why it neednt matter" - Stephan Bönnemann](https://www.youtube.com/watch?v=tc2UgG5L7WM)
## Articles
- ["Introduction to SemVer" - Irina Gebauer](https://blog.greenkeeper.io/introduction-to-semver-d272990c44f2)
- ["Introduction to Semantic Release" - liv](https://blog.greenkeeper.io/introduction-to-semantic-release-33f73b117c8)
## Tutorials
- ["How to Write a JavaScript Library - Automating Releases with semantic-release" egghead.io](https://egghead.io/lessons/javascript-automating-releases-with-semantic-release)
+45 -35
View File
@@ -1,46 +1,56 @@
# Troubleshooting
## ENOTINHISTORY Commit not in history
This error happens when the commit associated with the last release cannot be found in the branch history.
Multiple situation can cause this issue:
- The package name configured in your `package.json` already exists and **semantic-release** obtains the last release of that package, which is unrelated to yours
- The commit history has been rewritten since the last release (with `git rebase` and `git push -f`)
If the package name configured in your `package.json` already exits, you should change it, and commit the `package.json`. Then **semantic-release** will proceed normally and make the initial release.
If you can identify the commit in your branch history that should be associated with the release version mentioned in the error message you can recover by tagging this commit:
```bash
$ git tag -f v<version of the last release> <commit sha1 corresponding to last release>
$ git push -f --tags origin <your release branch>
```
## ENOGITHEAD There is no commit associated with last release
This error happens when there is no commit associated with the last release that can be found in the package metadata on the npm registry.
This usually happen when the last release has been made without access to the git repository informations.
You can recover from that issue by identifying the commit in your branch history that should have been associated with the release version mentioned in the error message and tagging this commit:
```bash
$ git tag -f v<version of the last release> <commit sha1 corresponding to last release>
$ git push -f --tags origin <your release branch>
```
## You do not have permission to publish 'package-name'
When running semantic-release you may encounter the following error:
When running semantic-release you might encounter the following error:
```
An error occurred while running semantic-release: { Error: Command failed: npm publish ./. --registry https://registry.npmjs.org/
```bash
npm ERR! publish Failed PUT 403
npm ERR! code E403
npm ERR! You do not have permission to publish "<package-name>". Are you logged in as the correct user? : <package-name>
```
This message is a little unclear, and might not have anything to with your `NPM_TOKEN` or authentication method. It might instead be related to the package name itself. If there is already a package with the same name as yours or, there is a very close match, it could trigger this error.
This is most likely related to a misconfiguration of the [npm registry authentication](https://github.com/semantic-release/npm#npm-registry-authentication) or to your user [missing permission](https://docs.npmjs.com/cli/team) for publishing.
Best way to be sure, is to search [npmjs.org](https://www.npmjs.com/)) using your package name. If there is a name conflict, rename your package in your `package.json`
It might also happen if the package name you are trying to publish already exists (in such case npm consider you are trying to publish a new version of a package that is not yours, hence the permission error).
To verify if your package name is available you can use [npm-name-cli](https://github.com/sindresorhus/npm-name-cli):
```bash
$ npm install --global npm-name-cli
$ npm-name <package-name>
```
If the package name is not available, change it in your `package.json` or consider using an [npm scope](https://docs.npmjs.com/misc/scope).
## Squashed commits are ignored by **semantic-release**
**semantic-release** parses commits according to a [commit message convention](https://github.com/semantic-release/semantic-release#commit-message-format) to figure out how they affect the codebase. Commits that doesn't follow the project's commit message convention are simply ignored.
When [squashing commits](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History#_squashing) most Git tools will by default generate a new commit message with a summary of the squashed commits. This commit message will most likely not be compliant with the project's commit message convention and therefore will be ignored by **semantic-release**.
When squashing commits make sure to rewrite the resulting commit message to be compliant with the project's commit message convention.
**Note**: if the resulting squashed commit would encompasses multiple changes (for example multiple unrelated features or fixes) then it's probably not a good idea to squash those commits together. A commit should contain exactly one self-contained functional change and a functional change should be contained in exactly one commit. See [atomic commits](https://en.wikipedia.org/wiki/Atomic_commit).
## `reference already exists` error when pushing tag
**semantic-release** read [Git tags](https://git-scm.com/book/en/v2/Git-Basics-Tagging) that are present in the history of your release branch in order to determine the last release published. Then it determines the next version to release based on the commits pushed since then and create the corresponding tag.
If a tag with the name already in your repository, Git will throw and error as tags must be unique across the repository.
This situation happens when you have a version tag identical to the new one **semantic-release** is trying to create that is not in the history of the current branch.
If an actual release with that version number was published you need to merge all the commits up to that release into your release branch.
If there is no published release with that version number, the tag must be deleted.
```bash
# Verify if the commit exists in the repository
$ git rev-list -1 <tag name>
# If a commit sha is returned, then the tag exists
# Verify the branches having the tagged commit in their history
$ git branch --contains <tag name>
# Delete the tag
$ git tag -d <tag name>
$ git push origin :refs/tags/<tag name>
```
+1
View File
@@ -1,5 +1,6 @@
# Usage
- [Getting started](getting-started.md#getting-started)
- [Installation](installation.md#installation)
- [CI Configuration](ci-configuration.md#ci-configuration)
- [Configuration](configuration.md#configuration)
+7 -24
View File
@@ -2,7 +2,7 @@
## Run `semantic-release` only after all tests succeeded
The `semantic-release` command must be executed only after all the tests in the CI build pass. If the build runs multiple jobs (for example to test on multiple Operating Systems or Node versions) the CI has to be configured to guarantee that the `semantic-release` command is executed only after all jobs are successful. This can be achieved with [Travis Build Stages](https://docs.travis-ci.com/user/build-stages), [CircleCI Workflows](https://circleci.com/docs/2.0/workflows), [Codeship Deployment Pipelines](https://documentation.codeship.com/basic/builds-and-configuration/deployment-pipelines), [GitLab Pipelines](https://docs.gitlab.com/ee/ci/pipelines.html#introduction-to-pipelines-and-jobs), [Wercker Workflows](http://devcenter.wercker.com/docs/workflows), [GoCD Pipelines](https://docs.gocd.org/current/introduction/concepts_in_go.html#pipeline) or specific tools like [`travis-deploy-once`](https://github.com/semantic-release/travis-deploy-once).
The `semantic-release` command must be executed only after all the tests in the CI build pass. If the build runs multiple jobs (for example to test on multiple Operating Systems or Node versions) the CI has to be configured to guarantee that the `semantic-release` command is executed only after all jobs are successful. This can be achieved with [Travis Build Stages](https://docs.travis-ci.com/user/build-stages), [CircleCI Workflows](https://circleci.com/docs/2.0/workflows), [Codeship Deployment Pipelines](https://documentation.codeship.com/basic/builds-and-configuration/deployment-pipelines), [GitLab Pipelines](https://docs.gitlab.com/ee/ci/pipelines.html#introduction-to-pipelines-and-jobs), [Codefresh Pipelines](https://codefresh.io/docs/docs/configure-ci-cd-pipeline/introduction-to-codefresh-pipelines), [Wercker Workflows](http://devcenter.wercker.com/docs/workflows) or [GoCD Pipelines](https://docs.gocd.org/current/introduction/concepts_in_go.html#pipeline).
See [CI configuration recipes](../recipes/README.md#ci-configurations) for more details.
@@ -10,14 +10,12 @@ See [CI configuration recipes](../recipes/README.md#ci-configurations) for more
**semantic-release** requires push access to the project Git repository in order to create [Git tags](https://git-scm.com/book/en/v2/Git-Basics-Tagging). The Git authentication can be set with one of the following environment variables:
| Variable | Description |
|---------------------------------|-------------------------------------------------------------------------------------------------------------------------------|
| `GH_TOKEN` or `GITHUB_TOKEN` | A GitHub [personal access token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line). |
| `GL_TOKEN` or `GITLAB_TOKEN` | A GitLab [personal access token](https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html). |
| `BB_TOKEN` or `BITBUCKET_TOKEN` | A Bitbucket [personal access token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html). |
| `GIT_CREDENTIALS` | [URL encoded basic HTTP Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication#URL_encoding) credentials). |
`GIT_CREDENTIALS` must be the Git username and password in the format `<username>:<password>`.
| Variable | Description |
|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `GH_TOKEN` or `GITHUB_TOKEN` | A GitHub [personal access token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line). |
| `GL_TOKEN` or `GITLAB_TOKEN` | A GitLab [personal access token](https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html). |
| `BB_TOKEN` or `BITBUCKET_TOKEN` | A Bitbucket [personal access token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html). |
| `GIT_CREDENTIALS` | [URL encoded](https://en.wikipedia.org/wiki/Percent-encoding) Git username and password in the format `<username>:<password>`. The username and password must each be individually URL encoded, not the `:` separating them. |
Alternatively the Git authentication can be set up via [SSH keys](../recipes/git-auth-ssh-keys.md).
@@ -33,18 +31,3 @@ See each plugin documentation for the environment variables to set up.
The authentication token/credentials have to be made available in the CI service via environment variables.
See [CI configuration recipes](../recipes/README.md#ci-configurations) for more details on how to configure environment variables in your CI service.
## Automatic setup with `semantic-release-cli`
[`semantic-release-cli`](https://github.com/semantic-release/cli) allows for easy [installation](installation.md) of **semantic-release** in your Node project as well as setting up the CI configuration:
```bash
npm install -g semantic-release-cli
cd your-module
semantic-release-cli setup
```
![dialogue](../../media/semantic-release-cli.png)
See the [semantic-release-cli](https://github.com/semantic-release/cli#what-it-does) documentation for more details.
+90 -131
View File
@@ -1,23 +1,42 @@
# Configuration
In order to customize **semantic-release**s behavior, [options](#options) and [plugins](plugins.md) can be set via:
**semantic-release** configuration consists of:
- Git repository options ([URL](#repositoryurl), [release branch](#branch) and [tag format](#tagformat))
- [plugins](#plugins) definition
- run mode ([debug](#debug), [dry run](#dryrun) and [local (no CI)](#ci))
All of these options can be configured directly or by extending a [shareable configuration](shareable-configurations.md).
Additionally, metadata of Git tags generated by **semantic-release** can be customized via standard [Git environment variables](#git-environment-variables).
## Configuration file
**semantic-release**s [options](#options), mode and [plugins](plugins.md) can be set via:
- A `.releaserc` file, written in YAML or JSON, with optional extensions: .`yaml`/`.yml`/`.json`/`.js`
- A `release.config.js` file that exports an object
- A `release` key in the project's `package.json` file
- CLI arguments
The following two examples are the same.
Alternatively, some options can be set via CLI arguments.
Via CLI argument:
```bash
$ semantic-release --branch next
```
The following three examples are the same.
Via `release` key in the project's `package.json` file:
```json
"release": {
{
"release": {
"branch": "next"
}
}
```
```bash
$ semantic-release
```
Via `.releaserc` file:
```json
{
"branch": "next"
}
```
@@ -25,25 +44,23 @@ Via `release` key in the project's `package.json` file:
$ semantic-release
```
Via CLI argument:
```bash
$ semantic-release --branch next
```
**Note**: CLI arguments take precedence over options configured in the configuration file.
**Note**: Plugin options cannot be defined via CLI arguments and must be defined in the configuration file.
## Environment variables
| Variable | Description | Default |
|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------|
| `GIT_AUTHOR_NAME` | The author name associated with the [Git release tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging). See [Git environment variables](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_committing). | @semantic-release-bot. |
| `GIT_AUTHOR_EMAIL` | The author email associated with the [Git release tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging). See [Git environment variables](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_committing). | @semantic-release-bot email address. |
| `GIT_COMMITTER_NAME` | The committer name associated with the [Git release tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging). See [Git environment variables](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_committing). | @semantic-release-bot. |
| `GIT_COMMITTER_EMAIL` | The committer email associated with the [Git release tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging). See [Git environment variables](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_committing). | @semantic-release-bot email address. |
**Note**: When configuring via `package.json`, the configuration must be under the `release` property. However, when using a `.releaserc` or a `release.config.js` file, the configuration must be set without a `release` property.
## Options
### extends
Type: `Array`, `String`
Type: `Array`, `String`<br>
CLI arguments: `-e`, `--extends`
List of modules or file paths containing a [shareable configuration](shareable-configurations.md). If multiple shareable configurations are set, they will be imported in the order defined with each configuration option taking precedence over the options defined in a previous shareable configuration.
@@ -52,20 +69,16 @@ List of modules or file paths containing a [shareable configuration](shareable-c
### branch
Type: `String`
Default: `master`
Type: `String`<br>
Default: `master`<br>
CLI arguments: `-b`, `--branch`
The branch on which releases should happen.
### repositoryUrl
Type: `String`
Default: `repository` property in `package.json` or [git origin url](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes)
Type: `String`<br>
Default: `repository` property in `package.json` or [git origin url](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes)<br>
CLI arguments: `-r`, `--repository-url`
The git repository URL.
@@ -74,138 +87,84 @@ Any valid git url format is supported (See [Git protocols](https://git-scm.com/b
### tagFormat
Type: `String`
Default: `v${version}`
Type: `String`<br>
Default: `v${version}`<br>
CLI arguments: `-t`, `--tag-format`
The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) format used by **semantic-release** to identify releases. The tag name is generated with [Lodash template](https://lodash.com/docs#template) and will be compiled with the `version` variable.
**Note**: The `tagFormat` must contain the `version` variable exactly once and compile to a [valid Git reference](https://git-scm.com/docs/git-check-ref-format#_description).
### plugins
Type: `Array`<br>
Default: `['@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator', '@semantic-release/npm', '@semantic-release/github']`<br>
CLI arguments: `-p`, `--plugins`
Define the list of plugins to use. Plugins will run in series, in the order defined, for each [steps](../../README.md#release-steps) if they implement it.
Plugins configuration can defined by wrapping the name and an options object in an array.
See [Plugins configuration](plugins.md#plugins) for more details.
### dryRun
Type: `Boolean`
Default: `false` if running in a CI environment, `true` otherwise
Type: `Boolean`<br>
Default: `false` if running in a CI environment, `true` otherwise<br>
CLI arguments: `-d`, `--dry-run`
Dry-run mode, skip publishing, print next version and release notes.
### noCi
### ci
Type: `Boolean`
Type: `Boolean`<br>
Default: `true`<br>
CLI arguments: `--ci` / `--no-ci`
Default: `false`
Set to `false` to skip Continuous Integration environment verifications. This allows for making releases from a local machine.
CLI arguments: `--no-ci`
Skip Continuous Integration environment verifications. This allows for making releases from a local machine.
**Note**: The CLI arguments `--no-ci` is equivalent to `--ci false`.
### debug
Type: `Boolean`
Default: `false`
Type: `Boolean`<br>
Default: `false`<br>
CLI argument: `--debug`
Output debugging information. It can also be enabled by setting the `DEBUG` environment variable to `semantic-release:*`.
Output debugging information. This can also be enabled by setting the `DEBUG` environment variable to `semantic-release:*`.
### verifyConditions
**Note**: The `debug` is used only supported via CLI argument. To enable debug mode from the [JS API](../developer-guide/js-api.md#javascript-api) use `require('debug').enable('semantic-release:*')`.
Type: `Array`, `String`, `Object`
## Git environment variables
Default: `['@semantic-release/npm', '@semantic-release/github']`
| Variable | Description | Default |
|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------|
| `GIT_AUTHOR_NAME` | The author name associated with the [Git release tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging). See [Git environment variables](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_committing). | @semantic-release-bot. |
| `GIT_AUTHOR_EMAIL` | The author email associated with the [Git release tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging). See [Git environment variables](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_committing). | @semantic-release-bot email address. |
| `GIT_COMMITTER_NAME` | The committer name associated with the [Git release tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging). See [Git environment variables](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_committing). | @semantic-release-bot. |
| `GIT_COMMITTER_EMAIL` | The committer email associated with the [Git release tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging). See [Git environment variables](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_committing). | @semantic-release-bot email address. |
CLI argument: `--verify-conditions`
## Existing version tags
Define the list of [verify conditions plugins](plugins.md#verifyconditions-plugin). Plugins will run in series, in the order defined in the `Array`.
**semantic-release** uses [Git tags](https://git-scm.com/book/en/v2/Git-Basics-Tagging) to determine the commits added since the last release. If a release has been published before setting up **semantic-release** you must make sure the most recent commit included in the last published release is in the [release branch](#branch) history and is tagged with the version released, formatted according to the [tag format](#tagformat) configured (defaults to `vx.y.z`).
See [Plugins configuration](plugins.md#configuration) for more details.
If the previous releases were published with [`npm publish`](https://docs.npmjs.com/cli/publish) this should already be the case.
### analyzeCommits
For example, if your release branch is `master`, the last release published on your project is `1.1.0` and the last commit included has the sha `1234567`, you must make sure this commit is in `master` history and is tagged with `v1.1.0`.
Type: `String`, `Object`
```bash
# Make sure the commit 1234567 is in the release branch history
$ git branch --contains 1234567
Default: `['@semantic-release/commit-analyzer']`
# If the commit is not in the branch history it means that either:
# - you use a different branch than the one your release from before
# - or the commit sha has been rewritten (with git rebase)
# In both cases you need to configure your repository to have the last release commit in the history of the release branch
CLI argument: `--analyze-commits`
# List the tags for the commit 1234567
$ git tag --contains 1234567
Define the [analyze commits plugin](plugins.md#analyzecommits-plugin).
See [Plugins configuration](plugins.md#configuration) for more details.
### verifyRelease
Type: `Array`, `String`, `Object`
Default: `[]`
CLI argument: `--verify-release`
Define the list of [verify release plugins](plugins.md#verifyrelease-plugin). Plugins will run in series, in the order defined in the `Array`.
See [Plugins configuration](plugins.md#configuration) for more details.
### generateNotes
Type: `String`, `Object`
Default: `['@semantic-release/release-notes-generator']`
CLI argument: `--generate-notes`
Define the [generate notes plugin](plugins.md#generatenotes-plugin).
See [Plugins configuration](plugins.md#configuration) for more details.
### prepare
Type: `Array`, `String`, `Object`
Default: `['@semantic-release/npm']`
CLI argument: `--prepare`
Define the list of [prepare plugins](plugins.md#prepare-plugin). Plugins will run in series, in the order defined in the `Array`.
See [Plugins configuration](plugins.md#configuration) for more details.
### publish
Type: `Array`, `String`, `Object`
Default: `['@semantic-release/npm', '@semantic-release/github']`
CLI argument: `--publish`
Define the list of [publish plugins](plugins.md#publish-plugin). Plugins will run in series, in the order defined in the `Array`.
See [Plugins configuration](plugins.md#configuration) for more details.
### success
Type: `Array`, `String`, `Object`
Default: `['@semantic-release/github']`
CLI argument: `--success`
Define the list of [success plugins](plugins.md#success-plugin). Plugins will run in series, in the order defined in the `Array`.
See [Plugins configuration](plugins.md#configuration) for more details.
### fail
Type: `Array`, `String`, `Object`
Default: `['@semantic-release/github']`
CLI argument: `--fail`
Define the list of [fail plugins](plugins.md#fail-plugin). Plugins will run in series, in the order defined in the `Array`.
See [Plugins configuration](plugins.md#configuration) for more details.
# If v1.1.0 is not in the list you add it with
$ git tag v1.1.0 1234567
$ git push origin v1.1.0
```
+22
View File
@@ -0,0 +1,22 @@
# Getting started
In order to use **semantic-release** you must follows this steps:
- [Install](./installation.md#installation) **semantic-release** in your project
- Configure your Continuous Integration service to [run **semantic-release**](./ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded)
- Configure your Git repository and package manager repository [authentication](ci-configuration.md#authentication) in your Continuous Integration service
- Configure **semantic-release** [options and plugins](./configuration.md#configuration)
Alternatively those steps can be easily done with the [**semantic-release** interactive CLI](https://github.com/semantic-release/cli):
```bash
npm install -g semantic-release-cli
cd your-module
semantic-release-cli setup
```
![dialogue](../../media/semantic-release-cli.png)
See the [semantic-release-cli](https://github.com/semantic-release/cli#what-it-does) documentation for more details.
**Note**: only a limited number of options, CI services and plugins is currently supported by `semantic-release-cli`.
+2 -2
View File
@@ -24,6 +24,6 @@ For other type of projects we recommend installing **semantic-release** directly
$ npx semantic-release
```
**Note:** For a global installation, it's recommended to specify the major **semantic-release** version to install (for example with with `npx semantic-release@12`, or `npm install -g semantic-release@12`). This way your build will not automatically use the next major **semantic-release** release that could possibly break your build. You will have to upgrade manually when a new major version is released.
**Note:** For a global installation, it's recommended to specify the major **semantic-release** version to install (for example with with `npx semantic-release@15`, or `npm install -g semantic-release@15`). This way your build will not automatically use the next major **semantic-release** release that could possibly break your build. You will have to upgrade manually when a new major version is released.
**Note:** `npx` is a tool bundled with `npm@>=5.2.0`. It is used to conveniently install the semantic-release binary and to execute it. See [What is npx](../support/FAQ.md#what-is-npx) for more details.
**Note:** `npx` is a tool bundled with `npm@>=5.2.0`. It is used to conveniently install the semantic-release binary and to execute it. See [What is npx](../support/FAQ.md#what-is-npx) for more details.
+64 -82
View File
@@ -1,98 +1,80 @@
# Plugins
Each [release step](../../README.md#release-steps) is implemented within a plugin or a list of plugins that can be configured. This allows for support of different [commit message formats](../../README.md#commit-message-format), release note generators and publishing platforms.
Each [release step](../../README.md#release-steps) is implemented by configurable plugins. This allows for support of different [commit message formats](../../README.md#commit-message-format), release note generators and publishing platforms.
See [plugins list](../extending/plugins-list.md).
A plugin is a npm module that can implement one or more of the following steps:
## Plugin types
| Step | Required | Description |
|--------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `verifyConditions` | No | Responsible for verifying conditions necessary to proceed with the release: configuration is correct, authentication token are valid, etc... |
| `analyzeCommits` | Yes | Responsible for determining the type of the next release (`major`, `minor` or `patch`). If multiple plugins with a `analyzeCommits` step are defined, the release type will be the highest one among plugins output. |
| `verifyRelease` | No | Responsible for verifying the parameters (version, type, dist-tag etc...) of the release that is about to be published. |
| `generateNotes` | No | Responsible for generating the content of the release note. If multiple plugins with a `generateNotes` step are defined, the release notes will be the result of the concatenation of each plugin output. |
| `prepare` | No | Responsible for preparing the release, for example creating or updating files such as `package.json`, `CHANGELOG.md`, documentation or compiled assets and pushing a commit. |
| `publish` | No | Responsible for publishing the release. |
| `success` | No | Responsible for notifying of a new release. |
| `fail` | No | Responsible for notifying of a failed release. |
### verifyConditions plugin
**Note:** If no plugin with a `analyzeCommits` step is defined `@semantic-release/commit-analyzer` will be used.
Responsible for verifying conditions necessary to proceed with the release: configuration is correct, authentication token are valid, etc...
See [available plugins](../extending/plugins-list.md).
Default implementation: [@semantic-release/npm](https://github.com/semantic-release/npm#verifyconditions) and [@semantic-release/github](https://github.com/semantic-release/github#verifyconditions).
## Plugins configuration
### analyzeCommits plugin
Each plugin must be installed and configured with the [`plugins` options](./configuration.md#plugins) by specifying the list of plugins by npm module name.
Responsible for determining the type of the next release (`major`, `minor` or `patch`).
```bash
$ npm install @semantic-release/commit-analyzer @semantic-release/release-notes-generator @semantic-release/npm -D
```
Default implementation: [@semantic-release/commit-analyzer](https://github.com/semantic-release/commit-analyzer).
### verifyRelease plugin
Responsible for verifying the parameters (version, type, dist-tag etc...) of the release that is about to be published match certain expectations. For example the [cracks plugin](https://github.com/semantic-release/cracks) is able to verify that if a release contains breaking changes, its type must be `major`.
Default implementation: none.
### generateNotes plugin
Responsible for generating release notes.
Default implementation: [@semantic-release/release-notes-generator](https://github.com/semantic-release/release-notes-generator).
### prepare plugin
Responsible for preparing the release, including:
- Creating or updating files such as `package.json`, `CHANGELOG.md`, documentation or compiled assets.
- Create and push commits
Default implementation: [@semantic-release/npm](https://github.com/semantic-release/npm#prepare).
### publish plugin
Responsible for publishing the release.
Default implementation: [@semantic-release/npm](https://github.com/semantic-release/npm#publish) and [@semantic-release/github](https://github.com/semantic-release/github#publish).
### success plugin
Responsible for notifying of a new release.
Default implementation: [@semantic-release/github](https://github.com/semantic-release/github#success).
### fail plugin
Responsible for notifying of a failed release.
Default implementation: [@semantic-release/github](https://github.com/semantic-release/github#fail).
## Configuration
Plugin can be configured by specifying the plugin's module name or file path directly as a `String` or within the `path` key of an `Object`.
Plugins specific options can be set similarly to the other **semantic-release** [options](configuration.md#options) or within the plugin `Object`. Plugins options defined along with the other **semantic-release** [options](configuration.md#options) will apply to all plugins. Options defined within the plugin `Object` will apply to that specific plugin.
For example:
```json
{
"release": {
"verifyConditions": [
{
"path": "@semantic-release/exec",
"cmd": "verify-conditions.sh"
},
"@semantic-release/npm",
"@semantic-release/github"
],
"analyzeCommits": "custom-plugin",
"verifyRelease": [
{
"path": "@semantic-release/exec",
"cmd": "verify-release.sh"
},
],
"generateNotes": "./build/my-plugin.js",
"githubUrl": "https://my-ghe.com",
"githubApiPathPrefix": "/api-prefix"
}
"plugins": ["@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/npm"]
}
```
## Plugin ordering
For each [release step](../../README.md#release-steps) the plugins that implement that step will be executed in the order in which the are defined.
```json
{
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/npm",
"@semantic-release/git"
]
}
```
With this configuration **semantic-release** will:
- execute the `verifyConditions` implementation of `@semantic-release/npm` then `@semantic-release/git`
- execute the `analyzeCommits` implementation of `@semantic-release/commit-analyzer`
- execute the `prepare` implementation of `@semantic-release/npm` then `@semantic-release/git`
- execute the `generateNotes` implementation of `@semantic-release/release-notes-generator`
- execute the `publish` implementation of `@semantic-release/npm`
## Plugin options
A plugin options can specified by wrapping the name and an options object in an array. Options configured this way will be passed only to that specific plugin.
Global plugin options can defined at the root of the **semantic-release** configuration object. Options configured this way will be passed to all plugins.
```json
{
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
["@semantic-release/github", {
"assets": ["dist/**"]
}],
"@semantic-release/git"
],
"preset": "angular"
}
```
With this configuration:
- the `custom-plugin` npm module will be used to [analyze commits](#analyzecommits-plugin)
- the `./build/my-plugin.js` script will be used to [generate release notes](#generatenotes-plugin)
- the [`@semantic-release/exec`](https://github.com/semantic-release/exec), [`@semantic-release/npm`](https://github.com/semantic-release/npm) and [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugins will be used to [verify conditions](#verifyconditions-plugin)
- the [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugin will be used to [verify the release](#verifyrelease-plugin)
- the `cmd` option will be set to `verify-conditions.sh` only for the [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugin used to [verify conditions](#verifyconditions-plugin)
- the `cmd` option will be set to `verify-release.sh` only for the [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugin used to [verify the release](#verifyrelease-plugin)
- the `githubUrl` and `githubApiPathPrefix` options will be set to respectively `https://my-ghe.com` and `/api-prefix` for all plugins
- All plugins will receive the `preset` option, which will be used by both `@semantic-release/commit-analyzer` and `@semantic-release/release-notes-generator` (and ignored by `@semantic-release/github` and `@semantic-release/git`)
- The `@semantic-release/github` plugin will receive the `assets` options (`@semantic-release/git` will not receive it and therefore will use it's default value for that option)
+92 -114
View File
@@ -1,4 +1,4 @@
const {template, isPlainObject, castArray} = require('lodash');
const {template, pick} = require('lodash');
const marked = require('marked');
const TerminalRenderer = require('marked-terminal');
const envCi = require('env-ci');
@@ -12,153 +12,128 @@ const getCommits = require('./lib/get-commits');
const getLastRelease = require('./lib/get-last-release');
const {extractErrors} = require('./lib/utils');
const getGitAuthUrl = require('./lib/get-git-auth-url');
const logger = require('./lib/logger');
const {unshallow, verifyAuth, isBranchUpToDate, gitHead: getGitHead, tag, push} = require('./lib/git');
const getLogger = require('./lib/get-logger');
const {fetch, verifyAuth, isBranchUpToDate, getGitHead, tag, push} = require('./lib/git');
const getError = require('./lib/get-error');
const {COMMIT_NAME, COMMIT_EMAIL} = require('./lib/definitions/constants');
marked.setOptions({renderer: new TerminalRenderer()});
async function run(options, plugins) {
const {isCi, branch, isPr} = envCi();
async function run(context, plugins) {
const {cwd, env, options, logger} = context;
const {isCi, branch: ciBranch, isPr} = envCi({env, cwd});
if (!isCi && !options.dryRun && !options.noCi) {
logger.log('This run was not triggered in a known CI environment, running in dry-run mode.');
logger.warn('This run was not triggered in a known CI environment, running in dry-run mode.');
options.dryRun = true;
} else {
// When running on CI, set the commits author and commiter info and prevent the `git` CLI to prompt for username/password. See #703.
process.env = {
Object.assign(env, {
GIT_AUTHOR_NAME: COMMIT_NAME,
GIT_AUTHOR_EMAIL: COMMIT_EMAIL,
GIT_COMMITTER_NAME: COMMIT_NAME,
GIT_COMMITTER_EMAIL: COMMIT_EMAIL,
...process.env,
...env,
GIT_ASKPASS: 'echo',
GIT_TERMINAL_PROMPT: 0,
};
});
}
if (isCi && isPr && !options.noCi) {
logger.log("This run was triggered by a pull request and therefore a new version won't be published.");
return;
return false;
}
if (branch !== options.branch) {
if (ciBranch !== options.branch) {
logger.log(
`This test run was triggered on the branch ${branch}, while semantic-release is configured to only publish from ${
`This test run was triggered on the branch ${ciBranch}, while semantic-release is configured to only publish from ${
options.branch
}, therefore a new version wont be published.`
);
return false;
}
logger[options.dryRun ? 'warn' : 'success'](
`Run automated release from branch ${ciBranch}${options.dryRun ? ' in dry-run mode' : ''}`
);
await verify(options);
await verify(context);
const {repositoryUrl} = options;
options.repositoryUrl = await getGitAuthUrl(options);
if (!(await isBranchUpToDate(options.branch))) {
logger.log(
"The local branch %s is behind the remote one, therefore a new version won't be published.",
options.branch
);
return false;
}
options.repositoryUrl = await getGitAuthUrl(context);
try {
await verifyAuth(options.repositoryUrl, options.branch);
} catch (err) {
logger.error(`The command "${err.cmd}" failed with the error message %s.`, err.stderr);
throw getError('EGITNOPERMISSION', {options, repositoryUrl});
try {
await verifyAuth(options.repositoryUrl, options.branch, {cwd, env});
} catch (error) {
if (!(await isBranchUpToDate(options.branch, {cwd, env}))) {
logger.log(
`The local branch ${options.branch} is behind the remote one, therefore a new version won't be published.`
);
return false;
}
throw error;
}
} catch (error) {
logger.error(`The command "${error.cmd}" failed with the error message ${error.stderr}.`);
throw getError('EGITNOPERMISSION', {options});
}
logger.log('Run automated release from branch %s', options.branch);
logger.success(`Allowed to push to the Git repository`);
logger.log('Call plugin %s', 'verify-conditions');
await plugins.verifyConditions({options, logger}, {settleAll: true});
await plugins.verifyConditions(context);
// Unshallow the repo in order to get all the tags
await unshallow(options.repositoryUrl);
await fetch(options.repositoryUrl, {cwd, env});
const lastRelease = await getLastRelease(options.tagFormat, logger);
const commits = await getCommits(lastRelease.gitHead, options.branch, logger);
context.lastRelease = await getLastRelease(context);
context.commits = await getCommits(context);
logger.log('Call plugin %s', 'analyze-commits');
const type = await plugins.analyzeCommits({
options,
logger,
lastRelease,
commits: commits.filter(commit => !/\[skip\s+release\]|\[release\s+skip\]/i.test(commit.message)),
});
if (!type) {
const nextRelease = {type: await plugins.analyzeCommits(context), gitHead: await getGitHead({cwd, env})};
if (!nextRelease.type) {
logger.log('There are no relevant changes, so no new version is released.');
return;
return false;
}
const version = getNextVersion(type, lastRelease, logger);
const nextRelease = {type, version, gitHead: await getGitHead(), gitTag: template(options.tagFormat)({version})};
context.nextRelease = nextRelease;
nextRelease.version = getNextVersion(context);
nextRelease.gitTag = template(options.tagFormat)({version: nextRelease.version});
logger.log('Call plugin %s', 'verify-release');
await plugins.verifyRelease({options, logger, lastRelease, commits, nextRelease}, {settleAll: true});
await plugins.verifyRelease(context);
const generateNotesParam = {options, logger, lastRelease, commits, nextRelease};
nextRelease.notes = await plugins.generateNotes(context);
await plugins.prepare(context);
if (options.dryRun) {
logger.log('Call plugin %s', 'generate-notes');
const notes = await plugins.generateNotes(generateNotesParam);
logger.log('Release note for version %s:\n', nextRelease.version);
process.stdout.write(`${marked(notes)}\n`);
logger.warn(`Skip ${nextRelease.gitTag} tag creation in dry-run mode`);
} else {
logger.log('Call plugin %s', 'generateNotes');
nextRelease.notes = await plugins.generateNotes(generateNotesParam);
logger.log('Call plugin %s', 'prepare');
await plugins.prepare(
{options, logger, lastRelease, commits, nextRelease},
{
getNextInput: async lastResult => {
const newGitHead = await getGitHead();
// If previous prepare plugin has created a commit (gitHead changed)
if (lastResult.nextRelease.gitHead !== newGitHead) {
nextRelease.gitHead = newGitHead;
// Regenerate the release notes
logger.log('Call plugin %s', 'generateNotes');
nextRelease.notes = await plugins.generateNotes(generateNotesParam);
}
// Call the next publish plugin with the updated `nextRelease`
return {options, logger, lastRelease, commits, nextRelease};
},
}
);
// Create the tag before calling the publish plugins as some require the tag to exists
logger.log('Create tag %s', nextRelease.gitTag);
await tag(nextRelease.gitTag);
await push(options.repositoryUrl, branch);
logger.log('Call plugin %s', 'publish');
const releases = await plugins.publish(
{options, logger, lastRelease, commits, nextRelease},
// Add nextRelease and plugin properties to published release
{transform: (release, step) => ({...(isPlainObject(release) ? release : {}), ...nextRelease, ...step})}
);
await plugins.success(
{options, logger, lastRelease, commits, nextRelease, releases: castArray(releases)},
{settleAll: true}
);
logger.log('Published release: %s', nextRelease.version);
await tag(nextRelease.gitTag, {cwd, env});
await push(options.repositoryUrl, {cwd, env});
logger.success(`Created tag ${nextRelease.gitTag}`);
}
return true;
context.releases = await plugins.publish(context);
await plugins.success(context);
logger.success(`Published release ${nextRelease.version}`);
if (options.dryRun) {
logger.log(`Release note for version ${nextRelease.version}:`);
if (nextRelease.notes) {
context.stdout.write(marked(nextRelease.notes));
}
}
return pick(context, ['lastRelease', 'commits', 'nextRelease', 'releases']);
}
function logErrors(err) {
function logErrors({logger, stderr}, err) {
const errors = extractErrors(err).sort(error => (error.semanticRelease ? -1 : 0));
for (const error of errors) {
if (error.semanticRelease) {
logger.log(`%s ${error.message}`, error.code);
logger.error(`${error.code} ${error.message}`);
if (error.details) {
process.stdout.write(`${marked(error.details)}\n`);
stderr.write(marked(error.details));
}
} else {
logger.error('An error occurred while running semantic-release: %O', error);
@@ -166,36 +141,39 @@ function logErrors(err) {
}
}
async function callFail(plugins, options, error) {
const errors = extractErrors(error).filter(error => error.semanticRelease);
async function callFail(context, plugins, err) {
const errors = extractErrors(err).filter(err => err.semanticRelease);
if (errors.length > 0) {
try {
await plugins.fail({options, logger, errors}, {settleAll: true});
} catch (err) {
logErrors(err);
await plugins.fail({...context, errors});
} catch (error) {
logErrors(context, error);
}
}
}
module.exports = async opts => {
logger.log(`Running %s version %s`, pkg.name, pkg.version);
const unhook = hookStd({silent: false}, hideSensitive);
module.exports = async (opts = {}, {cwd = process.cwd(), env = process.env, stdout, stderr} = {}) => {
const {unhook} = hookStd(
{silent: false, streams: [process.stdout, process.stderr, stdout, stderr].filter(Boolean)},
hideSensitive(env)
);
const context = {cwd, env, stdout: stdout || process.stdout, stderr: stderr || process.stderr};
context.logger = getLogger(context);
context.logger.log(`Running ${pkg.name} version ${pkg.version}`);
try {
const config = await getConfig(opts, logger);
const {plugins, options} = config;
const {plugins, options} = await getConfig(context, opts);
context.options = options;
try {
const result = await run(options, plugins);
const result = await run(context, plugins);
unhook();
return result;
} catch (err) {
if (!options.dryRun) {
await callFail(plugins, options, err);
}
throw err;
} catch (error) {
await callFail(context, plugins, error);
throw error;
}
} catch (err) {
logErrors(err);
} catch (error) {
logErrors(context, error);
unhook();
throw err;
throw error;
}
};
+16 -2
View File
@@ -1,4 +1,4 @@
const RELEASE_TYPE = ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease'];
const RELEASE_TYPE = ['prerelease', 'prepatch', 'patch', 'preminor', 'minor', 'premajor', 'major'];
const FIRST_RELEASE = '1.0.0';
@@ -6,4 +6,18 @@ const COMMIT_NAME = 'semantic-release-bot';
const COMMIT_EMAIL = 'semantic-release-bot@martynus.net';
module.exports = {RELEASE_TYPE, FIRST_RELEASE, COMMIT_NAME, COMMIT_EMAIL};
const RELEASE_NOTES_SEPARATOR = '\n\n';
const SECRET_REPLACEMENT = '[secure]';
const SECRET_MIN_SIZE = 5;
module.exports = {
RELEASE_TYPE,
FIRST_RELEASE,
COMMIT_NAME,
COMMIT_EMAIL,
RELEASE_NOTES_SEPARATOR,
SECRET_REPLACEMENT,
SECRET_MIN_SIZE,
};
+26 -18
View File
@@ -4,16 +4,16 @@ const {toLower, isString} = require('lodash');
const pkg = require('../../package.json');
const {RELEASE_TYPE} = require('./constants');
const homepage = url.format({...url.parse(pkg.homepage), ...{hash: null}});
const homepage = url.format({...url.parse(pkg.homepage), hash: null});
const stringify = obj => (isString(obj) ? obj : inspect(obj, {breakLength: Infinity, depth: 2, maxArrayLength: 5}));
const linkify = file => `${homepage}/blob/caribou/${file}`;
const linkify = file => `${homepage}/blob/master/${file}`;
module.exports = {
ENOGITREPO: () => ({
ENOGITREPO: ({cwd}) => ({
message: 'Not running from a git repository.',
details: `The \`semantic-release\` command must be executed from a Git repository.
The current working directory is \`${process.cwd()}\`.
The current working directory is \`${cwd}\`.
Please verify your CI configuration to make sure the \`semantic-release\` command is executed from the root of the cloned repository.`,
}),
@@ -27,11 +27,11 @@ Please make sure to add the \`repositoryUrl\` to the [semantic-release configura
'docs/usage/configuration.md'
)}).`,
}),
EGITNOPERMISSION: ({options, repositoryUrl}) => ({
EGITNOPERMISSION: ({options}) => ({
message: 'The push permission to the Git repository is required.',
details: `**semantic-release** cannot push the version tag to the branch \`${
options.branch
}\` on remote Git repository with URL \`${repositoryUrl}\`.
}\` on remote Git repository with URL \`${options.repositoryUrl}\`.
Please refer to the [authentication configuration documentation](${linkify(
'docs/usage/ci-configuration.md#authentication'
@@ -55,25 +55,33 @@ Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.`
Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.`,
}),
EPLUGINCONF: ({pluginType, pluginConf}) => ({
message: `The \`${pluginType}\` plugin configuration is invalid.`,
details: `The [${pluginType} plugin configuration](${linkify(
`docs/usage/plugins.md#${toLower(pluginType)}-plugin`
)}) if defined, must be a single or an array of plugins definition. A plugin definition is either a string or an object with a \`path\` property.
EPLUGINCONF: ({type, required, pluginConf}) => ({
message: `The \`${type}\` plugin configuration is invalid.`,
details: `The [${type} plugin configuration](${linkify(`docs/usage/plugins.md#${toLower(type)}-plugin`)}) ${
required ? 'is required and ' : ''
} must be a single or an array of plugins definition. A plugin definition is an npm module name, optionnaly wrapped in an array with an object.
Your configuration for the \`${pluginType}\` plugin is \`${stringify(pluginConf)}\`.`,
Your configuration for the \`${type}\` plugin is \`${stringify(pluginConf)}\`.`,
}),
EPLUGIN: ({pluginName, pluginType}) => ({
message: `A plugin configured in the step ${pluginType} is not a valid semantic-release plugin.`,
details: `A valid \`${pluginType}\` **semantic-release** plugin must be a function or an object with a function in the property \`${pluginType}\`.
EPLUGINSCONF: ({plugin}) => ({
message: 'The `plugins` configuration is invalid.',
details: `The [plugins](${linkify(
'docs/usage/configuration.md#plugins'
)}) option must be an array of plugin definions. A plugin definition is an npm module name, optionnaly wrapped in an array with an object.
The plugin \`${pluginName}\` doesn't have the property \`${pluginType}\` and cannot be used for the \`${pluginType}\` step.
The invalid configuration is \`${stringify(plugin)}\`.`,
}),
EPLUGIN: ({pluginName, type}) => ({
message: `A plugin configured in the step ${type} is not a valid semantic-release plugin.`,
details: `A valid \`${type}\` **semantic-release** plugin must be a function or an object with a function in the property \`${type}\`.
The plugin \`${pluginName}\` doesn't have the property \`${type}\` and cannot be used for the \`${type}\` step.
Please refer to the \`${pluginName}\` and [semantic-release plugins configuration](${linkify(
'docs/usage/plugins.md'
)}) documentation for more details.`,
}),
EANALYZEOUTPUT: ({result, pluginName}) => ({
EANALYZECOMMITSOUTPUT: ({result, pluginName}) => ({
message: 'The `analyzeCommits` plugin returned an invalid value. It must return a valid semver release type.',
details: `The \`analyzeCommits\` plugin must return a valid [semver](https://semver.org) release type. The valid values are: ${RELEASE_TYPE.map(
type => `\`${type}\``
@@ -89,7 +97,7 @@ We recommend to report the issue to the \`${pluginName}\` authors, providing the
'docs/developer-guide/plugin.md'
)})`,
}),
ERELEASENOTESOUTPUT: ({result, pluginName}) => ({
EGENERATENOTESOUTPUT: ({result, pluginName}) => ({
message: 'The `generateNotes` plugin returned an invalid value. It must return a `String`.',
details: `The \`generateNotes\` plugin must return a \`String\`.
+73 -48
View File
@@ -1,67 +1,92 @@
const {isString, isFunction, isArray, isPlainObject} = require('lodash');
const {RELEASE_TYPE} = require('./constants');
const validatePluginConfig = conf => isString(conf) || isString(conf.path) || isFunction(conf);
const {isString, isPlainObject} = require('lodash');
const {getGitHead} = require('../git');
const hideSensitive = require('../hide-sensitive');
const {hideSensitiveValues} = require('../utils');
const {RELEASE_TYPE, RELEASE_NOTES_SEPARATOR} = require('./constants');
module.exports = {
verifyConditions: {
default: ['@semantic-release/npm', '@semantic-release/github'],
config: {
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
},
required: false,
dryRun: true,
pipelineConfig: () => ({settleAll: true}),
},
analyzeCommits: {
default: '@semantic-release/commit-analyzer',
config: {
validator: conf => Boolean(conf) && validatePluginConfig(conf),
},
output: {
validator: output => !output || RELEASE_TYPE.includes(output),
error: 'EANALYZEOUTPUT',
},
default: ['@semantic-release/commit-analyzer'],
required: true,
dryRun: true,
outputValidator: output => !output || RELEASE_TYPE.includes(output),
preprocess: ({commits, ...inputs}) => ({
...inputs,
commits: commits.filter(commit => !/\[skip\s+release\]|\[release\s+skip\]/i.test(commit.message)),
}),
postprocess: results =>
RELEASE_TYPE[
results.reduce((highest, result) => {
const typeIndex = RELEASE_TYPE.indexOf(result);
return typeIndex > highest ? typeIndex : highest;
}, -1)
],
},
verifyRelease: {
default: false,
config: {
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
},
required: false,
dryRun: true,
pipelineConfig: () => ({settleAll: true}),
},
generateNotes: {
default: '@semantic-release/release-notes-generator',
config: {
validator: conf => !conf || validatePluginConfig(conf),
},
output: {
validator: output => !output || isString(output),
error: 'ERELEASENOTESOUTPUT',
},
required: false,
dryRun: true,
outputValidator: output => !output || isString(output),
pipelineConfig: () => ({
getNextInput: ({nextRelease, ...context}, notes) => ({
...context,
nextRelease: {
...nextRelease,
notes: `${nextRelease.notes ? `${nextRelease.notes}${RELEASE_NOTES_SEPARATOR}` : ''}${notes}`,
},
}),
}),
postprocess: (results, {env}) => hideSensitive(env)(results.filter(Boolean).join(RELEASE_NOTES_SEPARATOR)),
},
prepare: {
default: ['@semantic-release/npm'],
config: {
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
},
required: false,
dryRun: false,
pipelineConfig: ({generateNotes}) => ({
getNextInput: async context => {
const newGitHead = await getGitHead({cwd: context.cwd});
// If previous prepare plugin has created a commit (gitHead changed)
if (context.nextRelease.gitHead !== newGitHead) {
context.nextRelease.gitHead = newGitHead;
// Regenerate the release notes
context.nextRelease.notes = await generateNotes(context);
}
// Call the next prepare plugin with the updated `nextRelease`
return context;
},
}),
},
publish: {
default: ['@semantic-release/npm', '@semantic-release/github'],
config: {
validator: conf => Boolean(conf) && (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
},
output: {
validator: output => !output || isPlainObject(output),
error: 'EPUBLISHOUTPUT',
},
required: false,
dryRun: false,
outputValidator: output => !output || isPlainObject(output),
pipelineConfig: () => ({
// Add `nextRelease` and plugin properties to published release
transform: (release, step, {nextRelease}) => ({
...(release === false ? {} : nextRelease),
...release,
...step,
}),
}),
},
success: {
default: ['@semantic-release/github'],
config: {
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
},
required: false,
dryRun: false,
pipelineConfig: () => ({settleAll: true}),
preprocess: ({releases, env, ...inputs}) => ({...inputs, env, releases: hideSensitiveValues(env, releases)}),
},
fail: {
default: ['@semantic-release/github'],
config: {
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
},
required: false,
dryRun: false,
pipelineConfig: () => ({settleAll: true}),
preprocess: ({errors, env, ...inputs}) => ({...inputs, env, errors: hideSensitiveValues(env, errors)}),
},
};
+10 -12
View File
@@ -5,13 +5,11 @@ const debug = require('debug')('semantic-release:get-commits');
/**
* Retrieve the list of commits on the current branch since the commit sha associated with the last release, or all the commits of the current branch if there is no last released version.
*
* @param {String} gitHead The commit sha associated with the last release.
* @param {String} branch The branch to release from.
* @param {Object} logger Global logger.
* @param {Object} context semantic-release context.
*
* @return {Promise<Array<Object>>} The list of commits on the branch `branch` since the last release.
*/
module.exports = async (gitHead, branch, logger) => {
module.exports = async ({cwd, env, lastRelease: {gitHead}, logger}) => {
if (gitHead) {
debug('Use gitHead: %s', gitHead);
} else {
@@ -19,14 +17,14 @@ module.exports = async (gitHead, branch, logger) => {
}
Object.assign(gitLogParser.fields, {hash: 'H', message: 'B', gitTags: 'd', committerDate: {key: 'ci', type: Date}});
const commits = (await getStream.array(gitLogParser.parse({_: `${gitHead ? gitHead + '..' : ''}HEAD`}))).map(
commit => {
commit.message = commit.message.trim();
commit.gitTags = commit.gitTags.trim();
return commit;
}
);
logger.log('Found %s commits since last release', commits.length);
const commits = (await getStream.array(
gitLogParser.parse({_: `${gitHead ? gitHead + '..' : ''}HEAD`}, {cwd, env: {...process.env, ...env}})
)).map(commit => {
commit.message = commit.message.trim();
commit.gitTags = commit.gitTags.trim();
return commit;
});
logger.log(`Found ${commits.length} commits since last release`);
debug('Parsed commits: %o', commits);
return commits;
};
+38 -21
View File
@@ -1,4 +1,4 @@
const {castArray, pickBy, isUndefined, isNull, isString, isPlainObject} = require('lodash');
const {castArray, pickBy, isNil, isString, isPlainObject} = require('lodash');
const readPkgUp = require('read-pkg-up');
const cosmiconfig = require('cosmiconfig');
const resolveFrom = require('resolve-from');
@@ -6,6 +6,7 @@ const debug = require('debug')('semantic-release:config');
const {repoUrl} = require('./git');
const PLUGINS_DEFINITIONS = require('./definitions/plugins');
const plugins = require('./plugins');
const {validatePlugin, parseConfig} = require('./plugins/utils');
const CONFIG_NAME = 'release';
const CONFIG_FILES = [
@@ -18,10 +19,17 @@ const CONFIG_FILES = [
`${CONFIG_NAME}.config.js`,
];
module.exports = async (opts, logger) => {
const {config} = (await cosmiconfig(CONFIG_NAME, {searchPlaces: CONFIG_FILES}).search()) || {};
module.exports = async (context, opts) => {
const {cwd, env} = context;
const {config, filepath} = (await cosmiconfig(CONFIG_NAME, {searchPlaces: CONFIG_FILES}).search(cwd)) || {};
debug('load config from: %s', filepath);
// Merge config file options and CLI/API options
let options = {...config, ...opts};
if (options.ci === false) {
options.noCi = true;
}
const pluginsPath = {};
let extendPaths;
({extends: extendPaths, ...options} = options);
@@ -29,22 +37,25 @@ module.exports = async (opts, logger) => {
// If `extends` is defined, load and merge each shareable config with `options`
options = {
...castArray(extendPaths).reduce((result, extendPath) => {
const extendsOpts = require(resolveFrom.silent(__dirname, extendPath) ||
resolveFrom(process.cwd(), extendPath));
const extendsOpts = require(resolveFrom.silent(__dirname, extendPath) || resolveFrom(cwd, extendPath));
// For each plugin defined in a shareable config, save in `pluginsPath` the extendable config path,
// so those plugin will be loaded relatively to the config file
Object.keys(extendsOpts).reduce((pluginsPath, option) => {
if (PLUGINS_DEFINITIONS[option]) {
castArray(extendsOpts[option])
.filter(plugin => isString(plugin) || (isPlainObject(plugin) && isString(plugin.path)))
.map(plugin => (isString(plugin) ? plugin : plugin.path))
.forEach(plugin => {
pluginsPath[plugin] = extendPath;
});
}
return pluginsPath;
}, pluginsPath);
Object.entries(extendsOpts)
.filter(([, value]) => Boolean(value))
.reduce((pluginsPath, [option, value]) => {
castArray(value).forEach(plugin => {
if (option === 'plugins' && validatePlugin(plugin)) {
pluginsPath[parseConfig(plugin)[0]] = extendPath;
} else if (
PLUGINS_DEFINITIONS[option] &&
(isString(plugin) || (isPlainObject(plugin) && isString(plugin.path)))
) {
pluginsPath[isString(plugin) ? plugin : plugin.path] = extendPath;
}
});
return pluginsPath;
}, pluginsPath);
return {...result, ...extendsOpts};
}, {}),
@@ -55,18 +66,24 @@ module.exports = async (opts, logger) => {
// Set default options values if not defined yet
options = {
branch: 'master',
repositoryUrl: (await pkgRepoUrl()) || (await repoUrl()),
repositoryUrl: (await pkgRepoUrl({normalize: false, cwd})) || (await repoUrl({cwd, env})),
tagFormat: `v\${version}`,
plugins: [
'@semantic-release/commit-analyzer',
'@semantic-release/release-notes-generator',
'@semantic-release/npm',
'@semantic-release/github',
],
// Remove `null` and `undefined` options so they can be replaced with default ones
...pickBy(options, option => !isUndefined(option) && !isNull(option)),
...pickBy(options, option => !isNil(option)),
};
debug('options values: %O', options);
return {options, plugins: await plugins(options, pluginsPath, logger)};
return {options, plugins: await plugins({...context, options}, pluginsPath)};
};
async function pkgRepoUrl() {
const {pkg} = await readPkgUp({normalize: false});
async function pkgRepoUrl(opts) {
const {pkg} = await readPkgUp(opts);
return pkg && (isPlainObject(pkg.repository) ? pkg.repository.url : pkg.repository);
}
+20 -21
View File
@@ -1,6 +1,5 @@
const {parse, format} = require('url');
const {isUndefined} = require('lodash');
const gitUrlParse = require('git-url-parse');
const {isNil} = require('lodash');
const hostedGitInfo = require('hosted-git-info');
const {verifyAuth} = require('./git');
@@ -21,38 +20,38 @@ const GIT_TOKENS = {
*
* In addition, expand shortcut URLs (`owner/repo` => `https://github.com/owner/repo.git`) and transform `git+https` / `git+http` URLs to `https` / `http`.
*
* @param {String} repositoryUrl The user provided Git repository URL.
* @param {Object} context semantic-release context.
*
* @return {String} The formatted Git repository URL.
*/
module.exports = async ({repositoryUrl, branch}) => {
module.exports = async ({cwd, env, options: {repositoryUrl, branch}}) => {
const info = hostedGitInfo.fromUrl(repositoryUrl, {noGitPlus: true});
const {protocol, ...parsed} = parse(repositoryUrl);
if (info && info.getDefaultRepresentation() === 'shortcut') {
// Expand shorthand URLs (such as `owner/repo` or `gitlab:owner/repo`)
repositoryUrl = info.https();
} else {
const {protocols} = gitUrlParse(repositoryUrl);
} else if (protocol && protocol.includes('http')) {
// Replace `git+https` and `git+http` with `https` or `http`
if (protocols.includes('http') || protocols.includes('https')) {
repositoryUrl = format({
...parse(repositoryUrl),
...{protocol: protocols.includes('https') ? 'https' : 'http'},
});
}
repositoryUrl = format({...parsed, protocol: protocol.includes('https') ? 'https' : 'http', href: null});
}
// Test if push is allowed without transforming the URL (e.g. is ssh keys are set up)
try {
await verifyAuth(repositoryUrl, branch);
} catch (err) {
const envVar = Object.keys(GIT_TOKENS).find(envVar => !isUndefined(process.env[envVar]));
const gitCredentials = `${GIT_TOKENS[envVar] || ''}${process.env[envVar] || ''}`;
const {protocols, ...parsed} = gitUrlParse(repositoryUrl);
const protocol = protocols.includes('https') ? 'https' : protocols.includes('http') ? 'http' : 'https';
await verifyAuth(repositoryUrl, branch, {cwd, env});
} catch (error) {
const envVar = Object.keys(GIT_TOKENS).find(envVar => !isNil(env[envVar]));
const gitCredentials = `${GIT_TOKENS[envVar] || ''}${env[envVar] || ''}`;
// If credentials are set via anvironment variables, convert the URL to http/https and add basic auth, otherwise return `repositoryUrl` as is
return gitCredentials ? {...parsed, protocols: [protocol], user: gitCredentials}.toString(protocol) : repositoryUrl;
if (gitCredentials) {
// If credentials are set via anvironment variables, convert the URL to http/https and add basic auth, otherwise return `repositoryUrl` as is
const [match, auth, host, path] = /^(?!.+:\/\/)(?:(.*)@)?(.*?):(.*)$/.exec(repositoryUrl) || [];
return format({
...parse(match ? `ssh://${auth ? `${auth}@` : ''}${host}/${path}` : repositoryUrl),
auth: gitCredentials,
protocol: protocol && /http[^s]/.test(protocol) ? 'http' : 'https',
});
}
}
return repositoryUrl;
+8 -9
View File
@@ -2,7 +2,7 @@ const {escapeRegExp, template} = require('lodash');
const semver = require('semver');
const pLocate = require('p-locate');
const debug = require('debug')('semantic-release:get-last-release');
const {gitTags, isRefInHistory, gitTagHead} = require('./git');
const {getTags, isRefInHistory, getTagHead} = require('./git');
/**
* Last release.
@@ -20,18 +20,17 @@ const {gitTags, isRefInHistory, gitTagHead} = require('./git');
* - Sort the versions
* - Retrive the highest version
*
* @param {String} tagFormat Git tag format.
* @param {Object} logger Global logger.
* @param {Object} context semantic-release context.
*
* @return {Promise<LastRelease>} The last tagged release or `undefined` if none is found.
*/
module.exports = async (tagFormat, logger) => {
module.exports = async ({cwd, env, options: {tagFormat}, logger}) => {
// Generate a regex to parse tags formatted with `tagFormat`
// by replacing the `version` variable in the template by `(.+)`.
// The `tagFormat` is compiled with space as the `version` as it's an invalid tag character,
// so it's guaranteed to no be present in the `tagFormat`.
const tagRegexp = `^${escapeRegExp(template(tagFormat)({version: ' '})).replace(' ', '(.+)')}`;
const tags = (await gitTags())
const tags = (await getTags({cwd, env}))
.map(tag => ({gitTag: tag, version: (tag.match(tagRegexp) || new Array(2))[1]}))
.filter(
tag => tag.version && semver.valid(semver.clean(tag.version)) && !semver.prerelease(semver.clean(tag.version))
@@ -40,11 +39,11 @@ module.exports = async (tagFormat, logger) => {
debug('found tags: %o', tags);
const tag = await pLocate(tags, tag => isRefInHistory(tag.gitTag), {concurrency: 1, preserveOrder: true});
const tag = await pLocate(tags, tag => isRefInHistory(tag.gitTag, {cwd, env}), {preserveOrder: true});
if (tag) {
logger.log('Found git tag %s associated with version %s', tag.gitTag, tag.version);
return {gitHead: await gitTagHead(tag.gitTag), ...tag};
logger.log(`Found git tag ${tag.gitTag} associated with version ${tag.version}`);
return {gitHead: await getTagHead(tag.gitTag, {cwd, env}), ...tag};
}
logger.log('No git tag version found');
+16
View File
@@ -0,0 +1,16 @@
const {Signale} = require('signale');
const figures = require('figures');
module.exports = ({stdout, stderr}) =>
new Signale({
config: {displayTimestamp: true, underlineMessage: false, displayLabel: false},
disabled: false,
interactive: false,
scope: 'semantic-release',
stream: [stdout],
types: {
error: {badge: figures.cross, color: 'red', label: '', stream: [stderr]},
log: {badge: figures.info, color: 'magenta', label: '', stream: [stdout]},
success: {badge: figures.tick, color: 'green', label: '', stream: [stdout]},
},
});
+3 -3
View File
@@ -1,14 +1,14 @@
const semver = require('semver');
const {FIRST_RELEASE} = require('./definitions/constants');
module.exports = (type, lastRelease, logger) => {
module.exports = ({nextRelease: {type}, lastRelease, logger}) => {
let version;
if (lastRelease.version) {
version = semver.inc(lastRelease.version, type);
logger.log('The next release version is %s', version);
logger.log(`The next release version is ${version}`);
} else {
version = FIRST_RELEASE;
logger.log('There is no previous release, the next release version is %s', version);
logger.log(`There is no previous release, the next release version is ${version}`);
}
return version;
+96 -61
View File
@@ -4,85 +4,108 @@ const debug = require('debug')('semantic-release:git');
/**
* Get the commit sha for a given tag.
*
* @param {string} tagName Tag name for which to retrieve the commit sha.
* @param {String} tagName Tag name for which to retrieve the commit sha.
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @return {string} The commit sha of the tag in parameter or `null`.
*/
async function gitTagHead(tagName) {
async function getTagHead(tagName, execaOpts) {
try {
return await execa.stdout('git', ['rev-list', '-1', tagName]);
} catch (err) {
debug(err);
return await execa.stdout('git', ['rev-list', '-1', tagName], execaOpts);
} catch (error) {
debug(error);
}
}
/**
* Get all the repository tags.
*
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @return {Array<String>} List of git tags.
* @throws {Error} If the `git` command fails.
*/
async function gitTags() {
return (await execa.stdout('git', ['tag']))
async function getTags(execaOpts) {
return (await execa.stdout('git', ['tag'], execaOpts))
.split('\n')
.map(tag => tag.trim())
.filter(tag => Boolean(tag));
.filter(Boolean);
}
/**
* Verify if the `ref` is in the direct history of the current branch.
*
* @param {string} ref The reference to look for.
* @param {String} ref The reference to look for.
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @return {boolean} `true` if the reference is in the history of the current branch, falsy otherwise.
* @return {Boolean} `true` if the reference is in the history of the current branch, falsy otherwise.
*/
async function isRefInHistory(ref) {
async function isRefInHistory(ref, execaOpts) {
try {
await execa('git', ['merge-base', '--is-ancestor', ref, 'HEAD']);
await execa('git', ['merge-base', '--is-ancestor', ref, 'HEAD'], execaOpts);
return true;
} catch (err) {
if (err.code === 1) {
} catch (error) {
if (error.code === 1) {
return false;
}
debug(err);
throw err;
debug(error);
throw error;
}
}
/**
* Unshallow the git repository (retriving every commits and tags).
* Unshallow the git repository if necessary and fetch all the tags.
*
* @param {String} repositoryUrl The remote repository URL.
* @param {Object} [execaOpts] Options to pass to `execa`.
*/
async function unshallow(repositoryUrl) {
await execa('git', ['fetch', '--unshallow', '--tags', repositoryUrl], {reject: false});
}
/**
* @return {string} the sha of the HEAD commit.
*/
async function gitHead() {
return execa.stdout('git', ['rev-parse', 'HEAD']);
}
/**
* @return {string} The value of the remote git URL.
*/
async function repoUrl() {
async function fetch(repositoryUrl, execaOpts) {
try {
return await execa.stdout('git', ['remote', 'get-url', 'origin']);
} catch (err) {
debug(err);
await execa('git', ['fetch', '--unshallow', '--tags', repositoryUrl], execaOpts);
} catch (error) {
await execa('git', ['fetch', '--tags', repositoryUrl], execaOpts);
}
}
/**
* Get the HEAD sha.
*
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @return {String} the sha of the HEAD commit.
*/
function getGitHead(execaOpts) {
return execa.stdout('git', ['rev-parse', 'HEAD'], execaOpts);
}
/**
* Get the repository remote URL.
*
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @return {string} The value of the remote git URL.
*/
async function repoUrl(execaOpts) {
try {
return await execa.stdout('git', ['config', '--get', 'remote.origin.url'], execaOpts);
} catch (error) {
debug(error);
}
}
/**
* Test if the current working directory is a Git repository.
*
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @return {Boolean} `true` if the current working directory is in a git repository, falsy otherwise.
*/
async function isGitRepo() {
async function isGitRepo(execaOpts) {
try {
return (await execa('git', ['rev-parse', '--git-dir'])).code === 0;
} catch (err) {
debug(err);
return (await execa('git', ['rev-parse', '--git-dir'], execaOpts)).code === 0;
} catch (error) {
debug(error);
}
}
@@ -91,15 +114,16 @@ async function isGitRepo() {
*
* @param {String} repositoryUrl The remote repository URL.
* @param {String} branch The repositoru branch for which to verify write access.
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @throws {Error} if not authorized to push.
*/
async function verifyAuth(repositoryUrl, branch) {
async function verifyAuth(repositoryUrl, branch, execaOpts) {
try {
await execa('git', ['push', '--dry-run', repositoryUrl, `HEAD:${branch}`]);
} catch (err) {
debug(err);
throw err;
await execa('git', ['push', '--dry-run', repositoryUrl, `HEAD:${branch}`], execaOpts);
} catch (error) {
debug(error);
throw error;
}
}
@@ -107,34 +131,39 @@ async function verifyAuth(repositoryUrl, branch) {
* Tag the commit head on the local repository.
*
* @param {String} tagName The name of the tag.
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @throws {Error} if the tag creation failed.
*/
async function tag(tagName) {
await execa('git', ['tag', tagName]);
async function tag(tagName, execaOpts) {
await execa('git', ['tag', tagName], execaOpts);
}
/**
* Push to the remote repository.
*
* @param {String} repositoryUrl The remote repository URL.
* @param {String} branch The branch to push.
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @throws {Error} if the push failed.
*/
async function push(repositoryUrl, branch) {
await execa('git', ['push', '--tags', repositoryUrl, `HEAD:${branch}`]);
async function push(repositoryUrl, execaOpts) {
await execa('git', ['push', '--tags', repositoryUrl], execaOpts);
}
/**
* Verify a tag name is a valid Git reference.
*
* @param {string} tagName the tag name to verify.
* @return {boolean} `true` if valid, falsy otherwise.
* @param {String} tagName the tag name to verify.
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @return {Boolean} `true` if valid, falsy otherwise.
*/
async function verifyTagName(tagName) {
async function verifyTagName(tagName, execaOpts) {
try {
return (await execa('git', ['check-ref-format', `refs/tags/${tagName}`])).code === 0;
} catch (err) {
debug(err);
return (await execa('git', ['check-ref-format', `refs/tags/${tagName}`], execaOpts)).code === 0;
} catch (error) {
debug(error);
}
}
@@ -142,19 +171,25 @@ async function verifyTagName(tagName) {
* Verify the local branch is up to date with the remote one.
*
* @param {String} branch The repository branch for which to verify status.
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @return {Boolean} `true` is the HEAD of the current local branch is the same as the HEAD of the remote branch, falsy otherwise.
*/
async function isBranchUpToDate(branch) {
return isRefInHistory(await execa.stdout('git', ['rev-parse', `origin/${branch}`]));
async function isBranchUpToDate(branch, execaOpts) {
const remoteHead = await execa.stdout('git', ['ls-remote', '--heads', 'origin', branch], execaOpts);
try {
return await isRefInHistory(remoteHead.match(/^(\w+)?/)[1], execaOpts);
} catch (error) {
debug(error);
}
}
module.exports = {
gitTagHead,
gitTags,
getTagHead,
getTags,
isRefInHistory,
unshallow,
gitHead,
fetch,
getGitHead,
repoUrl,
isGitRepo,
verifyAuth,
+9 -8
View File
@@ -1,11 +1,12 @@
const {escapeRegExp} = require('lodash');
const {escapeRegExp, size, isString} = require('lodash');
const {SECRET_REPLACEMENT, SECRET_MIN_SIZE} = require('./definitions/constants');
const toReplace = Object.keys(process.env).filter(
envVar => /token|password|credential|secret|private/i.test(envVar) && process.env[envVar].trim()
);
module.exports = env => {
const toReplace = Object.keys(env).filter(
envVar => /token|password|credential|secret|private/i.test(envVar) && size(env[envVar].trim()) >= SECRET_MIN_SIZE
);
const regexp = new RegExp(toReplace.map(envVar => escapeRegExp(process.env[envVar])).join('|'), 'g');
module.exports = output => {
return output && toReplace.length > 0 ? output.toString().replace(regexp, '[secure]') : output;
const regexp = new RegExp(toReplace.map(envVar => escapeRegExp(env[envVar])).join('|'), 'g');
return output =>
output && isString(output) && toReplace.length > 0 ? output.toString().replace(regexp, SECRET_REPLACEMENT) : output;
};
-23
View File
@@ -1,23 +0,0 @@
const chalk = require('chalk');
/**
* Logger with `log` and `error` function.
*/
module.exports = {
log(...args) {
const [format, ...rest] = args;
console.log(
`${chalk.grey('[Semantic release]:')}${
typeof format === 'string' ? ` ${format.replace(/%[^%]/g, seq => chalk.magenta(seq))}` : ''
}`,
...(typeof format === 'string' ? [] : [format]).concat(rest)
);
},
error(...args) {
const [format, ...rest] = args;
console.error(
`${chalk.grey('[Semantic release]:')}${typeof format === 'string' ? ` ${chalk.red(format)}` : ''}`,
...(typeof format === 'string' ? [] : [format]).concat(rest)
);
},
};
+77 -26
View File
@@ -1,40 +1,91 @@
const {isArray, isObject, omit, castArray, isUndefined} = require('lodash');
const {identity, isPlainObject, omit, castArray, isNil, isString} = require('lodash');
const AggregateError = require('aggregate-error');
const getError = require('../get-error');
const PLUGINS_DEFINITIONS = require('../definitions/plugins');
const {validatePlugin, validateStep, loadPlugin, parseConfig} = require('./utils');
const pipeline = require('./pipeline');
const normalize = require('./normalize');
module.exports = (options, pluginsPath, logger) => {
module.exports = (context, pluginsPath) => {
let {options, logger} = context;
const errors = [];
const plugins = Object.keys(PLUGINS_DEFINITIONS).reduce((plugins, pluginType) => {
const {config, default: def} = PLUGINS_DEFINITIONS[pluginType];
let pluginConfs;
if (isUndefined(options[pluginType])) {
pluginConfs = def;
} else {
// If an object is passed and the path is missing, set the default one for single plugins
if (isObject(options[pluginType]) && !options[pluginType].path && !isArray(def)) {
options[pluginType].path = def;
}
if (config && !config.validator(options[pluginType])) {
errors.push(getError('EPLUGINCONF', {pluginType, pluginConf: options[pluginType]}));
const plugins = options.plugins
? castArray(options.plugins).reduce((plugins, plugin) => {
if (validatePlugin(plugin)) {
const [name, config] = parseConfig(plugin);
plugin = isString(name) ? loadPlugin(context, name, pluginsPath) : name;
if (isPlainObject(plugin)) {
Object.entries(plugin).forEach(([type, func]) => {
if (PLUGINS_DEFINITIONS[type]) {
Reflect.defineProperty(func, 'pluginName', {
value: isPlainObject(name) ? 'Inline plugin' : name,
writable: false,
enumerable: true,
});
plugins[type] = [...(plugins[type] || []), [func, config]];
}
});
} else {
errors.push(getError('EPLUGINSCONF', {plugin}));
}
} else {
errors.push(getError('EPLUGINSCONF', {plugin}));
}
return plugins;
}
pluginConfs = options[pluginType];
}
}, {})
: [];
const globalOpts = omit(options, Object.keys(PLUGINS_DEFINITIONS));
plugins[pluginType] = pipeline(
castArray(pluginConfs).map(conf => normalize(pluginType, pluginsPath, globalOpts, conf, logger))
);
return plugins;
}, {});
if (errors.length > 0) {
throw new AggregateError(errors);
}
return plugins;
options = {...plugins, ...options};
const pluginsConf = Object.entries(PLUGINS_DEFINITIONS).reduce(
(pluginsConf, [type, {required, default: def, pipelineConfig, postprocess = identity, preprocess = identity}]) => {
let pluginOpts;
if (isNil(options[type]) && def) {
pluginOpts = def;
} else {
// If an object is passed and the path is missing, merge it with step options
if (isPlainObject(options[type]) && !options[type].path) {
options[type] = castArray(plugins[type]).map(plugin =>
plugin ? [plugin[0], Object.assign(plugin[1], options[type])] : plugin
);
}
if (!validateStep({required}, options[type])) {
errors.push(getError('EPLUGINCONF', {type, required, pluginConf: options[type]}));
return pluginsConf;
}
pluginOpts = options[type];
}
const steps = castArray(pluginOpts).map(pluginOpt =>
normalize(
{...context, options: omit(options, Object.keys(PLUGINS_DEFINITIONS), 'plugins')},
type,
pluginOpt,
pluginsPath
)
);
pluginsConf[type] = async input =>
postprocess(
await pipeline(steps, pipelineConfig && pipelineConfig(pluginsConf, logger))(await preprocess(input)),
input
);
return pluginsConf;
},
plugins
);
if (errors.length > 0) {
throw new AggregateError(errors);
}
return pluginsConf;
};
+42 -33
View File
@@ -1,56 +1,65 @@
const {dirname} = require('path');
const {isString, isPlainObject, isFunction, noop, cloneDeep} = require('lodash');
const resolveFrom = require('resolve-from');
const {isPlainObject, isFunction, noop, cloneDeep, omit} = require('lodash');
const debug = require('debug')('semantic-release:plugins');
const getError = require('../get-error');
const {extractErrors} = require('../utils');
const PLUGINS_DEFINITIONS = require('../definitions/plugins');
const {loadPlugin, parseConfig} = require('./utils');
module.exports = (pluginType, pluginsPath, globalOpts, pluginOpts, logger) => {
if (!pluginOpts) {
module.exports = (context, type, pluginOpt, pluginsPath) => {
const {stdout, stderr, options, logger} = context;
if (!pluginOpt) {
return noop;
}
const {path, ...config} = isString(pluginOpts) || isFunction(pluginOpts) ? {path: pluginOpts} : pluginOpts;
const pluginName = isFunction(path) ? `[Function: ${path.name}]` : path;
const [name, config] = parseConfig(pluginOpt);
const pluginName = name.pluginName ? name.pluginName : isFunction(name) ? `[Function: ${name.name}]` : name;
const plugin = loadPlugin(context, name, pluginsPath);
if (!isFunction(pluginOpts)) {
if (pluginsPath[path]) {
logger.log('Load plugin %s from %s in shareable config %s', pluginType, path, pluginsPath[path]);
} else {
logger.log('Load plugin %s from %s', pluginType, path);
}
}
const basePath = pluginsPath[path]
? dirname(resolveFrom.silent(__dirname, pluginsPath[path]) || resolveFrom(process.cwd(), pluginsPath[path]))
: __dirname;
const plugin = isFunction(path)
? path
: require(resolveFrom.silent(basePath, path) || resolveFrom(process.cwd(), path));
debug(`options for ${pluginName}/${type}: %O`, config);
let func;
if (isFunction(plugin)) {
func = plugin.bind(null, cloneDeep({...globalOpts, ...config}));
} else if (isPlainObject(plugin) && plugin[pluginType] && isFunction(plugin[pluginType])) {
func = plugin[pluginType].bind(null, cloneDeep({...globalOpts, ...config}));
func = plugin.bind(null, cloneDeep({...options, ...config}));
} else if (isPlainObject(plugin) && plugin[type] && isFunction(plugin[type])) {
func = plugin[type].bind(null, cloneDeep({...options, ...config}));
} else {
throw getError('EPLUGIN', {pluginType, pluginName});
throw getError('EPLUGIN', {type, pluginName});
}
const validator = async input => {
const definition = PLUGINS_DEFINITIONS[pluginType];
const {dryRun, outputValidator} = PLUGINS_DEFINITIONS[type] || {};
try {
const result = await func(cloneDeep(input));
if (definition && definition.output && !definition.output.validator(result)) {
throw getError(PLUGINS_DEFINITIONS[pluginType].output.error, {result, pluginName});
if (!input.options.dryRun || dryRun) {
logger.log(`Start step "${type}" of plugin "${pluginName}"`);
const result = await func({
...cloneDeep(omit(input, ['stdout', 'stderr', 'logger'])),
stdout,
stderr,
logger: logger.scope(logger.scopeName, pluginName),
});
if (outputValidator && !outputValidator(result)) {
throw getError(`E${type.toUpperCase()}OUTPUT`, {result, pluginName});
}
logger.success(`Completed step "${type}" of plugin "${pluginName}"`);
return result;
}
return result;
} catch (err) {
extractErrors(err).forEach(err => Object.assign(err, {pluginName}));
throw err;
logger.warn(`Skip step "${type}" of plugin "${pluginName}" in dry-run mode`);
} catch (error) {
logger.error(`Failed step "${type}" of plugin "${pluginName}"`);
extractErrors(error).forEach(err => Object.assign(err, {pluginName}));
throw error;
}
};
Reflect.defineProperty(validator, 'pluginName', {value: pluginName, writable: false, enumerable: true});
if (!isFunction(pluginOpt)) {
if (pluginsPath[name]) {
logger.success(`Loaded plugin "${type}" from "${pluginName}" in shareable config "${pluginsPath[name]}"`);
} else {
logger.success(`Loaded plugin "${type}" from "${pluginName}"`);
}
}
return validator;
};
+16 -15
View File
@@ -8,10 +8,6 @@ const {extractErrors} = require('../utils');
*
* @typedef {Function} Pipeline
* @param {Any} input Argument to pass to the first step in the pipeline.
* @param {Object} options Pipeline options.
* @param {Boolean} [options.settleAll=false] If `true` all the steps in the pipeline are executed, even if one rejects, if `false` the execution stops after a steps rejects.
* @param {Function} [options.getNextInput=identity] Function called after each step is executed, with the last and current step results; the returned value will be used as the argument of the next step.
* @param {Function} [options.transform=identity] Function called after each step is executed, with the current step result and the step function; the returned value will be saved in the pipeline results.
*
* @return {Array<*>|*} An Array with the result of each step in the pipeline; if there is only 1 step in the pipeline, the result of this step is returned directly.
*
@@ -22,34 +18,39 @@ const {extractErrors} = require('../utils');
* Create a Pipeline with a list of Functions.
*
* @param {Array<Function>} steps The list of Function to execute.
* @param {Object} options Pipeline options.
* @param {Boolean} [options.settleAll=false] If `true` all the steps in the pipeline are executed, even if one rejects, if `false` the execution stops after a steps rejects.
* @param {Function} [options.getNextInput=identity] Function called after each step is executed, with the last step input and the current current step result; the returned value will be used as the input of the next step.
* @param {Function} [options.transform=identity] Function called after each step is executed, with the current step result, the step function and the last step input; the returned value will be saved in the pipeline results.
*
* @return {Pipeline} A Function that execute the `steps` sequencially
*/
module.exports = steps => async (input, {settleAll = false, getNextInput = identity, transform = identity} = {}) => {
module.exports = (steps, {settleAll = false, getNextInput = identity, transform = identity} = {}) => async input => {
const results = [];
const errors = [];
await pReduce(
steps,
async (lastResult, step) => {
async (lastInput, step) => {
let result;
try {
// Call the step with the input computed at the end of the previous iteration and save intermediary result
result = await transform(await step(lastResult), step);
result = await transform(await step(lastInput), step, lastInput);
results.push(result);
} catch (err) {
} catch (error) {
if (settleAll) {
errors.push(...extractErrors(err));
result = err;
errors.push(...extractErrors(error));
result = error;
} else {
throw err;
throw error;
}
}
// Prepare input for the next step, passing the result of the last iteration (or initial parameter for the first iteration) and the current one
return getNextInput(lastResult, result);
// Prepare input for the next step, passing the input of the last iteration (or initial parameter for the first iteration) and the result of the current one
return getNextInput(lastInput, result);
},
input
);
if (errors.length > 0) {
throw errors.length === 1 ? errors[0] : new AggregateError(errors);
throw new AggregateError(errors);
}
return results.length <= 1 ? results[0] : results;
return results;
};
+65
View File
@@ -0,0 +1,65 @@
const {dirname} = require('path');
const {isString, isFunction, castArray, isArray, isPlainObject, isNil} = require('lodash');
const resolveFrom = require('resolve-from');
const validateSteps = conf => {
return conf.every(conf => {
if (
isArray(conf) &&
(conf.length === 1 || conf.length === 2) &&
(isString(conf[0]) || isFunction(conf[0])) &&
(isNil(conf[1]) || isPlainObject(conf[1]))
) {
return true;
}
conf = castArray(conf);
if (conf.length !== 1) {
return false;
}
const [name, config] = parseConfig(conf[0]);
return (isString(name) || isFunction(name)) && isPlainObject(config);
});
};
function validatePlugin(conf) {
return (
isString(conf) ||
(isArray(conf) &&
(conf.length === 1 || conf.length === 2) &&
(isString(conf[0]) || isPlainObject(conf[0])) &&
(isNil(conf[1]) || isPlainObject(conf[1]))) ||
(isPlainObject(conf) && (isNil(conf.path) || isString(conf.path) || isPlainObject(conf.path)))
);
}
function validateStep({required}, conf) {
conf = castArray(conf).filter(Boolean);
if (required) {
return conf.length >= 1 && validateSteps(conf);
}
return conf.length === 0 || validateSteps(conf);
}
function loadPlugin({cwd}, name, pluginsPath) {
const basePath = pluginsPath[name]
? dirname(resolveFrom.silent(__dirname, pluginsPath[name]) || resolveFrom(cwd, pluginsPath[name]))
: __dirname;
return isFunction(name) ? name : require(resolveFrom.silent(basePath, name) || resolveFrom(cwd, name));
}
function parseConfig(plugin) {
let path;
let config;
if (isArray(plugin)) {
[path, config] = plugin;
} else if (isPlainObject(plugin) && !isNil(plugin.path)) {
({path, ...config} = plugin);
} else {
path = plugin;
}
return [path, config || {}];
}
module.exports = {validatePlugin, validateStep, loadPlugin, parseConfig};
+14 -1
View File
@@ -1,7 +1,20 @@
const {isFunction} = require('lodash');
const hideSensitive = require('./hide-sensitive');
function extractErrors(err) {
return err && isFunction(err[Symbol.iterator]) ? [...err] : [err];
}
module.exports = {extractErrors};
function hideSensitiveValues(env, objs) {
const hideFunction = hideSensitive(env);
return objs.map(obj => {
Object.getOwnPropertyNames(obj).forEach(prop => {
if (obj[prop]) {
obj[prop] = hideFunction(obj[prop]);
}
});
return obj;
});
}
module.exports = {extractErrors, hideSensitiveValues};
+8 -8
View File
@@ -3,25 +3,25 @@ const AggregateError = require('aggregate-error');
const {isGitRepo, verifyTagName} = require('./git');
const getError = require('./get-error');
module.exports = async options => {
module.exports = async ({cwd, env, options: {repositoryUrl, tagFormat}}) => {
const errors = [];
if (!(await isGitRepo())) {
errors.push(getError('ENOGITREPO'));
} else if (!options.repositoryUrl) {
if (!(await isGitRepo({cwd, env}))) {
errors.push(getError('ENOGITREPO', {cwd}));
} else if (!repositoryUrl) {
errors.push(getError('ENOREPOURL'));
}
// Verify that compiling the `tagFormat` produce a valid Git tag
if (!(await verifyTagName(template(options.tagFormat)({version: '0.0.0'})))) {
errors.push(getError('EINVALIDTAGFORMAT', {tagFormat: options.tagFormat}));
if (!(await verifyTagName(template(tagFormat)({version: '0.0.0'})))) {
errors.push(getError('EINVALIDTAGFORMAT', {tagFormat}));
}
// Verify the `tagFormat` contains the variable `version` by compiling the `tagFormat` template
// with a space as the `version` value and verify the result contains the space.
// The space is used as it's an invalid tag character, so it's guaranteed to no be present in the `tagFormat`.
if ((template(options.tagFormat)({version: ' '}).match(/ /g) || []).length !== 1) {
errors.push(getError('ETAGNOVERSION', {tagFormat: options.tagFormat}));
if ((template(tagFormat)({version: ' '}).match(/ /g) || []).length !== 1) {
errors.push(getError('ETAGNOVERSION', {tagFormat}));
}
if (errors.length > 0) {
+31 -32
View File
@@ -19,52 +19,54 @@
"Pierre Vanduynslager (https://twitter.com/@pvdlg_)"
],
"dependencies": {
"@semantic-release/commit-analyzer": "^5.0.0",
"@semantic-release/commit-analyzer": "^6.1.0",
"@semantic-release/error": "^2.2.0",
"@semantic-release/github": "^4.1.0",
"@semantic-release/npm": "^3.2.0",
"@semantic-release/release-notes-generator": "^6.0.0",
"aggregate-error": "^1.0.0",
"chalk": "^2.3.0",
"@semantic-release/github": "^5.1.0",
"@semantic-release/npm": "^5.0.5",
"@semantic-release/release-notes-generator": "^7.1.2",
"aggregate-error": "^2.0.0",
"cosmiconfig": "^5.0.1",
"debug": "^3.1.0",
"env-ci": "^2.0.0",
"execa": "^0.10.0",
"get-stream": "^3.0.0",
"debug": "^4.0.0",
"env-ci": "^3.0.0",
"execa": "^1.0.0",
"figures": "^2.0.0",
"find-versions": "^3.0.0",
"get-stream": "^4.0.0",
"git-log-parser": "^1.2.0",
"git-url-parse": "^9.0.0",
"hook-std": "^0.4.0",
"hosted-git-info": "^2.6.0",
"hook-std": "^1.1.0",
"hosted-git-info": "^2.7.1",
"lodash": "^4.17.4",
"marked": "^0.3.9",
"marked-terminal": "^2.0.0",
"p-locate": "^2.0.0",
"marked": "^0.6.0",
"marked-terminal": "^3.2.0",
"p-locate": "^3.0.0",
"p-reduce": "^1.0.0",
"read-pkg-up": "^3.0.0",
"read-pkg-up": "^4.0.0",
"resolve-from": "^4.0.0",
"semver": "^5.4.1",
"yargs": "^11.0.0"
"signale": "^1.2.1",
"yargs": "^12.0.0"
},
"devDependencies": {
"ava": "^0.25.0",
"clear-module": "^2.1.0",
"ava": "^1.0.1",
"clear-module": "^3.0.0",
"codecov": "^3.0.0",
"commitizen": "^2.9.6",
"commitizen": "^3.0.0",
"cz-conventional-changelog": "^2.0.0",
"delay": "^2.0.0",
"delay": "^4.0.0",
"dockerode": "^2.5.2",
"file-url": "^2.0.2",
"fs-extra": "^6.0.0",
"got": "^8.0.0",
"fs-extra": "^7.0.0",
"got": "^9.0.0",
"js-yaml": "^3.10.0",
"mockserver-client": "^5.1.1",
"nock": "^9.0.2",
"nyc": "^11.2.1",
"p-retry": "^2.0.0",
"nock": "^10.0.0",
"nyc": "^13.1.0",
"p-retry": "^3.0.0",
"proxyquire": "^2.0.0",
"sinon": "^5.0.1",
"sinon": "^7.1.1",
"stream-buffers": "^3.0.2",
"tempy": "^0.2.1",
"xo": "^0.21.0"
"xo": "^0.23.0"
},
"engines": {
"node": ">=8.3"
@@ -110,9 +112,6 @@
"publishConfig": {
"tag": "next"
},
"release": {
"branch": "caribou"
},
"repository": {
"type": "git",
"url": "git+https://github.com/semantic-release/semantic-release.git"
+67 -82
View File
@@ -1,14 +1,12 @@
import test from 'ava';
import {escapeRegExp} from 'lodash';
import proxyquire from 'proxyquire';
import clearModule from 'clear-module';
import {stub} from 'sinon';
import {SECRET_REPLACEMENT} from '../lib/definitions/constants';
// Save the current process.env and process.argv
const envBackup = Object.assign({}, process.env);
const argvBackup = Object.assign({}, process.argv);
const requireNoCache = proxyquire.noPreserveCache();
test.beforeEach(t => {
clearModule('yargs');
t.context.logs = '';
t.context.errors = '';
t.context.stdout = stub(process.stdout, 'write').callsFake(val => {
@@ -20,18 +18,13 @@ test.beforeEach(t => {
});
test.afterEach.always(t => {
process.env = envBackup;
process.argv = argvBackup;
t.context.stdout.restore();
t.context.stderr.restore();
delete process.exitCode;
});
test.serial('Pass options to semantic-release API', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
process.argv = [
const argv = [
'',
'',
'-b',
@@ -40,6 +33,9 @@ test.serial('Pass options to semantic-release API', async t => {
'https://github/com/owner/repo.git',
'-t',
`v\${version}`,
'-p',
'plugin1',
'plugin2',
'-e',
'config1',
'config2',
@@ -68,17 +64,19 @@ test.serial('Pass options to semantic-release API', async t => {
'--debug',
'-d',
];
const cli = requireNoCache('../cli', {'.': run, process: {...process, argv}});
await cli();
const exitCode = await cli();
t.is(run.args[0][0].branch, 'master');
t.is(run.args[0][0].repositoryUrl, 'https://github/com/owner/repo.git');
t.is(run.args[0][0].tagFormat, `v\${version}`);
t.deepEqual(run.args[0][0].plugins, ['plugin1', 'plugin2']);
t.deepEqual(run.args[0][0].extends, ['config1', 'config2']);
t.deepEqual(run.args[0][0].verifyConditions, ['condition1', 'condition2']);
t.is(run.args[0][0].analyzeCommits, 'analyze');
t.deepEqual(run.args[0][0].verifyRelease, ['verify1', 'verify2']);
t.is(run.args[0][0].generateNotes, 'notes');
t.deepEqual(run.args[0][0].generateNotes, ['notes']);
t.deepEqual(run.args[0][0].prepare, ['prepare1', 'prepare2']);
t.deepEqual(run.args[0][0].publish, ['publish1', 'publish2']);
t.deepEqual(run.args[0][0].success, ['success1', 'success2']);
@@ -86,14 +84,12 @@ test.serial('Pass options to semantic-release API', async t => {
t.is(run.args[0][0].debug, true);
t.is(run.args[0][0].dryRun, true);
t.is(process.exitCode, 0);
t.is(exitCode, 0);
});
test.serial('Pass options to semantic-release API with alias arguments', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
process.argv = [
const argv = [
'',
'',
'--branch',
@@ -102,136 +98,125 @@ test.serial('Pass options to semantic-release API with alias arguments', async t
'https://github/com/owner/repo.git',
'--tag-format',
`v\${version}`,
'--plugins',
'plugin1',
'plugin2',
'--extends',
'config1',
'config2',
'--dry-run',
];
const cli = requireNoCache('../cli', {'.': run, process: {...process, argv}});
await cli();
const exitCode = await cli();
t.is(run.args[0][0].branch, 'master');
t.is(run.args[0][0].repositoryUrl, 'https://github/com/owner/repo.git');
t.is(run.args[0][0].tagFormat, `v\${version}`);
t.deepEqual(run.args[0][0].plugins, ['plugin1', 'plugin2']);
t.deepEqual(run.args[0][0].extends, ['config1', 'config2']);
t.is(run.args[0][0].dryRun, true);
t.is(process.exitCode, 0);
t.is(exitCode, 0);
});
test.serial('Pass unknown options to semantic-release API', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
const argv = ['', '', '--bool', '--first-option', 'value1', '--second-option', 'value2', '--second-option', 'value3'];
const cli = requireNoCache('../cli', {'.': run, process: {...process, argv}});
process.argv = [
'',
'',
'--bool',
'--first-option',
'value1',
'--second-option',
'value2',
'--second-option',
'value3',
];
await cli();
const exitCode = await cli();
t.is(run.args[0][0].bool, true);
t.is(run.args[0][0].firstOption, 'value1');
t.deepEqual(run.args[0][0].secondOption, ['value2', 'value3']);
t.is(process.exitCode, 0);
t.is(exitCode, 0);
});
test.serial('Pass empty Array to semantic-release API for list option set to "false"', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
const argv = ['', '', '--publish', 'false'];
const cli = requireNoCache('../cli', {'.': run, process: {...process, argv}});
process.argv = ['', '', '--publish', 'false'];
await cli();
const exitCode = await cli();
t.deepEqual(run.args[0][0].publish, []);
t.is(process.exitCode, 0);
t.is(exitCode, 0);
});
test.serial('Do not set properties in option for which arg is not in command line', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
process.argv = ['', '', '-b', 'master'];
const argv = ['', '', '-b', 'master'];
const cli = requireNoCache('../cli', {'.': run, process: {...process, argv}});
await cli();
t.false(Reflect.apply(Object.prototype.hasOwnProperty, run.args[0][0], ['ci']));
t.false(Reflect.apply(Object.prototype.hasOwnProperty, run.args[0][0], ['d']));
t.false(Reflect.apply(Object.prototype.hasOwnProperty, run.args[0][0], ['dry-run']));
t.false(Reflect.apply(Object.prototype.hasOwnProperty, run.args[0][0], ['debug']));
t.false(Reflect.apply(Object.prototype.hasOwnProperty, run.args[0][0], ['r']));
t.false(Reflect.apply(Object.prototype.hasOwnProperty, run.args[0][0], ['t']));
});
test.serial('Set "noCi" options to "true" with "--no-ci"', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
process.argv = ['', '', '--no-ci'];
await cli();
t.is(run.args[0][0].noCi, true);
t.is(process.exitCode, 0);
t.false('ci' in run.args[0][0]);
t.false('d' in run.args[0][0]);
t.false('dry-run' in run.args[0][0]);
t.false('debug' in run.args[0][0]);
t.false('r' in run.args[0][0]);
t.false('t' in run.args[0][0]);
t.false('p' in run.args[0][0]);
t.false('e' in run.args[0][0]);
});
test.serial('Display help', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
const argv = ['', '', '--help'];
const cli = requireNoCache('../cli', {'.': run, process: {...process, argv}});
process.argv = ['', '', '--help'];
await cli();
const exitCode = await cli();
t.regex(t.context.logs, /Run automated package publishing/);
t.is(process.exitCode, 0);
t.is(exitCode, 0);
});
test.serial('Returns error code and prints help if called with a command', async t => {
test.serial('Return error code and prints help if called with a command', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
const argv = ['', '', 'pre'];
const cli = requireNoCache('../cli', {'.': run, process: {...process, argv}});
process.argv = ['', '', 'pre'];
await cli();
const exitCode = await cli();
t.regex(t.context.errors, /Run automated package publishing/);
t.regex(t.context.errors, /Too many non-option arguments/);
t.is(process.exitCode, 1);
t.is(exitCode, 1);
});
test.serial('Return error code if multiple plugin are set for single plugin', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
const argv = ['', '', '--analyze-commits', 'analyze1', 'analyze2'];
const cli = requireNoCache('../cli', {'.': run, process: {...process, argv}});
process.argv = ['', '', '--analyze-commits', 'analyze1', 'analyze2'];
await cli();
const exitCode = await cli();
t.regex(t.context.errors, /Run automated package publishing/);
t.regex(t.context.errors, /Too many non-option arguments/);
t.is(process.exitCode, 1);
t.is(exitCode, 1);
});
test.serial('Return error code if semantic-release throw error', async t => {
const run = stub().rejects(new Error('semantic-release error'));
const cli = proxyquire('../cli', {'.': run});
const argv = ['', ''];
const cli = requireNoCache('../cli', {'.': run, process: {...process, argv}});
process.argv = ['', ''];
await cli();
const exitCode = await cli();
t.regex(t.context.errors, /semantic-release error/);
t.is(process.exitCode, 1);
t.is(exitCode, 1);
});
test.serial('Hide sensitive environment variable values from the logs', async t => {
const env = {MY_TOKEN: 'secret token'};
const run = stub().rejects(new Error(`Throw error: Exposing token ${env.MY_TOKEN}`));
const argv = ['', ''];
const cli = requireNoCache('../cli', {'.': run, process: {...process, argv, env: {...process.env, ...env}}});
const exitCode = await cli();
t.regex(t.context.errors, new RegExp(`Throw error: Exposing token ${escapeRegExp(SECRET_REPLACEMENT)}`));
t.is(exitCode, 1);
});
+47 -116
View File
@@ -1,133 +1,64 @@
import test from 'ava';
import plugins from '../../lib/definitions/plugins';
import errors from '../../lib/definitions/errors';
test('The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition', t => {
t.false(plugins.verifyConditions.config.validator({}));
t.false(plugins.verifyConditions.config.validator({path: null}));
t.true(plugins.verifyConditions.config.validator({path: 'plugin-path.js'}));
t.true(plugins.verifyConditions.config.validator());
t.true(plugins.verifyConditions.config.validator('plugin-path.js'));
t.true(plugins.verifyConditions.config.validator(() => {}));
t.true(plugins.verifyConditions.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
});
test('The "analyzeCommits" plugin is mandatory, and must be a single plugin definition', t => {
t.false(plugins.analyzeCommits.config.validator({}));
t.false(plugins.analyzeCommits.config.validator({path: null}));
t.false(plugins.analyzeCommits.config.validator([]));
t.false(plugins.analyzeCommits.config.validator());
t.true(plugins.analyzeCommits.config.validator({path: 'plugin-path.js'}));
t.true(plugins.analyzeCommits.config.validator('plugin-path.js'));
t.true(plugins.analyzeCommits.config.validator(() => {}));
});
test('The "verifyRelease" plugin, if defined, must be a single or an array of plugins definition', t => {
t.false(plugins.verifyRelease.config.validator({}));
t.false(plugins.verifyRelease.config.validator({path: null}));
t.true(plugins.verifyRelease.config.validator({path: 'plugin-path.js'}));
t.true(plugins.verifyRelease.config.validator());
t.true(plugins.verifyRelease.config.validator('plugin-path.js'));
t.true(plugins.verifyRelease.config.validator(() => {}));
t.true(plugins.verifyRelease.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
});
test('The "generateNotes" plugin, if defined, must be a single plugin definition', t => {
t.false(plugins.generateNotes.config.validator({}));
t.false(plugins.generateNotes.config.validator({path: null}));
t.false(plugins.generateNotes.config.validator([]));
t.true(plugins.generateNotes.config.validator());
t.true(plugins.generateNotes.config.validator({path: 'plugin-path.js'}));
t.true(plugins.generateNotes.config.validator('plugin-path.js'));
t.true(plugins.generateNotes.config.validator(() => {}));
});
test('The "prepare" plugin, if defined, must be a single or an array of plugins definition', t => {
t.false(plugins.verifyRelease.config.validator({}));
t.false(plugins.verifyRelease.config.validator({path: null}));
t.true(plugins.verifyRelease.config.validator({path: 'plugin-path.js'}));
t.true(plugins.verifyRelease.config.validator());
t.true(plugins.verifyRelease.config.validator('plugin-path.js'));
t.true(plugins.verifyRelease.config.validator(() => {}));
t.true(plugins.verifyRelease.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
});
test('The "publish" plugin is mandatory, and must be a single or an array of plugins definition', t => {
t.false(plugins.publish.config.validator({}));
t.false(plugins.publish.config.validator({path: null}));
t.false(plugins.publish.config.validator());
t.true(plugins.publish.config.validator({path: 'plugin-path.js'}));
t.true(plugins.publish.config.validator('plugin-path.js'));
t.true(plugins.publish.config.validator(() => {}));
t.true(plugins.publish.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
});
test('The "success" plugin, if defined, must be a single or an array of plugins definition', t => {
t.false(plugins.success.config.validator({}));
t.false(plugins.success.config.validator({path: null}));
t.true(plugins.success.config.validator({path: 'plugin-path.js'}));
t.true(plugins.success.config.validator());
t.true(plugins.success.config.validator('plugin-path.js'));
t.true(plugins.success.config.validator(() => {}));
t.true(plugins.success.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
});
test('The "fail" plugin, if defined, must be a single or an array of plugins definition', t => {
t.false(plugins.fail.config.validator({}));
t.false(plugins.fail.config.validator({path: null}));
t.true(plugins.fail.config.validator({path: 'plugin-path.js'}));
t.true(plugins.fail.config.validator());
t.true(plugins.fail.config.validator('plugin-path.js'));
t.true(plugins.fail.config.validator(() => {}));
t.true(plugins.fail.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
});
import {RELEASE_NOTES_SEPARATOR, SECRET_REPLACEMENT} from '../../lib/definitions/constants';
test('The "analyzeCommits" plugin output must be either undefined or a valid semver release type', t => {
t.false(plugins.analyzeCommits.output.validator('invalid'));
t.false(plugins.analyzeCommits.output.validator(1));
t.false(plugins.analyzeCommits.output.validator({}));
t.false(plugins.analyzeCommits.outputValidator('invalid'));
t.false(plugins.analyzeCommits.outputValidator(1));
t.false(plugins.analyzeCommits.outputValidator({}));
t.true(plugins.analyzeCommits.output.validator());
t.true(plugins.analyzeCommits.output.validator(null));
t.true(plugins.analyzeCommits.output.validator('major'));
t.true(plugins.analyzeCommits.outputValidator());
t.true(plugins.analyzeCommits.outputValidator(null));
t.true(plugins.analyzeCommits.outputValidator('major'));
});
test('The "generateNotes" plugin output, if defined, must be a string', t => {
t.false(plugins.generateNotes.output.validator(1));
t.false(plugins.generateNotes.output.validator({}));
t.false(plugins.generateNotes.outputValidator(1));
t.false(plugins.generateNotes.outputValidator({}));
t.true(plugins.generateNotes.output.validator());
t.true(plugins.generateNotes.output.validator(null));
t.true(plugins.generateNotes.output.validator(''));
t.true(plugins.generateNotes.output.validator('string'));
t.true(plugins.generateNotes.outputValidator());
t.true(plugins.generateNotes.outputValidator(null));
t.true(plugins.generateNotes.outputValidator(''));
t.true(plugins.generateNotes.outputValidator('string'));
});
test('The "publish" plugin output, if defined, must be an object', t => {
t.false(plugins.publish.output.validator(1));
t.false(plugins.publish.output.validator('string'));
test('The "publish" plugin output, if defined, must be an object or "false"', t => {
t.false(plugins.publish.outputValidator(1));
t.false(plugins.publish.outputValidator('string'));
t.true(plugins.publish.output.validator({}));
t.true(plugins.publish.output.validator());
t.true(plugins.publish.output.validator(null));
t.true(plugins.publish.output.validator(''));
t.true(plugins.publish.outputValidator({}));
t.true(plugins.publish.outputValidator());
t.true(plugins.publish.outputValidator(null));
t.true(plugins.publish.outputValidator(''));
t.true(plugins.publish.outputValidator(false));
});
test('The "analyzeCommits" plugin output definition return an existing error code', t => {
t.true(Object.keys(errors).includes(plugins.analyzeCommits.output.error));
test('The "generateNotes" plugins output are concatenated with separator and sensitive data is hidden', t => {
const env = {MY_TOKEN: 'secret token'};
t.is(plugins.generateNotes.postprocess(['note 1', 'note 2'], {env}), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
t.is(plugins.generateNotes.postprocess(['', 'note'], {env}), 'note');
t.is(plugins.generateNotes.postprocess([undefined, 'note'], {env}), 'note');
t.is(plugins.generateNotes.postprocess(['note 1', '', 'note 2'], {env}), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
t.is(
plugins.generateNotes.postprocess(['note 1', undefined, 'note 2'], {env}),
`note 1${RELEASE_NOTES_SEPARATOR}note 2`
);
t.is(
plugins.generateNotes.postprocess(
[`Note 1: Exposing token ${env.MY_TOKEN}`, `Note 2: Exposing token ${SECRET_REPLACEMENT}`],
{env}
),
`Note 1: Exposing token ${SECRET_REPLACEMENT}${RELEASE_NOTES_SEPARATOR}Note 2: Exposing token ${SECRET_REPLACEMENT}`
);
});
test('The "generateNotes" plugin output definition return an existing error code', t => {
t.true(Object.keys(errors).includes(plugins.generateNotes.output.error));
});
test('The "publish" plugin output definition return an existing error code', t => {
t.true(Object.keys(errors).includes(plugins.publish.output.error));
test('The "analyzeCommits" plugins output are reduced to the highest release type', t => {
t.is(plugins.analyzeCommits.postprocess(['major', 'minor']), 'major');
t.is(plugins.analyzeCommits.postprocess(['', 'minor']), 'minor');
t.is(plugins.analyzeCommits.postprocess([undefined, 'patch']), 'patch');
t.is(plugins.analyzeCommits.postprocess([null, 'patch']), 'patch');
t.is(plugins.analyzeCommits.postprocess(['wrong_type', 'minor']), 'minor');
t.is(plugins.analyzeCommits.postprocess([]), undefined);
t.is(plugins.analyzeCommits.postprocess(['wrong_type']), undefined);
});
-1
View File
@@ -1,2 +1 @@
module.exports = () => {};
+1 -1
View File
@@ -1,7 +1,7 @@
const SemanticReleaseError = require('@semantic-release/error');
class InheritedError extends SemanticReleaseError {
constructor(message, code, newProperty) {
constructor(message, code) {
super(message);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
+1 -1
View File
@@ -1 +1 @@
module.exports = (pluginConfig, options) => options;
module.exports = (pluginConfig, context) => context;
+6
View File
@@ -0,0 +1,6 @@
module.exports = (pluginConfig, {env, logger}) => {
console.log(`Console: Exposing token ${env.MY_TOKEN}`);
logger.log(`Log: Exposing token ${env.MY_TOKEN}`);
logger.error(`Error: Console token ${env.MY_TOKEN}`);
throw new Error(`Throw error: Exposing ${env.MY_TOKEN}`);
};
+1 -1
View File
@@ -1 +1 @@
module.exports = (pluginConfig, options) => ({pluginConfig, options});
module.exports = (pluginConfig, context) => ({pluginConfig, context});
+32 -28
View File
@@ -3,9 +3,6 @@ import {stub} from 'sinon';
import getCommits from '../lib/get-commits';
import {gitRepo, gitCommits, gitDetachedHead} from './helpers/git-utils';
// Save the current working diretory
const cwd = process.cwd();
test.beforeEach(t => {
// Stub the logger functions
t.context.log = stub();
@@ -13,49 +10,52 @@ test.beforeEach(t => {
t.context.logger = {log: t.context.log, error: t.context.error};
});
test.afterEach.always(() => {
// Restore the current working directory
process.chdir(cwd);
});
test.serial('Get all commits when there is no last release', async t => {
test('Get all commits when there is no last release', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const {cwd} = await gitRepo();
// Add commits to the master branch
const commits = await gitCommits(['First', 'Second']);
const commits = await gitCommits(['First', 'Second'], {cwd});
// Retrieve the commits with the commits module
const result = await getCommits(undefined, 'master', t.context.logger);
const result = await getCommits({cwd, lastRelease: {}, logger: t.context.logger});
// Verify the commits created and retrieved by the module are identical
t.is(result.length, 2);
t.deepEqual(result, commits);
});
test.serial('Get all commits since gitHead (from lastRelease)', async t => {
test('Get all commits since gitHead (from lastRelease)', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const {cwd} = await gitRepo();
// Add commits to the master branch
const commits = await gitCommits(['First', 'Second', 'Third']);
const commits = await gitCommits(['First', 'Second', 'Third'], {cwd});
// Retrieve the commits with the commits module, since commit 'First'
const result = await getCommits(commits[commits.length - 1].hash, 'master', t.context.logger);
const result = await getCommits({
cwd,
lastRelease: {gitHead: commits[commits.length - 1].hash},
logger: t.context.logger,
});
// Verify the commits created and retrieved by the module are identical
t.is(result.length, 2);
t.deepEqual(result, commits.slice(0, 2));
});
test.serial('Get all commits since gitHead (from lastRelease) on a detached head repo', async t => {
test('Get all commits since gitHead (from lastRelease) on a detached head repo', async t => {
// Create a git repository, set the current working directory at the root of the repo
const repo = await gitRepo();
let {cwd, repositoryUrl} = await gitRepo();
// Add commits to the master branch
const commits = await gitCommits(['First', 'Second', 'Third']);
const commits = await gitCommits(['First', 'Second', 'Third'], {cwd});
// Create a detached head repo at commit 'feat: Second'
await gitDetachedHead(repo, commits[1].hash);
cwd = await gitDetachedHead(repositoryUrl, commits[1].hash);
// Retrieve the commits with the commits module, since commit 'First'
const result = await getCommits(commits[commits.length - 1].hash, 'master', t.context.logger);
const result = await getCommits({
cwd,
lastRelease: {gitHead: commits[commits.length - 1].hash},
logger: t.context.logger,
});
// Verify the module retrieved only the commit 'feat: Second' (included in the detached and after 'fix: First')
t.is(result.length, 1);
@@ -66,25 +66,29 @@ test.serial('Get all commits since gitHead (from lastRelease) on a detached head
t.truthy(result[0].committer.name);
});
test.serial('Return empty array if lastRelease.gitHead is the last commit', async t => {
test('Return empty array if lastRelease.gitHead is the last commit', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const {cwd} = await gitRepo();
// Add commits to the master branch
const commits = await gitCommits(['First', 'Second']);
const commits = await gitCommits(['First', 'Second'], {cwd});
// Retrieve the commits with the commits module, since commit 'Second' (therefore none)
const result = await getCommits(commits[0].hash, 'master', t.context.logger);
const result = await getCommits({
cwd,
lastRelease: {gitHead: commits[0].hash},
logger: t.context.logger,
});
// Verify no commit is retrieved
t.deepEqual(result, []);
});
test.serial('Return empty array if there is no commits', async t => {
test('Return empty array if there is no commits', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const {cwd} = await gitRepo();
// Retrieve the commits with the commits module
const result = await getCommits(undefined, 'master', t.context.logger);
const result = await getCommits({cwd, lastRelease: {}, logger: t.context.logger});
// Verify no commit is retrieved
t.deepEqual(result, []);
+210 -192
View File
@@ -1,3 +1,4 @@
import path from 'path';
import {format} from 'util';
import test from 'ava';
import {writeFile, outputJson} from 'fs-extra';
@@ -7,184 +8,190 @@ import {stub} from 'sinon';
import yaml from 'js-yaml';
import {gitRepo, gitCommits, gitShallowClone, gitAddConfig} from './helpers/git-utils';
// Save the current process.env
const envBackup = Object.assign({}, process.env);
// Save the current working diretory
const cwd = process.cwd();
const DEFAULT_PLUGINS = [
'@semantic-release/commit-analyzer',
'@semantic-release/release-notes-generator',
'@semantic-release/npm',
'@semantic-release/github',
];
test.beforeEach(t => {
delete process.env.GIT_CREDENTIALS;
delete process.env.GH_TOKEN;
delete process.env.GITHUB_TOKEN;
delete process.env.GL_TOKEN;
delete process.env.GITLAB_TOKEN;
// Delete environment variables that could have been set on the machine running the tests
t.context.plugins = stub().returns({});
t.context.getConfig = proxyquire('../lib/get-config', {'./plugins': t.context.plugins});
});
test.afterEach.always(() => {
// Restore process.env
process.env = envBackup;
// Restore the current working directory
process.chdir(cwd);
});
test.serial('Default values, reading repositoryUrl from package.json', async t => {
test('Default values, reading repositoryUrl from package.json', async t => {
const pkg = {repository: 'https://host.null/owner/package.git'};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
await gitCommits(['First']);
const {cwd} = await gitRepo();
await gitCommits(['First'], {cwd});
// Add remote.origin.url config
await gitAddConfig('remote.origin.url', 'git@host.null:owner/repo.git');
await gitAddConfig('remote.origin.url', 'git@host.null:owner/repo.git', {cwd});
// Create package.json in repository root
await outputJson('./package.json', pkg);
await outputJson(path.resolve(cwd, 'package.json'), pkg);
const {options} = await t.context.getConfig();
const {options: result} = await t.context.getConfig({cwd});
// Verify the default options are set
t.is(options.branch, 'master');
t.is(options.repositoryUrl, 'https://host.null/owner/package.git');
t.is(options.tagFormat, `v\${version}`);
t.is(result.branch, 'master');
t.is(result.repositoryUrl, 'https://host.null/owner/package.git');
t.is(result.tagFormat, `v\${version}`);
});
test.serial('Default values, reading repositoryUrl from repo if not set in package.json', async t => {
test('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();
const {cwd} = await gitRepo();
// Add remote.origin.url config
await gitAddConfig('remote.origin.url', 'https://host.null/owner/module.git');
await gitAddConfig('remote.origin.url', 'https://host.null/owner/module.git', {cwd});
const {options} = await t.context.getConfig();
const {options: result} = await t.context.getConfig({cwd});
// Verify the default options are set
t.is(options.branch, 'master');
t.is(options.repositoryUrl, 'https://host.null/owner/module.git');
t.is(options.tagFormat, `v\${version}`);
t.is(result.branch, 'master');
t.is(result.repositoryUrl, 'https://host.null/owner/module.git');
t.is(result.tagFormat, `v\${version}`);
});
test.serial('Default values, reading repositoryUrl (http url) from package.json if not set in repo', async t => {
test('Default values, reading repositoryUrl (http url) from package.json if not set in repo', async t => {
const pkg = {repository: 'https://host.null/owner/module.git'};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const {cwd} = await gitRepo();
// Create package.json in repository root
await outputJson('./package.json', pkg);
await outputJson(path.resolve(cwd, 'package.json'), pkg);
const {options} = await t.context.getConfig();
const {options: result} = await t.context.getConfig({cwd});
// Verify the default options are set
t.is(options.branch, 'master');
t.is(options.repositoryUrl, 'https://host.null/owner/module.git');
t.is(options.tagFormat, `v\${version}`);
t.is(result.branch, 'master');
t.is(result.repositoryUrl, 'https://host.null/owner/module.git');
t.is(result.tagFormat, `v\${version}`);
});
test.serial('Read options from package.json', async t => {
const release = {
test('Convert "ci" option to "noCi"', async t => {
const pkg = {repository: 'https://host.null/owner/module.git', release: {ci: false}};
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
// Create package.json in repository root
await outputJson(path.resolve(cwd, 'package.json'), pkg);
const {options: result} = await t.context.getConfig({cwd});
t.is(result.noCi, true);
});
test('Read options from package.json', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
const options = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
generateNotes: 'generateNotes',
branch: 'test_branch',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
plugins: false,
};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json in repository root
await outputJson('./package.json', {release});
await outputJson(path.resolve(cwd, 'package.json'), {release: options});
const {options} = await t.context.getConfig();
const {options: result} = await t.context.getConfig({cwd});
// Verify the options contains the plugin config from package.json
t.deepEqual(options, release);
t.deepEqual(result, options);
// Verify the plugins module is called with the plugin options from package.json
t.deepEqual(t.context.plugins.args[0][0], release);
t.deepEqual(t.context.plugins.args[0][0], {cwd, options});
});
test.serial('Read options from .releaserc.yml', async t => {
const release = {
test('Read options from .releaserc.yml', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
const options = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
plugins: false,
};
// 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));
await writeFile(path.resolve(cwd, '.releaserc.yml'), yaml.safeDump(options));
const {options} = await t.context.getConfig();
const {options: result} = await t.context.getConfig({cwd});
// Verify the options contains the plugin config from package.json
t.deepEqual(options, release);
t.deepEqual(result, options);
// Verify the plugins module is called with the plugin options from package.json
t.deepEqual(t.context.plugins.args[0][0], release);
t.deepEqual(t.context.plugins.args[0][0], {cwd, options});
});
test.serial('Read options from .releaserc.json', async t => {
const release = {
test('Read options from .releaserc.json', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
const options = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
plugins: false,
};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json in repository root
await outputJson('.releaserc.json', release);
await outputJson(path.resolve(cwd, '.releaserc.json'), options);
const {options} = await t.context.getConfig();
const {options: result} = await t.context.getConfig({cwd});
// Verify the options contains the plugin config from package.json
t.deepEqual(options, release);
t.deepEqual(result, options);
// Verify the plugins module is called with the plugin options from package.json
t.deepEqual(t.context.plugins.args[0][0], release);
t.deepEqual(t.context.plugins.args[0][0], {cwd, options});
});
test.serial('Read options from .releaserc.js', async t => {
const release = {
test('Read options from .releaserc.js', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
const options = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
plugins: false,
};
// 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)}`);
await writeFile(path.resolve(cwd, '.releaserc.js'), `module.exports = ${JSON.stringify(options)}`);
const {options} = await t.context.getConfig();
const {options: result} = await t.context.getConfig({cwd});
// Verify the options contains the plugin config from package.json
t.deepEqual(options, release);
t.deepEqual(result, options);
// Verify the plugins module is called with the plugin options from package.json
t.deepEqual(t.context.plugins.args[0][0], release);
t.deepEqual(t.context.plugins.args[0][0], {cwd, options});
});
test.serial('Read options from release.config.js', async t => {
const release = {
test('Read options from release.config.js', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
const options = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
plugins: false,
};
// 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)}`);
await writeFile(path.resolve(cwd, 'release.config.js'), `module.exports = ${JSON.stringify(options)}`);
const {options} = await t.context.getConfig();
const {options: result} = await t.context.getConfig({cwd});
// Verify the options contains the plugin config from package.json
t.deepEqual(options, release);
t.deepEqual(result, options);
// Verify the plugins module is called with the plugin options from package.json
t.deepEqual(t.context.plugins.args[0][0], release);
t.deepEqual(t.context.plugins.args[0][0], {cwd, options});
});
test.serial('Prioritise CLI/API parameters over file configuration and git repo', async t => {
const release = {
test('Prioritise CLI/API parameters over file configuration and git repo', async t => {
// Create a git repository, set the current working directory at the root of the repo
let {cwd, repositoryUrl} = await gitRepo();
await gitCommits(['First'], {cwd});
// Create a clone
cwd = await gitShallowClone(repositoryUrl);
const pkgOptions = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_pkg'},
branch: 'branch_pkg',
};
@@ -193,110 +200,107 @@ test.serial('Prioritise CLI/API parameters over file configuration and git repo'
branch: 'branch_cli',
repositoryUrl: 'http://cli-url.com/owner/package',
tagFormat: `cli\${version}`,
plugins: false,
};
const pkg = {release, repository: 'git@host.null:owner/module.git'};
// 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);
const pkg = {release: pkgOptions, repository: 'git@host.null:owner/module.git'};
// Create package.json in repository root
await outputJson('./package.json', pkg);
await outputJson(path.resolve(cwd, 'package.json'), pkg);
const result = await t.context.getConfig(options);
const result = await t.context.getConfig({cwd}, options);
// Verify the options contains the plugin config from CLI/API
t.deepEqual(result.options, options);
// Verify the plugins module is called with the plugin options from CLI/API
t.deepEqual(t.context.plugins.args[0][0], options);
t.deepEqual(t.context.plugins.args[0][0], {cwd, options});
});
test.serial('Read configuration from file path in "extends"', async t => {
const release = {extends: './shareable.json'};
const shareable = {
test('Read configuration from file path in "extends"', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
const pkgOptions = {extends: './shareable.json'};
const options = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
generateNotes: 'generateNotes',
branch: 'test_branch',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
plugins: ['plugin-1', ['plugin-2', {plugin2Opt: 'value'}]],
};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json and shareable.json in repository root
await outputJson('./package.json', {release});
await outputJson('./shareable.json', shareable);
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
await outputJson(path.resolve(cwd, 'shareable.json'), options);
const {options} = await t.context.getConfig();
const {options: result} = await t.context.getConfig({cwd});
// Verify the options contains the plugin config from shareable.json
t.deepEqual(options, shareable);
t.deepEqual(result, options);
// Verify the plugins module is called with the plugin options from shareable.json
t.deepEqual(t.context.plugins.args[0][0], shareable);
t.deepEqual(t.context.plugins.args[0][0], {cwd, options});
t.deepEqual(t.context.plugins.args[0][1], {
analyzeCommits: './shareable.json',
generateNotes: './shareable.json',
'plugin-1': './shareable.json',
'plugin-2': './shareable.json',
});
});
test.serial('Read configuration from module path in "extends"', async t => {
const release = {extends: 'shareable'};
const shareable = {
test('Read configuration from module path in "extends"', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
const pkgOptions = {extends: 'shareable'};
const options = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
generateNotes: 'generateNotes',
branch: 'test_branch',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
plugins: false,
};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json and shareable.json in repository root
await outputJson('./package.json', {release});
await outputJson('./node_modules/shareable/index.json', shareable);
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
await outputJson(path.resolve(cwd, 'node_modules/shareable/index.json'), options);
const {options} = await t.context.getConfig();
const {options: results} = await t.context.getConfig({cwd});
// Verify the options contains the plugin config from shareable.json
t.deepEqual(options, shareable);
t.deepEqual(results, options);
// Verify the plugins module is called with the plugin options from shareable.json
t.deepEqual(t.context.plugins.args[0][0], shareable);
t.deepEqual(t.context.plugins.args[0][0], {cwd, options});
t.deepEqual(t.context.plugins.args[0][1], {
analyzeCommits: 'shareable',
generateNotes: 'shareable',
});
});
test.serial('Read configuration from an array of paths in "extends"', async t => {
const release = {extends: ['./shareable1.json', './shareable2.json']};
const shareable1 = {
test('Read configuration from an array of paths in "extends"', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
const pkgOptions = {extends: ['./shareable1.json', './shareable2.json']};
const options1 = {
verifyRelease: 'verifyRelease1',
analyzeCommits: {path: 'analyzeCommits1', param: 'analyzeCommits_param1'},
branch: 'test_branch',
repositoryUrl: 'https://host.null/owner/module.git',
};
const shareable2 = {
const options2 = {
verifyRelease: 'verifyRelease2',
generateNotes: 'generateNotes2',
analyzeCommits: {path: 'analyzeCommits2', param: 'analyzeCommits_param2'},
branch: 'test_branch',
tagFormat: `v\${version}`,
plugins: false,
};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json and shareable.json in repository root
await outputJson('./package.json', {release});
await outputJson('./shareable1.json', shareable1);
await outputJson('./shareable2.json', shareable2);
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
await outputJson(path.resolve(cwd, 'shareable1.json'), options1);
await outputJson(path.resolve(cwd, 'shareable2.json'), options2);
const {options} = await t.context.getConfig();
const {options: results} = await t.context.getConfig({cwd});
// Verify the options contains the plugin config from shareable1.json and shareable2.json
t.deepEqual(options, {...shareable1, ...shareable2});
t.deepEqual(results, {...options1, ...options2});
// Verify the plugins module is called with the plugin options from shareable1.json and shareable2.json
t.deepEqual(t.context.plugins.args[0][0], {...shareable1, ...shareable2});
t.deepEqual(t.context.plugins.args[0][0], {cwd, options: {...options1, ...options2}});
t.deepEqual(t.context.plugins.args[0][1], {
verifyRelease1: './shareable1.json',
verifyRelease2: './shareable2.json',
@@ -306,34 +310,34 @@ test.serial('Read configuration from an array of paths in "extends"', async t =>
});
});
test.serial('Prioritize configuration from config file over "extends"', async t => {
const release = {
test('Prioritize configuration from config file over "extends"', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
const pkgOptions = {
extends: './shareable.json',
branch: 'test_pkg',
generateNotes: 'generateNotes',
publish: [{path: 'publishPkg', param: 'publishPkg_param'}],
};
const shareable = {
const options1 = {
analyzeCommits: 'analyzeCommits',
generateNotes: 'generateNotesShareable',
publish: [{path: 'publishShareable', param: 'publishShareable_param'}],
branch: 'test_branch',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
plugins: false,
};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json and shareable.json in repository root
await outputJson('./package.json', {release});
await outputJson('./shareable.json', shareable);
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
await outputJson(path.resolve(cwd, 'shareable.json'), options1);
const {options} = await t.context.getConfig();
const {options} = await t.context.getConfig({cwd});
// Verify the options contains the plugin config from package.json and shareable.json
t.deepEqual(options, omit({...shareable, ...release}, 'extends'));
t.deepEqual(options, omit({...options1, ...pkgOptions}, 'extends'));
// Verify the plugins module is called with the plugin options from package.json and shareable.json
t.deepEqual(t.context.plugins.args[0][0], omit({...shareable, ...release}, 'extends'));
t.deepEqual(t.context.plugins.args[0][0], {cwd, options: omit({...options, ...pkgOptions}, 'extends')});
t.deepEqual(t.context.plugins.args[0][1], {
analyzeCommits: './shareable.json',
generateNotesShareable: './shareable.json',
@@ -341,110 +345,125 @@ test.serial('Prioritize configuration from config file over "extends"', async t
});
});
test.serial('Prioritize configuration from cli/API options over "extends"', async t => {
const opts = {
test('Prioritize configuration from cli/API options over "extends"', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
const cliOptions = {
extends: './shareable2.json',
branch: 'branch_opts',
publish: [{path: 'publishOpts', param: 'publishOpts_param'}],
repositoryUrl: 'https://host.null/owner/module.git',
};
const release = {
const pkgOptions = {
extends: './shareable1.json',
branch: 'branch_pkg',
generateNotes: 'generateNotes',
publish: [{path: 'publishPkg', param: 'publishPkg_param'}],
};
const shareable1 = {
const options1 = {
analyzeCommits: 'analyzeCommits1',
generateNotes: 'generateNotesShareable1',
publish: [{path: 'publishShareable', param: 'publishShareable_param1'}],
branch: 'test_branch1',
repositoryUrl: 'https://host.null/owner/module.git',
};
const shareable2 = {
const options2 = {
analyzeCommits: 'analyzeCommits2',
publish: [{path: 'publishShareable', param: 'publishShareable_param2'}],
branch: 'test_branch2',
tagFormat: `v\${version}`,
plugins: false,
};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json, shareable1.json and shareable2.json in repository root
await outputJson('./package.json', {release});
await outputJson('./shareable1.json', shareable1);
await outputJson('./shareable2.json', shareable2);
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
await outputJson(path.resolve(cwd, 'shareable1.json'), options1);
await outputJson(path.resolve(cwd, 'shareable2.json'), options2);
const {options} = await t.context.getConfig(opts);
const {options} = await t.context.getConfig({cwd}, cliOptions);
// Verify the options contains the plugin config from package.json and shareable2.json
t.deepEqual(options, omit({...shareable2, ...release, ...opts}, 'extends'));
t.deepEqual(options, omit({...options2, ...pkgOptions, ...cliOptions}, 'extends'));
// Verify the plugins module is called with the plugin options from package.json and shareable2.json
t.deepEqual(t.context.plugins.args[0][0], omit({...shareable2, ...release, ...opts}, 'extends'));
t.deepEqual(t.context.plugins.args[0][0], {
cwd,
options: omit({...options2, ...pkgOptions, ...cliOptions}, 'extends'),
});
});
test.serial('Allow to unset properties defined in shareable config with "null"', async t => {
const release = {
test('Allow to unset properties defined in shareable config with "null"', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
const pkgOptions = {
extends: './shareable.json',
analyzeCommits: null,
branch: 'test_branch',
repositoryUrl: 'https://host.null/owner/module.git',
plugins: null,
};
const shareable = {
const options1 = {
generateNotes: 'generateNotes',
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
tagFormat: `v\${version}`,
plugins: ['test-plugin'],
};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json and shareable.json in repository root
await outputJson('./package.json', {release});
await outputJson('./shareable.json', shareable);
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
await outputJson(path.resolve(cwd, 'shareable.json'), options1);
const {options} = await t.context.getConfig();
const {options} = await t.context.getConfig({cwd});
// Verify the options contains the plugin config from shareable.json
t.deepEqual(options, {...omit(shareable, 'analyzeCommits'), ...omit(release, ['extends', 'analyzeCommits'])});
// Verify the plugins module is called with the plugin options from shareable.json
// Verify the options contains the plugin config from shareable.json and the default `plugins`
t.deepEqual(options, {
...omit(options1, ['analyzeCommits']),
...omit(pkgOptions, ['extends', 'analyzeCommits']),
plugins: DEFAULT_PLUGINS,
});
// Verify the plugins module is called with the plugin options from shareable.json and the default `plugins`
t.deepEqual(t.context.plugins.args[0][0], {
...omit(shareable, 'analyzeCommits'),
...omit(release, ['extends', 'analyzeCommits']),
options: {
...omit(options1, 'analyzeCommits'),
...omit(pkgOptions, ['extends', 'analyzeCommits']),
plugins: DEFAULT_PLUGINS,
},
cwd,
});
t.deepEqual(t.context.plugins.args[0][1], {
generateNotes: './shareable.json',
analyzeCommits: './shareable.json',
'test-plugin': './shareable.json',
});
});
test.serial('Allow to unset properties defined in shareable config with "undefined"', async t => {
const release = {
test('Allow to unset properties defined in shareable config with "undefined"', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
const pkgOptions = {
extends: './shareable.json',
analyzeCommits: undefined,
branch: 'test_branch',
repositoryUrl: 'https://host.null/owner/module.git',
};
const shareable = {
const options1 = {
generateNotes: 'generateNotes',
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
tagFormat: `v\${version}`,
plugins: false,
};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json and release.config.js in repository root
// await outputJson('./package.json', {release});
await writeFile('release.config.js', `module.exports = ${format(release)}`);
await outputJson('./shareable.json', shareable);
await writeFile(path.resolve(cwd, 'release.config.js'), `module.exports = ${format(pkgOptions)}`);
await outputJson(path.resolve(cwd, 'shareable.json'), options1);
const {options} = await t.context.getConfig();
const {options} = await t.context.getConfig({cwd});
// Verify the options contains the plugin config from shareable.json
t.deepEqual(options, {...omit(shareable, 'analyzeCommits'), ...omit(release, ['extends', 'analyzeCommits'])});
t.deepEqual(options, {...omit(options1, 'analyzeCommits'), ...omit(pkgOptions, ['extends', 'analyzeCommits'])});
// Verify the plugins module is called with the plugin options from shareable.json
t.deepEqual(t.context.plugins.args[0][0], {
...omit(shareable, 'analyzeCommits'),
...omit(release, ['extends', 'analyzeCommits']),
options: {
...omit(options1, 'analyzeCommits'),
...omit(pkgOptions, ['extends', 'analyzeCommits']),
},
cwd,
});
t.deepEqual(t.context.plugins.args[0][1], {
generateNotes: './shareable.json',
@@ -452,17 +471,16 @@ test.serial('Allow to unset properties defined in shareable config with "undefin
});
});
test.serial('Throw an Error if one of the shareable config cannot be found', async t => {
const release = {extends: ['./shareable1.json', 'non-existing-path']};
const shareable = {analyzeCommits: 'analyzeCommits'};
test('Throw an Error if one of the shareable config cannot be found', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const {cwd} = await gitRepo();
const pkhOptions = {extends: ['./shareable1.json', 'non-existing-path']};
const options1 = {analyzeCommits: 'analyzeCommits'};
// Create package.json and shareable.json in repository root
await outputJson('./package.json', {release});
await outputJson('./shareable1.json', shareable);
await outputJson(path.resolve(cwd, 'package.json'), {release: pkhOptions});
await outputJson(path.resolve(cwd, 'shareable1.json'), options1);
const error = await t.throws(t.context.getConfig(), Error);
const error = await t.throwsAsync(t.context.getConfig({cwd}), Error);
t.is(error.message, "Cannot find module 'non-existing-path'");
t.is(error.code, 'MODULE_NOT_FOUND');
+221 -136
View File
@@ -2,194 +2,279 @@ import test from 'ava';
import getAuthUrl from '../lib/get-git-auth-url';
import {gitRepo} from './helpers/git-utils';
// Save the current process.env
const envBackup = Object.assign({}, process.env);
// Save the current working diretory
const cwd = process.cwd();
const env = {GIT_ASKPASS: 'echo', GIT_TERMINAL_PROMPT: 0};
test.beforeEach(() => {
delete process.env.GIT_CREDENTIALS;
delete process.env.GH_TOKEN;
delete process.env.GITHUB_TOKEN;
delete process.env.GL_TOKEN;
delete process.env.GITLAB_TOKEN;
delete process.env.BB_TOKEN;
delete process.env.BITBUCKET_TOKEN;
process.env.GIT_ASKPASS = 'echo';
process.env.GIT_TERMINAL_PROMPT = 0;
});
test('Return the same "git" formatted URL if "gitCredentials" is not defined', async t => {
const {cwd} = await gitRepo();
test.afterEach.always(() => {
// Restore process.env
process.env = envBackup;
// Restore the current working directory
process.chdir(cwd);
});
test.serial('Return the same "git" formatted URL if "gitCredentials" is not defined', async t => {
t.is(await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}), 'git@host.null:owner/repo.git');
});
test.serial('Return the same "https" formatted URL if "gitCredentials" is not defined', async t => {
t.is(await getAuthUrl({repositoryUrl: 'https://host.null/owner/repo.git'}), 'https://host.null/owner/repo.git');
});
test.serial(
'Return the "https" formatted URL if "gitCredentials" is not defined and repositoryUrl is a "git+https" URL',
async t => {
t.is(await getAuthUrl({repositoryUrl: 'git+https://host.null/owner/repo.git'}), 'https://host.null/owner/repo.git');
}
);
test.serial('Do not add trailing ".git" if not present in the origian URL', async t => {
t.is(await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo'}), 'git@host.null:owner/repo');
});
test.serial('Handle "https" URL with group and subgroup', async t => {
t.is(
await getAuthUrl({repositoryUrl: 'https://host.null/group/subgroup/owner/repo.git'}),
await getAuthUrl({cwd, env, options: {branch: 'master', repositoryUrl: 'git@host.null:owner/repo.git'}}),
'git@host.null:owner/repo.git'
);
});
test('Return the same "https" formatted URL if "gitCredentials" is not defined', async t => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({cwd, env, options: {branch: 'master', repositoryUrl: 'https://host.null/owner/repo.git'}}),
'https://host.null/owner/repo.git'
);
});
test('Return the "https" formatted URL if "gitCredentials" is not defined and repositoryUrl is a "git+https" URL', async t => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({cwd, env, options: {branch: 'master', repositoryUrl: 'git+https://host.null/owner/repo.git'}}),
'https://host.null/owner/repo.git'
);
});
test('Do not add trailing ".git" if not present in the origian URL', async t => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({cwd, env, options: {branch: 'master', repositoryUrl: 'git@host.null:owner/repo'}}),
'git@host.null:owner/repo'
);
});
test('Handle "https" URL with group and subgroup', async t => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({
cwd,
env,
options: {branch: 'master', repositoryUrl: 'https://host.null/group/subgroup/owner/repo.git'},
}),
'https://host.null/group/subgroup/owner/repo.git'
);
});
test.serial('Handle "git" URL with group and subgroup', async t => {
test('Handle "git" URL with group and subgroup', async t => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({repositoryUrl: 'git@host.null:group/subgroup/owner/repo.git'}),
await getAuthUrl({
cwd,
env,
options: {branch: 'master', repositoryUrl: 'git@host.null:group/subgroup/owner/repo.git'},
}),
'git@host.null:group/subgroup/owner/repo.git'
);
});
test.serial('Convert shorthand URL', async t => {
test('Convert shorthand URL', async t => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({repositoryUrl: 'semanitc-release/semanitc-release'}),
await getAuthUrl({cwd, env, options: {repositoryUrl: 'semanitc-release/semanitc-release'}}),
'https://github.com/semanitc-release/semanitc-release.git'
);
});
test.serial('Convert GitLab shorthand URL', async t => {
test('Convert GitLab shorthand URL', async t => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({repositoryUrl: 'gitlab:semanitc-release/semanitc-release'}),
await getAuthUrl({
cwd,
env,
options: {branch: 'master', repositoryUrl: 'gitlab:semanitc-release/semanitc-release'},
}),
'https://gitlab.com/semanitc-release/semanitc-release.git'
);
});
test.serial(
'Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git" URL',
async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(
await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}),
'https://user:pass@host.null/owner/repo.git'
);
}
);
test('Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git" URL', async t => {
const {cwd} = await gitRepo();
test.serial(
'Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "https" URL',
async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(
await getAuthUrl({repositoryUrl: 'https://host.null/owner/repo.git'}),
'https://user:pass@host.null/owner/repo.git'
);
}
);
test.serial(
'Return the "http" formatted URL if "gitCredentials" is defined and repositoryUrl is a "http" URL',
async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(
await getAuthUrl({repositoryUrl: 'http://host.null/owner/repo.git'}),
'http://user:pass@host.null/owner/repo.git'
);
}
);
test.serial(
'Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git+https" URL',
async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(
await getAuthUrl({repositoryUrl: 'git+https://host.null/owner/repo.git'}),
'https://user:pass@host.null/owner/repo.git'
);
}
);
test.serial(
'Return the "http" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git+http" URL',
async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(
await getAuthUrl({repositoryUrl: 'git+http://host.null/owner/repo.git'}),
'http://user:pass@host.null/owner/repo.git'
);
}
);
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GH_TOKEN"', async t => {
process.env.GH_TOKEN = 'token';
t.is(await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}), 'https://token@host.null/owner/repo.git');
});
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GITHUB_TOKEN"', async t => {
process.env.GITHUB_TOKEN = 'token';
t.is(await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}), 'https://token@host.null/owner/repo.git');
});
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GL_TOKEN"', async t => {
process.env.GL_TOKEN = 'token';
t.is(
await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}),
await getAuthUrl({
cwd,
env: {...env, GIT_CREDENTIALS: 'user:pass'},
options: {branch: 'master', repositoryUrl: 'git@host.null:owner/repo.git'},
}),
'https://user:pass@host.null/owner/repo.git'
);
});
test('Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git" URL without user', async t => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({
cwd,
env: {...env, GIT_CREDENTIALS: 'user:pass'},
options: {branch: 'master', repositoryUrl: 'host.null:owner/repo.git'},
}),
'https://user:pass@host.null/owner/repo.git'
);
});
test('Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "https" URL', async t => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({
cwd,
env: {...env, GIT_CREDENTIALS: 'user:pass'},
options: {branch: 'master', repositoryUrl: 'https://host.null/owner/repo.git'},
}),
'https://user:pass@host.null/owner/repo.git'
);
});
test('Return the "http" formatted URL if "gitCredentials" is defined and repositoryUrl is a "http" URL', async t => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({
cwd,
env: {...env, GIT_CREDENTIALS: 'user:pass'},
options: {branch: 'master', repositoryUrl: 'http://host.null/owner/repo.git'},
}),
'http://user:pass@host.null/owner/repo.git'
);
});
test('Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git+https" URL', async t => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({
cwd,
env: {...env, GIT_CREDENTIALS: 'user:pass'},
options: {branch: 'master', repositoryUrl: 'git+https://host.null/owner/repo.git'},
}),
'https://user:pass@host.null/owner/repo.git'
);
});
test('Return the "http" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git+http" URL', async t => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({
cwd,
env: {...env, GIT_CREDENTIALS: 'user:pass'},
options: {branch: 'master', repositoryUrl: 'git+http://host.null/owner/repo.git'},
}),
'http://user:pass@host.null/owner/repo.git'
);
});
test('Return the "https" formatted URL if "gitCredentials" is defined with "GH_TOKEN"', async t => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({
cwd,
env: {...env, GH_TOKEN: 'token'},
options: {branch: 'master', repositoryUrl: 'git@host.null:owner/repo.git'},
}),
'https://token@host.null/owner/repo.git'
);
});
test('Return the "https" formatted URL if "gitCredentials" is defined with "GITHUB_TOKEN"', async t => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({
cwd,
env: {...env, GITHUB_TOKEN: 'token'},
options: {branch: 'master', repositoryUrl: 'git@host.null:owner/repo.git'},
}),
'https://token@host.null/owner/repo.git'
);
});
test('Return the "https" formatted URL if "gitCredentials" is defined with "GL_TOKEN"', async t => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({
cwd,
env: {...env, GL_TOKEN: 'token'},
options: {branch: 'master', repositoryUrl: 'git@host.null:owner/repo.git'},
}),
'https://gitlab-ci-token:token@host.null/owner/repo.git'
);
});
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GITLAB_TOKEN"', async t => {
process.env.GITLAB_TOKEN = 'token';
test('Return the "https" formatted URL if "gitCredentials" is defined with "GITLAB_TOKEN"', async t => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}),
await getAuthUrl({
cwd,
env: {...env, GITLAB_TOKEN: 'token'},
options: {branch: 'master', repositoryUrl: 'git@host.null:owner/repo.git'},
}),
'https://gitlab-ci-token:token@host.null/owner/repo.git'
);
});
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "BB_TOKEN"', async t => {
process.env.BB_TOKEN = 'token';
test('Return the "https" formatted URL if "gitCredentials" is defined with "BB_TOKEN"', async t => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}),
await getAuthUrl({
cwd,
env: {...env, BB_TOKEN: 'token'},
options: {branch: 'master', repositoryUrl: 'git@host.null:owner/repo.git'},
}),
'https://x-token-auth:token@host.null/owner/repo.git'
);
});
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "BITBUCKET_TOKEN"', async t => {
process.env.BITBUCKET_TOKEN = 'token';
test('Return the "https" formatted URL if "gitCredentials" is defined with "BITBUCKET_TOKEN"', async t => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}),
await getAuthUrl({
cwd,
env: {...env, BITBUCKET_TOKEN: 'token'},
options: {branch: 'master', repositoryUrl: 'git@host.null:owner/repo.git'},
}),
'https://x-token-auth:token@host.null/owner/repo.git'
);
});
test.serial('Handle "https" URL with group and subgroup, with "GIT_CREDENTIALS"', async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
test('Handle "https" URL with group and subgroup, with "GIT_CREDENTIALS"', async t => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({repositoryUrl: 'https://host.null/group/subgroup/owner/repo.git'}),
await getAuthUrl({
cwd,
env: {...env, GIT_CREDENTIALS: 'user:pass'},
options: {branch: 'master', repositoryUrl: 'https://host.null/group/subgroup/owner/repo.git'},
}),
'https://user:pass@host.null/group/subgroup/owner/repo.git'
);
});
test.serial('Handle "git" URL with group and subgroup, with "GIT_CREDENTIALS', async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
test('Handle "git" URL with group and subgroup, with "GIT_CREDENTIALS', async t => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({repositoryUrl: 'git@host.null:group/subgroup/owner/repo.git'}),
await getAuthUrl({
cwd,
env: {...env, GIT_CREDENTIALS: 'user:pass'},
options: {branch: 'master', repositoryUrl: 'git@host.null:group/subgroup/owner/repo.git'},
}),
'https://user:pass@host.null/group/subgroup/owner/repo.git'
);
});
test.serial('Do not add git credential to repositoryUrl if push is allowed', async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
// Create a git repository, set the current working directory at the root of the repo
const repositoryUrl = await gitRepo(true);
test('Do not add git credential to repositoryUrl if push is allowed', async t => {
const {cwd, repositoryUrl} = await gitRepo(true);
t.is(await getAuthUrl({repositoryUrl}), repositoryUrl);
t.is(
await getAuthUrl({cwd, env: {...env, GIT_CREDENTIALS: 'user:pass'}, options: {branch: 'master', repositoryUrl}}),
repositoryUrl
);
});
+65 -73
View File
@@ -3,155 +3,147 @@ import {stub} from 'sinon';
import getLastRelease from '../lib/get-last-release';
import {gitRepo, gitCommits, gitTagVersion, gitCheckout} from './helpers/git-utils';
// Save the current working diretory
const cwd = process.cwd();
test.beforeEach(t => {
// Stub the logger functions
t.context.log = stub();
t.context.logger = {log: t.context.log};
});
test.afterEach.always(() => {
// Restore the current working directory
process.chdir(cwd);
});
test.serial('Get the highest non-prerelease valid tag', async t => {
test('Get the highest non-prerelease valid tag', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const {cwd} = await gitRepo();
// Create some commits and tags
await gitCommits(['First']);
await gitTagVersion('foo');
const commits = await gitCommits(['Second']);
await gitTagVersion('v2.0.0');
await gitCommits(['Third']);
await gitTagVersion('v1.0.0');
await gitCommits(['Fourth']);
await gitTagVersion('v3.0');
await gitCommits(['Fifth']);
await gitTagVersion('v3.0.0-beta.1');
await gitCommits(['First'], {cwd});
await gitTagVersion('foo', undefined, {cwd});
const commits = await gitCommits(['Second'], {cwd});
await gitTagVersion('v2.0.0', undefined, {cwd});
await gitCommits(['Third'], {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
await gitCommits(['Fourth'], {cwd});
await gitTagVersion('v3.0', undefined, {cwd});
await gitCommits(['Fifth'], {cwd});
await gitTagVersion('v3.0.0-beta.1', undefined, {cwd});
const result = await getLastRelease(`v\${version}`, t.context.logger);
const result = await getLastRelease({cwd, options: {tagFormat: `v\${version}`}, logger: t.context.logger});
t.deepEqual(result, {gitHead: commits[0].hash, gitTag: 'v2.0.0', version: '2.0.0'});
t.deepEqual(t.context.log.args[0], ['Found git tag %s associated with version %s', 'v2.0.0', '2.0.0']);
t.deepEqual(t.context.log.args[0], ['Found git tag v2.0.0 associated with version 2.0.0']);
});
test.serial('Get the highest tag in the history of the current branch', async t => {
test('Get the highest tag in the history of the current branch', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const {cwd} = await gitRepo();
// Add commit to the master branch
await gitCommits(['First']);
await gitCommits(['First'], {cwd});
// Create the tag corresponding to version 1.0.0
// Create the new branch 'other-branch' from master
await gitCheckout('other-branch');
await gitCheckout('other-branch', true, {cwd});
// Add commit to the 'other-branch' branch
await gitCommits(['Second']);
await gitCommits(['Second'], {cwd});
// Create the tag corresponding to version 3.0.0
await gitTagVersion('v3.0.0');
await gitTagVersion('v3.0.0', undefined, {cwd});
// Checkout master
await gitCheckout('master', false);
await gitCheckout('master', false, {cwd});
// Add another commit to the master branch
const commits = await gitCommits(['Third']);
const commits = await gitCommits(['Third'], {cwd});
// Create the tag corresponding to version 2.0.0
await gitTagVersion('v2.0.0');
await gitTagVersion('v2.0.0', undefined, {cwd});
const result = await getLastRelease(`v\${version}`, t.context.logger);
const result = await getLastRelease({cwd, options: {tagFormat: `v\${version}`}, logger: t.context.logger});
t.deepEqual(result, {gitHead: commits[0].hash, gitTag: 'v2.0.0', version: '2.0.0'});
});
test.serial('Match the tag name from the begining of the string', async t => {
test('Match the tag name from the begining of the string', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const commits = await gitCommits(['First']);
await gitTagVersion('prefix/v1.0.0');
await gitTagVersion('prefix/v2.0.0');
await gitTagVersion('other-prefix/v3.0.0');
const {cwd} = await gitRepo();
const commits = await gitCommits(['First'], {cwd});
await gitTagVersion('prefix/v1.0.0', undefined, {cwd});
await gitTagVersion('prefix/v2.0.0', undefined, {cwd});
await gitTagVersion('other-prefix/v3.0.0', undefined, {cwd});
const result = await getLastRelease(`prefix/v\${version}`, t.context.logger);
const result = await getLastRelease({cwd, options: {tagFormat: `prefix/v\${version}`}, logger: t.context.logger});
t.deepEqual(result, {gitHead: commits[0].hash, gitTag: 'prefix/v2.0.0', version: '2.0.0'});
});
test.serial('Return empty object if no valid tag is found', async t => {
test('Return empty object if no valid tag is found', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const {cwd} = await gitRepo();
// Create some commits and tags
await gitCommits(['First']);
await gitTagVersion('foo');
await gitCommits(['Second']);
await gitTagVersion('v2.0.x');
await gitCommits(['Third']);
await gitTagVersion('v3.0');
await gitCommits(['First'], {cwd});
await gitTagVersion('foo', undefined, {cwd});
await gitCommits(['Second'], {cwd});
await gitTagVersion('v2.0.x', undefined, {cwd});
await gitCommits(['Third'], {cwd});
await gitTagVersion('v3.0', undefined, {cwd});
const result = await getLastRelease(`v\${version}`, t.context.logger);
const result = await getLastRelease({cwd, options: {tagFormat: `v\${version}`}, logger: t.context.logger});
t.deepEqual(result, {});
t.is(t.context.log.args[0][0], 'No git tag version found');
});
test.serial('Return empty object if no valid tag is found in history', async t => {
test('Return empty object if no valid tag is found in history', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
await gitCommits(['First']);
await gitCheckout('other-branch');
await gitCommits(['Second']);
await gitTagVersion('v1.0.0');
await gitTagVersion('v2.0.0');
await gitTagVersion('v3.0.0');
await gitCheckout('master', false);
const {cwd} = await gitRepo();
await gitCommits(['First'], {cwd});
await gitCheckout('other-branch', true, {cwd});
await gitCommits(['Second'], {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
await gitTagVersion('v2.0.0', undefined, {cwd});
await gitTagVersion('v3.0.0', undefined, {cwd});
await gitCheckout('master', false, {cwd});
const result = await getLastRelease(`v\${version}`, t.context.logger);
const result = await getLastRelease({cwd, options: {tagFormat: `v\${version}`}, logger: t.context.logger});
t.deepEqual(result, {});
t.is(t.context.log.args[0][0], 'No git tag version found');
});
test.serial('Get the highest valid tag corresponding to the "tagFormat"', async t => {
test('Get the highest valid tag corresponding to the "tagFormat"', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const {cwd} = await gitRepo();
// Create some commits and tags
const [{hash: gitHead}] = await gitCommits(['First']);
const [{hash: gitHead}] = await gitCommits(['First'], {cwd});
await gitTagVersion('1.0.0');
t.deepEqual(await getLastRelease(`\${version}`, t.context.logger), {
await gitTagVersion('1.0.0', undefined, {cwd});
t.deepEqual(await getLastRelease({cwd, options: {tagFormat: `\${version}`}, logger: t.context.logger}), {
gitHead,
gitTag: '1.0.0',
version: '1.0.0',
});
await gitTagVersion('foo-1.0.0-bar');
t.deepEqual(await getLastRelease(`foo-\${version}-bar`, t.context.logger), {
await gitTagVersion('foo-1.0.0-bar', undefined, {cwd});
t.deepEqual(await getLastRelease({cwd, options: {tagFormat: `foo-\${version}-bar`}, logger: t.context.logger}), {
gitHead,
gitTag: 'foo-1.0.0-bar',
version: '1.0.0',
});
await gitTagVersion('foo-v1.0.0-bar');
t.deepEqual(await getLastRelease(`foo-v\${version}-bar`, t.context.logger), {
await gitTagVersion('foo-v1.0.0-bar', undefined, {cwd});
t.deepEqual(await getLastRelease({cwd, options: {tagFormat: `foo-v\${version}-bar`}, logger: t.context.logger}), {
gitHead,
gitTag: 'foo-v1.0.0-bar',
version: '1.0.0',
});
await gitTagVersion('(.+)/1.0.0/(a-z)');
t.deepEqual(await getLastRelease(`(.+)/\${version}/(a-z)`, t.context.logger), {
await gitTagVersion('(.+)/1.0.0/(a-z)', undefined, {cwd});
t.deepEqual(await getLastRelease({cwd, options: {tagFormat: `(.+)/\${version}/(a-z)`}, logger: t.context.logger}), {
gitHead,
gitTag: '(.+)/1.0.0/(a-z)',
version: '1.0.0',
});
await gitTagVersion('2.0.0-1.0.0-bar.1');
t.deepEqual(await getLastRelease(`2.0.0-\${version}-bar.1`, t.context.logger), {
await gitTagVersion('2.0.0-1.0.0-bar.1', undefined, {cwd});
t.deepEqual(await getLastRelease({cwd, options: {tagFormat: `2.0.0-\${version}-bar.1`}, logger: t.context.logger}), {
gitHead,
gitTag: '2.0.0-1.0.0-bar.1',
version: '1.0.0',
});
await gitTagVersion('3.0.0-bar.1');
t.deepEqual(await getLastRelease(`\${version}-bar.1`, t.context.logger), {
await gitTagVersion('3.0.0-bar.1', undefined, {cwd});
t.deepEqual(await getLastRelease({cwd, options: {tagFormat: `\${version}-bar.1`}, logger: t.context.logger}), {
gitHead,
gitTag: '3.0.0-bar.1',
version: '3.0.0',
+17
View File
@@ -0,0 +1,17 @@
import test from 'ava';
import {spy} from 'sinon';
import getLogger from '../lib/get-logger';
test('Expose "error", "success" and "log" functions', t => {
const stdout = spy();
const stderr = spy();
const logger = getLogger({stdout: {write: stdout}, stderr: {write: stderr}});
logger.log('test log');
logger.success('test success');
logger.error('test error');
t.regex(stdout.args[0][0], /.*test log/);
t.regex(stdout.args[1][0], /.*test success/);
t.regex(stderr.args[0][0], /.*test error/);
});
+16 -4
View File
@@ -9,21 +9,33 @@ test.beforeEach(t => {
});
test('Increase version for patch release', t => {
const version = getNextVersion('patch', {version: '1.0.0'}, t.context.logger);
const version = getNextVersion({
nextRelease: {type: 'patch'},
lastRelease: {version: '1.0.0'},
logger: t.context.logger,
});
t.is(version, '1.0.1');
});
test('Increase version for minor release', t => {
const version = getNextVersion('minor', {version: '1.0.0'}, t.context.logger);
const version = getNextVersion({
nextRelease: {type: 'minor'},
lastRelease: {version: '1.0.0'},
logger: t.context.logger,
});
t.is(version, '1.1.0');
});
test('Increase version for major release', t => {
const version = getNextVersion('major', {version: '1.0.0'}, t.context.logger);
const version = getNextVersion({
nextRelease: {type: 'major'},
lastRelease: {version: '1.0.0'},
logger: t.context.logger,
});
t.is(version, '2.0.0');
});
test('Return 1.0.0 if there is no previous release', t => {
const version = getNextVersion('minor', {}, t.context.logger);
const version = getNextVersion({nextRelease: {type: 'minor'}, lastRelease: {}, logger: t.context.logger});
t.is(version, '1.0.0');
});
+125 -102
View File
@@ -1,14 +1,14 @@
import test from 'ava';
import tempy from 'tempy';
import {
gitTagHead,
getTagHead,
isRefInHistory,
unshallow,
gitHead,
fetch,
getGitHead,
repoUrl,
tag,
push,
gitTags,
getTags,
isGitRepo,
verifyTagName,
isBranchUpToDate,
@@ -23,197 +23,220 @@ import {
gitAddConfig,
gitCommitTag,
gitRemoteTagHead,
push as pushUtil,
reset,
gitPush,
gitDetachedHead,
} from './helpers/git-utils';
// Save the current working diretory
const cwd = process.cwd();
test.afterEach.always(() => {
// Restore the current working directory
process.chdir(cwd);
});
test.serial('Get the last commit sha', async t => {
test('Get the last commit sha', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const {cwd} = await gitRepo();
// Add commits to the master branch
const commits = await gitCommits(['First']);
const commits = await gitCommits(['First'], {cwd});
const result = await gitHead();
const result = await getGitHead({cwd});
t.is(result, commits[0].hash);
});
test.serial('Throw error if the last commit sha cannot be found', async t => {
test('Throw error if the last commit sha cannot be found', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const {cwd} = await gitRepo();
await t.throws(gitHead(), Error);
await t.throwsAsync(getGitHead({cwd}), Error);
});
test.serial('Unshallow repository', async t => {
test('Unshallow and fetch repository', async t => {
// Create a git repository, set the current working directory at the root of the repo
const repo = await gitRepo();
let {cwd, repositoryUrl} = await gitRepo();
// Add commits to the master branch
await gitCommits(['First', 'Second']);
await gitCommits(['First', 'Second'], {cwd});
// Create a shallow clone with only 1 commit
await gitShallowClone(repo);
cwd = await gitShallowClone(repositoryUrl);
// Verify the shallow clone contains only one commit
t.is((await gitGetCommits()).length, 1);
t.is((await gitGetCommits(undefined, {cwd})).length, 1);
await unshallow(repo);
await fetch(repositoryUrl, {cwd});
// Verify the shallow clone contains all the commits
t.is((await gitGetCommits()).length, 2);
t.is((await gitGetCommits(undefined, {cwd})).length, 2);
});
test.serial('Do not throw error when unshallow a complete repository', async t => {
test('Do not throw error when unshallow a complete repository', async t => {
// Create a git repository, set the current working directory at the root of the repo
const repo = await gitRepo();
const {cwd, repositoryUrl} = await gitRepo();
// Add commits to the master branch
await gitCommits(['First']);
await t.notThrows(unshallow(repo));
await gitCommits(['First'], {cwd});
await t.notThrowsAsync(fetch(repositoryUrl, {cwd}));
});
test.serial('Verify if the commit `sha` is in the direct history of the current branch', async t => {
test('Fetch all tags on a detached head repository', async t => {
let {cwd, repositoryUrl} = await gitRepo();
await gitCommits(['First'], {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
await gitCommits(['Second'], {cwd});
await gitTagVersion('v1.0.1', undefined, {cwd});
const [commit] = await gitCommits(['Third'], {cwd});
await gitTagVersion('v1.1.0', undefined, {cwd});
await gitPush(repositoryUrl, 'master', {cwd});
cwd = await gitDetachedHead(repositoryUrl, commit.hash);
await fetch(repositoryUrl, {cwd});
t.deepEqual((await getTags({cwd})).sort(), ['v1.0.0', 'v1.0.1', 'v1.1.0'].sort());
});
test('Verify if the commit `sha` is in the direct history of the current branch', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const {cwd} = await gitRepo();
// Add commits to the master branch
const commits = await gitCommits(['First']);
const commits = await gitCommits(['First'], {cwd});
// Create the new branch 'other-branch' from master
await gitCheckout('other-branch');
await gitCheckout('other-branch', true, {cwd});
// Add commits to the 'other-branch' branch
const otherCommits = await gitCommits(['Second']);
await gitCheckout('master', false);
const otherCommits = await gitCommits(['Second'], {cwd});
await gitCheckout('master', false, {cwd});
t.true(await isRefInHistory(commits[0].hash));
t.falsy(await isRefInHistory(otherCommits[0].hash));
await t.throws(isRefInHistory('non-existant-sha'));
t.true(await isRefInHistory(commits[0].hash, {cwd}));
t.falsy(await isRefInHistory(otherCommits[0].hash, {cwd}));
await t.throwsAsync(isRefInHistory('non-existant-sha', {cwd}));
});
test.serial('Get the commit sha for a given tag or falsy if the tag does not exists', async t => {
test('Get the commit sha for a given tag or falsy if the tag does not exists', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const {cwd} = await gitRepo();
// Add commits to the master branch
const commits = await gitCommits(['First']);
const commits = await gitCommits(['First'], {cwd});
// Create the tag corresponding to version 1.0.0
await gitTagVersion('v1.0.0');
await gitTagVersion('v1.0.0', undefined, {cwd});
t.is(await gitTagHead('v1.0.0'), commits[0].hash);
t.falsy(await gitTagHead('missing_tag'));
t.is(await getTagHead('v1.0.0', {cwd}), commits[0].hash);
t.falsy(await getTagHead('missing_tag', {cwd}));
});
test.serial('Return git remote repository url from config', async t => {
test('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();
const {cwd} = await gitRepo();
// Add remote.origin.url config
await gitAddConfig('remote.origin.url', 'git@hostname.com:owner/package.git');
await gitAddConfig('remote.origin.url', 'git@hostname.com:owner/package.git', {cwd});
t.is(await repoUrl(), 'git@hostname.com:owner/package.git');
t.is(await repoUrl({cwd}), 'git@hostname.com:owner/package.git');
});
test.serial('Return git remote repository url set while cloning', async t => {
test('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']);
let {cwd, repositoryUrl} = await gitRepo();
await gitCommits(['First'], {cwd});
// Create a clone
await gitShallowClone(repo);
cwd = await gitShallowClone(repositoryUrl);
t.is(await repoUrl(), repo);
t.is(await repoUrl({cwd}), repositoryUrl);
});
test.serial('Return falsy if git repository url is not set', async t => {
test('Return falsy 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();
const {cwd} = await gitRepo();
t.falsy(await repoUrl());
t.falsy(await repoUrl({cwd}));
});
test.serial('Add tag on head commit', async t => {
test('Add tag on head commit', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const commits = await gitCommits(['Test commit']);
const {cwd} = await gitRepo();
const commits = await gitCommits(['Test commit'], {cwd});
await tag('tag_name');
await tag('tag_name', {cwd});
await t.is(await gitCommitTag(commits[0].hash), 'tag_name');
await t.is(await gitCommitTag(commits[0].hash, {cwd}), 'tag_name');
});
test.serial('Push tag and commit to remote repository', async t => {
test('Push tag to remote repository', async t => {
// Create a git repository with a remote, set the current working directory at the root of the repo
const repo = await gitRepo(true);
const commits = await gitCommits(['Test commit']);
const {cwd, repositoryUrl} = await gitRepo(true);
const commits = await gitCommits(['Test commit'], {cwd});
await tag('tag_name');
await push(repo, 'master');
await tag('tag_name', {cwd});
await push(repositoryUrl, {cwd});
t.is(await gitRemoteTagHead(repo, 'tag_name'), commits[0].hash);
t.is(await gitRemoteTagHead(repositoryUrl, 'tag_name', {cwd}), commits[0].hash);
});
test.serial('Return "true" if in a Git repository', async t => {
test('Push tag to remote repository with remote branch ahaed', async t => {
const {cwd, repositoryUrl} = await gitRepo(true);
const commits = await gitCommits(['First'], {cwd});
await gitPush(repositoryUrl, 'master', {cwd});
const tmpRepo = await gitShallowClone(repositoryUrl);
await gitCommits(['Second'], {cwd: tmpRepo});
await gitPush('origin', 'master', {cwd: tmpRepo});
await tag('tag_name', {cwd});
await push(repositoryUrl, {cwd});
t.is(await gitRemoteTagHead(repositoryUrl, 'tag_name', {cwd}), commits[0].hash);
});
test('Return "true" if in a Git repository', async t => {
// Create a git repository with a remote, set the current working directory at the root of the repo
await gitRepo(true);
const {cwd} = await gitRepo(true);
t.true(await isGitRepo());
t.true(await isGitRepo({cwd}));
});
test.serial('Return falsy if not in a Git repository', async t => {
const dir = tempy.directory();
process.chdir(dir);
test('Return falsy if not in a Git repository', async t => {
const cwd = tempy.directory();
t.falsy(await isGitRepo());
t.falsy(await isGitRepo({cwd}));
});
test.serial('Return "true" for valid tag names', async t => {
test('Return "true" for valid tag names', async t => {
t.true(await verifyTagName('1.0.0'));
t.true(await verifyTagName('v1.0.0'));
t.true(await verifyTagName('tag_name'));
t.true(await verifyTagName('tag/name'));
});
test.serial('Return falsy for invalid tag names', async t => {
test('Return falsy for invalid tag names', async t => {
t.falsy(await verifyTagName('?1.0.0'));
t.falsy(await verifyTagName('*1.0.0'));
t.falsy(await verifyTagName('[1.0.0]'));
t.falsy(await verifyTagName('1.0.0..'));
});
test.serial('Throws error if obtaining the tags fails', async t => {
const dir = tempy.directory();
process.chdir(dir);
test('Throws error if obtaining the tags fails', async t => {
const cwd = tempy.directory();
await t.throws(gitTags());
await t.throwsAsync(getTags({cwd}));
});
test.serial('Return "true" if repository is up to date', async t => {
await gitRepo(true);
await gitCommits(['First']);
await pushUtil();
test('Return "true" if repository is up to date', async t => {
const {cwd, repositoryUrl} = await gitRepo(true);
await gitCommits(['First'], {cwd});
await gitPush(repositoryUrl, 'master', {cwd});
t.true(await isBranchUpToDate('master'));
t.true(await isBranchUpToDate('master', {cwd}));
});
test.serial('Return falsy if repository is not up to date', async t => {
await gitRepo(true);
await gitCommits(['First']);
await gitCommits(['Second']);
await pushUtil();
test('Return falsy if repository is not up to date', async t => {
const {cwd, repositoryUrl} = await gitRepo(true);
await gitCommits(['First'], {cwd});
await gitCommits(['Second'], {cwd});
await gitPush(repositoryUrl, 'master', {cwd});
t.true(await isBranchUpToDate('master'));
t.true(await isBranchUpToDate('master', {cwd}));
await reset();
const tmpRepo = await gitShallowClone(repositoryUrl);
await gitCommits(['Third'], {cwd: tmpRepo});
await gitPush('origin', 'master', {cwd: tmpRepo});
t.falsy(await isBranchUpToDate('master'));
t.falsy(await isBranchUpToDate('master', {cwd}));
});
test.serial('Return "true" if local repository is ahead', async t => {
await gitRepo(true);
await gitCommits(['First']);
await pushUtil();
await gitCommits(['Second']);
test('Return "true" if local repository is ahead', async t => {
const {cwd, repositoryUrl} = await gitRepo(true);
await gitCommits(['First'], {cwd});
await gitPush(repositoryUrl, 'master', {cwd});
await gitCommits(['Second'], {cwd});
t.true(await isBranchUpToDate('master'));
t.true(await isBranchUpToDate('master', {cwd}));
});
+68 -64
View File
@@ -24,18 +24,21 @@ import getStream from 'get-stream';
* @return {String} The path of the clone if `withRemote` is `true`, the path of the repository otherwise.
*/
export async function gitRepo(withRemote, branch = 'master') {
const dir = tempy.directory();
let cwd = tempy.directory();
process.chdir(dir);
await execa('git', ['init'].concat(withRemote ? ['--bare'] : []));
await execa('git', ['init', ...(withRemote ? ['--bare'] : [])], {cwd});
const repositoryUrl = fileUrl(cwd);
if (withRemote) {
await initBareRepo(fileUrl(dir), branch);
await gitShallowClone(fileUrl(dir));
await initBareRepo(repositoryUrl, branch);
cwd = await gitShallowClone(repositoryUrl, branch);
} else {
await gitCheckout(branch);
await gitCheckout(branch, true, {cwd});
}
return fileUrl(dir);
await execa('git', ['config', 'commit.gpgsign', false], {cwd});
return {cwd, repositoryUrl};
}
/**
@@ -50,44 +53,41 @@ export async function gitRepo(withRemote, branch = 'master') {
* @param {String} [branch='master'] the branch to initialize.
*/
export async function initBareRepo(repositoryUrl, branch = 'master') {
const clone = tempy.directory();
await execa('git', ['clone', '--no-hardlinks', repositoryUrl, clone]);
process.chdir(clone);
await gitCheckout(branch);
await gitCommits(['Initial commit']);
await execa('git', ['push', repositoryUrl, branch]);
const cwd = tempy.directory();
await execa('git', ['clone', '--no-hardlinks', repositoryUrl, cwd], {cwd});
await gitCheckout(branch, true, {cwd});
await gitCommits(['Initial commit'], {cwd});
await execa('git', ['push', repositoryUrl, branch], {cwd});
}
/**
* Create commits on the current git repository.
*
* @param {Array<string>} messages commit messages.
* @param {Array<string>} messages Commit messages.
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @returns {Array<Commit>} The created commits, in reverse order (to match `git log` order).
*/
export async function gitCommits(messages) {
await pReduce(
messages,
async (commits, msg) => {
const stdout = await execa.stdout('git', ['commit', '-m', msg, '--allow-empty', '--no-gpg-sign']);
const [, hash] = /^\[(?:\w+)\(?.*?\)?(\w+)\] .+(?:\n|$)/.exec(stdout);
commits.push(hash);
return commits;
},
[]
export async function gitCommits(messages, execaOpts) {
await pReduce(messages, (_, message) =>
execa.stdout('git', ['commit', '-m', message, '--allow-empty', '--no-gpg-sign'], execaOpts)
);
return (await gitGetCommits()).slice(0, messages.length);
return (await gitGetCommits(undefined, execaOpts)).slice(0, messages.length);
}
/**
* Get the list of parsed commits since a git reference.
*
* @param {String} [from] Git reference from which to seach commits.
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @return {Array<Object>} The list of parsed commits.
*/
export async function gitGetCommits(from) {
export async function gitGetCommits(from, execaOpts) {
Object.assign(gitLogParser.fields, {hash: 'H', message: 'B', gitTags: 'd', committerDate: {key: 'ci', type: Date}});
return (await getStream.array(gitLogParser.parse({_: `${from ? from + '..' : ''}HEAD`}))).map(commit => {
return (await getStream.array(
gitLogParser.parse({_: `${from ? from + '..' : ''}HEAD`}, {...execaOpts, env: {...process.env, ...execaOpts.env}})
)).map(commit => {
commit.message = commit.message.trim();
commit.gitTags = commit.gitTags.trim();
return commit;
@@ -98,17 +98,22 @@ export async function gitGetCommits(from) {
* Checkout a branch on the current git repository.
*
* @param {String} branch Branch name.
* @param {Boolean} create `true` to create the branche ans switch, `false` to only switch.
* @param {Boolean} create `true` to create the branch, `false` to checkout an existing branch.
* @param {Object} [execaOpts] Options to pass to `execa`.
*/
export async function gitCheckout(branch, create = true) {
await execa('git', create ? ['checkout', '-b', branch] : ['checkout', branch]);
export async function gitCheckout(branch, create = true, execaOpts) {
await execa('git', create ? ['checkout', '-b', branch] : ['checkout', branch], execaOpts);
}
/**
* Get the HEAD sha.
*
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @return {String} The sha of the head commit in the current git repository.
*/
export async function gitHead() {
return execa.stdout('git', ['rev-parse', 'HEAD']);
export function gitHead(execaOpts) {
return execa.stdout('git', ['rev-parse', 'HEAD'], execaOpts);
}
/**
@@ -116,9 +121,10 @@ export async function gitHead() {
*
* @param {String} tagName The tag name to create.
* @param {String} [sha] The commit on which to create the tag. If undefined the tag is created on the last commit.
* @param {Object} [execaOpts] Options to pass to `execa`.
*/
export async function gitTagVersion(tagName, sha) {
await execa('git', sha ? ['tag', '-f', tagName, sha] : ['tag', tagName]);
export async function gitTagVersion(tagName, sha, execaOpts) {
await execa('git', sha ? ['tag', '-f', tagName, sha] : ['tag', tagName], execaOpts);
}
/**
@@ -131,11 +137,12 @@ export async function gitTagVersion(tagName, sha) {
* @return {String} The path of the cloned repository.
*/
export async function gitShallowClone(repositoryUrl, branch = 'master', depth = 1) {
const dir = tempy.directory();
const cwd = tempy.directory();
process.chdir(dir);
await execa('git', ['clone', '--no-hardlinks', '--no-tags', '-b', branch, '--depth', depth, repositoryUrl, dir]);
return dir;
await execa('git', ['clone', '--no-hardlinks', '--no-tags', '-b', branch, '--depth', depth, repositoryUrl, cwd], {
cwd,
});
return cwd;
}
/**
@@ -146,14 +153,13 @@ export async function gitShallowClone(repositoryUrl, branch = 'master', depth =
* @return {String} The path of the new repository.
*/
export async function gitDetachedHead(repositoryUrl, head) {
const dir = tempy.directory();
const cwd = tempy.directory();
process.chdir(dir);
await execa('git', ['init']);
await execa('git', ['remote', 'add', 'origin', repositoryUrl]);
await execa('git', ['fetch']);
await execa('git', ['checkout', head]);
return dir;
await execa('git', ['init'], {cwd});
await execa('git', ['remote', 'add', 'origin', repositoryUrl], {cwd});
await execa('git', ['fetch', repositoryUrl], {cwd});
await execa('git', ['checkout', head], {cwd});
return cwd;
}
/**
@@ -161,20 +167,22 @@ export async function gitDetachedHead(repositoryUrl, head) {
*
* @param {String} name Config name.
* @param {String} value Config value.
* @param {Object} [execaOpts] Options to pass to `execa`.
*/
export async function gitAddConfig(name, value) {
await execa('git', ['config', '--add', name, value]);
export async function gitAddConfig(name, value, execaOpts) {
await execa('git', ['config', '--add', name, value], execaOpts);
}
/**
* Get the first commit sha referenced by the tag `tagName` in the local repository.
*
* @param {String} tagName Tag name for which to retrieve the commit sha.
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @return {String} The sha of the commit associated with `tagName` on the local repository.
*/
export async function gitTagHead(tagName) {
return execa.stdout('git', ['rev-list', '-1', tagName]);
export function gitTagHead(tagName, execaOpts) {
return execa.stdout('git', ['rev-list', '-1', tagName], execaOpts);
}
/**
@@ -182,10 +190,12 @@ export async function gitTagHead(tagName) {
*
* @param {String} repositoryUrl The repository remote URL.
* @param {String} tagName The tag name to seach for.
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @return {String} The sha of the commit associated with `tagName` on the remote repository.
*/
export async function gitRemoteTagHead(repositoryUrl, tagName) {
return (await execa.stdout('git', ['ls-remote', '--tags', repositoryUrl, tagName]))
export async function gitRemoteTagHead(repositoryUrl, tagName, execaOpts) {
return (await execa.stdout('git', ['ls-remote', '--tags', repositoryUrl, tagName], execaOpts))
.split('\n')
.filter(tag => Boolean(tag))
.map(tag => tag.match(/^(\S+)/)[1])[0];
@@ -195,11 +205,12 @@ export async function gitRemoteTagHead(repositoryUrl, tagName) {
* Get the tag associated with a commit sha.
*
* @param {String} gitHead The commit sha for which to retrieve the associated tag.
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @return {String} The tag associatedwith the sha in parameter or `null`.
*/
export async function gitCommitTag(gitHead) {
return execa.stdout('git', ['describe', '--tags', '--exact-match', gitHead]);
export function gitCommitTag(gitHead, execaOpts) {
return execa.stdout('git', ['describe', '--tags', '--exact-match', gitHead], execaOpts);
}
/**
@@ -207,17 +218,10 @@ export async function gitCommitTag(gitHead) {
*
* @param {String} repositoryUrl The remote repository URL.
* @param {String} branch The branch to push.
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @throws {Error} if the push failed.
*/
export async function push(repositoryUrl = 'origin', branch = 'master') {
await execa('git', ['push', '--tags', repositoryUrl, `HEAD:${branch}`]);
}
/**
* Reset repository to a commit.
*
* @param {String} [commit='HEAD~1'] Commit reference to reset the repo to.
*/
export async function reset(commit = 'HEAD~1') {
await execa('git', ['reset', commit]);
export async function gitPush(repositoryUrl = 'origin', branch = 'master', execaOpts) {
await execa('git', ['push', '--tags', repositoryUrl, `HEAD:${branch}`], execaOpts);
}
+3 -2
View File
@@ -64,8 +64,9 @@ async function createRepo(name, branch = 'master', description = `Repository ${n
// Retry as the server might take a few ms to make the repo available push
await pRetry(() => initBareRepo(authUrl, branch), {retries: 3, minTimeout: 500, factor: 2});
await gitShallowClone(authUrl);
return {repositoryUrl, authUrl};
const cwd = await gitShallowClone(authUrl);
return {cwd, repositoryUrl, authUrl};
}
export default {start, stop, gitCredential, createRepo};
-16
View File
@@ -1,16 +0,0 @@
import nock from 'nock';
/**
* Retun a `nock` object setup to respond to a github authentication request. Other expectation and responses can be chained.
*
* @param {String} [githubToken='GH_TOKEN'] The github token to return in the authentication response.
* @param {String} [githubUrl='https://api.github.com'] The url on which to intercept http requests.
* @return {Object} A `nock` object ready to respond to a github authentication request.
*/
export default function authenticate({
githubToken = 'GH_TOKEN',
githubUrl = 'https://api.github.com',
githubApiPathPrefix = '',
} = {}) {
return nock(`${githubUrl}/${githubApiPathPrefix}`, {reqheaders: {Authorization: `token ${githubToken}`}});
}
+2 -2
View File
@@ -30,7 +30,7 @@ async function start() {
minTimeout: 1000,
factor: 2,
});
} catch (err) {
} catch (error) {
throw new Error(`Couldn't start mock-server after 2 min`);
}
}
@@ -96,7 +96,7 @@ async function mock(
* @param {Object} expectation The expectation created with `mock` function.
* @return {Promise} A Promise that resolves if the expectation is met or reject otherwise.
*/
async function verify(expectation) {
function verify(expectation) {
return client.verify(expectation);
}
+10 -7
View File
@@ -4,10 +4,12 @@ import got from 'got';
import delay from 'delay';
import pRetry from 'p-retry';
const IMAGE = 'npmjs/npm-docker-couchdb:1.6.1';
const IMAGE = 'semanticrelease/npm-registry-docker:latest';
const SERVER_PORT = 15986;
const COUCHDB_PORT = 5984;
const SERVER_HOST = 'localhost';
const COUCHDB_USER = 'admin';
const COUCHDB_PASSWORD = 'password';
const NPM_USERNAME = 'integration';
const NPM_PASSWORD = 'suchsecure';
const NPM_EMAIL = 'integration@test.com';
@@ -15,7 +17,7 @@ const docker = new Docker();
let container;
/**
* Download the `npm-docker-couchdb` Docker image, create a new container and start it.
* Download the `npm-registry-docker` Docker image, create a new container and start it.
*/
async function start() {
await getStream(await docker.pull(IMAGE));
@@ -24,10 +26,11 @@ async function start() {
Tty: true,
Image: IMAGE,
PortBindings: {[`${COUCHDB_PORT}/tcp`]: [{HostPort: `${SERVER_PORT}`}]},
Env: [`COUCHDB_USER=${COUCHDB_USER}`, `COUCHDB_PASSWORD=${COUCHDB_PASSWORD}`],
});
await container.start();
await delay(3000);
await delay(4000);
try {
// Wait for the registry to be ready
@@ -36,14 +39,14 @@ async function start() {
minTimeout: 1000,
factor: 2,
});
} catch (err) {
throw new Error(`Couldn't start npm-docker-couchdb after 2 min`);
} catch (error) {
throw new Error(`Couldn't start npm-registry-docker after 2 min`);
}
// Create user
await got(`http://${SERVER_HOST}:${SERVER_PORT}/_users/org.couchdb.user:${NPM_USERNAME}`, {
json: true,
auth: 'admin:admin',
auth: `${COUCHDB_USER}:${COUCHDB_PASSWORD}`,
method: 'PUT',
body: {
_id: `org.couchdb.user:${NPM_USERNAME}`,
@@ -66,7 +69,7 @@ const authEnv = {
};
/**
* Stop and remote the `npm-docker-couchdb` Docker container.
* Stop and remote the `npm-registry-docker` Docker container.
*/
async function stop() {
await container.stop();
+35 -34
View File
@@ -1,57 +1,58 @@
import test from 'ava';
import clearModule from 'clear-module';
import {repeat} from 'lodash';
import hideSensitive from '../lib/hide-sensitive';
import {SECRET_REPLACEMENT, SECRET_MIN_SIZE} from '../lib/definitions/constants';
test.beforeEach(() => {
process.env = {};
clearModule('../lib/hide-sensitive');
});
test.serial('Replace multiple sensitive environment variable values', t => {
process.env.SOME_PASSWORD = 'password';
process.env.SOME_TOKEN = 'secret';
test('Replace multiple sensitive environment variable values', t => {
const env = {SOME_PASSWORD: 'password', SOME_TOKEN: 'secret'};
t.is(
require('../lib/hide-sensitive')(
`https://user:${process.env.SOME_PASSWORD}@host.com?token=${process.env.SOME_TOKEN}`
),
'https://user:[secure]@host.com?token=[secure]'
hideSensitive(env)(`https://user:${env.SOME_PASSWORD}@host.com?token=${env.SOME_TOKEN}`),
`https://user:${SECRET_REPLACEMENT}@host.com?token=${SECRET_REPLACEMENT}`
);
});
test.serial('Replace multiple occurences of sensitive environment variable values', t => {
process.env.secretKey = 'secret';
test('Replace multiple occurences of sensitive environment variable values', t => {
const env = {secretKey: 'secret'};
t.is(
require('../lib/hide-sensitive')(`https://user:${process.env.secretKey}@host.com?token=${process.env.secretKey}`),
'https://user:[secure]@host.com?token=[secure]'
hideSensitive(env)(`https://user:${env.secretKey}@host.com?token=${env.secretKey}`),
`https://user:${SECRET_REPLACEMENT}@host.com?token=${SECRET_REPLACEMENT}`
);
});
test.serial('Escape regexp special characters', t => {
process.env.SOME_CREDENTIALS = 'p$^{.+}\\w[a-z]o.*rd';
test('Escape regexp special characters', t => {
const env = {SOME_CREDENTIALS: 'p$^{.+}\\w[a-z]o.*rd'};
t.is(
require('../lib/hide-sensitive')(`https://user:${process.env.SOME_CREDENTIALS}@host.com`),
'https://user:[secure]@host.com'
hideSensitive(env)(`https://user:${env.SOME_CREDENTIALS}@host.com`),
`https://user:${SECRET_REPLACEMENT}@host.com`
);
});
test.serial('Accept "undefined" input', t => {
t.is(require('../lib/hide-sensitive')(), undefined);
test('Accept "undefined" input', t => {
t.is(hideSensitive({})(), undefined);
});
test.serial('Return same string if no environment variable has to be replaced', t => {
t.is(require('../lib/hide-sensitive')('test'), 'test');
test('Return same string if no environment variable has to be replaced', t => {
t.is(hideSensitive({})('test'), 'test');
});
test.serial('Exclude empty environment variables from the regexp', t => {
process.env.SOME_PASSWORD = 'password';
process.env.SOME_TOKEN = '';
test('Exclude empty environment variables from the regexp', t => {
const env = {SOME_PASSWORD: 'password', SOME_TOKEN: ''};
t.is(
require('../lib/hide-sensitive')(`https://user:${process.env.SOME_PASSWORD}@host.com?token=`),
'https://user:[secure]@host.com?token='
hideSensitive(env)(`https://user:${env.SOME_PASSWORD}@host.com?token=`),
`https://user:${SECRET_REPLACEMENT}@host.com?token=`
);
});
test.serial('Exclude empty environment variables from the regexp if there is only empty ones', t => {
process.env.SOME_PASSWORD = '';
process.env.SOME_TOKEN = ' \n ';
t.is(require('../lib/hide-sensitive')(`https://host.com?token=`), 'https://host.com?token=');
test('Exclude empty environment variables from the regexp if there is only empty ones', t => {
t.is(hideSensitive({SOME_PASSWORD: '', SOME_TOKEN: ' \n '})(`https://host.com?token=`), 'https://host.com?token=');
});
test('Exclude environment variables with value shorter than SECRET_MIN_SIZE from the regexp', t => {
const SHORT_TOKEN = repeat('a', SECRET_MIN_SIZE - 1);
const LONG_TOKEN = repeat('b', SECRET_MIN_SIZE);
const env = {SHORT_TOKEN, LONG_TOKEN};
t.is(
hideSensitive(env)(`https://user:${SHORT_TOKEN}@host.com?token=${LONG_TOKEN}`),
`https://user:${SHORT_TOKEN}@host.com?token=${SECRET_REPLACEMENT}`
);
});
+552 -316
View File
File diff suppressed because it is too large Load Diff
+174 -190
View File
@@ -1,102 +1,59 @@
import path from 'path';
import proxyquire from 'proxyquire';
import test from 'ava';
import {escapeRegExp} from 'lodash';
import {writeJson, readJson} from 'fs-extra';
import {stub} from 'sinon';
import execa from 'execa';
import {gitHead as getGitHead, gitTagHead, gitRepo, gitCommits, gitRemoteTagHead} from './helpers/git-utils';
import {WritableStreamBuffer} from 'stream-buffers';
import {SECRET_REPLACEMENT} from '../lib/definitions/constants';
import {gitHead, gitTagHead, gitRepo, gitCommits, gitRemoteTagHead, gitPush} from './helpers/git-utils';
import gitbox from './helpers/gitbox';
import mockServer from './helpers/mockserver';
import npmRegistry from './helpers/npm-registry';
import semanticRelease from '..';
/* eslint camelcase: ["error", {properties: "never"}] */
const requireNoCache = proxyquire.noPreserveCache();
// Environment variables used with semantic-release cli (similar to what a user would setup)
const env = {
...npmRegistry.authEnv,
GH_TOKEN: gitbox.gitCredential,
GITHUB_URL: mockServer.url,
NPM_EMAIL: 'integration@test.com',
NPM_USERNAME: 'integration',
NPM_PASSWORD: 'suchsecure',
TRAVIS: 'true',
CI: 'true',
TRAVIS_BRANCH: 'master',
TRAVIS_PULL_REQUEST: 'false',
};
// Environment variables used only for the local npm command used to do verification
const testEnv = Object.assign({}, process.env, {
const testEnv = {
...process.env,
...npmRegistry.authEnv,
npm_config_registry: npmRegistry.url,
NPM_EMAIL: 'integration@test.com',
LEGACY_TOKEN: Buffer.from(`${process.env.NPM_USERNAME}:${process.env.NPM_PASSWORD}`, 'utf8').toString('base64'),
});
// Save the current process.env
const envBackup = Object.assign({}, process.env);
LEGACY_TOKEN: Buffer.from(`${env.NPM_USERNAME}:${env.NPM_PASSWORD}`, 'utf8').toString('base64'),
};
const cli = require.resolve('../bin/semantic-release');
const pluginError = require.resolve('./fixtures/plugin-error');
const pluginInheritedError = require.resolve('./fixtures/plugin-error-inherited');
// Save the current working diretory
const cwd = process.cwd();
// Disable logs during tests
stub(process.stdout, 'write');
stub(process.stderr, 'write');
const pluginLogEnv = require.resolve('./fixtures/plugin-log-env');
test.before(async () => {
// Start the Git server
await gitbox.start();
// Start the local NPM registry
await npmRegistry.start();
// Start Mock Server
await mockServer.start();
});
test.beforeEach(() => {
// Delete environment variables that could have been set on the machine running the tests
delete process.env.NPM_TOKEN;
delete process.env.NPM_USERNAME;
delete process.env.NPM_PASSWORD;
delete process.env.NPM_EMAIL;
delete process.env.GH_URL;
delete process.env.GITHUB_URL;
delete process.env.GH_PREFIX;
delete process.env.GITHUB_PREFIX;
delete process.env.GIT_CREDENTIALS;
delete process.env.GH_TOKEN;
delete process.env.GITHUB_TOKEN;
delete process.env.GL_TOKEN;
delete process.env.GITLAB_TOKEN;
process.env.TRAVIS = 'true';
process.env.CI = 'true';
process.env.TRAVIS_BRANCH = 'master';
process.env.TRAVIS_PULL_REQUEST = 'false';
// Delete all `npm_config` environment variable set by CI as they take precedence over the `.npmrc` because the process that runs the tests is started before the `.npmrc` is created
for (let i = 0, keys = Object.keys(process.env); i < keys.length; i++) {
if (keys[i].startsWith('npm_')) {
delete process.env[keys[i]];
}
}
});
test.afterEach.always(() => {
// Restore process.env
process.env = envBackup;
// Restore the current working directory
process.chdir(cwd);
await Promise.all([gitbox.start(), npmRegistry.start(), mockServer.start()]);
});
test.after.always(async () => {
// Stop the Git server
await gitbox.stop();
// Stop the local NPM registry
await npmRegistry.stop();
// Stop Mock Server
await mockServer.stop();
await Promise.all([gitbox.stop(), npmRegistry.stop(), mockServer.stop()]);
});
test.serial('Release patch, minor and major versions', async t => {
test('Release patch, minor and major versions', async t => {
const packageName = 'test-release';
const owner = 'git';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository and package.json');
const {repositoryUrl, authUrl} = await gitbox.createRepo(packageName);
const {cwd, repositoryUrl, authUrl} = await gitbox.createRepo(packageName);
// Create package.json in repository root
await writeJson('./package.json', {
await writeJson(path.resolve(cwd, 'package.json'), {
name: packageName,
version: '0.0.0-dev',
repository: {url: repositoryUrl},
@@ -104,19 +61,18 @@ test.serial('Release patch, minor and major versions', async t => {
release: {success: false, fail: false},
});
// Create a npm-shrinkwrap.json file
await execa('npm', ['shrinkwrap'], {env: testEnv});
await execa('npm', ['shrinkwrap'], {env: testEnv, cwd});
/* No release */
let verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {permissions: {push: true}}, method: 'GET'}
);
t.log('Commit a chore');
await gitCommits(['chore: Init repository']);
await gitCommits(['chore: Init repository'], {cwd});
t.log('$ semantic-release');
let {stdout, code} = await execa(cli, [], {env});
let {stdout, code} = await execa(cli, [], {env, cwd});
t.regex(stdout, /There are no relevant changes, so no new version is released/);
t.is(code, 0);
@@ -137,27 +93,27 @@ test.serial('Release patch, minor and major versions', async t => {
);
t.log('Commit a feature');
await gitCommits(['feat: Initial commit']);
await gitCommits(['feat: Initial commit'], {cwd});
t.log('$ semantic-release');
({stdout, code} = await execa(cli, [], {env}));
({stdout, code} = await execa(cli, [], {env, cwd}));
t.regex(stdout, new RegExp(`Published GitHub release: release-url/${version}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
// Verify package.json and npm-shrinkwrap.json have been updated
t.is((await readJson('./package.json')).version, version);
t.is((await readJson('./npm-shrinkwrap.json')).version, version);
t.is((await readJson(path.resolve(cwd, 'package.json'))).version, version);
t.is((await readJson(path.resolve(cwd, 'npm-shrinkwrap.json'))).version, version);
// Retrieve the published package from the registry and check version and gitHead
let [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv, cwd})).stdout
);
let gitHead = await getGitHead();
let head = await gitHead({cwd});
t.is(releasedVersion, version);
t.is(releasedGitHead, gitHead);
t.is(await gitTagHead(`v${version}`), gitHead);
t.is(await gitRemoteTagHead(authUrl, `v${version}`), gitHead);
t.log(`+ released ${releasedVersion} with gitHead ${releasedGitHead}`);
t.is(releasedGitHead, head);
t.is(await gitTagHead(`v${version}`, {cwd}), head);
t.is(await gitRemoteTagHead(authUrl, `v${version}`, {cwd}), head);
t.log(`+ released ${releasedVersion} with head ${releasedGitHead}`);
await mockServer.verify(verifyMock);
await mockServer.verify(createReleaseMock);
@@ -179,27 +135,27 @@ test.serial('Release patch, minor and major versions', async t => {
);
t.log('Commit a fix');
await gitCommits(['fix: bar']);
await gitCommits(['fix: bar'], {cwd});
t.log('$ semantic-release');
({stdout, code} = await execa(cli, [], {env}));
({stdout, code} = await execa(cli, [], {env, cwd}));
t.regex(stdout, new RegExp(`Published GitHub release: release-url/${version}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
// Verify package.json and npm-shrinkwrap.json have been updated
t.is((await readJson('./package.json')).version, version);
t.is((await readJson('./npm-shrinkwrap.json')).version, version);
t.is((await readJson(path.resolve(cwd, 'package.json'))).version, version);
t.is((await readJson(path.resolve(cwd, 'npm-shrinkwrap.json'))).version, version);
// Retrieve the published package from the registry and check version and gitHead
[, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv, cwd})).stdout
);
gitHead = await getGitHead();
head = await gitHead({cwd});
t.is(releasedVersion, version);
t.is(releasedGitHead, gitHead);
t.is(await gitTagHead(`v${version}`), gitHead);
t.is(await gitRemoteTagHead(authUrl, `v${version}`), gitHead);
t.log(`+ released ${releasedVersion} with gitHead ${releasedGitHead}`);
t.is(releasedGitHead, head);
t.is(await gitTagHead(`v${version}`, {cwd}), head);
t.is(await gitRemoteTagHead(authUrl, `v${version}`, {cwd}), head);
t.log(`+ released ${releasedVersion} with head ${releasedGitHead}`);
await mockServer.verify(verifyMock);
await mockServer.verify(createReleaseMock);
@@ -221,27 +177,27 @@ test.serial('Release patch, minor and major versions', async t => {
);
t.log('Commit a feature');
await gitCommits(['feat: baz']);
await gitCommits(['feat: baz'], {cwd});
t.log('$ semantic-release');
({stdout, code} = await execa(cli, [], {env}));
({stdout, code} = await execa(cli, [], {env, cwd}));
t.regex(stdout, new RegExp(`Published GitHub release: release-url/${version}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
// Verify package.json and npm-shrinkwrap.json have been updated
t.is((await readJson('./package.json')).version, version);
t.is((await readJson('./npm-shrinkwrap.json')).version, version);
t.is((await readJson(path.resolve(cwd, 'package.json'))).version, version);
t.is((await readJson(path.resolve(cwd, 'npm-shrinkwrap.json'))).version, version);
// Retrieve the published package from the registry and check version and gitHead
[, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv, cwd})).stdout
);
gitHead = await getGitHead();
head = await gitHead({cwd});
t.is(releasedVersion, version);
t.is(releasedGitHead, gitHead);
t.is(await gitTagHead(`v${version}`), gitHead);
t.is(await gitRemoteTagHead(authUrl, `v${version}`), gitHead);
t.log(`+ released ${releasedVersion} with gitHead ${releasedGitHead}`);
t.is(releasedGitHead, head);
t.is(await gitTagHead(`v${version}`, {cwd}), head);
t.is(await gitRemoteTagHead(authUrl, `v${version}`, {cwd}), head);
t.log(`+ released ${releasedVersion} with head ${releasedGitHead}`);
await mockServer.verify(verifyMock);
await mockServer.verify(createReleaseMock);
@@ -263,97 +219,97 @@ test.serial('Release patch, minor and major versions', async t => {
);
t.log('Commit a breaking change');
await gitCommits(['feat: foo\n\n BREAKING CHANGE: bar']);
await gitCommits(['feat: foo\n\n BREAKING CHANGE: bar'], {cwd});
t.log('$ semantic-release');
({stdout, code} = await execa(cli, [], {env}));
({stdout, code} = await execa(cli, [], {env, cwd}));
t.regex(stdout, new RegExp(`Published GitHub release: release-url/${version}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
// Verify package.json and npm-shrinkwrap.json have been updated
t.is((await readJson('./package.json')).version, version);
t.is((await readJson('./npm-shrinkwrap.json')).version, version);
t.is((await readJson(path.resolve(cwd, 'package.json'))).version, version);
t.is((await readJson(path.resolve(cwd, 'npm-shrinkwrap.json'))).version, version);
// Retrieve the published package from the registry and check version and gitHead
[, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv, cwd})).stdout
);
gitHead = await getGitHead();
head = await gitHead({cwd});
t.is(releasedVersion, version);
t.is(releasedGitHead, gitHead);
t.is(await gitTagHead(`v${version}`), gitHead);
t.is(await gitRemoteTagHead(authUrl, `v${version}`), gitHead);
t.log(`+ released ${releasedVersion} with gitHead ${releasedGitHead}`);
t.is(releasedGitHead, head);
t.is(await gitTagHead(`v${version}`, {cwd}), head);
t.is(await gitRemoteTagHead(authUrl, `v${version}`, {cwd}), head);
t.log(`+ released ${releasedVersion} with head ${releasedGitHead}`);
await mockServer.verify(verifyMock);
await mockServer.verify(createReleaseMock);
});
test.serial('Exit with 1 if a plugin is not found', async t => {
test('Exit with 1 if a plugin is not found', async t => {
const packageName = 'test-plugin-not-found';
const owner = 'test-repo';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository');
await gitRepo();
await writeJson('./package.json', {
const {cwd} = await gitRepo();
await writeJson(path.resolve(cwd, 'package.json'), {
name: packageName,
version: '0.0.0-dev',
repository: {url: `git+https://github.com/${owner}/${packageName}`},
release: {analyzeCommits: 'non-existing-path', success: false, fail: false},
});
const {code, stderr} = await t.throws(execa(cli, [], {env}));
const {code, stderr} = await t.throwsAsync(execa(cli, [], {env, cwd}));
t.is(code, 1);
t.regex(stderr, /Cannot find module/);
});
test.serial('Exit with 1 if a shareable config is not found', async t => {
test('Exit with 1 if a shareable config is not found', async t => {
const packageName = 'test-config-not-found';
const owner = 'test-repo';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository');
await gitRepo();
await writeJson('./package.json', {
const {cwd} = await gitRepo();
await writeJson(path.resolve(cwd, 'package.json'), {
name: packageName,
version: '0.0.0-dev',
repository: {url: `git+https://github.com/${owner}/${packageName}`},
release: {extends: 'non-existing-path', success: false, fail: false},
});
const {code, stderr} = await t.throws(execa(cli, [], {env}));
const {code, stderr} = await t.throwsAsync(execa(cli, [], {env, cwd}));
t.is(code, 1);
t.regex(stderr, /Cannot find module/);
});
test.serial('Exit with 1 if a shareable config reference a not found plugin', async t => {
test('Exit with 1 if a shareable config reference a not found plugin', async t => {
const packageName = 'test-config-ref-not-found';
const owner = 'test-repo';
const shareable = {analyzeCommits: 'non-existing-path'};
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository');
await gitRepo();
await writeJson('./package.json', {
const {cwd} = await gitRepo();
await writeJson(path.resolve(cwd, 'package.json'), {
name: packageName,
version: '0.0.0-dev',
repository: {url: `git+https://github.com/${owner}/${packageName}`},
release: {extends: './shareable.json', success: false, fail: false},
});
await writeJson('./shareable.json', shareable);
await writeJson(path.resolve(cwd, 'shareable.json'), shareable);
const {code, stderr} = await t.throws(execa(cli, [], {env}));
const {code, stderr} = await t.throwsAsync(execa(cli, [], {env, cwd}));
t.is(code, 1);
t.regex(stderr, /Cannot find module/);
});
test.serial('Dry-run', async t => {
test('Dry-run', async t => {
const packageName = 'test-dry-run';
const owner = 'git';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository and package.json');
const {repositoryUrl} = await gitbox.createRepo(packageName);
const {cwd, repositoryUrl} = await gitbox.createRepo(packageName);
// Create package.json in repository root
await writeJson('./package.json', {
await writeJson(path.resolve(cwd, 'package.json'), {
name: packageName,
version: '0.0.0-dev',
repository: {url: repositoryUrl},
@@ -369,29 +325,30 @@ test.serial('Dry-run', async t => {
);
const version = '1.0.0';
t.log('Commit a feature');
await gitCommits(['feat: Initial commit']);
await gitCommits(['feat: Initial commit'], {cwd});
t.log('$ semantic-release -d');
const {stdout, code} = await execa(cli, ['-d'], {env});
const {stdout, code} = await execa(cli, ['-d'], {env, cwd});
t.regex(stdout, new RegExp(`There is no previous release, the next release version is ${version}`));
t.regex(stdout, new RegExp(`Release note for version ${version}`));
t.regex(stdout, /Initial commit/);
t.is(code, 0);
// Verify package.json and has not been modified
t.is((await readJson('./package.json')).version, '0.0.0-dev');
t.is((await readJson(path.resolve(cwd, 'package.json'))).version, '0.0.0-dev');
await mockServer.verify(verifyMock);
});
test.serial('Allow local releases with "noCi" option', async t => {
delete process.env.TRAVIS;
delete process.env.CI;
test('Allow local releases with "noCi" option', async t => {
const envNoCi = {...env};
delete envNoCi.TRAVIS;
delete envNoCi.CI;
const packageName = 'test-no-ci';
const owner = 'git';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository and package.json');
const {repositoryUrl, authUrl} = await gitbox.createRepo(packageName);
const {cwd, repositoryUrl, authUrl} = await gitbox.createRepo(packageName);
// Create package.json in repository root
await writeJson('./package.json', {
await writeJson(path.resolve(cwd, 'package.json'), {
name: packageName,
version: '0.0.0-dev',
repository: {url: repositoryUrl},
@@ -416,39 +373,39 @@ test.serial('Allow local releases with "noCi" option', async t => {
);
t.log('Commit a feature');
await gitCommits(['feat: Initial commit']);
await gitCommits(['feat: Initial commit'], {cwd});
t.log('$ semantic-release --no-ci');
const {stdout, code} = await execa(cli, ['--no-ci'], {env});
const {stdout, code} = await execa(cli, ['--no-ci'], {env: envNoCi, cwd});
t.regex(stdout, new RegExp(`Published GitHub release: release-url/${version}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
// Verify package.json and has been updated
t.is((await readJson('./package.json')).version, version);
t.is((await readJson(path.resolve(cwd, 'package.json'))).version, version);
// Retrieve the published package from the registry and check version and gitHead
const [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv, cwd})).stdout
);
const gitHead = await getGitHead();
const head = await gitHead({cwd});
t.is(releasedVersion, version);
t.is(releasedGitHead, gitHead);
t.is(await gitTagHead(`v${version}`), gitHead);
t.is(await gitRemoteTagHead(authUrl, `v${version}`), gitHead);
t.log(`+ released ${releasedVersion} with gitHead ${releasedGitHead}`);
t.is(releasedGitHead, head);
t.is(await gitTagHead(`v${version}`, {cwd}), head);
t.is(await gitRemoteTagHead(authUrl, `v${version}`, {cwd}), head);
t.log(`+ released ${releasedVersion} with head ${releasedGitHead}`);
await mockServer.verify(verifyMock);
await mockServer.verify(createReleaseMock);
});
test.serial('Pass options via CLI arguments', async t => {
test('Pass options via CLI arguments', async t => {
const packageName = 'test-cli';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository and package.json');
const {repositoryUrl, authUrl} = await gitbox.createRepo(packageName);
const {cwd, repositoryUrl, authUrl} = await gitbox.createRepo(packageName);
// Create package.json in repository root
await writeJson('./package.json', {
await writeJson(path.resolve(cwd, 'package.json'), {
name: packageName,
version: '0.0.0-dev',
repository: {url: repositoryUrl},
@@ -458,7 +415,7 @@ test.serial('Pass options via CLI arguments', async t => {
/* Initial release */
const version = '1.0.0';
t.log('Commit a feature');
await gitCommits(['feat: Initial commit']);
await gitCommits(['feat: Initial commit'], {cwd});
t.log('$ semantic-release');
const {stdout, code} = await execa(
cli,
@@ -473,38 +430,46 @@ test.serial('Pass options via CLI arguments', async t => {
false,
'--debug',
],
{env}
{env, cwd}
);
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
// Verify package.json and has been updated
t.is((await readJson('./package.json')).version, version);
t.is((await readJson(path.resolve(cwd, 'package.json'))).version, version);
// Retrieve the published package from the registry and check version and gitHead
const [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv, cwd})).stdout
);
const gitHead = await getGitHead();
const head = await gitHead({cwd});
t.is(releasedVersion, version);
t.is(releasedGitHead, gitHead);
t.is(await gitTagHead(`v${version}`), gitHead);
t.is(await gitRemoteTagHead(authUrl, `v${version}`), gitHead);
t.log(`+ released ${releasedVersion} with gitHead ${releasedGitHead}`);
t.is(releasedGitHead, head);
t.is(await gitTagHead(`v${version}`, {cwd}), head);
t.is(await gitRemoteTagHead(authUrl, `v${version}`, {cwd}), head);
t.log(`+ released ${releasedVersion} with head ${releasedGitHead}`);
});
test.serial('Run via JS API', async t => {
test('Run via JS API', async t => {
const semanticRelease = requireNoCache('..', {
'./lib/logger': {log: () => {}, error: () => {}, stdout: () => {}},
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
const packageName = 'test-js-api';
const owner = 'git';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository and package.json');
const {repositoryUrl, authUrl} = await gitbox.createRepo(packageName);
const {cwd, repositoryUrl, authUrl} = await gitbox.createRepo(packageName);
// Create package.json in repository root
await writeJson('./package.json', {
await writeJson(path.resolve(cwd, 'package.json'), {
name: packageName,
version: '0.0.0-dev',
repository: {url: repositoryUrl},
publishConfig: {registry: npmRegistry.url},
release: {
fail: false,
success: false,
},
});
/* Initial release */
@@ -523,38 +488,36 @@ test.serial('Run via JS API', async t => {
{body: {html_url: `release-url/${version}`}}
);
process.env = Object.assign(process.env, env);
t.log('Commit a feature');
await gitCommits(['feat: Initial commit']);
await gitCommits(['feat: Initial commit'], {cwd});
t.log('$ Call semantic-release via API');
await semanticRelease({fail: false, success: false});
await semanticRelease(undefined, {cwd, env, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()});
// Verify package.json and has been updated
t.is((await readJson('./package.json')).version, version);
t.is((await readJson(path.resolve(cwd, 'package.json'))).version, version);
// Retrieve the published package from the registry and check version and gitHead
const [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv, cwd})).stdout
);
const gitHead = await getGitHead();
const head = await gitHead({cwd});
t.is(releasedVersion, version);
t.is(releasedGitHead, gitHead);
t.is(await gitTagHead(`v${version}`), gitHead);
t.is(await gitRemoteTagHead(authUrl, `v${version}`), gitHead);
t.log(`+ released ${releasedVersion} with gitHead ${releasedGitHead}`);
t.is(releasedGitHead, head);
t.is(await gitTagHead(`v${version}`, {cwd}), head);
t.is(await gitRemoteTagHead(authUrl, `v${version}`, {cwd}), head);
t.log(`+ released ${releasedVersion} with head ${releasedGitHead}`);
await mockServer.verify(verifyMock);
await mockServer.verify(createReleaseMock);
});
test.serial('Log unexpected errors from plugins and exit with 1', async t => {
test('Log unexpected errors from plugins and exit with 1', async t => {
const packageName = 'test-unexpected-error';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository and package.json');
const {repositoryUrl} = await gitbox.createRepo(packageName);
const {cwd, repositoryUrl} = await gitbox.createRepo(packageName);
// Create package.json in repository root
await writeJson('./package.json', {
await writeJson(path.resolve(cwd, 'package.json'), {
name: packageName,
version: '0.0.0-dev',
repository: {url: repositoryUrl},
@@ -563,9 +526,9 @@ test.serial('Log unexpected errors from plugins and exit with 1', async t => {
/* Initial release */
t.log('Commit a feature');
await gitCommits(['feat: Initial commit']);
await gitCommits(['feat: Initial commit'], {cwd});
t.log('$ semantic-release');
const {stderr, code} = await execa(cli, [], {env, reject: false});
const {stderr, code} = await execa(cli, [], {env, cwd, reject: false});
// Verify the type and message are logged
t.regex(stderr, /Error: a/);
// Verify the the stacktrace is logged
@@ -575,13 +538,13 @@ test.serial('Log unexpected errors from plugins and exit with 1', async t => {
t.is(code, 1);
});
test.serial('Log errors inheriting SemanticReleaseError and exit with 1', async t => {
test('Log errors inheriting SemanticReleaseError and exit with 1', async t => {
const packageName = 'test-inherited-error';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository and package.json');
const {repositoryUrl} = await gitbox.createRepo(packageName);
const {cwd, repositoryUrl} = await gitbox.createRepo(packageName);
// Create package.json in repository root
await writeJson('./package.json', {
await writeJson(path.resolve(cwd, 'package.json'), {
name: packageName,
version: '0.0.0-dev',
repository: {url: repositoryUrl},
@@ -590,32 +553,53 @@ test.serial('Log errors inheriting SemanticReleaseError and exit with 1', async
/* Initial release */
t.log('Commit a feature');
await gitCommits(['feat: Initial commit']);
await gitCommits(['feat: Initial commit'], {cwd});
t.log('$ semantic-release');
const {stdout, code} = await execa(cli, [], {env, reject: false});
const {stderr, code} = await execa(cli, [], {env, cwd, reject: false});
// Verify the type and message are logged
t.regex(stdout, /EINHERITED Inherited error/);
t.regex(stderr, /EINHERITED Inherited error/);
t.is(code, 1);
});
test.serial('Exit with 1 if missing permission to push to the remote repository', async t => {
test('Exit with 1 if missing permission to push to the remote repository', async t => {
const packageName = 'unauthorized';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository');
await gitbox.createRepo(packageName);
await writeJson('./package.json', {name: packageName, version: '0.0.0-dev'});
const {cwd} = await gitbox.createRepo(packageName);
await writeJson(path.resolve(cwd, 'package.json'), {name: packageName, version: '0.0.0-dev'});
/* Initial release */
t.log('Commit a feature');
await gitCommits(['feat: Initial commit']);
await gitCommits(['feat: Initial commit'], {cwd});
await gitPush('origin', 'master', {cwd});
t.log('$ semantic-release');
const {stdout, code} = await execa(
const {stderr, code} = await execa(
cli,
['--repository-url', 'http://user:wrong_pass@localhost:2080/git/unauthorized.git'],
{env: {...env, ...{GH_TOKEN: 'user:wrong_pass'}}, reject: false}
{env: {...env, GH_TOKEN: 'user:wrong_pass'}, cwd, reject: false}
);
// Verify the type and message are logged
t.regex(stdout, /EGITNOPERMISSION/);
t.regex(stderr, /EGITNOPERMISSION/);
t.is(code, 1);
});
test('Hide sensitive environment variable values from the logs', async t => {
const packageName = 'log-secret';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository');
const {cwd, repositoryUrl} = await gitbox.createRepo(packageName);
await writeJson(path.resolve(cwd, 'package.json'), {
name: packageName,
version: '0.0.0-dev',
repository: {url: repositoryUrl},
release: {verifyConditions: [pluginLogEnv], fail: false, success: false},
});
t.log('$ semantic-release');
const {stdout, stderr} = await execa(cli, [], {env: {...env, MY_TOKEN: 'secret token'}, cwd, reject: false});
t.regex(stdout, new RegExp(`Console: Exposing token ${escapeRegExp(SECRET_REPLACEMENT)}`));
t.regex(stdout, new RegExp(`Log: Exposing token ${escapeRegExp(SECRET_REPLACEMENT)}`));
t.regex(stderr, new RegExp(`Error: Console token ${escapeRegExp(SECRET_REPLACEMENT)}`));
t.regex(stderr, new RegExp(`Throw error: Exposing ${escapeRegExp(SECRET_REPLACEMENT)}`));
});
-51
View File
@@ -1,51 +0,0 @@
import test from 'ava';
import {stub} from 'sinon';
import logger from '../lib/logger';
test.beforeEach(t => {
t.context.log = stub(console, 'log');
t.context.error = stub(console, 'error');
});
test.afterEach.always(t => {
t.context.log.restore();
t.context.error.restore();
});
test.serial('Basic log', t => {
logger.log('test log');
logger.error('test error');
t.regex(t.context.log.args[0][0], /.*test log/);
t.regex(t.context.error.args[0][0], /.*test error/);
});
test.serial('Log object', t => {
const obj = {a: 1, b: '2'};
logger.log(obj);
logger.error(obj);
t.is(t.context.log.args[0][1], obj);
t.is(t.context.error.args[0][1], obj);
});
test.serial('Log with string formatting', t => {
logger.log('test log %s', 'log value');
logger.error('test error %s', 'error value');
t.regex(t.context.log.args[0][0], /.*test log/);
t.regex(t.context.error.args[0][0], /.*test error/);
t.is(t.context.log.args[0][1], 'log value');
t.is(t.context.error.args[0][1], 'error value');
});
test.serial('Log with error stacktrace and properties', t => {
const error = new Error('error message');
logger.error(error);
const otherError = new Error('other error message');
logger.error('test error %O', otherError);
t.is(t.context.error.args[0][1], error);
t.regex(t.context.error.args[1][0], /.*test error/);
t.is(t.context.error.args[1][1], otherError);
});
+131 -68
View File
@@ -3,71 +3,76 @@ import {noop} from 'lodash';
import {stub} from 'sinon';
import normalize from '../../lib/plugins/normalize';
const cwd = process.cwd();
test.beforeEach(t => {
// Stub the logger functions
t.context.log = stub();
t.context.logger = {log: t.context.log};
t.context.error = stub();
t.context.success = stub();
t.context.stderr = {write: stub()};
t.context.logger = {
log: t.context.log,
error: t.context.error,
success: t.context.success,
scope: () => t.context.logger,
};
});
test('Normalize and load plugin from string', t => {
const plugin = normalize('verifyConditions', {}, {}, './test/fixtures/plugin-noop', t.context.logger);
const plugin = normalize(
{cwd, options: {}, logger: t.context.logger},
'verifyConditions',
'./test/fixtures/plugin-noop',
{}
);
t.is(plugin.pluginName, './test/fixtures/plugin-noop');
t.is(typeof plugin, 'function');
t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'verifyConditions', './test/fixtures/plugin-noop']);
t.deepEqual(t.context.success.args[0], ['Loaded plugin "verifyConditions" from "./test/fixtures/plugin-noop"']);
});
test('Normalize and load plugin from object', t => {
const plugin = normalize('publish', {}, {}, {path: './test/fixtures/plugin-noop'}, t.context.logger);
const plugin = normalize(
{cwd, options: {}, logger: t.context.logger},
'publish',
{path: './test/fixtures/plugin-noop'},
{}
);
t.is(plugin.pluginName, './test/fixtures/plugin-noop');
t.is(typeof plugin, 'function');
t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'publish', './test/fixtures/plugin-noop']);
t.deepEqual(t.context.success.args[0], ['Loaded plugin "publish" from "./test/fixtures/plugin-noop"']);
});
test('Normalize and load plugin from a base file path', t => {
const plugin = normalize(
'verifyConditions',
{'./plugin-noop': './test/fixtures'},
{},
'./plugin-noop',
t.context.logger
);
const plugin = normalize({cwd, options: {}, logger: t.context.logger}, 'verifyConditions', './plugin-noop', {
'./plugin-noop': './test/fixtures',
});
t.is(plugin.pluginName, './plugin-noop');
t.is(typeof plugin, 'function');
t.deepEqual(t.context.log.args[0], [
'Load plugin %s from %s in shareable config %s',
'verifyConditions',
'./plugin-noop',
'./test/fixtures',
t.deepEqual(t.context.success.args[0], [
'Loaded plugin "verifyConditions" from "./plugin-noop" in shareable config "./test/fixtures"',
]);
});
test('Wrap plugin in a function that add the "pluginName" to the error"', async t => {
const plugin = normalize(
'verifyConditions',
{'./plugin-error': './test/fixtures'},
{},
'./plugin-error',
t.context.logger
);
const plugin = normalize({cwd, options: {}, logger: t.context.logger}, 'verifyConditions', './plugin-error', {
'./plugin-error': './test/fixtures',
});
const error = await t.throws(plugin());
const error = await t.throwsAsync(plugin({options: {}}));
t.is(error.pluginName, './plugin-error');
});
test('Wrap plugin in a function that add the "pluginName" to multiple errors"', async t => {
const plugin = normalize(
'verifyConditions',
{'./plugin-errors': './test/fixtures'},
{},
'./plugin-errors',
t.context.logger
);
const plugin = normalize({cwd, options: {}, logger: t.context.logger}, 'verifyConditions', './plugin-errors', {
'./plugin-errors': './test/fixtures',
});
const errors = [...(await t.throws(plugin()))];
const errors = [...(await t.throwsAsync(plugin({options: {}})))];
for (const error of errors) {
t.is(error.pluginName, './plugin-errors');
}
@@ -75,122 +80,180 @@ test('Wrap plugin in a function that add the "pluginName" to multiple errors"',
test('Normalize and load plugin from function', t => {
const pluginFunction = () => {};
const plugin = normalize('', {}, {}, pluginFunction, t.context.logger);
const plugin = normalize({cwd, options: {}, logger: t.context.logger}, '', pluginFunction, {});
t.is(plugin.pluginName, '[Function: pluginFunction]');
t.is(typeof plugin, 'function');
});
test('Normalize and load plugin that retuns multiple functions', t => {
const plugin = normalize('verifyConditions', {}, {}, './test/fixtures/multi-plugin', t.context.logger);
const plugin = normalize(
{cwd, options: {}, logger: t.context.logger},
'verifyConditions',
'./test/fixtures/multi-plugin',
{}
);
t.is(typeof plugin, 'function');
t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'verifyConditions', './test/fixtures/multi-plugin']);
t.deepEqual(t.context.success.args[0], ['Loaded plugin "verifyConditions" from "./test/fixtures/multi-plugin"']);
});
test('Wrap "analyzeCommits" plugin in a function that validate the output of the plugin', async t => {
const analyzeCommits = stub().resolves(2);
const plugin = normalize('analyzeCommits', {}, {}, analyzeCommits, t.context.logger);
const plugin = normalize(
{cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger},
'analyzeCommits',
analyzeCommits,
{}
);
const error = await t.throws(plugin());
const error = await t.throwsAsync(plugin({options: {}}));
t.is(error.code, 'EANALYZEOUTPUT');
t.is(error.code, 'EANALYZECOMMITSOUTPUT');
t.is(error.name, 'SemanticReleaseError');
t.truthy(error.message);
t.truthy(error.details);
t.regex(error.details, /2/);
});
test('Wrap "generateNotes" plugin in a function that validate the output of the plugin', async t => {
const generateNotes = stub().resolves(2);
const plugin = normalize('generateNotes', {}, {}, generateNotes, t.context.logger);
const plugin = normalize(
{cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger},
'generateNotes',
generateNotes,
{}
);
const error = await t.throws(plugin());
const error = await t.throwsAsync(plugin({options: {}}));
t.is(error.code, 'ERELEASENOTESOUTPUT');
t.is(error.code, 'EGENERATENOTESOUTPUT');
t.is(error.name, 'SemanticReleaseError');
t.truthy(error.message);
t.truthy(error.details);
t.regex(error.details, /2/);
});
test('Wrap "publish" plugin in a function that validate the output of the plugin', async t => {
const publish = stub().resolves(2);
const plugin = normalize(
{cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger},
'publish',
{'./plugin-identity': './test/fixtures'},
{},
'./plugin-identity',
t.context.logger
publish,
{}
);
const error = await t.throws(plugin(2));
const error = await t.throwsAsync(plugin({options: {}}));
t.is(error.code, 'EPUBLISHOUTPUT');
t.is(error.name, 'SemanticReleaseError');
t.truthy(error.message);
t.truthy(error.details);
t.regex(error.details, /2/);
});
test('Plugin is called with "pluginConfig" (omitting "path", adding global config) and input', async t => {
test('Plugin is called with "pluginConfig" (with object definition) and input', async t => {
const pluginFunction = stub().resolves();
const conf = {path: pluginFunction, conf: 'confValue'};
const globalConf = {global: 'globalValue'};
const plugin = normalize('', {}, globalConf, conf, t.context.logger);
await plugin('param');
const pluginConf = {path: pluginFunction, conf: 'confValue'};
const options = {global: 'globalValue'};
const plugin = normalize({cwd, options, logger: t.context.logger}, '', pluginConf, {});
await plugin({options: {}, param: 'param'});
t.true(pluginFunction.calledWith({conf: 'confValue', global: 'globalValue'}, 'param'));
t.true(
pluginFunction.calledWithMatch(
{conf: 'confValue', global: 'globalValue'},
{param: 'param', logger: t.context.logger}
)
);
});
test('Plugin is called with "pluginConfig" (with array definition) and input', async t => {
const pluginFunction = stub().resolves();
const pluginConf = [pluginFunction, {conf: 'confValue'}];
const options = {global: 'globalValue'};
const plugin = normalize({cwd, options, logger: t.context.logger}, '', pluginConf, {});
await plugin({options: {}, param: 'param'});
t.true(
pluginFunction.calledWithMatch(
{conf: 'confValue', global: 'globalValue'},
{param: 'param', logger: t.context.logger}
)
);
});
test('Prevent plugins to modify "pluginConfig"', async t => {
const pluginFunction = stub().callsFake(pluginConfig => {
pluginConfig.conf.subConf = 'otherConf';
});
const conf = {path: pluginFunction, conf: {subConf: 'originalConf'}};
const globalConf = {globalConf: {globalSubConf: 'originalGlobalConf'}};
const plugin = normalize('', {}, globalConf, conf, t.context.logger);
await plugin();
const pluginConf = {path: pluginFunction, conf: {subConf: 'originalConf'}};
const options = {globalConf: {globalSubConf: 'originalGlobalConf'}};
const plugin = normalize({cwd, options, logger: t.context.logger}, '', pluginConf, {});
await plugin({options: {}});
t.is(conf.conf.subConf, 'originalConf');
t.is(globalConf.globalConf.globalSubConf, 'originalGlobalConf');
t.is(pluginConf.conf.subConf, 'originalConf');
t.is(options.globalConf.globalSubConf, 'originalGlobalConf');
});
test('Prevent plugins to modify its input', async t => {
const pluginFunction = stub().callsFake((pluginConfig, options) => {
options.param.subParam = 'otherParam';
});
const input = {param: {subParam: 'originalSubParam'}};
const plugin = normalize('', {}, {}, pluginFunction, t.context.logger);
const input = {param: {subParam: 'originalSubParam'}, options: {}};
const plugin = normalize({cwd, options: {}, logger: t.context.logger}, '', pluginFunction, {});
await plugin(input);
t.is(input.param.subParam, 'originalSubParam');
});
test('Return noop if the plugin is not defined', t => {
const plugin = normalize();
const plugin = normalize({cwd, options: {}, logger: t.context.logger});
t.is(plugin, noop);
});
test('Always pass a defined "pluginConfig" for plugin defined with string', async t => {
// Call the normalize function with the path of a plugin that returns its config
const plugin = normalize('', {}, {}, './test/fixtures/plugin-result-config', t.context.logger);
const pluginResult = await plugin();
const plugin = normalize(
{cwd, options: {}, logger: t.context.logger},
'',
'./test/fixtures/plugin-result-config',
{}
);
const pluginResult = await plugin({options: {}});
t.deepEqual(pluginResult.pluginConfig, {});
});
test('Always pass a defined "pluginConfig" for plugin defined with path', async t => {
// Call the normalize function with the path of a plugin that returns its config
const plugin = normalize('', {}, {}, {path: './test/fixtures/plugin-result-config'}, t.context.logger);
const pluginResult = await plugin();
const plugin = normalize(
{cwd, options: {}, logger: t.context.logger},
'',
{path: './test/fixtures/plugin-result-config'},
{}
);
const pluginResult = await plugin({options: {}});
t.deepEqual(pluginResult.pluginConfig, {});
});
test('Throws an error if the plugin return an object without the expected plugin function', t => {
const error = t.throws(() => normalize('inexistantPlugin', {}, {}, './test/fixtures/multi-plugin', t.context.logger));
const error = t.throws(() =>
normalize({cwd, options: {}, logger: t.context.logger}, 'inexistantPlugin', './test/fixtures/multi-plugin', {})
);
t.is(error.code, 'EPLUGIN');
t.is(error.name, 'SemanticReleaseError');
t.truthy(error.message);
t.truthy(error.details);
});
test('Throws an error if the plugin is not found', t => {
const error = t.throws(() => normalize('inexistantPlugin', {}, {}, 'non-existing-path', t.context.logger), Error);
const error = t.throws(
() => normalize({cwd, options: {}, logger: t.context.logger}, 'inexistantPlugin', 'non-existing-path', {}),
Error
);
t.is(error.message, "Cannot find module 'non-existing-path'");
t.is(error.code, 'MODULE_NOT_FOUND');
+10 -28
View File
@@ -18,24 +18,6 @@ test('Execute each function in series passing the same input', async t => {
t.true(step2.calledBefore(step3));
});
test('With one step, returns the step values rather than an Array ', async t => {
const step1 = stub().resolves(1);
const result = await pipeline([step1])(0);
t.deepEqual(result, 1);
t.true(step1.calledWith(0));
});
test('With one step, throws the error rather than an AggregateError ', async t => {
const error = new Error('test error 1');
const step1 = stub().rejects(error);
const thrown = await t.throws(pipeline([step1])(0));
t.is(error, thrown);
});
test('Execute each function in series passing a transformed input from "getNextInput"', async t => {
const step1 = stub().resolves(1);
const step2 = stub().resolves(2);
@@ -43,7 +25,7 @@ test('Execute each function in series passing a transformed input from "getNextI
const step4 = stub().resolves(4);
const getNextInput = (lastResult, result) => lastResult + result;
const result = await pipeline([step1, step2, step3, step4])(0, {settleAll: false, getNextInput});
const result = await pipeline([step1, step2, step3, step4], {settleAll: false, getNextInput})(0);
t.deepEqual(result, [1, 2, 3, 4]);
t.true(step1.calledWith(0));
@@ -62,7 +44,7 @@ test('Execute each function in series passing the "lastResult" and "result" to "
const step4 = stub().resolves(4);
const getNextInput = stub().returnsArg(0);
const result = await pipeline([step1, step2, step3, step4])(5, {settleAll: false, getNextInput});
const result = await pipeline([step1, step2, step3, step4], {settleAll: false, getNextInput})(5);
t.deepEqual(result, [1, 2, 3, 4]);
t.deepEqual(getNextInput.args, [[5, 1], [5, 2], [5, 3], [5, 4]]);
@@ -76,7 +58,7 @@ test('Execute each function in series calling "transform" to modify the results'
const getNextInput = stub().returnsArg(0);
const transform = stub().callsFake(result => result + 1);
const result = await pipeline([step1, step2, step3, step4])(5, {getNextInput, transform});
const result = await pipeline([step1, step2, step3, step4], {getNextInput, transform})(5);
t.deepEqual(result, [1 + 1, 2 + 1, 3 + 1, 4 + 1]);
t.deepEqual(getNextInput.args, [[5, 1 + 1], [5, 2 + 1], [5, 3 + 1], [5, 4 + 1]]);
@@ -90,18 +72,18 @@ test('Execute each function in series calling "transform" to modify the results
const getNextInput = stub().returnsArg(0);
const transform = stub().callsFake(result => result + 1);
const result = await pipeline([step1, step2, step3, step4])(5, {settleAll: true, getNextInput, transform});
const result = await pipeline([step1, step2, step3, step4], {settleAll: true, getNextInput, transform})(5);
t.deepEqual(result, [1 + 1, 2 + 1, 3 + 1, 4 + 1]);
t.deepEqual(getNextInput.args, [[5, 1 + 1], [5, 2 + 1], [5, 3 + 1], [5, 4 + 1]]);
});
test('Stop execution and throw error is a step rejects', async t => {
test('Stop execution and throw error if a step rejects', async t => {
const step1 = stub().resolves(1);
const step2 = stub().rejects(new Error('test error'));
const step3 = stub().resolves(3);
const error = await t.throws(pipeline([step1, step2, step3])(0), Error);
const error = await t.throwsAsync(pipeline([step1, step2, step3])(0), Error);
t.is(error.message, 'test error');
t.true(step1.calledWith(0));
t.true(step2.calledWith(0));
@@ -116,7 +98,7 @@ test('Throw all errors from the first step throwing an AggregateError', async t
const step2 = stub().rejects(new AggregateError([error1, error2]));
const step3 = stub().resolves(3);
const errors = await t.throws(pipeline([step1, step2, step3])(0));
const errors = await t.throwsAsync(pipeline([step1, step2, step3])(0));
t.deepEqual([...errors], [error1, error2]);
t.true(step1.calledWith(0));
@@ -131,7 +113,7 @@ test('Execute all even if a Promise rejects', async t => {
const step2 = stub().rejects(error1);
const step3 = stub().rejects(error2);
const errors = await t.throws(pipeline([step1, step2, step3])(0, {settleAll: true}));
const errors = await t.throwsAsync(pipeline([step1, step2, step3], {settleAll: true})(0));
t.deepEqual([...errors], [error1, error2]);
t.true(step1.calledWith(0));
@@ -147,7 +129,7 @@ test('Throw all errors from all steps throwing an AggregateError', async t => {
const step1 = stub().rejects(new AggregateError([error1, error2]));
const step2 = stub().rejects(new AggregateError([error3, error4]));
const errors = await t.throws(pipeline([step1, step2])(0, {settleAll: true}));
const errors = await t.throwsAsync(pipeline([step1, step2], {settleAll: true})(0));
t.deepEqual([...errors], [error1, error2, error3, error4]);
t.true(step1.calledWith(0));
@@ -163,7 +145,7 @@ test('Execute each function in series passing a transformed input even if a step
const step4 = stub().resolves(4);
const getNextInput = (prevResult, result) => prevResult + result;
const errors = await t.throws(pipeline([step1, step2, step3, step4])(0, {settleAll: true, getNextInput}));
const errors = await t.throwsAsync(pipeline([step1, step2, step3, step4], {settleAll: true, getNextInput})(0));
t.deepEqual([...errors], [error2, error3]);
t.true(step1.calledWith(0));
+216 -53
View File
@@ -11,16 +11,12 @@ const cwd = process.cwd();
test.beforeEach(t => {
// Stub the logger functions
t.context.log = stub();
t.context.logger = {log: t.context.log};
});
test.afterEach.always(() => {
// Restore the current working directory
process.chdir(cwd);
t.context.success = stub();
t.context.logger = {log: t.context.log, success: t.context.success, scope: () => t.context.logger};
});
test('Export default plugins', t => {
const plugins = getPlugins({}, {}, t.context.logger);
const plugins = getPlugins({cwd, options: {}, logger: t.context.logger}, {});
// Verify the module returns a function for each plugin
t.is(typeof plugins.verifyConditions, 'function');
@@ -33,16 +29,19 @@ test('Export default plugins', t => {
t.is(typeof plugins.fail, 'function');
});
test('Export plugins based on config', t => {
test('Export plugins based on steps config', t => {
const plugins = getPlugins(
{
verifyConditions: ['./test/fixtures/plugin-noop', {path: './test/fixtures/plugin-noop'}],
generateNotes: './test/fixtures/plugin-noop',
analyzeCommits: {path: './test/fixtures/plugin-noop'},
verifyRelease: () => {},
cwd,
logger: t.context.logger,
options: {
verifyConditions: ['./test/fixtures/plugin-noop', {path: './test/fixtures/plugin-noop'}],
generateNotes: './test/fixtures/plugin-noop',
analyzeCommits: {path: './test/fixtures/plugin-noop'},
verifyRelease: () => {},
},
},
{},
t.context.logger
{}
);
// Verify the module returns a function for each plugin
@@ -56,24 +55,108 @@ test('Export plugins based on config', t => {
t.is(typeof plugins.fail, 'function');
});
test.serial('Export plugins loaded from the dependency of a shareable config module', async t => {
const temp = tempy.directory();
test('Export plugins based on "plugins" config (array)', async t => {
const plugin1 = {verifyConditions: stub(), publish: stub()};
const plugin2 = {verifyConditions: stub(), verifyRelease: stub()};
const plugins = getPlugins(
{cwd, logger: t.context.logger, options: {plugins: [plugin1, [plugin2, {}]], verifyRelease: () => {}}},
{}
);
await plugins.verifyConditions({options: {}});
t.true(plugin1.verifyConditions.calledOnce);
t.true(plugin2.verifyConditions.calledOnce);
await plugins.publish({options: {}});
t.true(plugin1.publish.calledOnce);
await plugins.verifyRelease({options: {}});
t.true(plugin2.verifyRelease.notCalled);
// Verify the module returns a function for each plugin
t.is(typeof plugins.verifyConditions, 'function');
t.is(typeof plugins.analyzeCommits, 'function');
t.is(typeof plugins.verifyRelease, 'function');
t.is(typeof plugins.generateNotes, 'function');
t.is(typeof plugins.prepare, 'function');
t.is(typeof plugins.publish, 'function');
t.is(typeof plugins.success, 'function');
t.is(typeof plugins.fail, 'function');
});
test('Export plugins based on "plugins" config (single definition)', async t => {
const plugin1 = {verifyConditions: stub(), publish: stub()};
const plugins = getPlugins({cwd, logger: t.context.logger, options: {plugins: plugin1}}, {});
await plugins.verifyConditions({options: {}});
t.true(plugin1.verifyConditions.calledOnce);
await plugins.publish({options: {}});
t.true(plugin1.publish.calledOnce);
// Verify the module returns a function for each plugin
t.is(typeof plugins.verifyConditions, 'function');
t.is(typeof plugins.analyzeCommits, 'function');
t.is(typeof plugins.verifyRelease, 'function');
t.is(typeof plugins.generateNotes, 'function');
t.is(typeof plugins.prepare, 'function');
t.is(typeof plugins.publish, 'function');
t.is(typeof plugins.success, 'function');
t.is(typeof plugins.fail, 'function');
});
test('Merge global options, "plugins" options and step options', async t => {
const plugin1 = [{verifyConditions: stub(), publish: stub()}, {pluginOpt1: 'plugin1'}];
const plugin2 = [{verifyConditions: stub()}, {pluginOpt2: 'plugin2'}];
const plugin3 = [stub(), {pluginOpt3: 'plugin3'}];
const plugins = getPlugins(
{
cwd,
logger: t.context.logger,
options: {globalOpt: 'global', plugins: [plugin1, plugin2], verifyRelease: [plugin3]},
},
{}
);
await plugins.verifyConditions({options: {}});
t.deepEqual(plugin1[0].verifyConditions.args[0][0], {globalOpt: 'global', pluginOpt1: 'plugin1'});
t.deepEqual(plugin2[0].verifyConditions.args[0][0], {globalOpt: 'global', pluginOpt2: 'plugin2'});
await plugins.publish({options: {}});
t.deepEqual(plugin1[0].publish.args[0][0], {globalOpt: 'global', pluginOpt1: 'plugin1'});
await plugins.verifyRelease({options: {}});
t.deepEqual(plugin3[0].args[0][0], {globalOpt: 'global', pluginOpt3: 'plugin3'});
});
test('Unknown steps of plugins configured in "plugins" are ignored', t => {
const plugin1 = {verifyConditions: () => {}, unknown: () => {}};
const plugins = getPlugins({cwd, logger: t.context.logger, options: {plugins: [plugin1]}}, {});
t.is(typeof plugins.verifyConditions, 'function');
t.is(plugins.unknown, undefined);
});
test('Export plugins loaded from the dependency of a shareable config module', async t => {
const cwd = tempy.directory();
await copy(
'./test/fixtures/plugin-noop.js',
path.join(temp, 'node_modules/shareable-config/node_modules/custom-plugin/index.js')
path.resolve(cwd, 'node_modules/shareable-config/node_modules/custom-plugin/index.js')
);
await outputFile(path.join(temp, 'node_modules/shareable-config/index.js'), '');
process.chdir(temp);
await outputFile(path.resolve(cwd, 'node_modules/shareable-config/index.js'), '');
const plugins = getPlugins(
{
verifyConditions: ['custom-plugin', {path: 'custom-plugin'}],
generateNotes: 'custom-plugin',
analyzeCommits: {path: 'custom-plugin'},
verifyRelease: () => {},
cwd,
logger: t.context.logger,
options: {
verifyConditions: ['custom-plugin', {path: 'custom-plugin'}],
generateNotes: 'custom-plugin',
analyzeCommits: {path: 'custom-plugin'},
verifyRelease: () => {},
},
},
{'custom-plugin': 'shareable-config'},
t.context.logger
{'custom-plugin': 'shareable-config'}
);
// Verify the module returns a function for each plugin
@@ -87,21 +170,23 @@ test.serial('Export plugins loaded from the dependency of a shareable config mod
t.is(typeof plugins.fail, 'function');
});
test.serial('Export plugins loaded from the dependency of a shareable config file', async t => {
const temp = tempy.directory();
await copy('./test/fixtures/plugin-noop.js', path.join(temp, 'plugin/plugin-noop.js'));
await outputFile(path.join(temp, 'shareable-config.js'), '');
process.chdir(temp);
test('Export plugins loaded from the dependency of a shareable config file', async t => {
const cwd = tempy.directory();
await copy('./test/fixtures/plugin-noop.js', path.resolve(cwd, 'plugin/plugin-noop.js'));
await outputFile(path.resolve(cwd, 'shareable-config.js'), '');
const plugins = getPlugins(
{
verifyConditions: ['./plugin/plugin-noop', {path: './plugin/plugin-noop'}],
generateNotes: './plugin/plugin-noop',
analyzeCommits: {path: './plugin/plugin-noop'},
verifyRelease: () => {},
cwd,
logger: t.context.logger,
options: {
verifyConditions: ['./plugin/plugin-noop', {path: './plugin/plugin-noop'}],
generateNotes: './plugin/plugin-noop',
analyzeCommits: {path: './plugin/plugin-noop'},
verifyRelease: () => {},
},
},
{'./plugin/plugin-noop': './shareable-config.js'},
t.context.logger
{'./plugin/plugin-noop': './shareable-config.js'}
);
// Verify the module returns a function for each plugin
@@ -116,41 +201,119 @@ test.serial('Export plugins loaded from the dependency of a shareable config fil
});
test('Use default when only options are passed for a single plugin', t => {
const plugins = getPlugins({generateNotes: {}, analyzeCommits: {}}, {}, t.context.logger);
const analyzeCommits = {};
const generateNotes = {};
const publish = {};
const success = () => {};
const fail = [() => {}];
const plugins = getPlugins(
{
cwd,
logger: t.context.logger,
options: {
plugins: ['@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator'],
analyzeCommits,
generateNotes,
publish,
success,
fail,
},
},
{}
);
// Verify the module returns a function for each plugin
t.is(typeof plugins.generateNotes, 'function');
t.is(typeof plugins.analyzeCommits, 'function');
t.is(typeof plugins.generateNotes, 'function');
t.is(typeof plugins.success, 'function');
t.is(typeof plugins.fail, 'function');
// Verify only the plugins defined as an object with no `path` are set to the default value
t.falsy(success.path);
t.falsy(fail.path);
});
test('Merge global options with plugin options', async t => {
const plugins = getPlugins(
{
globalOpt: 'global',
otherOpt: 'globally-defined',
verifyRelease: {path: './test/fixtures/plugin-result-config', localOpt: 'local', otherOpt: 'locally-defined'},
cwd,
logger: t.context.logger,
options: {
globalOpt: 'global',
otherOpt: 'globally-defined',
verifyRelease: {path: './test/fixtures/plugin-result-config', localOpt: 'local', otherOpt: 'locally-defined'},
},
},
{},
t.context.logger
{}
);
const result = await plugins.verifyRelease();
const [result] = await plugins.verifyRelease({options: {}});
t.deepEqual(result.pluginConfig, {localOpt: 'local', globalOpt: 'global', otherOpt: 'locally-defined'});
});
test('Throw an error if plugins configuration are missing a path for plugin pipeline', t => {
const errors = [...t.throws(() => getPlugins({verifyConditions: {}}, {}, t.context.logger))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'EPLUGINCONF');
});
test('Throw an error if an array of plugin configuration is missing a path for plugin pipeline', t => {
test('Throw an error for each invalid plugin configuration', t => {
const errors = [
...t.throws(() => getPlugins({verifyConditions: [{path: '@semantic-release/npm'}, {}]}, {}, t.context.logger)),
...t.throws(() =>
getPlugins(
{
cwd,
logger: t.context.logger,
options: {
plugins: ['@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator'],
verifyConditions: 1,
analyzeCommits: [],
verifyRelease: [{}],
generateNotes: [{path: null}],
},
},
{}
)
),
];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'EPLUGINCONF');
t.is(errors[1].name, 'SemanticReleaseError');
t.is(errors[1].code, 'EPLUGINCONF');
t.is(errors[2].name, 'SemanticReleaseError');
t.is(errors[2].code, 'EPLUGINCONF');
t.is(errors[3].name, 'SemanticReleaseError');
t.is(errors[3].code, 'EPLUGINCONF');
});
test('Throw EPLUGINSCONF error if the "plugins" option contains an old plugin definition (returns a function)', t => {
const errors = [
...t.throws(() =>
getPlugins(
{
cwd,
logger: t.context.logger,
options: {plugins: ['./test/fixtures/multi-plugin', './test/fixtures/plugin-noop', () => {}]},
},
{}
)
),
];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'EPLUGINSCONF');
t.is(errors[1].name, 'SemanticReleaseError');
t.is(errors[1].code, 'EPLUGINSCONF');
});
test('Throw EPLUGINSCONF error for each invalid definition if the "plugins" option', t => {
const errors = [
...t.throws(() =>
getPlugins({cwd, logger: t.context.logger, options: {plugins: [1, {path: 1}, [() => {}, {}, {}]]}}, {})
),
];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'EPLUGINSCONF');
t.is(errors[1].name, 'SemanticReleaseError');
t.is(errors[1].code, 'EPLUGINSCONF');
t.is(errors[2].name, 'SemanticReleaseError');
t.is(errors[2].code, 'EPLUGINSCONF');
});
+214
View File
@@ -0,0 +1,214 @@
import test from 'ava';
import {validatePlugin, validateStep, loadPlugin, parseConfig} from '../../lib/plugins/utils';
test('validatePlugin', t => {
const path = 'plugin-module';
const options = {option1: 'value1', option2: 'value2'};
t.true(validatePlugin(path), 'String definition');
t.true(validatePlugin({publish: () => {}}), 'Object definition');
t.true(validatePlugin([path]), 'Array definition');
t.true(validatePlugin([path, options]), 'Array definition with options');
t.true(validatePlugin([{publish: () => {}}, options]), 'Array definition with options and path as object');
t.true(validatePlugin({path}), 'Object with path definition');
t.true(validatePlugin({path, ...options}), 'Object with path definition with options');
t.true(
validatePlugin({path: {publish: () => {}}, ...options}),
'Object with path definition with options and path as object'
);
t.false(validatePlugin(1), 'String definition, wrong path');
t.false(validatePlugin([]), 'Array definition, missing path');
t.false(validatePlugin([path, options, {}]), 'Array definition, additional parameter');
t.false(validatePlugin([1]), 'Array definition, wrong path');
t.false(validatePlugin([path, 1]), 'Array definition, wrong options');
t.false(validatePlugin({path: 1}), 'Object definition, wrong path');
});
test('validateStep: optional plugin configuration', t => {
const type = {multiple: true, required: false};
// Empty config
t.true(validateStep(type));
t.true(validateStep(type, []));
// Single value definition
t.true(validateStep(type, 'plugin-path.js'));
t.true(validateStep(type, () => {}));
t.true(validateStep(type, ['plugin-path.js']));
t.true(validateStep(type, [() => {}]));
t.false(validateStep(type, {}));
t.false(validateStep(type, [{}]));
// Array type definition
t.true(validateStep(type, [['plugin-path.js']]));
t.true(validateStep(type, [['plugin-path.js', {options: 'value'}]]));
t.true(validateStep(type, [[() => {}, {options: 'value'}]]));
t.false(validateStep(type, [['plugin-path.js', 1]]));
// Object type definition
t.true(validateStep(type, {path: 'plugin-path.js'}));
t.true(validateStep(type, {path: 'plugin-path.js', options: 'value'}));
t.true(validateStep(type, {path: () => {}, options: 'value'}));
t.false(validateStep(type, {path: null}));
// Considered as an Array of 2 definitions and not as one Array definition in case of a muliple plugin type
t.false(validateStep(type, [() => {}, {options: 'value'}]));
t.false(validateStep(type, ['plugin-path.js', {options: 'value'}]));
// Multiple definitions
t.true(
validateStep(type, [
'plugin-path.js',
() => {},
['plugin-path.js'],
['plugin-path.js', {options: 'value'}],
[() => {}, {options: 'value'}],
{path: 'plugin-path.js'},
{path: 'plugin-path.js', options: 'value'},
{path: () => {}, options: 'value'},
])
);
t.false(
validateStep(type, [
'plugin-path.js',
() => {},
['plugin-path.js'],
['plugin-path.js', 1],
[() => {}, {options: 'value'}],
{path: 'plugin-path.js'},
{path: 'plugin-path.js', options: 'value'},
{path: () => {}, options: 'value'},
])
);
t.false(
validateStep(type, [
'plugin-path.js',
{},
['plugin-path.js'],
['plugin-path.js', {options: 'value'}],
[() => {}, {options: 'value'}],
{path: 'plugin-path.js'},
{path: 'plugin-path.js', options: 'value'},
{path: () => {}, options: 'value'},
])
);
t.false(
validateStep(type, [
'plugin-path.js',
() => {},
['plugin-path.js'],
['plugin-path.js', {options: 'value'}],
[() => {}, {options: 'value'}],
{path: null},
{path: 'plugin-path.js', options: 'value'},
{path: () => {}, options: 'value'},
])
);
});
test('validateStep: required plugin configuration', t => {
const type = {required: true};
// Empty config
t.false(validateStep(type));
t.false(validateStep(type, []));
// Single value definition
t.true(validateStep(type, 'plugin-path.js'));
t.true(validateStep(type, () => {}));
t.true(validateStep(type, ['plugin-path.js']));
t.true(validateStep(type, [() => {}]));
t.false(validateStep(type, {}));
t.false(validateStep(type, [{}]));
// Array type definition
t.true(validateStep(type, [['plugin-path.js']]));
t.true(validateStep(type, [['plugin-path.js', {options: 'value'}]]));
t.true(validateStep(type, [[() => {}, {options: 'value'}]]));
t.false(validateStep(type, [['plugin-path.js', 1]]));
// Object type definition
t.true(validateStep(type, {path: 'plugin-path.js'}));
t.true(validateStep(type, {path: 'plugin-path.js', options: 'value'}));
t.true(validateStep(type, {path: () => {}, options: 'value'}));
t.false(validateStep(type, {path: null}));
// Considered as an Array of 2 definitions and not as one Array definition in the case of a muliple plugin type
t.false(validateStep(type, [() => {}, {options: 'value'}]));
t.false(validateStep(type, ['plugin-path.js', {options: 'value'}]));
// Multiple definitions
t.true(
validateStep(type, [
'plugin-path.js',
() => {},
['plugin-path.js'],
['plugin-path.js', {options: 'value'}],
[() => {}, {options: 'value'}],
{path: 'plugin-path.js'},
{path: 'plugin-path.js', options: 'value'},
{path: () => {}, options: 'value'},
])
);
t.false(
validateStep(type, [
'plugin-path.js',
() => {},
['plugin-path.js'],
['plugin-path.js', 1],
[() => {}, {options: 'value'}],
{path: 'plugin-path.js'},
{path: 'plugin-path.js', options: 'value'},
{path: () => {}, options: 'value'},
])
);
t.false(
validateStep(type, [
'plugin-path.js',
{},
['plugin-path.js'],
['plugin-path.js', {options: 'value'}],
[() => {}, {options: 'value'}],
{path: 'plugin-path.js'},
{path: 'plugin-path.js', options: 'value'},
{path: () => {}, options: 'value'},
])
);
t.false(
validateStep(type, [
'plugin-path.js',
() => {},
['plugin-path.js'],
['plugin-path.js', {options: 'value'}],
[() => {}, {options: 'value'}],
{path: null},
{path: 'plugin-path.js', options: 'value'},
{path: () => {}, options: 'value'},
])
);
});
test('loadPlugin', t => {
const cwd = process.cwd();
const func = () => {};
t.is(require('../fixtures/plugin-noop'), loadPlugin({cwd: './test/fixtures'}, './plugin-noop', {}), 'From cwd');
t.is(
require('../fixtures/plugin-noop'),
loadPlugin({cwd}, './plugin-noop', {'./plugin-noop': './test/fixtures'}),
'From a shareable config context'
);
t.is(func, loadPlugin({cwd}, func, {}), 'Defined as a function');
});
test('parseConfig', t => {
const path = 'plugin-module';
const options = {option1: 'value1', option2: 'value2'};
t.deepEqual(parseConfig(path), [path, {}], 'String definition');
t.deepEqual(parseConfig({path}), [path, {}], 'Object definition');
t.deepEqual(parseConfig({path, ...options}), [path, options], 'Object definition with options');
t.deepEqual(parseConfig([path]), [path, {}], 'Array definition');
t.deepEqual(parseConfig([path, options]), [path, options], 'Array definition with options');
});
+35 -41
View File
@@ -3,83 +3,77 @@ import tempy from 'tempy';
import verify from '../lib/verify';
import {gitRepo} from './helpers/git-utils';
// Save the current process.env
const envBackup = Object.assign({}, process.env);
// Save the current working diretory
const cwd = process.cwd();
test('Throw a AggregateError', async t => {
const {cwd} = await gitRepo();
const options = {};
test.beforeEach(() => {
// Delete environment variables that could have been set on the machine running the tests
delete process.env.GIT_CREDENTIALS;
delete process.env.GH_TOKEN;
delete process.env.GITHUB_TOKEN;
delete process.env.GL_TOKEN;
delete process.env.GITLAB_TOKEN;
});
test.afterEach.always(() => {
// Restore process.env
process.env = envBackup;
// Restore the current working directory
process.chdir(cwd);
});
test.serial('Throw a AggregateError', async t => {
await gitRepo();
const errors = [...(await t.throws(verify({})))];
const errors = [...(await t.throwsAsync(verify({cwd, options})))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'ENOREPOURL');
t.truthy(errors[0].message);
t.truthy(errors[0].details);
t.is(errors[1].name, 'SemanticReleaseError');
t.is(errors[1].code, 'EINVALIDTAGFORMAT');
t.truthy(errors[1].message);
t.truthy(errors[1].details);
t.is(errors[2].name, 'SemanticReleaseError');
t.is(errors[2].code, 'ETAGNOVERSION');
t.truthy(errors[2].message);
t.truthy(errors[2].details);
});
test.serial('Throw a SemanticReleaseError if does not run on a git repository', async t => {
const dir = tempy.directory();
process.chdir(dir);
test('Throw a SemanticReleaseError if does not run on a git repository', async t => {
const cwd = tempy.directory();
const options = {};
const errors = [...(await t.throws(verify({})))];
const errors = [...(await t.throwsAsync(verify({cwd, options})))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'ENOGITREPO');
t.truthy(errors[0].message);
t.truthy(errors[0].details);
});
test.serial('Throw a SemanticReleaseError if the "tagFormat" is not valid', async t => {
const repositoryUrl = await gitRepo(true);
test('Throw a SemanticReleaseError if the "tagFormat" is not valid', async t => {
const {cwd, repositoryUrl} = await gitRepo(true);
const options = {repositoryUrl, tagFormat: `?\${version}`};
const errors = [...(await t.throws(verify(options, 'master', t.context.logger)))];
const errors = [...(await t.throwsAsync(verify({cwd, options})))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'EINVALIDTAGFORMAT');
t.truthy(errors[0].message);
t.truthy(errors[0].details);
});
test.serial('Throw a SemanticReleaseError if the "tagFormat" does not contains the "version" variable', async t => {
const repositoryUrl = await gitRepo(true);
test('Throw a SemanticReleaseError if the "tagFormat" does not contains the "version" variable', async t => {
const {cwd, repositoryUrl} = await gitRepo(true);
const options = {repositoryUrl, tagFormat: 'test'};
const errors = [...(await t.throws(verify(options, 'master', t.context.logger)))];
const errors = [...(await t.throwsAsync(verify({cwd, options})))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'ETAGNOVERSION');
t.truthy(errors[0].message);
t.truthy(errors[0].details);
});
test.serial('Throw a SemanticReleaseError if the "tagFormat" contains multiple "version" variables', async t => {
const repositoryUrl = await gitRepo(true);
test('Throw a SemanticReleaseError if the "tagFormat" contains multiple "version" variables', async t => {
const {cwd, repositoryUrl} = await gitRepo(true);
const options = {repositoryUrl, tagFormat: `\${version}v\${version}`};
const errors = [...(await t.throws(verify(options)))];
const errors = [...(await t.throwsAsync(verify({cwd, options})))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'ETAGNOVERSION');
t.truthy(errors[0].message);
t.truthy(errors[0].details);
});
test.serial('Return "true" if all verification pass', async t => {
const repositoryUrl = await gitRepo(true);
const options = {repositoryUrl, tagFormat: `v\${version}`, branch: 'master'};
test('Return "true" if all verification pass', async t => {
const {cwd, repositoryUrl} = await gitRepo(true);
const options = {repositoryUrl, tagFormat: `v\${version}`};
await t.notThrows(verify(options));
await t.notThrowsAsync(verify({cwd, options}));
});