Repository: semantic-release/gitlab
Pull request #343 (2022-03-22 13:15:36)

feat(generics): allow upload of build assets as generic packages #174
> Progress of #283 which I closed due to missing time on my side

Gitlab allows to add generic packages as assets attached to a release. #174 This is a suggestion to define a list of generics to be published into the generics repository of a project and then to be used as assets for the release.

fgreinacher: @JonasSchubert Could you check if it makes sense to pull the changes from #322 in this MR? It seems to touch the very same code paths.
JonasSchubert: > @JonasSchubert Could you check if it makes sense to pull the changes from #322 in this MR? It seems to touch the very same code paths.

I would tackle this in a separate PR. I checked the code and in my opinion it needs major adjustments regarding assets handling.
JonasSchubert: I would wait for #322 and merge afterwards.
fgreinacher: > I would wait for #322 and merge afterwards.

Fine for me! Ping me again in case you change your mind :)
mfoti: any update on this? 
bgoareguer: @fgreinacher @JonasSchubert #322 has been merged. Can you please merge this PR?
fgreinacher: Happy to merge once conflicts are resolved. @JonasSchubert are you still willing to work on this?
JonasSchubert: Sorry everyone! I lost track of some stuff...
Fixed the conflicts and ready to merge. Will keep an eye in case any issues should occur
JonasSchubert: @fgreinacher Rebased to v10 and fixed lint errors. Should be fine now.

