Skip to content

Commit

Permalink
feat: comment on issues and merge requests resolved by current release (
Browse files Browse the repository at this point in the history
#340)

BREAKING CHANGE: By default all related issues and MRs are commented. Set the `successComment` option to `false` to disable this behavior.
  • Loading branch information
fgreinacher committed Mar 21, 2022
1 parent cf9c4b9 commit 42c31ac
Show file tree
Hide file tree
Showing 9 changed files with 320 additions and 17 deletions.
28 changes: 22 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
|--------------------|-----------------------------------------------------------------------------------------------------------------------|
| `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. |

## Install

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

### 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). | - |
| 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](<gitlab_release_url>) |

#### assets

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

#### successComment

The message for the issue comments 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`. |
| `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.

## 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.
Expand Down
12 changes: 11 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const verifyGitLab = require('./lib/verify');
const publishGitLab = require('./lib/publish');
const successGitLab = require('./lib/success');

let verified;

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

module.exports = {verifyConditions, publish};
async function success(pluginConfig, context) {
if (!verified) {
await verifyGitLab(pluginConfig, context);
verified = true;
}

return successGitLab(pluginConfig, context);
}

module.exports = {verifyConditions, publish, success};
3 changes: 3 additions & 0 deletions lib/definitions/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const RELEASE_NAME = 'GitLab release';

module.exports = {RELEASE_NAME};
17 changes: 17 additions & 0 deletions lib/get-success-comment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const HOME_URL = 'https://github.com/semantic-release/semantic-release';
const linkify = releaseInfo =>
`${releaseInfo.url ? `[${releaseInfo.name}](${releaseInfo.url})` : `\`${releaseInfo.name}\``}`;

module.exports = (issueOrMergeRequest, releaseInfos, nextRelease) =>
`:tada: This ${issueOrMergeRequest.isMergeRequest ? 'MR is included' : 'issue has been resolved'} in version ${
nextRelease.version
} :tada:${
releaseInfos.length > 0
? `\n\nThe release is available on${
releaseInfos.length === 1
? ` ${linkify(releaseInfos[0])}`
: `:\n${releaseInfos.map(releaseInfo => `- ${linkify(releaseInfo)}`).join('\n')}`
}`
: ''
}
Your **[semantic-release](${HOME_URL})** bot :package::rocket:`;
5 changes: 4 additions & 1 deletion lib/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const debug = require('debug')('semantic-release:gitlab');
const resolveConfig = require('./resolve-config');
const getRepoId = require('./get-repo-id');
const getAssets = require('./glob-assets');
const {RELEASE_NAME} = require('./definitions/constants');

module.exports = async (pluginConfig, context) => {
const {
Expand Down Expand Up @@ -117,5 +118,7 @@ module.exports = async (pluginConfig, context) => {

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

return {url: urlJoin(gitlabUrl, encodedRepoId, `/-/releases/${encodedGitTag}`), name: 'GitLab release'};
const releaseUrl = urlJoin(gitlabUrl, encodedRepoId, `/-/releases/${encodedGitTag}`);

return {name: RELEASE_NAME, url: releaseUrl};
};
4 changes: 2 additions & 2 deletions lib/resolve-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const {castArray, isNil} = require('lodash');
const urlJoin = require('url-join');

module.exports = (
{gitlabUrl, gitlabApiPathPrefix, assets, milestones},
{gitlabUrl, gitlabApiPathPrefix, assets, milestones, successComment},
{
envCi: {service} = {},
env: {
Expand All @@ -29,7 +29,6 @@ module.exports = (
(service === 'gitlab' && CI_PROJECT_URL && CI_PROJECT_PATH
? CI_PROJECT_URL.replace(new RegExp(`/${CI_PROJECT_PATH}$`), '')
: 'https://gitlab.com');

return {
gitlabToken: GL_TOKEN || GITLAB_TOKEN,
gitlabUrl: defaultedGitlabUrl,
Expand All @@ -41,5 +40,6 @@ module.exports = (
: urlJoin(defaultedGitlabUrl, isNil(userGitlabApiPathPrefix) ? '/api/v4' : userGitlabApiPathPrefix),
assets: assets ? castArray(assets) : assets,
milestones: milestones ? castArray(milestones) : milestones,
successComment,
};
};
96 changes: 96 additions & 0 deletions lib/success.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
const {uniqWith, isEqual, 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 getSuccessComment = require('./get-success-comment');

module.exports = async (pluginConfig, context) => {
const {
options: {repositoryUrl},
nextRelease,
logger,
commits,
releases,
} = context;
const {gitlabToken, gitlabUrl, gitlabApiUrl, successComment} = resolveConfig(pluginConfig, context);
const repoId = getRepoId(context, gitlabUrl, repositoryUrl);
const encodedRepoId = encodeURIComponent(repoId);
const apiOptions = {headers: {'PRIVATE-TOKEN': gitlabToken}};

if (successComment === false) {
logger.log('Skip commenting on issues and pull requests.');
} else {
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,
json: {body},
});
};

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,
json: {body},
});
};

const getRelatedMergeRequests = async commitHash => {
const relatedMergeRequestsEndpoint = urlJoin(
gitlabApiUrl,
`/projects/${encodedRepoId}/repository/commits/${commitHash}/merge_requests`
);
debug('Getting MRs from %s', relatedMergeRequestsEndpoint);
const relatedMergeRequests = await got
.get(relatedMergeRequestsEndpoint, {
...apiOptions,
})
.json();

return relatedMergeRequests.filter(x => x.state === 'merged');
};

const getRelatedIssues = async mergeRequest => {
const relatedIssuesEndpoint = urlJoin(
gitlabApiUrl,
`/projects/${mergeRequest.project_id}/merge_requests/${mergeRequest.iid}/closes_issues`
);
debug('Getting related issues from %s', relatedIssuesEndpoint);
const relatedIssues = await got
.get(relatedIssuesEndpoint, {
...apiOptions,
})
.json();

return relatedIssues.filter(x => x.state === 'closed');
};

const relatedMergeRequests = uniqWith(
(await Promise.all(commits.map(x => x.hash).map(getRelatedMergeRequests))).flat(),
isEqual
);
const relatedIssues = uniqWith((await Promise.all(relatedMergeRequests.map(getRelatedIssues))).flat(), isEqual);
await Promise.all(relatedIssues.map(postCommentToIssue));
await Promise.all(relatedMergeRequests.map(postCommentToMergeRequest));
} catch (error) {
logger.error('An error occurred while posting comments to related issues and merge requests:\n%O', error);
throw error;
}
}
};
29 changes: 22 additions & 7 deletions test/resolve-config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@ test('Returns user config', t => {
const gitlabUrl = 'https://host.com';
const gitlabApiPathPrefix = '/api/prefix';
const assets = ['file.js'];
const postComments = true;

t.deepEqual(resolveConfig({gitlabUrl, gitlabApiPathPrefix, assets}, {env: {GITLAB_TOKEN: gitlabToken}}), {
gitlabToken,
gitlabUrl,
gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
assets,
milestones: undefined,
});
t.deepEqual(
resolveConfig({gitlabUrl, gitlabApiPathPrefix, assets, postComments}, {env: {GITLAB_TOKEN: gitlabToken}}),
{
gitlabToken,
gitlabUrl,
gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
assets,
milestones: undefined,
successComment: undefined,
}
);
});

test('Returns user config via environment variables', t => {
Expand All @@ -35,6 +40,7 @@ test('Returns user config via environment variables', t => {
gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
assets,
milestones,
successComment: undefined,
}
);
});
Expand All @@ -53,6 +59,7 @@ test('Returns user config via alternative environment variables', t => {
gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
assets,
milestones: undefined,
successComment: undefined,
}
);
});
Expand All @@ -68,6 +75,7 @@ test('Returns default config', t => {
gitlabApiUrl: urlJoin('https://gitlab.com', '/api/v4'),
assets: undefined,
milestones: undefined,
successComment: undefined,
});

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

t.deepEqual(resolveConfig({gitlabUrl}, {env: {GL_TOKEN: gitlabToken}}), {
Expand All @@ -84,6 +93,7 @@ test('Returns default config', t => {
gitlabApiUrl: urlJoin(gitlabUrl, '/api/v4'),
assets: undefined,
milestones: undefined,
successComment: undefined,
});
});

Expand All @@ -107,6 +117,7 @@ test('Returns default config via GitLab CI/CD environment variables', t => {
gitlabApiUrl: CI_API_V4_URL,
assets: undefined,
milestones: undefined,
successComment: undefined,
}
);
});
Expand Down Expand Up @@ -134,6 +145,7 @@ test('Returns user config over GitLab CI/CD environment variables', t => {
gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
assets,
milestones: undefined,
successComment: undefined,
}
);
});
Expand Down Expand Up @@ -167,6 +179,7 @@ test('Returns user config via environment variables over GitLab CI/CD environmen
gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
assets: undefined,
milestones: undefined,
successComment: undefined,
}
);
});
Expand Down Expand Up @@ -200,6 +213,7 @@ test('Returns user config via alternative environment variables over GitLab CI/C
gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
assets: undefined,
milestones: undefined,
successComment: undefined,
}
);
});
Expand All @@ -224,6 +238,7 @@ test('Ignore GitLab CI/CD environment variables if not running on GitLab CI/CD',
gitlabApiUrl: urlJoin('https://gitlab.com', '/api/v4'),
assets: undefined,
milestones: undefined,
successComment: undefined,
}
);
});

0 comments on commit 42c31ac

Please sign in to comment.