Repository: semantic-release/gitlab (js)

Issue:
Migrate from deprecated Tags API endpoint to Release API endpoint
In GitLab 14.0, the ability to manage release data (i.e. release notes) through the `/projects/<project id>/repository/tags/<tag name>/release` endpoint will be removed: https://gitlab.com/gitlab-org/gitlab/-/issues/290311

This module currently uses this endpoint to create a release in `publish.js`:

https://github.com/semantic-release/gitlab/blob/81cd6a88972e2e59ca6cf5d89ecea9d4529cdc9e/lib/publish.js#L71

This API call should be updated to use the [Release API](https://docs.gitlab.com/ee/api/releases/#create-a-release) instead.


Pull request:
refactor: replace deprecated Tags API call
### What does this PR do?

Resolves #183 by updating all requests to:

```
https://example.gitlab.com/api/v4/project/<project id>/repository/tags<tag name>/release`
```

to instead call:

```
https://example.gitlab.com/api/v4/project/<project id>/releases`
```

The former is deprecated and will be removed in GitLab 14.0 (in May 2021); see https://gitlab.com/gitlab-org/gitlab/-/issues/290311.

This has the added benefit of simplifying the process of uploading assets. The Releases API allows a release to be created with assets in a single request, removing the need to create assets separately.

This change is backwards compatible (the Releases API has been available since GitLab 11.7 - when the releases feature itself was delivered) and shouldn't impact current users of this plugin.

#### Testing

I've tested this change in a test project on GitLab.com:

- A release with assets: https://gitlab.com/nfriend/semantic-release-test-project/-/releases/v1.7.4
- A release without assets: https://gitlab.com/nfriend/semantic-release-test-project/-/releases/v1.8.0

Patch:
diff --git a/lib/publish.js b/lib/publish.js
index ba4dbf3..97c0c5e 100644
--- a/lib/publish.js
+++ b/lib/publish.js
@@ -67,24 +67,25 @@ module.exports = async (pluginConfig, context) => {
     );
   }
 
-  debug('Update git tag %o with commit %o and release description', gitTag, gitHead);
-  await got.post(urlJoin(gitlabApiUrl, `/projects/${encodedRepoId}/repository/tags/${encodedGitTag}/release`), {
+  debug('Create a release for git tag %o with commit %o', gitTag, gitHead);
+  await got.post(urlJoin(gitlabApiUrl, `/projects/${encodedRepoId}/releases`), {
     ...apiOptions,
-    json: {tag_name: gitTag, description: notes && notes.trim() ? notes : gitTag}, // eslint-disable-line camelcase
+    json: {
+      /* eslint-disable camelcase */
+      tag_name: gitTag,
+      description: notes && notes.trim() ? notes : gitTag,
+      assets: {
+        links: assetsList.map(({label, alt, url}) => {
+          return {
+            name: label || alt,
+            url: urlJoin(gitlabUrl, repoId, url),
+          };
+        }),
+      },
+      /* eslint-enable camelcase */
+    },
   });
 
-  if (assetsList.length > 0) {
-    await Promise.all(
-      assetsList.map(({label, alt, url}) => {
-        debug('Add link to asset %o', label || alt);
-        return got.post(urlJoin(gitlabApiUrl, `/projects/${encodedRepoId}/releases/${encodedGitTag}/assets/links`), {
-          ...apiOptions,
-          json: {name: label || alt, url: urlJoin(gitlabUrl, repoId, url)},
-        });
-      })
-    );
-  }
-
   logger.log('Published GitLab release: %s', gitTag);
 
   return {url: urlJoin(gitlabUrl, encodedRepoId, `/tags/${encodedGitTag}`), name: 'GitLab release'};


Test patch:
diff --git a/test/integration.test.js b/test/integration.test.js
index 319dfe5..8884713 100644
--- a/test/integration.test.js
+++ b/test/integration.test.js
@@ -65,9 +65,12 @@ test.serial('Publish a release', async t => {
   const gitlab = authenticate(env)
     .get(`/projects/${encodedRepoId}`)
     .reply(200, {permissions: {project_access: {access_level: 30}}})
-    .post(`/projects/${encodedRepoId}/repository/tags/${nextRelease.gitTag}/release`, {
+    .post(`/projects/${encodedRepoId}/releases`, {
       tag_name: nextRelease.gitTag,
       description: nextRelease.notes,
+      assets: {
+        links: [],
+      },
     })
     .reply(200);
 
@@ -90,9 +93,12 @@ test.serial('Verify Github auth and release', async t => {
   const gitlab = authenticate(env)
     .get(`/projects/${encodedRepoId}`)
     .reply(200, {permissions: {project_access: {access_level: 30}}})
-    .post(`/projects/${encodedRepoId}/repository/tags/${nextRelease.gitTag}/release`, {
+    .post(`/projects/${encodedRepoId}/releases`, {
       tag_name: nextRelease.gitTag,
       description: nextRelease.notes,
+      assets: {
+        links: [],
+      },
     })
     .reply(200);
 
diff --git a/test/publish.test.js b/test/publish.test.js
index 270ea72..f24c01f 100644
--- a/test/publish.test.js
+++ b/test/publish.test.js
@@ -29,9 +29,12 @@ test.serial('Publish a release', async t => {
   const encodedRepoId = encodeURIComponent(`${owner}/${repo}`);
   const encodedGitTag = encodeURIComponent(nextRelease.gitTag);
   const gitlab = authenticate(env)
-    .post(`/projects/${encodedRepoId}/repository/tags/${encodedGitTag}/release`, {
+    .post(`/projects/${encodedRepoId}/releases`, {
       tag_name: nextRelease.gitTag,
       description: nextRelease.notes,
+      assets: {
+        links: [],
+      },
     })
     .reply(200);
 
@@ -54,20 +57,22 @@ test.serial('Publish a release with assets', async t => {
   const uploaded = {url: '/uploads/file.css', alt: 'file.css'};
   const assets = [['**', '!**/*.txt', '!.dotfile']];
   const gitlab = authenticate(env)
-    .post(`/projects/${encodedRepoId}/repository/tags/${encodedGitTag}/release`, {
+    .post(`/projects/${encodedRepoId}/releases`, {
       tag_name: nextRelease.gitTag,
       description: nextRelease.notes,
+      assets: {
+        links: [
+          {
+            name: uploaded.alt,
+            url: `https://gitlab.com/${owner}/${repo}${uploaded.url}`,
+          },
+        ],
+      },
     })
     .reply(200);
   const gitlabUpload = authenticate(env)
     .post(`/projects/${encodedRepoId}/uploads`, /filename="file.css"/gm)
     .reply(200, uploaded);
