Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: comment on issues and merge requests resolved by current release #332

Merged
merged 6 commits into from
Mar 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# @semantic-release/gitlab
# @semantic-release/gitlab

[**semantic-release**](https://github.com/semantic-release/semantic-release) plugin to publish a
[GitLab release](https://docs.gitlab.com/ee/user/project/releases/).
Expand All @@ -10,6 +11,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 +60,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 +103,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,
travi marked this conversation as resolved.
Show resolved Hide resolved
}
);
});

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,
}
);
});
Loading