Repository: semantic-release/gitlab
Pull request #242 (2021-06-01 16:26:14)

feat: support asset type and filepath
Implemented two options for assets according to GitLab [document on asset permanent link and link type](https://git.bdaa.pro/help/user/project/releases/index#permanent-links-to-release-assets), addressing #161.

It can also be a solution to #167, with which now we can download asset using URL like this: `https://host/namespace/project/releases/v1.0.0/downloads/build.tar.gz`

yxonic: @nfriend thanks for your time reviewing the code! I've made several changes according to your suggestions:

1. I agree with your comments, so `link_type` is now undefined as default, which also fixes tests.
2. Added a testcase to cover these fields. I'm not exactly sure if I did it right, so if there's any problem, just let me know :)


yxonic: @nfriend Hi~ I just fixed the links. regarding the duplicate `filepath` issue, I took a slightly different approach here. I noticed that `label` is basically ignored when there is more than one files, and I think we can do the same for `filepath` for consistency: `filepath` will still work when there is only one file matched (user can still use `*`), and won't break the pipeline if more than one file is matched later. This behavior is also documented and tested.
nfriend: @yxonic Sorry for the slow response here, I'm going to try and get to this tomorrow!
nfriend: > Regarding the duplicate filepath issue, I took a slightly different approach here.

@yxonic Good call! This is a simpler and more consistent solution 👍 And thanks for documenting this behavior.