Patch:
diff --git a/README.md b/README.md
index 4e93ef4..fa26ae6 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@ The plugin can be configured in the [**semantic-release** configuration file](ht
         "gitlabUrl": "https://custom.gitlab.com",
         "assets": [
           { "path": "dist/asset.min.css", "label": "CSS distribution" },
-          { "path": "dist/asset.min.js", "label": "JS distribution" },
+          { "path": "dist/asset.min.js", "label": "JS distribution", "type": "generic_package" },
           { "path": "dist/asset.min.js", "label": "v${nextRelease.version}.js" },
           { "url": "https://gitlab.com/gitlab-org/gitlab/-/blob/master/README.md" }
         ]
@@ -106,6 +106,8 @@ Can be a [glob](https://github.com/isaacs/node-glob#glob-primer) or and `Array`
 | `label`    | Short description of the file displayed on the GitLab release. Ignored if `path` matches more than one file. Supports [Lodash templating](https://lodash.com/docs#template).                                                                                                                                                       | 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)). Supports [Lodash templating](https://lodash.com/docs#template).                                                       | `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. Supports [Lodash templating](https://lodash.com/docs#template). | -                                    |
+| `target`   | Controls where the file is uploaded to. Can be set to `project_upload` for storing the file as [project upload](https://docs.gitlab.com/ee/api/projects.html#upload-a-file) or `generic_package` for storing the file as [generic package](https://docs.gitlab.com/ee/user/packages/generic_packages/).                            | `project_upload`                     |
+| `status`   | This is only applied, if `target` is set to `generic_package`. The generic package status. Can be `default` and `hidden` (see official documents on [generic packages](https://docs.gitlab.com/ee/user/packages/generic_packages/)).                                                                                               | `default`                            |
 
 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/publish.js b/lib/publish.js
index 3d917c6..b081dfa 100644
--- a/lib/publish.js
+++ b/lib/publish.js
@@ -50,6 +50,9 @@ export default async (pluginConfig, context) => {
         const label = asset.label ? template(asset.label)(context) : undefined;
         const type = asset.type ? template(asset.type)(context) : undefined;
         const filepath = asset.filepath ? template(asset.filepath)(context) : undefined;
+        const target = asset.target ? template(asset.target)(context) : undefined;
+        const status = asset.status ? template(asset.status)(context) : undefined;
+
         if (_url) {
           assetsList.push({ label, rawUrl: _url, type, filepath });
           debug("use link from release setting: %s", _url);
@@ -74,28 +77,59 @@ export default async (pluginConfig, context) => {
           debug("file label: %o", label);
           debug("file type: %o", type);
           debug("file filepath: %o", filepath);
+          debug("file target: %o", target);
+          debug("file status: %o", status);
 
           // Uploaded assets to the project
           const form = new FormData();
           form.append("file", createReadStream(file));
 
-          const uploadEndpoint = urlJoin(gitlabApiUrl, `/projects/${encodedRepoId}/uploads`);
+          let uploadEndpoint;
+          let response;
 
-          debug("POST-ing the file %s to %s", file, uploadEndpoint);
+          if (target === "generic_package") {
+            // Upload generic packages
+            const encodedLabel = encodeURIComponent(label);
+            uploadEndpoint = urlJoin(
+              gitlabApiUrl,
+              `/projects/${encodedRepoId}/packages/generic/release/${encodedGitTag}/${encodedLabel}?${
+                status ? `status=${status}&` : ""
+              }select=package_file`
+            );
 
-          let response;
-          try {
-            response = await got.post(uploadEndpoint, { ...apiOptions, ...proxy, body: form }).json();
-          } catch (error) {
-            logger.error("An error occurred while uploading %s to the GitLab project uploads API:\n%O", file, error);
-            throw error;
-          }
+            debug("PUT-ing the file %s to %s", file, uploadEndpoint);
+
+            try {
+              response = await got.put(uploadEndpoint, { ...apiOptions, body: form }).json();
+            } catch (error) {
+              logger.error("An error occurred while uploading %s to the GitLab generics package API:\n%O", file, error);
+              throw error;
+            }
 
-          const { url, alt } = response;
+            const { url } = response.file;
 
-          assetsList.push({ label, alt, url, type, filepath });
+            assetsList.push({ label, alt: "release", url, type: "package", filepath });
 
-          logger.log("Uploaded file: %s", url);
+            logger.log("Uploaded file: %s", url);
+          } else {
+            // Handle normal assets
+            uploadEndpoint = urlJoin(gitlabApiUrl, `/projects/${encodedRepoId}/uploads`);
+
+            debug("POST-ing the file %s to %s", file, uploadEndpoint);
+
+            try {
+              response = await got.post(uploadEndpoint, { ...apiOptions, ...proxy, body: form }).json();
+            } catch (error) {
+              logger.error("An error occurred while uploading %s to the GitLab project uploads API:\n%O", file, error);
+              throw error;
+            }
+
+            const { url, alt } = response;
+
+            assetsList.push({ label, alt, url, type, filepath });
+
+            logger.log("Uploaded file: %s", url);
+          }
         }
       })
     );


Test patch:
diff --git a/test/publish.test.js b/test/publish.test.js
index e0ecd66..4e15ebe 100644
--- a/test/publish.test.js
+++ b/test/publish.test.js
@@ -83,6 +83,50 @@ test.serial("Publish a release with assets", async (t) => {
   t.true(gitlab.isDone());
 });
 
+test.serial("Publish a release with generics", 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 = { file: { url: "/uploads/file.css" } };
+  const generic = { path: "file.css", label: "Style package", target: "generic_package", status: "hidden" };
+  const assets = [generic];
+  const encodedLabel = encodeURIComponent(generic.label);
+  const gitlab = authenticate(env)
+    .post(`/projects/${encodedRepoId}/releases`, {
+      tag_name: nextRelease.gitTag,
+      description: nextRelease.notes,
+      assets: {
+        links: [
+          {
+            name: "Style package",
+            url: `https://gitlab.com/${owner}/${repo}${uploaded.file.url}`,
+            link_type: "package",
+          },
+        ],
+      },
+    })
+    .reply(200);
+  const gitlabUpload = authenticate(env)
+    .put(
+      `/projects/${encodedRepoId}/packages/generic/release/${encodedGitTag}/${encodedLabel}?status=${generic.status}&select=package_file`,
+      /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/${owner}/${repo}/-/releases/${encodedGitTag}`);
+  t.deepEqual(t.context.log.args[0], ["Uploaded file: %s", uploaded.file.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 asset type and permalink", async (t) => {
   const cwd = "test/fixtures/files";
   const owner = "test_user";