diff --git a/README.md b/README.md index c145f795..9c77c311 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,12 @@ [![Build Status](https://github.com/semantic-release/gitlab/workflows/Test/badge.svg)](https://github.com/semantic-release/gitlab/actions?query=workflow%3ATest+branch%3Amaster) [![npm latest version](https://img.shields.io/npm/v/@semantic-release/gitlab/latest.svg)](https://www.npmjs.com/package/@semantic-release/gitlab) [![npm next version](https://img.shields.io/npm/v/@semantic-release/gitlab/next.svg)](https://www.npmjs.com/package/@semantic-release/gitlab) -| Step | Description | -|--------------------|-----------------------------------------------------------------------------------------------------------------------| -| `verifyConditions` | Verify the presence and the validity of the authentication (set via [environment variables](#environment-variables)). | -| `publish` | Publish a [GitLab release](https://docs.gitlab.com/ee/user/project/releases/). | -| `success` | Add a comment to each GitLab Issue or Merge Request resolved by the release. | +| Step | Description | +|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| +| `verifyConditions` | Verify the presence and the validity of the authentication (set via [environment variables](#environment-variables)). | +| `publish` | Publish a [GitLab release](https://docs.gitlab.com/ee/user/project/releases/). | +| `success` | Add a comment to each GitLab Issue or Merge Request resolved by the release. | +| `fail` | Open or update a [GitLab Issue](https://docs.gitlab.com/ee/user/project/issues/) with information about the errors that caused the release to fail. | ## Install @@ -70,13 +71,17 @@ If your GitLab instance is exposed via plain HTTP (not recommended!) use `HTTP_P ### Options -| Option | Description | Default | -|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `gitlabUrl` | The GitLab endpoint. | `GL_URL` or `GITLAB_URL` environment variable or CI provided environment variables if running on [GitLab CI/CD](https://docs.gitlab.com/ee/ci) or `https://gitlab.com`. | -| `gitlabApiPathPrefix` | The GitLab API prefix. | `GL_PREFIX` or `GITLAB_PREFIX` environment variable or CI provided environment variables if running on [GitLab CI/CD](https://docs.gitlab.com/ee/ci) or `/api/v4`. | -| `assets` | An array of files to upload to the release. See [assets](#assets). | - | -| `milestones` | An array of milestone titles to associate to the release. See [GitLab Release API](https://docs.gitlab.com/ee/api/releases/#create-a-release). | - | -| `successComment` | The comment to add to each Issue and Merge Request resolved by the release. Set to false to disable commenting. See [successComment](#successComment). | :tada: This issue has been resolved in version ${nextRelease.version} :tada:\n\nThe release is available on [GitLab release]() | +| Option | Description | Default | +|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `gitlabUrl` | The GitLab endpoint. | `GL_URL` or `GITLAB_URL` environment variable or CI provided environment variables if running on [GitLab CI/CD](https://docs.gitlab.com/ee/ci) or `https://gitlab.com`. | +| `gitlabApiPathPrefix` | The GitLab API prefix. | `GL_PREFIX` or `GITLAB_PREFIX` environment variable or CI provided environment variables if running on [GitLab CI/CD](https://docs.gitlab.com/ee/ci) or `/api/v4`. | +| `assets` | An array of files to upload to the release. See [assets](#assets). | - | +| `milestones` | An array of milestone titles to associate to the release. See [GitLab Release API](https://docs.gitlab.com/ee/api/releases/#create-a-release). | - | +| `successComment` | The comment to add to each Issue and Merge Request resolved by the release. Set to false to disable commenting. See [successComment](#successComment). | :tada: This issue has been resolved in version ${nextRelease.version} :tada:\n\nThe release is available on [GitLab release]() | +| `failComment` | The content of the issue created when a release fails. Set to `false` to disable opening an issue when a release fails. See [failComment](#failcomment). | Friendly message with links to **semantic-release** documentation and support, with the list of errors that caused the release to fail. | +| `failTitle` | The title of the issue created when a release fails. Set to `false` to disable opening an issue when a release fails. | `The automated release is failing 🚨` | +| `labels` | The [labels](https://docs.gitlab.com/ee/user/project/labels.html#labels) to add to the issue created when a release fails. Set to `false` to not add any label. | `semantic-release` | +| `assignee` | The [assignee](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#assignee) to add to the issue created when a release fails. | - | #### assets @@ -127,6 +132,23 @@ The message for the issue comments is generated with [Lodash template](https://l | `mergeRequest` | A [GitLab API Issue object](https://docs.gitlab.com/ee/api/issues.html#single-issue) the comment will be posted to, or `false` when commenting Merge Requests. | `issue` | A [GitHub API Merge Request object](https://docs.gitlab.com/ee/api/merge_requests.html#get-single-mr) the comment will be posted to, or `false` when commenting Issues. +#### failComment + +The message for the issue content is generated with [Lodash template](https://lodash.com/docs#template). The following variables are available: + +| Parameter | Description | +|-----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `branch` | The branch from which the release had failed. | +| `errors` | An `Array` of [SemanticReleaseError](https://github.com/semantic-release/error). Each error has the `message`, `code`, `pluginName` and `details` properties.
`pluginName` contains the package name of the plugin that threw the error.
`details` contains a information about the error formatted in markdown. | + +##### failComment example + +The `failComment` `This release from branch ${branch.name} had failed due to the following errors:\n- ${errors.map(err => err.message).join('\\n- ')}` will generate the comment: + +> This release from branch master had failed due to the following errors: +> - Error message 1 +> - Error message 2 + ## Compatibility The latest version of this plugin is compatible with all currently-supported versions of GitLab, [which is the current major version and previous two major versions](https://about.gitlab.com/support/statement-of-support.html#version-support). This plugin is not guaranteed to work with unsupported versions of GitLab. diff --git a/index.js b/index.js index 13f184a1..645c2933 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,7 @@ const verifyGitLab = require('./lib/verify'); const publishGitLab = require('./lib/publish'); const successGitLab = require('./lib/success'); +const failGitLab = require('./lib/fail'); let verified; @@ -29,4 +30,13 @@ async function success(pluginConfig, context) { return successGitLab(pluginConfig, context); } -module.exports = {verifyConditions, publish, success}; +async function fail(pluginConfig, context) { + if (!verified) { + await verifyGitLab(pluginConfig, context); + verified = true; + } + + return failGitLab(pluginConfig, context); +} + +module.exports = {verifyConditions, publish, success, fail}; diff --git a/lib/definitions/constants.js b/lib/definitions/constants.js index 8e157727..5f2d2c7d 100644 --- a/lib/definitions/constants.js +++ b/lib/definitions/constants.js @@ -1,3 +1,5 @@ +const HOME_URL = 'https://github.com/semantic-release/semantic-release'; + const RELEASE_NAME = 'GitLab release'; -module.exports = {RELEASE_NAME}; +module.exports = {HOME_URL, RELEASE_NAME}; diff --git a/lib/definitions/errors.js b/lib/definitions/errors.js index 7d99d5fd..053e2dd0 100644 --- a/lib/definitions/errors.js +++ b/lib/definitions/errors.js @@ -13,6 +13,30 @@ module.exports = { )}) must be an \`Array\` of \`Strings\` or \`Objects\` with a \`path\` property. Your configuration for the \`assets\` option is \`${stringify(assets)}\`.`, }), + EINVALIDFAILTITLE: ({failTitle}) => ({ + message: 'Invalid `failTitle` option.', + details: `The [failTitle option](${linkify('README.md#failtitle')}) if defined, must be a non empty \`String\`. + +Your configuration for the \`failTitle\` option is \`${stringify(failTitle)}\`.`, + }), + EINVALIDFAILCOMMENT: ({failComment}) => ({ + message: 'Invalid `failComment` option.', + details: `The [failComment option](${linkify('README.md#failcomment')}) if defined, must be a non empty \`String\`. + +Your configuration for the \`failComment\` option is \`${stringify(failComment)}\`.`, + }), + EINVALIDLABELS: ({labels}) => ({ + message: 'Invalid `labels` option.', + details: `The [labels option](${linkify('README.md#labels')}) if defined, must be a non empty \`String\`. + +Your configuration for the \`labels\` option is \`${stringify(labels)}\`.`, + }), + EINVALIDASSIGNEE: ({assignee}) => ({ + message: 'Invalid `assignee` option.', + details: `The [assignee option](${linkify('README.md#assignee')}) if defined, must be a non empty \`String\`. + + Your configuration for the \`assignee\` option is \`${stringify(assignee)}\`.`, + }), EINVALIDGITLABURL: () => ({ message: 'The git repository URL is not a valid GitLab URL.', details: `The **semantic-release** \`repositoryUrl\` option must a valid GitLab URL with the format \`/.git\`. diff --git a/lib/fail.js b/lib/fail.js new file mode 100644 index 00000000..38b00fd3 --- /dev/null +++ b/lib/fail.js @@ -0,0 +1,62 @@ +const {template} = require('lodash'); +const urlJoin = require('url-join'); +const got = require('got'); +const debug = require('debug')('semantic-release:gitlab'); +const resolveConfig = require('./resolve-config'); +const getRepoId = require('./get-repo-id'); +const getFailComment = require('./get-fail-comment'); + +module.exports = async (pluginConfig, context) => { + const { + options: {repositoryUrl}, + branch, + errors, + logger, + } = context; + const {gitlabToken, gitlabUrl, gitlabApiUrl, failComment, failTitle, labels, assignee} = resolveConfig( + pluginConfig, + context + ); + const repoId = getRepoId(context, gitlabUrl, repositoryUrl); + const encodedRepoId = encodeURIComponent(repoId); + const apiOptions = {headers: {'PRIVATE-TOKEN': gitlabToken}}; + + if (failComment === false || failTitle === false) { + logger.log('Skip issue creation.'); + } else { + const encodedFailTitle = encodeURIComponent(failTitle); + const description = failComment ? template(failComment)({branch, errors}) : getFailComment(branch, errors); + + const issuesEndpoint = urlJoin(gitlabApiUrl, `/projects/${repoId}/issues`); + const openFailTitleIssueEndpoint = urlJoin(issuesEndpoint, `?state=opened&search=${encodedFailTitle}`); + + const openFailTitleIssues = await got(openFailTitleIssueEndpoint, {...apiOptions}).json(); + const existingIssue = openFailTitleIssues.find(openFailTitleIssue => openFailTitleIssue.title === failTitle); + + if (existingIssue) { + debug('comment on issue: %O', existingIssue); + + const issueNotesEndpoint = urlJoin( + gitlabApiUrl, + `/projects/${existingIssue.project_id}/issues/${existingIssue.iid}/notes` + ); + await got.post(issueNotesEndpoint, { + ...apiOptions, + json: {body: description}, + }); + + const {id, web_url} = existingIssue; + logger.log('Commented on issue #%d: %s.', id, web_url); + } else { + const newIssue = {id: encodedRepoId, description, labels, title: failTitle, assignee_id: assignee}; + debug('create issue: %O', newIssue); + + /* eslint camelcase: off */ + const {id, web_url} = await got.post(issuesEndpoint, { + ...apiOptions, + json: newIssue, + }); + logger.log('Created issue #%d: %s.', id, web_url); + } + } +}; diff --git a/lib/get-fail-comment.js b/lib/get-fail-comment.js new file mode 100644 index 00000000..42d2c624 --- /dev/null +++ b/lib/get-fail-comment.js @@ -0,0 +1,46 @@ +const {HOME_URL} = require('./definitions/constants'); + +const FAQ_URL = `${HOME_URL}/blob/master/docs/support/FAQ.md`; +const GET_HELP_URL = `${HOME_URL}#get-help`; +const USAGE_DOC_URL = `${HOME_URL}/blob/master/docs/usage/README.md`; +const NEW_ISSUE_URL = `${HOME_URL}/issues/new`; + +const formatError = error => `### ${error.message} + +${error.details || + `Unfortunately this error doesn't have any additional information.${ + error.pluginName + ? ` Feel free to kindly ask the author of the \`${error.pluginName}\` plugin to add more helpful information.` + : '' + }`}`; + +module.exports = (branch, errors) => `## :rotating_light: The automated release from the \`${ + branch.name +}\` branch failed. :rotating_light: + +I recommend you give this issue a high priority, so other packages depending on you can benefit from your bug fixes and new features again. + +You can find below the list of errors reported by **semantic-release**. Each one of them has to be resolved in order to automatically publish your package. I'm sure you can fix this 💪. + +Errors are usually caused by a misconfiguration or an authentication problem. With each error reported below you will find explanation and guidance to help you to resolve it. + +Once all the errors are resolved, **semantic-release** will release your package the next time you push a commit to the \`${ + branch.name +}\` branch. You can also manually restart the failed CI job that runs **semantic-release**. + +If you are not sure how to resolve this, here are some links that can help you: +- [Usage documentation](${USAGE_DOC_URL}) +- [Frequently Asked Questions](${FAQ_URL}) +- [Support channels](${GET_HELP_URL}) + +If those don't help, or if this issue is reporting something you think isn't right, you can always ask the humans behind **[semantic-release](${NEW_ISSUE_URL})**. + +--- + +${errors.map(error => formatError(error)).join('\n\n---\n\n')} + +--- + +Good luck with your project ✨ + +Your **[semantic-release](${HOME_URL})** bot :package: :rocket:`; diff --git a/lib/get-success-comment.js b/lib/get-success-comment.js index 3e48dae0..a07783b1 100644 --- a/lib/get-success-comment.js +++ b/lib/get-success-comment.js @@ -1,4 +1,5 @@ -const HOME_URL = 'https://github.com/semantic-release/semantic-release'; +const {HOME_URL} = require('./definitions/constants'); + const linkify = releaseInfo => `${releaseInfo.url ? `[${releaseInfo.name}](${releaseInfo.url})` : `\`${releaseInfo.name}\``}`; diff --git a/lib/resolve-config.js b/lib/resolve-config.js index 3b0e9fad..581f96b5 100644 --- a/lib/resolve-config.js +++ b/lib/resolve-config.js @@ -3,7 +3,7 @@ const urlJoin = require('url-join'); const {HttpProxyAgent, HttpsProxyAgent} = require('hpagent'); module.exports = ( - {gitlabUrl, gitlabApiPathPrefix, assets, milestones, successComment}, + {gitlabUrl, gitlabApiPathPrefix, assets, milestones, successComment, failTitle, failComment, labels, assignee}, { envCi: {service} = {}, env: { @@ -45,6 +45,10 @@ module.exports = ( milestones: milestones ? castArray(milestones) : milestones, successComment, proxy: getProxyConfiguration(defaultedGitlabUrl, HTTP_PROXY, HTTPS_PROXY), + failTitle: isNil(failTitle) ? 'The automated release is failing 🚨' : failTitle, + failComment, + labels: isNil(labels) ? 'semantic-release' : labels === false ? false : labels, + assignee, }; }; diff --git a/lib/verify.js b/lib/verify.js index bc4fca57..4b523e2d 100644 --- a/lib/verify.js +++ b/lib/verify.js @@ -1,4 +1,4 @@ -const {isString, isPlainObject, isArray} = require('lodash'); +const {isString, isPlainObject, isNil, isArray} = require('lodash'); const urlJoin = require('url-join'); const got = require('got'); const debug = require('debug')('semantic-release:gitlab'); @@ -10,11 +10,16 @@ const getError = require('./get-error'); const isNonEmptyString = value => isString(value) && value.trim(); const isStringOrStringArray = value => isNonEmptyString(value) || (isArray(value) && value.every(isNonEmptyString)); const isArrayOf = validator => array => isArray(array) && array.every(value => validator(value)); +const canBeDisabled = validator => value => value === false || validator(value); const VALIDATORS = { assets: isArrayOf( asset => isStringOrStringArray(asset) || (isPlainObject(asset) && isStringOrStringArray(asset.path)) ), + failTitle: canBeDisabled(isNonEmptyString), + failComment: canBeDisabled(isNonEmptyString), + labels: isNonEmptyString, + assignee: isNonEmptyString, }; module.exports = async (pluginConfig, context) => { @@ -22,20 +27,23 @@ module.exports = async (pluginConfig, context) => { options: {repositoryUrl}, logger, } = context; - const errors = []; - const {gitlabToken, gitlabUrl, gitlabApiUrl, assets, proxy} = resolveConfig(pluginConfig, context); + const {gitlabToken, gitlabUrl, gitlabApiUrl, proxy, ...options} = resolveConfig(pluginConfig, context); const repoId = getRepoId(context, gitlabUrl, repositoryUrl); debug('apiUrl: %o', gitlabApiUrl); debug('repoId: %o', repoId); + const errors = Object.entries({...options}).reduce( + (errors, [option, value]) => + !isNil(value) && !VALIDATORS[option](value) + ? [...errors, getError(`EINVALID${option.toUpperCase()}`, {[option]: value})] + : errors, + [] + ); + if (!repoId) { errors.push(getError('EINVALIDGITLABURL')); } - if (assets && !VALIDATORS.assets(assets)) { - errors.push(getError('EINVALIDASSETS')); - } - if (!gitlabToken) { errors.push(getError('ENOGLTOKEN', {repositoryUrl})); } diff --git a/test/fail.test.js b/test/fail.test.js new file mode 100644 index 00000000..f634e88d --- /dev/null +++ b/test/fail.test.js @@ -0,0 +1,183 @@ +const test = require('ava'); +const nock = require('nock'); +const {stub} = require('sinon'); +const fail = require('../lib/fail'); +const authenticate = require('./helpers/mock-gitlab'); + +/* eslint camelcase: ["error", {properties: "never"}] */ + +test.beforeEach(t => { + // Mock logger + t.context.log = stub(); + t.context.error = stub(); + t.context.logger = {log: t.context.log, error: t.context.error}; +}); + +test.afterEach.always(() => { + // Clear nock + nock.cleanAll(); +}); + +test.serial('Post new issue if none exists yet', async t => { + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_TOKEN: 'gitlab_token'}; + const pluginConfig = {}; + const branch = {name: 'main'}; + const options = {repositoryUrl: `https://gitlab.com/${owner}/${repo}.git`}; + const errors = [{message: 'An error occured'}]; + const encodedFailTitle = encodeURIComponent('The automated release is failing 🚨'); + const gitlab = authenticate(env) + .get(`/projects/${owner}/${repo}/issues?state=opened&&search=${encodedFailTitle}`) + .reply(200, [ + { + id: 2, + iid: 2, + project_id: 1, + web_url: 'https://gitlab.com/test_user/test_repo/issues/2', + title: 'API should implemented authentication', + }, + ]) + .post(`/projects/${owner}/${repo}/issues`, { + id: 'test_user%2Ftest_repo', + description: `## :rotating_light: The automated release from the \`main\` branch failed. :rotating_light: + +I recommend you give this issue a high priority, so other packages depending on you can benefit from your bug fixes and new features again. + +You can find below the list of errors reported by **semantic-release**. Each one of them has to be resolved in order to automatically publish your package. I'm sure you can fix this 💪. + +Errors are usually caused by a misconfiguration or an authentication problem. With each error reported below you will find explanation and guidance to help you to resolve it. + +Once all the errors are resolved, **semantic-release** will release your package the next time you push a commit to the \`main\` branch. You can also manually restart the failed CI job that runs **semantic-release**. + +If you are not sure how to resolve this, here are some links that can help you: +- [Usage documentation](https://github.com/semantic-release/semantic-release/blob/master/docs/usage/README.md) +- [Frequently Asked Questions](https://github.com/semantic-release/semantic-release/blob/master/docs/support/FAQ.md) +- [Support channels](https://github.com/semantic-release/semantic-release#get-help) + +If those don't help, or if this issue is reporting something you think isn't right, you can always ask the humans behind **[semantic-release](https://github.com/semantic-release/semantic-release/issues/new)**. + +--- + +### An error occured + +Unfortunately this error doesn't have any additional information. + +--- + +Good luck with your project ✨ + +Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package: :rocket:`, + labels: 'semantic-release', + title: 'The automated release is failing 🚨', + }) + .reply(200); + + await fail(pluginConfig, {env, options, branch, errors, logger: t.context.logger}); + + t.true(gitlab.isDone()); +}); + +test.serial('Post comments to existing issue', async t => { + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_TOKEN: 'gitlab_token'}; + const pluginConfig = {}; + const branch = {name: 'main'}; + const options = {repositoryUrl: `https://gitlab.com/${owner}/${repo}.git`}; + const errors = [{message: 'An error occured'}]; + const encodedFailTitle = encodeURIComponent('The automated release is failing 🚨'); + const gitlab = authenticate(env) + .get(`/projects/${owner}/${repo}/issues?state=opened&search=${encodedFailTitle}`) + .reply(200, [ + { + id: 1, + iid: 1, + project_id: 1, + web_url: 'https://gitlab.com/test_user%2Ftest_repo/issues/1', + title: 'The automated release is failing 🚨', + }, + { + id: 2, + iid: 2, + project_id: 1, + web_url: 'https://gitlab.com/test_user%2Ftest_repo/issues/2', + title: 'API should implemented authentication', + }, + ]) + .post(`/projects/1/issues/1/notes`, { + body: `## :rotating_light: The automated release from the \`main\` branch failed. :rotating_light: + +I recommend you give this issue a high priority, so other packages depending on you can benefit from your bug fixes and new features again. + +You can find below the list of errors reported by **semantic-release**. Each one of them has to be resolved in order to automatically publish your package. I'm sure you can fix this 💪. + +Errors are usually caused by a misconfiguration or an authentication problem. With each error reported below you will find explanation and guidance to help you to resolve it. + +Once all the errors are resolved, **semantic-release** will release your package the next time you push a commit to the \`main\` branch. You can also manually restart the failed CI job that runs **semantic-release**. + +If you are not sure how to resolve this, here are some links that can help you: +- [Usage documentation](https://github.com/semantic-release/semantic-release/blob/master/docs/usage/README.md) +- [Frequently Asked Questions](https://github.com/semantic-release/semantic-release/blob/master/docs/support/FAQ.md) +- [Support channels](https://github.com/semantic-release/semantic-release#get-help) + +If those don't help, or if this issue is reporting something you think isn't right, you can always ask the humans behind **[semantic-release](https://github.com/semantic-release/semantic-release/issues/new)**. + +--- + +### An error occured + +Unfortunately this error doesn't have any additional information. + +--- + +Good luck with your project ✨ + +Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package: :rocket:`, + }) + .reply(200); + + await fail(pluginConfig, {env, options, branch, errors, logger: t.context.logger}); + + t.true(gitlab.isDone()); +}); + +test.serial('Post comments to existing issue with custom template', async t => { + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_TOKEN: 'gitlab_token'}; + const pluginConfig = { + failComment: `Error: Release for branch \${branch.name} failed with error: \${errors.map(error => error.message).join(';')}`, + failTitle: 'Semantic Release Failure', + }; + const branch = {name: 'main'}; + const options = {repositoryUrl: `https://gitlab.com/${owner}/${repo}.git`}; + const errors = [{message: 'An error occured'}]; + const encodedFailTitle = encodeURIComponent('Semantic Release Failure'); + const gitlab = authenticate(env) + .get(`/projects/${owner}/${repo}/issues?state=opened&search=${encodedFailTitle}`) + .reply(200, [ + { + id: 1, + iid: 1, + project_id: 1, + web_url: 'https://gitlab.com/test_user%2Ftest_repo/issues/1', + title: 'Semantic Release Failure', + }, + { + id: 2, + iid: 2, + project_id: 1, + web_url: 'https://gitlab.com/test_user%2Ftest_repo/issues/2', + title: 'API should implemented authentication', + }, + ]) + .post(`/projects/1/issues/1/notes`, { + body: `Error: Release for branch main failed with error: An error occured`, + }) + .reply(200); + + await fail(pluginConfig, {env, options, branch, errors, logger: t.context.logger}); + + t.true(gitlab.isDone()); +}); diff --git a/test/resolve-config.test.js b/test/resolve-config.test.js index f50e8a13..c5960341 100644 --- a/test/resolve-config.test.js +++ b/test/resolve-config.test.js @@ -3,6 +3,20 @@ const urlJoin = require('url-join'); const {HttpProxyAgent, HttpsProxyAgent} = require('hpagent'); const resolveConfig = require('../lib/resolve-config'); +const defaultOptions = { + gitlabToken: undefined, + gitlabUrl: 'https://gitlab.com', + gitlabApiUrl: urlJoin('https://gitlab.com', '/api/v4'), + assets: undefined, + milestones: undefined, + successComment: undefined, + failTitle: 'The automated release is failing 🚨', + failComment: undefined, + labels: 'semantic-release', + assignee: undefined, + proxy: {}, +}; + test('Returns user config', t => { const gitlabToken = 'TOKEN'; const gitlabUrl = 'https://host.com'; @@ -10,28 +24,27 @@ test('Returns user config', t => { const assets = ['file.js']; const postComments = true; const proxy = {}; + const labels = false; t.deepEqual( - resolveConfig({gitlabUrl, gitlabApiPathPrefix, assets, postComments}, {env: {GITLAB_TOKEN: gitlabToken}}), + resolveConfig({gitlabUrl, gitlabApiPathPrefix, assets, postComments, labels}, {env: {GITLAB_TOKEN: gitlabToken}}), { + ...defaultOptions, gitlabToken, gitlabUrl, gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix), assets, - milestones: undefined, - proxy, - successComment: undefined, + labels: false, } ); t.deepEqual(resolveConfig({gitlabUrl, gitlabApiPathPrefix, assets, proxy}, {env: {GITLAB_TOKEN: gitlabToken}}), { + ...defaultOptions, gitlabToken, gitlabUrl, gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix), assets, - milestones: undefined, proxy, - successComment: undefined, }); }); @@ -41,7 +54,6 @@ test('Returns user config via environment variables', t => { const gitlabApiPathPrefix = '/api/prefix'; const assets = ['file.js']; const milestones = ['1.2.3']; - const proxy = {}; t.deepEqual( resolveConfig( @@ -49,13 +61,12 @@ test('Returns user config via environment variables', t => { {env: {GITLAB_TOKEN: gitlabToken, GITLAB_URL: gitlabUrl, GITLAB_PREFIX: gitlabApiPathPrefix}} ), { + ...defaultOptions, gitlabToken, gitlabUrl, gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix), assets, milestones, - successComment: undefined, - proxy, } ); }); @@ -65,18 +76,17 @@ test('Returns user config via alternative environment variables', t => { const gitlabUrl = 'https://host.com'; const gitlabApiPathPrefix = '/api/prefix'; const assets = ['file.js']; - const proxy = {}; t.deepEqual( resolveConfig({assets}, {env: {GL_TOKEN: gitlabToken, GL_URL: gitlabUrl, GL_PREFIX: gitlabApiPathPrefix}}), { + ...defaultOptions, gitlabToken, gitlabUrl, gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix), assets, milestones: undefined, successComment: undefined, - proxy, } ); }); @@ -153,13 +163,11 @@ test('Returns user config via alternative environment variables with mismatching } ), { + ...defaultOptions, gitlabToken: 'TOKEN', gitlabUrl: 'http://host.com', gitlabApiUrl: 'http://host.com/api/prefix', assets: ['file.js'], - milestones: undefined, - successComment: undefined, - proxy: {}, } ); @@ -177,13 +185,11 @@ test('Returns user config via alternative environment variables with mismatching } ), { + ...defaultOptions, gitlabToken: 'TOKEN', gitlabUrl: 'https://host.com', gitlabApiUrl: 'https://host.com/api/prefix', assets: ['file.js'], - milestones: undefined, - successComment: undefined, - proxy: {}, } ); }); @@ -194,33 +200,21 @@ test('Returns default config', t => { const gitlabUrl = 'https://gitlab.com'; t.deepEqual(resolveConfig({}, {env: {GL_TOKEN: gitlabToken}}), { + ...defaultOptions, gitlabToken, - gitlabUrl: 'https://gitlab.com', - gitlabApiUrl: urlJoin('https://gitlab.com', '/api/v4'), - assets: undefined, - milestones: undefined, - successComment: undefined, - proxy: {}, }); t.deepEqual(resolveConfig({gitlabApiPathPrefix}, {env: {GL_TOKEN: gitlabToken}}), { + ...defaultOptions, gitlabToken, - gitlabUrl: 'https://gitlab.com', gitlabApiUrl: urlJoin('https://gitlab.com', gitlabApiPathPrefix), - assets: undefined, - milestones: undefined, - successComment: undefined, - proxy: {}, }); t.deepEqual(resolveConfig({gitlabUrl}, {env: {GL_TOKEN: gitlabToken}}), { + ...defaultOptions, gitlabToken, gitlabUrl: 'https://gitlab.com', gitlabApiUrl: urlJoin(gitlabUrl, '/api/v4'), - assets: undefined, - milestones: undefined, - successComment: undefined, - proxy: {}, }); }); @@ -239,13 +233,10 @@ test('Returns default config via GitLab CI/CD environment variables', t => { } ), { + ...defaultOptions, gitlabToken, gitlabUrl: 'http://ci-host.com', gitlabApiUrl: CI_API_V4_URL, - assets: undefined, - milestones: undefined, - successComment: undefined, - proxy: {}, } ); }); @@ -255,26 +246,28 @@ test('Returns user config over GitLab CI/CD environment variables', t => { const gitlabUrl = 'https://host.com'; const gitlabApiPathPrefix = '/api/prefix'; const assets = ['file.js']; + const failTitle = 'The automated release unfortunately failed!'; + const labels = 'bot,release-failed'; const CI_PROJECT_URL = 'http://ci-host.com/ci-owner/ci-repo'; const CI_PROJECT_PATH = 'ci-owner/ci-repo'; const CI_API_V4_URL = 'http://ci-host-api.com/prefix'; t.deepEqual( resolveConfig( - {gitlabUrl, gitlabApiPathPrefix, assets}, + {gitlabUrl, gitlabApiPathPrefix, assets, failTitle, labels}, { envCi: {service: 'gitlab'}, env: {GL_TOKEN: gitlabToken, CI_PROJECT_URL, CI_PROJECT_PATH, CI_API_V4_URL}, } ), { + ...defaultOptions, gitlabToken, gitlabUrl, gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix), assets, - milestones: undefined, - successComment: undefined, - proxy: {}, + failTitle: 'The automated release unfortunately failed!', + labels: 'bot,release-failed', } ); }); @@ -303,13 +296,10 @@ test('Returns user config via environment variables over GitLab CI/CD environmen } ), { + ...defaultOptions, gitlabToken, gitlabUrl, gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix), - assets: undefined, - milestones: undefined, - successComment: undefined, - proxy: {}, } ); }); @@ -338,13 +328,10 @@ test('Returns user config via alternative environment variables over GitLab CI/C } ), { + ...defaultOptions, gitlabToken, gitlabUrl, gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix), - assets: undefined, - milestones: undefined, - successComment: undefined, - proxy: {}, } ); }); @@ -364,13 +351,10 @@ test('Ignore GitLab CI/CD environment variables if not running on GitLab CI/CD', } ), { + ...defaultOptions, gitlabToken, gitlabUrl: 'https://gitlab.com', gitlabApiUrl: urlJoin('https://gitlab.com', '/api/v4'), - assets: undefined, - milestones: undefined, - successComment: undefined, - proxy: {}, } ); }); diff --git a/test/verify.test.js b/test/verify.test.js index 6e78dc59..2c9f91bf 100644 --- a/test/verify.test.js +++ b/test/verify.test.js @@ -451,7 +451,7 @@ test.serial('Throw AggregateError if multiple verification fails', async t => { const gitlabUrl = 'https://gitlab.com/context'; const assets = 42; - const [invalidUrlError, invalidAssetsError, noTokenError, ...errors] = await t.throwsAsync( + const [invalidAssetsError, invalidUrlError, noTokenError, ...errors] = await t.throwsAsync( verify( {assets, gitlabUrl}, {env, options: {repositoryUrl: 'git+ssh://git@gitlab.com/context.git'}, logger: t.context.logger} @@ -459,10 +459,10 @@ test.serial('Throw AggregateError if multiple verification fails', async t => { ); t.is(errors.length, 0); - t.is(invalidUrlError.name, 'SemanticReleaseError'); - t.is(invalidUrlError.code, 'EINVALIDGITLABURL'); t.is(invalidAssetsError.name, 'SemanticReleaseError'); t.is(invalidAssetsError.code, 'EINVALIDASSETS'); + t.is(invalidUrlError.name, 'SemanticReleaseError'); + t.is(invalidUrlError.code, 'EINVALIDGITLABURL'); t.is(noTokenError.name, 'SemanticReleaseError'); t.is(noTokenError.code, 'ENOGLTOKEN'); }); @@ -519,3 +519,267 @@ test.serial('Throw error if GitLab API return any other errors', async t => { t.is(error.response.statusCode, 500); t.true(gitlab.isDone()); }); + +test.serial('Throw SemanticReleaseError if "failTitle" option is not a String', async t => { + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_TOKEN: 'gitlab_token'}; + const failTitle = 42; + const gitlab = authenticate(env) + .get(`/projects/${owner}%2F${repo}`) + .reply(200, {permissions: {project_access: {access_level: 40}}}); + + const [error, ...errors] = await t.throwsAsync( + verify( + {failTitle}, + {env, options: {repositoryUrl: `https://gitlab.com/${owner}/${repo}.git`}, logger: t.context.logger} + ) + ); + + t.is(errors.length, 0); + t.is(error.name, 'SemanticReleaseError'); + t.is(error.code, 'EINVALIDFAILTITLE'); + t.true(gitlab.isDone()); +}); + +test.serial('Throw SemanticReleaseError if "failTitle" option is an empty String', async t => { + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_TOKEN: 'gitlab_token'}; + const failTitle = ''; + const gitlab = authenticate(env) + .get(`/projects/${owner}%2F${repo}`) + .reply(200, {permissions: {project_access: {access_level: 40}}}); + + const [error, ...errors] = await t.throwsAsync( + verify( + {failTitle}, + {env, options: {repositoryUrl: `https://gitlab.com/${owner}/${repo}.git`}, logger: t.context.logger} + ) + ); + + t.is(errors.length, 0); + t.is(error.name, 'SemanticReleaseError'); + t.is(error.code, 'EINVALIDFAILTITLE'); + t.true(gitlab.isDone()); +}); + +test.serial('Throw SemanticReleaseError if "failTitle" option is a whitespace String', async t => { + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_TOKEN: 'gitlab_token'}; + const failTitle = ' \n \r '; + const gitlab = authenticate(env) + .get(`/projects/${owner}%2F${repo}`) + .reply(200, {permissions: {project_access: {access_level: 40}}}); + + const [error, ...errors] = await t.throwsAsync( + verify( + {failTitle}, + {env, options: {repositoryUrl: `https://gitlab.com/${owner}/${repo}.git`}, logger: t.context.logger} + ) + ); + + t.is(errors.length, 0); + t.is(error.name, 'SemanticReleaseError'); + t.is(error.code, 'EINVALIDFAILTITLE'); + t.true(gitlab.isDone()); +}); + +test.serial('Throw SemanticReleaseError if "failComment" option is not a String', async t => { + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_TOKEN: 'gitlab_token'}; + const failComment = 42; + const gitlab = authenticate(env) + .get(`/projects/${owner}%2F${repo}`) + .reply(200, {permissions: {project_access: {access_level: 40}}}); + + const [error, ...errors] = await t.throwsAsync( + verify( + {failComment}, + {env, options: {repositoryUrl: `https://gitlab.com/${owner}/${repo}.git`}, logger: t.context.logger} + ) + ); + + t.is(errors.length, 0); + t.is(error.name, 'SemanticReleaseError'); + t.is(error.code, 'EINVALIDFAILCOMMENT'); + t.true(gitlab.isDone()); +}); + +test.serial('Throw SemanticReleaseError if "failComment" option is an empty String', async t => { + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_TOKEN: 'gitlab_token'}; + const failComment = ''; + const gitlab = authenticate(env) + .get(`/projects/${owner}%2F${repo}`) + .reply(200, {permissions: {project_access: {access_level: 40}}}); + + const [error, ...errors] = await t.throwsAsync( + verify( + {failComment}, + {env, options: {repositoryUrl: `https://gitlab.com/${owner}/${repo}.git`}, logger: t.context.logger} + ) + ); + + t.is(errors.length, 0); + t.is(error.name, 'SemanticReleaseError'); + t.is(error.code, 'EINVALIDFAILCOMMENT'); + t.true(gitlab.isDone()); +}); + +test.serial('Throw SemanticReleaseError if "failComment" option is a whitespace String', async t => { + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_TOKEN: 'gitlab_token'}; + const failComment = ' \n \r '; + const gitlab = authenticate(env) + .get(`/projects/${owner}%2F${repo}`) + .reply(200, {permissions: {project_access: {access_level: 40}}}); + + const [error, ...errors] = await t.throwsAsync( + verify( + {failComment}, + {env, options: {repositoryUrl: `https://gitlab.com/${owner}/${repo}.git`}, logger: t.context.logger} + ) + ); + + t.is(errors.length, 0); + t.is(error.name, 'SemanticReleaseError'); + t.is(error.code, 'EINVALIDFAILCOMMENT'); + t.true(gitlab.isDone()); +}); + +test.serial('Throw SemanticReleaseError if "labels" option is not a String', async t => { + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_TOKEN: 'gitlab_token'}; + const labels = 42; + const gitlab = authenticate(env) + .get(`/projects/${owner}%2F${repo}`) + .reply(200, {permissions: {project_access: {access_level: 40}}}); + + const [error, ...errors] = await t.throwsAsync( + verify( + {labels}, + {env, options: {repositoryUrl: `https://gitlab.com/${owner}/${repo}.git`}, logger: t.context.logger} + ) + ); + + t.is(errors.length, 0); + t.is(error.name, 'SemanticReleaseError'); + t.is(error.code, 'EINVALIDLABELS'); + t.true(gitlab.isDone()); +}); + +test.serial('Throw SemanticReleaseError if "labels" option is an empty String', async t => { + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_TOKEN: 'gitlab_token'}; + const labels = ''; + const gitlab = authenticate(env) + .get(`/projects/${owner}%2F${repo}`) + .reply(200, {permissions: {project_access: {access_level: 40}}}); + + const [error, ...errors] = await t.throwsAsync( + verify( + {labels}, + {env, options: {repositoryUrl: `https://gitlab.com/${owner}/${repo}.git`}, logger: t.context.logger} + ) + ); + + t.is(errors.length, 0); + t.is(error.name, 'SemanticReleaseError'); + t.is(error.code, 'EINVALIDLABELS'); + t.true(gitlab.isDone()); +}); + +test.serial('Throw SemanticReleaseError if "labels" option is a whitespace String', async t => { + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_TOKEN: 'gitlab_token'}; + const labels = ' \n \r '; + const gitlab = authenticate(env) + .get(`/projects/${owner}%2F${repo}`) + .reply(200, {permissions: {project_access: {access_level: 40}}}); + + const [error, ...errors] = await t.throwsAsync( + verify( + {labels}, + {env, options: {repositoryUrl: `https://gitlab.com/${owner}/${repo}.git`}, logger: t.context.logger} + ) + ); + + t.is(errors.length, 0); + t.is(error.name, 'SemanticReleaseError'); + t.is(error.code, 'EINVALIDLABELS'); + t.true(gitlab.isDone()); +}); + +test.serial('Throw SemanticReleaseError if "assignee" option is not a String', async t => { + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_TOKEN: 'gitlab_token'}; + const assignee = 42; + const gitlab = authenticate(env) + .get(`/projects/${owner}%2F${repo}`) + .reply(200, {permissions: {project_access: {access_level: 40}}}); + + const [error, ...errors] = await t.throwsAsync( + verify( + {assignee}, + {env, options: {repositoryUrl: `https://gitlab.com/${owner}/${repo}.git`}, logger: t.context.logger} + ) + ); + + t.is(errors.length, 0); + t.is(error.name, 'SemanticReleaseError'); + t.is(error.code, 'EINVALIDASSIGNEE'); + t.true(gitlab.isDone()); +}); + +test.serial('Throw SemanticReleaseError if "assignee" option is an empty String', async t => { + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_TOKEN: 'gitlab_token'}; + const assignee = ''; + const gitlab = authenticate(env) + .get(`/projects/${owner}%2F${repo}`) + .reply(200, {permissions: {project_access: {access_level: 40}}}); + + const [error, ...errors] = await t.throwsAsync( + verify( + {assignee}, + {env, options: {repositoryUrl: `https://gitlab.com/${owner}/${repo}.git`}, logger: t.context.logger} + ) + ); + + t.is(errors.length, 0); + t.is(error.name, 'SemanticReleaseError'); + t.is(error.code, 'EINVALIDASSIGNEE'); + t.true(gitlab.isDone()); +}); + +test.serial('Throw SemanticReleaseError if "assignee" option is a whitespace String', async t => { + const owner = 'test_user'; + const repo = 'test_repo'; + const env = {GITLAB_TOKEN: 'gitlab_token'}; + const assignee = ' \n \r '; + const gitlab = authenticate(env) + .get(`/projects/${owner}%2F${repo}`) + .reply(200, {permissions: {project_access: {access_level: 40}}}); + + const [error, ...errors] = await t.throwsAsync( + verify( + {assignee}, + {env, options: {repositoryUrl: `https://gitlab.com/${owner}/${repo}.git`}, logger: t.context.logger} + ) + ); + + t.is(errors.length, 0); + t.is(error.name, 'SemanticReleaseError'); + t.is(error.code, 'EINVALIDASSIGNEE'); + t.true(gitlab.isDone()); +});