Compare commits

...
135 Commits
Author SHA1 Message Date
Arthur ChaloinandGitHub 2bf377194e fix: use valid git credentials when multiple are provided (#1669) 2020-10-29 11:09:19 -07:00
Arthur ChaloinandGitHub 77a75f072b fix: don't parse port as part of the path in repository URLs (#1671) 2020-10-28 15:53:03 -07:00
Guillaume GautreauandGitHub d74ffef788 docs: add npm-deprecate-old-versions in plugins list (#1667) 2020-10-23 14:26:20 -07:00
Matt Travi 3abcbaf256 Revert "feat: throw an Error if package.json has duplicate "repository" key (#1656)"
This reverts commit b8fb35c7e1.

fixes #1657
2020-10-12 08:21:24 -05:00
Gregor MartynusandGitHub b8fb35c7e1 feat: throw an Error if package.json has duplicate "repository" key (#1656) 2020-10-11 14:59:51 -07:00
Graeme CasselsandGitHub 18e35b28ad docs: reorder default plugins list (#1650)
Reorder the list default plugins from alphabetical to order of execution.
2020-10-04 11:52:17 -07:00
Micael JarniacandGitHub e35e5bb73f docs(contributing): fix commit message examples (#1648)
It had the backticks (`\``) around the messages, but I think they were unintentional, probably a leftover from copy-pasting.
2020-10-02 15:12:20 -07:00
Gregor MartynusandGitHub 311c46580f docs(README): welcome @travi, add alumni section 2020-09-25 15:31:11 -07:00
b4c5d0a436 fix: add logging for when ssh falls back to http (#1639)
Co-authored-by: thomas williams <thomas.williams@thetradedesk.com>
2020-09-17 10:15:04 -07:00
Micael JarniacandGitHub c982249f87 docs(contributing): typo fix (#1638) 2020-09-16 11:39:04 -07:00
Joep KockelkornandGitHub 9635f50da6 docs: improve github actions recipe on git plugin (#1626) 2020-09-06 10:38:29 -07:00
ylemkimonandGitHub d036a899d5 ci(docs): use actions/checkout@v2 (#1620) 2020-08-17 16:26:13 -07:00
AbdelRahman WahdanandGitHub 9303d1dba0 docs(resources.md): added more sematnic release article (#1610) 2020-07-30 17:17:36 -07:00
kopalandGitHub b72cdb331b docs(configuration.md): Updated documentation for dry-run feature of semantic Release (#1607) 2020-07-29 15:25:32 -07:00
Ahmed HassaneinandGitHub ee44ee80c8 docs(github-actions): suggest action_dispatch as trigger (#1605) 2020-07-28 06:57:05 -07:00
Rylan CollinsandGitHub b24d2474b1 docs: add semantic-release-rubygem to community plugins (#1602) 2020-07-22 16:35:05 -07:00
Gavin StaniforthandGitHub 6d118c6c14 docs: be clear about what module of semantic-release handles updating the package.json (#1601) 2020-07-12 12:31:40 -07:00
Shun KakinokiandMatt Travi b5c9dea215 docs: update github documentation to docs.github.com 2020-07-09 22:44:15 -05:00
kopalandGitHub 1405b94296 docs: added recipe for Jenkins CI configuration (#1) (#1591) 2020-06-30 14:54:33 -07:00
Emmanuel OgbiziandGitHub 0f0c650b41 fix: use correct ci branch context (#1521) 2020-06-25 09:30:12 -07:00
Sven LiebigandGitHub a4658016d9 feat(bitbucket-basic-auth): support for bitbucket server basic auth (#1578) 2020-06-22 11:26:24 -07:00
Daniel WirtzandGitHub 6d48663a77 build(gitattributes): eol=lf for Windows users (#1575) 2020-06-15 09:16:33 -07:00
Nicholas ShineandGitHub eed1d3c8cb fix: prevent false positive secret replacement for Golang projects (#1562) 2020-05-24 11:53:00 -07:00
Daniel TschinderandGitHub 5f3a8bb14e docs: Recommend using npx instead of installing globally (#1563) 2020-05-24 10:20:58 -07:00
Lucas ShadlerandGitHub 0ef52e7a5f docs: adjust minor typos (#1554) 2020-05-19 14:15:26 -07:00
Aaron BatiloandGitHub a62bc7cd85 docs(GitHub Actions): Add alternative trigger options (#1471)
These options work the same as the curl command, but use a GitHub App so there's no need to provision and manage a Personal Access Token in order to trigger the Semantic Release action.
2020-05-06 11:50:31 -07:00
solidnerdandGregor Martynus 22321cff1c docs: update recipe for gitlab-ci with Node.js >=10.18
Signed-off-by: solidnerd <niclas@mietz.io>
2020-04-22 15:15:05 -07:00
greenkeeper[bot]andGitHub d64db31e76 fix(package): update marked to version 1.0.0 (#1534) 2020-04-21 13:04:21 -07:00
greenkeeper[bot]andGitHub 2322a7028f build(deps): update got to version 11.0.0 (#1533) 2020-04-20 10:17:39 -07:00
Gregor MartynusandGitHub 431d571a7b fix: adapt for semver to version 7.3.2 (part II) (#1530) 2020-04-16 14:42:25 -07:00
Gregor Martynus 0363790b8a fix: adapt for semver to version 7.3.2
see https://github.com/npm/node-semver/issues/322
2020-04-16 13:54:47 -07:00
greenkeeper[bot]andGitHub 93cb76653d chore(package): update semver to version 7.3.2 2020-04-14 17:46:03 +00:00
Gregor Martynus caa3526caa style: xo 2020-04-08 15:13:33 -07:00
greenkeeper[bot]andGitHub ed5f26d10b chore(package): update xo to version 0.29.0 2020-04-08 21:14:42 +00:00
Justin PoehneltandGitHub ff1d53df58 docs: add @google/semantic-release-replace-plugin to plugin list (#1509) 2020-04-02 16:23:31 -07:00
greenkeeper[bot]andPierre Vanduynslager f899fa9a7c chore(package): update xo to version 0.28.1
Closes #1488
2020-03-26 23:55:26 -04:00
greenkeeper[bot]andGregor Martynus b5488ae7a5 chore(package): update fs-extra to version 9.0.0 2020-03-19 11:31:14 -07:00
greenkeeper[bot]andPierre Vanduynslager da5d1cf9c6 chore(package): update tempy to version 0.5.0 2020-03-09 11:22:55 -04:00
Gaurav NelsonandGitHub da5d06cf78 docs: fix formatting for GitBook 2020-02-27 02:52:32 -05:00
greenkeeper[bot]andPierre Vanduynslager b85adbdb8f chore(package): update xo to version 0.27.0 2020-02-25 17:55:12 -05:00
greenkeeper[bot]andPierre Vanduynslager bce633a41e chore(package): update sinon to version 9.0.0 2020-02-19 18:08:22 -05:00
Pierre Vanduynslager 55be0ba2b1 fix: add repositoryUrl in logs 2020-02-17 14:09:01 -05:00
Raphaël HuchetandGitHub ec516a34bf docs(configuration): fix CLI usage with --branches option (#1465) 2020-02-17 10:05:33 -08:00
greenkeeper[bot]andGregor Martynus 5f89e6bfc0 chore(package): update nock to version 12.0.0 2020-02-16 13:54:44 -08:00
greenkeeper[bot]andPierre Vanduynslager 195e5b18c7 chore(package): update xo to version 0.26.0 2020-02-12 20:09:31 -05:00
Pierre Vanduynslager e7bede1866 fix: pass a branch name to getGitAuthUrl 2020-02-12 19:56:12 -05:00
greenkeeper[bot]andPierre Vanduynslager 8426b4203e chore(package): update tempy to version 0.4.0 2020-02-12 16:09:26 -05:00
Gregor MartynusandGitHub 804fc2a943 docs(Troubleshooting): release not found in prereleases branch (e.g. beta) after rebase on master) (#1444) 2020-02-08 11:03:48 -08:00
greenkeeper[bot]andPierre Vanduynslager 389e3316e9 chore(package): update got to version 10.5.2 2020-02-07 19:00:38 -05:00
Pierre Vanduynslager a93c96fec9 revert: fix: allow plugins to set environment variables to be used by other plugins
This reverts commit 68f7e928f9.
2020-02-07 17:50:14 -05:00
Pierre VanduynslagerandGregor Martynus 68f7e928f9 fix: allow plugins to set environment variables to be used by other plugins 2020-02-07 13:51:20 -08:00
Gregor MartynusandGitHub 8ce2d6e834 fix(package): update marked-terminal to version 4.0.0 2020-01-31 11:14:54 -05:00
Pierre Vanduynslager 45695b9183 fix(package): update @semantic-release/commit-analyzer to version 8.0.0 2020-01-28 11:31:01 -05:00
Pierre Vanduynslager 3c7b114eed fix(package): update @semantic-release/release-notes-generator to version 9.0.0 2020-01-28 11:31:01 -05:00
Pierre Vanduynslager f2b5826c0c fix(package): update @semantic-release/npm to version 7.0.0 2020-01-28 11:31:01 -05:00
greenkeeper[bot]andPierre Vanduynslager c48bd3ac36 fix(package): update @semantic-release/github to version 7.0.0 2020-01-28 11:31:01 -05:00
Pierre Vanduynslager bc97537c97 test: fix copy/paste typo in test titles 2020-01-28 10:33:24 -05:00
Mario LubenkaandPierre Vanduynslager c7e461dc1c docs: add @saithodev/semantic-release-gitea to community plugins 2020-01-27 14:23:38 -05:00
greenkeeper[bot]andPierre Vanduynslager ef1b8a0b91 chore(package): update ava to version 3.1.0 2020-01-27 13:52:59 -05:00
Pierre Vanduynslager bec57cd2ed chore: require Node.js >=10.18
BREAKING CHANGE: Require Node.js >= 10.18
2020-01-27 13:52:59 -05:00
Jozef CipaandPierre Vanduynslager c6b10766a7 fix: correct error when remote repository has no branches 2020-01-27 11:59:38 -05:00
Pierre Vanduynslager b54b20d412 fix: use --no-verify when testing the Git permissions 2020-01-22 13:08:59 -05:00
DanielHabenichtandGregor Martynus 88fe81915b docs: fix typo
just a little type fist -> first
2020-01-21 15:34:01 -08:00
Michaël De BoeyandGregor Martynus 559c152647 docs(README): update minimal required Node version / FAQ link (#1422) 2020-01-20 12:06:39 -08:00
Prokop SimekandGregor Martynus 31e78767d9 docs(travis): build on all branches by default
If you want to have more prereleases than default so you have to enable deploy on all branches. ref.: https://docs.travis-ci.com/user/deployment#examples-of-conditional-deployment
2020-01-20 08:40:16 -08:00
Daniel BasilioandGregor Martynus 8c0490d057 docs: replacing firefox plugin in the list (#1416) 2020-01-15 20:02:00 -08:00
Pierre Vanduynslager 6b5b02ea75 fix: fetch tags on repo cached by the CI 2020-01-15 13:43:44 -05:00
Pierre Vanduynslager 28b54800cd docs: correct plugin execution order 2020-01-15 11:34:26 -05:00
greenkeeper[bot]andPierre Vanduynslager 3739ab5f34 fix(package): update env-ci to version 5.0.0 2020-01-10 11:58:36 -05:00
greenkeeper[bot]andPierre Vanduynslager 11665b214d chore(package): update dependencies 2020-01-09 16:34:01 -05:00
Pierre Vanduynslager 0785a844fa fix: update plugin versions 2020-01-09 15:14:55 -05:00
Pierre Vanduynslager 152bf452e1 Merge remote-tracking branch 'origin/beta' 2020-01-09 15:08:45 -05:00
Pierre Vanduynslager 3ba8f2a929 Merge remote-tracking branch 'origin/master' into beta 2020-01-09 15:01:19 -05:00
Pierre Vanduynslager a8c747d592 feat: pass envi-ci values to plugins context 2019-12-20 19:36:02 -05:00
Pierre Vanduynslager fc70726d90 chore: add Mockserver generated file to gitignore 2019-12-20 19:36:02 -05:00
Pierre Vanduynslager fc7205d659 fix: correctly display command that errored out in logs 2019-12-15 17:47:46 -05:00
Pierre Vanduynslager 9772563a22 fix: look also for previous prerelease versions to determine the next one 2019-12-13 16:09:27 -05:00
Pierre Vanduynslager 61665be9ec fix: correct log when adding channel to tag 2019-12-10 00:23:38 -05:00
Pierre Vanduynslager a8747c4f86 fix: verify is branch is up to date by comparing remote and local HEAD 2019-12-10 00:23:38 -05:00
Pierre Vanduynslager 9a1af4de44 fix: remove unnecessary await 2019-12-10 00:23:38 -05:00
Pierre Vanduynslager 9ecc7a369c fix: increase next version on prerelease branch based on highest commit type 2019-12-09 22:33:29 -05:00
Pierre Vanduynslager c16fcc9d15 Merge branch 'master' into beta 2019-12-05 15:42:14 -05:00
Pierre Vanduynslager a373f8be14 ci: use Travis import to share config across organization 2019-12-05 15:39:31 -05:00
Pierre Vanduynslager 0716a45b7d feat: require Node.js >=10.13
BREAKING CHANGE: Require Node.js >= 10.13
2019-12-05 13:18:42 -05:00
Pierre Vanduynslager 916c2685c5 feat: allow to release any version on a branch if up to date with next branch 2019-12-04 18:00:16 -05:00
Pierre Vanduynslager 534c0dbc89 Merge branch 'master' into beta 2019-12-03 20:29:06 -05:00
Pierre Vanduynslager ec54c0be8a test: fix calls to fetch in tests 2019-12-03 20:20:04 -05:00
Pierre Vanduynslager cbef9d18da fix: modify fetch function to handle CircleCI specifics 2019-12-03 18:07:12 -05:00
Pierre Vanduynslager ecc4e663f5 docs: clarify GitLab CI/CD protected variable usage 2019-12-03 11:49:56 -05:00
Pierre Vanduynslager b2c1b2c670 feat: use Git notes to store the channels on which a version has been released
BREAKING CHANGE: this feature change the way semantic-release keep track of the channels on which a version has been released.
It now use a JSON object stored in a [Git note](https://git-scm.com/docs/git-notes) instead of Git tags formatted as v{version}@{channel}.

The tags formatted as v{version}@{channel} will now be ignored. If you have made releases with v16.0.0 on branches other than the default one you will have to update your repository.

The changes to make consist in:
- Finding all the versions that have been released on a branch other than the default one by searching for all tags formatted v{version}@{channel}
- For each of those version:
  - Create a tag without the {@channel} if none doesn't already exists
  - Add a Git note to the tag without the {@channel} containing the channels on which the version was released formatted as `{"channels":["channel1","channel2"]}` and using `null` for the default channel (for example.`{"channels":[null,"channel1","channel2"]}`)
  - Push the tags and notes
  - Update the GitHub releases that refer to a tag formatted as v{version}@{channel} to use the tag without it
  - Delete the tags formatted as v{version}@{channel}
2019-12-02 23:38:40 -05:00
Gregor MartynusandPierre Vanduynslager 29d6b5d6fa build: remove .github/airtable-crm.yml
I don't think we use that
2019-12-02 13:27:31 -05:00
Pierre Vanduynslager 2caafbaa2b fix: add a flag indicate which branch is the main one 2019-11-28 13:59:06 -05:00
Pierre Vanduynslager e722354f4f style: merge both filter functions in get-last-release 2019-11-27 15:18:23 -05:00
Pierre Vanduynslager 97748c5e25 fix: do not create tags in dry-run mode for released to add to a channel 2019-11-27 15:18:23 -05:00
Pierre Vanduynslager aec96c791f fix: correctly determine release to add to a channel
- Add only the most recent release to a channel (rather than adding all the one not added yet)
- Avoid attempting to ad the version twice in case that version is already present in multiple upper branches
2019-11-27 15:18:23 -05:00
Pierre Vanduynslager 5744c5ecd2 fix: add channel to publish success log 2019-11-27 15:18:23 -05:00
Pierre Vanduynslager 1275b8c005 revert: fix: update commit-analyzer dep version off errant publish
This reverts commit 8d8910577f.
2019-11-23 11:06:31 -05:00
Eddie LefflerandPierre Vanduynslager 8d8910577f fix: update commit-analyzer dep version off errant publish 2019-11-20 23:11:33 -05:00
EddieandPierre Vanduynslager 9890584007 fix: update plugins dependencies 2019-11-20 22:29:59 -05:00
Pierre Vanduynslager b2b7b57fbd style: prettier formatting 2019-11-20 20:25:36 -05:00
Pierre Vanduynslager 63f51ae6dd fix: allow multiple branches with same channel 2019-11-20 16:17:24 -05:00
Pierre Vanduynslager 20e7a38cdb style: prettier formatting 2019-11-20 14:16:13 -05:00
Pierre Vanduynslager e06eb82cff chore: add Mockserver generated file to gitignore 2019-11-20 14:16:13 -05:00
Pierre Vanduynslager 751a5f1349 fix: handle branch properties set to false 2019-11-20 14:16:13 -05:00
Sean DawsonandPierre Vanduynslager 9575198266 docs: add leiningen-semantic-release to list of community plugins 2019-11-18 11:29:10 -05:00
EdandPierre Vanduynslager 09754ac001 docs(shareable-configurations-list): add semantic-release-npm-github-publish 2019-11-18 11:27:53 -05:00
greenkeeper[bot]andPierre Vanduynslager 2c13136ff4 fix(package): update yargs to version 15.0.1 2019-11-17 18:13:39 -05:00
Pierre Vanduynslager 00420a83c0 fix: simplify get-tags algorithm 2019-11-08 12:35:34 -05:00
Pierre Vanduynslager 131734873e fix: throws error if the commit associated with a tag cannot be found 2019-11-07 14:33:53 -05:00
Pierre Vanduynslager de77a799a8 fix: call getTagHead only when necessary 2019-11-07 14:33:53 -05:00
greenkeeper[bot]andPierre Vanduynslager ffff100f47 fix(package): update cosmiconfig to version 6.0.0 2019-11-02 15:32:18 -04:00
Pierre Vanduynslager 56186419a7 fix(package): update micromatch to version 4.0.2 2019-11-01 22:23:18 -04:00
Pierre Vanduynslager 7a939a8970 fix: use authenticated URL to check if local branch is up to date 2019-11-01 20:44:31 -04:00
Pierre Vanduynslager 2ec856eb1d docs: add requirements section to README 2019-11-01 16:58:55 -04:00
Pierre Vanduynslager cffe9a8d33 perf: use git tag --merge <branch> to filter tags present in a branch history
BREAKING CHANGE: Git CLI version 2.7.1 or higher is now required

The `--merge` option of the `git tag` command has been added in Git version 2.7.1 and is now used by semantic-release
2019-11-01 16:58:55 -04:00
Pierre Vanduynslager 844e0b07e0 fix: remove hack to workaround GitHub Rebase & Merge 2019-11-01 16:58:55 -04:00
Pierre Vanduynslager 1edae67326 fix: display erroring git commands properly 2019-11-01 16:58:55 -04:00
Pierre Vanduynslager e5344cd89b fix(package): update micromatch to version 3.1.10 2019-10-29 13:26:41 -04:00
Pierre Vanduynslager 2c655b2328 fix(package): update p-each-series to version 2.1.0 2019-10-29 13:26:08 -04:00
Pierre Vanduynslager 581998cff0 fix(package): update semver-diff to version 3.1.1 2019-10-29 13:25:07 -04:00
Pierre Vanduynslager 65c412293e fix(package): update @semantic-release/npm to version 6.0.0-beta.3 2019-10-29 12:01:59 -04:00
Pierre Vanduynslager 6053b89af3 fix(package): update @semantic-release/github to version 5.6.0-beta.1 2019-10-29 12:01:33 -04:00
Pierre Vanduynslager 3daf78081f Merge branch 'master' into beta 2019-10-29 11:57:31 -04:00
Justin DietzandPierre Vanduynslager 9eaf9552c1 docs(contributing): correct lint fix command 2019-10-27 21:56:05 -04:00
Pierre Vanduynslager 1693073814 fix(package): update execa to version 3.2.0 2019-10-26 00:26:50 -04:00
Pierre Vanduynslager 2f3d934069 fix: require Node.js >=8.16 2019-10-26 00:26:50 -04:00
f645547f2f docs(recipes): GitHub actions (#1317)
Co-Authored-By: Natan Sągol <m@merlinnot.com>
2019-10-23 10:47:24 -07:00
Pierre Vanduynslager 3438ec5c79 test: fix typo in test examples 2019-10-18 16:25:40 -04:00
Pierre Vanduynslager 4af8548366 fix: ignore custom port when converting ssh repo URL to https 2019-10-18 16:25:40 -04:00
Alexander GandGregor Martynus dc19dfa2e9 docs: remove unnecessarily apostrophe
- improve punctuation of the how-to revert a release section
2019-10-10 07:32:14 -07:00
knidarknessandPierre Vanduynslager 79d22a2556 fix: clarify message for EGITNOPERMISSION error 2019-10-09 17:57:18 -04:00
Pierre Vanduynslager 9f2ec79d67 docs: limit list of suggested tools for commit linting 2019-10-09 16:42:52 -04:00
greenkeeper[bot]andPierre Vanduynslager 0e24022608 fix(package): update read-pkg-up to version 7.0.0 2019-09-27 10:37:25 -07:00
greenkeeper[bot]andPierre Vanduynslager 5986bf920e chore(package): update xo to version 0.25.0 2019-09-26 10:18:12 -07:00
greenkeeper[bot]andPierre Vanduynslager c86045ca49 chore(package): update dockerode to version 3.0.0 2019-09-24 13:37:46 -07:00
84 changed files with 2741 additions and 1666 deletions
+1
View File
@@ -0,0 +1 @@
* text=auto eol=lf
-1
View File
@@ -1 +0,0 @@
base: app0ZOxG0FnHmOiuU
+3
View File
@@ -131,3 +131,6 @@ yarn.lock
# Gitbook
_book
# Mockserver
CertificateAuthorityCertificate.pem
+7 -32
View File
@@ -1,36 +1,11 @@
language: node_js
version: ~> 1.0
services:
- docker
node_js:
- 12
- 10
- 8.15
# Trigger a push build on release 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:
- master
- next
- beta
- /^\d+\.(\d+|x)(\.x)?$/
- /^greenkeeper.*$/
# Retry install on fail to avoid failing a build on network/disk/external errors
install:
- travis_retry npm install
script:
- npm run test
after_success:
- npm run codecov
jobs:
include:
- stage: release
node_js: lts/*
script:
- npm run semantic-release
import:
- .travis/node.yml
- .travis/node-versions.yml
- .travis/semantic-release.yml
- .travis/greenkeeper.yml
- .travis/codecov.yml
+2
View File
@@ -0,0 +1,2 @@
after_success:
- npm run codecov
+3
View File
@@ -0,0 +1,3 @@
branches:
only:
- /^greenkeeper.*$/
+3
View File
@@ -0,0 +1,3 @@
node_js:
- 12
- 10.18
+11
View File
@@ -0,0 +1,11 @@
language: node_js
cache:
npm: false
# Retry install on fail to avoid failing a build on network/disk/external errors
install:
- travis_retry npm install
script:
- npm run test
+15
View File
@@ -0,0 +1,15 @@
branches:
only:
- master
- next
- beta
- /^\d+\.(\d+|x)(\.x)?$/
jobs:
include:
- stage: release
node_js: lts/*
install:
- travis_retry npm install
script:
- npm run semantic-release
+6 -6
View File
@@ -107,11 +107,11 @@ To ensure consistency and quality, all documentation modifications must:
- a third-party product/brand/service, i.e. Integrate with [GitHub](https://github.com)
- an external concept or feature, i.e. Create a [GitHub release](https://help.github.com/articles/creating-releases)
- a package or module, i.e. The [`@semantic-release/github`](https://github.com/semantic-release/github) module
- Use the the [single backtick `code` quoting](https://help.github.com/articles/basic-writing-and-formatting-syntax/#quoting-code) for:
- Use the [single backtick `code` quoting](https://help.github.com/articles/basic-writing-and-formatting-syntax/#quoting-code) for:
- commands inside sentences, i.e. the `semantic-release` command
- programming language keywords, i.e. `function`, `async`, `String`
- packages or modules, i.e. The [`@semantic-release/github`](https://github.com/semantic-release/github) module
- Use the the [triple backtick `code` formatting](https://help.github.com/articles/creating-and-highlighting-code-blocks) for:
- Use the [triple backtick `code` formatting](https://help.github.com/articles/creating-and-highlighting-code-blocks) for:
- code examples
- configuration examples
- sequence of command lines
@@ -183,17 +183,17 @@ The footer should contain any information about **Breaking Changes** and is also
#### Examples
```commit
`fix(pencil): stop graphite breaking when too much pressure applied`
fix(pencil): stop graphite breaking when too much pressure applied
```
```commit
`feat(pencil): add 'graphiteWidth' option`
feat(pencil): add 'graphiteWidth' option
Fix #42
```
```commit
perf(pencil): remove graphiteWidth option`
perf(pencil): remove graphiteWidth option
BREAKING CHANGE: The graphiteWidth option has been removed.
@@ -224,7 +224,7 @@ All the [semantic-release](https://github.com/semantic-release) repositories use
Before pushing your code changes make sure there are no linting errors with `npm run lint`.
**Tips**:
- Most linting errors can be automatically fixed with `npm run lint --fix`.
- Most linting errors can be automatically fixed with `npm run lint -- --fix`.
- Install the [XO plugin](https://github.com/sindresorhus/xo#editor-plugins) for your editor to see linting errors directly in your editor and automatically fix them on save.
### Tests
+19 -4
View File
@@ -56,7 +56,7 @@ This removes the immediate connection between human emotions and version numbers
By default **semantic-release** uses [Angular Commit Message Conventions](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines). The commit message format can be changed with the [`preset` or `config` options](docs/usage/configuration.md#options) of the [@semantic-release/commit-analyzer](https://github.com/semantic-release/commit-analyzer#options) and [@semantic-release/release-notes-generator](https://github.com/semantic-release/release-notes-generator#options) plugins.
Tools such as [commitizen](https://github.com/commitizen/cz-cli), [commitlint](https://github.com/conventional-changelog/commitlint) or [semantic-git-commit-cli](https://github.com/JPeer264/node-semantic-git-commit-cli) can be used to help contributors and enforce valid commit messages.
Tools such as [commitizen](https://github.com/commitizen/cz-cli) or [commitlint](https://github.com/conventional-changelog/commitlint) can be used to help contributors and enforce valid commit messages.
Here is an example of the release type that will be done based on a commit messages:
@@ -95,6 +95,14 @@ After running the tests, the command `semantic-release` will execute the followi
| Publish | Publish the release. |
| Notify | Notify of new releases or errors. |
## Requirements
In order to use **semantic-release** you need:
- To host your code in a [Git repository](https://git-scm.com)
- Use a Continuous Integration service that allows you to [securely set up credentials](docs/usage/ci-configuration.md#authentication)
- Git CLI version [2.7.1 or higher](docs/support/FAQ.md#why-does-semantic-release-require-git-version--271) installed in your Continuous Integration environment
- [Node.js](https://nodejs.org) version [10.18 or higher](docs/support/FAQ.md#why-does-semantic-release-require-node-version--1018) installed in your Continuous Integration environment
## Documentation
- Usage
@@ -142,9 +150,16 @@ Let people know that your package is published using **semantic-release** by inc
## Team
| [![Stephan Bönnemann](https://github.com/boennemann.png?size=100)](https://github.com/boennemann) | [![Rolf Erik Lekang](https://github.com/relekang.png?size=100)](https://github.com/relekang) | [![Johannes Jörg Schmidt](https://github.com/jo.png?size=100)](https://github.com/jo) | [![Gregor Martynus](https://github.com/gr2m.png?size=100)](https://github.com/gr2m) | [![Pierre Vanduynslager](https://github.com/finnp.png?size=100)](https://github.com/finnp) | [![Pierre Vanduynslager](https://github.com/pvdlg.png?size=100)](https://github.com/pvdlg) | [![Christoph Witzko](https://github.com/christophwitzko.png?size=100)](https://github.com/christophwitzko) |
|---------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|
| [Stephan Bönnemann](https://github.com/boennemann) | [Rolf Erik Lekang](https://github.com/relekang) | [Johannes Jörg Schmidt](https://github.com/jo) | [Gregor Martynus](https://github.com/gr2m) | [Finn Pauls](https://github.com/finnp) | [Pierre Vanduynslager](https://github.com/pvdlg) | [Christoph Witzko](https://github.com/christophwitzko) |
| [![Gregor Martynus](https://github.com/gr2m.png?size=100)](https://github.com/gr2m) | [![Pierre Vanduynslager](https://github.com/pvdlg.png?size=100)](https://github.com/pvdlg) | [![Matt Travi](https://github.com/travi.png?size=100)](https://github.com/travi) |
| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
| [Gregor Martynus](https://github.com/gr2m) | [Pierre Vanduynslager](https://github.com/pvdlg) | [Matt Travi](https://github.com/travi) |
## Alumni
| [![Stephan Bönnemann](https://github.com/boennemann.png?size=100)](https://github.com/boennemann) | [![Rolf Erik Lekang](https://github.com/relekang.png?size=100)](https://github.com/relekang) | [![Johannes Jörg Schmidt](https://github.com/jo.png?size=100)](https://github.com/jo) | [![Finn Pauls](https://github.com/finnp.png?size=100)](https://github.com/finnp) | [![Christoph Witzko](https://github.com/christophwitzko.png?size=100)](https://github.com/christophwitzko) |
| ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| [Stephan Bönnemann](https://github.com/boennemann) | [Rolf Erik Lekang](https://github.com/relekang) | [Johannes Jörg Schmidt](https://github.com/jo) | [Finn Pauls](https://github.com/finnp) | [Christoph Witzko](https://github.com/christophwitzko) |
<p align="center">
<img alt="Kill all humans" src="media/bender.png">
+4 -4
View File
@@ -10,7 +10,7 @@ var execa = require('execa');
var findVersions = require('find-versions');
var pkg = require('../package.json');
var MIN_GIT_VERSION = '2.0.0';
var MIN_GIT_VERSION = '2.7.1';
if (!semver.satisfies(process.version, pkg.engines.node)) {
console.error(
@@ -29,15 +29,15 @@ execa('git', ['--version'])
process.exit(1);
}
})
.catch(error => {
.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
// Node 10+ from this point on
require('../cli')()
.then(exitCode => {
.then((exitCode) => {
process.exitCode = exitCode;
})
.catch(() => {
+6 -6
View File
@@ -5,15 +5,15 @@ const hideSensitive = require('./lib/hide-sensitive');
const stringList = {
type: 'string',
array: true,
coerce: values =>
coerce: (values) =>
values.length === 1 && values[0].trim() === 'false'
? []
: values.reduce((values, value) => values.concat(value.split(',').map(value => value.trim())), []),
: values.reduce((values, value) => values.concat(value.split(',').map((value) => value.trim())), []),
};
module.exports = async () => {
const cli = require('yargs')
.command('$0', 'Run automated package publishing', yargs => {
.command('$0', 'Run automated package publishing', (yargs) => {
yargs.demandCommand(0, 0).usage(`Run automated package publishing
Usage:
@@ -41,18 +41,18 @@ Usage:
.exitProcess(false);
try {
const {help, version, ...opts} = cli.parse(argv.slice(2));
const {help, version, ...options} = cli.parse(argv.slice(2));
if (Boolean(help) || Boolean(version)) {
return 0;
}
if (opts.debug) {
if (options.debug) {
// Debug must be enabled before other requires in order to work
require('debug').enable('semantic-release:*');
}
await require('.')(opts);
await require('.')(options);
return 0;
} catch (error) {
if (error.name !== 'YError') {
+21 -4
View File
@@ -65,10 +65,10 @@
- `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-firefox-add-on](https://github.com/tophat/semantic-release-firefox-add-on)
- `verifyConditions`: Verify that all required options are present and authentication is set via environment variables
- `prepare`: Write the correct version to the `manifest.json`
- `publish`: Creates an unsigned `.xpi` file, and submits it to the Mozilla Add On store for signing. Once the package is signed, downloads the signed `.xpi` to a local directory
- [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)
@@ -91,3 +91,20 @@
- [semantic-release-github-pages](https://github.com/qiwi/semantic-release-gh-pages-plugin)
- `verifyConditions`: Verify the presence of the auth token set via environment variables.
- `publish`: Pushes commit to the documentation branch.
- [leiningen-semantic-release](https://github.com/NoxHarmonium/leiningen-semantic-release)
- `verifyConditions`: Checks the project.clj is syntactically valid.
- `prepare`: Update the project.clj version and package the output jar file.
- `publish`: Publish the jar (and generated Maven metadata) to a maven repository (or clojars).
- [@saithodev/semantic-release-gitea](https://github.com/saitho/semantic-release-gitea)
- `verifyConditions`: Verify the presence and the validity of the authentication and the assets option configuration.
- `publish`: Publish a Gitea release, optionally uploading file assets.
- `addChannel`: Update a Gitea release's pre-release field.
- [@google/semantic-release-replace-plugin](https://github.com/google/semantic-release-replace-plugin)
- `prepare`: Replace version strings in files using regex and glob.
- [semantic-release-rubygem](https://github.com/Gusto/semantic-release-rubygem)
- `verifyConditions`: Locate and validate a `.gemspec` file, locate and validate a `lib/**/version.rb` file, verify the presence of the `GEM_HOST_API_KEY` environment variable, and create a credentials file with the API key.
- `prepare`: Update the version in the `lib/**/version.rb` version file and [build](https://guides.rubygems.org/command-reference/#gem-build) the gem.
- `publish`: [Push the Ruby gem](https://guides.rubygems.org/command-reference/#gem-push) to the gem server.
- [semantic-release-npm-deprecate-old-versions](https://github.com/ghusse/semantic-release-npm-deprecate-old-versions)
- `verifyConditions`: Validates configuration.
- `publish`: Deprecates old versions, based on the declaration of supported versions in the config.
@@ -11,3 +11,11 @@
- Publishes the same tarball to [npm](https://github.com/semantic-release/npm).
- Commits the version change in `package.json`.
- Creates or updates a [changelog](https://github.com/semantic-release/changelog) file.
- [semantic-release-npm-github-publish](https://github.com/oleg-koval/semantic-release-npm-github-publish)
- Based on [angular preset](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular).
- Adds more keywords for the `chore` **PATCH** release.
- Generates or updates a [changelog](https://github.com/semantic-release/changelog) file including all **PATCH** keywords (not included in default angular package).
- Updates GitHub release with release-notes.
- Bumps a version in package.json.
- Publishes the new version to [NPM](https://npmjs.org).
+2
View File
@@ -4,6 +4,8 @@
- [CircleCI 2.0 workflows](circleci-workflows.md)
- [Travis CI](travis.md)
- [GitLab CI](gitlab-ci.md)
- [GitHub Actions](github-actions.md)
- [Jenkins CI](jenkins-ci.md)
## Git hosted services
- [Git authentication with SSH keys](git-auth-ssh-keys.md)
+90
View File
@@ -0,0 +1,90 @@
# Using semantic-release with [GitHub Actions](https://help.github.com/en/categories/automating-your-workflow-with-github-actions)
## Environment variables
The [Authentication](../usage/ci-configuration.md#authentication) environment variables can be configured with [Secret Variables](https://help.github.com/en/articles/virtual-environments-for-github-actions#creating-and-using-secrets-encrypted-variables).
In this example an [`NPM_TOKEN`](https://docs.npmjs.com/creating-and-viewing-authentication-tokens) is required to publish a package to the npm registry. GitHub Actions [automatically populate](https://help.github.com/en/articles/virtual-environments-for-github-actions#github_token-secret) a [`GITHUB_TOKEN`](https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line) environment variable which can be used in Workflows.
## Node project configuration
[GitHub Actions](https://github.com/features/actions) support [Workflows](https://help.github.com/en/articles/configuring-workflows), allowing to run tests on multiple Node versions and publish a release only when all test pass.
**Note**: The publish pipeline must run on [Node version >= 10.18](../support/FAQ.md#why-does-semantic-release-require-node-version--1018).
### `.github/workflows/release.yml` configuration for Node projects
The following is a minimal configuration for [`semantic-release`](https://github.com/semantic-release/semantic-release) with a build running on Node 12 when a new commit is pushed to a `master` branch. See [Configuring a Workflow](https://help.github.com/en/articles/configuring-a-workflow) for additional configuration options.
```yaml
name: Release
on:
push:
branches:
- master
jobs:
release:
name: Release
runs-on: ubuntu-18.04
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: 12
- name: Install dependencies
run: npm ci
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx semantic-release
```
## Pushing `package.json` changes to a `master` branch
To keep `package.json` updated in the `master` branch, [`@semantic-release/git`](https://github.com/semantic-release/git) plugin can be used.
**Note**: Automatically populated `GITHUB_TOKEN` cannot be used if branch protection is enabled for the target branch. It is **not** advised to mitigate this limitation by overriding an automatically populated `GITHUB_TOKEN` variable with a [Personal Access Tokens](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line), as it poses a security risk. Since Secret Variables are available for Workflows triggered by any branch, it becomes a potential vector of attack, where a Workflow triggered from a non-protected branch can expose and use a token with elevated permissions, yielding branch protection insignificant. One can use Personal Access Tokens in trusted environments, where all developers should have the ability to perform administrative actions in the given repository and branch protection is enabled solely for convenience purposes, to remind about required reviews or CI checks.
If the risk is acceptible, some extra configuration is needed. The [actions/checkout `persist-credentials`](https://github.com/marketplace/actions/checkout#usage) option needs to be `false`, otherwise the generated `GITHUB_TOKEN` will interfere with the custom one. Example:
```yaml
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
persist-credentials: false # <--- this
```
## Trigger semantic-release on demand
### Using GUI:
You can use [Manual Triggers](https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/) for GitHub Actions.
### Using HTTP:
Use [`repository_dispatch`](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#repository_dispatch) event to have control on when to generate a release by making an HTTP request, e.g.:
```yaml
name: Release
on:
repository_dispatch:
types: [semantic-release]
jobs:
# ...
```
To trigger a release, call (with a [Personal Access Tokens](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line) stored in `GITHUB_TOKEN` environment variable):
```
$ curl -v -H "Accept: application/vnd.github.everest-preview+json" -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/repos/[org-name-or-username]/[repository]/dispatches -d '{ "event_type": "semantic-release" }'
```
### Using 3rd party apps:
If you'd like to use a GitHub app to manage this instead of creating a personal access token, you could consider using a project like:
* [Actions Panel](https://www.actionspanel.app/) - A declaratively configured way for triggering GitHub Actions
* [Action Button](https://github-action-button.web.app/#details) - A simple badge based mechanism for triggering GitHub Actions
+45 -8
View File
@@ -2,17 +2,19 @@
## Environment variables
The [Authentication](../usage/ci-configuration.md#authentication) environment variables can be configured with [Secret variables](https://docs.gitlab.com/ce/ci/variables/README.html#secret-variables).
The [Authentication](../usage/ci-configuration.md#authentication) environment variables can be configured with [Protected variables](https://docs.gitlab.com/ce/ci/variables/README.html#protected-environment-variables).
**Note**: Make sure to configure your release branch as [protected](https://docs.gitlab.com/ce/user/project/protected_branches.html) in order for the CI/CD build to access the protected variables.
## Node project configuration
GitLab CI supports [Pipelines](https://docs.gitlab.com/ee/ci/pipelines.html) allowing to test on multiple Node versions and publishing a release only when all test pass.
**Note**: The publish pipeline must run a [Node >= 8 version](../support/FAQ.md#why-does-semantic-release-require-node-version--815).
**Note**: The publish pipeline must run a [Node >= 10.18 version](../support/FAQ.md#why-does-semantic-release-require-node-version--1018).
### `.gitlab-ci.yml` configuration for Node projects
This example is a minimal configuration for **semantic-release** with a build running Node 6 and 8. See [GitLab CI - Configuration of your jobs with .gitlab-ci.yml](https://docs.gitlab.com/ee/ci/yaml/README.html) for additional configuration options.
This example is a minimal configuration for **semantic-release** with a build running Node 10 and 12. See [GitLab CI - Configuration of your jobs with .gitlab-ci.yml](https://docs.gitlab.com/ee/ci/yaml/README.html) for additional configuration options.
**Note**: The`semantic-release` execution command varies depending if you are using a [local](../usage/installation.md#local-installation) or [global](../usage/installation.md#global-installation) **semantic-release** installation.
@@ -25,25 +27,60 @@ stages:
before_script:
- npm install
node:6:
image: node:6
node:10:
image: node:10
stage: test
script:
- npm test
node:8:
image: node:8
node:12:
image: node:12
stage: test
script:
- npm test
publish:
image: node:8
image: node:12
stage: release
script:
- npx semantic-release
```
### `.gitlab-ci.yml` configuration for all projects
This example is a minimal configuration for **semantic-release** with a build running Node 10 and 12. See [GitLab CI - Configuration of your jobs with .gitlab-ci.yml](https://docs.gitlab.com/ee/ci/yaml/README.html) for additional configuration options.
**Note**: The`semantic-release` execution command varies depending if you are using a [local](../usage/installation.md#local-installation) or [global](../usage/installation.md#global-installation) **semantic-release** installation.
```yaml
# The release pipeline will run only on the master branch a commit is triggered
stages:
- release
release:
image: node:10-buster-slim
stage: release
before_script:
- apt-get update && apt-get install -y --no-install-recommends git-core ca-certificates
- npm install -g semantic-release @semantic-release/gitlab
script:
- semantic-release
only:
- master
release:
image: node:12-buster-slim
stage: release
before_script:
- apt-get update && apt-get install -y --no-install-recommends git-core ca-certificates
- npm install -g semantic-release @semantic-release/gitlab
script:
- semantic-release
only:
- master
```
### `package.json` configuration
A `package.json` is required only for [local](../usage/installation.md#local-installation) **semantic-release** installation.
+61
View File
@@ -0,0 +1,61 @@
# Using semantic-release with [Jenkins CI](https://www.jenkins.io/doc/book/pipeline/)
## Environment variables
The [Authentication](../usage/ci-configuration.md#authentication) environment variables can be configured in [Jenkins Project Settings](https://www.jenkins.io/doc/pipeline/tour/environment/)..
Alternatively, the default `NPM_TOKEN` and `GH_TOKEN` can be easily [setup with semantic-release-cli](../usage/getting-started.md#getting-started).
## Node.js project configuration
### `Jenkinsfile (Declarative Pipeline)` configuration for a Node.js job
**Note**: The publish pipeline must run a [Node >= 10.18 version](../support/FAQ.md#why-does-semantic-release-require-node-version--1018).
This example is a minimal configuration for **semantic-release** with a build running Node 10.18. See [Jenkins documentation](https://www.jenkins.io/doc/) for additional configuration options.
The`semantic-release` execution command varies depending if you are using a [local](../usage/installation.md#local-installation) or [global](../usage/installation.md#global-installation) **semantic-release** installation.
```yaml
// The release stage in the pipeline will run only if the test stage in the pipeline is successful
pipeline {
agent any
environment {
GH_TOKEN = credentials('some-id')
}
stages {
stage('Test') {
steps {
sh '''
# Configure your test steps here (checkout, npm install, tests etc)
npm install
npm test
'''
}
}
stage('Release') {
tools {
nodejs "node 10.18"
}
steps {
sh '''
# Run optional required steps before releasing
npx semantic-release
'''
}
}
}
}
```
### `package.json` configuration for a Node job
A `package.json` is required only for [local](../usage/installation.md#local-installation) **semantic-release** installation.
```json
{
"devDependencies": {
"semantic-release": "^15.0.0"
}
}
```
+1 -1
View File
@@ -169,7 +169,7 @@ The Git history of the repository is now:
We can now start to work on a new future major release, version `4.0.0`, on the `@beta` distribution channel.
To do so we fist need to update the `beta` branch with all the changes from `master` (the commits `fix: a fix`). As `beta` and `master` branches have diverged, this merge might require to resolve conflicts.
To do so we first need to update the `beta` branch with all the changes from `master` (the commits `fix: a fix`). As `beta` and `master` branches have diverged, this merge might require to resolve conflicts.
We can now commit our new feature on `beta`. When pushing that commit, **semantic-release** will publish the pre-release version `3.1.0-beta.1` on the dist-tag `@beta`. That allow us to run integration tests by installing our module with `npm install example-module@beta`. Other users installing with `npm install example-module` will still receive the version `3.0.0`.
+2
View File
@@ -92,4 +92,6 @@ jobs:
# Use nvm to install and use the Node LTS version (nvm is installed on all Travis images)
- nvm install lts/*
- npx semantic-release
on:
all_branches: true
```
+15 -9
View File
@@ -2,12 +2,14 @@
## Why is the `package.json`’s version not updated in my repository?
**semantic-release** takes care of updating the `package.json`’s version before publishing to [npm](https://www.npmjs.com).
[`@semantic-release/npm`](https://github.com/semantic-release/npm) takes care of updating the `package.json`’s version before publishing to [npm](https://www.npmjs.com).
By default, only the published package will contain the version, which is the only place where it is *really* required, but the updated `package.json` will not be pushed to the Git repository
However, the [`@semantic-release/git`](https://github.com/semantic-release/git) plugin can be used to push the updated `package.json` as well as other files to the Git repository.
If you wish to only update the `package.json` and push via Git you can set the project to `"private": true,` within your `package.json` to prevent publishing to [npm](https://www.npmjs.com). This can be useful for using **semantic-release** with a non-node project.
## How can I use a npm build script that requires the `package.json`’s version ?
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.
@@ -38,7 +40,7 @@ Yes with the [dry-run options](../usage/configuration.md#dryrun) which prints to
## Can I use semantic-release with Yarn?
If you are using a [local](../usage/installation.md#local-installation) **semantic-release** installation and run multiple CI jobs with different versions, the `yarn install` command will fail on jobs running with Node < 8 as **semantic-release** requires [Node >= 8.15](#why-does-semantic-release-require-node-version--815) and specifies it in its `package.json`s [`engines`](https://docs.npmjs.com/files/package.json#engines) key.
If you are using a [local](../usage/installation.md#local-installation) **semantic-release** installation and run multiple CI jobs with different versions, the `yarn install` command will fail on jobs running with Node < 8 as **semantic-release** requires [Node >= 10.18](#why-does-semantic-release-require-node-version--1018) and specifies it in its `package.json`s [`engines`](https://docs.npmjs.com/files/package.json#engines) key.
The recommended solution is to use the [Yarn](https://yarnpkg.com) [--ignore-engines](https://yarnpkg.com/en/docs/cli/install#toc-yarn-install-ignore-engines) option to install the project dependencies on the CI environment, so Yarn will ignore the **semantic-release**'s `engines` key:
@@ -48,7 +50,7 @@ $ yarn install --ignore-engines
**Note**: Several CI services use Yarn by default if your repository contains a `yarn.lock` file. So you should override the install step to specify `yarn install --ignore-engines`.
Alternatively you can use a [global](../usage/installation.md#global-installation) **semantic-release** installation and make sure to install and run the `semantic-release` command only in a CI jobs running with Node >= 8.15.
Alternatively you can use a [global](../usage/installation.md#global-installation) **semantic-release** installation and make sure to install and run the `semantic-release` command only in a CI jobs running with Node >= 10.18.
If your CI environment provides [nvm](https://github.com/creationix/nvm) you can switch to Node 8 before installing and running the `semantic-release` command:
@@ -73,7 +75,7 @@ Yes, **semantic-release** is a Node CLI application but it can be used to publis
To publish a non-Node package (without a `package.json`) you would need to:
- Use a [global](../usage/installation.md#global-installation) **semantic-release** installation
- Set **semantic-release** [options](../usage/configuration.md#options) via [CLI arguments or rc file](../usage/configuration.md#configuration)
- Make sure your CI job executing the `semantic-release` command has access to [Node >= 8](#why-does-semantic-release-require-node-version--83) to execute the `semantic-release` command
- Make sure your CI job executing the `semantic-release` command has access to [Node >= 10.18](#why-does-semantic-release-require-node-version--1018) to execute the `semantic-release` command
See the [CI configuration recipes](../recipes/README.md#ci-configurations) for more details on specific CI environments.
@@ -141,11 +143,11 @@ See the [`@semantic-release/npm`](https://github.com/semantic-release/npm#semant
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
- 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.
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 from downloading it by accident. For example npm allows you to [un-publish](https://docs.npmjs.com/cli/unpublish) [within 72 hours](https://www.npmjs.com/policies/unpublish) after releasing. You may also [deprecate](https://docs.npmjs.com/cli/deprecate) a release if you would rather avoid un-publishing.
Depending on the package manager you are using, you might be able to un-publish or deprecate a release, in order to prevent users from downloading it by accident. For example, npm allows you to [un-publish](https://docs.npmjs.com/cli/unpublish) [within 72 hours](https://www.npmjs.com/policies/unpublish) after release. You may also [deprecate](https://docs.npmjs.com/cli/deprecate) a release if you would rather avoid un-publishing.
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.
@@ -232,12 +234,16 @@ See [“Introduction to SemVer” - Irina Gebauer](https://blog.greenkeeper.io/i
In addition the [verify conditions step](../../README.md#release-steps) verifies that all necessary conditions for proceeding with a release are met, and a new release will be performed [only if all your tests pass](../usage/ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded).
## Why does semantic-release require Node version >= 8.15?
## Why does semantic-release require Node version >= 10.18?
**semantic-release** is written using the latest [ECMAScript 2017](https://www.ecma-international.org/publications/standards/Ecma-262.htm) features, without transpilation which **requires Node version 8.15 or higher**.
**semantic-release** is written using the latest [ECMAScript 2017](https://www.ecma-international.org/publications/standards/Ecma-262.htm) features, without transpilation which **requires Node version 10.18 or higher**.
See [Node version requirement](./node-version.md#node-version-requirement) for more details and solutions.
## Why does semantic-release require Git version >= 2.7.1?
**semantic-release** uses Git CLI commands to read information about the repository such as branches, commit history and tags. Certain commands and options (such as [the `--merged` option of the `git tag` command](https://git-scm.com/docs/git-tag/2.7.0#git-tag---no-mergedltcommitgt) or bug fixes related to `git ls-files`) used by **semantic-release** are only available in Git version 2.7.1 and higher.
## 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.
+3 -3
View File
@@ -1,6 +1,6 @@
# Node version requirement
**semantic-release** is written using the latest [ECMAScript 2017](https://www.ecma-international.org/publications/standards/Ecma-262.htm) features, without transpilation which requires **requires Node version 8.15 or higher**.
**semantic-release** is written using the latest [ECMAScript 2017](https://www.ecma-international.org/publications/standards/Ecma-262.htm) features, without transpilation which requires **requires Node version 10 or higher**.
**semantic-release** is meant to be used in a CI environment as a development support tool, not as a production dependency. Therefore the only constraint is to run the `semantic-release` in a CI environment providing Node 8 or higher.
@@ -8,9 +8,9 @@ See our [Node Support Policy](node-support-policy.md) for our long-term promise
## Recommended solution
### Run at least one CI job with Node >= 8.15
### Run at least one CI job with Node >= 10.18
The recommended approach is to run the `semantic-release` command from a CI job running on Node 8.15 or higher. This can either be a job used by your project to test on Node >= 8.15 or a dedicated job for the release steps.
The recommended approach is to run the `semantic-release` command from a CI job running on Node 10.18 or higher. This can either be a job used by your project to test on Node >= 10.18 or a dedicated job for the release steps.
See [CI configuration](../usage/ci-configuration.md) and [CI configuration recipes](../recipes/README.md#ci-configurations) for more details.
+1
View File
@@ -14,6 +14,7 @@
- ["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)
- ["Series - Semantic Release Automation" - Abdelrahman Wahdan](https://dev.to/abdelrahmanahmed/semantic-release-and-how-to-automate-it-part-1-4pa2)
## Tutorials
+13
View File
@@ -54,3 +54,16 @@ $ git branch --contains <tag name>
$ git tag -d <tag name>
$ git push origin :refs/tags/<tag name>
```
## release not found release branch after `git push --force`
**semantic-release** is using both [git tags](https://git-scm.com/docs/git-tag) and [git notes](https://git-scm.com/docs/git-notes) to store information about which releases happened in which branch.
After a git history rewrite due to a `git push --force`, the git tags and notes referencing the commits that were rewritten are lost.
To recover from that situation, do the following:
1. Delete the tag(s) for the release(s) that have been lost from the git history. You can delete each tag from remote using `git push origin :[TAG NAME]`, e.g. `git push origin :v2.0.0-beta.1`. You can delete tags locally using `git tag -d [TAG NAME]`, e.g. `git tag -d v2.0.0-beta.1`.
2. Re-create the tags locally: `git tag [TAG NAME] [COMMIT HASH]`, where `[COMMIT HASH]` is the new commit that created the release for the lost tag. E.g. `git tag v2.0.0-beta.1 abcdef0`
3. Re-create the git notes for each release tag, e.g. `git notes --ref semantic-release add -f -m '{"channels":["beta"]}' v3.0.0-beta.1`. If the release was also published in the default channel (usually `master`), then set the first channel to `null`, e.g. `git notes --ref semantic-release add -f -m '{"channels":[null, "beta"]}'
4. Push the local notes: `git push --force origin refs/notes/semantic-release`. The `--force` is needed after the rebase. Be careful.
+8 -6
View File
@@ -6,6 +6,7 @@ The `semantic-release` command must be executed only after all the tests in the
Here is a few example of the CI services that can be used to achieve this:
- [Travis Build Stages](https://docs.travis-ci.com/user/build-stages)
- [CircleCI Workflows](https://circleci.com/docs/2.0/workflows)
- [GitHub Actions](https://github.com/features/actions)
- [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)
@@ -20,12 +21,13 @@ 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](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. |
| 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). |
| `BB_TOKEN_BASIC_AUTH` or `BITBUCKET_TOKEN_BASIC_AUTH` | A Bitbucket [personal access token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html) with basic auth support. For clearification `user:token` has to be the value of this env. |
| `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).
+5 -3
View File
@@ -38,7 +38,7 @@ The following three examples are the same.
- Via CLI argument:
```bash
$ semantic-release --branch next
$ semantic-release --branches next
```
**Note**: CLI arguments take precedence over options configured in the configuration file.
@@ -74,7 +74,7 @@ The branches on which releases should happen. By default **semantic-release** wi
**Note**: If your repository does not have a release branch, then **semantic-release** will fail with an `ERELEASEBRANCHES` error message. If you are using the default configuration, you can fix this error by pushing a `master` branch.
**Note**: Once **semantic-release** is configured, any user with the permission to push commits on one of those branches will be able to publish a release. It is recommended to protect those branches, for example with [GitHub protected branches](https://help.github.com/articles/about-protected-branches).
**Note**: Once **semantic-release** is configured, any user with the permission to push commits on one of those branches will be able to publish a release. It is recommended to protect those branches, for example with [GitHub protected branches](https://docs.github.com/github/administering-a-repository/about-protected-branches).
See [Workflow configuration](workflow-configuration.md#workflow-configuration) for more details.
@@ -116,7 +116,9 @@ 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.
The objective of the dry-run mode is to get a preview of the pending release. Dry-run mode skips the following steps: prepare, publish, success and fail. In addition to this it prints the next version and release notes to the console.
**Note**: The Dry-run mode verifies the repository push permission, even though nothing will be pushed. The verification is done to help user to figure out potential configuration issues.
### ci
+2 -3
View File
@@ -1,6 +1,7 @@
# Getting started
In order to use **semantic-release** you must follow these steps:
1. [Install](./installation.md#installation) **semantic-release** in your project
2. Configure your Continuous Integration service to [run **semantic-release**](./ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded)
3. Configure your Git repository and package manager repository [authentication](ci-configuration.md#authentication) in your Continuous Integration service
@@ -9,10 +10,8 @@ In order to use **semantic-release** you must follow these steps:
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
npx semantic-release-cli setup
```
![dialogue](../../media/semantic-release-cli.png)
+4 -4
View File
@@ -21,12 +21,12 @@ A plugin is a npm module that can implement one or more of the following steps:
### Default plugins
These four plugins are already part of **semantic-release** and don't have to be installed separately:
These four plugins are already part of **semantic-release** and are listed in order of execution. They do not have to be installed separately:
```
"@semantic-release/commit-analyzer"
"@semantic-release/github"
"@semantic-release/npm"
"@semantic-release/release-notes-generator"
"@semantic-release/npm"
"@semantic-release/github"
```
### Additional plugins
@@ -65,8 +65,8 @@ For each [release step](../../README.md#release-steps) the plugins that implemen
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 `prepare` implementation of `@semantic-release/npm` then `@semantic-release/git`
- execute the `publish` implementation of `@semantic-release/npm`
## Plugin options configuration
+15 -16
View File
@@ -48,8 +48,10 @@ For example the configuration `['+([0-9])?(.{+([0-9]),x}).x', 'master', 'next']`
### channel
The `channel` can be defined for any branch type. If it's not defined, releases will be done on the default distribution channel (for example the `@latest` [dist-tag](https://docs.npmjs.com/cli/dist-tag) for npm).
The value of `channel`, if defined, is generated with [Lodash template](https://lodash.com/docs#template) with the variable `name` available.
The `channel` can be defined for any branch type. By default releases will be done on the default distribution channel (for example the `@latest` [dist-tag](https://docs.npmjs.com/cli/dist-tag) for npm) for the first [release branch](#release-branches) and on a distribution channel named based on the branch `name` for any other branch.
If the `channel` property is set to `false` the default channel will be used.
The value of `channel`, if defined as a string, is generated with [Lodash template](https://lodash.com/docs#template) with the variable `name` available.
For example the configuration `['master', {name: 'next', channel: 'channel-${name}'}]` will be expanded as:
```js
@@ -78,7 +80,7 @@ For example the configuration `['1.1.x', '1.2.x', 'master']` will be expanded as
### prerelease
A `prerelease` property applies only to pre-release branches, is required and The `prerelease` value must be valid per the [Semantic Versioning Specification](https://semver.org/#spec-item-9). It will determine the name of versions (for example if `prerelease` is set to `beta` the version be formatted like `2.0.0-beta.1`, `2.0.0-beta.2` etc...).
A `prerelease` property applies only to pre-release branches and the `prerelease` value must be valid per the [Semantic Versioning Specification](https://semver.org/#spec-item-9). It will determine the name of versions (for example if `prerelease` is set to `beta` the version be formatted like `2.0.0-beta.1`, `2.0.0-beta.2` etc...).
If the `prerelease` property is set to `true` the `name` value will be used.
The value of `prerelease`, if defined as a string, is generated with [Lodash template](https://lodash.com/docs#template) with the variable `name` available.
@@ -98,7 +100,7 @@ For example the configuration `['master', {name: 'pre/rc', prerelease: '${name.r
### Release branches
A release branch is the base type of branch used by **semantic-release** that allows to publish releases with a [semantic version](https://semver.org), optionally on a specific distribution channel. Distribution channels (for example [npm dist-tags](https://docs.npmjs.com/cli/dist-tag) or [Chrome release channels](https://www.chromium.org/getting-involved/dev-channel)) are a way to distribute new releases only to a subset of users in order to get early feedback. Later on those releases can be added to the general distribution channel to be made available to all users.
A release branch is the base type of branch used by **semantic-release** that allows to publish releases with a [semantic version](https://semver.org), optionally on a specific distribution channel. Distribution channels (for example [npm dist-tags](https://docs.npmjs.com/cli/dist-tag) or [Chrome release channels](https://www.chromium.org/getting-involved/dev-channel)) are a way to distribute new releases only to a subset of users in order to get early feedback. Later on, those releases can be added to the general distribution channel to be made available to all users.
**semantic-release** will automatically add releases to the corresponding distribution channel when code is [merged from a release branch to another](#merging-into-a-release-branch).
@@ -110,20 +112,17 @@ See [publishing on distribution channels recipe](../recipes/distribution-channel
#### Pushing to a release branch
With the configuration `"branches": ["master", "next"]`, if the last release published from `master` is `1.0.0` then:
With the configuration `"branches": ["master", "next"]`, if the last release published from `master` is `1.0.0` and the last one from `next` is `2.0.0` then:
- Only versions in range `1.x.x` can be published from `master`, so only `fix` and `feat` commits can be pushed to `master`
- Only versions in range `>=2.0.0` release can be published from `next`, so a `BREAKING CHANGE` commit must be pushed first on `next` and can be followed by any type of commits
- Once `next` get merged into `master` the release `2.0.0` will be made available on the channel associated with `master` and both `master` and `next` will accept any commit type
With the configuration `"branches": ["master", "next", "next-major"]`, if the last release published from `master` is `1.0.0` then:
- Only versions in range `1.0.x` can be published from `master`, so only `fix` commits can be pushed to `master`
- Only versions in range `>=1.1.0` can be published from `next`, so a `feat` commit must be pushed first on `next` and can be followed by `fix` and `feat` commits
- Only versions in range `>=2.0.0` release can be published from `next`, so a `BREAKING CHANGE` commit must be pushed first on `next-major` and can be followed by any type of commits
Those verifications prevent situations such as:
1. Create a `feat` commit on `next` which triggers the release of version `1.0.0` on channel `next`
This verification prevent scenario such as:
1. Create a `feat` commit on `next` which triggers the release of version `1.0.0` on the `next` channel
2. Merge `next` into `master` which adds `1.0.0` on the default channel
3. Push a `fix` commit to `master` which triggers the release of version `1.0.1` on the default channel
4. Push a `fix` commit to `next` which would attempt to release the version `1.0.1` on channel `next` and fails as this version already exists
3. Create a `feat` commit on `next` which triggers the release of version `1.1.0` on the `next` channel
4. Create a `feat` commit on `master` which would attempt to release the version `1.1.0` on the default channel
In step 4 **semantic-release** will throw an `EINVALIDNEXTVERSION` error to prevent the attempt at releasing version `1.1.0` which was already released on step 3 with a different codebase. The error will indicate that the commit should be created on `next` instead. Alternatively if the `next` branch is merged into `master`, the version `1.1.0` will be made available on the default channel and the `feat` commit would be allowed on `master` to release `1.2.0`.
#### Merging into a release branch
@@ -141,7 +140,7 @@ A maintenance branch is characterized by a range which defines the versions that
Maintenance branches are always considered lower than [release branches](#release-branches) and similarly to them, when a commit that would create a version conflict is pushed, **semantic-release** will not perform the release and will throw an `EINVALIDNEXTVERSION` error, listing the problematic commits and the valid branches on which to move them.
**semantic-release** will automatically add releases to the corresponding distribution channel when code is [merged from a release or maintenance branch to another maintenance branch](#merging-into-a-maintenance-branch), however only version version within the branch `range` can be merged. Ia merged version is outside the maintenance branch `range` **semantic-release** will not add to the corresponding channel and will throw an `EINVALIDMAINTENANCEMERGE` error.
**semantic-release** will automatically add releases to the corresponding distribution channel when code is [merged from a release or maintenance branch to another maintenance branch](#merging-into-a-maintenance-branch), however only versions within the branch `range` can be merged. If a merged version is outside the maintenance branch `range`, **semantic-release** will not add to the corresponding channel and will throw an `EINVALIDMAINTENANCEMERGE` error.
See [publishing maintenance releases recipe](../recipes/maintenance-releases.md) for a detailed example.
+68 -40
View File
@@ -3,7 +3,6 @@ const marked = require('marked');
const TerminalRenderer = require('marked-terminal');
const envCi = require('env-ci');
const hookStd = require('hook-std');
const pEachSeries = require('p-each-series');
const semver = require('semver');
const AggregateError = require('aggregate-error');
const pkg = require('./package.json');
@@ -13,21 +12,22 @@ const verify = require('./lib/verify');
const getNextVersion = require('./lib/get-next-version');
const getCommits = require('./lib/get-commits');
const getLastRelease = require('./lib/get-last-release');
const getReleasesToAdd = require('./lib/get-releases-to-add');
const getReleaseToAdd = require('./lib/get-release-to-add');
const {extractErrors, makeTag} = require('./lib/utils');
const getGitAuthUrl = require('./lib/get-git-auth-url');
const getBranches = require('./lib/branches');
const getLogger = require('./lib/get-logger');
const {verifyAuth, isBranchUpToDate, getGitHead, tag, push} = require('./lib/git');
const {verifyAuth, isBranchUpToDate, getGitHead, tag, push, pushNotes, getTagHead, addNote} = require('./lib/git');
const getError = require('./lib/get-error');
const {COMMIT_NAME, COMMIT_EMAIL} = require('./lib/definitions/constants');
marked.setOptions({renderer: new TerminalRenderer()});
/* eslint complexity: ["warn", 25] */
/* eslint complexity: off */
async function run(context, plugins) {
const {cwd, env, options, logger} = context;
const {isCi, branch: ciBranch, isPr} = envCi({env, cwd});
const {isCi, branch, prBranch, isPr} = context.envCi;
const ciBranch = isPr ? prBranch : branch;
if (!isCi && !options.dryRun && !options.noCi) {
logger.warn('This run was not triggered in a known CI environment, running in dry-run mode.');
@@ -53,8 +53,8 @@ async function run(context, plugins) {
// Verify config
await verify(context);
options.repositoryUrl = await getGitAuthUrl(context);
context.branches = await getBranches(options.repositoryUrl, context);
options.repositoryUrl = await getGitAuthUrl({...context, branch: {name: ciBranch}});
context.branches = await getBranches(options.repositoryUrl, ciBranch, context);
context.branch = context.branches.find(({name}) => name === ciBranch);
if (!context.branch) {
@@ -67,7 +67,9 @@ async function run(context, plugins) {
}
logger[options.dryRun ? 'warn' : 'success'](
`Run automated release from branch ${ciBranch}${options.dryRun ? ' in dry-run mode' : ''}`
`Run automated release from branch ${ciBranch} on repository ${options.repositoryUrl}${
options.dryRun ? ' in dry-run mode' : ''
}`
);
try {
@@ -84,7 +86,7 @@ async function run(context, plugins) {
throw error;
}
} catch (error) {
logger.error(`The command "${error.cmd}" failed with the error message ${error.stderr}.`);
logger.error(`The command "${error.command}" failed with the error message ${error.stderr}.`);
throw getError('EGITNOPERMISSION', context);
}
@@ -92,39 +94,55 @@ async function run(context, plugins) {
await plugins.verifyConditions(context);
const releasesToAdd = getReleasesToAdd(context);
const errors = [];
context.releases = [];
const releaseToAdd = getReleaseToAdd(context);
await pEachSeries(releasesToAdd, async ({lastRelease, currentRelease, nextRelease}) => {
if (releaseToAdd) {
const {lastRelease, currentRelease, nextRelease} = releaseToAdd;
nextRelease.gitHead = await getTagHead(nextRelease.gitHead, {cwd, env});
currentRelease.gitHead = await getTagHead(currentRelease.gitHead, {cwd, env});
if (context.branch.mergeRange && !semver.satisfies(nextRelease.version, context.branch.mergeRange)) {
errors.push(getError('EINVALIDMAINTENANCEMERGE', {...context, nextRelease}));
return;
} else {
const commits = await getCommits({...context, lastRelease, nextRelease});
nextRelease.notes = await plugins.generateNotes({...context, commits, lastRelease, nextRelease});
if (options.dryRun) {
logger.warn(`Skip ${nextRelease.gitTag} tag creation in dry-run mode`);
} else {
await addNote({channels: [...currentRelease.channels, nextRelease.channel]}, nextRelease.gitHead, {cwd, env});
await push(options.repositoryUrl, {cwd, env});
await pushNotes(options.repositoryUrl, {cwd, env});
logger.success(
`Add ${nextRelease.channel ? `channel ${nextRelease.channel}` : 'default channel'} to tag ${
nextRelease.gitTag
}`
);
}
context.branch.tags.push({
version: nextRelease.version,
channel: nextRelease.channel,
gitTag: nextRelease.gitTag,
gitHead: nextRelease.gitHead,
});
const releases = await plugins.addChannel({...context, commits, lastRelease, currentRelease, nextRelease});
context.releases.push(...releases);
await plugins.success({...context, lastRelease, commits, nextRelease, releases});
}
const commits = await getCommits({...context, lastRelease, nextRelease});
nextRelease.notes = await plugins.generateNotes({...context, commits, lastRelease, nextRelease});
logger.log('Create tag %s', nextRelease.gitTag);
await tag(nextRelease.gitTag, nextRelease.gitHead, {cwd, env});
await push(options.repositoryUrl, {cwd, env});
context.branch.tags.push({
version: nextRelease.version,
channel: nextRelease.channel,
gitTag: nextRelease.gitTag,
gitHead: nextRelease.gitHead,
});
const releases = await plugins.addChannel({...context, commits, lastRelease, currentRelease, nextRelease});
context.releases.push(...releases);
await plugins.success({...context, lastRelease, commits, nextRelease, releases});
});
}
if (errors.length > 0) {
throw new AggregateError(errors);
}
context.lastRelease = await getLastRelease(context);
context.lastRelease = getLastRelease(context);
if (context.lastRelease.gitHead) {
context.lastRelease.gitHead = await getTagHead(context.lastRelease.gitHead, {cwd, env});
}
if (context.lastRelease.gitTag) {
logger.log(
@@ -138,7 +156,7 @@ async function run(context, plugins) {
const nextRelease = {
type: await plugins.analyzeCommits(context),
channel: context.branch.channel,
channel: context.branch.channel || null,
gitHead: await getGitHead({cwd, env}),
};
if (!nextRelease.type) {
@@ -148,8 +166,8 @@ async function run(context, plugins) {
context.nextRelease = nextRelease;
nextRelease.version = getNextVersion(context);
nextRelease.gitTag = makeTag(options.tagFormat, nextRelease.version, nextRelease.channel);
nextRelease.name = makeTag(options.tagFormat, nextRelease.version);
nextRelease.gitTag = makeTag(options.tagFormat, nextRelease.version);
nextRelease.name = nextRelease.gitTag;
if (context.branch.type !== 'prerelease' && !semver.satisfies(nextRelease.version, context.branch.range)) {
throw getError('EINVALIDNEXTVERSION', {
@@ -171,7 +189,9 @@ async function run(context, plugins) {
} else {
// Create the tag before calling the publish plugins as some require the tag to exists
await tag(nextRelease.gitTag, nextRelease.gitHead, {cwd, env});
await addNote({channels: [nextRelease.channel]}, nextRelease.gitHead, {cwd, env});
await push(options.repositoryUrl, {cwd, env});
await pushNotes(options.repositoryUrl, {cwd, env});
logger.success(`Created tag ${nextRelease.gitTag}`);
}
@@ -180,7 +200,9 @@ async function run(context, plugins) {
await plugins.success({...context, releases});
logger.success(`Published release ${nextRelease.version}`);
logger.success(
`Published release ${nextRelease.version} on ${nextRelease.channel ? nextRelease.channel : 'default'} channel`
);
if (options.dryRun) {
logger.log(`Release note for version ${nextRelease.version}:`);
@@ -193,7 +215,7 @@ async function run(context, plugins) {
}
function logErrors({logger, stderr}, err) {
const errors = extractErrors(err).sort(error => (error.semanticRelease ? -1 : 0));
const errors = extractErrors(err).sort((error) => (error.semanticRelease ? -1 : 0));
for (const error of errors) {
if (error.semanticRelease) {
logger.error(`${error.code} ${error.message}`);
@@ -207,7 +229,7 @@ function logErrors({logger, stderr}, err) {
}
async function callFail(context, plugins, err) {
const errors = extractErrors(err).filter(err => err.semanticRelease);
const errors = extractErrors(err).filter((err) => err.semanticRelease);
if (errors.length > 0) {
try {
await plugins.fail({...context, errors});
@@ -217,16 +239,22 @@ async function callFail(context, plugins, err) {
}
}
module.exports = async (opts = {}, {cwd = process.cwd(), env = process.env, stdout, stderr} = {}) => {
module.exports = async (cliOptions = {}, {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};
const context = {
cwd,
env,
stdout: stdout || process.stdout,
stderr: stderr || process.stderr,
envCi: envCi({env, cwd}),
};
context.logger = getLogger(context);
context.logger.log(`Running ${pkg.name} version ${pkg.version}`);
try {
const {plugins, options} = await getConfig(context, opts);
const {plugins, options} = await getConfig(context, cliOptions);
context.options = options;
try {
const result = await run(context, plugins);
+2 -2
View File
@@ -8,9 +8,9 @@ module.exports = async (repositoryUrl, {cwd}, branches) => {
return branches.reduce(
(branches, branch) => [
...branches,
...remove(gitBranches, name => micromatch(gitBranches, branch.name).includes(name)).map(name => ({
...remove(gitBranches, (name) => micromatch(gitBranches, branch.name).includes(name)).map((name) => ({
name,
...mapValues(omit(branch, 'name'), value => (isString(value) ? template(value)({name}) : value)),
...mapValues(omit(branch, 'name'), (value) => (isString(value) ? template(value)({name}) : value)),
})),
],
[]
+9 -15
View File
@@ -2,32 +2,26 @@ const {template, escapeRegExp} = require('lodash');
const semver = require('semver');
const pReduce = require('p-reduce');
const debug = require('debug')('semantic-release:get-tags');
const {getTags, isRefInHistory, getTagHead} = require('../../lib/git');
const {getTags, getNote} = require('../../lib/git');
module.exports = async ({cwd, env, options: {tagFormat}}, branches) => {
// 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 getTags({cwd, env}))
.map(tag => {
const [, version, channel] = tag.match(tagRegexp) || [];
return {gitTag: tag, version, channel};
})
.filter(({version}) => version && semver.valid(semver.clean(version)));
debug('found tags: %o', tags);
const tagRegexp = `^${escapeRegExp(template(tagFormat)({version: ' '})).replace(' ', '(.+)')}`;
return pReduce(
branches,
async (branches, branch) => {
const branchTags = await pReduce(
tags,
async (tags, {gitTag, ...rest}) =>
(await isRefInHistory(gitTag, branch.name, true, {cwd, env}))
? [...tags, {...rest, gitTag, gitHead: await getTagHead(gitTag, {cwd, env})}]
: tags,
await getTags(branch.name, {cwd, env}),
async (branchTags, tag) => {
const [, version] = tag.match(tagRegexp) || [];
return version && semver.valid(semver.clean(version))
? [...branchTags, {gitTag: tag, version, channels: (await getNote(tag, {cwd, env})).channels || [null]}]
: branchTags;
},
[]
);
+10 -8
View File
@@ -3,24 +3,26 @@ const AggregateError = require('aggregate-error');
const pEachSeries = require('p-each-series');
const DEFINITIONS = require('../definitions/branches');
const getError = require('../get-error');
const {fetch, verifyBranchName} = require('../git');
const {fetch, fetchNotes, verifyBranchName} = require('../git');
const expand = require('./expand');
const getTags = require('./get-tags');
const normalize = require('./normalize');
module.exports = async (repositoryUrl, context) => {
module.exports = async (repositoryUrl, ciBranch, context) => {
const {cwd, env} = context;
const remoteBranches = await expand(
repositoryUrl,
context,
context.options.branches.map(branch => (isString(branch) || isRegExp(branch) ? {name: branch} : branch))
context.options.branches.map((branch) => (isString(branch) || isRegExp(branch) ? {name: branch} : branch))
);
await pEachSeries(remoteBranches, async ({name}) => {
await fetch(repositoryUrl, name, {cwd, env});
await fetch(repositoryUrl, name, ciBranch, {cwd, env});
});
await fetchNotes(repositoryUrl, {cwd, env});
const branches = await getTags(context, remoteBranches);
const errors = [];
@@ -30,7 +32,7 @@ module.exports = async (repositoryUrl, context) => {
);
const result = Object.entries(DEFINITIONS).reduce((result, [type, {branchesValidator, branchValidator}]) => {
branchesByType[type].forEach(branch => {
branchesByType[type].forEach((branch) => {
if (branchValidator && !branchValidator(branch)) {
errors.push(getError(`E${type.toUpperCase()}BRANCH`, {branch}));
}
@@ -46,15 +48,15 @@ module.exports = async (repositoryUrl, context) => {
}, {});
const duplicates = [...branches]
.map(branch => branch.name)
.map((branch) => branch.name)
.sort()
.filter((val, idx, arr) => arr[idx] === arr[idx + 1] && arr[idx] !== arr[idx - 1]);
.filter((_, idx, array) => array[idx] === array[idx + 1] && array[idx] !== array[idx - 1]);
if (duplicates.length > 0) {
errors.push(getError('EDUPLICATEBRANCHES', {duplicates}));
}
await pEachSeries(branches, async branch => {
await pEachSeries(branches, async (branch) => {
if (!(await verifyBranchName(branch.name))) {
errors.push(getError('EINVALIDBRANCHNAME', branch));
}
+14 -20
View File
@@ -1,5 +1,4 @@
const {sortBy, isNil} = require('lodash');
const semver = require('semver');
const semverDiff = require('semver-diff');
const {FIRST_RELEASE, RELEASE_TYPE} = require('../definitions/constants');
const {
@@ -61,36 +60,31 @@ function release({release}) {
return release;
}
const breakpoints = release.length > 2 ? ['minor', 'major'] : ['major'];
// The intial bound is the last release from the base branch of `FIRST_RELEASE` (1.0.0)
let bound = getLatestVersion(tagsToVersions(release[0].tags)) || FIRST_RELEASE;
// The intial lastVersion is the last release from the base branch of `FIRST_RELEASE` (1.0.0)
let lastVersion = getLatestVersion(tagsToVersions(release[0].tags)) || FIRST_RELEASE;
return release.map(({name, tags, channel, ...rest}, idx) => {
const versions = tagsToVersions(tags);
// The lower bound is the highest version between the current branch last release and the previous branch upper bound (`bound`)
const min = highest(getLatestVersion(versions), bound);
if (release.length - 1 === idx) {
// If the current branch is the last one of the release branch, there is no upper bound
bound = undefined;
} else {
// The default upper bound is the lower bound increment with the release type of the current branch position
const upperBound = semver.inc(min, breakpoints[idx]);
// Find the lowest version that is present on the current branch but none of the previous ones
const nextFirstVersion = getFirstVersion(tagsToVersions(release[idx + 1].tags), release.slice(0, idx + 1));
// The upper bound is the lowest version between `nextFirstVersion` and the default upper bound
bound = lowest(nextFirstVersion, upperBound);
}
// The new lastVersion is the highest version between the current branch last release and the previous branch lastVersion
lastVersion = highest(getLatestVersion(versions), lastVersion);
// The upper bound is:
// - None if the current branch is the last one of the release branches
// - Otherwise, The upper bound is the lowest version that is present on the current branch but none of the previous ones
const bound =
release.length - 1 === idx
? undefined
: getFirstVersion(tagsToVersions(release[idx + 1].tags), release.slice(0, idx + 1));
const diff = bound ? semverDiff(min, bound) : null;
const diff = bound ? semverDiff(lastVersion, bound) : null;
return {
...rest,
channel: idx === 0 ? channel : isNil(channel) ? name : channel,
tags,
type: 'release',
name,
range: getRange(min, bound),
range: getRange(lastVersion, bound),
accept: bound ? RELEASE_TYPE.slice(0, RELEASE_TYPE.indexOf(diff)) : RELEASE_TYPE,
main: idx === 0,
};
});
}
+8 -8
View File
@@ -1,23 +1,23 @@
const {isUndefined, uniqBy} = require('lodash');
const {isNil, uniqBy} = require('lodash');
const semver = require('semver');
const {isMaintenanceRange} = require('../utils');
const maintenance = {
filter: ({name, range}) => !isUndefined(range) || isMaintenanceRange(name),
branchValidator: ({range}) => (isUndefined(range) ? true : isMaintenanceRange(range)),
branchesValidator: branches => uniqBy(branches, ({range}) => semver.validRange(range)).length === branches.length,
filter: ({name, range}) => (!isNil(range) && range !== false) || isMaintenanceRange(name),
branchValidator: ({range}) => (isNil(range) ? true : isMaintenanceRange(range)),
branchesValidator: (branches) => uniqBy(branches, ({range}) => semver.validRange(range)).length === branches.length,
};
const prerelease = {
filter: ({prerelease}) => !isUndefined(prerelease),
filter: ({prerelease}) => !isNil(prerelease) && prerelease !== false,
branchValidator: ({name, prerelease}) =>
Boolean(prerelease) && Boolean(semver.valid(`1.0.0-${prerelease === true ? name : prerelease}.1`)),
branchesValidator: branches => uniqBy(branches, 'prerelease').length === branches.length,
branchesValidator: (branches) => uniqBy(branches, 'prerelease').length === branches.length,
};
const release = {
filter: branch => !maintenance.filter(branch) && !prerelease.filter(branch),
branchesValidator: branches => branches.length <= 3 && branches.length > 0,
filter: (branch) => !maintenance.filter(branch) && !prerelease.filter(branch),
branchesValidator: (branches) => branches.length <= 3 && branches.length > 0,
};
module.exports = {maintenance, prerelease, release};
+3
View File
@@ -14,6 +14,8 @@ const SECRET_REPLACEMENT = '[secure]';
const SECRET_MIN_SIZE = 5;
const GIT_NOTE_REF = 'semantic-release';
module.exports = {
RELEASE_TYPE,
FIRST_RELEASE,
@@ -23,4 +25,5 @@ module.exports = {
RELEASE_NOTES_SEPARATOR,
SECRET_REPLACEMENT,
SECRET_MIN_SIZE,
GIT_NOTE_REF,
};
+14 -12
View File
@@ -4,9 +4,10 @@ const pkg = require('../../package.json');
const {RELEASE_TYPE} = require('./constants');
const [homepage] = pkg.homepage.split('#');
const stringify = obj => (isString(obj) ? obj : inspect(obj, {breakLength: Infinity, depth: 2, maxArrayLength: 5}));
const linkify = file => `${homepage}/blob/master/${file}`;
const wordsList = words =>
const stringify = (object) =>
isString(object) ? object : inspect(object, {breakLength: Infinity, depth: 2, maxArrayLength: 5});
const linkify = (file) => `${homepage}/blob/master/${file}`;
const wordsList = (words) =>
`${words.slice(0, -1).join(', ')}${words.length > 1 ? ` or ${words[words.length - 1]}` : trim(words[0])}`;
module.exports = {
@@ -29,14 +30,15 @@ Please make sure to add the \`repositoryUrl\` to the [semantic-release configura
)}).`,
}),
EGITNOPERMISSION: ({options: {repositoryUrl}, branch: {name}}) => ({
message: 'The push permission to the Git repository is required.',
details: `**semantic-release** cannot push the version tag to the branch \`${name}\` on remote Git repository with URL \`${repositoryUrl}\`.
message: 'Cannot push to the Git repository.',
details: `**semantic-release** cannot push the version tag to the branch \`${name}\` on the remote Git repository with URL \`${repositoryUrl}\`.
Please refer to the [authentication configuration documentation](${linkify(
'docs/usage/ci-configuration.md#authentication'
)}) to configure the Git credentials on your CI environment and make sure the [repositoryUrl](${linkify(
'docs/usage/configuration.md#repositoryurl'
)}) is configured with a [valid Git URL](https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols).`,
This can be caused by:
- a misconfiguration of the [repositoryUrl](${linkify('docs/usage/configuration.md#repositoryurl')}) option
- the repository being unavailable
- or missing push permission for the user configured via the [Git credentials on your CI environment](${linkify(
'docs/usage/ci-configuration.md#authentication'
)})`,
}),
EINVALIDTAGFORMAT: ({options: {tagFormat}}) => ({
message: 'Invalid `tagFormat` option.',
@@ -83,7 +85,7 @@ Please refer to the \`${pluginName}\` and [semantic-release plugins configuratio
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}\``
(type) => `\`${type}\``
).join(', ')}.
The \`analyzeCommits\` function of the \`${pluginName}\` returned \`${stringify(result)}\` instead.
@@ -183,7 +185,7 @@ Your configuration for the problematic branches is \`${stringify(branches)}\`.`,
details: `A minimum of 1 and a maximum of 3 release branches are required in the [branches configuration](${linkify(
'docs/usage/configuration.md#branches'
)}).
This may occur if your repository does not have a release branch, such as \`master\`.
Your configuration for the problematic branches is \`${stringify(branches)}\`.`,
+9 -7
View File
@@ -1,3 +1,5 @@
/* eslint require-atomic-updates: off */
const {isString, isPlainObject} = require('lodash');
const {getGitHead} = require('../git');
const hideSensitive = require('../hide-sensitive');
@@ -14,12 +16,12 @@ module.exports = {
default: ['@semantic-release/commit-analyzer'],
required: true,
dryRun: true,
outputValidator: output => !output || RELEASE_TYPE.includes(output),
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)),
commits: commits.filter((commit) => !/\[skip\s+release]|\[release\s+skip]/i.test(commit.message)),
}),
postprocess: results =>
postprocess: (results) =>
RELEASE_TYPE[
results.reduce((highest, result) => {
const typeIndex = RELEASE_TYPE.indexOf(result);
@@ -35,7 +37,7 @@ module.exports = {
generateNotes: {
required: false,
dryRun: true,
outputValidator: output => !output || isString(output),
outputValidator: (output) => !output || isString(output),
pipelineConfig: () => ({
getNextInput: ({nextRelease, ...context}, notes) => ({
...context,
@@ -51,7 +53,7 @@ module.exports = {
required: false,
dryRun: false,
pipelineConfig: ({generateNotes}) => ({
getNextInput: async context => {
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) {
@@ -68,7 +70,7 @@ module.exports = {
publish: {
required: false,
dryRun: false,
outputValidator: output => !output || isPlainObject(output),
outputValidator: (output) => !output || isPlainObject(output),
pipelineConfig: () => ({
// Add `nextRelease` and plugin properties to published release
transform: (release, step, {nextRelease}) => ({
@@ -81,7 +83,7 @@ module.exports = {
addChannel: {
required: false,
dryRun: false,
outputValidator: output => !output || isPlainObject(output),
outputValidator: (output) => !output || isPlainObject(output),
pipelineConfig: () => ({
// Add `nextRelease` and plugin properties to published release
transform: (release, step, {nextRelease}) => ({
+11 -11
View File
@@ -1,6 +1,6 @@
const {castArray, pickBy, isNil, isString, isPlainObject} = require('lodash');
const readPkgUp = require('read-pkg-up');
const cosmiconfig = require('cosmiconfig');
const {cosmiconfig} = require('cosmiconfig');
const resolveFrom = require('resolve-from');
const debug = require('debug')('semantic-release:config');
const {repoUrl} = require('./git');
@@ -19,14 +19,14 @@ const CONFIG_FILES = [
`${CONFIG_NAME}.config.js`,
];
module.exports = async (context, opts) => {
module.exports = async (context, cliOptions) => {
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};
let options = {...config, ...cliOptions};
if (options.ci === false) {
options.noCi = true;
}
@@ -38,14 +38,14 @@ module.exports = async (context, opts) => {
// 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(cwd, extendPath));
const extendsOptions = 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.entries(extendsOpts)
Object.entries(extendsOptions)
.filter(([, value]) => Boolean(value))
.reduce((pluginsPath, [option, value]) => {
castArray(value).forEach(plugin => {
castArray(value).forEach((plugin) => {
if (option === 'plugins' && validatePlugin(plugin)) {
pluginsPath[parseConfig(plugin)[0]] = extendPath;
} else if (
@@ -58,7 +58,7 @@ module.exports = async (context, opts) => {
return pluginsPath;
}, pluginsPath);
return {...result, ...extendsOpts};
return {...result, ...extendsOptions};
}, {}),
...options,
};
@@ -83,7 +83,7 @@ module.exports = async (context, opts) => {
'@semantic-release/github',
],
// Remove `null` and `undefined` options so they can be replaced with default ones
...pickBy(options, option => !isNil(option)),
...pickBy(options, (option) => !isNil(option)),
...(options.branches ? {branches: castArray(options.branches)} : {}),
};
@@ -92,7 +92,7 @@ module.exports = async (context, opts) => {
return {options, plugins: await plugins({...context, options}, pluginsPath)};
};
async function pkgRepoUrl(opts) {
const {package: pkg} = (await readPkgUp(opts)) || {};
return pkg && (isPlainObject(pkg.repository) ? pkg.repository.url : pkg.repository);
async function pkgRepoUrl(options) {
const {packageJson} = (await readPkgUp(options)) || {};
return packageJson && (isPlainObject(packageJson.repository) ? packageJson.repository.url : packageJson.repository);
}
+74 -12
View File
@@ -2,6 +2,49 @@ const {parse, format} = require('url'); // eslint-disable-line node/no-deprecate
const {isNil} = require('lodash');
const hostedGitInfo = require('hosted-git-info');
const {verifyAuth} = require('./git');
const debug = require('debug')('semantic-release:get-git-auth-url');
/**
* Machinery to format a repository URL with the given credentials
*
* @param {String} protocol URL protocol (which should not be present in repositoryUrl)
* @param {String} repositoryUrl User-given repository URL
* @param {String} gitCredentials The basic auth part of the URL
*
* @return {String} The formatted Git repository URL.
*/
function formatAuthUrl(protocol, repositoryUrl, gitCredentials) {
const [match, auth, host, basePort, path] =
/^(?!.+:\/\/)(?:(?<auth>.*)@)?(?<host>.*?):(?<port>\d+)?:?\/?(?<path>.*)$/.exec(repositoryUrl) || [];
const {port, hostname, ...parsed} = parse(
match ? `ssh://${auth ? `${auth}@` : ''}${host}${basePort ? `:${basePort}` : ''}/${path}` : repositoryUrl
);
return format({
...parsed,
auth: gitCredentials,
host: `${hostname}${protocol === 'ssh:' ? '' : port ? `:${port}` : ''}`,
protocol: protocol && /http[^s]/.test(protocol) ? 'http' : 'https',
});
}
/**
* Verify authUrl by calling git.verifyAuth, but don't throw on failure
*
* @param {Object} context semantic-release context.
* @param {String} authUrl Repository URL to verify
*
* @return {String} The authUrl as is if the connection was successfull, null otherwise
*/
async function ensureValidAuthUrl({cwd, env, branch}, authUrl) {
try {
await verifyAuth(authUrl, branch.name, {cwd, env});
return authUrl;
} catch (error) {
debug(error);
return null;
}
}
/**
* Determine the the git repository URL to use to push, either:
@@ -14,7 +57,8 @@ const {verifyAuth} = require('./git');
*
* @return {String} The formatted Git repository URL.
*/
module.exports = async ({cwd, env, branch, options: {repositoryUrl}}) => {
module.exports = async (context) => {
const {cwd, env, branch} = context;
const GIT_TOKENS = {
GIT_CREDENTIALS: undefined,
GH_TOKEN: undefined,
@@ -25,8 +69,11 @@ module.exports = async ({cwd, env, branch, options: {repositoryUrl}}) => {
GITLAB_TOKEN: 'gitlab-ci-token:',
BB_TOKEN: 'x-token-auth:',
BITBUCKET_TOKEN: 'x-token-auth:',
BB_TOKEN_BASIC_AUTH: '',
BITBUCKET_TOKEN_BASIC_AUTH: '',
};
let {repositoryUrl} = context.options;
const info = hostedGitInfo.fromUrl(repositoryUrl, {noGitPlus: true});
const {protocol, ...parsed} = parse(repositoryUrl);
@@ -40,19 +87,34 @@ module.exports = async ({cwd, env, branch, options: {repositoryUrl}}) => {
// Test if push is allowed without transforming the URL (e.g. is ssh keys are set up)
try {
debug('Verifying ssh auth by attempting to push to %s', repositoryUrl);
await verifyAuth(repositoryUrl, branch.name, {cwd, env});
} catch (error) {
const envVar = Object.keys(GIT_TOKENS).find(envVar => !isNil(env[envVar]));
const gitCredentials = `${GIT_TOKENS[envVar] || ''}${env[envVar] || ''}`;
} catch (_) {
debug('SSH key auth failed, falling back to https.');
const envVars = Object.keys(GIT_TOKENS).filter((envVar) => !isNil(env[envVar]));
if (gitCredentials) {
// If credentials are set via environment 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',
});
// Skip verification if there is no ambiguity on which env var to use for authentication
if (envVars.length === 1) {
const gitCredentials = `${GIT_TOKENS[envVars[0]] || ''}${env[envVars[0]]}`;
return formatAuthUrl(protocol, repositoryUrl, gitCredentials);
}
if (envVars.length > 1) {
debug(`Found ${envVars.length} credentials in environment, trying all of them`);
const candidateRepositoryUrls = [];
for (const envVar of envVars) {
const gitCredentials = `${GIT_TOKENS[envVar] || ''}${env[envVar]}`;
const authUrl = formatAuthUrl(protocol, repositoryUrl, gitCredentials);
candidateRepositoryUrls.push(ensureValidAuthUrl(context, authUrl));
}
const validRepositoryUrls = await Promise.all(candidateRepositoryUrls);
const chosenAuthUrlIndex = validRepositoryUrls.findIndex((url) => url !== null);
if (chosenAuthUrlIndex > -1) {
debug(`Using "${envVars[chosenAuthUrlIndex]}" to authenticate`);
return validRepositoryUrls[chosenAuthUrlIndex];
}
}
}
+13 -6
View File
@@ -1,13 +1,16 @@
const {isUndefined} = require('lodash');
const semver = require('semver');
const {makeTag} = require('./utils');
const {makeTag, isSameChannel} = require('./utils');
/**
* Last release.
*
* @typedef {Object} LastRelease
* @property {string} version The version number of the last release.
* @property {string} [gitHead] The Git reference used to make the last release.
* @property {string} gitHead The Git reference used to make the last release.
* @property {string} gitTag The git tag associated with the last release.
* @property {string} channel The channel on which of the last release was published.
* @property {string} name The name of the last release.
*/
/**
@@ -24,13 +27,17 @@ const {makeTag} = require('./utils');
* @return {LastRelease} The last tagged release or empty object if none is found.
*/
module.exports = ({branch, options: {tagFormat}}, {before} = {}) => {
const [{version, gitTag, gitHead, channel} = {}] = branch.tags
.filter(tag => (branch.type === 'prerelease' && branch.channel === tag.channel) || !semver.prerelease(tag.version))
.filter(tag => isUndefined(before) || semver.lt(tag.version, before))
const [{version, gitTag, channels} = {}] = branch.tags
.filter(
(tag) =>
((branch.type === 'prerelease' && tag.channels.some((channel) => isSameChannel(branch.channel, channel))) ||
!semver.prerelease(tag.version)) &&
(isUndefined(before) || semver.lt(tag.version, before))
)
.sort((a, b) => semver.rcompare(a.version, b.version));
if (gitTag) {
return {version, gitTag, gitHead, channel, name: makeTag(tagFormat, version)};
return {version, gitTag, channels, gitHead: gitTag, name: makeTag(tagFormat, version)};
}
return {};
+20 -6
View File
@@ -1,16 +1,30 @@
const semver = require('semver');
const {FIRST_RELEASE, FIRSTPRERELEASE} = require('./definitions/constants');
const {isSameChannel, getLatestVersion, tagsToVersions, highest} = require('./utils');
module.exports = ({branch, nextRelease: {type, channel}, lastRelease, logger}) => {
let version;
if (lastRelease.version) {
const {major, minor, patch} = semver.parse(lastRelease.version);
version =
branch.type === 'prerelease'
? semver.prerelease(lastRelease.version) && lastRelease.channel === channel
? semver.inc(lastRelease.version, 'prerelease')
: `${semver.inc(`${major}.${minor}.${patch}`, type)}-${branch.prerelease}.${FIRSTPRERELEASE}`
: semver.inc(lastRelease.version, type);
if (branch.type === 'prerelease') {
if (
semver.prerelease(lastRelease.version) &&
lastRelease.channels.some((lastReleaseChannel) => isSameChannel(lastReleaseChannel, channel))
) {
version = highest(
semver.inc(lastRelease.version, 'prerelease'),
`${semver.inc(getLatestVersion(tagsToVersions(branch.tags), {withPrerelease: true}), type)}-${
branch.prerelease
}.${FIRSTPRERELEASE}`
);
} else {
version = `${semver.inc(`${major}.${minor}.${patch}`, type)}-${branch.prerelease}.${FIRSTPRERELEASE}`;
}
} else {
version = semver.inc(lastRelease.version, type);
}
logger.log('The next release version is %s', version);
} else {
version = branch.type === 'prerelease' ? `${FIRST_RELEASE}-${branch.prerelease}.${FIRSTPRERELEASE}` : FIRST_RELEASE;
+60
View File
@@ -0,0 +1,60 @@
const {uniqBy, intersection} = require('lodash');
const semver = require('semver');
const semverDiff = require('semver-diff');
const getLastRelease = require('./get-last-release');
const {makeTag, getLowerBound} = require('./utils');
/**
* Find releases that have been merged from from a higher branch but not added on the channel of the current branch.
*
* @param {Object} context semantic-release context.
*
* @return {Array<Object>} Last release and next release to be added on the channel of the current branch.
*/
module.exports = (context) => {
const {
branch,
branches,
options: {tagFormat},
} = context;
const higherChannels = branches
// Consider only releases of higher branches
.slice(branches.findIndex(({name}) => name === branch.name) + 1)
// Exclude prerelease branches
.filter(({type}) => type !== 'prerelease')
.map(({channel}) => channel || null);
const versiontoAdd = uniqBy(
branch.tags.filter(
({channels, version}) =>
!channels.includes(branch.channel || null) &&
intersection(channels, higherChannels).length > 0 &&
(branch.type !== 'maintenance' || semver.gte(version, getLowerBound(branch.mergeRange)))
),
'version'
).sort((a, b) => semver.compare(b.version, a.version))[0];
if (versiontoAdd) {
const {version, gitTag, channels} = versiontoAdd;
const lastRelease = getLastRelease(context, {before: version});
if (semver.gt(getLastRelease(context).version, version)) {
return;
}
const type = lastRelease.version ? semverDiff(lastRelease.version, version) : 'major';
const name = makeTag(tagFormat, version);
return {
lastRelease,
currentRelease: {type, version, channels, gitTag, name, gitHead: gitTag},
nextRelease: {
type,
version,
channel: branch.channel || null,
gitTag: makeTag(tagFormat, version),
name,
gitHead: gitTag,
},
};
}
};
-71
View File
@@ -1,71 +0,0 @@
const {uniq} = require('lodash');
const semver = require('semver');
const semverDiff = require('semver-diff');
const getLastRelease = require('./get-last-release');
const {makeTag, getLowerBound} = require('./utils');
/**
* Find releases that have been merged from from a higher branch but not added on the channel of the current branch.
*
* @param {Object} context semantic-release context.
*
* @return {Array<Object>} Last release and next release to be added on the channel of the current branch.
*/
module.exports = context => {
const {
branch,
branches,
options: {tagFormat},
} = context;
return (
branches
// Consider only releases of higher branches
.slice(branches.findIndex(({name}) => name === branch.name) + 1)
// Exclude prerelease branches
.filter(({type}) => type !== 'prerelease')
// Find higher branch releases merged to building branch but not released on associated channel
.reduce(
(releases, higherBranch) => [
...releases,
// For all unique release version of the higher branch merged on current branch, excluding lower than start range version for maintenance branches
...uniq(
branch.tags.filter(
({channel, version}) =>
channel === higherBranch.channel &&
channel !== branch.channel &&
(branch.type !== 'maintenance' || semver.gte(version, getLowerBound(branch.mergeRange)))
)
)
// Find ones that are not released on the building branch channel
.filter(tag =>
branch.tags.every(
({version, channel}) =>
version !== tag.version || channel === higherBranch.channel || channel !== branch.channel
)
)
// Sort in ascending order to add the most recent release last
.sort((a, b) => semver.compare(a.version, b.version))
// Construct the last and next release to add to the building branch channel
.map(({version, gitHead, gitTag}) => {
const lastRelease = getLastRelease(context, {before: version});
const type = lastRelease.version ? semverDiff(lastRelease.version, version) : 'major';
const name = makeTag(tagFormat, version);
return {
lastRelease,
currentRelease: {type, version, channel: higherBranch.channel, gitTag, name, gitHead},
nextRelease: {
type,
version,
channel: branch.channel,
gitTag: makeTag(tagFormat, version, branch.channel),
name,
gitHead,
},
};
}),
],
[]
)
);
};
+128 -91
View File
@@ -1,8 +1,8 @@
const {matches, pick, memoize} = require('lodash');
const gitLogParser = require('git-log-parser');
const getStream = require('get-stream');
const execa = require('execa');
const debug = require('debug')('semantic-release:git');
const {GIT_NOTE_REF} = require('./definitions/constants');
Object.assign(gitLogParser.fields, {hash: 'H', message: 'B', gitTags: 'd', committerDate: {key: 'ci', type: Date}});
@@ -14,26 +14,23 @@ Object.assign(gitLogParser.fields, {hash: 'H', message: 'B', gitTags: 'd', commi
*
* @return {String} The commit sha of the tag in parameter or `null`.
*/
async function getTagHead(tagName, execaOpts) {
try {
return (await execa('git', ['rev-list', '-1', tagName], execaOpts)).stdout;
} catch (error) {
debug(error);
}
async function getTagHead(tagName, execaOptions) {
return (await execa('git', ['rev-list', '-1', tagName], execaOptions)).stdout;
}
/**
* Get all the repository tags.
* Get all the tags for a given branch.
*
* @param {String} branch The branch for which to retrieve the 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 getTags(execaOpts) {
return (await execa('git', ['tag'], execaOpts)).stdout
async function getTags(branch, execaOptions) {
return (await execa('git', ['tag', '--merged', branch], execaOptions)).stdout
.split('\n')
.map(tag => tag.trim())
.map((tag) => tag.trim())
.filter(Boolean);
}
@@ -45,13 +42,15 @@ async function getTags(execaOpts) {
* @param {Object} [execaOpts] Options to pass to `execa`.
* @return {Promise<Array<Object>>} The list of commits between `from` and `to`.
*/
async function getCommits(from, to, execaOpts) {
return (await getStream.array(
gitLogParser.parse(
{_: `${from ? from + '..' : ''}${to}`},
{cwd: execaOpts.cwd, env: {...process.env, ...execaOpts.env}}
async function getCommits(from, to, execaOptions) {
return (
await getStream.array(
gitLogParser.parse(
{_: `${from ? from + '..' : ''}${to}`},
{cwd: execaOptions.cwd, env: {...process.env, ...execaOptions.env}}
)
)
)).map(({message, gitTags, ...commit}) => ({...commit, message: message.trim(), gitTags: gitTags.trim()}));
).map(({message, gitTags, ...commit}) => ({...commit, message: message.trim(), gitTags: gitTags.trim()}));
}
/**
@@ -63,50 +62,11 @@ async function getCommits(from, to, execaOpts) {
* @return {Array<String>} List of git branches.
* @throws {Error} If the `git` command fails.
*/
async function getBranches(repositoryUrl, execaOpts) {
return (await execa('git', ['ls-remote', '--heads', repositoryUrl], execaOpts)).stdout
async function getBranches(repositoryUrl, execaOptions) {
return (await execa('git', ['ls-remote', '--heads', repositoryUrl], execaOptions)).stdout
.split('\n')
.map(branch => branch.match(/^.+refs\/heads\/(.+)$/)[1])
.filter(Boolean);
}
const getBranchCommits = memoize((branch, execaOpts) =>
getStream.array(gitLogParser.parse({_: branch}, {cwd: execaOpts.cwd, env: {...process.env, ...execaOpts.env}}))
);
/**
* Verify if the `ref` is in the direct history of a given branch.
*
* @param {String} ref The reference to look for.
* @param {String} branch The branch for which to check if the `ref` is in history.
* @param {Boolean} findRebasedTags Weither consider in history tags associated with a commit that was rebased to another branch (i.e. GitHub Rebase and Merge feature).
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @return {Boolean} `true` if the reference is in the history of the current branch, falsy otherwise.
*/
async function isRefInHistory(ref, branch, findRebasedTags, execaOpts) {
if (!(await isRefExists(branch, execaOpts))) {
return false;
}
try {
await execa('git', ['merge-base', '--is-ancestor', ref, branch], execaOpts);
return true;
} catch (error) {
if (error.exitCode === 1) {
if (findRebasedTags) {
const [tagCommit] = await getStream.array(
gitLogParser.parse({_: ref, n: '1'}, {cwd: execaOpts.cwd, env: {...process.env, ...execaOpts.env}})
);
return (await getBranchCommits(branch, execaOpts)).some(matches(pick(tagCommit, ['message', 'author'])));
}
return false;
}
debug(error);
throw error;
}
.filter(Boolean)
.map((branch) => branch.match(/^.+refs\/heads\/(?<branch>.+)$/)[1]);
}
/**
@@ -117,24 +77,31 @@ async function isRefInHistory(ref, branch, findRebasedTags, execaOpts) {
*
* @return {Boolean} `true` if the reference exists, falsy otherwise.
*/
async function isRefExists(ref, execaOpts) {
async function isRefExists(ref, execaOptions) {
try {
return (await execa('git', ['rev-parse', '--verify', ref], execaOpts)).exitCode === 0;
return (await execa('git', ['rev-parse', '--verify', ref], execaOptions)).exitCode === 0;
} catch (error) {
debug(error);
}
}
/**
* Unshallow the git repository if necessary and fetch all the tags.
* Fetch all the tags from a branch. Unshallow if necessary.
* This will update the local branch from the latest on the remote if:
* - The branch is not the one that triggered the CI
* - The CI created a detached head
*
* Otherwise it just calls `git fetch` without specifying the `refspec` option to avoid overwritting the head commit set by the CI.
*
* The goal is to retrieve the informations on all the release branches without "disturbing" the CI, leaving the trigger branch or the detached head intact.
*
* @param {String} repositoryUrl The remote repository URL.
* @param {String} branch The repository branch to fetch.
* @param {Object} [execaOpts] Options to pass to `execa`.
*/
async function fetch(repositoryUrl, branch, execaOpts) {
const isLocalExists =
(await execa('git', ['rev-parse', '--verify', branch], {...execaOpts, reject: false})).exitCode === 0;
async function fetch(repositoryUrl, branch, ciBranch, execaOptions) {
const isDetachedHead =
(await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {...execaOptions, reject: false})).stdout === 'HEAD';
try {
await execa(
@@ -143,23 +110,48 @@ async function fetch(repositoryUrl, branch, execaOpts) {
'fetch',
'--unshallow',
'--tags',
...(isLocalExists ? [repositoryUrl] : [repositoryUrl, `+refs/heads/${branch}:refs/heads/${branch}`]),
...(branch === ciBranch && !isDetachedHead
? [repositoryUrl]
: ['--update-head-ok', repositoryUrl, `+refs/heads/${branch}:refs/heads/${branch}`]),
],
execaOpts
execaOptions
);
} catch (error) {
} catch (_) {
await execa(
'git',
[
'fetch',
'--tags',
...(isLocalExists ? [repositoryUrl] : [repositoryUrl, `+refs/heads/${branch}:refs/heads/${branch}`]),
...(branch === ciBranch && !isDetachedHead
? [repositoryUrl]
: ['--update-head-ok', repositoryUrl, `+refs/heads/${branch}:refs/heads/${branch}`]),
],
execaOpts
execaOptions
);
}
}
/**
* Unshallow the git repository if necessary and fetch all the notes.
*
* @param {String} repositoryUrl The remote repository URL.
* @param {Object} [execaOpts] Options to pass to `execa`.
*/
async function fetchNotes(repositoryUrl, execaOptions) {
try {
await execa(
'git',
['fetch', '--unshallow', repositoryUrl, `+refs/notes/${GIT_NOTE_REF}:refs/notes/${GIT_NOTE_REF}`],
execaOptions
);
} catch (_) {
await execa('git', ['fetch', repositoryUrl, `+refs/notes/${GIT_NOTE_REF}:refs/notes/${GIT_NOTE_REF}`], {
...execaOptions,
reject: false,
});
}
}
/**
* Get the HEAD sha.
*
@@ -167,8 +159,8 @@ async function fetch(repositoryUrl, branch, execaOpts) {
*
* @return {String} the sha of the HEAD commit.
*/
async function getGitHead(execaOpts) {
return (await execa('git', ['rev-parse', 'HEAD'], execaOpts)).stdout;
async function getGitHead(execaOptions) {
return (await execa('git', ['rev-parse', 'HEAD'], execaOptions)).stdout;
}
/**
@@ -178,9 +170,9 @@ async function getGitHead(execaOpts) {
*
* @return {string} The value of the remote git URL.
*/
async function repoUrl(execaOpts) {
async function repoUrl(execaOptions) {
try {
return (await execa('git', ['config', '--get', 'remote.origin.url'], execaOpts)).stdout;
return (await execa('git', ['config', '--get', 'remote.origin.url'], execaOptions)).stdout;
} catch (error) {
debug(error);
}
@@ -193,9 +185,9 @@ async function repoUrl(execaOpts) {
*
* @return {Boolean} `true` if the current working directory is in a git repository, falsy otherwise.
*/
async function isGitRepo(execaOpts) {
async function isGitRepo(execaOptions) {
try {
return (await execa('git', ['rev-parse', '--git-dir'], execaOpts)).exitCode === 0;
return (await execa('git', ['rev-parse', '--git-dir'], execaOptions)).exitCode === 0;
} catch (error) {
debug(error);
}
@@ -210,9 +202,9 @@ async function isGitRepo(execaOpts) {
*
* @throws {Error} if not authorized to push.
*/
async function verifyAuth(repositoryUrl, branch, execaOpts) {
async function verifyAuth(repositoryUrl, branch, execaOptions) {
try {
await execa('git', ['push', '--dry-run', repositoryUrl, `HEAD:${branch}`], execaOpts);
await execa('git', ['push', '--dry-run', '--no-verify', repositoryUrl, `HEAD:${branch}`], execaOptions);
} catch (error) {
debug(error);
throw error;
@@ -228,8 +220,8 @@ async function verifyAuth(repositoryUrl, branch, execaOpts) {
*
* @throws {Error} if the tag creation failed.
*/
async function tag(tagName, ref, execaOpts) {
await execa('git', ['tag', tagName, ref], execaOpts);
async function tag(tagName, ref, execaOptions) {
await execa('git', ['tag', tagName, ref], execaOptions);
}
/**
@@ -240,8 +232,20 @@ async function tag(tagName, ref, execaOpts) {
*
* @throws {Error} if the push failed.
*/
async function push(repositoryUrl, execaOpts) {
await execa('git', ['push', '--tags', repositoryUrl], execaOpts);
async function push(repositoryUrl, execaOptions) {
await execa('git', ['push', '--tags', repositoryUrl], execaOptions);
}
/**
* Push notes to the remote repository.
*
* @param {String} repositoryUrl The remote repository URL.
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @throws {Error} if the push failed.
*/
async function pushNotes(repositoryUrl, execaOptions) {
await execa('git', ['push', repositoryUrl, `refs/notes/${GIT_NOTE_REF}`], execaOptions);
}
/**
@@ -252,9 +256,9 @@ async function push(repositoryUrl, execaOpts) {
*
* @return {Boolean} `true` if valid, falsy otherwise.
*/
async function verifyTagName(tagName, execaOpts) {
async function verifyTagName(tagName, execaOptions) {
try {
return (await execa('git', ['check-ref-format', `refs/tags/${tagName}`], execaOpts)).exitCode === 0;
return (await execa('git', ['check-ref-format', `refs/tags/${tagName}`], execaOptions)).exitCode === 0;
} catch (error) {
debug(error);
}
@@ -268,9 +272,9 @@ async function verifyTagName(tagName, execaOpts) {
*
* @return {Boolean} `true` if valid, falsy otherwise.
*/
async function verifyBranchName(branch, execaOpts) {
async function verifyBranchName(branch, execaOptions) {
try {
return (await execa('git', ['check-ref-format', `refs/heads/${branch}`], execaOpts)).exitCode === 0;
return (await execa('git', ['check-ref-format', `refs/heads/${branch}`], execaOptions)).exitCode === 0;
} catch (error) {
debug(error);
}
@@ -285,30 +289,63 @@ async function verifyBranchName(branch, execaOpts) {
*
* @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(repositoryUrl, branch, execaOpts) {
const {stdout: remoteHead} = await execa('git', ['ls-remote', '--heads', repositoryUrl, branch], execaOpts);
async function isBranchUpToDate(repositoryUrl, branch, execaOptions) {
return (
(await getGitHead(execaOptions)) ===
(await execa('git', ['ls-remote', '--heads', repositoryUrl, branch], execaOptions)).stdout.match(/^(?<ref>\w+)?/)[1]
);
}
/**
* Get and parse the JSON note of a given reference.
*
* @param {String} ref The Git reference for which to retrieve the note.
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @return {Object} the parsed JSON note if there is one, an empty object otherwise.
*/
async function getNote(ref, execaOptions) {
try {
return await isRefInHistory(remoteHead.match(/^(\w+)?/)[1], branch, false, execaOpts);
return JSON.parse((await execa('git', ['notes', '--ref', GIT_NOTE_REF, 'show', ref], execaOptions)).stdout);
} catch (error) {
if (error.exitCode === 1) {
return {};
}
debug(error);
throw error;
}
}
/**
* Get and parse the JSON note of a given reference.
*
* @param {Object} note The object to save in the reference note.
* @param {String} ref The Git reference to add the note to.
* @param {Object} [execaOpts] Options to pass to `execa`.
*/
async function addNote(note, ref, execaOptions) {
await execa('git', ['notes', '--ref', GIT_NOTE_REF, 'add', '-f', '-m', JSON.stringify(note), ref], execaOptions);
}
module.exports = {
getTagHead,
getTags,
getCommits,
getBranches,
isRefInHistory,
isRefExists,
fetch,
fetchNotes,
getGitHead,
repoUrl,
isGitRepo,
verifyAuth,
tag,
push,
pushNotes,
verifyTagName,
isBranchUpToDate,
verifyBranchName,
getNote,
addNote,
};
+11 -6
View File
@@ -1,12 +1,17 @@
const {escapeRegExp, size, isString} = require('lodash');
const {SECRET_REPLACEMENT, SECRET_MIN_SIZE} = require('./definitions/constants');
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
);
module.exports = (env) => {
const toReplace = Object.keys(env).filter((envVar) => {
// https://github.com/semantic-release/semantic-release/issues/1558
if (envVar === 'GOPRIVATE') {
return false;
}
const regexp = new RegExp(toReplace.map(envVar => escapeRegExp(env[envVar])).join('|'), 'g');
return output =>
return /token|password|credential|secret|private/i.test(envVar) && size(env[envVar].trim()) >= SECRET_MIN_SIZE;
});
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;
};
+6 -6
View File
@@ -46,14 +46,14 @@ module.exports = (context, pluginsPath) => {
const pluginsConf = Object.entries(PLUGINS_DEFINITIONS).reduce(
(pluginsConf, [type, {required, default: def, pipelineConfig, postprocess = identity, preprocess = identity}]) => {
let pluginOpts;
let pluginOptions;
if (isNil(options[type]) && def) {
pluginOpts = def;
pluginOptions = 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 =>
options[type] = castArray(plugins[type]).map((plugin) =>
plugin ? [plugin[0], Object.assign(plugin[1], options[type])] : plugin
);
}
@@ -63,10 +63,10 @@ module.exports = (context, pluginsPath) => {
return pluginsConf;
}
pluginOpts = options[type];
pluginOptions = options[type];
}
const steps = castArray(pluginOpts).map(pluginOpt =>
const steps = castArray(pluginOptions).map((pluginOpt) =>
normalize(
{...context, options: omit(options, Object.keys(PLUGINS_DEFINITIONS), 'plugins')},
type,
@@ -75,7 +75,7 @@ module.exports = (context, pluginsPath) => {
)
);
pluginsConf[type] = async input =>
pluginsConf[type] = async (input) =>
postprocess(
await pipeline(steps, pipelineConfig && pipelineConfig(pluginsConf, logger))(await preprocess(input)),
input
+2 -2
View File
@@ -26,7 +26,7 @@ module.exports = (context, type, pluginOpt, pluginsPath) => {
throw getError('EPLUGIN', {type, pluginName});
}
const validator = async input => {
const validator = async (input) => {
const {dryRun, outputValidator} = PLUGINS_DEFINITIONS[type] || {};
try {
if (!input.options.dryRun || dryRun) {
@@ -48,7 +48,7 @@ module.exports = (context, type, pluginOpt, pluginsPath) => {
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}));
extractErrors(error).forEach((err) => Object.assign(err, {pluginName}));
throw error;
}
};
+1 -1
View File
@@ -25,7 +25,7 @@ const {extractErrors} = require('../utils');
*
* @return {Pipeline} A Function that execute the `steps` sequencially
*/
module.exports = (steps, {settleAll = false, getNextInput = identity, transform = identity} = {}) => async input => {
module.exports = (steps, {settleAll = false, getNextInput = identity, transform = identity} = {}) => async (input) => {
const results = [];
const errors = [];
await pReduce(
+2 -2
View File
@@ -2,8 +2,8 @@ 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 => {
const validateSteps = (conf) => {
return conf.every((conf) => {
if (
isArray(conf) &&
(conf.length === 1 || conf.length === 2) &&
+24 -12
View File
@@ -8,13 +8,13 @@ function extractErrors(err) {
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 objs.map((object) => {
Object.getOwnPropertyNames(object).forEach((prop) => {
if (object[prop]) {
object[prop] = hideFunction(object[prop]);
}
});
return obj;
return object;
});
}
@@ -31,11 +31,18 @@ function isMaintenanceRange(range) {
}
function getUpperBound(range) {
return semver.valid(range) ? range : ((semver.validRange(range) || '').match(/<(\d+\.\d+\.\d+)$/) || [])[1];
const result = semver.valid(range)
? range
: ((semver.validRange(range) || '').match(/<(?<upperBound>\d+\.\d+\.\d+(-\d+)?)$/) || [])[1];
return result
? // https://github.com/npm/node-semver/issues/322
result.replace(/-\d+$/, '')
: result;
}
function getLowerBound(range) {
return ((semver.validRange(range) || '').match(/(\d+\.\d+\.\d+)/) || [])[1];
return ((semver.validRange(range) || '').match(/(?<lowerBound>\d+\.\d+\.\d+)/) || [])[1];
}
function highest(version1, version2) {
@@ -47,17 +54,17 @@ function lowest(version1, version2) {
}
function getLatestVersion(versions, {withPrerelease} = {}) {
return versions.filter(version => withPrerelease || !semver.prerelease(version)).sort(semver.rcompare)[0];
return versions.filter((version) => withPrerelease || !semver.prerelease(version)).sort(semver.rcompare)[0];
}
function getEarliestVersion(versions, {withPrerelease} = {}) {
return versions.filter(version => withPrerelease || !semver.prerelease(version)).sort(semver.compare)[0];
return versions.filter((version) => withPrerelease || !semver.prerelease(version)).sort(semver.compare)[0];
}
function getFirstVersion(versions, lowerBranches) {
const lowerVersion = union(...lowerBranches.map(({tags}) => tagsToVersions(tags))).sort(semver.rcompare);
if (lowerVersion[0]) {
return versions.sort(semver.compare).find(version => semver.gt(version, lowerVersion[0]));
return versions.sort(semver.compare).find((version) => semver.gt(version, lowerVersion[0]));
}
return getEarliestVersion(versions);
@@ -67,8 +74,12 @@ function getRange(min, max) {
return `>=${min}${max ? ` <${max}` : ''}`;
}
function makeTag(tagFormat, version, channel) {
return template(tagFormat)({version: `${version}${channel ? `@${channel}` : ''}`});
function makeTag(tagFormat, version) {
return template(tagFormat)({version});
}
function isSameChannel(channel, otherChannel) {
return channel === otherChannel || (!channel && !otherChannel);
}
module.exports = {
@@ -86,4 +97,5 @@ module.exports = {
getFirstVersion,
getRange,
makeTag,
isSameChannel,
};
+2 -2
View File
@@ -3,7 +3,7 @@ const AggregateError = require('aggregate-error');
const {isGitRepo, verifyTagName} = require('./git');
const getError = require('./get-error');
module.exports = async context => {
module.exports = async (context) => {
const {
cwd,
env,
@@ -29,7 +29,7 @@ module.exports = async context => {
errors.push(getError('ETAGNOVERSION', context));
}
branches.forEach(branch => {
branches.forEach((branch) => {
if (
!((isString(branch) && branch.trim()) || (isPlainObject(branch) && isString(branch.name) && branch.name.trim()))
) {
+30 -29
View File
@@ -7,9 +7,7 @@
"files": [
"test/**/*.test.js"
],
"helpers": [
"test/helpers/**/*"
]
"timeout": "2m"
},
"bin": {
"semantic-release": "bin/semantic-release.js"
@@ -22,16 +20,16 @@
"Pierre Vanduynslager (https://twitter.com/@pvdlg_)"
],
"dependencies": {
"@semantic-release/commit-analyzer": "7.0.0-beta.4",
"@semantic-release/commit-analyzer": "^8.0.0",
"@semantic-release/error": "^2.2.0",
"@semantic-release/github": "5.6.0-beta.1",
"@semantic-release/npm": "6.0.0-beta.1",
"@semantic-release/release-notes-generator": "^7.1.2",
"@semantic-release/github": "^7.0.0",
"@semantic-release/npm": "^7.0.0",
"@semantic-release/release-notes-generator": "^9.0.0",
"aggregate-error": "^3.0.0",
"cosmiconfig": "^5.0.1",
"cosmiconfig": "^6.0.0",
"debug": "^4.0.0",
"env-ci": "^4.0.0",
"execa": "^3.0.0",
"env-ci": "^5.0.0",
"execa": "^4.0.0",
"figures": "^3.0.0",
"find-versions": "^3.0.0",
"get-stream": "^5.0.0",
@@ -39,40 +37,40 @@
"hook-std": "^2.0.0",
"hosted-git-info": "^3.0.0",
"lodash": "^4.17.15",
"marked": "^0.7.0",
"marked-terminal": "^3.2.0",
"micromatch": "3.1.5",
"p-each-series": "^1.0.0",
"marked": "^1.0.0",
"marked-terminal": "^4.0.0",
"micromatch": "^4.0.2",
"p-each-series": "^2.1.0",
"p-reduce": "^2.0.0",
"read-pkg-up": "^6.0.0",
"read-pkg-up": "^7.0.0",
"resolve-from": "^5.0.0",
"semver": "^6.0.0",
"semver-diff": "^2.1.0",
"semver": "^7.3.2",
"semver-diff": "^3.1.1",
"signale": "^1.2.1",
"yargs": "^14.0.0"
"yargs": "^15.0.1"
},
"devDependencies": {
"ava": "^2.0.0",
"ava": "^3.1.0",
"clear-module": "^4.0.0",
"codecov": "^3.0.0",
"delay": "^4.0.0",
"dockerode": "^2.5.2",
"dockerode": "^3.0.0",
"file-url": "^3.0.0",
"fs-extra": "^8.0.0",
"got": "^9.0.0",
"fs-extra": "^9.0.0",
"got": "^11.0.0",
"js-yaml": "^3.10.0",
"mockserver-client": "^5.1.1",
"nock": "^11.1.0",
"nyc": "^14.0.0",
"nock": "^12.0.0",
"nyc": "^15.0.0",
"p-retry": "^4.0.0",
"proxyquire": "^2.0.0",
"sinon": "^7.2.7",
"sinon": "^9.0.0",
"stream-buffers": "^3.0.2",
"tempy": "^0.3.0",
"xo": "^0.24.0"
"tempy": "^0.5.0",
"xo": "^0.29.0"
},
"engines": {
"node": ">=8.15"
"node": ">=10.18"
},
"files": [
"bin",
@@ -128,6 +126,9 @@
},
"xo": {
"prettier": true,
"space": true
"space": true,
"rules": {
"unicorn/string-content": "off"
}
}
}
+108 -48
View File
@@ -1,7 +1,7 @@
import test from 'ava';
import {union} from 'lodash';
import semver from 'semver';
import proxyquire from 'proxyquire';
const test = require('ava');
const {union} = require('lodash');
const semver = require('semver');
const proxyquire = require('proxyquire');
const getBranch = (branches, branch) => branches.find(({name}) => name === branch);
const release = (branches, name, version) => getBranch(branches, name).tags.push({version});
@@ -12,7 +12,7 @@ const merge = (branches, source, target, tag) => {
);
};
test('Enforce ranges with branching release workflow', async t => {
test('Enforce ranges with branching release workflow', async (t) => {
const branches = [
{name: '1.x', tags: []},
{name: '1.0.x', tags: []},
@@ -24,51 +24,82 @@ test('Enforce ranges with branching release workflow', async t => {
];
const getBranches = proxyquire('../../lib/branches', {'./get-tags': () => branches, './expand': () => []});
let result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range}));
let result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({
name,
range,
}));
t.is(getBranch(result, '1.0.x').range, '>=1.0.0 <1.0.0', 'Cannot release on 1.0.x before a releasing on master');
t.is(getBranch(result, '1.x').range, '>=1.1.0 <1.0.0', 'Cannot release on 1.x before a releasing on master');
t.is(getBranch(result, 'master').range, '>=1.0.0 <1.1.0', 'Can release only patch on master');
t.is(getBranch(result, 'next').range, '>=1.1.0 <2.0.0', 'Can release only minor on next');
t.is(getBranch(result, 'next-major').range, '>=2.0.0', 'Can release only major on next-major');
t.is(getBranch(result, 'master').range, '>=1.0.0');
t.is(getBranch(result, 'next').range, '>=1.0.0');
t.is(getBranch(result, 'next-major').range, '>=1.0.0');
release(branches, 'master', '1.0.0');
result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range}));
result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({
name,
range,
}));
t.is(getBranch(result, '1.0.x').range, '>=1.0.0 <1.0.0', 'Cannot release on 1.0.x before a releasing on master');
t.is(getBranch(result, '1.x').range, '>=1.1.0 <1.0.0', 'Cannot release on 1.x before a releasing on master');
t.is(getBranch(result, 'master').range, '>=1.0.0 <1.1.0', 'Can release only patch on master');
t.is(getBranch(result, 'master').range, '>=1.0.0');
t.is(getBranch(result, 'next').range, '>=1.0.0');
t.is(getBranch(result, 'next-major').range, '>=1.0.0');
release(branches, 'master', '1.0.1');
result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range}));
t.is(getBranch(result, 'master').range, '>=1.0.1 <1.1.0', 'Can release only patch, > than 1.0.1 on master');
result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({
name,
range,
}));
t.is(getBranch(result, 'master').range, '>=1.0.1', 'Can release only > than 1.0.1 on master');
t.is(getBranch(result, 'next').range, '>=1.0.1', 'Can release only > than 1.0.1 on next');
t.is(getBranch(result, 'next-major').range, '>=1.0.1', 'Can release only > than 1.0.1 on next-major');
merge(branches, 'master', 'next');
merge(branches, 'master', 'next-major');
result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range}));
t.is(getBranch(result, 'master').range, '>=1.0.1 <1.1.0', 'Can release only patch, > than 1.0.1 on master');
t.is(getBranch(result, 'next').range, '>=1.1.0 <2.0.0', 'Can release only minor on next');
t.is(getBranch(result, 'next-major').range, '>=2.0.0', 'Can release only major on next-major');
result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({
name,
range,
}));
t.is(getBranch(result, 'master').range, '>=1.0.1', 'Can release only > than 1.0.1 on master');
t.is(getBranch(result, 'next').range, '>=1.0.1', 'Can release only > than 1.0.1 on next');
t.is(getBranch(result, 'next-major').range, '>=1.0.1', 'Can release only > than 1.0.1 on next-major');
release(branches, 'next', '1.1.0');
release(branches, 'next', '1.1.1');
result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range}));
t.is(getBranch(result, 'next').range, '>=1.1.1 <2.0.0', 'Can release only patch or minor, > than 1.1.0 on next');
result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({
name,
range,
}));
t.is(getBranch(result, 'master').range, '>=1.0.1 <1.1.0', 'Can release only patch, > than 1.0.1 on master');
t.is(getBranch(result, 'next').range, '>=1.1.1', 'Can release only > than 1.1.1 on next');
t.is(getBranch(result, 'next-major').range, '>=1.1.1', 'Can release > than 1.1.1 on next-major');
release(branches, 'next-major', '2.0.0');
release(branches, 'next-major', '2.0.1');
result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range}));
result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({
name,
range,
}));
t.is(getBranch(result, 'master').range, '>=1.0.1 <1.1.0', 'Can release only patch, > than 1.0.1 on master');
t.is(getBranch(result, 'next').range, '>=1.1.1 <2.0.0', 'Can release only patch or minor, > than 1.1.0 on next');
t.is(getBranch(result, 'next-major').range, '>=2.0.1', 'Can release any version, > than 2.0.1 on next-major');
merge(branches, 'next-major', 'beta');
release(branches, 'beta', '3.0.0-beta.1');
merge(branches, 'beta', 'alpha');
release(branches, 'alpha', '4.0.0-alpha.1');
result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range}));
result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({
name,
range,
}));
t.is(getBranch(result, 'next-major').range, '>=2.0.1', 'Can release any version, > than 2.0.1 on next-major');
merge(branches, 'master', '1.0.x');
merge(branches, 'master', '1.x');
release(branches, 'master', '1.0.1');
result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range}));
result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({
name,
range,
}));
t.is(getBranch(result, 'master').range, '>=1.0.1 <1.1.0', 'Can release only patch, > than 1.0.1 on master');
t.is(
getBranch(result, '1.0.x').range,
@@ -80,7 +111,10 @@ test('Enforce ranges with branching release workflow', async t => {
release(branches, 'master', '1.0.2');
release(branches, 'master', '1.0.3');
release(branches, 'master', '1.0.4');
result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range}));
result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({
name,
range,
}));
t.is(getBranch(result, 'master').range, '>=1.0.4 <1.1.0', 'Can release only patch, > than 1.0.4 on master');
t.is(
getBranch(result, '1.0.x').range,
@@ -90,9 +124,13 @@ test('Enforce ranges with branching release workflow', async t => {
t.is(getBranch(result, '1.x').range, '>=1.1.0 <1.0.2', 'Cannot release on 1.x before >= 1.2.0 is released on master');
merge(branches, 'next', 'master');
result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range}));
t.is(getBranch(result, 'master').range, '>=1.1.1 <1.2.0', 'Can release only patch, > than 1.1.1 on master');
t.is(getBranch(result, 'next').range, '>=1.2.0 <2.0.0', 'Can release only patch or minor, > than 1.2.0 on next');
result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({
name,
range,
}));
t.is(getBranch(result, 'master').range, '>=1.1.1', 'Can release only > than 1.1.1 on master');
t.is(getBranch(result, 'next').range, '>=1.1.1 <2.0.0', 'Can release only patch or minor, > than 1.1.1 on next');
t.is(getBranch(result, 'next-major').range, '>=2.0.1', 'Can release any version, > than 2.0.1 on next-major');
t.is(
getBranch(result, '1.0.x').range,
@@ -102,40 +140,56 @@ test('Enforce ranges with branching release workflow', async t => {
t.is(getBranch(result, '1.x').range, '>=1.1.0 <1.0.2', 'Cannot release on 1.x before >= 2.0.0 is released on master');
merge(branches, 'master', '1.0.x', '1.0.4');
result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range}));
t.is(getBranch(result, 'master').range, '>=1.1.1 <1.2.0', 'Can release only patch, > than 1.1.1 on master');
result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({
name,
range,
}));
t.is(getBranch(result, 'master').range, '>=1.1.1', 'Can release only > than 1.1.1 on master');
t.is(getBranch(result, '1.0.x').range, '>=1.0.4 <1.1.0', 'Can release on 1.0.x only within range');
t.is(getBranch(result, '1.x').range, '>=1.1.0 <1.1.0', 'Cannot release on 1.x before >= 2.0.0 is released on master');
merge(branches, 'master', '1.x');
result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range}));
t.is(getBranch(result, 'master').range, '>=1.1.1 <1.2.0', 'Can release only patch, > than 1.1.1 on master');
result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({
name,
range,
}));
t.is(getBranch(result, 'master').range, '>=1.1.1', 'Can release only > than 1.1.1 on master');
t.is(getBranch(result, '1.0.x').range, '>=1.0.4 <1.1.0', 'Can release on 1.0.x only within range');
t.is(getBranch(result, '1.x').range, '>=1.1.1 <1.1.1', 'Cannot release on 1.x before >= 2.0.0 is released on master');
merge(branches, 'next-major', 'next');
merge(branches, 'next', 'master');
result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range}));
t.is(getBranch(result, 'master').range, '>=2.0.1 <2.1.0', 'Can release only patch, > than 2.0.1 on master');
t.is(getBranch(result, 'next').range, '>=2.1.0 <3.0.0', 'Can release only minor on next');
t.is(getBranch(result, 'next-major').range, '>=3.0.0', 'Can release only major on next-major');
result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({
name,
range,
}));
t.is(getBranch(result, 'master').range, '>=2.0.1', 'Can release only > than 2.0.1 on master');
t.is(getBranch(result, 'next').range, '>=2.0.1', 'Can release only > than 2.0.1 on next');
t.is(getBranch(result, 'next-major').range, '>=2.0.1', 'Can release only > than 2.0.1 on next-major');
t.is(getBranch(result, '1.x').range, '>=1.1.1 <2.0.0', 'Can release on 1.x only within range');
merge(branches, 'beta', 'master');
result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range}));
t.is(getBranch(result, 'master').range, '>=2.0.1 <2.1.0', 'Can release only patch, > than 2.0.1 on master');
t.is(getBranch(result, 'next').range, '>=2.1.0 <3.0.0', 'Can release only minor on next');
t.is(getBranch(result, 'next-major').range, '>=3.0.0', 'Can release only major on next-major');
release(branches, 'master', '3.0.0');
result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({
name,
range,
}));
t.is(getBranch(result, 'master').range, '>=3.0.0', 'Can release only > than 3.0.0 on master');
t.is(getBranch(result, 'next').range, '>=3.0.0', 'Can release only > than 3.0.0 on next');
t.is(getBranch(result, 'next-major').range, '>=3.0.0', 'Can release only > than 3.0.0 on next-major');
branches.push({name: '1.1.x', tags: []});
merge(branches, '1.x', '1.1.x');
result = (await getBranches('repositoryUrl', {options: {branches}})).map(({name, range}) => ({name, range}));
result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({
name,
range,
}));
t.is(getBranch(result, '1.0.x').range, '>=1.0.4 <1.1.0', 'Can release on 1.0.x only within range');
t.is(getBranch(result, '1.1.x').range, '>=1.1.1 <1.2.0', 'Can release on 1.1.x only within range');
t.is(getBranch(result, '1.x').range, '>=1.2.0 <2.0.0', 'Can release on 1.x only within range');
});
test('Throw SemanticReleaseError for invalid configurations', async t => {
test('Throw SemanticReleaseError for invalid configurations', async (t) => {
const branches = [
{name: '123', range: '123', tags: []},
{name: '1.x', tags: []},
@@ -146,7 +200,7 @@ test('Throw SemanticReleaseError for invalid configurations', async t => {
{name: 'preview', prerelease: 'alpha', tags: []},
];
const getBranches = proxyquire('../../lib/branches', {'./get-tags': () => branches, './expand': () => []});
const errors = [...(await t.throwsAsync(getBranches('repositoryUrl', {options: {branches}})))];
const errors = [...(await t.throwsAsync(getBranches('repositoryUrl', 'master', {options: {branches}})))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'EMAINTENANCEBRANCH');
@@ -170,11 +224,14 @@ test('Throw SemanticReleaseError for invalid configurations', async t => {
t.truthy(errors[4].details);
});
test('Throw a SemanticReleaseError if there is duplicate branches', async t => {
const branches = [{name: 'master', tags: []}, {name: 'master', tags: []}];
test('Throw a SemanticReleaseError if there is duplicate branches', async (t) => {
const branches = [
{name: 'master', tags: []},
{name: 'master', tags: []},
];
const getBranches = proxyquire('../../lib/branches', {'./get-tags': () => branches, './expand': () => []});
const errors = [...(await t.throwsAsync(getBranches('repositoryUrl', {options: {branches}})))];
const errors = [...(await t.throwsAsync(getBranches('repositoryUrl', 'master', {options: {branches}})))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'EDUPLICATEBRANCHES');
@@ -182,11 +239,14 @@ test('Throw a SemanticReleaseError if there is duplicate branches', async t => {
t.truthy(errors[0].details);
});
test('Throw a SemanticReleaseError for each invalid branch name', async t => {
const branches = [{name: '~master', tags: []}, {name: '^master', tags: []}];
test('Throw a SemanticReleaseError for each invalid branch name', async (t) => {
const branches = [
{name: '~master', tags: []},
{name: '^master', tags: []},
];
const getBranches = proxyquire('../../lib/branches', {'./get-tags': () => branches, './expand': () => []});
const errors = [...(await t.throwsAsync(getBranches('repositoryUrl', {options: {branches}})))];
const errors = [...(await t.throwsAsync(getBranches('repositoryUrl', 'master', {options: {branches}})))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'EINVALIDBRANCHNAME');
+4 -4
View File
@@ -1,8 +1,8 @@
import test from 'ava';
import expand from '../../lib/branches/expand';
import {gitRepo, gitCommits, gitCheckout, gitPush} from '../helpers/git-utils';
const test = require('ava');
const expand = require('../../lib/branches/expand');
const {gitRepo, gitCommits, gitCheckout, gitPush} = require('../helpers/git-utils');
test('Expand branches defined with globs', async t => {
test('Expand branches defined with globs', async (t) => {
const {cwd, repositoryUrl} = await gitRepo(true);
await gitCommits(['First'], {cwd});
await gitPush(repositoryUrl, 'master', {cwd});
+43 -92
View File
@@ -1,8 +1,8 @@
import test from 'ava';
import getTags from '../../lib/branches/get-tags';
import {gitRepo, gitCommits, gitTagVersion, gitCheckout, merge, changeAuthor} from '../helpers/git-utils';
const test = require('ava');
const getTags = require('../../lib/branches/get-tags');
const {gitRepo, gitCommits, gitTagVersion, gitCheckout, gitAddNote} = require('../helpers/git-utils');
test('Get the valid tags', async t => {
test('Get the valid tags', async (t) => {
const {cwd} = await gitRepo();
const commits = await gitCommits(['First'], {cwd});
await gitTagVersion('foo', undefined, {cwd});
@@ -20,30 +20,31 @@ test('Get the valid tags', async t => {
{
name: 'master',
tags: [
{gitTag: 'v1.0.0', version: '1.0.0', channel: undefined, gitHead: commits[1].hash},
{gitTag: 'v2.0.0', version: '2.0.0', channel: undefined, gitHead: commits[0].hash},
{gitTag: 'v3.0.0-beta.1', version: '3.0.0-beta.1', channel: undefined, gitHead: commits[3].hash},
{gitTag: 'v1.0.0', version: '1.0.0', channels: [null]},
{gitTag: 'v2.0.0', version: '2.0.0', channels: [null]},
{gitTag: 'v3.0.0-beta.1', version: '3.0.0-beta.1', channels: [null]},
],
},
]);
});
test('Get the valid tags from multiple branches', async t => {
test('Get the valid tags from multiple branches', async (t) => {
const {cwd} = await gitRepo();
const commits = await gitCommits(['First'], {cwd});
await gitCommits(['First'], {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
await gitTagVersion('v1.0.0@1.x', undefined, {cwd});
commits.push(...(await gitCommits(['Second'], {cwd})));
await gitAddNote(JSON.stringify({channels: [null, '1.x']}), 'v1.0.0', {cwd});
await gitCommits(['Second'], {cwd});
await gitTagVersion('v1.1.0', undefined, {cwd});
await gitTagVersion('v1.1.0@1.x', undefined, {cwd});
await gitAddNote(JSON.stringify({channels: [null, '1.x']}), 'v1.1.0', {cwd});
await gitCheckout('1.x', true, {cwd});
await gitCheckout('master', false, {cwd});
commits.push(...(await gitCommits(['Third'], {cwd})));
await gitCommits(['Third'], {cwd});
await gitTagVersion('v2.0.0', undefined, {cwd});
await gitTagVersion('v2.0.0@next', undefined, {cwd});
await gitAddNote(JSON.stringify({channels: [null, 'next']}), 'v2.0.0', {cwd});
await gitCheckout('next', true, {cwd});
commits.push(...(await gitCommits(['Fourth'], {cwd})));
await gitTagVersion('v3.0.0@next', undefined, {cwd});
await gitCommits(['Fourth'], {cwd});
await gitTagVersion('v3.0.0', undefined, {cwd});
await gitAddNote(JSON.stringify({channels: ['next']}), 'v3.0.0', {cwd});
const result = await getTags({cwd, options: {tagFormat: `v\${version}`}}, [
{name: '1.x'},
@@ -55,52 +56,22 @@ test('Get the valid tags from multiple branches', async t => {
{
name: '1.x',
tags: [
{gitTag: 'v1.0.0', version: '1.0.0', channel: undefined, gitHead: commits[0].hash},
{gitTag: 'v1.0.0@1.x', version: '1.0.0', channel: '1.x', gitHead: commits[0].hash},
{gitTag: 'v1.1.0', version: '1.1.0', channel: undefined, gitHead: commits[1].hash},
{gitTag: 'v1.1.0@1.x', version: '1.1.0', channel: '1.x', gitHead: commits[1].hash},
{gitTag: 'v1.0.0', version: '1.0.0', channels: [null, '1.x']},
{gitTag: 'v1.1.0', version: '1.1.0', channels: [null, '1.x']},
],
},
{
name: 'master',
tags: [
...result[0].tags,
{gitTag: 'v2.0.0', version: '2.0.0', channel: undefined, gitHead: commits[2].hash},
{gitTag: 'v2.0.0@next', version: '2.0.0', channel: 'next', gitHead: commits[2].hash},
],
tags: [...result[0].tags, {gitTag: 'v2.0.0', version: '2.0.0', channels: [null, 'next']}],
},
{
name: 'next',
tags: [...result[1].tags, {gitTag: 'v3.0.0@next', version: '3.0.0', channel: 'next', gitHead: commits[3].hash}],
tags: [...result[1].tags, {gitTag: 'v3.0.0', version: '3.0.0', channels: ['next']}],
},
]);
});
test('Match the tag name from the begining of the string and the channel from the last "@"', async t => {
const {cwd} = await gitRepo();
const commits = await gitCommits(['First'], {cwd});
await gitTagVersion('prefix@v1.0.0', undefined, {cwd});
await gitTagVersion('prefix@v1.0.0@next', undefined, {cwd});
await gitTagVersion('prefix@v2.0.0', undefined, {cwd});
await gitTagVersion('prefix@v2.0.0@next', undefined, {cwd});
await gitTagVersion('other-prefix@v3.0.0', undefined, {cwd});
const result = await getTags({cwd, options: {tagFormat: `prefix@v\${version}`}}, [{name: 'master'}]);
t.deepEqual(result, [
{
name: 'master',
tags: [
{gitTag: 'prefix@v1.0.0', version: '1.0.0', channel: undefined, gitHead: commits[0].hash},
{gitTag: 'prefix@v1.0.0@next', version: '1.0.0', channel: 'next', gitHead: commits[0].hash},
{gitTag: 'prefix@v2.0.0', version: '2.0.0', channel: undefined, gitHead: commits[0].hash},
{gitTag: 'prefix@v2.0.0@next', version: '2.0.0', channel: 'next', gitHead: commits[0].hash},
],
},
]);
});
test('Return branches with and empty tags array if no valid tag is found', async t => {
test('Return branches with and empty tags array if no valid tag is found', async (t) => {
const {cwd} = await gitRepo();
await gitCommits(['First'], {cwd});
await gitTagVersion('foo', undefined, {cwd});
@@ -109,48 +80,53 @@ test('Return branches with and empty tags array if no valid tag is found', async
await gitCommits(['Third'], {cwd});
await gitTagVersion('v3.0', undefined, {cwd});
const result = await getTags({cwd, options: {tagFormat: `prefix@v\${version}`}}, [{name: 'master'}, {name: 'next'}]);
const result = await getTags({cwd, options: {tagFormat: `prefix@v\${version}`}}, [{name: 'master'}]);
t.deepEqual(result, [{name: 'master', tags: []}, {name: 'next', tags: []}]);
t.deepEqual(result, [{name: 'master', tags: []}]);
});
test('Return branches with and empty tags array if no valid tag is found in history of configured branches', async t => {
test('Return branches with and empty tags array if no valid tag is found in history of configured branches', async (t) => {
const {cwd} = await gitRepo();
await gitCommits(['First'], {cwd});
await gitCheckout('other-branch', true, {cwd});
await gitCheckout('next', true, {cwd});
await gitCommits(['Second'], {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
await gitTagVersion('v1.0.0@next', undefined, {cwd});
await gitAddNote(JSON.stringify({channels: [null, 'next']}), 'v1.0.0', {cwd});
await gitCommits(['Third'], {cwd});
await gitTagVersion('v2.0.0', undefined, {cwd});
await gitTagVersion('v2.0.0@next', undefined, {cwd});
await gitAddNote(JSON.stringify({channels: [null, 'next']}), 'v2.0.0', {cwd});
await gitCommits(['Fourth'], {cwd});
await gitTagVersion('v3.0.0', undefined, {cwd});
await gitTagVersion('v3.0.0@next', undefined, {cwd});
await gitAddNote(JSON.stringify({channels: [null, 'next']}), 'v3.0.0', {cwd});
await gitCheckout('master', false, {cwd});
const result = await getTags({cwd, options: {tagFormat: `prefix@v\${version}`}}, [{name: 'master'}, {name: 'next'}]);
t.deepEqual(result, [{name: 'master', tags: []}, {name: 'next', tags: []}]);
t.deepEqual(result, [
{name: 'master', tags: []},
{name: 'next', tags: []},
]);
});
test('Get the highest valid tag corresponding to the "tagFormat"', async t => {
test('Get the highest valid tag corresponding to the "tagFormat"', async (t) => {
const {cwd} = await gitRepo();
const commits = await gitCommits(['First'], {cwd});
await gitCommits(['First'], {cwd});
await gitTagVersion('1.0.0', undefined, {cwd});
t.deepEqual(await getTags({cwd, options: {tagFormat: `\${version}`}}, [{name: 'master'}]), [
{name: 'master', tags: [{gitTag: '1.0.0', version: '1.0.0', channel: undefined, gitHead: commits[0].hash}]},
{name: 'master', tags: [{gitTag: '1.0.0', version: '1.0.0', channels: [null]}]},
]);
await gitTagVersion('foo-1.0.0-bar', undefined, {cwd});
t.deepEqual(await getTags({cwd, options: {tagFormat: `foo-\${version}-bar`}}, [{name: 'master'}]), [
{name: 'master', tags: [{gitTag: 'foo-1.0.0-bar', version: '1.0.0', channel: undefined, gitHead: commits[0].hash}]},
{name: 'master', tags: [{gitTag: 'foo-1.0.0-bar', version: '1.0.0', channels: [null]}]},
]);
await gitTagVersion('foo-v1.0.0-bar', undefined, {cwd});
t.deepEqual(await getTags({cwd, options: {tagFormat: `foo-v\${version}-bar`}}, [{name: 'master'}]), [
{
name: 'master',
tags: [{gitTag: 'foo-v1.0.0-bar', version: '1.0.0', channel: undefined, gitHead: commits[0].hash}],
tags: [{gitTag: 'foo-v1.0.0-bar', version: '1.0.0', channels: [null]}],
},
]);
@@ -158,7 +134,7 @@ test('Get the highest valid tag corresponding to the "tagFormat"', async t => {
t.deepEqual(await getTags({cwd, options: {tagFormat: `(.+)/\${version}/(a-z)`}}, [{name: 'master'}]), [
{
name: 'master',
tags: [{gitTag: '(.+)/1.0.0/(a-z)', version: '1.0.0', channel: undefined, gitHead: commits[0].hash}],
tags: [{gitTag: '(.+)/1.0.0/(a-z)', version: '1.0.0', channels: [null]}],
},
]);
@@ -166,37 +142,12 @@ test('Get the highest valid tag corresponding to the "tagFormat"', async t => {
t.deepEqual(await getTags({cwd, options: {tagFormat: `2.0.0-\${version}-bar.1`}}, [{name: 'master'}]), [
{
name: 'master',
tags: [{gitTag: '2.0.0-1.0.0-bar.1', version: '1.0.0', channel: undefined, gitHead: commits[0].hash}],
tags: [{gitTag: '2.0.0-1.0.0-bar.1', version: '1.0.0', channels: [null]}],
},
]);
await gitTagVersion('3.0.0-bar.2', undefined, {cwd});
t.deepEqual(await getTags({cwd, options: {tagFormat: `\${version}-bar.2`}}, [{name: 'master'}]), [
{name: 'master', tags: [{gitTag: '3.0.0-bar.2', version: '3.0.0', channel: undefined, gitHead: commits[0].hash}]},
]);
});
test('Get the tag on branch where commits have been rebased', async t => {
const {cwd} = await gitRepo();
const commits = await gitCommits(['First'], {cwd});
await gitCheckout('next', true, {cwd});
commits.push(...(await gitCommits(['Second/n/n/commit body'], {cwd})));
await gitTagVersion('v1.0.0@next', undefined, {cwd});
await gitCheckout('master', false, {cwd});
await merge('next', {cwd});
// Simulate GitHub "Rebase and Merge" by changing the committer info, which will result in a new commit sha and losing the tag
await changeAuthor(commits[1].hash, {cwd});
const result = await getTags({cwd, options: {tagFormat: `v\${version}`}}, [{name: 'master'}, {name: 'next'}]);
t.deepEqual(result, [
{
name: 'master',
tags: [{gitTag: 'v1.0.0@next', version: '1.0.0', channel: 'next', gitHead: commits[1].hash}],
},
{
name: 'next',
tags: [{gitTag: 'v1.0.0@next', version: '1.0.0', channel: 'next', gitHead: commits[1].hash}],
},
{name: 'master', tags: [{gitTag: '3.0.0-bar.2', version: '3.0.0', channels: [null]}]},
]);
});
+141 -70
View File
@@ -1,10 +1,14 @@
import test from 'ava';
import normalize from '../../lib/branches/normalize';
const test = require('ava');
const normalize = require('../../lib/branches/normalize');
const toTags = versions => versions.map(version => ({version}));
const toTags = (versions) => versions.map((version) => ({version}));
test('Maintenance branches - initial state', t => {
const maintenance = [{name: '1.x', channel: '1.x', tags: []}, {name: '1.1.x', tags: []}, {name: '1.2.x', tags: []}];
test('Maintenance branches - initial state', (t) => {
const maintenance = [
{name: '1.x', channel: '1.x', tags: []},
{name: '1.1.x', tags: []},
{name: '1.2.x', tags: []},
];
const release = [{name: 'master', tags: []}];
t.deepEqual(
normalize.maintenance({maintenance, release}).map(({type, name, range, accept, channel, mergeRange}) => ({
@@ -44,7 +48,7 @@ test('Maintenance branches - initial state', t => {
);
});
test('Maintenance branches - cap range to first release present on default branch and not in any Maintenance one', t => {
test('Maintenance branches - cap range to first release present on default branch and not in any Maintenance one', (t) => {
const maintenance = [
{name: '1.x', tags: toTags(['1.0.0', '1.1.0', '1.1.1', '1.2.0', '1.2.1', '1.3.0', '1.4.0', '1.5.0'])},
{name: 'name', range: '1.1.x', tags: toTags(['1.0.0', '1.0.1', '1.1.0', '1.1.1'])},
@@ -106,7 +110,7 @@ test('Maintenance branches - cap range to first release present on default branc
);
});
test('Maintenance branches - cap range to default branch last release if all release are also present on maintenance branch', t => {
test('Maintenance branches - cap range to default branch last release if all release are also present on maintenance branch', (t) => {
const maintenance = [
{name: '1.x', tags: toTags(['1.0.0', '1.2.0', '1.3.0'])},
{name: '2.x.x', tags: toTags(['1.0.0', '1.2.0', '1.3.0', '2.0.0'])},
@@ -143,7 +147,7 @@ test('Maintenance branches - cap range to default branch last release if all rel
);
});
test('Release branches - initial state', t => {
test('Release branches - initial state', (t) => {
const release = [
{name: 'master', tags: []},
{name: 'next', channel: 'next', tags: []},
@@ -151,22 +155,39 @@ test('Release branches - initial state', t => {
];
t.deepEqual(
normalize.release({release}).map(({type, name, range, accept, channel}) => ({type, name, range, accept, channel})),
normalize
.release({release})
.map(({type, name, range, accept, channel, main}) => ({type, name, range, accept, channel, main})),
[
{type: 'release', name: 'master', range: '>=1.0.0 <1.1.0', accept: ['patch'], channel: undefined},
{type: 'release', name: 'next', range: '>=1.1.0 <2.0.0', accept: ['patch', 'minor'], channel: 'next'},
{
type: 'release',
name: 'master',
range: '>=1.0.0',
accept: ['patch', 'minor', 'major'],
channel: undefined,
main: true,
},
{
type: 'release',
name: 'next',
range: '>=1.0.0',
accept: ['patch', 'minor', 'major'],
channel: 'next',
main: false,
},
{
type: 'release',
name: 'next-major',
range: '>=2.0.0',
range: '>=1.0.0',
accept: ['patch', 'minor', 'major'],
channel: 'next-major',
main: false,
},
]
);
});
test('Release branches - 3 release branches', t => {
test('Release branches - 3 release branches', (t) => {
const release = [
{name: 'master', tags: toTags(['1.0.0', '1.0.1', '1.0.2'])},
{name: 'next', tags: toTags(['1.0.0', '1.0.1', '1.0.2', '1.1.0', '1.2.0'])},
@@ -174,37 +195,63 @@ test('Release branches - 3 release branches', t => {
];
t.deepEqual(
normalize.release({release}).map(({type, name, range, accept, channel}) => ({type, name, range, accept, channel})),
normalize
.release({release})
.map(({type, name, range, accept, channel, main}) => ({type, name, range, accept, channel, main})),
[
{type: 'release', name: 'master', range: '>=1.0.2 <1.1.0', accept: ['patch'], channel: undefined},
{type: 'release', name: 'next', range: '>=1.2.0 <2.0.0', accept: ['patch', 'minor'], channel: 'next'},
{type: 'release', name: 'master', range: '>=1.0.2 <1.1.0', accept: ['patch'], channel: undefined, main: true},
{
type: 'release',
name: 'next',
range: '>=1.2.0 <2.0.0',
accept: ['patch', 'minor'],
channel: 'next',
main: false,
},
{
type: 'release',
name: 'next-major',
range: '>=2.1.0',
accept: ['patch', 'minor', 'major'],
channel: 'next-major',
main: false,
},
]
);
});
test('Release branches - 2 release branches', t => {
test('Release branches - 2 release branches', (t) => {
const release = [
{name: 'master', tags: toTags(['1.0.0', '1.0.1', '1.1.0', '1.1.1', '1.2.0'])},
{name: 'next', tags: toTags(['1.0.0', '1.0.1', '1.1.0', '1.1.1', '1.2.0', '2.0.0', '2.0.1', '2.1.0'])},
];
t.deepEqual(
normalize.release({release}).map(({type, name, range, accept, channel}) => ({type, name, range, accept, channel})),
normalize
.release({release})
.map(({type, name, range, accept, channel, main}) => ({type, name, range, accept, channel, main})),
[
{type: 'release', name: 'master', range: '>=1.2.0 <2.0.0', accept: ['patch', 'minor'], channel: undefined},
{type: 'release', name: 'next', range: '>=2.1.0', accept: ['patch', 'minor', 'major'], channel: 'next'},
{
type: 'release',
name: 'master',
range: '>=1.2.0 <2.0.0',
accept: ['patch', 'minor'],
channel: undefined,
main: true,
},
{
type: 'release',
name: 'next',
range: '>=2.1.0',
accept: ['patch', 'minor', 'major'],
channel: 'next',
main: false,
},
]
);
});
test('Release branches - 1 release branches', t => {
test('Release branches - 1 release branches', (t) => {
const release = [{name: 'master', tags: toTags(['1.0.0', '1.1.0', '1.1.1', '1.2.0'])}];
t.deepEqual(
@@ -213,7 +260,7 @@ test('Release branches - 1 release branches', t => {
);
});
test('Release branches - cap ranges to first release only present on following branch', t => {
test('Release branches - cap ranges to first release only present on following branch', (t) => {
const release = [
{name: 'master', tags: toTags(['1.0.0', '1.1.0', '1.2.0', '2.0.0'])},
{name: 'next', tags: toTags(['1.0.0', '1.1.0', '1.2.0', '2.0.0', '2.1.0'])},
@@ -221,60 +268,56 @@ test('Release branches - cap ranges to first release only present on following b
];
t.deepEqual(
normalize.release({release}).map(({type, name, range, accept, channel}) => ({type, name, range, accept, channel})),
normalize
.release({release})
.map(({type, name, range, accept, channel, main}) => ({type, name, range, accept, channel, main})),
[
{type: 'release', name: 'master', range: '>=2.0.0 <2.1.0', accept: ['patch'], channel: undefined},
{type: 'release', name: 'next', range: '>=2.1.0 <2.2.0', accept: ['patch'], channel: 'next'},
{type: 'release', name: 'master', range: '>=2.0.0 <2.1.0', accept: ['patch'], channel: undefined, main: true},
{type: 'release', name: 'next', range: '>=2.1.0 <2.2.0', accept: ['patch'], channel: 'next', main: false},
{
type: 'release',
name: 'next-major',
range: '>=2.2.0',
accept: ['patch', 'minor', 'major'],
channel: 'next-major',
main: false,
},
]
);
});
test('Release branches - Handle missing previous tags in branch history', t => {
test('Release branches - Handle missing previous tags in branch history', (t) => {
const release = [
{name: 'master', tags: toTags(['1.0.0', '2.0.0'])},
{name: 'next', tags: toTags(['1.0.0', '1.1.0', '1.1.1', '1.2.0', '2.0.0'])},
];
t.deepEqual(
normalize.release({release}).map(({type, name, range, accept, channel}) => ({type, name, range, accept, channel})),
normalize
.release({release})
.map(({type, name, range, accept, channel, main}) => ({type, name, range, accept, channel, main})),
[
{type: 'release', name: 'master', range: '>=2.0.0 <3.0.0', accept: ['patch', 'minor'], channel: undefined},
{type: 'release', name: 'next', range: '>=3.0.0', accept: ['patch', 'minor', 'major'], channel: 'next'},
]
);
});
test('Release branches - enforce release gaps after downstream merge', t => {
const release = [
{name: 'master', tags: toTags(['1.0.0', '1.1.0', '2.0.0'])},
{name: 'next', tags: toTags(['1.0.0', '1.1.0', '2.0.0'])},
{name: 'next-major', tags: toTags(['1.0.0', '1.1.0', '2.0.0'])},
];
t.deepEqual(
normalize.release({release}).map(({type, name, range, accept, channel}) => ({type, name, range, accept, channel})),
[
{type: 'release', name: 'master', range: '>=2.0.0 <2.1.0', accept: ['patch'], channel: undefined},
{type: 'release', name: 'next', range: '>=2.1.0 <3.0.0', accept: ['patch', 'minor'], channel: 'next'},
{
type: 'release',
name: 'next-major',
range: '>=3.0.0',
name: 'master',
range: '>=2.0.0',
accept: ['patch', 'minor', 'major'],
channel: 'next-major',
channel: undefined,
main: true,
},
{
type: 'release',
name: 'next',
range: '>=2.0.0',
accept: ['patch', 'minor', 'major'],
channel: 'next',
main: false,
},
]
);
});
test('Release branches - limit releases on 2nd and 3rd branche based on 1st branch last release', t => {
test('Release branches - limit releases on 2nd and 3rd branch based on 1st branch last release', (t) => {
const release = [
{name: 'master', tags: toTags(['1.0.0', '1.1.0', '2.0.0', '3.0.0'])},
{name: 'next', tags: toTags(['1.0.0', '1.1.0'])},
@@ -282,45 +325,73 @@ test('Release branches - limit releases on 2nd and 3rd branche based on 1st bran
];
t.deepEqual(
normalize.release({release}).map(({type, name, range, accept, channel}) => ({type, name, range, accept, channel})),
normalize
.release({release})
.map(({type, name, range, accept, channel, main}) => ({type, name, range, accept, channel, main})),
[
{type: 'release', name: 'master', range: '>=3.0.0 <3.1.0', accept: ['patch'], channel: undefined},
{type: 'release', name: 'next', range: '>=3.1.0 <4.0.0', accept: ['patch', 'minor'], channel: 'next'},
{
type: 'release',
name: 'master',
range: '>=3.0.0',
accept: ['patch', 'minor', 'major'],
channel: undefined,
main: true,
},
{
type: 'release',
name: 'next',
range: '>=3.0.0',
accept: ['patch', 'minor', 'major'],
channel: 'next',
main: false,
},
{
type: 'release',
name: 'next-major',
range: '>=4.0.0',
range: '>=3.0.0',
accept: ['patch', 'minor', 'major'],
channel: 'next-major',
main: false,
},
]
);
});
test('Prerelease branches', t => {
test('Prerelease branches', (t) => {
const prerelease = [
{name: 'beta', channel: 'beta', prerelease: true, tags: []},
{name: 'alpha', prerelease: 'preview', tags: []},
];
t.deepEqual(normalize.prerelease({prerelease}).map(({type, name, channel}) => ({type, name, channel})), [
{type: 'prerelease', name: 'beta', channel: 'beta'},
{type: 'prerelease', name: 'alpha', channel: 'alpha'},
]);
t.deepEqual(
normalize.prerelease({prerelease}).map(({type, name, channel}) => ({type, name, channel})),
[
{type: 'prerelease', name: 'beta', channel: 'beta'},
{type: 'prerelease', name: 'alpha', channel: 'alpha'},
]
);
});
test('Allow to set channel to "false" to prevent default', t => {
test('Allow to set channel to "false" to prevent default', (t) => {
const maintenance = [{name: '1.x', channel: false, tags: []}];
const release = [{name: 'master', channel: false, tags: []}, {name: 'next', channel: false, tags: []}];
const release = [
{name: 'master', channel: false, tags: []},
{name: 'next', channel: false, tags: []},
];
const prerelease = [{name: 'beta', channel: false, prerelease: true, tags: []}];
t.deepEqual(normalize.maintenance({maintenance, release}).map(({name, channel}) => ({name, channel})), [
{name: '1.x', channel: false},
]);
t.deepEqual(normalize.release({release}).map(({name, channel}) => ({name, channel})), [
{name: 'master', channel: false},
{name: 'next', channel: false},
]);
t.deepEqual(normalize.prerelease({prerelease}).map(({name, channel}) => ({name, channel})), [
{name: 'beta', channel: false},
]);
t.deepEqual(
normalize.maintenance({maintenance, release}).map(({name, channel}) => ({name, channel})),
[{name: '1.x', channel: false}]
);
t.deepEqual(
normalize.release({release}).map(({name, channel}) => ({name, channel})),
[
{name: 'master', channel: false},
{name: 'next', channel: false},
]
);
t.deepEqual(
normalize.prerelease({prerelease}).map(({name, channel}) => ({name, channel})),
[{name: 'beta', channel: false}]
);
});
+31 -33
View File
@@ -1,28 +1,26 @@
import test from 'ava';
import {escapeRegExp} from 'lodash';
import proxyquire from 'proxyquire';
import {stub} from 'sinon';
import {SECRET_REPLACEMENT} from '../lib/definitions/constants';
const test = require('ava');
const {escapeRegExp} = require('lodash');
const proxyquire = require('proxyquire').noPreserveCache();
const {stub} = require('sinon');
const {SECRET_REPLACEMENT} = require('../lib/definitions/constants');
const requireNoCache = proxyquire.noPreserveCache();
test.beforeEach(t => {
test.beforeEach((t) => {
t.context.logs = '';
t.context.errors = '';
t.context.stdout = stub(process.stdout, 'write').callsFake(val => {
t.context.logs += val.toString();
t.context.stdout = stub(process.stdout, 'write').callsFake((value) => {
t.context.logs += value.toString();
});
t.context.stderr = stub(process.stderr, 'write').callsFake(val => {
t.context.errors += val.toString();
t.context.stderr = stub(process.stderr, 'write').callsFake((value) => {
t.context.errors += value.toString();
});
});
test.afterEach.always(t => {
test.afterEach.always((t) => {
t.context.stdout.restore();
t.context.stderr.restore();
});
test.serial('Pass options to semantic-release API', async t => {
test.serial('Pass options to semantic-release API', async (t) => {
const run = stub().resolves(true);
const argv = [
'',
@@ -65,7 +63,7 @@ test.serial('Pass options to semantic-release API', async t => {
'--debug',
'-d',
];
const cli = requireNoCache('../cli', {'.': run, process: {...process, argv}});
const cli = proxyquire('../cli', {'.': run, process: {...process, argv}});
const exitCode = await cli();
@@ -88,7 +86,7 @@ test.serial('Pass options to semantic-release API', async t => {
t.is(exitCode, 0);
});
test.serial('Pass options to semantic-release API with alias arguments', async t => {
test.serial('Pass options to semantic-release API with alias arguments', async (t) => {
const run = stub().resolves(true);
const argv = [
'',
@@ -107,7 +105,7 @@ test.serial('Pass options to semantic-release API with alias arguments', async t
'config2',
'--dry-run',
];
const cli = requireNoCache('../cli', {'.': run, process: {...process, argv}});
const cli = proxyquire('../cli', {'.': run, process: {...process, argv}});
const exitCode = await cli();
@@ -121,10 +119,10 @@ test.serial('Pass options to semantic-release API with alias arguments', async t
t.is(exitCode, 0);
});
test.serial('Pass unknown options to semantic-release API', async t => {
test.serial('Pass unknown options to semantic-release API', async (t) => {
const run = stub().resolves(true);
const argv = ['', '', '--bool', '--first-option', 'value1', '--second-option', 'value2', '--second-option', 'value3'];
const cli = requireNoCache('../cli', {'.': run, process: {...process, argv}});
const cli = proxyquire('../cli', {'.': run, process: {...process, argv}});
const exitCode = await cli();
@@ -135,10 +133,10 @@ test.serial('Pass unknown options to semantic-release API', async t => {
t.is(exitCode, 0);
});
test.serial('Pass empty Array to semantic-release API for list option set to "false"', async t => {
test.serial('Pass empty Array to semantic-release API for list option set to "false"', async (t) => {
const run = stub().resolves(true);
const argv = ['', '', '--publish', 'false'];
const cli = requireNoCache('../cli', {'.': run, process: {...process, argv}});
const cli = proxyquire('../cli', {'.': run, process: {...process, argv}});
const exitCode = await cli();
@@ -147,10 +145,10 @@ test.serial('Pass empty Array to semantic-release API for list option set to "fa
t.is(exitCode, 0);
});
test.serial('Do not set properties in option for which arg is not in command line', async t => {
test.serial('Do not set properties in option for which arg is not in command line', async (t) => {
const run = stub().resolves(true);
const argv = ['', '', '-b', 'master'];
const cli = requireNoCache('../cli', {'.': run, process: {...process, argv}});
const cli = proxyquire('../cli', {'.': run, process: {...process, argv}});
await cli();
@@ -164,10 +162,10 @@ test.serial('Do not set properties in option for which arg is not in command lin
t.false('e' in run.args[0][0]);
});
test.serial('Display help', async t => {
test.serial('Display help', async (t) => {
const run = stub().resolves(true);
const argv = ['', '', '--help'];
const cli = requireNoCache('../cli', {'.': run, process: {...process, argv}});
const cli = proxyquire('../cli', {'.': run, process: {...process, argv}});
const exitCode = await cli();
@@ -175,10 +173,10 @@ test.serial('Display help', async t => {
t.is(exitCode, 0);
});
test.serial('Return error code and prints help if called with a command', async t => {
test.serial('Return error exitCode and prints help if called with a command', async (t) => {
const run = stub().resolves(true);
const argv = ['', '', 'pre'];
const cli = requireNoCache('../cli', {'.': run, process: {...process, argv}});
const cli = proxyquire('../cli', {'.': run, process: {...process, argv}});
const exitCode = await cli();
@@ -187,10 +185,10 @@ test.serial('Return error code and prints help if called with a command', async
t.is(exitCode, 1);
});
test.serial('Return error code if multiple plugin are set for single plugin', async t => {
test.serial('Return error exitCode if multiple plugin are set for single plugin', async (t) => {
const run = stub().resolves(true);
const argv = ['', '', '--analyze-commits', 'analyze1', 'analyze2'];
const cli = requireNoCache('../cli', {'.': run, process: {...process, argv}});
const cli = proxyquire('../cli', {'.': run, process: {...process, argv}});
const exitCode = await cli();
@@ -199,10 +197,10 @@ test.serial('Return error code if multiple plugin are set for single plugin', as
t.is(exitCode, 1);
});
test.serial('Return error code if semantic-release throw error', async t => {
test.serial('Return error exitCode if semantic-release throw error', async (t) => {
const run = stub().rejects(new Error('semantic-release error'));
const argv = ['', ''];
const cli = requireNoCache('../cli', {'.': run, process: {...process, argv}});
const cli = proxyquire('../cli', {'.': run, process: {...process, argv}});
const exitCode = await cli();
@@ -210,11 +208,11 @@ test.serial('Return error code if semantic-release throw error', async t => {
t.is(exitCode, 1);
});
test.serial('Hide sensitive environment variable values from the logs', async t => {
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 cli = proxyquire('../cli', {'.': run, process: {...process, argv, env: {...process.env, ...env}}});
const exitCode = await cli();
+16 -16
View File
@@ -1,22 +1,23 @@
import test from 'ava';
import {maintenance, prerelease, release} from '../../lib/definitions/branches';
const test = require('ava');
const {maintenance, prerelease, release} = require('../../lib/definitions/branches');
test('A "maintenance" branch is identified by having a "range" property or a "name" formatted like "N.x", "N.x.x" or "N.N.x"', t => {
test('A "maintenance" branch is identified by having a "range" property or a "name" formatted like "N.x", "N.x.x" or "N.N.x"', (t) => {
t.true(maintenance.filter({name: '1.x.x'}));
t.true(maintenance.filter({name: '1.0.x'}));
t.true(maintenance.filter({name: '1.x'}));
t.true(maintenance.filter({name: 'some-name', range: '1.x.x'}));
t.true(maintenance.filter({name: 'some-name', range: '1.1.x'}));
t.true(maintenance.filter({name: 'some-name', range: ''}));
t.true(maintenance.filter({name: 'some-name', range: null}));
t.true(maintenance.filter({name: 'some-name', range: false}));
t.true(maintenance.filter({name: 'some-name', range: true}));
t.false(maintenance.filter({name: 'some-name', range: null}));
t.false(maintenance.filter({name: 'some-name', range: false}));
t.false(maintenance.filter({name: 'some-name'}));
t.false(maintenance.filter({name: '1.0.0'}));
t.false(maintenance.filter({name: 'x.x.x'}));
});
test('A "maintenance" branches must have a "range" property formatted like "N.x", "N.x.x" or "N.N.x"', t => {
test('A "maintenance" branches must have a "range" property formatted like "N.x", "N.x.x" or "N.N.x"', (t) => {
t.true(maintenance.branchValidator({name: 'some-name', range: '1.x.x'}));
t.true(maintenance.branchValidator({name: 'some-name', range: '1.1.x'}));
@@ -24,29 +25,28 @@ test('A "maintenance" branches must have a "range" property formatted like "N.x"
t.false(maintenance.branchValidator({name: 'some-name', range: '>=1.0.0 <2.0.0'}));
t.false(maintenance.branchValidator({name: 'some-name', range: '1.0.0'}));
t.false(maintenance.branchValidator({name: 'some-name', range: 'wrong-range'}));
t.false(maintenance.branchValidator({name: 'some-name', range: true}));
t.false(maintenance.branchValidator({name: 'some-name', range: ''}));
t.false(maintenance.branchValidator({name: 'some-name', range: null}));
t.false(maintenance.branchValidator({name: 'some-name', range: false}));
});
test('The "maintenance" branches must have unique ranges', t => {
test('The "maintenance" branches must have unique ranges', (t) => {
t.true(maintenance.branchesValidator([{range: '1.x.x'}, {range: '1.0.x'}]));
t.false(maintenance.branchesValidator([{range: '1.x.x'}, {range: '1.x.x'}]));
t.false(maintenance.branchesValidator([{range: '1.x.x'}, {range: '1.x'}]));
});
test('A "prerelease" branch is identified by having a range "prerelease" property', t => {
test('A "prerelease" branch is identified by having a thruthy "prerelease" property', (t) => {
t.true(prerelease.filter({name: 'some-name', prerelease: true}));
t.true(prerelease.filter({name: 'some-name', prerelease: 'beta'}));
t.true(prerelease.filter({name: 'some-name', prerelease: ''}));
t.true(prerelease.filter({name: 'some-name', prerelease: null}));
t.true(prerelease.filter({name: 'some-name', prerelease: false}));
t.false(prerelease.filter({name: 'some-name', prerelease: null}));
t.false(prerelease.filter({name: 'some-name', prerelease: false}));
t.false(prerelease.filter({name: 'some-name'}));
});
test('A "prerelease" branch must have a valid prerelease detonation in "prerelease" property or in "name" if "prerelease" is "true"', t => {
test('A "prerelease" branch must have a valid prerelease detonation in "prerelease" property or in "name" if "prerelease" is "true"', (t) => {
t.true(prerelease.branchValidator({name: 'beta', prerelease: true}));
t.true(prerelease.branchValidator({name: 'some-name', prerelease: 'beta'}));
@@ -59,13 +59,13 @@ test('A "prerelease" branch must have a valid prerelease detonation in "prerelea
t.false(prerelease.branchValidator({name: '#beta', prerelease: true}));
});
test('The "prerelease" branches must have unique "prerelease" property', t => {
test('The "prerelease" branches must have unique "prerelease" property', (t) => {
t.true(prerelease.branchesValidator([{prerelease: 'beta'}, {prerelease: 'alpha'}]));
t.false(prerelease.branchesValidator([{range: 'beta'}, {range: 'beta'}, {range: 'alpha'}]));
});
test('A "release" branch is identified by not havin a "range" or "prerelease" property or a "name" formatted like "N.x", "N.x.x" or "N.N.x"', t => {
test('A "release" branch is identified by not havin a "range" or "prerelease" property or a "name" formatted like "N.x", "N.x.x" or "N.N.x"', (t) => {
t.true(release.filter({name: 'some-name'}));
t.false(release.filter({name: '1.x.x'}));
@@ -76,7 +76,7 @@ test('A "release" branch is identified by not havin a "range" or "prerelease" pr
t.false(release.filter({name: 'some-name', prerelease: 'beta'}));
});
test('There must be between 1 and 3 release branches', t => {
test('There must be between 1 and 3 release branches', (t) => {
t.true(release.branchesValidator([{name: 'branch1'}]));
t.true(release.branchesValidator([{name: 'branch1'}, {name: 'branch2'}]));
t.true(release.branchesValidator([{name: 'branch1'}, {name: 'branch2'}, {name: 'branch3'}]));
+9 -9
View File
@@ -1,8 +1,8 @@
import test from 'ava';
import plugins from '../../lib/definitions/plugins';
import {RELEASE_NOTES_SEPARATOR, SECRET_REPLACEMENT} from '../../lib/definitions/constants';
const test = require('ava');
const plugins = require('../../lib/definitions/plugins');
const {RELEASE_NOTES_SEPARATOR, SECRET_REPLACEMENT} = require('../../lib/definitions/constants');
test('The "analyzeCommits" plugin output must be either undefined or a valid semver release type', t => {
test('The "analyzeCommits" plugin output must be either undefined or a valid semver release type', (t) => {
t.false(plugins.analyzeCommits.outputValidator('invalid'));
t.false(plugins.analyzeCommits.outputValidator(1));
t.false(plugins.analyzeCommits.outputValidator({}));
@@ -12,7 +12,7 @@ test('The "analyzeCommits" plugin output must be either undefined or a valid sem
t.true(plugins.analyzeCommits.outputValidator('major'));
});
test('The "generateNotes" plugin output, if defined, must be a string', t => {
test('The "generateNotes" plugin output, if defined, must be a string', (t) => {
t.false(plugins.generateNotes.outputValidator(1));
t.false(plugins.generateNotes.outputValidator({}));
@@ -22,7 +22,7 @@ test('The "generateNotes" plugin output, if defined, must be a string', t => {
t.true(plugins.generateNotes.outputValidator('string'));
});
test('The "publish" plugin output, if defined, must be an object or "false"', t => {
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'));
@@ -33,7 +33,7 @@ test('The "publish" plugin output, if defined, must be an object or "false"', t
t.true(plugins.publish.outputValidator(false));
});
test('The "addChannel" plugin output, if defined, must be an object', t => {
test('The "addChannel" plugin output, if defined, must be an object', (t) => {
t.false(plugins.addChannel.outputValidator(1));
t.false(plugins.addChannel.outputValidator('string'));
@@ -43,7 +43,7 @@ test('The "addChannel" plugin output, if defined, must be an object', t => {
t.true(plugins.addChannel.outputValidator(''));
});
test('The "generateNotes" plugins output are concatenated with separator and sensitive data is hidden', t => {
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');
@@ -63,7 +63,7 @@ test('The "generateNotes" plugins output are concatenated with separator and sen
);
});
test('The "analyzeCommits" plugins output are reduced to the highest release type', t => {
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');
+12 -12
View File
@@ -1,16 +1,16 @@
import test from 'ava';
import {stub} from 'sinon';
import getCommits from '../lib/get-commits';
import {gitRepo, gitCommits, gitDetachedHead} from './helpers/git-utils';
const test = require('ava');
const {stub} = require('sinon');
const getCommits = require('../lib/get-commits');
const {gitRepo, gitCommits, gitDetachedHead} = require('./helpers/git-utils');
test.beforeEach(t => {
test.beforeEach((t) => {
// Stub the logger functions
t.context.log = stub();
t.context.error = stub();
t.context.logger = {log: t.context.log, error: t.context.error};
});
test('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
const {cwd} = await gitRepo();
// Add commits to the master branch
@@ -24,7 +24,7 @@ test('Get all commits when there is no last release', async t => {
t.deepEqual(result, commits);
});
test('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
const {cwd} = await gitRepo();
// Add commits to the master branch
@@ -42,7 +42,7 @@ test('Get all commits since gitHead (from lastRelease)', async t => {
t.deepEqual(result, commits.slice(0, 2));
});
test('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
let {cwd, repositoryUrl} = await gitRepo();
// Add commits to the master branch
@@ -66,7 +66,7 @@ test('Get all commits since gitHead (from lastRelease) on a detached head repo',
t.truthy(result[0].committer.name);
});
test('Get all commits between lastRelease.gitHead and a shas', async t => {
test('Get all commits between lastRelease.gitHead and a shas', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
// Add commits to the master branch
@@ -82,10 +82,10 @@ test('Get all commits between lastRelease.gitHead and a shas', async t => {
// Verify the commits created and retrieved by the module are identical
t.is(result.length, 1);
t.deepEqual(result, commits.slice(1, commits.length - 1));
t.deepEqual(result, commits.slice(1, -1));
});
test('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
const {cwd} = await gitRepo();
// Add commits to the master branch
@@ -102,7 +102,7 @@ test('Return empty array if lastRelease.gitHead is the last commit', async t =>
t.deepEqual(result, []);
});
test('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
const {cwd} = await gitRepo();
+32 -32
View File
@@ -1,12 +1,12 @@
import path from 'path';
import {format} from 'util';
import test from 'ava';
import {writeFile, outputJson} from 'fs-extra';
import {omit} from 'lodash';
import proxyquire from 'proxyquire';
import {stub} from 'sinon';
import yaml from 'js-yaml';
import {gitRepo, gitTagVersion, gitCommits, gitShallowClone, gitAddConfig} from './helpers/git-utils';
const path = require('path');
const {format} = require('util');
const test = require('ava');
const {writeFile, outputJson} = require('fs-extra');
const {omit} = require('lodash');
const proxyquire = require('proxyquire');
const {stub} = require('sinon');
const yaml = require('js-yaml');
const {gitRepo, gitTagVersion, gitCommits, gitShallowClone, gitAddConfig} = require('./helpers/git-utils');
const DEFAULT_PLUGINS = [
'@semantic-release/commit-analyzer',
@@ -15,12 +15,12 @@ const DEFAULT_PLUGINS = [
'@semantic-release/github',
];
test.beforeEach(t => {
test.beforeEach((t) => {
t.context.plugins = stub().returns({});
t.context.getConfig = proxyquire('../lib/get-config', {'./plugins': t.context.plugins});
});
test('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
const {cwd} = await gitRepo(true);
@@ -47,7 +47,7 @@ test('Default values, reading repositoryUrl from package.json', async t => {
t.is(result.tagFormat, `v\${version}`);
});
test('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
const {cwd} = await gitRepo(true);
// Add remote.origin.url config
@@ -68,7 +68,7 @@ test('Default values, reading repositoryUrl from repo if not set in package.json
t.is(result.tagFormat, `v\${version}`);
});
test('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
const {cwd} = await gitRepo();
@@ -90,7 +90,7 @@ test('Default values, reading repositoryUrl (http url) from package.json if not
t.is(result.tagFormat, `v\${version}`);
});
test('Convert "ci" option to "noCi"', async t => {
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();
@@ -102,7 +102,7 @@ test('Convert "ci" option to "noCi"', async t => {
t.is(result.noCi, true);
});
test('Read options from package.json', async t => {
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 = {
@@ -125,7 +125,7 @@ test('Read options from package.json', async t => {
t.deepEqual(t.context.plugins.args[0][0], {options: expected, cwd});
});
test('Read options from .releaserc.yml', async t => {
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 = {
@@ -147,7 +147,7 @@ test('Read options from .releaserc.yml', async t => {
t.deepEqual(t.context.plugins.args[0][0], {options: expected, cwd});
});
test('Read options from .releaserc.json', async t => {
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 = {
@@ -169,7 +169,7 @@ test('Read options from .releaserc.json', async t => {
t.deepEqual(t.context.plugins.args[0][0], {options: expected, cwd});
});
test('Read options from .releaserc.js', async t => {
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 = {
@@ -191,7 +191,7 @@ test('Read options from .releaserc.js', async t => {
t.deepEqual(t.context.plugins.args[0][0], {options: expected, cwd});
});
test('Read options from release.config.js', async t => {
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 = {
@@ -213,7 +213,7 @@ test('Read options from release.config.js', async t => {
t.deepEqual(t.context.plugins.args[0][0], {options: expected, cwd});
});
test('Prioritise CLI/API parameters over file configuration and git repo', async t => {
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});
@@ -243,7 +243,7 @@ test('Prioritise CLI/API parameters over file configuration and git repo', async
t.deepEqual(t.context.plugins.args[0][0], {options: expected, cwd});
});
test('Read configuration from file path in "extends"', async t => {
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'};
@@ -274,7 +274,7 @@ test('Read configuration from file path in "extends"', async t => {
});
});
test('Read configuration from module path in "extends"', async t => {
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'};
@@ -303,7 +303,7 @@ test('Read configuration from module path in "extends"', async t => {
});
});
test('Read configuration from an array of paths in "extends"', async t => {
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']};
@@ -342,7 +342,7 @@ test('Read configuration from an array of paths in "extends"', async t => {
});
});
test('Prioritize configuration from config file over "extends"', async t => {
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 = {
@@ -378,7 +378,7 @@ test('Prioritize configuration from config file over "extends"', async t => {
});
});
test('Prioritize configuration from cli/API options over "extends"', async t => {
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 = {
@@ -421,7 +421,7 @@ test('Prioritize configuration from cli/API options over "extends"', async t =>
t.deepEqual(t.context.plugins.args[0][0], {options: expected, cwd});
});
test('Allow to unset properties defined in shareable config with "null"', async t => {
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 = {
@@ -466,7 +466,7 @@ test('Allow to unset properties defined in shareable config with "null"', async
});
});
test('Allow to unset properties defined in shareable config with "undefined"', async t => {
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 = {
@@ -502,7 +502,7 @@ test('Allow to unset properties defined in shareable config with "undefined"', a
});
});
test('Throw an Error if one of the shareable config cannot be found', async t => {
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
const {cwd} = await gitRepo();
const pkhOptions = {extends: ['./shareable1.json', 'non-existing-path']};
@@ -511,8 +511,8 @@ test('Throw an Error if one of the shareable config cannot be found', async t =>
await outputJson(path.resolve(cwd, 'package.json'), {release: pkhOptions});
await outputJson(path.resolve(cwd, 'shareable1.json'), options1);
const error = await t.throwsAsync(t.context.getConfig({cwd}), Error);
t.regex(error.message, /Cannot find module 'non-existing-path'/);
t.is(error.code, 'MODULE_NOT_FOUND');
await t.throwsAsync(t.context.getConfig({cwd}), {
message: /Cannot find module 'non-existing-path'/,
code: 'MODULE_NOT_FOUND',
});
});
+111 -31
View File
@@ -1,10 +1,10 @@
import test from 'ava';
import getAuthUrl from '../lib/get-git-auth-url';
import {gitRepo} from './helpers/git-utils';
const test = require('ava');
const getAuthUrl = require('../lib/get-git-auth-url');
const {gitRepo} = require('./helpers/git-utils');
const env = {GIT_ASKPASS: 'echo', GIT_TERMINAL_PROMPT: 0};
test('Return the same "git" formatted URL if "gitCredentials" is not defined', async t => {
test('Return the same "git" formatted URL if "gitCredentials" is not defined', async (t) => {
const {cwd} = await gitRepo();
t.is(
@@ -13,7 +13,7 @@ test('Return the same "git" formatted URL if "gitCredentials" is not defined', a
);
});
test('Return the same "https" formatted URL if "gitCredentials" is not defined', async t => {
test('Return the same "https" formatted URL if "gitCredentials" is not defined', async (t) => {
const {cwd} = await gitRepo();
t.is(
@@ -27,7 +27,7 @@ test('Return the same "https" formatted URL if "gitCredentials" is not defined',
);
});
test('Return the "https" formatted URL if "gitCredentials" is not defined and repositoryUrl is a "git+https" URL', async t => {
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(
@@ -41,7 +41,7 @@ test('Return the "https" formatted URL if "gitCredentials" is not defined and re
);
});
test('Do not add trailing ".git" if not present in the origian URL', async t => {
test('Do not add trailing ".git" if not present in the origian URL', async (t) => {
const {cwd} = await gitRepo();
t.is(
@@ -50,7 +50,7 @@ test('Do not add trailing ".git" if not present in the origian URL', async t =>
);
});
test('Handle "https" URL with group and subgroup', async t => {
test('Handle "https" URL with group and subgroup', async (t) => {
const {cwd} = await gitRepo();
t.is(
@@ -64,7 +64,7 @@ test('Handle "https" URL with group and subgroup', async t => {
);
});
test('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(
@@ -78,7 +78,7 @@ test('Handle "git" URL with group and subgroup', async t => {
);
});
test('Convert shorthand URL', async t => {
test('Convert shorthand URL', async (t) => {
const {cwd} = await gitRepo();
t.is(
@@ -86,13 +86,13 @@ test('Convert shorthand URL', async t => {
cwd,
env,
branch: {name: 'master'},
options: {repositoryUrl: 'semanitc-release/semanitc-release'},
options: {repositoryUrl: 'semantic-release/semantic-release'},
}),
'https://github.com/semanitc-release/semanitc-release.git'
'https://github.com/semantic-release/semantic-release.git'
);
});
test('Convert GitLab shorthand URL', async t => {
test('Convert GitLab shorthand URL', async (t) => {
const {cwd} = await gitRepo();
t.is(
@@ -100,13 +100,13 @@ test('Convert GitLab shorthand URL', async t => {
cwd,
env,
branch: {name: 'master'},
options: {repositoryUrl: 'gitlab:semanitc-release/semanitc-release'},
options: {repositoryUrl: 'gitlab:semantic-release/semantic-release'},
}),
'https://gitlab.com/semanitc-release/semanitc-release.git'
'https://gitlab.com/semantic-release/semantic-release.git'
);
});
test('Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git" URL', async t => {
test('Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git" URL', async (t) => {
const {cwd} = await gitRepo();
t.is(
@@ -120,7 +120,7 @@ test('Return the "https" formatted URL if "gitCredentials" is defined and reposi
);
});
test('Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git" URL without user', async t => {
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(
@@ -133,7 +133,33 @@ test('Return the "https" formatted URL if "gitCredentials" is defined and reposi
);
});
test('Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "https" URL', async t => {
test('Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git" URL without user and with a custom port', async (t) => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({
cwd,
env: {...env, GIT_CREDENTIALS: 'user:pass'},
options: {branch: 'master', repositoryUrl: 'host.null:6666:owner/repo.git'},
}),
'https://user:pass@host.null:6666/owner/repo.git'
);
});
test('Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git" URL without user and with a custom port followed by a slash', async (t) => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({
cwd,
env: {...env, GIT_CREDENTIALS: 'user:pass'},
options: {branch: 'master', repositoryUrl: 'host.null:6666:/owner/repo.git'},
}),
'https://user:pass@host.null:6666/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(
@@ -147,7 +173,7 @@ test('Return the "https" formatted URL if "gitCredentials" is defined and reposi
);
});
test('Return the "http" formatted URL if "gitCredentials" is defined and repositoryUrl is a "http" URL', async t => {
test('Return the "http" formatted URL if "gitCredentials" is defined and repositoryUrl is a "http" URL', async (t) => {
const {cwd} = await gitRepo();
t.is(
@@ -161,7 +187,20 @@ test('Return the "http" formatted URL if "gitCredentials" is defined and reposit
);
});
test('Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git+https" URL', async t => {
test('Return the "http" formatted URL if "gitCredentials" is defined and repositoryUrl is a "http" URL with custom port', async (t) => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({
cwd,
env: {...env, GIT_CREDENTIALS: 'user:pass'},
options: {branch: 'master', repositoryUrl: 'http://host.null:8080/owner/repo.git'},
}),
'http://user:pass@host.null:8080/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(
@@ -175,7 +214,7 @@ test('Return the "https" formatted URL if "gitCredentials" is defined and reposi
);
});
test('Return the "http" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git+http" URL', async t => {
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(
@@ -189,7 +228,20 @@ test('Return the "http" formatted URL if "gitCredentials" is defined and reposit
);
});
test('Return the "https" formatted URL if "gitCredentials" is defined with "GH_TOKEN"', async t => {
test('Return the "http" formatted URL if "gitCredentials" is defined and repositoryUrl is a "ssh" URL', async (t) => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({
cwd,
env: {...env, GIT_CREDENTIALS: 'user:pass'},
options: {branch: 'master', repositoryUrl: 'ssh://git@host.null:2222/owner/repo.git'},
}),
'https://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(
@@ -203,7 +255,7 @@ test('Return the "https" formatted URL if "gitCredentials" is defined with "GH_T
);
});
test('Return the "https" formatted URL if "gitCredentials" is defined with "GITHUB_TOKEN"', async t => {
test('Return the "https" formatted URL if "gitCredentials" is defined with "GITHUB_TOKEN"', async (t) => {
const {cwd} = await gitRepo();
t.is(
@@ -217,7 +269,7 @@ test('Return the "https" formatted URL if "gitCredentials" is defined with "GITH
);
});
test('Return the "https" formatted URL if "gitCredentials" is defined with "GL_TOKEN"', async t => {
test('Return the "https" formatted URL if "gitCredentials" is defined with "GL_TOKEN"', async (t) => {
const {cwd} = await gitRepo();
t.is(
@@ -231,7 +283,7 @@ test('Return the "https" formatted URL if "gitCredentials" is defined with "GL_T
);
});
test('Return the "https" formatted URL if "gitCredentials" is defined with "GITLAB_TOKEN"', async t => {
test('Return the "https" formatted URL if "gitCredentials" is defined with "GITLAB_TOKEN"', async (t) => {
const {cwd} = await gitRepo();
t.is(
@@ -245,7 +297,7 @@ test('Return the "https" formatted URL if "gitCredentials" is defined with "GITL
);
});
test('Return the "https" formatted URL if "gitCredentials" is defined with "BB_TOKEN"', async t => {
test('Return the "https" formatted URL if "gitCredentials" is defined with "BB_TOKEN"', async (t) => {
const {cwd} = await gitRepo();
t.is(
@@ -259,7 +311,7 @@ test('Return the "https" formatted URL if "gitCredentials" is defined with "BB_T
);
});
test('Return the "https" formatted URL if "gitCredentials" is defined with "BITBUCKET_TOKEN"', async t => {
test('Return the "https" formatted URL if "gitCredentials" is defined with "BITBUCKET_TOKEN"', async (t) => {
const {cwd} = await gitRepo();
t.is(
@@ -273,7 +325,35 @@ test('Return the "https" formatted URL if "gitCredentials" is defined with "BITB
);
});
test('Return the "https" formatted URL if "GITHUB_ACTION" is set', async t => {
test('Return the "https" formatted URL if "gitCredentials" is defined with "BB_TOKEN_BASIC_AUTH"', async (t) => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({
cwd,
env: {...env, BB_TOKEN_BASIC_AUTH: 'username:token'},
branch: {name: 'master'},
options: {repositoryUrl: 'git@host.null:owner/repo.git'},
}),
'https://username:token@host.null/owner/repo.git'
);
});
test('Return the "https" formatted URL if "gitCredentials" is defined with "BITBUCKET_TOKEN_BASIC_AUTH"', async (t) => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({
cwd,
env: {...env, BITBUCKET_TOKEN_BASIC_AUTH: 'username:token'},
branch: {name: 'master'},
options: {repositoryUrl: 'git@host.null:owner/repo.git'},
}),
'https://username:token@host.null/owner/repo.git'
);
});
test('Return the "https" formatted URL if "GITHUB_ACTION" is set', async (t) => {
const {cwd} = await gitRepo();
t.is(
@@ -286,7 +366,7 @@ test('Return the "https" formatted URL if "GITHUB_ACTION" is set', async t => {
);
});
test('Handle "https" URL with group and subgroup, with "GIT_CREDENTIALS"', async t => {
test('Handle "https" URL with group and subgroup, with "GIT_CREDENTIALS"', async (t) => {
const {cwd} = await gitRepo();
t.is(
@@ -300,7 +380,7 @@ test('Handle "https" URL with group and subgroup, with "GIT_CREDENTIALS"', async
);
});
test('Handle "git" URL with group and subgroup, with "GIT_CREDENTIALS', async t => {
test('Handle "git" URL with group and subgroup, with "GIT_CREDENTIALS', async (t) => {
const {cwd} = await gitRepo();
t.is(
@@ -314,7 +394,7 @@ test('Handle "git" URL with group and subgroup, with "GIT_CREDENTIALS', async t
);
});
test('Do not add git credential to repositoryUrl if push is allowed', async t => {
test('Do not add git credential to repositoryUrl if push is allowed', async (t) => {
const {cwd, repositoryUrl} = await gitRepo(true);
t.is(
+23 -23
View File
@@ -1,33 +1,33 @@
import test from 'ava';
import getLastRelease from '../lib/get-last-release';
const test = require('ava');
const getLastRelease = require('../lib/get-last-release');
test('Get the highest non-prerelease valid tag', t => {
test('Get the highest non-prerelease valid tag', (t) => {
const result = getLastRelease({
branch: {
name: 'master',
tags: [
{version: '2.0.0', gitTag: 'v2.0.0', gitHead: '222'},
{version: '1.0.0', gitTag: 'v1.0.0', gitHead: '111'},
{version: '3.0.0-beta.1', gitTag: 'v3.0.0-beta.1@beta', gitHead: '333'},
{version: '2.0.0', gitTag: 'v2.0.0', gitHead: 'v2.0.0'},
{version: '1.0.0', gitTag: 'v1.0.0', gitHead: 'v1.0.0'},
{version: '3.0.0-beta.1', gitTag: 'v3.0.0-beta.1', gitHead: 'v3.0.0-beta.1'},
],
type: 'release',
},
options: {tagFormat: `v\${version}`},
});
t.deepEqual(result, {version: '2.0.0', gitTag: 'v2.0.0', name: 'v2.0.0', gitHead: '222', channel: undefined});
t.deepEqual(result, {version: '2.0.0', gitTag: 'v2.0.0', name: 'v2.0.0', gitHead: 'v2.0.0', channels: undefined});
});
test('Get the highest prerelease valid tag, ignoring other tags from other prerelease channels', t => {
test('Get the highest prerelease valid tag, ignoring other tags from other prerelease channels', (t) => {
const result = getLastRelease({
branch: {
name: 'beta',
prerelease: 'beta',
channel: 'beta',
tags: [
{version: '1.0.0-beta.1', gitTag: 'v1.0.0-beta.1@beta', gitHead: '111', channel: 'beta'},
{version: '1.0.0-beta.2', gitTag: 'v1.0.0-beta.2@beta', gitHead: '222', channel: 'beta'},
{version: '1.0.0-alpha.1', gitTag: 'v1.0.0-alpha.1@alpha', gitHead: '333', channel: 'alpha'},
{version: '1.0.0-beta.1', gitTag: 'v1.0.0-beta.1', gitHead: 'v1.0.0-beta.1', channels: ['beta']},
{version: '1.0.0-beta.2', gitTag: 'v1.0.0-beta.2', gitHead: 'v1.0.0-beta.2', channels: ['beta']},
{version: '1.0.0-alpha.1', gitTag: 'v1.0.0-alpha.1', gitHead: 'v1.0.0-alpha.1', channels: ['alpha']},
],
type: 'prerelease',
},
@@ -36,18 +36,18 @@ test('Get the highest prerelease valid tag, ignoring other tags from other prere
t.deepEqual(result, {
version: '1.0.0-beta.2',
gitTag: 'v1.0.0-beta.2@beta',
gitTag: 'v1.0.0-beta.2',
name: 'v1.0.0-beta.2',
gitHead: '222',
channel: 'beta',
gitHead: 'v1.0.0-beta.2',
channels: ['beta'],
});
});
test('Return empty object if no valid tag is found', t => {
test('Return empty object if no valid tag is found', (t) => {
const result = getLastRelease({
branch: {
name: 'master',
tags: [{version: '3.0.0-beta.1', gitTag: 'v3.0.0-beta.1@beta', gitHead: '111'}],
tags: [{version: '3.0.0-beta.1', gitTag: 'v3.0.0-beta.1', gitHead: 'v3.0.0-beta.1'}],
type: 'release',
},
options: {tagFormat: `v\${version}`},
@@ -56,18 +56,18 @@ test('Return empty object if no valid tag is found', t => {
t.deepEqual(result, {});
});
test('Get the highest non-prerelease valid tag before a certain version', t => {
test('Get the highest non-prerelease valid tag before a certain version', (t) => {
const result = getLastRelease(
{
branch: {
name: 'master',
channel: undefined,
tags: [
{version: '2.0.0', gitTag: 'v2.0.0', gitHead: '333'},
{version: '1.0.0', gitTag: 'v1.0.0', gitHead: '111'},
{version: '2.0.0-beta.1', gitTag: 'v2.0.0-beta.1@beta', gitHead: '222'},
{version: '2.1.0', gitTag: 'v2.1.0', gitHead: '444'},
{version: '2.1.1', gitTag: 'v2.1.1', gitHead: '555'},
{version: '2.0.0', gitTag: 'v2.0.0', gitHead: 'v2.0.0'},
{version: '1.0.0', gitTag: 'v1.0.0', gitHead: 'v1.0.0'},
{version: '2.0.0-beta.1', gitTag: 'v2.0.0-beta.1', gitHead: 'v2.0.0-beta.1'},
{version: '2.1.0', gitTag: 'v2.1.0', gitHead: 'v2.1.0'},
{version: '2.1.1', gitTag: 'v2.1.1', gitHead: 'v2.1.1'},
],
type: 'release',
},
@@ -76,5 +76,5 @@ test('Get the highest non-prerelease valid tag before a certain version', t => {
{before: '2.1.0'}
);
t.deepEqual(result, {version: '2.0.0', gitTag: 'v2.0.0', name: 'v2.0.0', gitHead: '333', channel: undefined});
t.deepEqual(result, {version: '2.0.0', gitTag: 'v2.0.0', name: 'v2.0.0', gitHead: 'v2.0.0', channels: undefined});
});
+4 -4
View File
@@ -1,8 +1,8 @@
import test from 'ava';
import {spy} from 'sinon';
import getLogger from '../lib/get-logger';
const test = require('ava');
const {spy} = require('sinon');
const getLogger = require('../lib/get-logger');
test('Expose "error", "success" and "log" functions', t => {
test('Expose "error", "success" and "log" functions', (t) => {
const stdout = spy();
const stderr = spy();
const logger = getLogger({stdout: {write: stdout}, stderr: {write: stderr}});
+162 -50
View File
@@ -1,53 +1,53 @@
import test from 'ava';
import {stub} from 'sinon';
import getNextVersion from '../lib/get-next-version';
const test = require('ava');
const {stub} = require('sinon');
const getNextVersion = require('../lib/get-next-version');
test.beforeEach(t => {
test.beforeEach((t) => {
// Stub the logger functions
t.context.log = stub();
t.context.logger = {log: t.context.log};
});
test('Increase version for patch release', t => {
test('Increase version for patch release', (t) => {
t.is(
getNextVersion({
branch: {name: 'master', type: 'release'},
branch: {name: 'master', type: 'release', tags: [{gitTag: 'v1.0.0', version: '1.0.0', channels: [null]}]},
nextRelease: {type: 'patch'},
lastRelease: {version: '1.0.0'},
lastRelease: {version: '1.0.0', channels: [null]},
logger: t.context.logger,
}),
'1.0.1'
);
});
test('Increase version for minor release', t => {
test('Increase version for minor release', (t) => {
t.is(
getNextVersion({
branch: {name: 'master', type: 'release'},
branch: {name: 'master', type: 'release', tags: [{gitTag: 'v1.0.0', version: '1.0.0', channels: [null]}]},
nextRelease: {type: 'minor'},
lastRelease: {version: '1.0.0'},
lastRelease: {version: '1.0.0', channels: [null]},
logger: t.context.logger,
}),
'1.1.0'
);
});
test('Increase version for major release', t => {
test('Increase version for major release', (t) => {
t.is(
getNextVersion({
branch: {name: 'master', type: 'release'},
branch: {name: 'master', type: 'release', tags: [{gitTag: 'v1.0.0', version: '1.0.0', channels: [null]}]},
nextRelease: {type: 'major'},
lastRelease: {version: '1.0.0'},
lastRelease: {version: '1.0.0', channels: [null]},
logger: t.context.logger,
}),
'2.0.0'
);
});
test('Return 1.0.0 if there is no previous release', t => {
test('Return 1.0.0 if there is no previous release', (t) => {
t.is(
getNextVersion({
branch: {name: 'master', type: 'release'},
branch: {name: 'master', type: 'release', tags: []},
nextRelease: {type: 'minor'},
lastRelease: {},
logger: t.context.logger,
@@ -56,12 +56,17 @@ test('Return 1.0.0 if there is no previous release', t => {
);
});
test('Increase version for patch release on prerelease branch', t => {
test('Increase version for patch release on prerelease branch', (t) => {
t.is(
getNextVersion({
branch: {name: 'beta', type: 'prerelease', prerelease: 'beta'},
nextRelease: {type: 'patch'},
lastRelease: {version: '1.0.0'},
branch: {
name: 'beta',
type: 'prerelease',
prerelease: 'beta',
tags: [{gitTag: 'v1.0.0', version: '1.0.0', channels: [null]}],
},
nextRelease: {type: 'patch', channel: 'beta'},
lastRelease: {version: '1.0.0', channels: [null]},
logger: t.context.logger,
}),
'1.0.1-beta.1'
@@ -69,31 +74,49 @@ test('Increase version for patch release on prerelease branch', t => {
t.is(
getNextVersion({
branch: {name: 'beta', type: 'prerelease', prerelease: 'beta'},
nextRelease: {type: 'patch'},
lastRelease: {version: '1.0.0-beta.1'},
branch: {
name: 'beta',
type: 'prerelease',
prerelease: 'beta',
tags: [
{gitTag: 'v1.0.0', version: '1.0.0', channels: [null]},
{gitTag: 'v1.0.1-beta.1', version: '1.0.1-beta.1', channels: ['beta']},
],
},
nextRelease: {type: 'patch', channel: 'beta'},
lastRelease: {version: '1.0.1-beta.1', channels: ['beta']},
logger: t.context.logger,
}),
'1.0.0-beta.2'
'1.0.1-beta.2'
);
t.is(
getNextVersion({
branch: {name: 'alpha', type: 'prerelease', prerelease: 'alpha'},
branch: {
name: 'alpha',
type: 'prerelease',
prerelease: 'alpha',
tags: [{gitTag: 'v1.0.1-beta.1', version: '1.0.1-beta.1', channels: ['beta']}],
},
nextRelease: {type: 'patch', channel: 'alpha'},
lastRelease: {version: '1.0.0-beta.1', channel: 'beta'},
lastRelease: {version: '1.0.1-beta.1', channels: ['beta']},
logger: t.context.logger,
}),
'1.0.1-alpha.1'
'1.0.2-alpha.1'
);
});
test('Increase version for minor release on prerelease branch', t => {
test('Increase version for minor release on prerelease branch', (t) => {
t.is(
getNextVersion({
branch: {name: 'beta', type: 'prerelease', prerelease: 'beta'},
nextRelease: {type: 'minor'},
lastRelease: {version: '1.0.0'},
branch: {
name: 'beta',
type: 'prerelease',
prerelease: 'beta',
tags: [{gitTag: 'v1.0.0', version: '1.0.0', channels: [null]}],
},
nextRelease: {type: 'minor', channel: 'beta'},
lastRelease: {version: '1.0.0', channels: [null]},
logger: t.context.logger,
}),
'1.1.0-beta.1'
@@ -101,31 +124,49 @@ test('Increase version for minor release on prerelease branch', t => {
t.is(
getNextVersion({
branch: {name: 'beta', type: 'prerelease', prerelease: 'beta'},
nextRelease: {type: 'minor'},
lastRelease: {version: '1.0.0-beta.1'},
branch: {
name: 'beta',
type: 'prerelease',
prerelease: 'beta',
tags: [
{gitTag: 'v1.0.0', version: '1.0.0', channels: [null]},
{gitTag: 'v1.1.0-beta.1', version: '1.1.0-beta.1', channels: ['beta']},
],
},
nextRelease: {type: 'minor', channel: 'beta'},
lastRelease: {version: '1.1.0-beta.1', channels: ['beta']},
logger: t.context.logger,
}),
'1.0.0-beta.2'
'1.1.0-beta.2'
);
t.is(
getNextVersion({
branch: {name: 'alpha', type: 'prerelease', prerelease: 'alpha'},
branch: {
name: 'alpha',
type: 'prerelease',
prerelease: 'alpha',
tags: [{gitTag: 'v1.1.0-beta.1', version: '1.1.0-beta.1', channels: ['beta']}],
},
nextRelease: {type: 'minor', channel: 'alpha'},
lastRelease: {version: '1.0.0-beta.1', channel: 'beta'},
lastRelease: {version: '1.1.0-beta.1', channels: ['beta']},
logger: t.context.logger,
}),
'1.1.0-alpha.1'
'1.2.0-alpha.1'
);
});
test('Increase version for major release on prerelease branch', t => {
test('Increase version for major release on prerelease branch', (t) => {
t.is(
getNextVersion({
branch: {name: 'beta', type: 'prerelease', prerelease: 'beta'},
nextRelease: {type: 'major'},
lastRelease: {version: '1.0.0'},
branch: {
name: 'beta',
type: 'prerelease',
prerelease: 'beta',
tags: [{gitTag: 'v1.0.0', version: '1.0.0', channels: [null]}],
},
nextRelease: {type: 'major', channel: 'beta'},
lastRelease: {version: '1.0.0', channels: [null]},
logger: t.context.logger,
}),
'2.0.0-beta.1'
@@ -133,29 +174,42 @@ test('Increase version for major release on prerelease branch', t => {
t.is(
getNextVersion({
branch: {name: 'beta', type: 'prerelease', prerelease: 'beta'},
nextRelease: {type: 'major'},
lastRelease: {version: '1.0.0-beta.1'},
branch: {
name: 'beta',
type: 'prerelease',
prerelease: 'beta',
tags: [
{gitTag: 'v1.0.0', version: '1.0.0', channels: [null]},
{gitTag: 'v2.0.0-beta.1', version: '2.0.0-beta.1', channels: ['beta']},
],
},
nextRelease: {type: 'major', channel: 'beta'},
lastRelease: {version: '2.0.0-beta.1', channels: ['beta']},
logger: t.context.logger,
}),
'1.0.0-beta.2'
'2.0.0-beta.2'
);
t.is(
getNextVersion({
branch: {name: 'alpha', type: 'prerelease', prerelease: 'alpha'},
branch: {
name: 'alpha',
type: 'prerelease',
prerelease: 'alpha',
tags: [{gitTag: 'v2.0.0-beta.1', version: '2.0.0-beta.1', channels: ['beta']}],
},
nextRelease: {type: 'major', channel: 'alpha'},
lastRelease: {version: '1.0.0-beta.1', channel: 'beta'},
lastRelease: {version: '2.0.0-beta.1', channels: ['beta']},
logger: t.context.logger,
}),
'2.0.0-alpha.1'
'3.0.0-alpha.1'
);
});
test('Return 1.0.0 if there is no previous release on prerelease branch', t => {
test('Return 1.0.0 if there is no previous release on prerelease branch', (t) => {
t.is(
getNextVersion({
branch: {name: 'beta', type: 'prerelease', prerelease: 'beta'},
branch: {name: 'beta', type: 'prerelease', prerelease: 'beta', tags: []},
nextRelease: {type: 'minor'},
lastRelease: {},
logger: t.context.logger,
@@ -163,3 +217,61 @@ test('Return 1.0.0 if there is no previous release on prerelease branch', t => {
'1.0.0-beta.1'
);
});
test('Increase version for release on prerelease branch after previous commits were merged to release branch', (t) => {
t.is(
getNextVersion({
branch: {
name: 'beta',
type: 'prerelease',
prerelease: 'beta',
tags: [
{gitTag: 'v1.0.0', version: '1.0.0', channels: [null]},
{gitTag: 'v1.1.0', version: '1.1.0', channels: [null]}, // Version v1.1.0 released on default branch after beta was merged into master
{gitTag: 'v1.1.0-beta.1', version: '1.1.0-beta.1', channels: [null, 'beta']},
],
},
nextRelease: {type: 'minor'},
lastRelease: {version: '1.1.0', channels: [null]},
logger: t.context.logger,
}),
'1.2.0-beta.1'
);
});
test('Increase version for release on prerelease branch based on highest commit type since last regular release', (t) => {
t.is(
getNextVersion({
branch: {
name: 'beta',
type: 'prerelease',
prerelease: 'beta',
tags: [
{gitTag: 'v1.0.0', version: '1.0.0', channels: [null]},
{gitTag: 'v1.1.0-beta.1', version: '1.1.0-beta.1', channels: [null, 'beta']},
],
},
nextRelease: {type: 'major'},
lastRelease: {version: 'v1.1.0-beta.1', channels: [null]},
logger: t.context.logger,
}),
'2.0.0-beta.1'
);
});
test('Increase version for release on prerelease branch when there is no regular releases on other branches', (t) => {
t.is(
getNextVersion({
branch: {
name: 'beta',
type: 'prerelease',
prerelease: 'beta',
tags: [{gitTag: 'v1.0.0-beta.1', version: '1.0.0-beta.1', channels: ['beta']}],
},
nextRelease: {type: 'minor', channel: 'beta'},
lastRelease: {version: 'v1.0.0-beta.1', channels: ['beta']},
logger: t.context.logger,
}),
'1.0.0-beta.2'
);
});
+282
View File
@@ -0,0 +1,282 @@
const test = require('ava');
const getReleaseToAdd = require('../lib/get-release-to-add');
test('Return versions merged from release to maintenance branch, excluding lower than branch start range', (t) => {
const result = getReleaseToAdd({
branch: {
name: '2.x',
channel: '2.x',
type: 'maintenance',
mergeRange: '>=2.0.0 <3.0.0',
tags: [
{gitTag: 'v2.0.0', version: '2.0.0', channels: ['2.x']},
{gitTag: 'v2.0.0', version: '2.0.0', channels: [null]},
{gitTag: 'v2.1.0', version: '2.1.0', channels: [null]},
{gitTag: 'v2.1.1', version: '2.1.1', channels: [null]},
{gitTag: 'v1.0.0', version: '1.0.0', channels: [null]},
{gitTag: 'v1.1.0', version: '1.1.0', channels: [null]},
],
},
branches: [{name: '2.x', channel: '2.x'}, {name: 'master'}],
options: {tagFormat: `v\${version}`},
});
t.deepEqual(result, {
lastRelease: {version: '2.1.0', channels: [null], gitTag: 'v2.1.0', name: 'v2.1.0', gitHead: 'v2.1.0'},
currentRelease: {
type: 'patch',
version: '2.1.1',
channels: [null],
gitTag: 'v2.1.1',
name: 'v2.1.1',
gitHead: 'v2.1.1',
},
nextRelease: {
type: 'patch',
version: '2.1.1',
channel: '2.x',
gitTag: 'v2.1.1',
name: 'v2.1.1',
gitHead: 'v2.1.1',
},
});
});
test('Return versions merged between release branches', (t) => {
const result = getReleaseToAdd({
branch: {
name: 'master',
tags: [
{gitTag: 'v1.0.0', version: '1.0.0', channels: [null, 'next']},
{gitTag: 'v1.1.0', version: '1.1.0', channels: ['next']},
{gitTag: 'v2.0.0', version: '2.0.0', channels: ['next-major']},
],
},
branches: [{name: 'master'}, {name: 'next', channel: 'next'}, {name: 'next-major', channel: 'next-major'}],
options: {tagFormat: `v\${version}`},
});
t.deepEqual(result, {
lastRelease: {
version: '1.1.0',
gitTag: 'v1.1.0',
name: 'v1.1.0',
gitHead: 'v1.1.0',
channels: ['next'],
},
currentRelease: {
type: 'major',
version: '2.0.0',
channels: ['next-major'],
gitTag: 'v2.0.0',
name: 'v2.0.0',
gitHead: 'v2.0.0',
},
nextRelease: {
type: 'major',
version: '2.0.0',
channel: null,
gitTag: 'v2.0.0',
name: 'v2.0.0',
gitHead: 'v2.0.0',
},
});
});
test('Return releases sorted by ascending order', (t) => {
const result = getReleaseToAdd({
branch: {
name: 'master',
tags: [
{gitTag: 'v2.0.0', version: '2.0.0', channels: ['next-major']},
{gitTag: 'v1.1.0', version: '1.1.0', channels: ['next']},
{gitTag: 'v1.0.0', version: '1.0.0', channels: [null, 'next']},
],
},
branches: [{name: 'master'}, {name: 'next', channel: 'next'}, {name: 'next-major', channel: 'next-major'}],
options: {tagFormat: `v\${version}`},
});
t.deepEqual(result, {
lastRelease: {version: '1.1.0', gitTag: 'v1.1.0', name: 'v1.1.0', gitHead: 'v1.1.0', channels: ['next']},
currentRelease: {
type: 'major',
version: '2.0.0',
channels: ['next-major'],
gitTag: 'v2.0.0',
name: 'v2.0.0',
gitHead: 'v2.0.0',
},
nextRelease: {
type: 'major',
version: '2.0.0',
channel: null,
gitTag: 'v2.0.0',
name: 'v2.0.0',
gitHead: 'v2.0.0',
},
});
});
test('No lastRelease', (t) => {
const result = getReleaseToAdd({
branch: {
name: 'master',
tags: [{gitTag: 'v1.0.0', version: '1.0.0', channels: ['next']}],
},
branches: [{name: 'master'}, {name: 'next', channel: 'next'}],
options: {tagFormat: `v\${version}`},
});
t.deepEqual(result, {
lastRelease: {},
currentRelease: {
type: 'major',
version: '1.0.0',
channels: ['next'],
gitTag: 'v1.0.0',
name: 'v1.0.0',
gitHead: 'v1.0.0',
},
nextRelease: {
type: 'major',
version: '1.0.0',
channel: null,
gitTag: 'v1.0.0',
name: 'v1.0.0',
gitHead: 'v1.0.0',
},
});
});
test('Ignore pre-release versions', (t) => {
const result = getReleaseToAdd({
branch: {
name: 'master',
tags: [
{gitTag: 'v1.0.0', version: '1.0.0', channels: [null, 'next']},
{gitTag: 'v1.1.0', version: '1.1.0', channels: ['next']},
{gitTag: 'v2.0.0-alpha.1', version: '2.0.0-alpha.1', channels: ['alpha']},
],
},
branches: [
{name: 'master'},
{name: 'next', channel: 'next'},
{name: 'alpha', type: 'prerelease', channel: 'alpha'},
],
options: {tagFormat: `v\${version}`},
});
t.deepEqual(result, {
lastRelease: {version: '1.0.0', channels: [null, 'next'], gitTag: 'v1.0.0', name: 'v1.0.0', gitHead: 'v1.0.0'},
currentRelease: {
type: 'minor',
version: '1.1.0',
channels: ['next'],
gitTag: 'v1.1.0',
name: 'v1.1.0',
gitHead: 'v1.1.0',
},
nextRelease: {
type: 'minor',
version: '1.1.0',
channel: null,
gitTag: 'v1.1.0',
name: 'v1.1.0',
gitHead: 'v1.1.0',
},
});
});
test('Exclude versions merged from release to maintenance branch if they have the same "channel"', (t) => {
const result = getReleaseToAdd({
branch: {
name: '2.x',
channel: 'latest',
type: 'maintenance',
mergeRange: '>=2.0.0 <3.0.0',
tags: [
{gitTag: 'v2.0.0', version: '2.0.0', channels: [null]},
{gitTag: 'v2.0.0', version: '2.0.0', channels: [null]},
{gitTag: 'v2.1.0', version: '2.1.0', channels: [null]},
{gitTag: 'v2.1.1', version: '2.1.1', channels: [null]},
{gitTag: 'v1.0.0', version: '1.0.0', channels: [null]},
{gitTag: 'v1.1.0', version: '1.1.0', channels: [null]},
],
},
branches: [
{name: '2.x', channel: 'latest'},
{name: 'master', channel: 'latest'},
],
options: {tagFormat: `v\${version}`},
});
t.is(result, undefined);
});
test('Exclude versions merged between release branches if they have the same "channel"', (t) => {
const result = getReleaseToAdd({
branch: {
name: 'master',
channel: 'latest',
tags: [
{gitTag: 'v1.0.0', channels: ['latest'], version: '1.0.0'},
{gitTag: 'v1.1.0', channels: ['latest'], version: '1.1.0'},
{gitTag: 'v2.0.0', channels: ['latest'], version: '2.0.0'},
],
},
branches: [
{name: 'master', channel: 'latest'},
{name: 'next', channel: 'latest'},
{name: 'next-major', channel: 'latest'},
],
options: {tagFormat: `v\${version}`},
});
t.is(result, undefined);
});
test('Exclude versions merged between release branches if they all have "channel" set to "false"', (t) => {
const result = getReleaseToAdd({
branch: {
name: 'master',
channel: false,
tags: [
{gitTag: 'v1.0.0', version: '1.0.0', channels: [null]},
{gitTag: 'v1.1.0', version: '1.1.0', channels: [null]},
{gitTag: 'v2.0.0', version: '2.0.0', channels: [null]},
],
},
branches: [
{name: 'master', channel: false},
{name: 'next', channel: false},
{name: 'next-major', channel: false},
],
options: {tagFormat: `v\${version}`},
});
t.is(result, undefined);
});
test('Exclude versions number less than the latest version already released on that branch', (t) => {
const result = getReleaseToAdd({
branch: {
name: '2.x',
channel: '2.x',
type: 'maintenance',
mergeRange: '>=2.0.0 <3.0.0',
tags: [
{gitTag: 'v2.0.0', version: '2.0.0', channels: ['2.x']},
{gitTag: 'v2.0.0', version: '2.0.0', channels: [null]},
{gitTag: 'v2.1.0', version: '2.1.0', channels: [null]},
{gitTag: 'v2.1.1', version: '2.1.1', channels: [null, '2.x']},
{gitTag: 'v1.0.0', version: '1.0.0', channels: [null]},
{gitTag: 'v1.1.0', version: '1.1.0', channels: [null]},
],
},
branches: [{name: '2.x', channel: '2.x'}, {name: 'master'}],
options: {tagFormat: `v\${version}`},
});
t.is(result, undefined);
});
-313
View File
@@ -1,313 +0,0 @@
import test from 'ava';
import getReleasesToAdd from '../lib/get-releases-to-add';
test('Return versions merged from release to maintenance branch, excluding lower than branch start range', t => {
const result = getReleasesToAdd({
branch: {
name: '2.x',
channel: '2.x',
type: 'maintenance',
mergeRange: '>=2.0.0 <3.0.0',
tags: [
{gitTag: 'v2.0.0@2.x', version: '2.0.0', channel: '2.x', gitHead: '111'},
{gitTag: 'v2.0.0', version: '2.0.0', gitHead: '111'},
{gitTag: 'v2.1.0', version: '2.1.0', gitHead: '222'},
{gitTag: 'v2.1.1', version: '2.1.1', gitHead: '333'},
{gitTag: 'v1.0.0', version: '1.0.0', gitHead: '444'},
{gitTag: 'v1.1.0', version: '1.1.0', gitHead: '555'},
],
},
branches: [{name: '2.x', channel: '2.x'}, {name: 'master'}],
options: {tagFormat: `v\${version}`},
});
t.deepEqual(result, [
{
lastRelease: {version: '2.0.0', channel: '2.x', gitTag: 'v2.0.0@2.x', name: 'v2.0.0', gitHead: '111'},
currentRelease: {
type: 'minor',
version: '2.1.0',
channel: undefined,
gitTag: 'v2.1.0',
name: 'v2.1.0',
gitHead: '222',
},
nextRelease: {
type: 'minor',
version: '2.1.0',
channel: '2.x',
gitTag: 'v2.1.0@2.x',
name: 'v2.1.0',
gitHead: '222',
},
},
{
lastRelease: {version: '2.1.0', channel: undefined, gitTag: 'v2.1.0', name: 'v2.1.0', gitHead: '222'},
currentRelease: {
type: 'patch',
version: '2.1.1',
channel: undefined,
gitTag: 'v2.1.1',
name: 'v2.1.1',
gitHead: '333',
},
nextRelease: {
type: 'patch',
version: '2.1.1',
channel: '2.x',
gitTag: 'v2.1.1@2.x',
name: 'v2.1.1',
gitHead: '333',
},
},
]);
});
test('Return versions merged between release branches', t => {
const result = getReleasesToAdd({
branch: {
name: 'master',
tags: [
{gitTag: 'v1.0.0', version: '1.0.0', gitHead: '111'},
{gitTag: 'v1.0.0@next', version: '1.0.0', channel: 'next', gitHead: '111'},
{gitTag: 'v1.1.0@next', version: '1.1.0', channel: 'next', gitHead: '222'},
{gitTag: 'v2.0.0@next-major', version: '2.0.0', channel: 'next-major', gitHead: '333'},
],
},
branches: [{name: 'master'}, {name: 'next', channel: 'next'}, {name: 'next-major', channel: 'next-major'}],
options: {tagFormat: `v\${version}`},
});
t.deepEqual(result, [
{
lastRelease: {version: '1.0.0', channel: undefined, gitTag: 'v1.0.0', name: 'v1.0.0', gitHead: '111'},
currentRelease: {
type: 'minor',
version: '1.1.0',
channel: 'next',
gitTag: 'v1.1.0@next',
name: 'v1.1.0',
gitHead: '222',
},
nextRelease: {
type: 'minor',
version: '1.1.0',
channel: undefined,
gitTag: 'v1.1.0',
name: 'v1.1.0',
gitHead: '222',
},
},
{
lastRelease: {version: '1.1.0', gitTag: 'v1.1.0@next', name: 'v1.1.0', gitHead: '222', channel: 'next'},
currentRelease: {
type: 'major',
version: '2.0.0',
channel: 'next-major',
gitTag: 'v2.0.0@next-major',
name: 'v2.0.0',
gitHead: '333',
},
nextRelease: {
type: 'major',
version: '2.0.0',
channel: undefined,
gitTag: 'v2.0.0',
name: 'v2.0.0',
gitHead: '333',
},
},
]);
});
test('Return releases sorted by ascending order', t => {
const result = getReleasesToAdd({
branch: {
name: 'master',
tags: [
{gitTag: 'v2.0.0@next-major', version: '2.0.0', channel: 'next-major', gitHead: '333'},
{gitTag: 'v1.1.0@next', version: '1.1.0', channel: 'next', gitHead: '222'},
{gitTag: 'v1.0.0', version: '1.0.0', gitHead: '111'},
{gitTag: 'v1.0.0@next', version: '1.0.0', channel: 'next', gitHead: '111'},
],
},
branches: [{name: 'master'}, {name: 'next', channel: 'next'}, {name: 'next-major', channel: 'next-major'}],
options: {tagFormat: `v\${version}`},
});
t.deepEqual(result, [
{
lastRelease: {version: '1.0.0', channel: undefined, gitTag: 'v1.0.0', name: 'v1.0.0', gitHead: '111'},
currentRelease: {
type: 'minor',
version: '1.1.0',
channel: 'next',
gitTag: 'v1.1.0@next',
name: 'v1.1.0',
gitHead: '222',
},
nextRelease: {
type: 'minor',
version: '1.1.0',
channel: undefined,
gitTag: 'v1.1.0',
name: 'v1.1.0',
gitHead: '222',
},
},
{
lastRelease: {version: '1.1.0', gitTag: 'v1.1.0@next', name: 'v1.1.0', gitHead: '222', channel: 'next'},
currentRelease: {
type: 'major',
version: '2.0.0',
channel: 'next-major',
gitTag: 'v2.0.0@next-major',
name: 'v2.0.0',
gitHead: '333',
},
nextRelease: {
type: 'major',
version: '2.0.0',
channel: undefined,
gitTag: 'v2.0.0',
name: 'v2.0.0',
gitHead: '333',
},
},
]);
});
test('No lastRelease', t => {
const result = getReleasesToAdd({
branch: {name: 'master', tags: [{gitTag: 'v1.0.0@next', version: '1.0.0', channel: 'next', gitHead: '111'}]},
branches: [{name: 'master'}, {name: 'next', channel: 'next'}],
options: {tagFormat: `v\${version}`},
});
t.deepEqual(result, [
{
lastRelease: {},
currentRelease: {
type: 'major',
version: '1.0.0',
channel: 'next',
gitTag: 'v1.0.0@next',
name: 'v1.0.0',
gitHead: '111',
},
nextRelease: {
type: 'major',
version: '1.0.0',
channel: undefined,
gitTag: 'v1.0.0',
name: 'v1.0.0',
gitHead: '111',
},
},
]);
});
test('Ignore pre-release versions', t => {
const result = getReleasesToAdd({
branch: {
name: 'master',
tags: [
{gitTag: 'v1.0.0', version: '1.0.0', gitHead: '111'},
{gitTag: 'v1.0.0@next', version: '1.0.0', channel: 'next', gitHead: '111'},
{gitTag: 'v1.1.0@next', version: '1.1.0', channel: 'next', gitHead: '222'},
{gitTag: 'v2.0.0-alpha.1@alpha', version: '2.0.0', channel: 'alpha', gitHead: '333'},
],
},
branches: [
{name: 'master'},
{name: 'next', channel: 'next'},
{name: 'alpha', type: 'prerelease', channel: 'alpha'},
],
options: {tagFormat: `v\${version}`},
});
t.deepEqual(result, [
{
lastRelease: {version: '1.0.0', channel: undefined, gitTag: 'v1.0.0', name: 'v1.0.0', gitHead: '111'},
currentRelease: {
type: 'minor',
version: '1.1.0',
channel: 'next',
gitTag: 'v1.1.0@next',
name: 'v1.1.0',
gitHead: '222',
},
nextRelease: {
type: 'minor',
version: '1.1.0',
channel: undefined,
gitTag: 'v1.1.0',
name: 'v1.1.0',
gitHead: '222',
},
},
]);
});
test('Exclude versions merged from release to maintenance branch if they have the same "channel"', t => {
const result = getReleasesToAdd({
branch: {
name: '2.x',
channel: 'latest',
type: 'maintenance',
mergeRange: '>=2.0.0 <3.0.0',
tags: [
{gitTag: 'v2.0.0', version: '2.0.0', gitHead: '111'},
{gitTag: 'v2.0.0', version: '2.0.0', gitHead: '111'},
{gitTag: 'v2.1.0', version: '2.1.0', gitHead: '222'},
{gitTag: 'v2.1.1', version: '2.1.1', gitHead: '333'},
{gitTag: 'v1.0.0', version: '1.0.0', gitHead: '444'},
{gitTag: 'v1.1.0', version: '1.1.0', gitHead: '555'},
],
},
branches: [{name: '2.x', channel: 'latest'}, {name: 'master', channel: 'latest'}],
options: {tagFormat: `v\${version}`},
});
t.deepEqual(result, []);
});
test('Exclude versions merged between release branches if they have the same "channel"', t => {
const result = getReleasesToAdd({
branch: {
name: 'master',
channel: 'latest',
tags: [
{gitTag: 'v1.0.0', channel: 'latest', version: '1.0.0', gitHead: '111'},
{gitTag: 'v1.1.0', channel: 'latest', version: '1.1.0', gitHead: '222'},
{gitTag: 'v2.0.0', channel: 'latest', version: '2.0.0', gitHead: '333'},
],
},
branches: [
{name: 'master', channel: 'latest'},
{name: 'next', channel: 'latest'},
{name: 'next-major', channel: 'latest'},
],
options: {tagFormat: `v\${version}`},
});
t.deepEqual(result, []);
});
test('Exclude versions merged between release branches if they all have "channel" set to "false"', t => {
const result = getReleasesToAdd({
branch: {
name: 'master',
channel: false,
tags: [
{gitTag: 'v1.0.0', version: '1.0.0', gitHead: '111'},
{gitTag: 'v1.1.0', version: '1.1.0', gitHead: '222'},
{gitTag: 'v2.0.0', version: '2.0.0', gitHead: '333'},
],
},
branches: [{name: 'master', channel: false}, {name: 'next', channel: false}, {name: 'next-major', channel: false}],
options: {tagFormat: `v\${version}`},
});
t.deepEqual(result, []);
});
+198 -60
View File
@@ -1,8 +1,7 @@
import test from 'ava';
import tempy from 'tempy';
import {
const test = require('ava');
const tempy = require('tempy');
const {
getTagHead,
isRefInHistory,
isRefExists,
fetch,
getGitHead,
@@ -14,8 +13,11 @@ import {
isGitRepo,
verifyTagName,
isBranchUpToDate,
} from '../lib/git';
import {
getNote,
addNote,
fetchNotes,
} = require('../lib/git');
const {
gitRepo,
gitCommits,
gitCheckout,
@@ -27,9 +29,14 @@ import {
gitRemoteTagHead,
gitPush,
gitDetachedHead,
} from './helpers/git-utils';
gitDetachedHeadFromBranch,
gitAddNote,
gitGetNote,
gitFetch,
initGit,
} = require('./helpers/git-utils');
test('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
const {cwd} = await gitRepo();
// Add commits to the master branch
@@ -40,14 +47,14 @@ test('Get the last commit sha', async t => {
t.is(result, commits[0].hash);
});
test('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
const {cwd} = await gitRepo();
await t.throwsAsync(getGitHead({cwd}), Error);
await t.throwsAsync(getGitHead({cwd}));
});
test('Unshallow and fetch 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
let {cwd, repositoryUrl} = await gitRepo();
// Add commits to the master branch
@@ -58,13 +65,13 @@ test('Unshallow and fetch repository', async t => {
// Verify the shallow clone contains only one commit
t.is((await gitGetCommits(undefined, {cwd})).length, 1);
await fetch(repositoryUrl, 'master', {cwd});
await fetch(repositoryUrl, 'master', 'master', {cwd});
// Verify the shallow clone contains all the commits
t.is((await gitGetCommits(undefined, {cwd})).length, 2);
});
test('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 {cwd, repositoryUrl} = await gitRepo(true);
await gitCommits(['First'], {cwd});
@@ -73,11 +80,11 @@ test('Do not throw error when unshallow a complete repository', async t => {
await gitCommits(['Second'], {cwd});
await gitPush(repositoryUrl, 'second-branch', {cwd});
await t.notThrowsAsync(fetch(repositoryUrl, 'master', {cwd}));
await t.notThrowsAsync(fetch(repositoryUrl, 'second-branch', {cwd}));
await t.notThrowsAsync(fetch(repositoryUrl, 'master', 'master', {cwd}));
await t.notThrowsAsync(fetch(repositoryUrl, 'second-branch', 'master', {cwd}));
});
test('Fetch all tags on a detached head repository', async t => {
test('Fetch all tags on a detached head repository', async (t) => {
let {cwd, repositoryUrl} = await gitRepo();
await gitCommits(['First'], {cwd});
@@ -89,29 +96,65 @@ test('Fetch all tags on a detached head repository', async t => {
await gitPush(repositoryUrl, 'master', {cwd});
cwd = await gitDetachedHead(repositoryUrl, commit.hash);
await fetch(repositoryUrl, 'master', {cwd});
await fetch(repositoryUrl, 'master', 'master', {cwd});
t.deepEqual((await getTags({cwd})).sort(), ['v1.0.0', 'v1.0.1', 'v1.1.0'].sort());
t.deepEqual((await getTags('master', {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
const {cwd} = await gitRepo();
// Add commits to the master branch
const commits = await gitCommits(['First'], {cwd});
// Create the new branch 'other-branch' from master
test('Fetch all tags on a repository with a detached head from branch (CircleCI)', 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});
await gitCheckout('other-branch', true, {cwd});
// Add commits to the 'other-branch' branch
const otherCommits = await gitCommits(['Second'], {cwd});
await gitPush(repositoryUrl, 'other-branch', {cwd});
await gitCheckout('master', false, {cwd});
await gitCommits(['Fourth'], {cwd});
await gitTagVersion('v2.0.0', undefined, {cwd});
await gitPush(repositoryUrl, 'master', {cwd});
cwd = await gitDetachedHeadFromBranch(repositoryUrl, 'other-branch', commit.hash);
t.true(await isRefInHistory(commits[0].hash, 'master', false, {cwd}));
t.falsy(await isRefInHistory(otherCommits[0].hash, 'master', false, {cwd}));
t.falsy(await isRefInHistory(otherCommits[0].hash, 'missing-branch', false, {cwd}));
await t.throwsAsync(isRefInHistory('non-existant-sha', 'master', false, {cwd}));
await fetch(repositoryUrl, 'master', 'other-branch', {cwd});
await fetch(repositoryUrl, 'other-branch', 'other-branch', {cwd});
t.deepEqual((await getTags('other-branch', {cwd})).sort(), ['v1.0.0', 'v1.0.1', 'v1.1.0'].sort());
t.deepEqual((await getTags('master', {cwd})).sort(), ['v1.0.0', 'v1.0.1', 'v1.1.0', 'v2.0.0'].sort());
});
test('Verify if a branch exists', async t => {
test('Fetch all tags on a detached head repository with outdated cached repo (GitLab CI)', async (t) => {
const {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});
let [commit] = await gitCommits(['Third'], {cwd});
await gitTagVersion('v1.1.0', undefined, {cwd});
await gitPush(repositoryUrl, 'master', {cwd});
// Create a clone (as first CI run would)
const cloneCwd = await gitShallowClone(repositoryUrl);
await gitFetch(repositoryUrl, {cwd: cloneCwd});
await gitCheckout(commit.hash, false, {cwd: cloneCwd});
// Push tag to remote
[commit] = await gitCommits(['Fourth'], {cwd});
await gitTagVersion('v1.2.0', undefined, {cwd});
await gitPush(repositoryUrl, 'master', {cwd});
// Fetch on the cached repo and make detached head, leaving master outdated
await fetch(repositoryUrl, 'master', 'master', {cwd: cloneCwd});
await gitCheckout(commit.hash, false, {cwd: cloneCwd});
t.deepEqual((await getTags('master', {cwd: cloneCwd})).sort(), ['v1.0.0', 'v1.0.1', 'v1.1.0', 'v1.2.0'].sort());
});
test('Verify if a branch exists', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
// Add commits to the master branch
@@ -126,7 +169,7 @@ test('Verify if a branch exists', async t => {
t.falsy(await isRefExists('next', {cwd}));
});
test('Get all branches', async t => {
test('Get all branches', async (t) => {
const {cwd, repositoryUrl} = await gitRepo(true);
await gitCommits(['First'], {cwd});
await gitPush(repositoryUrl, 'master', {cwd});
@@ -140,7 +183,12 @@ test('Get all branches', async t => {
t.deepEqual((await getBranches(repositoryUrl, {cwd})).sort(), ['master', 'second-branch', 'third-branch'].sort());
});
test('Get the commit sha for a given tag or falsy if the tag does not exists', async t => {
test('Return empty array if there are no branches', async (t) => {
const {cwd, repositoryUrl} = await initGit(true);
t.deepEqual(await getBranches(repositoryUrl, {cwd}), []);
});
test('Get the commit sha for a given tag', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
// Add commits to the master branch
@@ -149,10 +197,9 @@ test('Get the commit sha for a given tag or falsy if the tag does not exists', a
await gitTagVersion('v1.0.0', undefined, {cwd});
t.is(await getTagHead('v1.0.0', {cwd}), commits[0].hash);
t.falsy(await getTagHead('missing_tag', {cwd}));
});
test('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
const {cwd} = await gitRepo();
// Add remote.origin.url config
@@ -161,7 +208,7 @@ test('Return git remote repository url from config', async t => {
t.is(await repoUrl({cwd}), 'git@hostname.com:owner/package.git');
});
test('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
let {cwd, repositoryUrl} = await gitRepo();
await gitCommits(['First'], {cwd});
@@ -171,14 +218,14 @@ test('Return git remote repository url set while cloning', async t => {
t.is(await repoUrl({cwd}), repositoryUrl);
});
test('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
const {cwd} = await gitRepo();
t.falsy(await repoUrl({cwd}));
});
test('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
const {cwd} = await gitRepo();
const commits = await gitCommits(['Test commit'], {cwd});
@@ -188,7 +235,7 @@ test('Add tag on head commit', async t => {
await t.is(await gitCommitTag(commits[0].hash, {cwd}), 'tag_name');
});
test('Push tag 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 {cwd, repositoryUrl} = await gitRepo(true);
const commits = await gitCommits(['Test commit'], {cwd});
@@ -199,13 +246,13 @@ test('Push tag to remote repository', async t => {
t.is(await gitRemoteTagHead(repositoryUrl, 'tag_name', {cwd}), commits[0].hash);
});
test('Push tag to remote repository with remote branch ahead', async t => {
test('Push tag to remote repository with remote branch ahead', 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});
const temporaryRepo = await gitShallowClone(repositoryUrl);
await gitCommits(['Second'], {cwd: temporaryRepo});
await gitPush('origin', 'master', {cwd: temporaryRepo});
await tag('tag_name', 'HEAD', {cwd});
await push(repositoryUrl, {cwd});
@@ -213,40 +260,40 @@ test('Push tag to remote repository with remote branch ahead', async t => {
t.is(await gitRemoteTagHead(repositoryUrl, 'tag_name', {cwd}), commits[0].hash);
});
test('Return "true" if in a Git repository', async t => {
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
const {cwd} = await gitRepo(true);
t.true(await isGitRepo({cwd}));
});
test('Return falsy if not in a Git repository', async t => {
test('Return falsy if not in a Git repository', async (t) => {
const cwd = tempy.directory();
t.falsy(await isGitRepo({cwd}));
});
test('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('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('Throws error if obtaining the tags fails', async t => {
test('Throws error if obtaining the tags fails', async (t) => {
const cwd = tempy.directory();
await t.throwsAsync(getTags({cwd}));
await t.throwsAsync(getTags('master', {cwd}));
});
test('Return "true" if repository is up to date', async t => {
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});
@@ -254,7 +301,7 @@ test('Return "true" if repository is up to date', async t => {
t.true(await isBranchUpToDate(repositoryUrl, 'master', {cwd}));
});
test('Return falsy if repository is not up to date', async t => {
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});
@@ -262,18 +309,109 @@ test('Return falsy if repository is not up to date', async t => {
t.true(await isBranchUpToDate(repositoryUrl, 'master', {cwd}));
const tmpRepo = await gitShallowClone(repositoryUrl);
await gitCommits(['Third'], {cwd: tmpRepo});
await gitPush('origin', 'master', {cwd: tmpRepo});
const temporaryRepo = await gitShallowClone(repositoryUrl);
await gitCommits(['Third'], {cwd: temporaryRepo});
await gitPush('origin', 'master', {cwd: temporaryRepo});
t.falsy(await isBranchUpToDate(repositoryUrl, 'master', {cwd}));
});
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});
test('Return falsy if detached head repository is not up to date', async (t) => {
let {cwd, repositoryUrl} = await gitRepo();
t.true(await isBranchUpToDate(repositoryUrl, 'master', {cwd}));
const [commit] = await gitCommits(['First'], {cwd});
await gitCommits(['Second'], {cwd});
await gitPush(repositoryUrl, 'master', {cwd});
cwd = await gitDetachedHead(repositoryUrl, commit.hash);
await fetch(repositoryUrl, 'master', 'master', {cwd});
t.falsy(await isBranchUpToDate(repositoryUrl, 'master', {cwd}));
});
test('Get a commit note', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
// Add commits to the master branch
const commits = await gitCommits(['First'], {cwd});
await gitAddNote(JSON.stringify({note: 'note'}), commits[0].hash, {cwd});
t.deepEqual(await getNote(commits[0].hash, {cwd}), {note: 'note'});
});
test('Return empty object if there is no commit note', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
// Add commits to the master branch
const commits = await gitCommits(['First'], {cwd});
t.deepEqual(await getNote(commits[0].hash, {cwd}), {});
});
test('Throw error if a commit note in invalid', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
// Add commits to the master branch
const commits = await gitCommits(['First'], {cwd});
await gitAddNote('non-json note', commits[0].hash, {cwd});
await t.throwsAsync(getNote(commits[0].hash, {cwd}));
});
test('Add a commit note', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
// Add commits to the master branch
const commits = await gitCommits(['First'], {cwd});
await addNote({note: 'note'}, commits[0].hash, {cwd});
t.is(await gitGetNote(commits[0].hash, {cwd}), '{"note":"note"}');
});
test('Overwrite a commit note', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
// Add commits to the master branch
const commits = await gitCommits(['First'], {cwd});
await addNote({note: 'note'}, commits[0].hash, {cwd});
await addNote({note: 'note2'}, commits[0].hash, {cwd});
t.is(await gitGetNote(commits[0].hash, {cwd}), '{"note":"note2"}');
});
test('Unshallow and fetch repository with notes', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
let {cwd, repositoryUrl} = await gitRepo();
// Add commits to the master branch
const commits = await gitCommits(['First', 'Second'], {cwd});
await gitAddNote(JSON.stringify({note: 'note'}), commits[0].hash, {cwd});
// Create a shallow clone with only 1 commit
cwd = await gitShallowClone(repositoryUrl);
// Verify the shallow clone doesn't contains the note
await t.throwsAsync(gitGetNote(commits[0].hash, {cwd}));
await fetch(repositoryUrl, 'master', 'master', {cwd});
await fetchNotes(repositoryUrl, {cwd});
// Verify the shallow clone contains the note
t.is(await gitGetNote(commits[0].hash, {cwd}), '{"note":"note"}');
});
test('Fetch all notes on a detached head repository', async (t) => {
let {cwd, repositoryUrl} = await gitRepo();
await gitCommits(['First'], {cwd});
const [commit] = await gitCommits(['Second'], {cwd});
await gitPush(repositoryUrl, 'master', {cwd});
await gitAddNote(JSON.stringify({note: 'note'}), commit.hash, {cwd});
cwd = await gitDetachedHead(repositoryUrl, commit.hash);
await fetch(repositoryUrl, 'master', 'master', {cwd});
await fetchNotes(repositoryUrl, {cwd});
t.is(await gitGetNote(commit.hash, {cwd}), '{"note":"note"}');
});
+135 -61
View File
@@ -1,12 +1,13 @@
import tempy from 'tempy';
import execa from 'execa';
import fileUrl from 'file-url';
import pEachSeries from 'p-each-series';
import gitLogParser from 'git-log-parser';
import getStream from 'get-stream';
const tempy = require('tempy');
const execa = require('execa');
const fileUrl = require('file-url');
const pEachSeries = require('p-each-series');
const gitLogParser = require('git-log-parser');
const getStream = require('get-stream');
const {GIT_NOTE_REF} = require('../../lib/definitions/constants');
/**
* Commit message informations.
* Commit message information.
*
* @typedef {Object} Commit
* @property {String} branch The commit branch.
@@ -14,21 +15,34 @@ import getStream from 'get-stream';
* @property {String} message The commit message.
*/
/**
* Initialize git repository
* If `withRemote` is `true`, creates a bare repository and initialize it.
* If `withRemote` is `false`, creates a regular repository and initialize it.
*
* @param {Boolean} withRemote `true` to create a shallow clone of a bare repository.
* @return {String} The path of the repository
*/
async function initGit(withRemote) {
const cwd = tempy.directory();
await execa('git', ['init', ...(withRemote ? ['--bare'] : [])], {cwd});
const repositoryUrl = fileUrl(cwd);
return {cwd, repositoryUrl};
}
/**
* Create a temporary git repository.
* If `withRemote` is `true`, creates a bare repository, initialize it and create a shallow clone. Change the current working directory to the clone root.
* If `withRemote` is `false`, creates a regular repository and initialize it. Change the current working directory to the repository root.
* If `withRemote` is `true`, creates a shallow clone. Change the current working directory to the clone root.
* If `withRemote` is `false`, just change the current working directory to the repository root.
*
*
* @param {Boolean} withRemote `true` to create a shallow clone of a bare repository.
* @param {String} [branch='master'] The branch to initialize.
* @return {String} The path of the clone if `withRemote` is `true`, the path of the repository otherwise.
*/
export async function gitRepo(withRemote, branch = 'master') {
let cwd = tempy.directory();
await execa('git', ['init', ...(withRemote ? ['--bare'] : [])], {cwd});
const repositoryUrl = fileUrl(cwd);
async function gitRepo(withRemote, branch = 'master') {
let {cwd, repositoryUrl} = await initGit(withRemote);
if (withRemote) {
await initBareRepo(repositoryUrl, branch);
cwd = await gitShallowClone(repositoryUrl, branch);
@@ -52,7 +66,7 @@ export async function gitRepo(withRemote, branch = 'master') {
* @param {String} repositoryUrl The URL of the bare repository.
* @param {String} [branch='master'] the branch to initialize.
*/
export async function initBareRepo(repositoryUrl, branch = 'master') {
async function initBareRepo(repositoryUrl, branch = 'master') {
const cwd = tempy.directory();
await execa('git', ['clone', '--no-hardlinks', repositoryUrl, cwd], {cwd});
await gitCheckout(branch, true, {cwd});
@@ -68,12 +82,13 @@ export async function initBareRepo(repositoryUrl, branch = 'master') {
*
* @returns {Array<Commit>} The created commits, in reverse order (to match `git log` order).
*/
export async function gitCommits(messages, execaOpts) {
async function gitCommits(messages, execaOptions) {
await pEachSeries(
messages,
async message => (await execa('git', ['commit', '-m', message, '--allow-empty', '--no-gpg-sign'], execaOpts)).stdout
async (message) =>
(await execa('git', ['commit', '-m', message, '--allow-empty', '--no-gpg-sign'], execaOptions)).stdout
);
return (await gitGetCommits(undefined, execaOpts)).slice(0, messages.length);
return (await gitGetCommits(undefined, execaOptions)).slice(0, messages.length);
}
/**
@@ -84,11 +99,16 @@ export async function gitCommits(messages, execaOpts) {
*
* @return {Array<Object>} The list of parsed commits.
*/
export async function gitGetCommits(from, execaOpts) {
async function gitGetCommits(from, execaOptions) {
Object.assign(gitLogParser.fields, {hash: 'H', message: 'B', gitTags: 'd', committerDate: {key: 'ci', type: Date}});
return (await getStream.array(
gitLogParser.parse({_: `${from ? from + '..' : ''}HEAD`}, {...execaOpts, env: {...process.env, ...execaOpts.env}})
)).map(commit => {
return (
await getStream.array(
gitLogParser.parse(
{_: `${from ? from + '..' : ''}HEAD`},
{...execaOptions, env: {...process.env, ...execaOptions.env}}
)
)
).map((commit) => {
commit.message = commit.message.trim();
commit.gitTags = commit.gitTags.trim();
return commit;
@@ -99,11 +119,21 @@ export async function gitGetCommits(from, execaOpts) {
* Checkout a branch on the current git repository.
*
* @param {String} branch Branch name.
* @param {Boolean} create `true` to create the branch, `false` to checkout an existing branch.
* @param {Boolean} create 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, execaOpts) {
await execa('git', create ? ['checkout', '-b', branch] : ['checkout', branch], execaOpts);
async function gitCheckout(branch, create, execaOptions) {
await execa('git', create ? ['checkout', '-b', branch] : ['checkout', branch], execaOptions);
}
/**
* Fetch current git repository.
*
* @param {String} repositoryUrl The repository remote URL.
* @param {Object} [execaOpts] Options to pass to `execa`.
*/
async function gitFetch(repositoryUrl, execaOptions) {
await execa('git', ['fetch', repositoryUrl], execaOptions);
}
/**
@@ -113,8 +143,8 @@ export async function gitCheckout(branch, create = true, execaOpts) {
*
* @return {String} The sha of the head commit in the current git repository.
*/
export async function gitHead(execaOpts) {
return (await execa('git', ['rev-parse', 'HEAD'], execaOpts)).stdout;
async function gitHead(execaOptions) {
return (await execa('git', ['rev-parse', 'HEAD'], execaOptions)).stdout;
}
/**
@@ -124,8 +154,8 @@ export async function gitHead(execaOpts) {
* @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, execaOpts) {
await execa('git', sha ? ['tag', '-f', tagName, sha] : ['tag', tagName], execaOpts);
async function gitTagVersion(tagName, sha, execaOptions) {
await execa('git', sha ? ['tag', '-f', tagName, sha] : ['tag', tagName], execaOptions);
}
/**
@@ -137,7 +167,7 @@ export async function gitTagVersion(tagName, sha, execaOpts) {
* @param {Number} [depth=1] The number of commit to clone.
* @return {String} The path of the cloned repository.
*/
export async function gitShallowClone(repositoryUrl, branch = 'master', depth = 1) {
async function gitShallowClone(repositoryUrl, branch = 'master', depth = 1) {
const cwd = tempy.directory();
await execa('git', ['clone', '--no-hardlinks', '--no-tags', '-b', branch, '--depth', depth, repositoryUrl, cwd], {
@@ -153,7 +183,7 @@ export async function gitShallowClone(repositoryUrl, branch = 'master', depth =
* @param {Number} head A commit sha of the remote repo that will become the detached head of the new one.
* @return {String} The path of the new repository.
*/
export async function gitDetachedHead(repositoryUrl, head) {
async function gitDetachedHead(repositoryUrl, head) {
const cwd = tempy.directory();
await execa('git', ['init'], {cwd});
@@ -163,6 +193,17 @@ export async function gitDetachedHead(repositoryUrl, head) {
return cwd;
}
async function gitDetachedHeadFromBranch(repositoryUrl, branch, head) {
const cwd = tempy.directory();
await execa('git', ['init'], {cwd});
await execa('git', ['remote', 'add', 'origin', repositoryUrl], {cwd});
await execa('git', ['fetch', '--force', repositoryUrl, `${branch}:remotes/origin/${branch}`], {cwd});
await execa('git', ['reset', '--hard', head], {cwd});
await execa('git', ['checkout', '-q', '-B', branch], {cwd});
return cwd;
}
/**
* Add a new Git configuration.
*
@@ -170,8 +211,8 @@ export async function gitDetachedHead(repositoryUrl, head) {
* @param {String} value Config value.
* @param {Object} [execaOpts] Options to pass to `execa`.
*/
export async function gitAddConfig(name, value, execaOpts) {
await execa('git', ['config', '--add', name, value], execaOpts);
async function gitAddConfig(name, value, execaOptions) {
await execa('git', ['config', '--add', name, value], execaOptions);
}
/**
@@ -182,8 +223,8 @@ export async function gitAddConfig(name, value, execaOpts) {
*
* @return {String} The sha of the commit associated with `tagName` on the local repository.
*/
export async function gitTagHead(tagName, execaOpts) {
return (await execa('git', ['rev-list', '-1', tagName], execaOpts)).stdout;
async function gitTagHead(tagName, execaOptions) {
return (await execa('git', ['rev-list', '-1', tagName], execaOptions)).stdout;
}
/**
@@ -195,11 +236,11 @@ export async function gitTagHead(tagName, execaOpts) {
*
* @return {String} The sha of the commit associated with `tagName` on the remote repository.
*/
export async function gitRemoteTagHead(repositoryUrl, tagName, execaOpts) {
return (await execa('git', ['ls-remote', '--tags', repositoryUrl, tagName], execaOpts)).stdout
async function gitRemoteTagHead(repositoryUrl, tagName, execaOptions) {
return (await execa('git', ['ls-remote', '--tags', repositoryUrl, tagName], execaOptions)).stdout
.split('\n')
.filter(tag => Boolean(tag))
.map(tag => tag.match(/^(\S+)/)[1])[0];
.filter((tag) => Boolean(tag))
.map((tag) => tag.match(/^(?<tag>\S+)/)[1])[0];
}
/**
@@ -210,8 +251,8 @@ export async function gitRemoteTagHead(repositoryUrl, tagName, execaOpts) {
*
* @return {String} The tag associatedwith the sha in parameter or `null`.
*/
export async function gitCommitTag(gitHead, execaOpts) {
return (await execa('git', ['describe', '--tags', '--exact-match', gitHead], execaOpts)).stdout;
async function gitCommitTag(gitHead, execaOptions) {
return (await execa('git', ['describe', '--tags', '--exact-match', gitHead], execaOptions)).stdout;
}
/**
@@ -223,8 +264,8 @@ export async function gitCommitTag(gitHead, execaOpts) {
*
* @throws {Error} if the push failed.
*/
export async function gitPush(repositoryUrl = 'origin', branch = 'master', execaOpts) {
await execa('git', ['push', '--tags', repositoryUrl, `HEAD:${branch}`], execaOpts);
async function gitPush(repositoryUrl, branch, execaOptions) {
await execa('git', ['push', '--tags', repositoryUrl, `HEAD:${branch}`], execaOptions);
}
/**
@@ -233,8 +274,8 @@ export async function gitPush(repositoryUrl = 'origin', branch = 'master', execa
* @param {String} ref The ref to merge.
* @param {Object} [execaOpts] Options to pass to `execa`.
*/
export async function merge(ref, execaOpts) {
await execa('git', ['merge', '--no-ff', ref], execaOpts);
async function merge(ref, execaOptions) {
await execa('git', ['merge', '--no-ff', ref], execaOptions);
}
/**
@@ -243,8 +284,8 @@ export async function merge(ref, execaOpts) {
* @param {String} ref The ref to merge.
* @param {Object} [execaOpts] Options to pass to `execa`.
*/
export async function mergeFf(ref, execaOpts) {
await execa('git', ['merge', '--ff', ref], execaOpts);
async function mergeFf(ref, execaOptions) {
await execa('git', ['merge', '--ff', ref], execaOptions);
}
/**
@@ -253,19 +294,52 @@ export async function mergeFf(ref, execaOpts) {
* @param {String} ref The ref to merge.
* @param {Object} [execaOpts] Options to pass to `execa`.
*/
export async function rebase(ref, execaOpts) {
await execa('git', ['rebase', ref], execaOpts);
async function rebase(ref, execaOptions) {
await execa('git', ['rebase', ref], execaOptions);
}
export async function changeAuthor(sha, execaOpts) {
await execa(
'git',
[
'filter-branch',
'-f',
'--env-filter',
`if [[ "$GIT_COMMIT" = "${sha}" ]]; then export GIT_COMMITTER_NAME="New Author" GIT_COMMITTER_EMAIL="author@test.com"; fi`,
],
execaOpts
);
/**
* Add a note to a Git reference.
*
* @param {String} note The note to add.
* @param {String} ref The ref to add the note to.
* @param {Object} [execaOpts] Options to pass to `execa`.
*/
async function gitAddNote(note, ref, execaOptions) {
await execa('git', ['notes', '--ref', GIT_NOTE_REF, 'add', '-m', note, ref], execaOptions);
}
/**
* Get the note associated with a Git reference.
*
* @param {String} ref The ref to get the note from.
* @param {Object} [execaOpts] Options to pass to `execa`.
*/
async function gitGetNote(ref, execaOptions) {
return (await execa('git', ['notes', '--ref', GIT_NOTE_REF, 'show', ref], execaOptions)).stdout;
}
module.exports = {
initGit,
gitRepo,
initBareRepo,
gitCommits,
gitGetCommits,
gitCheckout,
gitFetch,
gitHead,
gitTagVersion,
gitShallowClone,
gitDetachedHead,
gitDetachedHeadFromBranch,
gitAddConfig,
gitTagHead,
gitRemoteTagHead,
gitCommitTag,
gitPush,
merge,
mergeFf,
rebase,
gitAddNote,
gitGetNote,
};
+5 -5
View File
@@ -1,7 +1,7 @@
import Docker from 'dockerode';
import getStream from 'get-stream';
import pRetry from 'p-retry';
import {initBareRepo, gitShallowClone} from './git-utils';
const Docker = require('dockerode');
const getStream = require('get-stream');
const pRetry = require('p-retry');
const {initBareRepo, gitShallowClone} = require('./git-utils');
const IMAGE = 'pvdlg/docker-gitbox:latest';
const SERVER_PORT = 80;
@@ -69,4 +69,4 @@ async function createRepo(name, branch = 'master', description = `Repository ${n
return {cwd, repositoryUrl, authUrl};
}
export default {start, stop, gitCredential, createRepo};
module.exports = {start, stop, gitCredential, createRepo};
+7 -7
View File
@@ -1,8 +1,8 @@
import Docker from 'dockerode';
import getStream from 'get-stream';
import got from 'got';
import pRetry from 'p-retry';
import {mockServerClient} from 'mockserver-client';
const Docker = require('dockerode');
const getStream = require('get-stream');
const got = require('got');
const pRetry = require('p-retry');
const {mockServerClient} = require('mockserver-client');
const IMAGE = 'jamesdbloom/mockserver:latest';
const MOCK_SERVER_PORT = 1080;
@@ -30,7 +30,7 @@ async function start() {
minTimeout: 1000,
factor: 2,
});
} catch (error) {
} catch (_) {
throw new Error(`Couldn't start mock-server after 2 min`);
}
}
@@ -100,4 +100,4 @@ function verify(expectation) {
return client.verify(expectation);
}
export default {start, stop, mock, verify, url};
module.exports = {start, stop, mock, verify, url};
+10 -10
View File
@@ -1,8 +1,8 @@
import Docker from 'dockerode';
import getStream from 'get-stream';
import got from 'got';
import delay from 'delay';
import pRetry from 'p-retry';
const Docker = require('dockerode');
const getStream = require('get-stream');
const got = require('got');
const delay = require('delay');
const pRetry = require('p-retry');
const IMAGE = 'semanticrelease/npm-registry-docker:latest';
const SERVER_PORT = 15986;
@@ -39,16 +39,16 @@ async function start() {
minTimeout: 1000,
factor: 2,
});
} catch (error) {
} catch (_) {
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: `${COUCHDB_USER}:${COUCHDB_PASSWORD}`,
username: COUCHDB_USER,
password: COUCHDB_PASSWORD,
method: 'PUT',
body: {
json: {
_id: `org.couchdb.user:${NPM_USERNAME}`,
name: NPM_USERNAME,
roles: [],
@@ -76,4 +76,4 @@ async function stop() {
await container.remove();
}
export default {start, stop, authEnv, url};
module.exports = {start, stop, authEnv, url};
+4 -2
View File
@@ -1,5 +1,7 @@
import execa from 'execa';
const execa = require('execa');
export async function npmView(packageName, env) {
async function npmView(packageName, env) {
return JSON.parse((await execa('npm', ['view', packageName, '--json'], {env})).stdout);
}
module.exports = {npmView};
+22 -12
View File
@@ -1,9 +1,9 @@
import test from 'ava';
import {repeat} from 'lodash';
import hideSensitive from '../lib/hide-sensitive';
import {SECRET_REPLACEMENT, SECRET_MIN_SIZE} from '../lib/definitions/constants';
const test = require('ava');
const {repeat} = require('lodash');
const hideSensitive = require('../lib/hide-sensitive');
const {SECRET_REPLACEMENT, SECRET_MIN_SIZE} = require('../lib/definitions/constants');
test('Replace multiple sensitive environment variable values', t => {
test('Replace multiple sensitive environment variable values', (t) => {
const env = {SOME_PASSWORD: 'password', SOME_TOKEN: 'secret'};
t.is(
hideSensitive(env)(`https://user:${env.SOME_PASSWORD}@host.com?token=${env.SOME_TOKEN}`),
@@ -11,7 +11,7 @@ test('Replace multiple sensitive environment variable values', t => {
);
});
test('Replace multiple occurences of sensitive environment variable values', t => {
test('Replace multiple occurences of sensitive environment variable values', (t) => {
const env = {secretKey: 'secret'};
t.is(
hideSensitive(env)(`https://user:${env.secretKey}@host.com?token=${env.secretKey}`),
@@ -19,7 +19,12 @@ test('Replace multiple occurences of sensitive environment variable values', t =
);
});
test('Escape regexp special characters', t => {
test('Replace sensitive environment variable matching specific regex for "private"', (t) => {
const env = {privateKey: 'secret', GOPRIVATE: 'host.com'};
t.is(hideSensitive(env)(`https://host.com?token=${env.privateKey}`), `https://host.com?token=${SECRET_REPLACEMENT}`);
});
test('Escape regexp special characters', (t) => {
const env = {SOME_CREDENTIALS: 'p$^{.+}\\w[a-z]o.*rd'};
t.is(
hideSensitive(env)(`https://user:${env.SOME_CREDENTIALS}@host.com`),
@@ -27,15 +32,15 @@ test('Escape regexp special characters', t => {
);
});
test('Accept "undefined" input', t => {
test('Accept "undefined" input', (t) => {
t.is(hideSensitive({})(), undefined);
});
test('Return same string if no environment variable has to be replaced', t => {
test('Return same string if no environment variable has to be replaced', (t) => {
t.is(hideSensitive({})('test'), 'test');
});
test('Exclude empty environment variables from the regexp', t => {
test('Exclude empty environment variables from the regexp', (t) => {
const env = {SOME_PASSWORD: 'password', SOME_TOKEN: ''};
t.is(
hideSensitive(env)(`https://user:${env.SOME_PASSWORD}@host.com?token=`),
@@ -43,11 +48,16 @@ test('Exclude empty environment variables from the regexp', t => {
);
});
test('Exclude empty environment variables from the regexp if there is only empty ones', t => {
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 => {
test('Exclude nonsensitive GOPRIVATE environment variable for Golang projects from the regexp', (t) => {
const env = {GOPRIVATE: 'host.com'};
t.is(hideSensitive(env)(`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};
+272 -143
View File
@@ -1,13 +1,13 @@
import test from 'ava';
import {escapeRegExp, isString, sortBy} from 'lodash';
import proxyquire from 'proxyquire';
import {spy, stub} from 'sinon';
import {WritableStreamBuffer} from 'stream-buffers';
import AggregateError from 'aggregate-error';
import SemanticReleaseError from '@semantic-release/error';
import {COMMIT_NAME, COMMIT_EMAIL, SECRET_REPLACEMENT} from '../lib/definitions/constants';
import {
gitHead as getGitHead,
const test = require('ava');
const {escapeRegExp, isString, sortBy, omit} = require('lodash');
const proxyquire = require('proxyquire');
const {spy, stub} = require('sinon');
const {WritableStreamBuffer} = require('stream-buffers');
const AggregateError = require('aggregate-error');
const SemanticReleaseError = require('@semantic-release/error');
const {COMMIT_NAME, COMMIT_EMAIL, SECRET_REPLACEMENT} = require('../lib/definitions/constants');
const {
gitHead: getGitHead,
gitCheckout,
gitTagHead,
gitRepo,
@@ -19,12 +19,14 @@ import {
merge,
mergeFf,
rebase,
} from './helpers/git-utils';
gitAddNote,
gitGetNote,
} = require('./helpers/git-utils');
const requireNoCache = proxyquire.noPreserveCache();
const pluginNoop = require.resolve('./fixtures/plugin-noop');
test.beforeEach(t => {
test.beforeEach((t) => {
// Stub the logger functions
t.context.log = spy();
t.context.error = spy();
@@ -39,13 +41,14 @@ test.beforeEach(t => {
};
});
test('Plugins are called with expected values', async t => {
test('Plugins are called with expected values', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd, repositoryUrl} = await gitRepo(true);
// Add commits to the master branch
let commits = await gitCommits(['First'], {cwd});
// Create the tag corresponding to version 1.0.0
await gitTagVersion('v1.0.0@next', undefined, {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
await gitAddNote(JSON.stringify({channels: ['next']}), 'v1.0.0', {cwd});
commits = (await gitCommits(['Second'], {cwd})).concat(commits);
await gitCheckout('next', true, {cwd});
await gitPush(repositoryUrl, 'next', {cwd});
@@ -55,9 +58,9 @@ test('Plugins are called with expected values', async t => {
const lastRelease = {
version: '1.0.0',
gitHead: commits[commits.length - 1].hash,
gitTag: 'v1.0.0@next',
gitTag: 'v1.0.0',
name: 'v1.0.0',
channel: 'next',
channels: ['next'],
};
const nextRelease = {
name: 'v1.1.0',
@@ -65,7 +68,7 @@ test('Plugins are called with expected values', async t => {
version: '1.1.0',
gitHead: await getGitHead({cwd}),
gitTag: 'v1.1.0',
channel: undefined,
channel: null,
};
const notes1 = 'Release notes 1';
const notes2 = 'Release notes 2';
@@ -94,18 +97,20 @@ test('Plugins are called with expected values', async t => {
{
channel: undefined,
name: 'master',
range: '>=1.0.0 <2.0.0',
accept: ['patch', 'minor'],
tags: [{channel: 'next', gitTag: 'v1.0.0@next', version: '1.0.0', gitHead: commits[commits.length - 1].hash}],
range: '>=1.0.0',
accept: ['patch', 'minor', 'major'],
tags: [{channels: ['next'], gitTag: 'v1.0.0', version: '1.0.0'}],
type: 'release',
main: true,
},
{
channel: 'next',
name: 'next',
range: '>=2.0.0',
range: '>=1.0.0',
accept: ['patch', 'minor', 'major'],
tags: [{channel: 'next', gitHead: commits[commits.length - 1].hash, gitTag: 'v1.0.0@next', version: '1.0.0'}],
tags: [{channels: ['next'], gitTag: 'v1.0.0', version: '1.0.0'}],
type: 'release',
main: false,
},
];
const branch = branches[0];
@@ -121,14 +126,15 @@ test('Plugins are called with expected values', async t => {
publish: [publish, pluginNoop],
success,
};
const envCi = {branch: 'master', isCi: true, isPr: false};
const releases = [
{
...lastRelease,
...omit(lastRelease, 'channels'),
...release1,
type: 'major',
version: '1.0.0',
channel: undefined,
channel: null,
gitTag: 'v1.0.0',
notes: `${notes1}\n\n${notes2}\n\n${notes3}`,
pluginName: '[Function: functionStub]',
@@ -139,7 +145,7 @@ test('Plugins are called with expected values', async t => {
const semanticRelease = requireNoCache('..', {
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
'env-ci': () => envCi,
});
const result = await semanticRelease(options, {
cwd,
@@ -155,6 +161,7 @@ test('Plugins are called with expected values', async t => {
t.deepEqual(verifyConditions1.args[0][1].branch, branch);
t.deepEqual(verifyConditions1.args[0][1].branches, branches);
t.deepEqual(verifyConditions1.args[0][1].logger, t.context.logger);
t.deepEqual(verifyConditions1.args[0][1].envCi, envCi);
t.is(verifyConditions2.callCount, 1);
t.deepEqual(verifyConditions2.args[0][0], config);
t.deepEqual(verifyConditions2.args[0][1].cwd, cwd);
@@ -162,6 +169,7 @@ test('Plugins are called with expected values', async t => {
t.deepEqual(verifyConditions2.args[0][1].branch, branch);
t.deepEqual(verifyConditions2.args[0][1].branches, branches);
t.deepEqual(verifyConditions2.args[0][1].logger, t.context.logger);
t.deepEqual(verifyConditions2.args[0][1].envCi, envCi);
t.is(generateNotes1.callCount, 2);
t.is(generateNotes2.callCount, 2);
@@ -176,13 +184,14 @@ test('Plugins are called with expected values', async t => {
t.deepEqual(generateNotes1.args[0][1].commits[0].hash, commits[1].hash);
t.deepEqual(generateNotes1.args[0][1].commits[0].message, commits[1].message);
t.deepEqual(generateNotes1.args[0][1].nextRelease, {
...lastRelease,
...omit(lastRelease, 'channels'),
type: 'major',
version: '1.0.0',
channel: undefined,
channel: null,
gitTag: 'v1.0.0',
name: 'v1.0.0',
});
t.deepEqual(generateNotes2.args[0][1].envCi, envCi);
t.deepEqual(generateNotes2.args[0][0], config);
t.deepEqual(generateNotes2.args[0][1].options, options);
@@ -193,14 +202,15 @@ test('Plugins are called with expected values', async t => {
t.deepEqual(generateNotes2.args[0][1].commits[0].hash, commits[1].hash);
t.deepEqual(generateNotes2.args[0][1].commits[0].message, commits[1].message);
t.deepEqual(generateNotes2.args[0][1].nextRelease, {
...lastRelease,
...omit(lastRelease, 'channels'),
type: 'major',
version: '1.0.0',
channel: undefined,
channel: null,
gitTag: 'v1.0.0',
name: 'v1.0.0',
notes: notes1,
});
t.deepEqual(generateNotes2.args[0][1].envCi, envCi);
t.deepEqual(generateNotes3.args[0][0], config);
t.deepEqual(generateNotes3.args[0][1].options, options);
@@ -211,18 +221,19 @@ test('Plugins are called with expected values', async t => {
t.deepEqual(generateNotes3.args[0][1].commits[0].hash, commits[1].hash);
t.deepEqual(generateNotes3.args[0][1].commits[0].message, commits[1].message);
t.deepEqual(generateNotes3.args[0][1].nextRelease, {
...lastRelease,
...omit(lastRelease, 'channels'),
type: 'major',
version: '1.0.0',
channel: undefined,
channel: null,
gitTag: 'v1.0.0',
name: 'v1.0.0',
notes: `${notes1}\n\n${notes2}`,
});
t.deepEqual(generateNotes3.args[0][1].envCi, envCi);
branch.tags.push({
version: '1.0.0',
channel: undefined,
channel: null,
gitTag: 'v1.0.0',
gitHead: commits[commits.length - 1].hash,
});
@@ -236,16 +247,17 @@ test('Plugins are called with expected values', async t => {
t.deepEqual(addChannel.args[0][1].lastRelease, {});
t.deepEqual(addChannel.args[0][1].currentRelease, {...lastRelease, type: 'major'});
t.deepEqual(addChannel.args[0][1].nextRelease, {
...lastRelease,
...omit(lastRelease, 'channels'),
type: 'major',
version: '1.0.0',
channel: undefined,
channel: null,
gitTag: 'v1.0.0',
name: 'v1.0.0',
notes: `${notes1}\n\n${notes2}\n\n${notes3}`,
});
t.deepEqual(addChannel.args[0][1].commits[0].hash, commits[1].hash);
t.deepEqual(addChannel.args[0][1].commits[0].message, commits[1].message);
t.deepEqual(addChannel.args[0][1].envCi, envCi);
t.is(analyzeCommits.callCount, 1);
t.deepEqual(analyzeCommits.args[0][0], config);
@@ -256,6 +268,7 @@ test('Plugins are called with expected values', async t => {
t.deepEqual(analyzeCommits.args[0][1].lastRelease, lastRelease);
t.deepEqual(analyzeCommits.args[0][1].commits[0].hash, commits[0].hash);
t.deepEqual(analyzeCommits.args[0][1].commits[0].message, commits[0].message);
t.deepEqual(analyzeCommits.args[0][1].envCi, envCi);
t.is(verifyRelease.callCount, 1);
t.deepEqual(verifyRelease.args[0][0], config);
@@ -267,6 +280,7 @@ test('Plugins are called with expected values', async t => {
t.deepEqual(verifyRelease.args[0][1].commits[0].hash, commits[0].hash);
t.deepEqual(verifyRelease.args[0][1].commits[0].message, commits[0].message);
t.deepEqual(verifyRelease.args[0][1].nextRelease, nextRelease);
t.deepEqual(verifyRelease.args[0][1].envCi, envCi);
t.deepEqual(generateNotes1.args[1][0], config);
t.deepEqual(generateNotes1.args[1][1].options, options);
@@ -277,6 +291,7 @@ test('Plugins are called with expected values', async t => {
t.deepEqual(generateNotes1.args[1][1].commits[0].hash, commits[0].hash);
t.deepEqual(generateNotes1.args[1][1].commits[0].message, commits[0].message);
t.deepEqual(generateNotes1.args[1][1].nextRelease, nextRelease);
t.deepEqual(generateNotes1.args[1][1].envCi, envCi);
t.deepEqual(generateNotes2.args[1][0], config);
t.deepEqual(generateNotes2.args[1][1].options, options);
@@ -287,6 +302,7 @@ test('Plugins are called with expected values', async t => {
t.deepEqual(generateNotes2.args[1][1].commits[0].hash, commits[0].hash);
t.deepEqual(generateNotes2.args[1][1].commits[0].message, commits[0].message);
t.deepEqual(generateNotes2.args[1][1].nextRelease, {...nextRelease, notes: notes1});
t.deepEqual(generateNotes2.args[1][1].envCi, envCi);
t.deepEqual(generateNotes3.args[1][0], config);
t.deepEqual(generateNotes3.args[1][1].options, options);
@@ -297,6 +313,7 @@ test('Plugins are called with expected values', async t => {
t.deepEqual(generateNotes3.args[1][1].commits[0].hash, commits[0].hash);
t.deepEqual(generateNotes3.args[1][1].commits[0].message, commits[0].message);
t.deepEqual(generateNotes3.args[1][1].nextRelease, {...nextRelease, notes: `${notes1}\n\n${notes2}`});
t.deepEqual(generateNotes3.args[1][1].envCi, envCi);
t.is(prepare.callCount, 1);
t.deepEqual(prepare.args[0][0], config);
@@ -308,6 +325,7 @@ test('Plugins are called with expected values', async t => {
t.deepEqual(prepare.args[0][1].commits[0].hash, commits[0].hash);
t.deepEqual(prepare.args[0][1].commits[0].message, commits[0].message);
t.deepEqual(prepare.args[0][1].nextRelease, {...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`});
t.deepEqual(prepare.args[0][1].envCi, envCi);
t.is(publish.callCount, 1);
t.deepEqual(publish.args[0][0], config);
@@ -319,6 +337,7 @@ test('Plugins are called with expected values', async t => {
t.deepEqual(publish.args[0][1].commits[0].hash, commits[0].hash);
t.deepEqual(publish.args[0][1].commits[0].message, commits[0].message);
t.deepEqual(publish.args[0][1].nextRelease, {...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`});
t.deepEqual(publish.args[0][1].envCi, envCi);
t.is(success.callCount, 2);
t.deepEqual(success.args[0][0], config);
@@ -330,15 +349,16 @@ test('Plugins are called with expected values', async t => {
t.deepEqual(success.args[0][1].commits[0].hash, commits[1].hash);
t.deepEqual(success.args[0][1].commits[0].message, commits[1].message);
t.deepEqual(success.args[0][1].nextRelease, {
...lastRelease,
...omit(lastRelease, 'channels'),
type: 'major',
version: '1.0.0',
channel: undefined,
channel: null,
gitTag: 'v1.0.0',
name: 'v1.0.0',
notes: `${notes1}\n\n${notes2}\n\n${notes3}`,
});
t.deepEqual(success.args[0][1].releases, [releases[0]]);
t.deepEqual(success.args[0][1].envCi, envCi);
t.deepEqual(success.args[1][0], config);
t.deepEqual(success.args[1][1].options, options);
@@ -350,6 +370,7 @@ test('Plugins are called with expected values', async t => {
t.deepEqual(success.args[1][1].commits[0].message, commits[0].message);
t.deepEqual(success.args[1][1].nextRelease, {...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`});
t.deepEqual(success.args[1][1].releases, [releases[1], releases[2]]);
t.deepEqual(success.args[1][1].envCi, envCi);
t.deepEqual(result, {
lastRelease,
@@ -369,7 +390,7 @@ test('Plugins are called with expected values', async t => {
t.is(env.GIT_COMMITTER_EMAIL, COMMIT_EMAIL);
});
test('Use custom tag format', async t => {
test('Use custom tag format', async (t) => {
const {cwd, repositoryUrl} = await gitRepo(true);
await gitCommits(['First'], {cwd});
await gitTagVersion('test-1.0.0', undefined, {cwd});
@@ -416,7 +437,7 @@ test('Use custom tag format', async t => {
t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag, {cwd}), nextRelease.gitHead);
});
test('Use new gitHead, and recreate release notes if a prepare plugin create a commit', async t => {
test('Use new gitHead, and recreate release notes if a prepare plugin create a commit', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd, repositoryUrl} = await gitRepo(true);
// Add commits to the master branch
@@ -433,7 +454,7 @@ test('Use new gitHead, and recreate release notes if a prepare plugin create a c
version: '2.0.0',
gitHead: await getGitHead({cwd}),
gitTag: 'v2.0.0',
channel: undefined,
channel: null,
};
const notes = 'Release notes';
@@ -490,17 +511,18 @@ test('Use new gitHead, and recreate release notes if a prepare plugin create a c
t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag, {cwd}), commits[0].hash);
});
test('Make a new release when a commit is forward-ported to an upper branch', async t => {
test('Make a new release when a commit is forward-ported to an upper branch', async (t) => {
const {cwd, repositoryUrl} = await gitRepo(true);
const commits = await gitCommits(['feat: initial release'], {cwd});
await gitCommits(['feat: initial release'], {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
await gitTagVersion('v1.0.0@1.0.x', undefined, {cwd});
await gitAddNote(JSON.stringify({channels: [null, '1.0.x']}), 'v1.0.0', {cwd});
await gitCheckout('1.0.x', true, {cwd});
commits.push(...(await gitCommits(['fix: fix on maintenance version 1.0.x'], {cwd})));
await gitTagVersion('v1.0.1@1.0.x', undefined, {cwd});
await gitCommits(['fix: fix on maintenance version 1.0.x'], {cwd});
await gitTagVersion('v1.0.1', undefined, {cwd});
await gitAddNote(JSON.stringify({channels: ['1.0.x']}), 'v1.0.1', {cwd});
await gitPush('origin', '1.0.x', {cwd});
await gitCheckout('master', false, {cwd});
commits.push(...(await gitCommits(['feat: new feature on master'], {cwd})));
await gitCommits(['feat: new feature on master'], {cwd});
await gitTagVersion('v1.1.0', undefined, {cwd});
await merge('1.0.x', {cwd});
await gitPush('origin', 'master', {cwd});
@@ -536,7 +558,7 @@ test('Make a new release when a commit is forward-ported to an upper branch', as
t.is(success.callCount, 1);
});
test('Publish a pre-release version', async t => {
test('Publish a pre-release version', async (t) => {
const {cwd, repositoryUrl} = await gitRepo(true);
await gitCommits(['feat: initial commit'], {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
@@ -566,7 +588,8 @@ test('Publish a pre-release version', async t => {
t.is(releases.length, 1);
t.is(releases[0].version, '1.1.0-beta.1');
t.is(releases[0].gitTag, 'v1.1.0-beta.1@beta');
t.is(releases[0].gitTag, 'v1.1.0-beta.1');
t.is(await gitGetNote('v1.1.0-beta.1', {cwd}), '{"channels":["beta"]}');
await gitCommits(['fix: a fix'], {cwd});
({releases} = await semanticRelease(options, {
@@ -578,19 +601,133 @@ test('Publish a pre-release version', async t => {
t.is(releases.length, 1);
t.is(releases[0].version, '1.1.0-beta.2');
t.is(releases[0].gitTag, 'v1.1.0-beta.2@beta');
t.is(releases[0].gitTag, 'v1.1.0-beta.2');
t.is(await gitGetNote('v1.1.0-beta.2', {cwd}), '{"channels":["beta"]}');
});
test('Do not add pre-releases to a different channel', async t => {
test('Publish releases from different branch on the same channel', async (t) => {
const {cwd, repositoryUrl} = await gitRepo(true);
const commits = await gitCommits(['feat: initial release'], {cwd});
await gitCommits(['feat: initial commit'], {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
await gitTagVersion('v1.0.0@beta', undefined, {cwd});
await gitPush(repositoryUrl, 'master', {cwd});
await gitCheckout('next-major', true, {cwd});
await gitPush(repositoryUrl, 'next-major', {cwd});
await gitCheckout('next', true, {cwd});
await gitCommits(['feat: a feature'], {cwd});
await gitPush(repositoryUrl, 'next', {cwd});
const config = {
branches: ['master', {name: 'next', channel: false}, {name: 'next-major', channel: false}],
repositoryUrl,
};
const addChannel = stub().resolves({});
const options = {
...config,
verifyConditions: stub().resolves(),
verifyRelease: stub().resolves(),
generateNotes: stub().resolves(''),
addChannel,
prepare: stub().resolves(),
publish: stub().resolves(),
success: stub().resolves(),
fail: stub().resolves(),
};
let semanticRelease = requireNoCache('..', {
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'next', isPr: false}),
});
let {releases} = await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}});
t.is(releases.length, 1);
t.is(releases[0].version, '1.1.0');
t.is(releases[0].gitTag, 'v1.1.0');
await gitCommits(['fix: a fix'], {cwd});
({releases} = await semanticRelease(options, {
cwd,
env: {},
stdout: {write: () => {}},
stderr: {write: () => {}},
}));
t.is(releases.length, 1);
t.is(releases[0].version, '1.1.1');
t.is(releases[0].gitTag, 'v1.1.1');
await gitCheckout('master', false, {cwd});
await merge('next', {cwd});
await gitPush('origin', 'master', {cwd});
semanticRelease = requireNoCache('..', {
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
t.falsy(await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}));
t.is(addChannel.callCount, 0);
});
test('Publish pre-releases the same channel as regular releases', async (t) => {
const {cwd, repositoryUrl} = await gitRepo(true);
await gitCommits(['feat: initial commit'], {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
await gitPush(repositoryUrl, 'master', {cwd});
await gitCheckout('beta', true, {cwd});
commits.push(...(await gitCommits(['feat: breaking change/n/nBREAKING CHANGE: break something'], {cwd})));
await gitTagVersion('v2.0.0-beta.1@beta', undefined, {cwd});
commits.push(...(await gitCommits(['fix: a fix'], {cwd})));
await gitTagVersion('v2.0.0-beta.2@beta', undefined, {cwd});
await gitCommits(['feat: a feature'], {cwd});
await gitPush(repositoryUrl, 'beta', {cwd});
const config = {
branches: ['master', {name: 'beta', channel: false, prerelease: true}],
repositoryUrl,
};
const options = {
...config,
verifyConditions: stub().resolves(),
verifyRelease: stub().resolves(),
generateNotes: stub().resolves(''),
addChannel: false,
prepare: stub().resolves(),
publish: stub().resolves(),
success: stub().resolves(),
fail: stub().resolves(),
};
const semanticRelease = requireNoCache('..', {
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'beta', isPr: false}),
});
let {releases} = await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}});
t.is(releases.length, 1);
t.is(releases[0].version, '1.1.0-beta.1');
t.is(releases[0].gitTag, 'v1.1.0-beta.1');
await gitCommits(['fix: a fix'], {cwd});
({releases} = await semanticRelease(options, {
cwd,
env: {},
stdout: {write: () => {}},
stderr: {write: () => {}},
}));
t.is(releases.length, 1);
t.is(releases[0].version, '1.1.0-beta.2');
t.is(releases[0].gitTag, 'v1.1.0-beta.2');
});
test('Do not add pre-releases to a different channel', async (t) => {
const {cwd, repositoryUrl} = await gitRepo(true);
await gitCommits(['feat: initial release'], {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
await gitAddNote(JSON.stringify({channels: [null, 'beta']}), 'v1.0.0', {cwd});
await gitCheckout('beta', true, {cwd});
await gitCommits(['feat: breaking change/n/nBREAKING CHANGE: break something'], {cwd});
await gitTagVersion('v2.0.0-beta.1', undefined, {cwd});
await gitAddNote(JSON.stringify({channels: ['beta']}), 'v2.0.0-beta.1', {cwd});
await gitCommits(['fix: a fix'], {cwd});
await gitTagVersion('v2.0.0-beta.2', undefined, {cwd});
await gitAddNote(JSON.stringify({channels: ['beta']}), 'v2.0.0-beta.2', {cwd});
await gitPush('origin', 'beta', {cwd});
await gitCheckout('master', false, {cwd});
await merge('beta', {cwd});
@@ -635,14 +772,18 @@ async function addChannelMacro(t, mergeFunction) {
const {cwd, repositoryUrl} = await gitRepo(true);
const commits = await gitCommits(['feat: initial release'], {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
await gitTagVersion('v1.0.0@next', undefined, {cwd});
await gitAddNote(JSON.stringify({channels: [null, 'next']}), 'v1.0.0', {cwd});
await gitCheckout('next', true, {cwd});
commits.push(...(await gitCommits(['feat: breaking change/n/nBREAKING CHANGE: break something'], {cwd})));
await gitTagVersion('v2.0.0@next', undefined, {cwd});
await gitTagVersion('v2.0.0', undefined, {cwd});
await gitAddNote(JSON.stringify({channels: ['next']}), 'v2.0.0', {cwd});
commits.push(...(await gitCommits(['fix: a fix'], {cwd})));
await gitTagVersion('v2.0.1@next', undefined, {cwd});
await gitTagVersion('v2.0.1', undefined, {cwd});
await gitAddNote(JSON.stringify({channels: ['next']}), 'v2.0.1', {cwd});
commits.push(...(await gitCommits(['feat: a feature'], {cwd})));
await gitTagVersion('v2.1.0@next', undefined, {cwd});
await gitTagVersion('v2.1.0', undefined, {cwd});
await gitAddNote(JSON.stringify({channels: ['next']}), 'v2.1.0', {cwd});
await gitPush('origin', 'next', {cwd});
await gitCheckout('master', false, {cwd});
// Merge all commits but last one from next to master
@@ -660,7 +801,14 @@ async function addChannelMacro(t, mergeFunction) {
const publish = stub().resolves();
const success = stub().resolves();
const config = {branches: [{name: 'master'}, {name: 'next'}], repositoryUrl, tagFormat: `v\${version}`};
const config = {
branches: [
{name: 'master', channel: 'latest'},
{name: 'next', channel: 'next'},
],
repositoryUrl,
tagFormat: `v\${version}`,
};
const options = {
...config,
verifyConditions,
@@ -671,19 +819,11 @@ async function addChannelMacro(t, mergeFunction) {
publish,
success,
};
const nextRelease1 = {
name: 'v2.0.0',
type: 'major',
version: '2.0.0',
channel: undefined,
gitTag: 'v2.0.0',
gitHead: commits[1].hash,
};
const nextRelease2 = {
const nextRelease = {
name: 'v2.0.1',
type: 'patch',
version: '2.0.1',
channel: undefined,
channel: 'latest',
gitTag: 'v2.0.1',
gitHead: commits[2].hash,
};
@@ -695,26 +835,22 @@ async function addChannelMacro(t, mergeFunction) {
const result = await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}});
t.deepEqual(result.releases, [
{...nextRelease1, ...release1, notes, pluginName: '[Function: functionStub]'},
{...nextRelease1, notes, pluginName: '[Function: functionStub]'},
{...nextRelease2, ...release1, notes, pluginName: '[Function: functionStub]'},
{...nextRelease2, notes, pluginName: '[Function: functionStub]'},
{...nextRelease, ...release1, notes, pluginName: '[Function: functionStub]'},
{...nextRelease, notes, pluginName: '[Function: functionStub]'},
]);
// Verify the tag has been created on the local and remote repo and reference
t.is(await gitTagHead(nextRelease1.gitTag, {cwd}), nextRelease1.gitHead);
t.is(await gitRemoteTagHead(repositoryUrl, nextRelease1.gitTag, {cwd}), nextRelease1.gitHead);
t.is(await gitTagHead(nextRelease2.gitTag, {cwd}), nextRelease2.gitHead);
t.is(await gitRemoteTagHead(repositoryUrl, nextRelease2.gitTag, {cwd}), nextRelease2.gitHead);
t.is(await gitTagHead(nextRelease.gitTag, {cwd}), nextRelease.gitHead);
t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag, {cwd}), nextRelease.gitHead);
}
addChannelMacro.title = providedTitle => `Add version to a channel after a merge (${providedTitle})`;
addChannelMacro.title = (providedTitle) => `Add version to a channel after a merge (${providedTitle})`;
test('fast-forward', addChannelMacro, mergeFf);
test('non fast-forward', addChannelMacro, merge);
test('rebase', addChannelMacro, rebase);
test('Call all "success" plugins even if one errors out', async t => {
test('Call all "success" plugins even if one errors out', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd, repositoryUrl} = await gitRepo(true);
// Add commits to the master branch
@@ -731,7 +867,7 @@ test('Call all "success" plugins even if one errors out', async t => {
version: '2.0.0',
gitHead: await getGitHead({cwd}),
gitTag: 'v2.0.0',
channel: undefined,
channel: null,
};
const notes = 'Release notes';
const verifyConditions1 = stub().resolves();
@@ -780,7 +916,7 @@ test('Call all "success" plugins even if one errors out', async t => {
]);
});
test('Log all "verifyConditions" errors', async t => {
test('Log all "verifyConditions" errors', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd, repositoryUrl} = await gitRepo(true);
// Add commits to the master branch
@@ -821,7 +957,7 @@ test('Log all "verifyConditions" errors', async t => {
t.deepEqual(fail.args[0][1].errors, [error2, error3]);
});
test('Log all "verifyRelease" errors', async t => {
test('Log all "verifyRelease" errors', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd, repositoryUrl} = await gitRepo(true);
// Add commits to the master branch
@@ -862,13 +998,15 @@ test('Log all "verifyRelease" errors', async t => {
t.deepEqual(fail.args[0][1].errors, [error1, error2]);
});
test('Dry-run skips addChannel, prepare, publish and success', async t => {
test('Dry-run skips addChannel, prepare, publish and success', async (t) => {
const {cwd, repositoryUrl} = await gitRepo(true);
await gitCommits(['First'], {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
await gitTagVersion('v1.0.0@next', undefined, {cwd});
await gitTagVersion('v1.1.0@next', undefined, {cwd});
await gitAddNote(JSON.stringify({channels: [null, 'next']}), 'v1.0.0', {cwd});
await gitCommits(['Second'], {cwd});
await gitTagVersion('v1.1.0', undefined, {cwd});
await gitAddNote(JSON.stringify({channels: ['next']}), 'v1.1.0', {cwd});
await gitPush(repositoryUrl, 'master', {cwd});
await gitCheckout('next', true, {cwd});
await gitPush('origin', 'next', {cwd});
@@ -926,7 +1064,7 @@ test('Dry-run skips addChannel, prepare, publish and success', async t => {
t.true(t.context.warn.calledWith(`Skip step "success" of plugin "[Function: ${success.name}]" in dry-run mode`));
});
test('Dry-run skips fail', async t => {
test('Dry-run skips fail', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd, repositoryUrl} = await gitRepo(true);
// Add commits to the master branch
@@ -966,7 +1104,7 @@ test('Dry-run skips fail', async t => {
t.true(t.context.warn.calledWith(`Skip step "fail" of plugin "[Function: ${fail.name}]" in dry-run mode`));
});
test('Force a dry-run if not on a CI and "noCi" is not explicitly set', async t => {
test('Force a dry-run if not on a CI and "noCi" is not explicitly set', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd, repositoryUrl} = await gitRepo(true);
// Add commits to the master branch
@@ -1031,7 +1169,7 @@ test('Force a dry-run if not on a CI and "noCi" is not explicitly set', async t
t.is(success.callCount, 0);
});
test('Dry-run does not print changelog if "generateNotes" return "undefined"', async t => {
test('Dry-run does not print changelog if "generateNotes" return "undefined"', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd, repositoryUrl} = await gitRepo(true);
// Add commits to the master branch
@@ -1075,7 +1213,7 @@ test('Dry-run does not print changelog if "generateNotes" return "undefined"', a
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['Release note for version 2.0.0:']);
});
test('Allow local releases with "noCi" option', async t => {
test('Allow local releases with "noCi" option', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd, repositoryUrl} = await gitRepo(true);
// Add commits to the master branch
@@ -1120,7 +1258,7 @@ test('Allow local releases with "noCi" option', async t => {
const semanticRelease = requireNoCache('..', {
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: false, branch: 'master', isPr: true}),
'env-ci': () => ({isCi: false, branch: 'master', isPr: false}),
});
t.truthy(
await semanticRelease(options, {
@@ -1144,12 +1282,14 @@ test('Allow local releases with "noCi" option', async t => {
t.is(success.callCount, 1);
});
test('Accept "undefined" value returned by "generateNotes" and "false" by "publish" and "addChannel"', async t => {
test('Accept "undefined" value returned by "generateNotes" and "false" by "publish" and "addChannel"', async (t) => {
const {cwd, repositoryUrl} = await gitRepo(true);
await gitCommits(['First'], {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
await gitTagVersion('v1.0.0@next', undefined, {cwd});
await gitTagVersion('v1.1.0@next', undefined, {cwd});
await gitAddNote(JSON.stringify({channels: [null, 'next']}), 'v1.0.0', {cwd});
await gitCommits(['Second'], {cwd});
await gitTagVersion('v1.1.0', undefined, {cwd});
await gitAddNote(JSON.stringify({channels: ['next']}), 'v1.1.0', {cwd});
await gitPush(repositoryUrl, 'master', {cwd});
await gitCheckout('next', true, {cwd});
await gitPush('origin', 'next', {cwd});
@@ -1161,7 +1301,7 @@ test('Accept "undefined" value returned by "generateNotes" and "false" by "publi
version: '1.2.0',
gitHead: await getGitHead({cwd}),
gitTag: 'v1.2.0',
channel: undefined,
channel: null,
};
const analyzeCommits = stub().resolves(nextRelease.type);
const verifyRelease = stub().resolves();
@@ -1211,13 +1351,13 @@ test('Accept "undefined" value returned by "generateNotes" and "false" by "publi
t.deepEqual(success.args[1][1].releases, [{pluginName: '[Function: functionStub]'}]);
});
test('Returns false if triggered by a PR', async t => {
test('Returns false if triggered by a PR', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd, repositoryUrl} = await gitRepo(true);
const semanticRelease = requireNoCache('..', {
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: true}),
'env-ci': () => ({isCi: true, branch: 'master', prBranch: 'patch-1', isPr: true}),
});
t.false(
@@ -1232,18 +1372,18 @@ test('Returns false if triggered by a PR', async t => {
);
});
test('Throws "EINVALIDNEXTVERSION" if next release is out of range of the current maintenance branch', async t => {
test('Throws "EINVALIDNEXTVERSION" if next release is out of range of the current maintenance branch', async (t) => {
const {cwd, repositoryUrl} = await gitRepo(true);
const commits = await gitCommits(['feat: initial commit'], {cwd});
await gitCommits(['feat: initial commit'], {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
await gitTagVersion('v1.0.0@1.x', undefined, {cwd});
await gitAddNote(JSON.stringify({channels: [null, '1.x']}), 'v1.0.0', {cwd});
await gitCheckout('1.x', true, {cwd});
await gitPush('origin', '1.x', {cwd});
await gitCheckout('master', false, {cwd});
commits.push(...(await gitCommits(['feat: new feature on master'], {cwd})));
await gitCommits(['feat: new feature on master'], {cwd});
await gitTagVersion('v1.1.0', undefined, {cwd});
await gitCheckout('1.x', false, {cwd});
commits.push(...(await gitCommits(['feat: feature on maintenance version 1.x'], {cwd})));
await gitCommits(['feat: feature on maintenance version 1.x'], {cwd});
await gitPush('origin', 'master', {cwd});
const verifyConditions = stub().resolves();
@@ -1283,18 +1423,19 @@ test('Throws "EINVALIDNEXTVERSION" if next release is out of range of the curren
t.regex(error.details, /A valid branch could be `master`./);
});
test('Throws "EINVALIDNEXTVERSION" if next release is out of range of the current release branch', async t => {
test('Throws "EINVALIDNEXTVERSION" if next release is out of range of the current release branch', async (t) => {
const {cwd, repositoryUrl} = await gitRepo(true);
const commits = await gitCommits(['feat: initial commit'], {cwd});
await gitCommits(['feat: initial commit'], {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
await gitCheckout('next', true, {cwd});
commits.push(...(await gitCommits(['feat: new feature on next'], {cwd})));
await gitTagVersion('v1.1.0@next', undefined, {cwd});
await gitCommits(['feat: new feature on next'], {cwd});
await gitTagVersion('v1.1.0', undefined, {cwd});
await gitAddNote(JSON.stringify({channels: ['next']}), 'v1.1.0', {cwd});
await gitPush('origin', 'next', {cwd});
await gitCheckout('next-major', true, {cwd});
await gitPush('origin', 'next-major', {cwd});
await gitCheckout('master', false, {cwd});
commits.push(...(await gitCommits(['feat: new feature on master', 'fix: new fix on master'], {cwd})));
await gitCommits(['feat: new feature on master', 'fix: new fix on master'], {cwd});
await gitPush('origin', 'master', {cwd});
const verifyConditions = stub().resolves();
@@ -1334,20 +1475,20 @@ test('Throws "EINVALIDNEXTVERSION" if next release is out of range of the curren
t.regex(error.details, /A valid branch could be `next` or `next-major`./);
});
test('Throws "EINVALIDMAINTENANCEMERGE" if merge an out of range release in a maintenance branch', async t => {
test('Throws "EINVALIDMAINTENANCEMERGE" if merge an out of range release in a maintenance branch', async (t) => {
const {cwd, repositoryUrl} = await gitRepo(true);
const commits = await gitCommits(['First'], {cwd});
await gitCommits(['First'], {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
await gitTagVersion('v1.0.0@1.1.x', undefined, {cwd});
commits.push(...(await gitCommits(['Second'], {cwd})));
await gitAddNote(JSON.stringify({channels: [null, '1.1.x']}), 'v1.0.0', {cwd});
await gitCommits(['Second'], {cwd});
await gitTagVersion('v1.1.0', undefined, {cwd});
await gitTagVersion('v1.1.0@1.1.x', undefined, {cwd});
await gitAddNote(JSON.stringify({channels: [null, '1.1.x']}), 'v1.1.0', {cwd});
await gitCheckout('1.1.x', 'master', {cwd});
await gitPush('origin', '1.1.x', {cwd});
await gitCheckout('master', false, {cwd});
commits.push(...(await gitCommits(['Third'], {cwd})));
await gitCommits(['Third'], {cwd});
await gitTagVersion('v1.1.1', undefined, {cwd});
commits.push(...(await gitCommits(['Fourth'], {cwd})));
await gitCommits(['Fourth'], {cwd});
await gitTagVersion('v1.2.0', undefined, {cwd});
await gitPush('origin', 'master', {cwd});
await gitCheckout('1.1.x', false, {cwd});
@@ -1379,15 +1520,6 @@ test('Throws "EINVALIDMAINTENANCEMERGE" if merge an out of range release in a ma
fail,
};
const nextRelease = {
type: 'patch',
version: '1.1.1',
channel: '1.1.x',
gitTag: 'v1.1.1@1.1.x',
name: 'v1.1.1',
gitHead: commits[2].hash,
};
const semanticRelease = proxyquire('..', {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: '1.1.x', isPr: false}),
@@ -1398,13 +1530,11 @@ test('Throws "EINVALIDMAINTENANCEMERGE" if merge an out of range release in a ma
)),
];
t.is(addChannel.callCount, 1);
t.deepEqual(addChannel.args[0][1].nextRelease, {...nextRelease, notes});
t.is(addChannel.callCount, 0);
t.is(publish.callCount, 0);
t.is(success.callCount, 1);
t.deepEqual(success.args[0][1].releases, [{...nextRelease, notes, pluginName: '[Function: functionStub]'}]);
t.is(success.callCount, 0);
t.is(fail.callCount, 1);
t.deepEqual(fail.args[0][1].errors, errors);
@@ -1415,7 +1545,7 @@ test('Throws "EINVALIDMAINTENANCEMERGE" if merge an out of range release in a ma
t.truthy(errors[0].details);
});
test('Returns false value if triggered on an outdated clone', async t => {
test('Returns false value if triggered on an outdated clone', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
let {cwd, repositoryUrl} = await gitRepo(true);
const repoDir = cwd;
@@ -1443,7 +1573,7 @@ test('Returns false value if triggered on an outdated clone', async t => {
]);
});
test('Returns false if not running from the configured branch', async t => {
test('Returns false if not running from the configured branch', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd, repositoryUrl} = await gitRepo(true);
const options = {
@@ -1479,7 +1609,7 @@ test('Returns false if not running from the configured branch', async t => {
);
});
test('Returns false if there is no relevant changes', async t => {
test('Returns false if there is no relevant changes', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd, repositoryUrl} = await gitRepo(true);
// Add commits to the master branch
@@ -1528,7 +1658,7 @@ test('Returns false if there is no relevant changes', async t => {
);
});
test('Exclude commits with [skip release] or [release skip] from analysis', async t => {
test('Exclude commits with [skip release] or [release skip] from analysis', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd, repositoryUrl} = await gitRepo(true);
// Add commits to the master branch
@@ -1577,7 +1707,7 @@ test('Exclude commits with [skip release] or [release skip] from analysis', asyn
t.deepEqual(analyzeCommits.args[0][1].commits[0], commits[commits.length - 1]);
});
test('Log both plugins errors and errors thrown by "fail" plugin', async t => {
test('Log both plugins errors and errors thrown by "fail" plugin', async (t) => {
const {cwd, repositoryUrl} = await gitRepo(true);
const pluginError = new SemanticReleaseError('Plugin error', 'ERR');
const failError1 = new Error('Fail error 1');
@@ -1603,7 +1733,7 @@ test('Log both plugins errors and errors thrown by "fail" plugin', async t => {
t.is(t.context.error.args[t.context.error.args.length - 2][1], failError2);
});
test('Call "fail" only if a plugin returns a SemanticReleaseError', async t => {
test('Call "fail" only if a plugin returns a SemanticReleaseError', async (t) => {
const {cwd, repositoryUrl} = await gitRepo(true);
const pluginError = new Error('Plugin error');
const fail = stub().resolves();
@@ -1627,7 +1757,7 @@ test('Call "fail" only if a plugin returns a SemanticReleaseError', async t => {
t.is(t.context.error.args[t.context.error.args.length - 1][1], pluginError);
});
test('Throw SemanticReleaseError if repositoryUrl is not set and cannot be found from repo config', async t => {
test('Throw SemanticReleaseError if repositoryUrl is not set and cannot be found from repo config', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
@@ -1648,7 +1778,7 @@ test('Throw SemanticReleaseError if repositoryUrl is not set and cannot be found
t.truthy(errors[0].details);
});
test('Throw an Error if plugin returns an unexpected value', async t => {
test('Throw an Error if plugin returns an unexpected value', async (t) => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd, repositoryUrl} = await gitRepo(true);
// Add commits to the master branch
@@ -1677,12 +1807,12 @@ test('Throw an Error if plugin returns an unexpected value', async t => {
});
const error = await t.throwsAsync(
semanticRelease(options, {cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()}),
Error
{instanceOf: SemanticReleaseError}
);
t.regex(error.details, /string/);
});
test('Hide sensitive information passed to "fail" plugin', async t => {
test('Hide sensitive information passed to "fail" plugin', async (t) => {
const {cwd, repositoryUrl} = await gitRepo(true);
const fail = stub().resolves();
@@ -1706,8 +1836,7 @@ test('Hide sensitive information passed to "fail" plugin', async t => {
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
await t.throwsAsync(
semanticRelease(options, {cwd, env, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()}),
Error
semanticRelease(options, {cwd, env, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()})
);
const error = fail.args[0][1].errors[0];
@@ -1715,14 +1844,14 @@ test('Hide sensitive information passed to "fail" plugin', async t => {
t.is(error.message, `Message: Exposing token ${SECRET_REPLACEMENT}`);
t.is(error.details, `Details: Exposing token ${SECRET_REPLACEMENT}`);
Object.getOwnPropertyNames(error).forEach(prop => {
Object.getOwnPropertyNames(error).forEach((prop) => {
if (isString(error[prop])) {
t.notRegex(error[prop], new RegExp(escapeRegExp(env.MY_TOKEN)));
}
});
});
test('Hide sensitive information passed to "success" plugin', async t => {
test('Hide sensitive information passed to "success" plugin', async (t) => {
const {cwd, repositoryUrl} = await gitRepo(true);
await gitCommits(['feat: initial release'], {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
@@ -1758,14 +1887,14 @@ test('Hide sensitive information passed to "success" plugin', async t => {
t.is(release.name, `Name: Exposing token ${SECRET_REPLACEMENT}`);
t.is(release.url, `URL: Exposing token ${SECRET_REPLACEMENT}`);
Object.getOwnPropertyNames(release).forEach(prop => {
Object.getOwnPropertyNames(release).forEach((prop) => {
if (isString(release[prop])) {
t.notRegex(release[prop], new RegExp(escapeRegExp(env.MY_TOKEN)));
}
});
});
test('Get all commits including the ones not in the shallow clone', async t => {
test('Get all commits including the ones not in the shallow clone', async (t) => {
let {cwd, repositoryUrl} = await gitRepo(true);
await gitTagVersion('v1.0.0', undefined, {cwd});
await gitCommits(['First', 'Second', 'Third'], {cwd});
+78 -37
View File
@@ -1,13 +1,14 @@
import path from 'path';
import proxyquire from 'proxyquire';
import test from 'ava';
import {escapeRegExp} from 'lodash';
import {writeJson, readJson} from 'fs-extra';
import execa from 'execa';
import {WritableStreamBuffer} from 'stream-buffers';
import delay from 'delay';
import {SECRET_REPLACEMENT} from '../lib/definitions/constants';
import {
const path = require('path');
const test = require('ava');
const proxyquire = require('proxyquire');
const {escapeRegExp} = require('lodash');
const {writeJson, readJson} = require('fs-extra');
const execa = require('execa');
const {WritableStreamBuffer} = require('stream-buffers');
const delay = require('delay');
const getAuthUrl = require('../lib/get-git-auth-url');
const {SECRET_REPLACEMENT} = require('../lib/definitions/constants');
const {
gitHead,
gitTagHead,
gitRepo,
@@ -16,11 +17,12 @@ import {
gitPush,
gitCheckout,
merge,
} from './helpers/git-utils';
import {npmView} from './helpers/npm-utils';
import gitbox from './helpers/gitbox';
import mockServer from './helpers/mockserver';
import npmRegistry from './helpers/npm-registry';
gitGetNote,
} = require('./helpers/git-utils');
const {npmView} = require('./helpers/npm-utils');
const gitbox = require('./helpers/gitbox');
const mockServer = require('./helpers/mockserver');
const npmRegistry = require('./helpers/npm-registry');
/* eslint camelcase: ["error", {properties: "never"}] */
@@ -57,7 +59,7 @@ test.after.always(async () => {
await Promise.all([gitbox.stop(), npmRegistry.stop(), mockServer.stop()]);
});
test('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
@@ -220,7 +222,7 @@ test('Release patch, minor and major versions', async t => {
createReleaseMock = await mockServer.mock(
`/repos/${owner}/${packageName}/releases`,
{
body: {tag_name: `v${version}@next`, name: `v${version}`},
body: {tag_name: `v${version}`, name: `v${version}`},
headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}],
},
{body: {html_url: `release-url/${version}`}}
@@ -246,8 +248,9 @@ test('Release patch, minor and major versions', async t => {
} = await npmView(packageName, testEnv));
head = await gitHead({cwd});
t.is(releasedVersion, version);
t.is(await gitTagHead(`v${version}@next`, {cwd}), head);
t.is(await gitRemoteTagHead(authUrl, `v${version}@next`, {cwd}), head);
t.is(await gitGetNote(`v${version}`, {cwd}), '{"channels":["next"]}');
t.is(await gitTagHead(`v${version}`, {cwd}), head);
t.is(await gitRemoteTagHead(authUrl, `v${version}`, {cwd}), head);
t.log(`+ released ${releasedVersion} on @next`);
await mockServer.verify(verifyMock);
@@ -262,14 +265,14 @@ test('Release patch, minor and major versions', async t => {
{body: {permissions: {push: true}}, method: 'GET'}
);
const getReleaseMock = await mockServer.mock(
`/repos/${owner}/${packageName}/releases/tags/v2.0.0@next`,
`/repos/${owner}/${packageName}/releases/tags/v2.0.0`,
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {id: releaseId}, method: 'GET'}
);
const updateReleaseMock = await mockServer.mock(
`/repos/${owner}/${packageName}/releases/${releaseId}`,
{
body: {tag_name: `v${version}`, name: `v${version}`, prerelease: false},
body: {name: `v${version}`, prerelease: false},
headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}],
},
{body: {html_url: `release-url/${version}`}, method: 'PATCH'}
@@ -292,11 +295,9 @@ test('Release patch, minor and major versions', async t => {
'dist-tags': {latest: releasedVersion},
} = await npmView(packageName, testEnv));
t.is(releasedVersion, version);
t.is(await gitTagHead(`v${version}`, {cwd}), await gitTagHead(`v${version}@next`, {cwd}));
t.is(
await gitRemoteTagHead(authUrl, `v${version}`, {cwd}),
await gitRemoteTagHead(authUrl, `v${version}@next`, {cwd})
);
t.is(await gitGetNote(`v${version}`, {cwd}), '{"channels":["next",null]}');
t.is(await gitTagHead(`v${version}`, {cwd}), await gitTagHead(`v${version}`, {cwd}));
t.is(await gitRemoteTagHead(authUrl, `v${version}`, {cwd}), await gitRemoteTagHead(authUrl, `v${version}`, {cwd}));
t.log(`+ added ${releasedVersion}`);
await mockServer.verify(verifyMock);
@@ -304,7 +305,7 @@ test('Release patch, minor and major versions', async t => {
await mockServer.verify(updateReleaseMock);
});
test('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
@@ -322,7 +323,7 @@ test('Exit with 1 if a plugin is not found', async t => {
t.regex(stderr, /Cannot find module/);
});
test('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
@@ -340,7 +341,7 @@ test('Exit with 1 if a shareable config is not found', async t => {
t.regex(stderr, /Cannot find module/);
});
test('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'};
@@ -361,7 +362,7 @@ test('Exit with 1 if a shareable config reference a not found plugin', async t =
t.regex(stderr, /Cannot find module/);
});
test('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
@@ -397,7 +398,7 @@ test('Dry-run', async t => {
await mockServer.verify(verifyMock);
});
test('Allow local releases with "noCi" option', async t => {
test('Allow local releases with "noCi" option', async (t) => {
const envNoCi = {...env};
delete envNoCi.TRAVIS;
delete envNoCi.CI;
@@ -456,7 +457,7 @@ test('Allow local releases with "noCi" option', async t => {
await mockServer.verify(createReleaseMock);
});
test('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');
@@ -505,7 +506,7 @@ test('Pass options via CLI arguments', async t => {
t.log(`+ released ${releasedVersion} with head ${releasedGitHead}`);
});
test('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}),
@@ -564,7 +565,7 @@ test('Run via JS API', async t => {
await mockServer.verify(createReleaseMock);
});
test('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');
@@ -591,7 +592,7 @@ test('Log unexpected errors from plugins and exit with 1', async t => {
t.is(exitCode, 1);
});
test('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');
@@ -614,7 +615,7 @@ test('Log errors inheriting SemanticReleaseError and exit with 1', async t => {
t.is(exitCode, 1);
});
test('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');
@@ -636,7 +637,7 @@ test('Exit with 1 if missing permission to push to the remote repository', async
t.is(exitCode, 1);
});
test('Hide sensitive environment variable values from the logs', async t => {
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');
@@ -656,3 +657,43 @@ test('Hide sensitive environment variable values from the logs', async t => {
t.regex(stderr, new RegExp(`Error: Console token ${escapeRegExp(SECRET_REPLACEMENT)}`));
t.regex(stderr, new RegExp(`Throw error: Exposing ${escapeRegExp(SECRET_REPLACEMENT)}`));
});
test('Use the valid git credentials when multiple are provided', async (t) => {
const {cwd, authUrl} = await gitbox.createRepo('test-auth');
t.is(
await getAuthUrl({
cwd,
env: {
GITHUB_TOKEN: 'dummy',
GITLAB_TOKEN: 'trash',
BB_TOKEN_BASIC_AUTH: gitbox.gitCredential,
GIT_ASKPASS: 'echo',
GIT_TERMINAL_PROMPT: 0,
},
branch: {name: 'master'},
options: {repositoryUrl: 'http://toto@localhost:2080/git/test-auth.git'},
}),
authUrl
);
});
test('Use the repository URL as is if none of the given git credentials are valid', async (t) => {
const {cwd} = await gitbox.createRepo('test-invalid-auth');
const dummyUrl = 'http://toto@localhost:2080/git/test-auth.git';
t.is(
await getAuthUrl({
cwd,
env: {
GITHUB_TOKEN: 'dummy',
GITLAB_TOKEN: 'trash',
GIT_ASKPASS: 'echo',
GIT_TERMINAL_PROMPT: 0,
},
branch: {name: 'master'},
options: {repositoryUrl: dummyUrl},
}),
dummyUrl
);
});
+31 -33
View File
@@ -1,11 +1,11 @@
import test from 'ava';
import {noop} from 'lodash';
import {stub} from 'sinon';
import normalize from '../../lib/plugins/normalize';
const test = require('ava');
const {noop} = require('lodash');
const {stub} = require('sinon');
const normalize = require('../../lib/plugins/normalize');
const cwd = process.cwd();
test.beforeEach(t => {
test.beforeEach((t) => {
// Stub the logger functions
t.context.log = stub();
t.context.error = stub();
@@ -19,7 +19,7 @@ test.beforeEach(t => {
};
});
test('Normalize and load plugin from string', t => {
test('Normalize and load plugin from string', (t) => {
const plugin = normalize(
{cwd, options: {}, logger: t.context.logger},
'verifyConditions',
@@ -32,7 +32,7 @@ test('Normalize and load plugin from string', t => {
t.deepEqual(t.context.success.args[0], ['Loaded plugin "verifyConditions" from "./test/fixtures/plugin-noop"']);
});
test('Normalize and load plugin from object', t => {
test('Normalize and load plugin from object', (t) => {
const plugin = normalize(
{cwd, options: {}, logger: t.context.logger},
'publish',
@@ -45,7 +45,7 @@ test('Normalize and load plugin from object', t => {
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 => {
test('Normalize and load plugin from a base file path', (t) => {
const plugin = normalize({cwd, options: {}, logger: t.context.logger}, 'verifyConditions', './plugin-noop', {
'./plugin-noop': './test/fixtures',
});
@@ -57,7 +57,7 @@ test('Normalize and load plugin from a base file path', t => {
]);
});
test('Wrap plugin in a function that add the "pluginName" to the error"', async t => {
test('Wrap plugin in a function that add the "pluginName" to the error"', async (t) => {
const plugin = normalize({cwd, options: {}, logger: t.context.logger}, 'verifyConditions', './plugin-error', {
'./plugin-error': './test/fixtures',
});
@@ -67,7 +67,7 @@ test('Wrap plugin in a function that add the "pluginName" to the error"', async
t.is(error.pluginName, './plugin-error');
});
test('Wrap plugin in a function that add the "pluginName" to multiple errors"', async t => {
test('Wrap plugin in a function that add the "pluginName" to multiple errors"', async (t) => {
const plugin = normalize({cwd, options: {}, logger: t.context.logger}, 'verifyConditions', './plugin-errors', {
'./plugin-errors': './test/fixtures',
});
@@ -78,7 +78,7 @@ test('Wrap plugin in a function that add the "pluginName" to multiple errors"',
}
});
test('Normalize and load plugin from function', t => {
test('Normalize and load plugin from function', (t) => {
const pluginFunction = () => {};
const plugin = normalize({cwd, options: {}, logger: t.context.logger}, '', pluginFunction, {});
@@ -86,7 +86,7 @@ test('Normalize and load plugin from function', t => {
t.is(typeof plugin, 'function');
});
test('Normalize and load plugin that retuns multiple functions', t => {
test('Normalize and load plugin that retuns multiple functions', (t) => {
const plugin = normalize(
{cwd, options: {}, logger: t.context.logger},
'verifyConditions',
@@ -98,7 +98,7 @@ test('Normalize and load plugin that retuns multiple functions', t => {
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 => {
test('Wrap "analyzeCommits" plugin in a function that validate the output of the plugin', async (t) => {
const analyzeCommits = stub().resolves(2);
const plugin = normalize(
{cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger},
@@ -116,7 +116,7 @@ test('Wrap "analyzeCommits" plugin in a function that validate the output of the
t.regex(error.details, /2/);
});
test('Wrap "generateNotes" plugin in a function that validate the output of the plugin', async t => {
test('Wrap "generateNotes" plugin in a function that validate the output of the plugin', async (t) => {
const generateNotes = stub().resolves(2);
const plugin = normalize(
{cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger},
@@ -134,7 +134,7 @@ test('Wrap "generateNotes" plugin in a function that validate the output of the
t.regex(error.details, /2/);
});
test('Wrap "publish" plugin in a function that validate the output of the plugin', async t => {
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},
@@ -152,7 +152,7 @@ test('Wrap "publish" plugin in a function that validate the output of the plugin
t.regex(error.details, /2/);
});
test('Wrap "addChannel" plugin in a function that validate the output of the plugin', async t => {
test('Wrap "addChannel" plugin in a function that validate the output of the plugin', async (t) => {
const addChannel = stub().resolves(2);
const plugin = normalize(
{cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger},
@@ -170,7 +170,7 @@ test('Wrap "addChannel" plugin in a function that validate the output of the plu
t.regex(error.details, /2/);
});
test('Plugin is called with "pluginConfig" (with object definition) and input', async t => {
test('Plugin is called with "pluginConfig" (with object definition) and input', async (t) => {
const pluginFunction = stub().resolves();
const pluginConf = {path: pluginFunction, conf: 'confValue'};
const options = {global: 'globalValue'};
@@ -185,7 +185,7 @@ test('Plugin is called with "pluginConfig" (with object definition) and input',
);
});
test('Plugin is called with "pluginConfig" (with array definition) and input', async t => {
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'};
@@ -200,8 +200,8 @@ test('Plugin is called with "pluginConfig" (with array definition) and input', a
);
});
test('Prevent plugins to modify "pluginConfig"', async t => {
const pluginFunction = stub().callsFake(pluginConfig => {
test('Prevent plugins to modify "pluginConfig"', async (t) => {
const pluginFunction = stub().callsFake((pluginConfig) => {
pluginConfig.conf.subConf = 'otherConf';
});
const pluginConf = {path: pluginFunction, conf: {subConf: 'originalConf'}};
@@ -213,7 +213,7 @@ test('Prevent plugins to modify "pluginConfig"', async t => {
t.is(options.globalConf.globalSubConf, 'originalGlobalConf');
});
test('Prevent plugins to modify its input', async t => {
test('Prevent plugins to modify its input', async (t) => {
const pluginFunction = stub().callsFake((pluginConfig, options) => {
options.param.subParam = 'otherParam';
});
@@ -224,13 +224,13 @@ test('Prevent plugins to modify its input', async t => {
t.is(input.param.subParam, 'originalSubParam');
});
test('Return noop if the plugin is not defined', t => {
test('Return noop if the plugin is not defined', (t) => {
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 => {
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(
{cwd, options: {}, logger: t.context.logger},
@@ -243,7 +243,7 @@ test('Always pass a defined "pluginConfig" for plugin defined with string', asyn
t.deepEqual(pluginResult.pluginConfig, {});
});
test('Always pass a defined "pluginConfig" for plugin defined with path', async t => {
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(
{cwd, options: {}, logger: t.context.logger},
@@ -256,7 +256,7 @@ test('Always pass a defined "pluginConfig" for plugin defined with path', async
t.deepEqual(pluginResult.pluginConfig, {});
});
test('Throws an error if the plugin return an object without the expected plugin function', t => {
test('Throws an error if the plugin return an object without the expected plugin function', (t) => {
const error = t.throws(() =>
normalize({cwd, options: {}, logger: t.context.logger}, 'inexistantPlugin', './test/fixtures/multi-plugin', {})
);
@@ -267,12 +267,10 @@ test('Throws an error if the plugin return an object without the expected plugin
t.truthy(error.details);
});
test('Throws an error if the plugin is not found', t => {
const error = t.throws(
() => normalize({cwd, options: {}, logger: t.context.logger}, 'inexistantPlugin', 'non-existing-path', {}),
Error
);
t.regex(error.message, /Cannot find module 'non-existing-path'/);
t.is(error.code, 'MODULE_NOT_FOUND');
test('Throws an error if the plugin is not found', (t) => {
t.throws(() => normalize({cwd, options: {}, logger: t.context.logger}, 'inexistantPlugin', 'non-existing-path', {}), {
message: /Cannot find module 'non-existing-path'/,
code: 'MODULE_NOT_FOUND',
instanceOf: Error,
});
});
+39 -21
View File
@@ -1,9 +1,9 @@
import test from 'ava';
import {stub} from 'sinon';
import AggregateError from 'aggregate-error';
import pipeline from '../../lib/plugins/pipeline';
const test = require('ava');
const {stub} = require('sinon');
const AggregateError = require('aggregate-error');
const pipeline = require('../../lib/plugins/pipeline');
test('Execute each function in series passing the same input', async t => {
test('Execute each function in series passing the same input', async (t) => {
const step1 = stub().resolves(1);
const step2 = stub().resolves(2);
const step3 = stub().resolves(3);
@@ -18,7 +18,7 @@ test('Execute each function in series passing the same input', async t => {
t.true(step2.calledBefore(step3));
});
test('Execute each function in series passing a transformed input from "getNextInput"', async t => {
test('Execute each function in series passing a transformed input from "getNextInput"', async (t) => {
const step1 = stub().resolves(1);
const step2 = stub().resolves(2);
const step3 = stub().resolves(3);
@@ -37,7 +37,7 @@ test('Execute each function in series passing a transformed input from "getNextI
t.true(step3.calledBefore(step4));
});
test('Execute each function in series passing the "lastResult" and "result" to "getNextInput"', async t => {
test('Execute each function in series passing the "lastResult" and "result" to "getNextInput"', async (t) => {
const step1 = stub().resolves(1);
const step2 = stub().resolves(2);
const step3 = stub().resolves(3);
@@ -47,50 +47,68 @@ test('Execute each function in series passing the "lastResult" and "result" to "
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]]);
t.deepEqual(getNextInput.args, [
[5, 1],
[5, 2],
[5, 3],
[5, 4],
]);
});
test('Execute each function in series calling "transform" to modify the results', async t => {
test('Execute each function in series calling "transform" to modify the results', async (t) => {
const step1 = stub().resolves(1);
const step2 = stub().resolves(2);
const step3 = stub().resolves(3);
const step4 = stub().resolves(4);
const getNextInput = stub().returnsArg(0);
const transform = stub().callsFake(result => result + 1);
const transform = stub().callsFake((result) => result + 1);
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]]);
t.deepEqual(getNextInput.args, [
[5, 1 + 1],
[5, 2 + 1],
[5, 3 + 1],
[5, 4 + 1],
]);
});
test('Execute each function in series calling "transform" to modify the results with "settleAll"', async t => {
test('Execute each function in series calling "transform" to modify the results with "settleAll"', async (t) => {
const step1 = stub().resolves(1);
const step2 = stub().resolves(2);
const step3 = stub().resolves(3);
const step4 = stub().resolves(4);
const getNextInput = stub().returnsArg(0);
const transform = stub().callsFake(result => result + 1);
const transform = stub().callsFake((result) => result + 1);
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]]);
t.deepEqual(getNextInput.args, [
[5, 1 + 1],
[5, 2 + 1],
[5, 3 + 1],
[5, 4 + 1],
]);
});
test('Stop execution and throw error if 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.throwsAsync(pipeline([step1, step2, step3])(0), Error);
const error = await t.throwsAsync(pipeline([step1, step2, step3])(0), {
instanceOf: Error,
message: 'test error',
});
t.is(error.message, 'test error');
t.true(step1.calledWith(0));
t.true(step2.calledWith(0));
t.true(step3.notCalled);
});
test('Throw all errors from the first step throwing an AggregateError', async t => {
test('Throw all errors from the first step throwing an AggregateError', async (t) => {
const error1 = new Error('test error 1');
const error2 = new Error('test error 2');
@@ -106,7 +124,7 @@ test('Throw all errors from the first step throwing an AggregateError', async t
t.true(step3.notCalled);
});
test('Execute all even if a Promise rejects', async t => {
test('Execute all even if a Promise rejects', async (t) => {
const error1 = new Error('test error 1');
const error2 = new Error('test error 2');
const step1 = stub().resolves(1);
@@ -121,7 +139,7 @@ test('Execute all even if a Promise rejects', async t => {
t.true(step3.calledWith(0));
});
test('Throw all errors from all steps throwing an AggregateError', async t => {
test('Throw all errors from all steps throwing an AggregateError', async (t) => {
const error1 = new Error('test error 1');
const error2 = new Error('test error 2');
const error3 = new Error('test error 3');
@@ -136,14 +154,14 @@ test('Throw all errors from all steps throwing an AggregateError', async t => {
t.true(step2.calledWith(0));
});
test('Execute each function in series passing a transformed input even if a step rejects', async t => {
test('Execute each function in series passing a transformed input even if a step rejects', async (t) => {
const error2 = new Error('test error 2');
const error3 = new Error('test error 3');
const step1 = stub().resolves(1);
const step2 = stub().rejects(error2);
const step3 = stub().rejects(error3);
const step4 = stub().resolves(4);
const getNextInput = (prevResult, result) => prevResult + result;
const getNextInput = (previousResult, result) => previousResult + result;
const errors = await t.throwsAsync(pipeline([step1, step2, step3, step4], {settleAll: true, getNextInput})(0));
+20 -20
View File
@@ -1,21 +1,21 @@
import path from 'path';
import test from 'ava';
import {copy, outputFile} from 'fs-extra';
import {stub} from 'sinon';
import tempy from 'tempy';
import getPlugins from '../../lib/plugins';
const path = require('path');
const test = require('ava');
const {copy, outputFile} = require('fs-extra');
const {stub} = require('sinon');
const tempy = require('tempy');
const getPlugins = require('../../lib/plugins');
// Save the current working diretory
const cwd = process.cwd();
test.beforeEach(t => {
test.beforeEach((t) => {
// Stub the logger functions
t.context.log = stub();
t.context.success = stub();
t.context.logger = {log: t.context.log, success: t.context.success, scope: () => t.context.logger};
});
test('Export default plugins', t => {
test('Export default plugins', (t) => {
const plugins = getPlugins({cwd, options: {}, logger: t.context.logger}, {});
// Verify the module returns a function for each plugin
@@ -29,7 +29,7 @@ test('Export default plugins', t => {
t.is(typeof plugins.fail, 'function');
});
test('Export plugins based on steps config', t => {
test('Export plugins based on steps config', (t) => {
const plugins = getPlugins(
{
cwd,
@@ -55,7 +55,7 @@ test('Export plugins based on steps config', t => {
t.is(typeof plugins.fail, 'function');
});
test('Export plugins based on "plugins" config (array)', async t => {
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(
@@ -84,7 +84,7 @@ test('Export plugins based on "plugins" config (array)', async t => {
t.is(typeof plugins.fail, 'function');
});
test('Export plugins based on "plugins" config (single definition)', async t => {
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}}, {});
@@ -105,7 +105,7 @@ test('Export plugins based on "plugins" config (single definition)', async t =>
t.is(typeof plugins.fail, 'function');
});
test('Merge global options, "plugins" options and step options', async t => {
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'}];
@@ -129,7 +129,7 @@ test('Merge global options, "plugins" options and step options', async t => {
t.deepEqual(plugin3[0].args[0][0], {globalOpt: 'global', pluginOpt3: 'plugin3'});
});
test('Unknown steps of plugins configured in "plugins" are ignored', t => {
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]}}, {});
@@ -137,7 +137,7 @@ test('Unknown steps of plugins configured in "plugins" are ignored', t => {
t.is(plugins.unknown, undefined);
});
test('Export plugins loaded from the dependency of a shareable config module', async t => {
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',
@@ -170,7 +170,7 @@ test('Export plugins loaded from the dependency of a shareable config module', a
t.is(typeof plugins.fail, 'function');
});
test('Export plugins loaded from the dependency of a shareable config file', async t => {
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'), '');
@@ -200,7 +200,7 @@ test('Export plugins loaded from the dependency of a shareable config file', asy
t.is(typeof plugins.fail, 'function');
});
test('Use default when only options are passed for a single plugin', t => {
test('Use default when only options are passed for a single plugin', (t) => {
const analyzeCommits = {};
const generateNotes = {};
const publish = {};
@@ -234,7 +234,7 @@ test('Use default when only options are passed for a single plugin', t => {
t.falsy(fail.path);
});
test('Merge global options with plugin options', async t => {
test('Merge global options with plugin options', async (t) => {
const plugins = getPlugins(
{
cwd,
@@ -253,7 +253,7 @@ test('Merge global options with plugin options', async t => {
t.deepEqual(result.pluginConfig, {localOpt: 'local', globalOpt: 'global', otherOpt: 'locally-defined'});
});
test('Throw an error for each invalid plugin configuration', t => {
test('Throw an error for each invalid plugin configuration', (t) => {
const errors = [
...t.throws(() =>
getPlugins(
@@ -283,7 +283,7 @@ test('Throw an error for each invalid plugin configuration', t => {
t.is(errors[3].code, 'EPLUGINCONF');
});
test('Throw EPLUGINSCONF error if the "plugins" option contains an old plugin definition (returns a function)', t => {
test('Throw EPLUGINSCONF error if the "plugins" option contains an old plugin definition (returns a function)', (t) => {
const errors = [
...t.throws(() =>
getPlugins(
@@ -303,7 +303,7 @@ test('Throw EPLUGINSCONF error if the "plugins" option contains an old plugin de
t.is(errors[1].code, 'EPLUGINSCONF');
});
test('Throw EPLUGINSCONF error for each invalid definition if the "plugins" option', t => {
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}, [() => {}, {}, {}]]}}, {})
+7 -7
View File
@@ -1,7 +1,7 @@
import test from 'ava';
import {validatePlugin, validateStep, loadPlugin, parseConfig} from '../../lib/plugins/utils';
const test = require('ava');
const {validatePlugin, validateStep, loadPlugin, parseConfig} = require('../../lib/plugins/utils');
test('validatePlugin', t => {
test('validatePlugin', (t) => {
const path = 'plugin-module';
const options = {option1: 'value1', option2: 'value2'};
@@ -25,7 +25,7 @@ test('validatePlugin', t => {
t.false(validatePlugin({path: 1}), 'Object definition, wrong path');
});
test('validateStep: optional plugin configuration', t => {
test('validateStep: optional plugin configuration', (t) => {
const type = {multiple: true, required: false};
// Empty config
@@ -107,7 +107,7 @@ test('validateStep: optional plugin configuration', t => {
);
});
test('validateStep: required plugin configuration', t => {
test('validateStep: required plugin configuration', (t) => {
const type = {required: true};
// Empty config
@@ -189,7 +189,7 @@ test('validateStep: required plugin configuration', t => {
);
});
test('loadPlugin', t => {
test('loadPlugin', (t) => {
const cwd = process.cwd();
const func = () => {};
@@ -202,7 +202,7 @@ test('loadPlugin', t => {
t.is(func, loadPlugin({cwd}, func, {}), 'Defined as a function');
});
test('parseConfig', t => {
test('parseConfig', (t) => {
const path = 'plugin-module';
const options = {option1: 'value1', option2: 'value2'};
+27 -21
View File
@@ -1,6 +1,6 @@
import test from 'ava';
import AggregateError from 'aggregate-error';
import {
const test = require('ava');
const AggregateError = require('aggregate-error');
const {
extractErrors,
tagsToVersions,
isMajorRange,
@@ -14,16 +14,17 @@ import {
getFirstVersion,
getRange,
makeTag,
} from '../lib/utils';
isSameChannel,
} = require('../lib/utils');
test('extractErrors', t => {
test('extractErrors', (t) => {
const errors = [new Error('Error 1'), new Error('Error 2')];
t.deepEqual(extractErrors(new AggregateError(errors)), errors);
t.deepEqual(extractErrors(errors[0]), [errors[0]]);
});
test('tagsToVersions', t => {
test('tagsToVersions', (t) => {
t.deepEqual(tagsToVersions([{version: '1.0.0'}, {version: '1.1.0'}, {version: '1.2.0'}]), [
'1.0.0',
'1.1.0',
@@ -31,7 +32,7 @@ test('tagsToVersions', t => {
]);
});
test('isMajorRange', t => {
test('isMajorRange', (t) => {
t.false(isMajorRange('1.1.x'));
t.false(isMajorRange('1.11.x'));
t.false(isMajorRange('11.1.x'));
@@ -47,7 +48,7 @@ test('isMajorRange', t => {
t.true(isMajorRange('1.X'));
});
test('isMaintenanceRange', t => {
test('isMaintenanceRange', (t) => {
t.true(isMaintenanceRange('1.1.x'));
t.true(isMaintenanceRange('11.1.x'));
t.true(isMaintenanceRange('11.11.x'));
@@ -68,7 +69,7 @@ test('isMaintenanceRange', t => {
t.false(isMaintenanceRange('^1.0.0'));
});
test('getUpperBound', t => {
test('getUpperBound', (t) => {
t.is(getUpperBound('1.x.x'), '2.0.0');
t.is(getUpperBound('1.X.X'), '2.0.0');
t.is(getUpperBound('10.x.x'), '11.0.0');
@@ -83,7 +84,7 @@ test('getUpperBound', t => {
t.is(getUpperBound('foo'), undefined);
});
test('getLowerBound', t => {
test('getLowerBound', (t) => {
t.is(getLowerBound('1.x.x'), '1.0.0');
t.is(getLowerBound('1.X.X'), '1.0.0');
t.is(getLowerBound('10.x.x'), '10.0.0');
@@ -98,7 +99,7 @@ test('getLowerBound', t => {
t.is(getLowerBound('foo'), undefined);
});
test('highest', t => {
test('highest', (t) => {
t.is(highest('1.0.0', '2.0.0'), '2.0.0');
t.is(highest('1.1.1', '1.1.0'), '1.1.1');
t.is(highest(null, '1.0.0'), '1.0.0');
@@ -106,14 +107,14 @@ test('highest', t => {
t.is(highest(), undefined);
});
test('lowest', t => {
test('lowest', (t) => {
t.is(lowest('1.0.0', '2.0.0'), '1.0.0');
t.is(lowest('1.1.1', '1.1.0'), '1.1.0');
t.is(lowest(null, '1.0.0'), '1.0.0');
t.is(lowest(), undefined);
});
test.serial('getLatestVersion', t => {
test.serial('getLatestVersion', (t) => {
t.is(getLatestVersion(['1.2.3-alpha.3', '1.2.0', '1.0.1', '1.0.0-alpha.1']), '1.2.0');
t.is(getLatestVersion(['1.2.3-alpha.3', '1.2.3-alpha.2']), undefined);
@@ -126,7 +127,7 @@ test.serial('getLatestVersion', t => {
t.is(getLatestVersion([]), undefined);
});
test.serial('getEarliestVersion', t => {
test.serial('getEarliestVersion', (t) => {
t.is(getEarliestVersion(['1.2.3-alpha.3', '1.2.0', '1.0.0', '1.0.1-alpha.1']), '1.0.0');
t.is(getEarliestVersion(['1.2.3-alpha.3', '1.2.3-alpha.2']), undefined);
@@ -142,7 +143,7 @@ test.serial('getEarliestVersion', t => {
t.is(getEarliestVersion([]), undefined);
});
test('getFirstVersion', t => {
test('getFirstVersion', (t) => {
t.is(getFirstVersion(['1.2.0', '1.0.0', '1.3.0', '1.1.0', '1.4.0'], []), '1.0.0');
t.is(
getFirstVersion(
@@ -166,15 +167,20 @@ test('getFirstVersion', t => {
);
});
test('getRange', t => {
test('getRange', (t) => {
t.is(getRange('1.0.0', '1.1.0'), '>=1.0.0 <1.1.0');
t.is(getRange('1.0.0'), '>=1.0.0');
});
test('makeTag', t => {
test('makeTag', (t) => {
t.is(makeTag(`v\${version}`, '1.0.0'), 'v1.0.0');
t.is(makeTag(`v\${version}`, '1.0.0', false), 'v1.0.0');
t.is(makeTag(`v\${version}`, '1.0.0', null), 'v1.0.0');
t.is(makeTag(`v\${version}`, '1.0.0', 'next'), 'v1.0.0@next');
t.is(makeTag(`v\${version}@test`, '1.0.0', 'next'), 'v1.0.0@next@test');
});
test('isSameChannel', (t) => {
t.true(isSameChannel('next', 'next'));
t.true(isSameChannel(null, undefined));
t.true(isSameChannel(false, undefined));
t.true(isSameChannel('', false));
t.false(isSameChannel('next', false));
});
+11 -11
View File
@@ -1,9 +1,9 @@
import test from 'ava';
import tempy from 'tempy';
import verify from '../lib/verify';
import {gitRepo} from './helpers/git-utils';
const test = require('ava');
const tempy = require('tempy');
const verify = require('../lib/verify');
const {gitRepo} = require('./helpers/git-utils');
test('Throw a AggregateError', async t => {
test('Throw a AggregateError', async (t) => {
const {cwd} = await gitRepo();
const options = {branches: [{name: 'master'}, {name: ''}]};
@@ -27,7 +27,7 @@ test('Throw a AggregateError', async t => {
t.truthy(errors[3].details);
});
test('Throw a SemanticReleaseError if does not run on a git repository', async t => {
test('Throw a SemanticReleaseError if does not run on a git repository', async (t) => {
const cwd = tempy.directory();
const options = {branches: []};
@@ -39,7 +39,7 @@ test('Throw a SemanticReleaseError if does not run on a git repository', async t
t.truthy(errors[0].details);
});
test('Throw a SemanticReleaseError if the "tagFormat" is not valid', async t => {
test('Throw a SemanticReleaseError if the "tagFormat" is not valid', async (t) => {
const {cwd, repositoryUrl} = await gitRepo(true);
const options = {repositoryUrl, tagFormat: `?\${version}`, branches: []};
@@ -51,7 +51,7 @@ test('Throw a SemanticReleaseError if the "tagFormat" is not valid', async t =>
t.truthy(errors[0].details);
});
test('Throw a SemanticReleaseError if the "tagFormat" does not contains the "version" variable', async t => {
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', branches: []};
@@ -63,7 +63,7 @@ test('Throw a SemanticReleaseError if the "tagFormat" does not contains the "ver
t.truthy(errors[0].details);
});
test('Throw a SemanticReleaseError if the "tagFormat" contains multiple "version" variables', async t => {
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}`, branches: []};
@@ -75,7 +75,7 @@ test('Throw a SemanticReleaseError if the "tagFormat" contains multiple "version
t.truthy(errors[0].details);
});
test('Throw a SemanticReleaseError for each invalid branch', async t => {
test('Throw a SemanticReleaseError for each invalid branch', async (t) => {
const {cwd, repositoryUrl} = await gitRepo(true);
const options = {
repositoryUrl,
@@ -109,7 +109,7 @@ test('Throw a SemanticReleaseError for each invalid branch', async t => {
t.truthy(errors[5].details);
});
test('Return "true" if all verification pass', async t => {
test('Return "true" if all verification pass', async (t) => {
const {cwd, repositoryUrl} = await gitRepo(true);
const options = {repositoryUrl, tagFormat: `v\${version}`, branches: [{name: 'master'}]};