From 83037d09a6907eb378727a4e929fa1daa6dc1110 Mon Sep 17 00:00:00 2001 From: Jonas Schubert Date: Thu, 20 Jun 2024 21:09:49 +0200 Subject: [PATCH 1/8] chore(commenting): log warning if successComment or failComment is used for disabling these --- README.md | 6 +++--- lib/fail.js | 2 ++ lib/success.js | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 129df9f8..f115a1af 100644 --- a/README.md +++ b/README.md @@ -88,9 +88,9 @@ If you need to bypass the proxy for some hosts, configure the `NO_PROXY` environ | `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](gitlab_release_url) | -| `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 🚨` | +| `successComment` | The comment to add to each Issue and Merge Request resolved by the release. See [successComment](#successComment). | :tada: This issue has been resolved in version ${nextRelease.version} :tada:\n\nThe release is available on [GitLab release](gitlab_release_url) | +| `failComment` | The content of the issue created 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. | `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. Labels should be comma-separated as described in the [official docs](https://docs.gitlab.com/ee/api/issues.html#new-issue), e.g. `"semantic-release,bot"`. | `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. | - | diff --git a/lib/fail.js b/lib/fail.js index d2706236..18ac6fa6 100644 --- a/lib/fail.js +++ b/lib/fail.js @@ -24,6 +24,8 @@ export default async (pluginConfig, context) => { if (failComment === false || failTitle === false) { logger.log("Skip issue creation."); + logger.error(`Disabling failure reporting should be done using 'failCommentCondition'. +Using 'false' for 'failComment' or 'failTitle' is deprecated and will be removed in a future major version!`); } else { const encodedFailTitle = encodeURIComponent(failTitle); const description = failComment ? template(failComment)({ branch, errors }) : getFailComment(branch, errors); diff --git a/lib/success.js b/lib/success.js index bc697b90..3cce4204 100644 --- a/lib/success.js +++ b/lib/success.js @@ -22,6 +22,8 @@ export default async (pluginConfig, context) => { if (successComment === false) { logger.log("Skip commenting on issues and pull requests."); + logger.error(`Disabling commenting on issues and pull requests should be done using 'successCommentCondition'. +Using 'false' for 'successComment' is deprecated and will be removed in a future major version!`); } else { const releaseInfos = releases.filter((release) => Boolean(release.name)); try { From c40251161a49aeab9769e13c18e81c9eb61dae42 Mon Sep 17 00:00:00 2001 From: Jonas Schubert Date: Thu, 20 Jun 2024 21:54:06 +0200 Subject: [PATCH 2/8] feat(success-comment): allow to skip based on provided condition #480 #636 --- README.md | 46 ++++++++--- lib/resolve-config.js | 14 +++- lib/success.js | 65 ++++++++++------ test/resolve-config.test.js | 1 + test/success.test.js | 151 ++++++++++++++++++++++++++++++++++++ 5 files changed, 241 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index f115a1af..27389da9 100644 --- a/README.md +++ b/README.md @@ -82,17 +82,18 @@ If you need to bypass the proxy for some hosts, configure the `NO_PROXY` environ ### 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. See [successComment](#successComment). | :tada: This issue has been resolved in version ${nextRelease.version} :tada:\n\nThe release is available on [GitLab release](gitlab_release_url) | -| `failComment` | The content of the issue created 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. | `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. Labels should be comma-separated as described in the [official docs](https://docs.gitlab.com/ee/api/issues.html#new-issue), e.g. `"semantic-release,bot"`. | `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. | - | +| 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. See [successComment](#successComment). | :tada: This issue has been resolved in version ${nextRelease.version} :tada:\n\nThe release is available on [GitLab release](gitlab_release_url) | +| `successCommentCondition` | Use this as condition, when to comment on issues or merge requests. See [successCommentCondition](#successCommentCondition). | - | +| `failComment` | The content of the issue created 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. | `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. Labels should be comma-separated as described in the [official docs](https://docs.gitlab.com/ee/api/issues.html#new-issue), e.g. `"semantic-release,bot"`. | `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 @@ -146,6 +147,29 @@ The message for the issue comments is generated with [Lodash template](https://l | `issue` | 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. | | `mergeRequest` | A [GitLab 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. | +#### successCommentCondition + +The success comment condition is generated with [Lodash template](https://lodash.com/docs#template). The following variables are available: + +| Parameter | Description | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `branch` | `Object` with `name`, `type`, `channel`, `range` and `prerelease` properties of the branch from which the release is done. | +| `lastRelease` | `Object` with `version`, `channel`, `gitTag` and `gitHead` of the last release. | +| `nextRelease` | `Object` with `version`, `channel`, `gitTag`, `gitHead` and `notes` of the release being done. | +| `commits` | `Array` of commit `Object`s with `hash`, `subject`, `body` `message` and `author`. | +| `releases` | `Array` with a release `Object`s for each release published, with optional release data such as `name` and `url`. | +| `issue` | A [GitLab API Issue object](https://docs.gitlab.com/ee/api/issues.html#single-issue) the comment will be posted to. | +| `mergeRequest` | A [GitLab API Merge Request object](https://docs.gitlab.com/ee/api/merge_requests.html#get-single-mr) the comment will be posted to. | + +##### successCommentCondition example + +- do no create any comments at all: `"<% return false; %>"` +- to only comment on issues: `"<% return issue %>"` +- to only comment on merge requests: `"<% return mergeRequest %>"` +- you can use labels to filter issues: `"<% return issue.labels?.includes('semantic-release-relevant') %>"` + +> check the [GitLab API Merge Request object](https://docs.gitlab.com/ee/api/merge_requests.html#get-single-mr) or the [GitLab API Issue object](https://docs.gitlab.com/ee/api/issues.html#single-issue) for properties which can be used for the filter + #### failComment The message for the issue content is generated with [Lodash template](https://lodash.com/docs#template). The following variables are available: diff --git a/lib/resolve-config.js b/lib/resolve-config.js index e28a3aeb..c3995c87 100644 --- a/lib/resolve-config.js +++ b/lib/resolve-config.js @@ -3,7 +3,18 @@ import urlJoin from "url-join"; import { HttpProxyAgent, HttpsProxyAgent } from "hpagent"; export default ( - { gitlabUrl, gitlabApiPathPrefix, assets, milestones, successComment, failTitle, failComment, labels, assignee }, + { + gitlabUrl, + gitlabApiPathPrefix, + assets, + milestones, + successComment, + successCommentCondition, + failTitle, + failComment, + labels, + assignee, + }, { envCi: { service } = {}, env: { @@ -45,6 +56,7 @@ export default ( assets: assets ? castArray(assets) : assets, milestones: milestones ? castArray(milestones) : milestones, successComment, + successCommentCondition, proxy: getProxyConfiguration(defaultedGitlabUrl, HTTP_PROXY, HTTPS_PROXY, NO_PROXY), failTitle: isNil(failTitle) ? "The automated release is failing 🚨" : failTitle, failComment, diff --git a/lib/success.js b/lib/success.js index 3cce4204..d0711f77 100644 --- a/lib/success.js +++ b/lib/success.js @@ -15,7 +15,10 @@ export default async (pluginConfig, context) => { commits, releases, } = context; - const { gitlabToken, gitlabUrl, gitlabApiUrl, successComment, proxy } = resolveConfig(pluginConfig, context); + const { gitlabToken, gitlabUrl, gitlabApiUrl, successComment, successCommentCondition, proxy } = resolveConfig( + pluginConfig, + context + ); const repoId = getRepoId(context, gitlabUrl, repositoryUrl); const encodedRepoId = encodeURIComponent(repoId); const apiOptions = { headers: { "PRIVATE-TOKEN": gitlabToken } }; @@ -28,32 +31,46 @@ Using 'false' for 'successComment' is deprecated and will be removed in a future const releaseInfos = releases.filter((release) => Boolean(release.name)); try { const postCommentToIssue = (issue) => { - const issueNotesEndpoint = urlJoin(gitlabApiUrl, `/projects/${issue.project_id}/issues/${issue.iid}/notes`); - debug("Posting issue note to %s", issueNotesEndpoint); - const body = successComment - ? template(successComment)({ ...context, issue, mergeRequest: false }) - : getSuccessComment(issue, releaseInfos, nextRelease); - return got.post(issueNotesEndpoint, { - ...apiOptions, - ...proxy, - json: { body }, - }); + const canCommentOnIssue = successCommentCondition + ? template(successCommentCondition)({ ...context, issue, mergeRequest: false }) + : true; + if (canCommentOnIssue) { + const issueNotesEndpoint = urlJoin(gitlabApiUrl, `/projects/${issue.project_id}/issues/${issue.iid}/notes`); + debug("Posting issue note to %s", issueNotesEndpoint); + const body = successComment + ? template(successComment)({ ...context, issue, mergeRequest: false }) + : getSuccessComment(issue, releaseInfos, nextRelease); + return got.post(issueNotesEndpoint, { + ...apiOptions, + ...proxy, + json: { body }, + }); + } else { + logger.log("Skip commenting on issue #%d.", issue.id); + } }; const postCommentToMergeRequest = (mergeRequest) => { - const mergeRequestNotesEndpoint = urlJoin( - gitlabApiUrl, - `/projects/${mergeRequest.project_id}/merge_requests/${mergeRequest.iid}/notes` - ); - debug("Posting MR note to %s", mergeRequestNotesEndpoint); - const body = successComment - ? template(successComment)({ ...context, issue: false, mergeRequest }) - : getSuccessComment({ isMergeRequest: true, ...mergeRequest }, releaseInfos, nextRelease); - return got.post(mergeRequestNotesEndpoint, { - ...apiOptions, - ...proxy, - json: { body }, - }); + const canCommentOnMergeRequest = successCommentCondition + ? template(successCommentCondition)({ ...context, issue: false, mergeRequest }) + : true; + if (canCommentOnMergeRequest) { + const mergeRequestNotesEndpoint = urlJoin( + gitlabApiUrl, + `/projects/${mergeRequest.project_id}/merge_requests/${mergeRequest.iid}/notes` + ); + debug("Posting MR note to %s", mergeRequestNotesEndpoint); + const body = successComment + ? template(successComment)({ ...context, issue: false, mergeRequest }) + : getSuccessComment({ isMergeRequest: true, ...mergeRequest }, releaseInfos, nextRelease); + return got.post(mergeRequestNotesEndpoint, { + ...apiOptions, + ...proxy, + json: { body }, + }); + } else { + logger.log("Skip commenting on merge request #%d.", mergeRequest.iid); + } }; const getRelatedMergeRequests = async (commitHash) => { diff --git a/test/resolve-config.test.js b/test/resolve-config.test.js index 475e41dd..e40c5fbc 100644 --- a/test/resolve-config.test.js +++ b/test/resolve-config.test.js @@ -10,6 +10,7 @@ const defaultOptions = { assets: undefined, milestones: undefined, successComment: undefined, + successCommentCondition: undefined, failTitle: "The automated release is failing 🚨", failComment: undefined, labels: "semantic-release", diff --git a/test/success.test.js b/test/success.test.js index 47d0c392..4276367e 100644 --- a/test/success.test.js +++ b/test/success.test.js @@ -138,3 +138,154 @@ test.serial("Does not post comments when successComment is set to false", async t.true(gitlab.isDone()); }); + +test.serial("Does not post comments when successCommentCondition disables it", async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { GITLAB_TOKEN: "gitlab_token" }; + const pluginConfig = { successCommentCondition: "<% return false; %>" }; + const nextRelease = { version: "1.0.0" }; + const releases = [{ name: RELEASE_NAME, url: "https://gitlab.com/test_user/test_repo/-/releases/v1.0.0" }]; + const options = { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }; + const encodedRepoId = encodeURIComponent(`${owner}/${repo}`); + const commits = [{ hash: "abcdef" }, { hash: "fedcba" }]; + const gitlab = authenticate(env) + .get(`/projects/${encodedRepoId}/repository/commits/abcdef/merge_requests`) + .reply(200, [ + { project_id: 100, iid: 1, state: "merged" }, + { project_id: 200, iid: 2, state: "closed" }, + { project_id: 300, iid: 3, state: "merged" }, + ]) + .get(`/projects/${encodedRepoId}/repository/commits/fedcba/merge_requests`) + .reply(200, [{ project_id: 100, iid: 1, state: "merged" }]) + .get(`/projects/100/merge_requests/1/closes_issues`) + .reply(200, [ + { project_id: 100, iid: 11, state: "closed" }, + { project_id: 100, iid: 12, state: "open" }, + { project_id: 100, iid: 13, state: "closed" }, + ]) + .get(`/projects/300/merge_requests/3/closes_issues`) + .reply(200, []); + + await success(pluginConfig, { env, options, nextRelease, logger: t.context.logger, commits, releases }); + + t.true(gitlab.isDone()); +}); + +test.serial("Does not post comments on issues when successCommentCondition disables issue commenting", async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { GITLAB_TOKEN: "gitlab_token" }; + const pluginConfig = { successCommentCondition: "<% return !issue; %>" }; + const nextRelease = { version: "1.0.0" }; + const releases = [{ name: RELEASE_NAME, url: "https://gitlab.com/test_user/test_repo/-/releases/v1.0.0" }]; + const options = { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }; + const encodedRepoId = encodeURIComponent(`${owner}/${repo}`); + const commits = [{ hash: "abcdef" }, { hash: "fedcba" }]; + const gitlab = authenticate(env) + .get(`/projects/${encodedRepoId}/repository/commits/abcdef/merge_requests`) + .reply(200, [ + { project_id: 100, iid: 1, state: "merged" }, + { project_id: 200, iid: 2, state: "closed" }, + { project_id: 300, iid: 3, state: "merged" }, + ]) + .get(`/projects/${encodedRepoId}/repository/commits/fedcba/merge_requests`) + .reply(200, [{ project_id: 100, iid: 1, state: "merged" }]) + .get(`/projects/100/merge_requests/1/closes_issues`) + .reply(200, [ + { project_id: 100, iid: 11, state: "closed" }, + { project_id: 100, iid: 12, state: "open" }, + { project_id: 100, iid: 13, state: "closed" }, + ]) + .get(`/projects/300/merge_requests/3/closes_issues`) + .reply(200, []) + .post(`/projects/100/merge_requests/1/notes`, { + body: ":tada: This MR is included in version 1.0.0 :tada:\n\nThe release is available on [GitLab release](https://gitlab.com/test_user/test_repo/-/releases/v1.0.0).\n\nYour **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package: :rocket:", + }) + .reply(200) + .post(`/projects/300/merge_requests/3/notes`) + .reply(200); + + await success(pluginConfig, { env, options, nextRelease, logger: t.context.logger, commits, releases }); + + t.true(gitlab.isDone()); +}); + +test.serial("Only posts comments on issues which are found using the successCommentCondition", async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { GITLAB_TOKEN: "gitlab_token" }; + const pluginConfig = { successCommentCondition: "<% return issue.labels?.includes('semantic-release-relevant'); %>" }; + const nextRelease = { version: "1.0.0" }; + const releases = [{ name: RELEASE_NAME, url: "https://gitlab.com/test_user/test_repo/-/releases/v1.0.0" }]; + const options = { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }; + const encodedRepoId = encodeURIComponent(`${owner}/${repo}`); + const commits = [{ hash: "abcdef" }, { hash: "fedcba" }]; + const gitlab = authenticate(env) + .get(`/projects/${encodedRepoId}/repository/commits/abcdef/merge_requests`) + .reply(200, [ + { project_id: 100, iid: 1, state: "merged" }, + { project_id: 200, iid: 2, state: "closed" }, + { project_id: 300, iid: 3, state: "merged" }, + ]) + .get(`/projects/${encodedRepoId}/repository/commits/fedcba/merge_requests`) + .reply(200, [{ project_id: 100, iid: 1, state: "merged" }]) + .get(`/projects/100/merge_requests/1/closes_issues`) + .reply(200, [ + { project_id: 100, iid: 11, labels: "doing,bug", state: "closed" }, + { project_id: 100, iid: 12, labels: "todo,feature", state: "open" }, + { project_id: 100, iid: 13, labels: "testing,semantic-release-relevant,critical", state: "closed" }, + ]) + .get(`/projects/300/merge_requests/3/closes_issues`) + .reply(200, []) + .post(`/projects/100/issues/13/notes`, { + body: ":tada: This issue has been resolved in version 1.0.0 :tada:\n\nThe release is available on [GitLab release](https://gitlab.com/test_user/test_repo/-/releases/v1.0.0).\n\nYour **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package: :rocket:", + }) + .reply(200); + + await success(pluginConfig, { env, options, nextRelease, logger: t.context.logger, commits, releases }); + + t.true(gitlab.isDone()); +}); + +test.serial( + "Does not post comments on merge requets when successCommentCondition disables merge request commenting", + async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { GITLAB_TOKEN: "gitlab_token" }; + const pluginConfig = { successCommentCondition: "<% return !mergeRequest; %>" }; + const nextRelease = { version: "1.0.0" }; + const releases = [{ name: RELEASE_NAME, url: "https://gitlab.com/test_user/test_repo/-/releases/v1.0.0" }]; + const options = { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }; + const encodedRepoId = encodeURIComponent(`${owner}/${repo}`); + const commits = [{ hash: "abcdef" }, { hash: "fedcba" }]; + const gitlab = authenticate(env) + .get(`/projects/${encodedRepoId}/repository/commits/abcdef/merge_requests`) + .reply(200, [ + { project_id: 100, iid: 1, state: "merged" }, + { project_id: 200, iid: 2, state: "closed" }, + { project_id: 300, iid: 3, state: "merged" }, + ]) + .get(`/projects/${encodedRepoId}/repository/commits/fedcba/merge_requests`) + .reply(200, [{ project_id: 100, iid: 1, state: "merged" }]) + .get(`/projects/100/merge_requests/1/closes_issues`) + .reply(200, [ + { project_id: 100, iid: 11, state: "closed" }, + { project_id: 100, iid: 12, state: "open" }, + { project_id: 100, iid: 13, state: "closed" }, + ]) + .get(`/projects/300/merge_requests/3/closes_issues`) + .reply(200, []) + .post(`/projects/100/issues/11/notes`, { + body: ":tada: This issue has been resolved in version 1.0.0 :tada:\n\nThe release is available on [GitLab release](https://gitlab.com/test_user/test_repo/-/releases/v1.0.0).\n\nYour **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package: :rocket:", + }) + .reply(200) + .post(`/projects/100/issues/13/notes`) + .reply(200); + + await success(pluginConfig, { env, options, nextRelease, logger: t.context.logger, commits, releases }); + + t.true(gitlab.isDone()); + } +); From 29b2a3a61db0ccee97fb1779d233e40630bd6c0e Mon Sep 17 00:00:00 2001 From: Jonas Schubert Date: Thu, 20 Jun 2024 22:07:21 +0200 Subject: [PATCH 3/8] feat(fail-comment): allow to skip based on provided condition #480 #636 --- README.md | 22 +++++++ lib/fail.js | 59 +++++++++-------- lib/resolve-config.js | 2 + test/fail.test.js | 127 ++++++++++++++++++++++++++++++++++++ test/resolve-config.test.js | 1 + 5 files changed, 184 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 27389da9..41518fbd 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ If you need to bypass the proxy for some hosts, configure the `NO_PROXY` environ | `successCommentCondition` | Use this as condition, when to comment on issues or merge requests. See [successCommentCondition](#successCommentCondition). | - | | `failComment` | The content of the issue created 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. | `The automated release is failing 🚨` | +| `failCommentCondition` | Use this as condition, when to comment on or create an issues in case of failures. See [failCommentCondition](#failCommentCondition). | - | | `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. Labels should be comma-separated as described in the [official docs](https://docs.gitlab.com/ee/api/issues.html#new-issue), e.g. `"semantic-release,bot"`. | `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. | - | @@ -188,6 +189,27 @@ The `failComment` `This release from branch ${branch.name} had failed due to the > - Error message 1 > - Error message 2 +#### failCommentCondition + +The fail comment condition is generated with [Lodash template](https://lodash.com/docs#template). The following variables are available: + +| Parameter | Description | +| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `branch` | `Object` with `name`, `type`, `channel`, `range` and `prerelease` properties of the branch from which the release is done. | +| `lastRelease` | `Object` with `version`, `channel`, `gitTag` and `gitHead` of the last release. | +| `nextRelease` | `Object` with `version`, `channel`, `gitTag`, `gitHead` and `notes` of the release being done. | +| `commits` | `Array` of commit `Object`s with `hash`, `subject`, `body` `message` and `author`. | +| `releases` | `Array` with a release `Object`s for each release published, with optional release data such as `name` and `url`. | +| `issue` | A [GitLab API Issue object](https://docs.gitlab.com/ee/api/issues.html#single-issue) the comment will be posted to - only available if an open issue exists. | + +##### failCommentCondition example + +- do no create any comments at all: `"<% return false; %>"` +- to only comment on main branch: `"<% return branch.name === 'main' %>"` +- you can use labels to filter issues, i.e. to not comment if the issue is labeled with `wip`: `"<% return !issue.labels?.includes('wip') %>"` + +> check the [GitLab API Issue object](https://docs.gitlab.com/ee/api/issues.html#single-issue) for properties which can be used for the filter + ## 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/lib/fail.js b/lib/fail.js index 18ac6fa6..708c4453 100644 --- a/lib/fail.js +++ b/lib/fail.js @@ -14,10 +14,8 @@ export default async (pluginConfig, context) => { errors, logger, } = context; - const { gitlabToken, gitlabUrl, gitlabApiUrl, failComment, failTitle, labels, assignee } = resolveConfig( - pluginConfig, - context - ); + const { gitlabToken, gitlabUrl, gitlabApiUrl, failComment, failTitle, failCommentCondition, labels, assignee } = + resolveConfig(pluginConfig, context); const repoId = getRepoId(context, gitlabUrl, repositoryUrl); const encodedRepoId = encodeURIComponent(repoId); const apiOptions = { headers: { "PRIVATE-TOKEN": gitlabToken } }; @@ -36,32 +34,39 @@ Using 'false' for 'failComment' or 'failTitle' is deprecated and will be removed const openFailTitleIssues = await got(openFailTitleIssueEndpoint, { ...apiOptions }).json(); const existingIssue = openFailTitleIssues.find((openFailTitleIssue) => openFailTitleIssue.title === failTitle); - if (existingIssue) { - debug("comment on issue: %O", existingIssue); + const canCommentOnOrCreateIssue = failCommentCondition + ? template(failCommentCondition)({ ...context, issue: existingIssue }) + : true; + if (canCommentOnOrCreateIssue) { + 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 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); + 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, - }) - .json(); - logger.log("Created issue #%d: %s.", id, web_url); + /* eslint camelcase: off */ + const { id, web_url } = await got + .post(issuesEndpoint, { + ...apiOptions, + json: newIssue, + }) + .json(); + logger.log("Created issue #%d: %s.", id, web_url); + } + } else { + logger.log("Skip commenting on or creating an issue."); } } }; diff --git a/lib/resolve-config.js b/lib/resolve-config.js index c3995c87..295fce3a 100644 --- a/lib/resolve-config.js +++ b/lib/resolve-config.js @@ -12,6 +12,7 @@ export default ( successCommentCondition, failTitle, failComment, + failCommentCondition, labels, assignee, }, @@ -60,6 +61,7 @@ export default ( proxy: getProxyConfiguration(defaultedGitlabUrl, HTTP_PROXY, HTTPS_PROXY, NO_PROXY), failTitle: isNil(failTitle) ? "The automated release is failing 🚨" : failTitle, failComment, + failCommentCondition, labels: isNil(labels) ? "semantic-release" : labels === false ? false : labels, assignee, }; diff --git a/test/fail.test.js b/test/fail.test.js index dfa7e57c..4668a504 100644 --- a/test/fail.test.js +++ b/test/fail.test.js @@ -243,3 +243,130 @@ test.serial("Does not post comments when failComment is set to false", async (t) t.true(gitlab.isDone()); }); + +test.serial("Does not post comments when failCommentCondition disables it", async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { GITLAB_TOKEN: "gitlab_token" }; + const pluginConfig = { failCommentCondition: "<% return false; %>" }; + const branch = { name: "main" }; + const options = { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }; + const errors = [{ message: "An error occured" }]; + const encodedRepoId = encodeURIComponent(`${owner}/${repo}`); + const encodedFailTitle = encodeURIComponent("The automated release is failing 🚨"); + const gitlab = authenticate(env) + .get(`/projects/${encodedRepoId}/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", + }, + ]); + + await fail(pluginConfig, { env, options, branch, errors, logger: t.context.logger }); + + t.true(gitlab.isDone()); +}); + +test.serial("Does not post comments on existing issues when failCommentCondition disables this", async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { GITLAB_TOKEN: "gitlab_token" }; + const pluginConfig = { failCommentCondition: "<% return !issue; %>" }; + const branch = { name: "main" }; + const options = { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }; + const errors = [{ message: "An error occured" }]; + const encodedRepoId = encodeURIComponent(`${owner}/${repo}`); + const encodedFailTitle = encodeURIComponent("The automated release is failing 🚨"); + const gitlab = authenticate(env) + .get(`/projects/${encodedRepoId}/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", + }, + ]); + + await fail(pluginConfig, { env, options, branch, errors, logger: t.context.logger }); + + t.true(gitlab.isDone()); +}); + +test.serial("Post new issue if none exists yet with disabled comment on existing issues", async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { GITLAB_TOKEN: "gitlab_token" }; + const pluginConfig = { failCommentCondition: "<% return !issue; %>" }; + const branch = { name: "main" }; + const options = { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }; + const errors = [{ message: "An error occured" }]; + const encodedRepoId = encodeURIComponent(`${owner}/${repo}`); + const encodedFailTitle = encodeURIComponent("The automated release is failing 🚨"); + const gitlab = authenticate(env) + .get(`/projects/${encodedRepoId}/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/${encodedRepoId}/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, { id: 3, web_url: "https://gitlab.com/test_user/test_repo/-/issues/3" }); + + await fail(pluginConfig, { env, options, branch, errors, logger: t.context.logger }); + + t.true(gitlab.isDone()); + t.deepEqual(t.context.log.args[0], [ + "Created issue #%d: %s.", + 3, + "https://gitlab.com/test_user/test_repo/-/issues/3", + ]); +}); diff --git a/test/resolve-config.test.js b/test/resolve-config.test.js index e40c5fbc..91efa766 100644 --- a/test/resolve-config.test.js +++ b/test/resolve-config.test.js @@ -13,6 +13,7 @@ const defaultOptions = { successCommentCondition: undefined, failTitle: "The automated release is failing 🚨", failComment: undefined, + failCommentCondition: undefined, labels: "semantic-release", assignee: undefined, proxy: {}, From 38b3409e3fb45bc23f78b769400ac07ce99aa3e7 Mon Sep 17 00:00:00 2001 From: Jonas Schubert Date: Mon, 1 Jul 2024 07:26:02 +0200 Subject: [PATCH 4/8] chore(commenting): improve message of deprecation logging Co-authored-by: Florian Greinacher --- lib/fail.js | 4 ++-- lib/success.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/fail.js b/lib/fail.js index 708c4453..1421da3f 100644 --- a/lib/fail.js +++ b/lib/fail.js @@ -22,8 +22,8 @@ export default async (pluginConfig, context) => { if (failComment === false || failTitle === false) { logger.log("Skip issue creation."); - logger.error(`Disabling failure reporting should be done using 'failCommentCondition'. -Using 'false' for 'failComment' or 'failTitle' is deprecated and will be removed in a future major version!`); + logger.error(`Failure reporting should be disabled via 'failCommentCondition'. +Using 'false' for 'failComment' or 'failTitle' is deprecated and will be removed in a future major version.`); } else { const encodedFailTitle = encodeURIComponent(failTitle); const description = failComment ? template(failComment)({ branch, errors }) : getFailComment(branch, errors); diff --git a/lib/success.js b/lib/success.js index d0711f77..891d94a1 100644 --- a/lib/success.js +++ b/lib/success.js @@ -25,8 +25,8 @@ export default async (pluginConfig, context) => { if (successComment === false) { logger.log("Skip commenting on issues and pull requests."); - logger.error(`Disabling commenting on issues and pull requests should be done using 'successCommentCondition'. -Using 'false' for 'successComment' is deprecated and will be removed in a future major version!`); + logger.error(`Issue and pull request comments should be disabled via 'successCommentCondition'. +Using 'false' for 'successComment' is deprecated and will be removed in a future major version.`); } else { const releaseInfos = releases.filter((release) => Boolean(release.name)); try { From de9896221059aab7c14cf56defb60980440989f5 Mon Sep 17 00:00:00 2001 From: Jonas Schubert Date: Mon, 1 Jul 2024 07:28:34 +0200 Subject: [PATCH 5/8] test(fail): improve readability --- test/fail.test.js | 34 +++++----------------------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/test/fail.test.js b/test/fail.test.js index 4668a504..30c69889 100644 --- a/test/fail.test.js +++ b/test/fail.test.js @@ -309,7 +309,10 @@ test.serial("Post new issue if none exists yet with disabled comment on existing const owner = "test_user"; const repo = "test_repo"; const env = { GITLAB_TOKEN: "gitlab_token" }; - const pluginConfig = { failCommentCondition: "<% return !issue; %>" }; + const pluginConfig = { + failComment: `Error: Release for branch \${branch.name} failed with error: \${errors.map(error => error.message).join(';')}`, + failCommentCondition: "<% return !issue; %>", + }; const branch = { name: "main" }; const options = { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }; const errors = [{ message: "An error occured" }]; @@ -328,34 +331,7 @@ test.serial("Post new issue if none exists yet with disabled comment on existing ]) .post(`/projects/${encodedRepoId}/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:`, + description: `Error: Release for branch main failed with error: An error occured`, labels: "semantic-release", title: "The automated release is failing 🚨", }) From 05d1a59331ad92e2bb8c10cfe83bd4b27404a520 Mon Sep 17 00:00:00 2001 From: Jonas Schubert Date: Mon, 1 Jul 2024 10:08:58 +0200 Subject: [PATCH 6/8] feat(commenting): disable if set to false --- README.md | 4 ++-- lib/fail.js | 2 ++ lib/success.js | 2 ++ test/fail.test.js | 15 +++++++++++++++ test/success.test.js | 16 ++++++++++++++++ 5 files changed, 37 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 41518fbd..0ae85479 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ The success comment condition is generated with [Lodash template](https://lodash ##### successCommentCondition example -- do no create any comments at all: `"<% return false; %>"` +- do no create any comments at all: set to `false` or templating: `"<% return false; %>"` - to only comment on issues: `"<% return issue %>"` - to only comment on merge requests: `"<% return mergeRequest %>"` - you can use labels to filter issues: `"<% return issue.labels?.includes('semantic-release-relevant') %>"` @@ -204,7 +204,7 @@ The fail comment condition is generated with [Lodash template](https://lodash.co ##### failCommentCondition example -- do no create any comments at all: `"<% return false; %>"` +- do no create any comments at all: set to `false` or templating: `"<% return false; %>"` - to only comment on main branch: `"<% return branch.name === 'main' %>"` - you can use labels to filter issues, i.e. to not comment if the issue is labeled with `wip`: `"<% return !issue.labels?.includes('wip') %>"` diff --git a/lib/fail.js b/lib/fail.js index 1421da3f..018f4519 100644 --- a/lib/fail.js +++ b/lib/fail.js @@ -24,6 +24,8 @@ export default async (pluginConfig, context) => { logger.log("Skip issue creation."); logger.error(`Failure reporting should be disabled via 'failCommentCondition'. Using 'false' for 'failComment' or 'failTitle' is deprecated and will be removed in a future major version.`); + } else if (failCommentCondition === false) { + logger.log("Skip issue creation."); } else { const encodedFailTitle = encodeURIComponent(failTitle); const description = failComment ? template(failComment)({ branch, errors }) : getFailComment(branch, errors); diff --git a/lib/success.js b/lib/success.js index 891d94a1..910f0201 100644 --- a/lib/success.js +++ b/lib/success.js @@ -27,6 +27,8 @@ export default async (pluginConfig, context) => { logger.log("Skip commenting on issues and pull requests."); logger.error(`Issue and pull request comments should be disabled via 'successCommentCondition'. Using 'false' for 'successComment' is deprecated and will be removed in a future major version.`); + } else if (successCommentCondition === false) { + logger.log("Skip commenting on issues and pull requests."); } else { const releaseInfos = releases.filter((release) => Boolean(release.name)); try { diff --git a/test/fail.test.js b/test/fail.test.js index 30c69889..9c6f52df 100644 --- a/test/fail.test.js +++ b/test/fail.test.js @@ -346,3 +346,18 @@ test.serial("Post new issue if none exists yet with disabled comment on existing "https://gitlab.com/test_user/test_repo/-/issues/3", ]); }); + +test.serial("Does not post comments when failCommentCondition is set to false", async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { GITLAB_TOKEN: "gitlab_token" }; + const pluginConfig = { failCommentCondition: false }; + const branch = { name: "main" }; + const options = { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }; + const errors = [{ message: "An error occured" }]; + const gitlab = authenticate(env); + + await fail(pluginConfig, { env, options, branch, errors, logger: t.context.logger }); + + t.true(gitlab.isDone()); +}); diff --git a/test/success.test.js b/test/success.test.js index 4276367e..9e6272f3 100644 --- a/test/success.test.js +++ b/test/success.test.js @@ -289,3 +289,19 @@ test.serial( t.true(gitlab.isDone()); } ); + +test.serial("Does not post comments when successCommentCondition is set to false", async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { GITLAB_TOKEN: "gitlab_token" }; + const pluginConfig = { successCommentCondition: false }; + const nextRelease = { version: "1.0.0" }; + const releases = [{ name: RELEASE_NAME, url: "https://gitlab.com/test_user/test_repo/-/releases/v1.0.0" }]; + const options = { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }; + const commits = [{ hash: "abcdef" }, { hash: "fedcba" }]; + const gitlab = authenticate(env); + + await success(pluginConfig, { env, options, nextRelease, logger: t.context.logger, commits, releases }); + + t.true(gitlab.isDone()); +}); From 495bc77a34a303d453da3d470f32ca8f5b249786 Mon Sep 17 00:00:00 2001 From: Olabode Lawal-Shittabey Date: Wed, 3 Jul 2024 15:38:32 +0100 Subject: [PATCH 7/8] Update README.md Co-authored-by: Gregor Martynus <39992+gr2m@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0ae85479..8417e4d8 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ The success comment condition is generated with [Lodash template](https://lodash ##### successCommentCondition example -- do no create any comments at all: set to `false` or templating: `"<% return false; %>"` +- do not create any comments at all: set to `false` or templating: `"<% return false; %>"` - to only comment on issues: `"<% return issue %>"` - to only comment on merge requests: `"<% return mergeRequest %>"` - you can use labels to filter issues: `"<% return issue.labels?.includes('semantic-release-relevant') %>"` From efd9caface3046140c310a75dd11cb026da23124 Mon Sep 17 00:00:00 2001 From: Olabode Lawal-Shittabey Date: Wed, 3 Jul 2024 15:38:40 +0100 Subject: [PATCH 8/8] Update README.md Co-authored-by: Gregor Martynus <39992+gr2m@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8417e4d8..12c7b21d 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ The fail comment condition is generated with [Lodash template](https://lodash.co ##### failCommentCondition example -- do no create any comments at all: set to `false` or templating: `"<% return false; %>"` +- do not create any comments at all: set to `false` or templating: `"<% return false; %>"` - to only comment on main branch: `"<% return branch.name === 'main' %>"` - you can use labels to filter issues, i.e. to not comment if the issue is labeled with `wip`: `"<% return !issue.labels?.includes('wip') %>"`