Skip to content

Commit 42c31ac

Browse files
authored
feat: comment on issues and merge requests resolved by current release (#340)
BREAKING CHANGE: By default all related issues and MRs are commented. Set the `successComment` option to `false` to disable this behavior.
1 parent cf9c4b9 commit 42c31ac

9 files changed

+320
-17
lines changed

Diff for: README.md

+22-6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
|--------------------|-----------------------------------------------------------------------------------------------------------------------|
1111
| `verifyConditions` | Verify the presence and the validity of the authentication (set via [environment variables](#environment-variables)). |
1212
| `publish` | Publish a [GitLab release](https://docs.gitlab.com/ee/user/project/releases/). |
13+
| `success` | Add a comment to each GitLab Issue or Merge Request resolved by the release. |
1314

1415
## Install
1516

@@ -58,12 +59,13 @@ Create a [personal access token](https://docs.gitlab.com/ce/user/profile/persona
5859

5960
### Options
6061

61-
| Option | Description | Default |
62-
|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
63-
| `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`. |
64-
| `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`. |
65-
| `assets` | An array of files to upload to the release. See [assets](#assets). | - |
66-
| `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). | - |
62+
| Option | Description | Default |
63+
|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
64+
| `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`. |
65+
| `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`. |
66+
| `assets` | An array of files to upload to the release. See [assets](#assets). | - |
67+
| `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). | - |
68+
| `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>) |
6769

6870
#### assets
6971

@@ -100,6 +102,20 @@ distribution` and `MyLibrary CSS distribution` in the GitLab release.
100102
`css` files in the `dist` directory and its sub-directories excluding the minified version, plus the
101103
`build/MyLibrary.zip` file and label it `MyLibrary` in the GitLab release.
102104

105+
#### successComment
106+
107+
The message for the issue comments is generated with [Lodash template](https://lodash.com/docs#template). The following variables are available:
108+
109+
| Parameter | Description |
110+
|----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
111+
| `branch` | `Object` with `name`, `type`, `channel`, `range` and `prerelease` properties of the branch from which the release is done. |
112+
| `lastRelease` | `Object` with `version`, `channel`, `gitTag` and `gitHead` of the last release. |
113+
| `nextRelease` | `Object` with `version`, `channel`, `gitTag`, `gitHead` and `notes` of the release being done. |
114+
| `commits` | `Array` of commit `Object`s with `hash`, `subject`, `body` `message` and `author`. |
115+
| `releases` | `Array` with a release `Object`s for each release published, with optional release data such as `name` and `url`. |
116+
| `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.
117+
| `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.
118+
103119
## Compatibility
104120

105121
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 for: index.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const verifyGitLab = require('./lib/verify');
44
const publishGitLab = require('./lib/publish');
5+
const successGitLab = require('./lib/success');
56

67
let verified;
78

@@ -19,4 +20,13 @@ async function publish(pluginConfig, context) {
1920
return publishGitLab(pluginConfig, context);
2021
}
2122

22-
module.exports = {verifyConditions, publish};
23+
async function success(pluginConfig, context) {
24+
if (!verified) {
25+
await verifyGitLab(pluginConfig, context);
26+
verified = true;
27+
}
28+
29+
return successGitLab(pluginConfig, context);
30+
}
31+
32+
module.exports = {verifyConditions, publish, success};

Diff for: lib/definitions/constants.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const RELEASE_NAME = 'GitLab release';
2+
3+
module.exports = {RELEASE_NAME};

Diff for: lib/get-success-comment.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const HOME_URL = 'https://github.com/semantic-release/semantic-release';
2+
const linkify = releaseInfo =>
3+
`${releaseInfo.url ? `[${releaseInfo.name}](${releaseInfo.url})` : `\`${releaseInfo.name}\``}`;
4+
5+
module.exports = (issueOrMergeRequest, releaseInfos, nextRelease) =>
6+
`:tada: This ${issueOrMergeRequest.isMergeRequest ? 'MR is included' : 'issue has been resolved'} in version ${
7+
nextRelease.version
8+
} :tada:${
9+
releaseInfos.length > 0
10+
? `\n\nThe release is available on${
11+
releaseInfos.length === 1
12+
? ` ${linkify(releaseInfos[0])}`
13+
: `:\n${releaseInfos.map(releaseInfo => `- ${linkify(releaseInfo)}`).join('\n')}`
14+
}`
15+
: ''
16+
}
17+
Your **[semantic-release](${HOME_URL})** bot :package::rocket:`;

Diff for: lib/publish.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const debug = require('debug')('semantic-release:gitlab');
99
const resolveConfig = require('./resolve-config');
1010
const getRepoId = require('./get-repo-id');
1111
const getAssets = require('./glob-assets');
12+
const {RELEASE_NAME} = require('./definitions/constants');
1213

1314
module.exports = async (pluginConfig, context) => {
1415
const {
@@ -117,5 +118,7 @@ module.exports = async (pluginConfig, context) => {
117118

118119
logger.log('Published GitLab release: %s', gitTag);
119120

120-
return {url: urlJoin(gitlabUrl, encodedRepoId, `/-/releases/${encodedGitTag}`), name: 'GitLab release'};
121+
const releaseUrl = urlJoin(gitlabUrl, encodedRepoId, `/-/releases/${encodedGitTag}`);
122+
123+
return {name: RELEASE_NAME, url: releaseUrl};
121124
};

Diff for: lib/resolve-config.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const {castArray, isNil} = require('lodash');
22
const urlJoin = require('url-join');
33

44
module.exports = (
5-
{gitlabUrl, gitlabApiPathPrefix, assets, milestones},
5+
{gitlabUrl, gitlabApiPathPrefix, assets, milestones, successComment},
66
{
77
envCi: {service} = {},
88
env: {
@@ -29,7 +29,6 @@ module.exports = (
2929
(service === 'gitlab' && CI_PROJECT_URL && CI_PROJECT_PATH
3030
? CI_PROJECT_URL.replace(new RegExp(`/${CI_PROJECT_PATH}$`), '')
3131
: 'https://gitlab.com');
32-
3332
return {
3433
gitlabToken: GL_TOKEN || GITLAB_TOKEN,
3534
gitlabUrl: defaultedGitlabUrl,
@@ -41,5 +40,6 @@ module.exports = (
4140
: urlJoin(defaultedGitlabUrl, isNil(userGitlabApiPathPrefix) ? '/api/v4' : userGitlabApiPathPrefix),
4241
assets: assets ? castArray(assets) : assets,
4342
milestones: milestones ? castArray(milestones) : milestones,
43+
successComment,
4444
};
4545
};

Diff for: lib/success.js

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
const {uniqWith, isEqual, template} = require('lodash');
2+
const urlJoin = require('url-join');
3+
const got = require('got');
4+
const debug = require('debug')('semantic-release:gitlab');
5+
const resolveConfig = require('./resolve-config');
6+
const getRepoId = require('./get-repo-id');
7+
const getSuccessComment = require('./get-success-comment');
8+
9+
module.exports = async (pluginConfig, context) => {
10+
const {
11+
options: {repositoryUrl},
12+
nextRelease,
13+
logger,
14+
commits,
15+
releases,
16+
} = context;
17+
const {gitlabToken, gitlabUrl, gitlabApiUrl, successComment} = resolveConfig(pluginConfig, context);
18+
const repoId = getRepoId(context, gitlabUrl, repositoryUrl);
19+
const encodedRepoId = encodeURIComponent(repoId);
20+
const apiOptions = {headers: {'PRIVATE-TOKEN': gitlabToken}};
21+
22+
if (successComment === false) {
23+
logger.log('Skip commenting on issues and pull requests.');
24+
} else {
25+
const releaseInfos = releases.filter(release => Boolean(release.name));
26+
try {
27+
const postCommentToIssue = issue => {
28+
const issueNotesEndpoint = urlJoin(gitlabApiUrl, `/projects/${issue.project_id}/issues/${issue.iid}/notes`);
29+
debug('Posting issue note to %s', issueNotesEndpoint);
30+
const body = successComment
31+
? template(successComment)({...context, issue, mergeRequest: false})
32+
: getSuccessComment(issue, releaseInfos, nextRelease);
33+
return got.post(issueNotesEndpoint, {
34+
...apiOptions,
35+
json: {body},
36+
});
37+
};
38+
39+
const postCommentToMergeRequest = mergeRequest => {
40+
const mergeRequestNotesEndpoint = urlJoin(
41+
gitlabApiUrl,
42+
`/projects/${mergeRequest.project_id}/merge_requests/${mergeRequest.iid}/notes`
43+
);
44+
debug('Posting MR note to %s', mergeRequestNotesEndpoint);
45+
const body = successComment
46+
? template(successComment)({...context, issue: false, mergeRequest})
47+
: getSuccessComment({isMergeRequest: true, ...mergeRequest}, releaseInfos, nextRelease);
48+
return got.post(mergeRequestNotesEndpoint, {
49+
...apiOptions,
50+
json: {body},
51+
});
52+
};
53+
54+
const getRelatedMergeRequests = async commitHash => {
55+
const relatedMergeRequestsEndpoint = urlJoin(
56+
gitlabApiUrl,
57+
`/projects/${encodedRepoId}/repository/commits/${commitHash}/merge_requests`
58+
);
59+
debug('Getting MRs from %s', relatedMergeRequestsEndpoint);
60+
const relatedMergeRequests = await got
61+
.get(relatedMergeRequestsEndpoint, {
62+
...apiOptions,
63+
})
64+
.json();
65+
66+
return relatedMergeRequests.filter(x => x.state === 'merged');
67+
};
68+
69+
const getRelatedIssues = async mergeRequest => {
70+
const relatedIssuesEndpoint = urlJoin(
71+
gitlabApiUrl,
72+
`/projects/${mergeRequest.project_id}/merge_requests/${mergeRequest.iid}/closes_issues`
73+
);
74+
debug('Getting related issues from %s', relatedIssuesEndpoint);
75+
const relatedIssues = await got
76+
.get(relatedIssuesEndpoint, {
77+
...apiOptions,
78+
})
79+
.json();
80+
81+
return relatedIssues.filter(x => x.state === 'closed');
82+
};
83+
84+
const relatedMergeRequests = uniqWith(
85+
(await Promise.all(commits.map(x => x.hash).map(getRelatedMergeRequests))).flat(),
86+
isEqual
87+
);
88+
const relatedIssues = uniqWith((await Promise.all(relatedMergeRequests.map(getRelatedIssues))).flat(), isEqual);
89+
await Promise.all(relatedIssues.map(postCommentToIssue));
90+
await Promise.all(relatedMergeRequests.map(postCommentToMergeRequest));
91+
} catch (error) {
92+
logger.error('An error occurred while posting comments to related issues and merge requests:\n%O', error);
93+
throw error;
94+
}
95+
}
96+
};

Diff for: test/resolve-config.test.js

+22-7
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,19 @@ test('Returns user config', t => {
77
const gitlabUrl = 'https://host.com';
88
const gitlabApiPathPrefix = '/api/prefix';
99
const assets = ['file.js'];
10+
const postComments = true;
1011

11-
t.deepEqual(resolveConfig({gitlabUrl, gitlabApiPathPrefix, assets}, {env: {GITLAB_TOKEN: gitlabToken}}), {
12-
gitlabToken,
13-
gitlabUrl,
14-
gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
15-
assets,
16-
milestones: undefined,
17-
});
12+
t.deepEqual(
13+
resolveConfig({gitlabUrl, gitlabApiPathPrefix, assets, postComments}, {env: {GITLAB_TOKEN: gitlabToken}}),
14+
{
15+
gitlabToken,
16+
gitlabUrl,
17+
gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
18+
assets,
19+
milestones: undefined,
20+
successComment: undefined,
21+
}
22+
);
1823
});
1924

2025
test('Returns user config via environment variables', t => {
@@ -35,6 +40,7 @@ test('Returns user config via environment variables', t => {
3540
gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
3641
assets,
3742
milestones,
43+
successComment: undefined,
3844
}
3945
);
4046
});
@@ -53,6 +59,7 @@ test('Returns user config via alternative environment variables', t => {
5359
gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
5460
assets,
5561
milestones: undefined,
62+
successComment: undefined,
5663
}
5764
);
5865
});
@@ -68,6 +75,7 @@ test('Returns default config', t => {
6875
gitlabApiUrl: urlJoin('https://gitlab.com', '/api/v4'),
6976
assets: undefined,
7077
milestones: undefined,
78+
successComment: undefined,
7179
});
7280

7381
t.deepEqual(resolveConfig({gitlabApiPathPrefix}, {env: {GL_TOKEN: gitlabToken}}), {
@@ -76,6 +84,7 @@ test('Returns default config', t => {
7684
gitlabApiUrl: urlJoin('https://gitlab.com', gitlabApiPathPrefix),
7785
assets: undefined,
7886
milestones: undefined,
87+
successComment: undefined,
7988
});
8089

8190
t.deepEqual(resolveConfig({gitlabUrl}, {env: {GL_TOKEN: gitlabToken}}), {
@@ -84,6 +93,7 @@ test('Returns default config', t => {
8493
gitlabApiUrl: urlJoin(gitlabUrl, '/api/v4'),
8594
assets: undefined,
8695
milestones: undefined,
96+
successComment: undefined,
8797
});
8898
});
8999

@@ -107,6 +117,7 @@ test('Returns default config via GitLab CI/CD environment variables', t => {
107117
gitlabApiUrl: CI_API_V4_URL,
108118
assets: undefined,
109119
milestones: undefined,
120+
successComment: undefined,
110121
}
111122
);
112123
});
@@ -134,6 +145,7 @@ test('Returns user config over GitLab CI/CD environment variables', t => {
134145
gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
135146
assets,
136147
milestones: undefined,
148+
successComment: undefined,
137149
}
138150
);
139151
});
@@ -167,6 +179,7 @@ test('Returns user config via environment variables over GitLab CI/CD environmen
167179
gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
168180
assets: undefined,
169181
milestones: undefined,
182+
successComment: undefined,
170183
}
171184
);
172185
});
@@ -200,6 +213,7 @@ test('Returns user config via alternative environment variables over GitLab CI/C
200213
gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
201214
assets: undefined,
202215
milestones: undefined,
216+
successComment: undefined,
203217
}
204218
);
205219
});
@@ -224,6 +238,7 @@ test('Ignore GitLab CI/CD environment variables if not running on GitLab CI/CD',
224238
gitlabApiUrl: urlJoin('https://gitlab.com', '/api/v4'),
225239
assets: undefined,
226240
milestones: undefined,
241+
successComment: undefined,
227242
}
228243
);
229244
});

0 commit comments

Comments
 (0)