Compare commits

...
43 Commits
Author SHA1 Message Date
Pierre Vanduynslager a72d8f52af fix: add clarification in EGITNOPERMISSION error message
Clarify that the error might be due to an invalid `repositoryUrl` configuration.
2018-05-20 23:14:44 +03:00
Pierre VanduynslagerandGregor Martynus d7081fadb1 fix: use git rev-parse origin/${branch} to verify origin head 2018-05-09 13:27:33 -07:00
Pierre Vanduynslager 61d7d38ec2 feat: set tag author and committer name/email
Use [Git Environment Variables](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#Committing). Set default values if environement variables are not set.
2018-05-08 14:25:23 -04:00
Cory ReedandGregor Martynus 4352144a98 fix: correct git merge-base error code handling 2018-05-08 10:03:46 -07:00
Even StensbergandPierre Vanduynslager 20245b733a docs: add comment about publishing with ssh keys 2018-05-08 12:01:06 -04:00
greenkeeper[bot]andPierre Vanduynslager 349b2e8d77 fix(package): update cosmiconfig to version 5.0.0 2018-05-05 15:04:42 -04:00
pvdlg 1ed466e337 ci: run test on Node.js 10 2018-05-04 21:55:30 -04:00
Pierre ColleandPierre Vanduynslager f0f325dd7f docs: add link to shareable configs and plugins list 2018-05-04 20:32:45 -04:00
greenkeeper[bot]andPierre Vanduynslager f3e4991819 chore(package): update xo to version 0.21.0 2018-05-04 16:06:24 -04:00
pvdlgandGregor Martynus cd9f2bdd44 feat: log git error message when authentication verification fails 2018-05-04 12:19:24 -07:00
Gregor d1c3ad0b57 docs: issue templates order 2018-05-02 16:01:16 -07:00
Gregor e3da17247c docs: remove old issue templates 2018-05-02 09:47:45 -07:00
Gregor Martynus 05b09b1738 docs(ISSUE_TEMPLATES): initial version 2018-05-02 09:47:45 -07:00
pvdlgandGregor Martynus c93775cc38 feat: add support for Bitbucket token in environment variables 2018-05-01 12:02:29 -07:00
greenkeeper[bot]andGregor Martynus 22c5bbe06b chore(package): update fs-extra to version 6.0.0 2018-05-01 08:27:42 -07:00
pvdlg e88ac6300c fix: fix Bitbucket authenticated URL 2018-05-01 10:44:02 -04:00
pvdlg a52f258fa4 refactor: rename origin parameters to repositoryUrl 2018-05-01 09:59:57 -04:00
pvdlg 3f614530a0 test: prevent username/password prompt in get-git-auth-url tests 2018-05-01 09:59:57 -04:00
pvdlg d15905c0d5 fix: verify the local branch is up to date with the remote one 2018-05-01 09:59:57 -04:00
pvdlg a11da0d5e3 docs: fix jsdoc typos 2018-05-01 09:59:57 -04:00
pvdlg 65aaa77e95 refactor: remove unused test util function 2018-05-01 09:59:57 -04:00
Anshuk KumarandPierre Vanduynslager 45d7e6f25f fix: unshallow repository with credentials 2018-05-01 09:47:15 -04:00
greenkeeper[bot]andPierre Vanduynslager c02b5cac6b chore(package): update sinon to version 5.0.1 2018-04-30 10:30:37 -04:00
greenkeeper[bot]andPierre Vanduynslager b9bd650967 fix(package): update env-ci to version 2.0.0 2018-04-29 16:13:22 -04:00
greenkeeper[bot]andPierre Vanduynslager 6e15c18bea chore(package): update p-retry to version 2.0.0 2018-04-26 16:31:39 -04:00
Schalk NeethlingandPierre Vanduynslager 5af90319a1 docs: add details on NPM error 403 to troubleshooting doc 2018-04-26 11:49:32 -04:00
Gabriel DuarteandPierre Vanduynslager 218aa32f79 docs: add semantic-release-chrome to the list of community plugins 2018-04-23 12:04:26 -04:00
Pierre Vanduynslager cb1f80cb56 fix: add trailing .git to repositoryUrl only if it's present in the configured URL 2018-04-12 17:42:53 -04:00
greenkeeper[bot]andPierre Vanduynslager 7c9ec41dd7 fix(package): update git-url-parse to version 9.0.0 2018-04-12 17:42:53 -04:00
Kévin BerthommierandPierre Vanduynslager a1138a6a80 docs: update plugins package name 2018-04-11 18:40:19 -04:00
Pierre ColleandPierre Vanduynslager 6172eb807c docs: add plugins to the community list 2018-04-11 18:40:19 -04:00
Pierre Vanduynslager 31ad23125a fix: match tag to tagFormat from the begining of the string 2018-04-11 17:11:33 -04:00
Pierre Vanduynslager a8a07b7d51 docs: clarify FAQ to publish non-JavaScript packages 2018-04-10 12:11:24 -04:00
greenkeeper[bot]andPierre Vanduynslager 02746aa276 fix(package): update git-url-parse to version 8.3.1 2018-04-03 16:04:39 -04:00
William HosfordandGregor Martynus e41726cb96 docs: fix grammar and typos in CI configuration, configuration, and shareable configurations 2018-03-21 20:03:21 -07:00
Pierre Vanduynslager 51e340f44e fix: handle case with no last release in history 2018-03-21 21:41:50 -04:00
Pierre Vanduynslager 30ee231116 fix: prevent git prompt before permissions verification 2018-03-20 11:24:08 -04:00
Pierre Vanduynslager 3c46455929 fix: remove execa timeout 2018-03-20 11:24:08 -04:00
Pierre Vanduynslager dff0a34ab2 docs: add "Introduction to Semantic Release" article 2018-03-16 16:17:07 -04:00
Pierre Vanduynslager 7c48afad47 fix: prevent git CLI to prompt user/password on CI 2018-03-16 14:33:18 -04:00
Honza JavorekandPierre Vanduynslager c84ac15020 fix(package): Remove commander.js dependency (#704) 2018-03-16 14:18:49 -04:00
Pierre VanduynslagerandGregor Martynus 5f1d530e2a feat: allow to use shorthand for repositoryUrl
- `owner/repo` => `https://github.com/owner/repo.git`
- `gitlab:owner/repo` => `https://gitlab.com/owner/repo.git`
- `bitbucket:owner/repo` => `https://bitbucket.com/owner/repo.git`
2018-03-15 22:40:09 +00:00
greenkeeper[bot]andPierre Vanduynslager f13ec6a615 fix(package): update execa to version 0.10.0 2018-03-15 08:18:04 -04:00
37 changed files with 629 additions and 331 deletions
-48
View File
@@ -1,48 +0,0 @@
<!--
Please fill the informations requested or use one of the following url:
- Bug report: https://github.com/semantic-release/semantic-release/issues/new?template=bug-report.md
- Feature request: https://github.com/semantic-release/semantic-release/issues/new?template=feature-request.md
- New plugin request: https://github.com/semantic-release/semantic-release/issues/new?template=plugin-request.md
-->
<!--------------------- FOR BUG REPORT ONLY --------------------->
## Current behavior
<!-- Describe how the issue manifests. -->
## Expected behavior
<!-- Describe what the desired behavior would be. -->
## Environment
- **semantic-release** version: <!-- Version set in package.json devDpendencies -->
- CI environment: <!-- CI service name -->
- Plugins used: <!-- List semantic-release plugin used if any -->
- **semantic-release** configuration: <!-- link to your repository or relevant part of the semantic-release config -->
- CI logs: <!-- link to your CI logs or semantic-release logs -->
<!--------------------- FOR FEATURE REQUEST ONLY --------------------->
## New feature motivation
<!-- Describe the context, the use-case and the advantages of the feature request. -->
## New feature description
<!-- Describe the functional changes that would have to be made in semantic-release or its plugins. -->
## New feature implementation
<!-- Optionally describe the technical changes to be made in semantic-release or its plugins. -->
<!--------------------- FOR NEW PLUGIN REQUEST ONLY --------------------->
## New plugin motivation
<!-- Describe the reasons to create a new plugin and why it's not covered by the existing ones. -->
## Third-party documentation
<!-- Provide explanation and documentation links for the platform to integrate with. -->
@@ -1,17 +1,21 @@
# Bug report
## Current behavior
<!-- Describe how the issue manifests. -->
## Expected behavior
<!-- Describe what the desired behavior would be. -->
## Environment
- **semantic-release** version: <!-- Version set in package.json devDpendencies -->
- CI environment: <!-- CI service name -->
- Plugins used: <!-- List semantic-release plugin used if any -->
- **semantic-release** configuration: <!-- link to your repository or relevant part of the semantic-release config -->
- CI logs: <!-- link to your CI logs or semantic-release logs -->
---
name: Bug report
about: Something not working as expected
---
## Current behavior
<!-- Describe how the issue manifests. -->
## Expected behavior
<!-- Describe what the desired behavior would be. -->
## Environment
- **semantic-release** version: <!-- Version set in package.json devDpendencies -->
- CI environment: <!-- CI service name -->
- Plugins used: <!-- List semantic-release plugin used if any -->
- **semantic-release** configuration: <!-- link to your repository or relevant part of the semantic-release config -->
- CI logs: <!-- link to your CI logs or semantic-release logs -->
@@ -1,13 +1,17 @@
# Feature request
## New feature motivation
<!-- Describe the context, the use-case and the advantages of the feature request. -->
## New feature description
<!-- Describe the functional changes that would have to be made in semantic-release or its plugins. -->
## New feature implementation
<!-- Optionally describe the technical changes to be made in semantic-release or its plugins. -->
---
name: Feature request
about: Wouldnt it be nice if semantic-release could ...
---
## New feature motivation
<!-- Describe the context, the use-case and the advantages of the feature request. -->
## New feature description
<!-- Describe the functional changes that would have to be made in semantic-release or its plugins. -->
## New feature implementation
<!-- Optionally describe the technical changes to be made in semantic-release or its plugins. -->
@@ -1,9 +1,13 @@
# New plugin request
## New plugin motivation
<!-- Describe the reasons to create a new plugin and why it's not covered by the existing ones. -->
## Third-party documentation
<!-- Provide explanation and documentation links for the platform to integrate with. -->
---
name: New plugin suggestion
about: Integrate with a new platform, etc
---
## New plugin motivation
<!-- Describe the reasons to create a new plugin and why it's not covered by the existing ones. -->
## Third-party documentation
<!-- Provide explanation and documentation links for the platform to integrate with. -->
+1 -1
View File
@@ -4,7 +4,7 @@ services:
- docker
node_js:
- 9
- 10
- 8
# Trigger a push build on caribou and greenkeeper branches + PRs build on every branches
+12
View File
@@ -34,3 +34,15 @@
- [fail](https://github.com/semantic-release/exec#fail): Execute a shell command to notify of a failed release
## Community plugins
[Open a Pull Request](https://github.com/semantic-release/semantic-release/blob/caribou/CONTRIBUTING.md#submitting-a-pull-request) to add your plugin to the list.
- [semantic-release-docker](https://github.com/felixfbecker/semantic-release-docker) Set of semantic-release plugins for publishing a docker image to Docker Hub (tested on semantic-release **^11.0.0**)
- [verifyConditions](https://github.com/felixfbecker/semantic-release-docker#verifyconditions) Verify that all needed configuration is present and login to the Docker registry.
- [publish](https://github.com/felixfbecker/semantic-release-docker#publish) Tag the image specified by `name` with the new version, push it to Docker Hub and update the latest tag.
- [semantic-release-verify-deps](https://github.com/piercus/semantic-release-verify-deps)
- [verifyConditions](https://github.com/piercus/semantic-release-verify-deps) Check the dependencies format against a regexp before a release
- [semantic-release-chrome](https://github.com/GabrielDuarteM/semantic-release-chrome) Set of semantic-release plugins for publishing a Chrome extension release.
- [verifyConditions](https://github.com/GabrielDuarteM/semantic-release-chrome#verifyconditions) Verify the presence of the authentication (set via environment variables).
- [prepare](https://github.com/GabrielDuarteM/semantic-release-chrome#prepare) Write the correct version to the manifest.json and creates a zip file of the whole dist folder.
- [publish](https://github.com/GabrielDuarteM/semantic-release-chrome#publish) Uploads the generated zip file to the webstore, and publish the item.
+2
View File
@@ -4,6 +4,8 @@ When using [environment variables](../usage/ci-configuration.md#authentication)
Alternatively the Git repository can be accessed via [SSH](https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols#_the_ssh_protocol) by creating SSH keys, adding the public one to your Git hosted account and making the private one available on the CI environment.
**Note:** SSH keys allow to push the [Git release tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) associated to the released version. Some plugins might also require an API token. See each plugin documentation for additional information.
## Generating the SSH keys
In your local repository root:
+1
View File
@@ -13,6 +13,7 @@
## Articles
- [“Introduction to SemVer” - Irina Gebauer](https://blog.greenkeeper.io/introduction-to-semver-d272990c44f2)
- ["Introduction to Semantic Release" - liv](https://blog.greenkeeper.io/introduction-to-semantic-release-33f73b117c8)
## Tutorials
+31 -2
View File
@@ -57,7 +57,36 @@ To publish a non-Node package (without a `package.json`) you would need to:
- 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
See the [CI configuration recipes](../usage/ci-configuration.md) for more details on specific CI environments.
See the [CI configuration recipes](../recipes/README.md#ci-configurations) for more details on specific CI environments.
In addition you will need to configure the **semantic-release** [plugins](../usage/plugins.md#plugins) to:
- Disable the [`@semantic-release/npm`](https://github.com/semantic-release/npm) plugin
- Define plugins for the [verifyConditions](../usage/plugins.md#verifyconditions-plugin), [prepare](../usage/plugins.md#prepare-plugin) and [publish](../usage/plugins.md#publish-plugin) steps to release on your package registry. The [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugin is recommended for situation where a release can be done with a shell command.
Here is a basic example to create [GitHub releases](https://help.github.com/articles/about-releases) and use shell command to publish:
```json
{
"verifyConditions": ["@semantic-release/github"],
"prepare": [
{
"path": "@semantic-release/exec",
"cmd": "set-version ${nextRelease.version}"
}
],
"publish": [
"@semantic-release/github",
{
"path": "@semantic-release/exec",
"cmd": "publish-package"
}
],
}
```
**Note**: This is a theoretical example where the command `set-version` update the project version with the value passed as its first argument and `publish-package` publishes the package to a registry.
See the [package managers and languages recipes](../recipes/README.md#package-managers-and-languages) for more details on specific project types.
## Can I use semantic-release with any CI service?
@@ -65,7 +94,7 @@ Yes, **semantic-release** can be used with any CI service, as long as it provide
- A way to set [authentication](../usage/ci-configuration.md#authentication) via environment variables
- A way to guarantee that the `semantic-release` command is [executed only after all the tests of all the jobs in the CI build pass](../usage/ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded)
See the [CI configuration recipes](../usage/ci-configuration.md) for more details on specific CI environments.
See the [CI configuration recipes](../recipes/README.md#ci-configurations) for more details on specific CI environments.
## Can I run semantic-release on my local machine rather than on a CI server?
+15
View File
@@ -29,3 +29,18 @@ You can recover from that issue by identifying the commit in your branch history
$ git tag -f v<version of the last release> <commit sha1 corresponding to last release>
$ git push -f --tags origin <your release branch>
```
## You do not have permission to publish 'package-name'
When running semantic-release you may encounter the following error:
```
An error occurred while running semantic-release: { Error: Command failed: npm publish ./. --registry https://registry.npmjs.org/
npm ERR! publish Failed PUT 403
npm ERR! code E403
npm ERR! You do not have permission to publish "<package-name>". Are you logged in as the correct user? : <package-name>
```
This message is a little unclear, and might not have anything to with your `NPM_TOKEN` or authentication method. It might instead be related to the package name itself. If there is already a package with the same name as yours or, there is a very close match, it could trigger this error.
Best way to be sure, is to search [npmjs.org](https://www.npmjs.com/)) using your package name. If there is a name conflict, rename your package in your `package.json`
+9 -8
View File
@@ -10,17 +10,18 @@ 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). |
| `GIT_CREDENTIALS` | [URL encoded basic HTTP Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication#URL_encoding) credentials). |
| Variable | Description |
|---------------------------------|-------------------------------------------------------------------------------------------------------------------------------|
| `GH_TOKEN` or `GITHUB_TOKEN` | A GitHub [personal access token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line). |
| `GL_TOKEN` or `GITLAB_TOKEN` | A GitLab [personal access token](https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html). |
| `BB_TOKEN` or `BITBUCKET_TOKEN` | A Bitbucket [personal access token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html). |
| `GIT_CREDENTIALS` | [URL encoded basic HTTP Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication#URL_encoding) credentials). |
`GIT_CREDENTIALS` can be the Git username and password in the format `<username>:<password>` or a token for certain Git providers like [Bitbucket](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html).
`GIT_CREDENTIALS` must be the Git username and password in the format `<username>:<password>`.
Alternatively the Git authentication can be set up via [SSH keys](../recipes/git-auth-ssh-keys.md).
Most **semantic-release** [plugins](plugins.md) require to set up authentication in order to publish to a package manager registry. The default [npm](https://github.com/semantic-release/npm#environment-variables) and [github](https://github.com/semantic-release/github#environment-variables) plugins require the following environment variables:
Most **semantic-release** [plugins](plugins.md) require setting up authentication in order to publish to a package manager registry. The default [@semantic-release/npm](https://github.com/semantic-release/npm#environment-variables) and [@semantic-release/github](https://github.com/semantic-release/github#environment-variables) plugins require the following environment variables:
| Variable | Description |
|-------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
@@ -35,7 +36,7 @@ See [CI configuration recipes](../recipes/README.md#ci-configurations) for more
## Automatic setup with `semantic-release-cli`
[`semantic-release-cli`](https://github.com/semantic-release/cli) allow to easily [install](installation.md) **semantic-release** in your Node project and set up the CI configuration:
[`semantic-release-cli`](https://github.com/semantic-release/cli) allows for easy [installation](installation.md) of **semantic-release** in your Node project as well as setting up the CI configuration:
```bash
npm install -g semantic-release-cli
+14 -7
View File
@@ -27,7 +27,16 @@ $ semantic-release
**Note**: CLI arguments take precedence over options configured in the configuration file.
**Note**: Plugins options cannot be defined via CLI arguments and must be defined in the configuration file.
**Note**: Plugin options cannot be defined via CLI arguments and must be defined in the configuration file.
## Environment variables
| Variable | Description | Default |
|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------|
| `GIT_AUTHOR_NAME` | The author name associated with the [Git release tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging). See [Git environment variables](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_committing). | @semantic-release-bot. |
| `GIT_AUTHOR_EMAIL` | The author email associated with the [Git release tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging). See [Git environment variables](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_committing). | @semantic-release-bot email address. |
| `GIT_COMMITTER_NAME` | The committer name associated with the [Git release tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging). See [Git environment variables](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_committing). | @semantic-release-bot. |
| `GIT_COMMITTER_EMAIL` | The committer email associated with the [Git release tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging). See [Git environment variables](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_committing). | @semantic-release-bot email address. |
## Options
@@ -37,7 +46,7 @@ Type: `Array`, `String`
CLI arguments: `-e`, `--extends`
List of modules or file paths containing a [shareable configuration](shareable-configurations.md). If multiple shareable configuration are set, they will be imported in the order defined with each configuration option taking precedence over the options defined in a previous shareable configuration.
List of modules or file paths containing a [shareable configuration](shareable-configurations.md). If multiple shareable configurations are set, they will be imported in the order defined with each configuration option taking precedence over the options defined in a previous shareable configuration.
**Note**: Options defined via CLI arguments or in the configuration file will take precedence over the ones defined in any shareable configuration.
@@ -63,8 +72,6 @@ The git repository URL.
Any valid git url format is supported (See [Git protocols](https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols)).
**Note**: If the [Github plugin](https://github.com/semantic-release/github) is used the URL must be a valid Github URL that include the `owner`, the `repository` name and the `host`. **The Github shorthand URL is not supported.**
### tagFormat
Type: `String`
@@ -81,7 +88,7 @@ The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) format used by
Type: `Boolean`
Default: `false` if running in a CI environment, `false` otherwise
Default: `false` if running in a CI environment, `true` otherwise
CLI arguments: `-d`, `--dry-run`
@@ -95,7 +102,7 @@ Default: `false`
CLI arguments: `--no-ci`
Skip Continuous Integration environment verifications, allowing to make releases from a local machine.
Skip Continuous Integration environment verifications. This allows for making releases from a local machine.
### debug
@@ -105,7 +112,7 @@ Default: `false`
CLI argument: `--debug`
Output debugging information. It can also be enabled by set the `DEBUG` environment variable to `semantic-release:*`.
Output debugging information. It can also be enabled by setting the `DEBUG` environment variable to `semantic-release:*`.
### verifyConditions
+7 -5
View File
@@ -2,13 +2,15 @@
Each [release step](../../README.md#release-steps) is implemented within a plugin or a list of plugins that can be configured. This allows for support of different [commit message formats](../../README.md#commit-message-format), release note generators and publishing platforms.
See [plugins list](../extending/plugins-list.md).
## Plugin types
### verifyConditions plugin
Responsible for verifying conditions necessary to proceed with the release: configuration is correct, authentication token are valid, etc...
Default implementation: [npm](https://github.com/semantic-release/npm#verifyconditions) and [github](https://github.com/semantic-release/github#verifyconditions).
Default implementation: [@semantic-release/npm](https://github.com/semantic-release/npm#verifyconditions) and [@semantic-release/github](https://github.com/semantic-release/github#verifyconditions).
### analyzeCommits plugin
@@ -34,25 +36,25 @@ Responsible for preparing the release, including:
- Creating or updating files such as `package.json`, `CHANGELOG.md`, documentation or compiled assets.
- Create and push commits
Default implementation: [npm](https://github.com/semantic-release/npm#prepare).
Default implementation: [@semantic-release/npm](https://github.com/semantic-release/npm#prepare).
### publish plugin
Responsible for publishing the release.
Default implementation: [npm](https://github.com/semantic-release/npm#publish) and [github](https://github.com/semantic-release/github#publish).
Default implementation: [@semantic-release/npm](https://github.com/semantic-release/npm#publish) and [@semantic-release/github](https://github.com/semantic-release/github#publish).
### success plugin
Responsible for notifying of a new release.
Default implementation: [github](https://github.com/semantic-release/github#success).
Default implementation: [@semantic-release/github](https://github.com/semantic-release/github#success).
### fail plugin
Responsible for notifying of a failed release.
Default implementation: [github](https://github.com/semantic-release/github#fail).
Default implementation: [@semantic-release/github](https://github.com/semantic-release/github#fail).
## Configuration
+3 -1
View File
@@ -1,5 +1,7 @@
# Shareable configurations
A sharable configuration is an [npm](https://www.npmjs.com/) package that exports a **semantic-release** configuration object. It allows to easily use the same configuration across several projects.
A sharable configuration is an [npm](https://www.npmjs.com/) package that exports a **semantic-release** configuration object. It allows for use of the same configuration across several projects.
The shareable configurations to use can be set with the [extends](configuration.md#extends) option.
See [shareable configurations list](../extending/shareable-configurations-list.md).
+34 -2
View File
@@ -11,8 +11,11 @@ const getNextVersion = require('./lib/get-next-version');
const getCommits = require('./lib/get-commits');
const getLastRelease = require('./lib/get-last-release');
const {extractErrors} = require('./lib/utils');
const getGitAuthUrl = require('./lib/get-git-auth-url');
const logger = require('./lib/logger');
const {unshallow, gitHead: getGitHead, tag, push} = require('./lib/git');
const {unshallow, verifyAuth, isBranchUpToDate, gitHead: getGitHead, tag, push} = require('./lib/git');
const getError = require('./lib/get-error');
const {COMMIT_NAME, COMMIT_EMAIL} = require('./lib/definitions/constants');
marked.setOptions({renderer: new TerminalRenderer()});
@@ -22,6 +25,17 @@ async function run(options, plugins) {
if (!isCi && !options.dryRun && !options.noCi) {
logger.log('This run was not triggered in a known CI environment, running in dry-run mode.');
options.dryRun = true;
} else {
// When running on CI, set the commits author and commiter info and prevent the `git` CLI to prompt for username/password. See #703.
process.env = {
GIT_AUTHOR_NAME: COMMIT_NAME,
GIT_AUTHOR_EMAIL: COMMIT_EMAIL,
GIT_COMMITTER_NAME: COMMIT_NAME,
GIT_COMMITTER_EMAIL: COMMIT_EMAIL,
...process.env,
GIT_ASKPASS: 'echo',
GIT_TERMINAL_PROMPT: 0,
};
}
if (isCi && isPr && !options.noCi) {
@@ -40,13 +54,31 @@ async function run(options, plugins) {
await verify(options);
const {repositoryUrl} = options;
options.repositoryUrl = await getGitAuthUrl(options);
if (!(await isBranchUpToDate(options.branch))) {
logger.log(
"The local branch %s is behind the remote one, therefore a new version won't be published.",
options.branch
);
return false;
}
try {
await verifyAuth(options.repositoryUrl, options.branch);
} catch (err) {
logger.error(`The command "${err.cmd}" failed with the error message %s.`, err.stderr);
throw getError('EGITNOPERMISSION', {options, repositoryUrl});
}
logger.log('Run automated release from branch %s', options.branch);
logger.log('Call plugin %s', 'verify-conditions');
await plugins.verifyConditions({options, logger}, {settleAll: true});
// Unshallow the repo in order to get all the tags
await unshallow();
await unshallow(options.repositoryUrl);
const lastRelease = await getLastRelease(options.tagFormat, logger);
const commits = await getCommits(lastRelease.gitHead, options.branch, logger);
+9
View File
@@ -0,0 +1,9 @@
const RELEASE_TYPE = ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease'];
const FIRST_RELEASE = '1.0.0';
const COMMIT_NAME = 'semantic-release-bot';
const COMMIT_EMAIL = 'semantic-release-bot@martynus.net';
module.exports = {RELEASE_TYPE, FIRST_RELEASE, COMMIT_NAME, COMMIT_EMAIL};
+6 -4
View File
@@ -2,7 +2,7 @@ const url = require('url');
const {inspect} = require('util');
const {toLower, isString} = require('lodash');
const pkg = require('../../package.json');
const RELEASE_TYPE = require('./release-types');
const {RELEASE_TYPE} = require('./constants');
const homepage = url.format({...url.parse(pkg.homepage), ...{hash: null}});
const stringify = obj => (isString(obj) ? obj : inspect(obj, {breakLength: Infinity, depth: 2, maxArrayLength: 5}));
@@ -27,15 +27,17 @@ Please make sure to add the \`repositoryUrl\` to the [semantic-release configura
'docs/usage/configuration.md'
)}).`,
}),
EGITNOPERMISSION: ({options}) => ({
EGITNOPERMISSION: ({options, repositoryUrl}) => ({
message: 'The push permission to the Git repository is required.',
details: `**semantic-release** cannot push the version tag to the branch \`${
options.branch
}\` on remote Git repository.
}\` on 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.`,
)}) 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).`,
}),
EINVALIDTAGFORMAT: ({tagFormat}) => ({
message: 'Invalid `tagFormat` option.',
+1 -1
View File
@@ -1,5 +1,5 @@
const {isString, isFunction, isArray, isPlainObject} = require('lodash');
const RELEASE_TYPE = require('./release-types');
const {RELEASE_TYPE} = require('./constants');
const validatePluginConfig = conf => isString(conf) || isString(conf.path) || isFunction(conf);
-1
View File
@@ -1 +0,0 @@
module.exports = ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease'];
+14 -16
View File
@@ -2,15 +2,24 @@ const {castArray, pickBy, isUndefined, isNull, isString, isPlainObject} = requir
const readPkgUp = require('read-pkg-up');
const cosmiconfig = require('cosmiconfig');
const resolveFrom = require('resolve-from');
const gitUrlParse = require('git-url-parse');
const debug = require('debug')('semantic-release:config');
const {repoUrl, verifyAuth} = require('./git');
const {repoUrl} = require('./git');
const PLUGINS_DEFINITIONS = require('./definitions/plugins');
const plugins = require('./plugins');
const getGitAuthUrl = require('./get-git-auth-url');
const CONFIG_NAME = 'release';
const CONFIG_FILES = [
'package.json',
`.${CONFIG_NAME}rc`,
`.${CONFIG_NAME}rc.json`,
`.${CONFIG_NAME}rc.yaml`,
`.${CONFIG_NAME}rc.yml`,
`.${CONFIG_NAME}rc.js`,
`${CONFIG_NAME}.config.js`,
];
module.exports = async (opts, logger) => {
const {config} = (await cosmiconfig('release', {rcExtensions: true}).load(process.cwd())) || {};
const {config} = (await cosmiconfig(CONFIG_NAME, {searchPlaces: CONFIG_FILES}).search()) || {};
// Merge config file options and CLI/API options
let options = {...config, ...opts};
const pluginsPath = {};
@@ -52,10 +61,6 @@ module.exports = async (opts, logger) => {
...pickBy(options, option => !isUndefined(option) && !isNull(option)),
};
if (!await verifyAuth(options.repositoryUrl, options.branch)) {
options.repositoryUrl = options.repositoryUrl ? getGitAuthUrl(options.repositoryUrl) : options.repositoryUrl;
}
debug('options values: %O', options);
return {options, plugins: await plugins(options, pluginsPath, logger)};
@@ -63,12 +68,5 @@ module.exports = async (opts, logger) => {
async function pkgRepoUrl() {
const {pkg} = await readPkgUp({normalize: false});
const repositoryUrl = pkg && (isPlainObject(pkg.repository) ? pkg.repository.url : pkg.repository);
if (repositoryUrl) {
const {protocols} = gitUrlParse(repositoryUrl);
return `${gitUrlParse(repositoryUrl).toString(
protocols.includes('https') ? 'https' : protocols.includes('http') ? 'http' : undefined
)}${protocols.includes('https') || protocols.includes('http') ? '.git' : ''}`;
}
return pkg && (isPlainObject(pkg.repository) ? pkg.repository.url : pkg.repository);
}
+45 -16
View File
@@ -1,30 +1,59 @@
const {parse, format} = require('url');
const {isUndefined} = require('lodash');
const gitUrlParse = require('git-url-parse');
const hostedGitInfo = require('hosted-git-info');
const {verifyAuth} = require('./git');
const GIT_TOKENS = ['GIT_CREDENTIALS', 'GH_TOKEN', 'GITHUB_TOKEN', 'GL_TOKEN', 'GITLAB_TOKEN'];
const GIT_TOKENS = {
GIT_CREDENTIALS: undefined,
GH_TOKEN: undefined,
GITHUB_TOKEN: undefined,
GL_TOKEN: 'gitlab-ci-token:',
GITLAB_TOKEN: 'gitlab-ci-token:',
BB_TOKEN: 'x-token-auth:',
BITBUCKET_TOKEN: 'x-token-auth:',
};
/**
* Generate the git repository URL with creadentials.
* If the `gitCredentials` is defined, returns a http or https URL with Basic Authentication (`https://username:passowrd@hostname:port/path.git`).
* If the `gitCredentials` is undefined, returns the `repositoryUrl`. In that case it's expected for the user to have setup the Git authentication on the CI (for example via SSH keys).
* Determine the the git repository URL to use to push, either:
* - The `repositoryUrl` as is if allowed to push
* - The `repositoryUrl` converted to `https` or `http` with Basic Authentication
*
* @param {String} gitCredentials Basic HTTP Authentication credentials, can be `username:password` or a token for certain Git providers.
* @param {String} repositoryUrl The git repository URL.
* In addition, expand shortcut URLs (`owner/repo` => `https://github.com/owner/repo.git`) and transform `git+https` / `git+http` URLs to `https` / `http`.
*
* @param {String} repositoryUrl The user provided Git repository URL.
* @return {String} The formatted Git repository URL.
*/
module.exports = repositoryUrl => {
const envVar = GIT_TOKENS.find(envVar => !isUndefined(process.env[envVar]));
const gitCredentials = ['GL_TOKEN', 'GITLAB_TOKEN'].includes(envVar)
? `gitlab-ci-token:${process.env[envVar]}`
: process.env[envVar];
module.exports = async ({repositoryUrl, branch}) => {
const info = hostedGitInfo.fromUrl(repositoryUrl, {noGitPlus: true});
const {protocols} = gitUrlParse(repositoryUrl);
const protocol = protocols.includes('https') ? 'https' : protocols.includes('http') ? 'http' : 'https';
if (info && info.getDefaultRepresentation() === 'shortcut') {
// Expand shorthand URLs (such as `owner/repo` or `gitlab:owner/repo`)
repositoryUrl = info.https();
} else {
const {protocols} = gitUrlParse(repositoryUrl);
if (!gitCredentials) {
return protocols.includes('https') ? `${gitUrlParse(repositoryUrl).toString(protocol)}.git` : repositoryUrl;
// Replace `git+https` and `git+http` with `https` or `http`
if (protocols.includes('http') || protocols.includes('https')) {
repositoryUrl = format({
...parse(repositoryUrl),
...{protocol: protocols.includes('https') ? 'https' : 'http'},
});
}
}
return format({...parse(`${gitUrlParse(repositoryUrl).toString(protocol)}.git`), ...{auth: gitCredentials}});
// Test if push is allowed without transforming the URL (e.g. is ssh keys are set up)
try {
await verifyAuth(repositoryUrl, branch);
} catch (err) {
const envVar = Object.keys(GIT_TOKENS).find(envVar => !isUndefined(process.env[envVar]));
const gitCredentials = `${GIT_TOKENS[envVar] || ''}${process.env[envVar] || ''}`;
const {protocols, ...parsed} = gitUrlParse(repositoryUrl);
const protocol = protocols.includes('https') ? 'https' : protocols.includes('http') ? 'http' : 'https';
// If credentials are set via anvironment variables, convert the URL to http/https and add basic auth, otherwise return `repositoryUrl` as is
return gitCredentials ? {...parsed, protocols: [protocol], user: gitCredentials}.toString(protocol) : repositoryUrl;
}
return repositoryUrl;
};
+8 -11
View File
@@ -20,6 +20,7 @@ const {gitTags, isRefInHistory, gitTagHead} = require('./git');
* - Sort the versions
* - Retrive the highest version
*
* @param {String} tagFormat Git tag format.
* @param {Object} logger Global logger.
* @return {Promise<LastRelease>} The last tagged release or `undefined` if none is found.
*/
@@ -28,11 +29,10 @@ module.exports = async (tagFormat, logger) => {
// 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 tagRegexp = `^${escapeRegExp(template(tagFormat)({version: ' '})).replace(' ', '(.+)')}`;
const tags = (await gitTags())
.map(tag => {
return {gitTag: tag, version: (tag.match(tagRegexp) || new Array(2))[1]};
})
.map(tag => ({gitTag: tag, version: (tag.match(tagRegexp) || new Array(2))[1]}))
.filter(
tag => tag.version && semver.valid(semver.clean(tag.version)) && !semver.prerelease(semver.clean(tag.version))
)
@@ -40,14 +40,11 @@ module.exports = async (tagFormat, logger) => {
debug('found tags: %o', tags);
if (tags.length > 0) {
const {gitTag, version} = await pLocate(tags, tag => isRefInHistory(tag.gitTag), {
concurrency: 1,
preserveOrder: true,
});
logger.log('Found git tag %s associated with version %s', gitTag, version);
const tag = await pLocate(tags, tag => isRefInHistory(tag.gitTag), {concurrency: 1, preserveOrder: true});
return {gitHead: await gitTagHead(gitTag), gitTag, version};
if (tag) {
logger.log('Found git tag %s associated with version %s', tag.gitTag, tag.version);
return {gitHead: await gitTagHead(tag.gitTag), ...tag};
}
logger.log('No git tag version found');
+2 -1
View File
@@ -1,4 +1,5 @@
const semver = require('semver');
const {FIRST_RELEASE} = require('./definitions/constants');
module.exports = (type, lastRelease, logger) => {
let version;
@@ -6,7 +7,7 @@ module.exports = (type, lastRelease, logger) => {
version = semver.inc(lastRelease.version, type);
logger.log('The next release version is %s', version);
} else {
version = '1.0.0';
version = FIRST_RELEASE;
logger.log('There is no previous release, the next release version is %s', version);
}
+31 -11
View File
@@ -36,17 +36,25 @@ async function gitTags() {
*/
async function isRefInHistory(ref) {
try {
return (await execa('git', ['merge-base', '--is-ancestor', ref, 'HEAD'])).code === 0;
await execa('git', ['merge-base', '--is-ancestor', ref, 'HEAD']);
return true;
} catch (err) {
if (err.code === 1) {
return false;
}
debug(err);
throw err;
}
}
/**
* Unshallow the git repository (retriving every commits and tags).
*
* @param {String} repositoryUrl The remote repository URL.
*/
async function unshallow() {
await execa('git', ['fetch', '--unshallow', '--tags'], {reject: false});
async function unshallow(repositoryUrl) {
await execa('git', ['fetch', '--unshallow', '--tags', repositoryUrl], {reject: false});
}
/**
@@ -81,16 +89,17 @@ async function isGitRepo() {
/**
* Verify the write access authorization to remote repository with push dry-run.
*
* @param {String} origin The remote repository URL.
* @param {String} repositoryUrl The remote repository URL.
* @param {String} branch The repositoru branch for which to verify write access.
*
* @return {Boolean} `true` is authorized to push, falsy otherwise.
* @throws {Error} if not authorized to push.
*/
async function verifyAuth(origin, branch) {
async function verifyAuth(repositoryUrl, branch) {
try {
return (await execa('git', ['push', '--dry-run', origin, `HEAD:${branch}`])).code === 0;
await execa('git', ['push', '--dry-run', repositoryUrl, `HEAD:${branch}`]);
} catch (err) {
debug(err);
throw err;
}
}
@@ -107,18 +116,17 @@ async function tag(tagName) {
/**
* Push to the remote repository.
*
* @param {String} origin The remote repository URL.
* @param {String} repositoryUrl The remote repository URL.
* @param {String} branch The branch to push.
* @throws {Error} if the push failed.
*/
async function push(origin, branch) {
await execa('git', ['push', '--tags', origin, `HEAD:${branch}`]);
async function push(repositoryUrl, branch) {
await execa('git', ['push', '--tags', repositoryUrl, `HEAD:${branch}`]);
}
/**
* Verify a tag name is a valid Git reference.
*
* @method verifyTagName
* @param {string} tagName the tag name to verify.
* @return {boolean} `true` if valid, falsy otherwise.
*/
@@ -130,6 +138,17 @@ async function verifyTagName(tagName) {
}
}
/**
* Verify the local branch is up to date with the remote one.
*
* @param {String} branch The repository branch for which to verify status.
*
* @return {Boolean} `true` is the HEAD of the current local branch is the same as the HEAD of the remote branch, falsy otherwise.
*/
async function isBranchUpToDate(branch) {
return isRefInHistory(await execa.stdout('git', ['rev-parse', `origin/${branch}`]));
}
module.exports = {
gitTagHead,
gitTags,
@@ -142,4 +161,5 @@ module.exports = {
tag,
push,
verifyTagName,
isBranchUpToDate,
};
+15 -16
View File
@@ -37,21 +37,20 @@ module.exports = (pluginType, pluginsPath, globalOpts, pluginOpts, logger) => {
throw getError('EPLUGIN', {pluginType, pluginName});
}
return Object.defineProperty(
async input => {
const definition = PLUGINS_DEFINITIONS[pluginType];
try {
const result = await func(cloneDeep(input));
if (definition && definition.output && !definition.output.validator(result)) {
throw getError(PLUGINS_DEFINITIONS[pluginType].output.error, {result, pluginName});
}
return result;
} catch (err) {
extractErrors(err).forEach(err => Object.assign(err, {pluginName}));
throw err;
const validator = async input => {
const definition = PLUGINS_DEFINITIONS[pluginType];
try {
const result = await func(cloneDeep(input));
if (definition && definition.output && !definition.output.validator(result)) {
throw getError(PLUGINS_DEFINITIONS[pluginType].output.error, {result, pluginName});
}
},
'pluginName',
{value: pluginName, writable: false, enumerable: true}
);
return result;
} catch (err) {
extractErrors(err).forEach(err => Object.assign(err, {pluginName}));
throw err;
}
};
Reflect.defineProperty(validator, 'pluginName', {value: pluginName, writable: false, enumerable: true});
return validator;
};
+3 -5
View File
@@ -1,21 +1,19 @@
const {template} = require('lodash');
const AggregateError = require('aggregate-error');
const {isGitRepo, verifyAuth, verifyTagName} = require('./git');
const {isGitRepo, verifyTagName} = require('./git');
const getError = require('./get-error');
module.exports = async options => {
const errors = [];
if (!await isGitRepo()) {
if (!(await isGitRepo())) {
errors.push(getError('ENOGITREPO'));
} else if (!options.repositoryUrl) {
errors.push(getError('ENOREPOURL'));
} else if (!await verifyAuth(options.repositoryUrl, options.branch)) {
errors.push(getError('EGITNOPERMISSION', {options}));
}
// Verify that compiling the `tagFormat` produce a valid Git tag
if (!await verifyTagName(template(options.tagFormat)({version: '0.0.0'}))) {
if (!(await verifyTagName(template(options.tagFormat)({version: '0.0.0'})))) {
errors.push(getError('EINVALIDTAGFORMAT', {tagFormat: options.tagFormat}));
}
+11 -10
View File
@@ -26,15 +26,15 @@
"@semantic-release/release-notes-generator": "^6.0.0",
"aggregate-error": "^1.0.0",
"chalk": "^2.3.0",
"commander": "^2.11.0",
"cosmiconfig": "^4.0.0",
"cosmiconfig": "^5.0.1",
"debug": "^3.1.0",
"env-ci": "^1.0.0",
"execa": "^0.9.0",
"env-ci": "^2.0.0",
"execa": "^0.10.0",
"get-stream": "^3.0.0",
"git-log-parser": "^1.2.0",
"git-url-parse": "^8.1.0",
"git-url-parse": "^9.0.0",
"hook-std": "^0.4.0",
"hosted-git-info": "^2.6.0",
"lodash": "^4.17.4",
"marked": "^0.3.9",
"marked-terminal": "^2.0.0",
@@ -54,17 +54,17 @@
"delay": "^2.0.0",
"dockerode": "^2.5.2",
"file-url": "^2.0.2",
"fs-extra": "^5.0.0",
"fs-extra": "^6.0.0",
"got": "^8.0.0",
"js-yaml": "^3.10.0",
"mockserver-client": "^5.1.1",
"nock": "^9.0.2",
"nyc": "^11.2.1",
"p-retry": "^1.0.0",
"p-retry": "^2.0.0",
"proxyquire": "^2.0.0",
"sinon": "^4.0.0",
"sinon": "^5.0.1",
"tempy": "^0.2.1",
"xo": "^0.20.0"
"xo": "^0.21.0"
},
"engines": {
"node": ">=8.3"
@@ -104,7 +104,8 @@
"all": true
},
"prettier": {
"printWidth": 120
"printWidth": 120,
"trailingComma": "es5"
},
"publishConfig": {
"tag": "next"
+6 -6
View File
@@ -165,12 +165,12 @@ test.serial('Do not set properties in option for which arg is not in command lin
await cli();
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 'ci'));
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 'd'));
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 'dry-run'));
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 'debug'));
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 'r'));
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 't'));
t.false(Reflect.apply(Object.prototype.hasOwnProperty, run.args[0][0], ['ci']));
t.false(Reflect.apply(Object.prototype.hasOwnProperty, run.args[0][0], ['d']));
t.false(Reflect.apply(Object.prototype.hasOwnProperty, run.args[0][0], ['dry-run']));
t.false(Reflect.apply(Object.prototype.hasOwnProperty, run.args[0][0], ['debug']));
t.false(Reflect.apply(Object.prototype.hasOwnProperty, run.args[0][0], ['r']));
t.false(Reflect.apply(Object.prototype.hasOwnProperty, run.args[0][0], ['t']));
});
test.serial('Set "noCi" options to "true" with "--no-ci"', async t => {
+21 -37
View File
@@ -31,13 +31,12 @@ test.afterEach.always(() => {
});
test.serial('Default values, reading repositoryUrl from package.json', async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
const pkg = {repository: 'https://package.com/owner/module.git'};
const pkg = {repository: 'https://host.null/owner/package.git'};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
await gitCommits(['First']);
// Add remote.origin.url config
await gitAddConfig('remote.origin.url', 'git@repo.com:owner/module.git');
await gitAddConfig('remote.origin.url', 'git@host.null:owner/repo.git');
// Create package.json in repository root
await outputJson('./package.json', pkg);
@@ -45,28 +44,26 @@ test.serial('Default values, reading repositoryUrl from package.json', async t =
// Verify the default options are set
t.is(options.branch, 'master');
t.is(options.repositoryUrl, 'https://user:pass@package.com/owner/module.git');
t.is(options.repositoryUrl, 'https://host.null/owner/package.git');
t.is(options.tagFormat, `v\${version}`);
});
test.serial('Default values, reading repositoryUrl from repo if not set in package.json', async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Add remote.origin.url config
await gitAddConfig('remote.origin.url', 'https://hostname.com/owner/module.git');
await gitAddConfig('remote.origin.url', 'https://host.null/owner/module.git');
const {options} = await t.context.getConfig();
// Verify the default options are set
t.is(options.branch, 'master');
t.is(options.repositoryUrl, 'https://user:pass@hostname.com/owner/module.git');
t.is(options.repositoryUrl, 'https://host.null/owner/module.git');
t.is(options.tagFormat, `v\${version}`);
});
test.serial('Default values, reading repositoryUrl (http url) from package.json if not set in repo', async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
const pkg = {repository: 'https://hostname.com/owner/module.git'};
const pkg = {repository: 'https://host.null/owner/module.git'};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json in repository root
@@ -76,29 +73,16 @@ test.serial('Default values, reading repositoryUrl (http url) from package.json
// Verify the default options are set
t.is(options.branch, 'master');
t.is(options.repositoryUrl, 'https://user:pass@hostname.com/owner/module.git');
t.is(options.repositoryUrl, 'https://host.null/owner/module.git');
t.is(options.tagFormat, `v\${version}`);
});
test.serial('Do not add git credential to repositoryUrl if push is allowed', async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
// Create a git repository, set the current working directory at the root of the repo
const repositoryUrl = await gitRepo(true);
const pkg = {repository: repositoryUrl};
// Create package.json in repository root
await outputJson('./package.json', pkg);
const {options} = await t.context.getConfig();
t.is(options.repositoryUrl, repositoryUrl);
});
test.serial('Read options from package.json', async t => {
const release = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
generateNotes: 'generateNotes',
branch: 'test_branch',
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
@@ -119,7 +103,7 @@ test.serial('Read options from .releaserc.yml', async t => {
const release = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
@@ -140,7 +124,7 @@ test.serial('Read options from .releaserc.json', async t => {
const release = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
@@ -161,7 +145,7 @@ test.serial('Read options from .releaserc.js', async t => {
const release = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
@@ -182,7 +166,7 @@ test.serial('Read options from release.config.js', async t => {
const release = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
@@ -210,7 +194,7 @@ test.serial('Prioritise CLI/API parameters over file configuration and git repo'
repositoryUrl: 'http://cli-url.com/owner/package',
tagFormat: `cli\${version}`,
};
const pkg = {release, repository: 'git@hostname.com:owner/module.git'};
const pkg = {release, repository: 'git@host.null:owner/module.git'};
// Create a git repository, set the current working directory at the root of the repo
const repo = await gitRepo();
await gitCommits(['First']);
@@ -233,7 +217,7 @@ test.serial('Read configuration from file path in "extends"', async t => {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
generateNotes: 'generateNotes',
branch: 'test_branch',
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
@@ -261,7 +245,7 @@ test.serial('Read configuration from module path in "extends"', async t => {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
generateNotes: 'generateNotes',
branch: 'test_branch',
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
@@ -289,7 +273,7 @@ test.serial('Read configuration from an array of paths in "extends"', async t =>
verifyRelease: 'verifyRelease1',
analyzeCommits: {path: 'analyzeCommits1', param: 'analyzeCommits_param1'},
branch: 'test_branch',
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
};
const shareable2 = {
@@ -334,7 +318,7 @@ test.serial('Prioritize configuration from config file over "extends"', async t
generateNotes: 'generateNotesShareable',
publish: [{path: 'publishShareable', param: 'publishShareable_param'}],
branch: 'test_branch',
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
@@ -362,7 +346,7 @@ test.serial('Prioritize configuration from cli/API options over "extends"', asyn
extends: './shareable2.json',
branch: 'branch_opts',
publish: [{path: 'publishOpts', param: 'publishOpts_param'}],
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
};
const release = {
extends: './shareable1.json',
@@ -375,7 +359,7 @@ test.serial('Prioritize configuration from cli/API options over "extends"', asyn
generateNotes: 'generateNotesShareable1',
publish: [{path: 'publishShareable', param: 'publishShareable_param1'}],
branch: 'test_branch1',
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
};
const shareable2 = {
analyzeCommits: 'analyzeCommits2',
@@ -404,7 +388,7 @@ test.serial('Allow to unset properties defined in shareable config with "null"',
extends: './shareable.json',
analyzeCommits: null,
branch: 'test_branch',
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
};
const shareable = {
generateNotes: 'generateNotes',
@@ -438,7 +422,7 @@ test.serial('Allow to unset properties defined in shareable config with "undefin
extends: './shareable.json',
analyzeCommits: undefined,
branch: 'test_branch',
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
};
const shareable = {
generateNotes: 'generateNotes',
+134 -44
View File
@@ -1,105 +1,195 @@
import test from 'ava';
import getAuthUrl from '../lib/get-git-auth-url';
import {gitRepo} from './helpers/git-utils';
// Save the current process.env
const envBackup = Object.assign({}, process.env);
// Save the current working diretory
const cwd = process.cwd();
test.beforeEach(() => {
// Restore process.env
process.env = {};
delete process.env.GIT_CREDENTIALS;
delete process.env.GH_TOKEN;
delete process.env.GITHUB_TOKEN;
delete process.env.GL_TOKEN;
delete process.env.GITLAB_TOKEN;
delete process.env.BB_TOKEN;
delete process.env.BITBUCKET_TOKEN;
process.env.GIT_ASKPASS = 'echo';
process.env.GIT_TERMINAL_PROMPT = 0;
});
test.afterEach.always(() => {
// Restore process.env
process.env = envBackup;
// Restore the current working directory
process.chdir(cwd);
});
test.serial('Return the same "git" formatted URL if "gitCredentials" is not defined', t => {
t.is(getAuthUrl('git@host.com:owner/repo.git'), 'git@host.com:owner/repo.git');
test.serial('Return the same "git" formatted URL if "gitCredentials" is not defined', async t => {
t.is(await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}), 'git@host.null:owner/repo.git');
});
test.serial('Return the same "https" formatted URL if "gitCredentials" is not defined', t => {
t.is(getAuthUrl('https://host.com/owner/repo.git'), 'https://host.com/owner/repo.git');
test.serial('Return the same "https" formatted URL if "gitCredentials" is not defined', async t => {
t.is(await getAuthUrl({repositoryUrl: 'https://host.null/owner/repo.git'}), 'https://host.null/owner/repo.git');
});
test.serial(
'Return the "https" formatted URL if "gitCredentials" is not defined and repositoryUrl is a "git+https" URL',
t => {
t.is(getAuthUrl('git+https://host.com/owner/repo.git'), 'https://host.com/owner/repo.git');
async t => {
t.is(await getAuthUrl({repositoryUrl: 'git+https://host.null/owner/repo.git'}), 'https://host.null/owner/repo.git');
}
);
test.serial('Handle "https" URL with group and subgroup', t => {
t.is(getAuthUrl('https://host.com/group/subgroup/owner/repo.git'), 'https://host.com/group/subgroup/owner/repo.git');
test.serial('Do not add trailing ".git" if not present in the origian URL', async t => {
t.is(await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo'}), 'git@host.null:owner/repo');
});
test.serial('Handle "git" URL with group and subgroup', t => {
t.is(getAuthUrl('git@host.com:group/subgroup/owner/repo.git'), 'git@host.com:group/subgroup/owner/repo.git');
test.serial('Handle "https" URL with group and subgroup', async t => {
t.is(
await getAuthUrl({repositoryUrl: 'https://host.null/group/subgroup/owner/repo.git'}),
'https://host.null/group/subgroup/owner/repo.git'
);
});
test.serial('Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git" URL', t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(getAuthUrl('git@host.com:owner/repo.git'), 'https://user:pass@host.com/owner/repo.git');
test.serial('Handle "git" URL with group and subgroup', async t => {
t.is(
await getAuthUrl({repositoryUrl: 'git@host.null:group/subgroup/owner/repo.git'}),
'git@host.null:group/subgroup/owner/repo.git'
);
});
test.serial('Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "https" URL', t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(getAuthUrl('https://host.com/owner/repo.git'), 'https://user:pass@host.com/owner/repo.git');
test.serial('Convert shorthand URL', async t => {
t.is(
await getAuthUrl({repositoryUrl: 'semanitc-release/semanitc-release'}),
'https://github.com/semanitc-release/semanitc-release.git'
);
});
test.serial('Return the "http" formatted URL if "gitCredentials" is defined and repositoryUrl is a "http" URL', t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(getAuthUrl('http://host.com/owner/repo.git'), 'http://user:pass@host.com/owner/repo.git');
test.serial('Convert GitLab shorthand URL', async t => {
t.is(
await getAuthUrl({repositoryUrl: 'gitlab:semanitc-release/semanitc-release'}),
'https://gitlab.com/semanitc-release/semanitc-release.git'
);
});
test.serial(
'Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git+https" URL',
t => {
'Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git" URL',
async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(getAuthUrl('git+https://host.com/owner/repo.git'), 'https://user:pass@host.com/owner/repo.git');
t.is(
await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}),
'https://user:pass@host.null/owner/repo.git'
);
}
);
test.serial(
'Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "https" URL',
async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(
await getAuthUrl({repositoryUrl: 'https://host.null/owner/repo.git'}),
'https://user:pass@host.null/owner/repo.git'
);
}
);
test.serial(
'Return the "http" formatted URL if "gitCredentials" is defined and repositoryUrl is a "http" URL',
async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(
await getAuthUrl({repositoryUrl: 'http://host.null/owner/repo.git'}),
'http://user:pass@host.null/owner/repo.git'
);
}
);
test.serial(
'Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git+https" URL',
async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(
await getAuthUrl({repositoryUrl: 'git+https://host.null/owner/repo.git'}),
'https://user:pass@host.null/owner/repo.git'
);
}
);
test.serial(
'Return the "http" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git+http" URL',
t => {
async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(getAuthUrl('git+http://host.com/owner/repo.git'), 'http://user:pass@host.com/owner/repo.git');
t.is(
await getAuthUrl({repositoryUrl: 'git+http://host.null/owner/repo.git'}),
'http://user:pass@host.null/owner/repo.git'
);
}
);
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GH_TOKEN"', t => {
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GH_TOKEN"', async t => {
process.env.GH_TOKEN = 'token';
t.is(getAuthUrl('git@host.com:owner/repo.git'), 'https://token@host.com/owner/repo.git');
t.is(await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}), 'https://token@host.null/owner/repo.git');
});
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GITHUB_TOKEN"', t => {
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GITHUB_TOKEN"', async t => {
process.env.GITHUB_TOKEN = 'token';
t.is(getAuthUrl('git@host.com:owner/repo.git'), 'https://token@host.com/owner/repo.git');
t.is(await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}), 'https://token@host.null/owner/repo.git');
});
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GL_TOKEN"', t => {
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GL_TOKEN"', async t => {
process.env.GL_TOKEN = 'token';
t.is(getAuthUrl('git@host.com:owner/repo.git'), 'https://gitlab-ci-token:token@host.com/owner/repo.git');
t.is(
await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}),
'https://gitlab-ci-token:token@host.null/owner/repo.git'
);
});
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GITLAB_TOKEN"', t => {
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GITLAB_TOKEN"', async t => {
process.env.GITLAB_TOKEN = 'token';
t.is(getAuthUrl('git@host.com:owner/repo.git'), 'https://gitlab-ci-token:token@host.com/owner/repo.git');
});
test.serial('Handle "https" URL with group and subgroup, with "GIT_CREDENTIALS"', t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(
getAuthUrl('https://host.com/group/subgroup/owner/repo.git'),
'https://user:pass@host.com/group/subgroup/owner/repo.git'
await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}),
'https://gitlab-ci-token:token@host.null/owner/repo.git'
);
});
test.serial('Handle "git" URL with group and subgroup, with "GIT_CREDENTIALS', t => {
process.env.GIT_CREDENTIALS = 'user:pass';
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "BB_TOKEN"', async t => {
process.env.BB_TOKEN = 'token';
t.is(
getAuthUrl('git@host.com:group/subgroup/owner/repo.git'),
'https://user:pass@host.com/group/subgroup/owner/repo.git'
await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}),
'https://x-token-auth:token@host.null/owner/repo.git'
);
});
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "BITBUCKET_TOKEN"', async t => {
process.env.BITBUCKET_TOKEN = 'token';
t.is(
await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}),
'https://x-token-auth:token@host.null/owner/repo.git'
);
});
test.serial('Handle "https" URL with group and subgroup, with "GIT_CREDENTIALS"', async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(
await getAuthUrl({repositoryUrl: 'https://host.null/group/subgroup/owner/repo.git'}),
'https://user:pass@host.null/group/subgroup/owner/repo.git'
);
});
test.serial('Handle "git" URL with group and subgroup, with "GIT_CREDENTIALS', async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(
await getAuthUrl({repositoryUrl: 'git@host.null:group/subgroup/owner/repo.git'}),
'https://user:pass@host.null/group/subgroup/owner/repo.git'
);
});
test.serial('Do not add git credential to repositoryUrl if push is allowed', async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
// Create a git repository, set the current working directory at the root of the repo
const repositoryUrl = await gitRepo(true);
t.is(await getAuthUrl({repositoryUrl}), repositoryUrl);
});
+30
View File
@@ -62,6 +62,19 @@ test.serial('Get the highest tag in the history of the current branch', async t
t.deepEqual(result, {gitHead: commits[0].hash, gitTag: 'v2.0.0', version: '2.0.0'});
});
test.serial('Match the tag name from the begining of the string', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const commits = await gitCommits(['First']);
await gitTagVersion('prefix/v1.0.0');
await gitTagVersion('prefix/v2.0.0');
await gitTagVersion('other-prefix/v3.0.0');
const result = await getLastRelease(`prefix/v\${version}`, t.context.logger);
t.deepEqual(result, {gitHead: commits[0].hash, gitTag: 'prefix/v2.0.0', version: '2.0.0'});
});
test.serial('Return empty object if no valid tag is found', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
@@ -79,6 +92,23 @@ test.serial('Return empty object if no valid tag is found', async t => {
t.is(t.context.log.args[0][0], 'No git tag version found');
});
test.serial('Return empty object if no valid tag is found in history', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
await gitCommits(['First']);
await gitCheckout('other-branch');
await gitCommits(['Second']);
await gitTagVersion('v1.0.0');
await gitTagVersion('v2.0.0');
await gitTagVersion('v3.0.0');
await gitCheckout('master', false);
const result = await getLastRelease(`v\${version}`, t.context.logger);
t.deepEqual(result, {});
t.is(t.context.log.args[0][0], 'No git tag version found');
});
test.serial('Get the highest valid tag corresponding to the "tagFormat"', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
+37 -3
View File
@@ -11,6 +11,7 @@ import {
gitTags,
isGitRepo,
verifyTagName,
isBranchUpToDate,
} from '../lib/git';
import {
gitRepo,
@@ -22,6 +23,8 @@ import {
gitAddConfig,
gitCommitTag,
gitRemoteTagHead,
push as pushUtil,
reset,
} from './helpers/git-utils';
// Save the current working diretory
@@ -61,7 +64,7 @@ test.serial('Unshallow repository', async t => {
// Verify the shallow clone contains only one commit
t.is((await gitGetCommits()).length, 1);
await unshallow();
await unshallow(repo);
// Verify the shallow clone contains all the commits
t.is((await gitGetCommits()).length, 2);
@@ -69,10 +72,10 @@ test.serial('Unshallow repository', async t => {
test.serial('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
await gitRepo();
const repo = await gitRepo();
// Add commits to the master branch
await gitCommits(['First']);
await t.notThrows(unshallow());
await t.notThrows(unshallow(repo));
});
test.serial('Verify if the commit `sha` is in the direct history of the current branch', async t => {
@@ -88,6 +91,7 @@ test.serial('Verify if the commit `sha` is in the direct history of the current
t.true(await isRefInHistory(commits[0].hash));
t.falsy(await isRefInHistory(otherCommits[0].hash));
await t.throws(isRefInHistory('non-existant-sha'));
});
test.serial('Get the commit sha for a given tag or falsy if the tag does not exists', async t => {
@@ -183,3 +187,33 @@ test.serial('Throws error if obtaining the tags fails', async t => {
await t.throws(gitTags());
});
test.serial('Return "true" if repository is up to date', async t => {
await gitRepo(true);
await gitCommits(['First']);
await pushUtil();
t.true(await isBranchUpToDate('master'));
});
test.serial('Return falsy if repository is not up to date', async t => {
await gitRepo(true);
await gitCommits(['First']);
await gitCommits(['Second']);
await pushUtil();
t.true(await isBranchUpToDate('master'));
await reset();
t.falsy(await isBranchUpToDate('master'));
});
test.serial('Return "true" if local repository is ahead', async t => {
await gitRepo(true);
await gitCommits(['First']);
await pushUtil();
await gitCommits(['Second']);
t.true(await isBranchUpToDate('master'));
});
+29 -23
View File
@@ -20,7 +20,7 @@ import getStream from 'get-stream';
* If `withRemote` is `false`, creates a regular repository and initialize it. Change the current working directory to the repository root.
*
* @param {Boolean} withRemote `true` to create a shallow clone of a bare repository.
* @param {String} [branc='master'] The branch to initialize.
* @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') {
@@ -46,16 +46,16 @@ export async function gitRepo(withRemote, branch = 'master') {
* - Create an initial commits
* - Push to origin
*
* @param {String} origin The URL of the bare repository.
* @param {String} repositoryUrl The URL of the bare repository.
* @param {String} [branch='master'] the branch to initialize.
*/
export async function initBareRepo(origin, branch = 'master') {
export async function initBareRepo(repositoryUrl, branch = 'master') {
const clone = tempy.directory();
await execa('git', ['clone', '--no-hardlinks', origin, clone]);
await execa('git', ['clone', '--no-hardlinks', repositoryUrl, clone]);
process.chdir(clone);
await gitCheckout(branch);
await gitCommits(['Initial commit']);
await execa('git', ['push', origin, branch]);
await execa('git', ['push', repositoryUrl, branch]);
}
/**
@@ -98,7 +98,7 @@ export async function gitGetCommits(from) {
* Checkout a branch on the current git repository.
*
* @param {String} branch Branch name.
* @param {boolean} create `true` to create the branche ans switch, `false` to only switch.
* @param {Boolean} create `true` to create the branche ans switch, `false` to only switch.
*/
export async function gitCheckout(branch, create = true) {
await execa('git', create ? ['checkout', '-b', branch] : ['checkout', branch]);
@@ -121,39 +121,36 @@ export async function gitTagVersion(tagName, sha) {
await execa('git', sha ? ['tag', '-f', tagName, sha] : ['tag', tagName]);
}
export async function gitRemoteTagVersion(origin, tagName, sha = 'HEAD') {
await execa('git', ['push', origin, `${sha}:refs/tags/${tagName}`]);
}
/**
* Create a shallow clone of a git repository and change the current working directory to the cloned repository root.
* The shallow will contain a limited number of commit and no tags.
*
* @param {String} origin The path of the repository to clone.
* @param {String} repositoryUrl The path of the repository to clone.
* @param {String} [branch='master'] the branch to clone.
* @param {Number} [depth=1] The number of commit to clone.
* @return {String} The path of the cloned repository.
*/
export async function gitShallowClone(origin, branch = 'master', depth = 1) {
export async function gitShallowClone(repositoryUrl, branch = 'master', depth = 1) {
const dir = tempy.directory();
process.chdir(dir);
await execa('git', ['clone', '--no-hardlinks', '--no-tags', '-b', branch, '--depth', depth, origin, dir]);
await execa('git', ['clone', '--no-hardlinks', '--no-tags', '-b', branch, '--depth', depth, repositoryUrl, dir]);
return dir;
}
/**
* Create a git repo with a detached head from another git repository and change the current working directory to the new repository root.
*
* @param {String} origin The path of the repository to clone.
* @param {Number} head A commit sha of the origin repo that will become the detached head of the new one.
* @param {String} repositoryUrl The path of the repository to clone.
* @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(origin, head) {
export async function gitDetachedHead(repositoryUrl, head) {
const dir = tempy.directory();
process.chdir(dir);
await execa('git', ['init']);
await execa('git', ['remote', 'add', 'origin', origin]);
await execa('git', ['remote', 'add', 'origin', repositoryUrl]);
await execa('git', ['fetch']);
await execa('git', ['checkout', head]);
return dir;
@@ -183,12 +180,12 @@ export async function gitTagHead(tagName) {
/**
* Get the first commit sha referenced by the tag `tagName` in the remote repository.
*
* @param {String} origin The repository remote URL.
* @param {String} repositoryUrl The repository remote URL.
* @param {String} tagName The tag name to seach for.
* @return {String} The sha of the commit associated with `tagName` on the remote repository.
*/
export async function gitRemoteTagHead(origin, tagName) {
return (await execa.stdout('git', ['ls-remote', '--tags', origin, tagName]))
export async function gitRemoteTagHead(repositoryUrl, tagName) {
return (await execa.stdout('git', ['ls-remote', '--tags', repositoryUrl, tagName]))
.split('\n')
.filter(tag => Boolean(tag))
.map(tag => tag.match(/^(\S+)/)[1])[0];
@@ -208,10 +205,19 @@ export async function gitCommitTag(gitHead) {
/**
* Push to the remote repository.
*
* @param {String} origin The remote repository URL.
* @param {String} repositoryUrl The remote repository URL.
* @param {String} branch The branch to push.
* @throws {Error} if the push failed.
*/
export async function push(origin, branch) {
await execa('git', ['push', '--tags', origin, `HEAD:${branch}`]);
export async function push(repositoryUrl = 'origin', branch = 'master') {
await execa('git', ['push', '--tags', repositoryUrl, `HEAD:${branch}`]);
}
/**
* Reset repository to a commit.
*
* @param {String} [commit='HEAD~1'] Commit reference to reset the repo to.
*/
export async function reset(commit = 'HEAD~1') {
await execa('git', ['reset', commit]);
}
-4
View File
@@ -16,8 +16,6 @@ const gitCredential = `${GIT_USERNAME}:${GIT_PASSWORD}`;
/**
* Download the `gitbox` Docker image, create a new container and start it.
*
* @return {Promise} Promise that resolves when the container is started.
*/
async function start() {
await getStream(await docker.pull(IMAGE));
@@ -39,8 +37,6 @@ async function start() {
/**
* Stop and remote the `mockserver` Docker container.
*
* @return {Promise} Promise that resolves when the container is stopped.
*/
async function stop() {
await container.stop();
-4
View File
@@ -12,8 +12,6 @@ let container;
/**
* Download the `mockserver` Docker image, create a new container and start it.
*
* @return {Promise} Promise that resolves when the container is started.
*/
async function start() {
await getStream(await docker.pull(IMAGE));
@@ -39,8 +37,6 @@ async function start() {
/**
* Stop and remote the `mockserver` Docker container.
*
* @return {Promise} Promise that resolves when the container is stopped.
*/
async function stop() {
await container.stop();
-4
View File
@@ -16,8 +16,6 @@ let container;
/**
* Download the `npm-docker-couchdb` Docker image, create a new container and start it.
*
* @return {Promise} Promise that resolves when the container is started.
*/
async function start() {
await getStream(await docker.pull(IMAGE));
@@ -69,8 +67,6 @@ const authEnv = {
/**
* Stop and remote the `npm-docker-couchdb` Docker container.
*
* @return {Promise} Promise that resolves when the container is stopped.
*/
async function stop() {
await container.stop();
+47 -1
View File
@@ -5,6 +5,7 @@ import clearModule from 'clear-module';
import AggregateError from 'aggregate-error';
import SemanticReleaseError from '@semantic-release/error';
import DEFINITIONS from '../lib/definitions/plugins';
import {COMMIT_NAME, COMMIT_EMAIL} from '../lib/definitions/constants';
import {
gitHead as getGitHead,
gitTagHead,
@@ -14,6 +15,7 @@ import {
gitRemoteTagHead,
push,
gitShallowClone,
reset,
} from './helpers/git-utils';
// Save the current process.env
@@ -57,6 +59,7 @@ test.serial('Plugins are called with expected values', async t => {
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
commits = (await gitCommits(['Second'])).concat(commits);
await push();
const lastRelease = {version: '1.0.0', gitHead: commits[commits.length - 1].hash, gitTag: 'v1.0.0'};
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
@@ -155,6 +158,12 @@ test.serial('Plugins are called with expected values', async t => {
// Verify the tag has been created on the local and remote repo and reference the gitHead
t.is(await gitTagHead(nextRelease.gitTag), nextRelease.gitHead);
t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag), nextRelease.gitHead);
// Verify the author/commiter name and email hve been set
t.is(process.env.GIT_AUTHOR_NAME, COMMIT_NAME);
t.is(process.env.GIT_AUTHOR_EMAIL, COMMIT_EMAIL);
t.is(process.env.GIT_COMMITTER_NAME, COMMIT_NAME);
t.is(process.env.GIT_COMMITTER_EMAIL, COMMIT_EMAIL);
});
test.serial('Use custom tag format', async t => {
@@ -162,6 +171,7 @@ test.serial('Use custom tag format', async t => {
await gitCommits(['First']);
await gitTagVersion('test-1.0.0');
await gitCommits(['Second']);
await push();
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'test-2.0.0'};
const notes = 'Release notes';
@@ -198,6 +208,7 @@ test.serial('Use new gitHead, and recreate release notes if a prepare plugin cre
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
commits = (await gitCommits(['Second'])).concat(commits);
await push();
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
const notes = 'Release notes';
@@ -257,6 +268,7 @@ test.serial('Call all "success" plugins even if one errors out', async t => {
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
await push();
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
const notes = 'Release notes';
@@ -304,6 +316,7 @@ test.serial('Log all "verifyConditions" errors', async t => {
const repositoryUrl = await gitRepo(true);
// Add commits to the master branch
await gitCommits(['First']);
await push();
const error1 = new Error('error 1');
const error2 = new SemanticReleaseError('error 2', 'ERR2');
@@ -346,6 +359,7 @@ test.serial('Log all "verifyRelease" errors', async t => {
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
await push();
const error1 = new SemanticReleaseError('error 1', 'ERR1');
const error2 = new SemanticReleaseError('error 2', 'ERR2');
@@ -382,6 +396,7 @@ test.serial('Dry-run skips publish and success', async t => {
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
await push();
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
const notes = 'Release notes';
@@ -430,6 +445,7 @@ test.serial('Dry-run skips fail', async t => {
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
await push();
const error1 = new SemanticReleaseError('error 1', 'ERR1');
const error2 = new SemanticReleaseError('error 2', 'ERR2');
@@ -464,6 +480,7 @@ test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', a
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
await push();
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
const notes = 'Release notes';
@@ -513,6 +530,7 @@ test.serial('Allow local releases with "noCi" option', async t => {
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
await push();
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
const notes = 'Release notes';
@@ -566,6 +584,7 @@ test.serial('Accept "undefined" value returned by the "generateNotes" plugins',
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
commits = (await gitCommits(['Second'])).concat(commits);
await push();
const lastRelease = {version: '1.0.0', gitHead: commits[commits.length - 1].hash, gitTag: 'v1.0.0'};
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
@@ -623,6 +642,27 @@ test.serial('Returns falsy value if triggered by a PR', async t => {
);
});
test.serial('Returns falsy value if triggered on an outdated clone', async t => {
// Create a git repository, set the current working directory at the root of the repo
const repositoryUrl = await gitRepo(true);
// Add commits to the master branch
await gitCommits(['First']);
await gitCommits(['Second']);
await push();
await reset();
const semanticRelease = proxyquire('..', {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
t.falsy(await semanticRelease({repositoryUrl}));
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], [
"The local branch %s is behind the remote one, therefore a new version won't be published.",
'master',
]);
});
test.serial('Returns falsy value 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 repositoryUrl = await gitRepo(true);
@@ -656,6 +696,7 @@ test.serial('Returns falsy value if there is no relevant changes', async t => {
const repositoryUrl = await gitRepo(true);
// Add commits to the master branch
await gitCommits(['First']);
await push();
const analyzeCommits = stub().resolves();
const verifyRelease = stub().resolves();
@@ -685,7 +726,10 @@ test.serial('Returns falsy value if there is no relevant changes', async t => {
t.is(verifyRelease.callCount, 0);
t.is(generateNotes.callCount, 0);
t.is(publish.callCount, 0);
t.is(t.context.log.args[7][0], 'There are no relevant changes, so no new version is released.');
t.is(
t.context.log.args[t.context.log.args.length - 1][0],
'There are no relevant changes, so no new version is released.'
);
});
test.serial('Exclude commits with [skip release] or [release skip] from analysis', async t => {
@@ -702,6 +746,7 @@ test.serial('Exclude commits with [skip release] or [release skip] from analysis
'Test commit\n\n commit body\n[skip release]',
'Test commit\n\n commit body\n[release skip]',
]);
await push();
const analyzeCommits = stub().resolves();
const config = {branch: 'master', repositoryUrl, globalOpt: 'global'};
const options = {
@@ -826,6 +871,7 @@ test.serial('Throw an Error if plugin returns an unexpected value', async t => {
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
await push();
const verifyConditions = stub().resolves();
const analyzeCommits = stub().resolves('string');