Patch:
diff --git a/README.md b/README.md
index 9b2a6d9..3112664 100644
--- a/README.md
+++ b/README.md
@@ -74,6 +74,8 @@ Can be a [glob](https://github.com/isaacs/node-glob#glob-primer) or and `Array`
 | -------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------ |
 | `path`   | **Required.** A [glob](https://github.com/isaacs/node-glob#glob-primer) to identify the files to upload.    | -                                    |
 | `label`  | Short description of the file displayed on the GitLab release. Ignored if `path` matches more than one file.| File name extracted from the `path`. |
+| `type` | Asset type displayed on the GitLab release. Can be `runbook`, `package`, `image` and `other` (see official documents on [release assets](https://docs.gitlab.com/ee/user/project/releases/#release-assets)). | `other` |
+| `filepath` | A filepath for creating a permalink pointing to the asset (requires GitLab 12.9+, see official documents on [permanent links](https://docs.gitlab.com/ee/user/project/releases/#permanent-links-to-release-assets)). Ignored if `path` matches more than one file. | - |
 
 Each entry in the `assets` `Array` is globbed individually. A [glob](https://github.com/isaacs/node-glob#glob-primer)
 can be a `String` (`"dist/**/*.js"` or `"dist/mylib.js"`) or an `Array` of `String`s that will be globbed together
diff --git a/lib/glob-assets.js b/lib/glob-assets.js
index 2231de7..e891448 100644
--- a/lib/glob-assets.js
+++ b/lib/glob-assets.js
@@ -39,8 +39,10 @@ module.exports = async ({cwd}, assets) =>
                 // Output an Object definition for each file matched and set each one with:
                 // - `path` of the matched file
                 // - `label` based on the actual file name (to avoid assets with duplicate `label`s)
+                // - `filepath` ignored (also to avoid duplicates)
                 // - other properties of the original asset definition
-                return globbed.map(file => ({...asset, path: file, label: basename(file)}));
+                const {filepath, ...others} = asset;
+                return globbed.map(file => ({...others, path: file, label: basename(file)}));
               }
 
               // If asset is an Object, output an Object definition with:
diff --git a/lib/publish.js b/lib/publish.js
index dd5d716..19e0329 100644
--- a/lib/publish.js
+++ b/lib/publish.js
@@ -35,7 +35,7 @@ module.exports = async (pluginConfig, context) => {
 
     await Promise.all(
       globbedAssets.map(async asset => {
-        const {path, label} = isPlainObject(asset) ? asset : {path: asset};
+        const {path, label, type, filepath} = isPlainObject(asset) ? asset : {path: asset};
         const file = resolve(cwd, path);
         let fileStat;
 
@@ -53,6 +53,8 @@ module.exports = async (pluginConfig, context) => {
 
         debug('file path: %o', path);
         debug('file label: %o', label);
+        debug('file type: %o', type);
+        debug('file filepath: %o', filepath);
 
         // Uploaded assets to the project
         const form = new FormData();
@@ -61,7 +63,7 @@ module.exports = async (pluginConfig, context) => {
           .post(urlJoin(gitlabApiUrl, `/projects/${encodedRepoId}/uploads`), {...apiOptions, body: form})
           .json();
 
-        assetsList.push({label, alt, url});
+        assetsList.push({label, alt, url, type, filepath});
 
         logger.log('Uploaded file: %s', url);
       })
@@ -77,10 +79,12 @@ module.exports = async (pluginConfig, context) => {
       description: notes && notes.trim() ? notes : gitTag,
       milestones,
       assets: {
-        links: assetsList.map(({label, alt, url}) => {
+        links: assetsList.map(({label, alt, url, type, filepath}) => {
           return {
             name: label || alt,
             url: urlJoin(gitlabUrl, repoId, url),
+            link_type: type,
+            filepath,
           };
         }),
       },


Test patch:
diff --git a/test/glob-assets.test.js b/test/glob-assets.test.js
index ac5c823..7e8adf3 100644
--- a/test/glob-assets.test.js
+++ b/test/glob-assets.test.js
@@ -70,7 +70,7 @@ test('Favor Object over String values when removing duplicates', async t => {
     'upload.txt',
     {path: 'upload.txt', name: 'upload_name'},
     'upload.txt',
-    {path: 'upload_other.txt', name: 'upload_other_name'},
+    {path: 'upload_other.txt', name: 'upload_other_name', filepath: '/path/to/other'},
     'upload.txt',
     'upload_other.txt',
   ]);
@@ -79,7 +79,7 @@ test('Favor Object over String values when removing duplicates', async t => {
     sortAssets(globbedAssets),
     sortAssets([
       {path: 'upload.txt', name: 'upload_name'},
-      {path: 'upload_other.txt', name: 'upload_other_name'},
+      {path: 'upload_other.txt', name: 'upload_other_name', filepath: '/path/to/other'},
     ])
   );
 });
@@ -138,6 +138,20 @@ test('Replace name by filename for Object that match multiple files', async t =>
   );
 });
 
+test('Ignore filepath for Object that match multiple files', async t => {
+  const cwd = tempy.directory();
+  await copy(fixtures, cwd);
+  const globbedAssets = await globAssets({cwd}, [{path: '*.txt', filepath: '/path/to/file'}]);
+
+  t.deepEqual(
+    sortAssets(globbedAssets),
+    sortAssets([
+      {path: 'upload.txt', label: 'upload.txt'},
+      {path: 'upload_other.txt', label: 'upload_other.txt'},
+    ])
+  );
+});
+
 test('Include dotfiles', async t => {
   const cwd = tempy.directory();
   await copy(fixtures, cwd);
diff --git a/test/publish.test.js b/test/publish.test.js
index cc512fe..af1f795 100644
--- a/test/publish.test.js
+++ b/test/publish.test.js
@@ -83,6 +83,52 @@ test.serial('Publish a release with assets', async t => {
   t.true(gitlab.isDone());
 });
 
+test.serial('Publish a release with asset type and permalink', async t => {
+  const cwd = 'test/fixtures/files';
+  const owner = 'test_user';
+  const repo = 'test_repo';
+  const env = {GITLAB_TOKEN: 'gitlab_token'};
+  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 uploaded = {url: '/uploads/file.css', alt: 'file.css', link_type: 'package', filepath: '/dist/file.css'};
+  const assets = [
+    {
+      path: ['**', '!**/*.txt', '!.dotfile'],
+      type: 'package',
+      filepath: '/dist/file.css',
+    },
+  ];
+  const gitlab = authenticate(env)
+    .post(`/projects/${encodedRepoId}/releases`, {
+      tag_name: nextRelease.gitTag,
+      description: nextRelease.notes,
+      assets: {
+        links: [
+          {
+            name: uploaded.alt,
+            url: `https://gitlab.com/${owner}/${repo}${uploaded.url}`,
+            link_type: uploaded.link_type,
+            filepath: uploaded.filepath,
+          },
+        ],
+      },
+    })
+    .reply(200);
+  const gitlabUpload = authenticate(env)
+    .post(`/projects/${encodedRepoId}/uploads`, /filename="file.css"/gm)
+    .reply(200, uploaded);
+
+  const result = await publish({assets}, {env, cwd, options, nextRelease, logger: t.context.logger});
+
+  t.is(result.url, `https://gitlab.com/${encodedRepoId}/-/releases/${encodedGitTag}`);
+  t.deepEqual(t.context.log.args[0], ['Uploaded file: %s', uploaded.url]);
+  t.deepEqual(t.context.log.args[1], ['Published GitLab release: %s', nextRelease.gitTag]);
+  t.true(gitlabUpload.isDone());
+  t.true(gitlab.isDone());
+});
+
 test.serial('Publish a release with a milestone', async t => {
   const owner = 'test_user';
   const repo = 'test_repo';