-  const gitlabAssetLink = authenticate(env)
-    .post(`/projects/${encodedRepoId}/releases/${encodedGitTag}/assets/links`, {
-      url: `https://gitlab.com/${owner}/${repo}${uploaded.url}`,
-      name: uploaded.alt,
-    })
-    .reply(200, {});
 
   const result = await publish({assets}, {env, cwd, options, nextRelease, logger: t.context.logger});
 
@@ -76,7 +81,6 @@ test.serial('Publish a release with assets', async t => {
   t.deepEqual(t.context.log.args[1], ['Published GitLab release: %s', nextRelease.gitTag]);
   t.true(gitlabUpload.isDone());
   t.true(gitlab.isDone());
-  t.true(gitlabAssetLink.isDone());
 });
 
 test.serial('Publish a release with array of missing assets', async t => {
@@ -91,9 +95,12 @@ test.serial('Publish a release with array of missing assets', async t => {
   const emptyDirectory = tempy.directory();
   const assets = [emptyDirectory, {path: 'missing.txt', label: 'missing.txt'}];
   const gitlab = authenticate(env)
-    .post(`/projects/${encodedRepoId}/repository/tags/${encodedGitTag}/release`, {
+    .post(`/projects/${encodedRepoId}/releases`, {
       tag_name: nextRelease.gitTag,
       description: nextRelease.notes,
+      assets: {
+        links: [],
+      },
     })
     .reply(200);
   const result = await publish({assets}, {env, cwd, options, nextRelease, logger: t.context.logger});
@@ -116,20 +123,22 @@ test.serial('Publish a release with one asset and custom label', async t => {
   const assetLabel = 'Custom Label';
   const assets = [{path: 'upload.txt', label: assetLabel}];
   const gitlab = authenticate(env)
-    .post(`/projects/${encodedRepoId}/repository/tags/${encodedGitTag}/release`, {
+    .post(`/projects/${encodedRepoId}/releases`, {
       tag_name: nextRelease.gitTag,
       description: nextRelease.notes,
+      assets: {
+        links: [
+          {
+            name: assetLabel,
+            url: `https://gitlab.com/${owner}/${repo}${uploaded.url}`,
+          },
+        ],
+      },
     })
     .reply(200);
   const gitlabUpload = authenticate(env)
     .post(`/projects/${encodedRepoId}/uploads`, /filename="upload.txt"/gm)
     .reply(200, uploaded);
-  const gitlabAssetLink = authenticate(env)
-    .post(`/projects/${encodedRepoId}/releases/${encodedGitTag}/assets/links`, {
-      url: `https://gitlab.com/${owner}/${repo}${uploaded.url}`,
-      name: assetLabel,
-    })
-    .reply(200, {});
 
   const result = await publish({assets}, {env, cwd, options, nextRelease, logger: t.context.logger});
 
@@ -138,10 +147,9 @@ test.serial('Publish a release with one asset and custom label', async t => {
   t.deepEqual(t.context.log.args[1], ['Published GitLab release: %s', nextRelease.gitTag]);
   t.true(gitlabUpload.isDone());
   t.true(gitlab.isDone());
-  t.true(gitlabAssetLink.isDone());
 });
 
-test.serial('Publish a release with missing releasae notes', async t => {
+test.serial('Publish a release with missing release notes', async t => {
   const owner = 'test_user';
   const repo = 'test_repo';
   const env = {GITLAB_TOKEN: 'gitlab_token'};
@@ -151,9 +159,12 @@ test.serial('Publish a release with missing releasae notes', async t => {
   const encodedRepoId = encodeURIComponent(`${owner}/${repo}`);
   const encodedGitTag = encodeURIComponent(nextRelease.gitTag);
   const gitlab = authenticate(env)
-    .post(`/projects/${encodedRepoId}/repository/tags/${encodedGitTag}/release`, {
+    .post(`/projects/${encodedRepoId}/releases`, {
       tag_name: nextRelease.gitTag,
       description: nextRelease.gitTag,
+      assets: {
+        links: [],
+      },
     })
     .reply(200);