From c40251161a49aeab9769e13c18e81c9eb61dae42 Mon Sep 17 00:00:00 2001 From: Jonas Schubert Date: Thu, 20 Jun 2024 21:54:06 +0200 Subject: [PATCH] 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()); + } +);