Repository: semantic-release/gitlab
Pull request #237 (2021-05-13 16:45:59)

feat: support gitlab milestones in config
Adds support to attach milestones to the created GitLab Release via the plugin configuration

See: https://docs.gitlab.com/ee/api/releases/#create-a-release

nfriend: @rjferguson21 This is really exciting! Sorry for the slow response here. I won't have time to take a look today or tomorrow, but I will prioritize this on Wednesday.
nfriend: > but I will prioritize this on Wednesday.

So sorry - ran out of time today! But will get to this tomorrow or Friday!
nfriend: @rjferguson21 Thanks for putting this together! 👏 

A couple of thoughts for you:

1. Let's update [the README](https://github.com/semantic-release/gitlab#options) with this new configuration option so that people know it's available.
2. I tested this out in a test project, and it works great! I'm curious how you plan on using this, though. Do you plan on hard-coding a specific milestone in your config? Or will you dynamically configure the `milestones` property in a `*.js` config? The reason I ask is that I'm having trouble thinking how I would automate this association. One pattern people use is to create a milestone with the same name as the release (for example, [this is what GitLab itself does](https://gitlab.com/gitlab-org/gitlab/-/releases)). But this would be difficult to do using this plugin, since the release version is managed by `semantic-release` 🤔 This isn't a reason _not_ to add this ability, but I just want to make sure I understand what kinds of workflows this will enable.
rjferguson21: Thanks for looking at this and the feedback @nfriend.  I've added the `milestones` option to the README. 

Your second question is valid, we're using a job like this:
```
release:
  image: node:lts-alpine3.13
  stage: release
  script:
    - apk add jq moreutils curl git
    - |
      export MILESTONE=$(curl --header "PRIVATE-TOKEN: $RELEASE_TOKEN" $GITLAB_URL/api/v4/groups/$GROUP_ID/milestones\?state\=active | jq .[-1].title)
    - jq ".plugins[4][1].milestones = [$MILESTONE]" .releaserc.json | sponge .releaserc.json
    - npm install -g semantic-release @semantic-release/git https://github.com/rjferguson21/gitlab#add-milestones @semantic-release/commit-analyzer @semantic-release/release-notes-generator @semantic-release/changelog
    - semantic-release
  only:
    - master
```

The idea is to associate project releases to a group level milestone, so this script block tries to go fetch the current active milestone and attaches the particular project's release to a group level milestone.  It is a bit challenging because semantic-release does not support passing in arguments to plugins via the command line, so I am having to mutate the project's release config in CI (I chose the `.json` version). 


nfriend: @rjferguson21 Awesome, thanks for updating the documentation!

And thanks for the details about automating the associating to the active group level milestone.

This PR looks good to me 👍 

@travi I noticed I now have permission to merge in this repo. Is there any particular process I should be following before merging? Or would you like me to continue pinging one of you for final review/merge?
nfriend: cc @pvdlg @gr2m in case you can answer my [question about the review process above](https://github.com/semantic-release/gitlab/pull/237#issuecomment-846295696) 😄 
travi: @nfriend sorry for missing your question before. you are welcome to merge if you believe that contributions are ready for release. ideally, the test suite should protect us from breakage. if we ever find that to be untrue, the goal would be to close the holes that we find. publishing a revert or a fix is an option in the cases where something slips through. 

you also now have triage rights across all semantic-release repos. we appreciate the help you've been giving for this plugin. up to you if you want to participate in other areas. 
nfriend: Awesome, thanks @travi!

And thanks for this @rjferguson21, merging now 👍 

Patch:
diff --git a/README.md b/README.md
index 7d39dc4..9b2a6d9 100644
--- a/README.md
+++ b/README.md
@@ -58,11 +58,12 @@ 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). | -                                                                                                                                                                       |
+| 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). | -                                                                                                                                                                       |
 
 #### assets
 
diff --git a/lib/publish.js b/lib/publish.js
index e6f6d28..dd5d716 100644
--- a/lib/publish.js
+++ b/lib/publish.js
@@ -17,7 +17,7 @@ module.exports = async (pluginConfig, context) => {
     nextRelease: {gitTag, gitHead, notes},
     logger,
   } = context;
-  const {gitlabToken, gitlabUrl, gitlabApiUrl, assets} = resolveConfig(pluginConfig, context);
+  const {gitlabToken, gitlabUrl, gitlabApiUrl, assets, milestones} = resolveConfig(pluginConfig, context);
   const assetsList = [];
   const repoId = getRepoId(context, gitlabUrl, repositoryUrl);
   const encodedRepoId = encodeURIComponent(repoId);
@@ -27,6 +27,7 @@ module.exports = async (pluginConfig, context) => {
   debug('repoId: %o', repoId);
   debug('release name: %o', gitTag);
   debug('release ref: %o', gitHead);
+  debug('milestones: %o', milestones);
 
   if (assets && assets.length > 0) {
     const globbedAssets = await getAssets(context, assets);
@@ -74,6 +75,7 @@ module.exports = async (pluginConfig, context) => {
       /* eslint-disable camelcase */
       tag_name: gitTag,
       description: notes && notes.trim() ? notes : gitTag,
+      milestones,
       assets: {
         links: assetsList.map(({label, alt, url}) => {
           return {
diff --git a/lib/resolve-config.js b/lib/resolve-config.js
index 6555af9..8fc3a41 100644
--- a/lib/resolve-config.js
+++ b/lib/resolve-config.js
@@ -2,7 +2,7 @@ const {castArray, isNil} = require('lodash');
 const urlJoin = require('url-join');
 
 module.exports = (
-  {gitlabUrl, gitlabApiPathPrefix, assets},
+  {gitlabUrl, gitlabApiPathPrefix, assets, milestones},
   {
     envCi: {service} = {},
     env: {
@@ -40,5 +40,6 @@ module.exports = (
         ? CI_API_V4_URL
         : urlJoin(defaultedGitlabUrl, isNil(userGitlabApiPathPrefix) ? '/api/v4' : userGitlabApiPathPrefix),
     assets: assets ? castArray(assets) : assets,
+    milestones: milestones ? castArray(milestones) : milestones,
   };
 };


Test patch:
diff --git a/test/publish.test.js b/test/publish.test.js
index 9a59966..cc512fe 100644
--- a/test/publish.test.js
+++ b/test/publish.test.js
@@ -83,6 +83,33 @@ test.serial('Publish a release with assets', async t => {
   t.true(gitlab.isDone());
 });
 
+test.serial('Publish a release with a milestone', async t => {
+  const owner = 'test_user';
+  const repo = 'test_repo';
+  const env = {GITLAB_TOKEN: 'gitlab_token'};
+  const pluginConfig = {milestones: ['1.2.3']};
+  const nextRelease = {gitHead: '123', gitTag: 'v1.0.0', notes: 'Test release note body'};
+  const options = {repositoryUrl: `https://gitlab.com/${owner}/${repo}.git`};
+  const encodedRepoId = encodeURIComponent(`${owner}/${repo}`);
+  const encodedGitTag = encodeURIComponent(nextRelease.gitTag);
+  const gitlab = authenticate(env)
+    .post(`/projects/${encodedRepoId}/releases`, {
+      tag_name: nextRelease.gitTag,
+      description: nextRelease.notes,
+      assets: {
+        links: [],
+      },
+      milestones: ['1.2.3'],
+    })
+    .reply(200);
+
+  const result = await publish(pluginConfig, {env, options, nextRelease, logger: t.context.logger});
+
+  t.is(result.url, `https://gitlab.com/${encodedRepoId}/-/releases/${encodedGitTag}`);
+  t.deepEqual(t.context.log.args[0], ['Published GitLab release: %s', nextRelease.gitTag]);
+  t.true(gitlab.isDone());
+});
+
 test.serial('Publish a release with array of missing assets', async t => {
   const cwd = 'test/fixtures/files';
   const owner = 'test_user';
diff --git a/test/resolve-config.test.js b/test/resolve-config.test.js
index f0bf261..3d0ddfd 100644
--- a/test/resolve-config.test.js
+++ b/test/resolve-config.test.js
@@ -13,6 +13,7 @@ test('Returns user config', t => {
     gitlabUrl,
     gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
     assets,
+    milestones: undefined,
   });
 });
 
@@ -21,13 +22,20 @@ test('Returns user config via environment variables', t => {
   const gitlabUrl = 'https://host.com';
   const gitlabApiPathPrefix = '/api/prefix';
   const assets = ['file.js'];
+  const milestones = ['1.2.3'];
 
   t.deepEqual(
     resolveConfig(
-      {assets},
+      {assets, milestones},
       {env: {GITLAB_TOKEN: gitlabToken, GITLAB_URL: gitlabUrl, GITLAB_PREFIX: gitlabApiPathPrefix}}
     ),
-    {gitlabToken, gitlabUrl, gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix), assets}
+    {
+      gitlabToken,
+      gitlabUrl,
+      gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
+      assets,
+      milestones,
+    }
   );
 });
 
@@ -39,7 +47,13 @@ test('Returns user config via alternative environment variables', t => {
 
   t.deepEqual(
     resolveConfig({assets}, {env: {GL_TOKEN: gitlabToken, GL_URL: gitlabUrl, GL_PREFIX: gitlabApiPathPrefix}}),
-    {gitlabToken, gitlabUrl, gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix), assets}
+    {
+      gitlabToken,
+      gitlabUrl,
+      gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
+      assets,
+      milestones: undefined,
+    }
   );
 });
 
@@ -53,6 +67,7 @@ test('Returns default config', t => {
     gitlabUrl: 'https://gitlab.com',
     gitlabApiUrl: urlJoin('https://gitlab.com', '/api/v4'),
     assets: undefined,
+    milestones: undefined,
   });
 
   t.deepEqual(resolveConfig({gitlabApiPathPrefix}, {env: {GL_TOKEN: gitlabToken}}), {
@@ -60,6 +75,7 @@ test('Returns default config', t => {
     gitlabUrl: 'https://gitlab.com',
     gitlabApiUrl: urlJoin('https://gitlab.com', gitlabApiPathPrefix),
     assets: undefined,
+    milestones: undefined,
   });
 
   t.deepEqual(resolveConfig({gitlabUrl}, {env: {GL_TOKEN: gitlabToken}}), {
@@ -67,6 +83,7 @@ test('Returns default config', t => {
     gitlabUrl: 'https://gitlab.com',
     gitlabApiUrl: urlJoin(gitlabUrl, '/api/v4'),
     assets: undefined,
+    milestones: undefined,
   });
 });
 
@@ -84,7 +101,13 @@ test('Returns default config via GitLab CI/CD environment variables', t => {
         env: {GL_TOKEN: gitlabToken, CI_PROJECT_URL, CI_PROJECT_PATH, CI_API_V4_URL},
       }
     ),
-    {gitlabToken, gitlabUrl: 'http://ci-host.com', gitlabApiUrl: CI_API_V4_URL, assets: undefined}
+    {
+      gitlabToken,
+      gitlabUrl: 'http://ci-host.com',
+      gitlabApiUrl: CI_API_V4_URL,
+      assets: undefined,
+      milestones: undefined,
+    }
   );
 });
 
@@ -105,7 +128,13 @@ test('Returns user config over GitLab CI/CD environment variables', t => {
         env: {GL_TOKEN: gitlabToken, CI_PROJECT_URL, CI_PROJECT_PATH, CI_API_V4_URL},
       }
     ),
-    {gitlabToken, gitlabUrl, gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix), assets}
+    {
+      gitlabToken,
+      gitlabUrl,
+      gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
+      assets,
+      milestones: undefined,
+    }
   );
 });
 
@@ -132,7 +161,13 @@ test('Returns user config via environment variables over GitLab CI/CD environmen
         },
       }
     ),
-    {gitlabToken, gitlabUrl, gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix), assets: undefined}
+    {
+      gitlabToken,
+      gitlabUrl,
+      gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
+      assets: undefined,
+      milestones: undefined,
+    }
   );
 });
 
@@ -159,7 +194,13 @@ test('Returns user config via alternative environment variables over GitLab CI/C
         },
       }
     ),
-    {gitlabToken, gitlabUrl, gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix), assets: undefined}
+    {
+      gitlabToken,
+      gitlabUrl,
+      gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
+      assets: undefined,
+      milestones: undefined,
+    }
   );
 });
 
@@ -182,6 +223,7 @@ test('Ignore GitLab CI/CD environment variables if not running on GitLab CI/CD',
       gitlabUrl: 'https://gitlab.com',
       gitlabApiUrl: urlJoin('https://gitlab.com', '/api/v4'),
       assets: undefined,
+      milestones: undefined,
     }
   );
 });