Repository: semantic-release/gitlab
Pull request #852 (2025-05-16 14:48:25)

feat: raise error when GitLab CLI is required but not installed
Contributes to https://github.com/semantic-release/gitlab/issues/822


Patch:
diff --git a/lib/definitions/errors.js b/lib/definitions/errors.js
index 5150853..7c9e8fc 100644
--- a/lib/definitions/errors.js
+++ b/lib/definitions/errors.js
@@ -87,4 +87,8 @@ Please make sure the GitLab user associated with the token has the [permission t
 
 Please make sure to create a [GitLab personal access token](https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html) and to set it in the \`GL_TOKEN\` or \`GITLAB_TOKEN\` environment variable on your CI environment. The token must allow to push to the repository ${repositoryUrl}.`,
   }),
+  EGLABNOTINSTALLED: () => ({
+    message: 'GitLab CLI not installed.',
+    details: 'The [GitLab CLI needs to be installed](https://gitlab.com/gitlab-org/cli#installation) so that the project\'s CI components can be published.',
+  }),
 };
diff --git a/lib/glab.js b/lib/glab.js
new file mode 100644
index 0000000..82fb2b9
--- /dev/null
+++ b/lib/glab.js
@@ -0,0 +1,5 @@
+import { execa } from "execa";
+
+export default async (args, options) => {
+  return execa("glab", args, options);
+};
diff --git a/lib/publish.js b/lib/publish.js
index 379cf68..379f299 100644
--- a/lib/publish.js
+++ b/lib/publish.js
@@ -12,7 +12,7 @@ import resolveConfig from "./resolve-config.js";
 import getAssets from "./glob-assets.js";
 import { RELEASE_NAME } from "./definitions/constants.js";
 import getProjectContext from "./get-project-context.js";
-import { execa } from "execa";
+import glab from "./glab.js";
 const isUrlScheme = (value) => /^(https|http|ftp):\/\//.test(value);
 
 export default async (pluginConfig, context) => {
@@ -195,7 +195,7 @@ export default async (pluginConfig, context) => {
 
   if (publishToCatalog) {
     try {
-      await execa("glab", ["repo", "publish", "catalog", gitTag], {
+      await glab(["repo", "publish", "catalog", gitTag], {
         cwd,
         timeout: 30 * 1000,
         env: { GITLAB_TOKEN: gitlabToken },
diff --git a/lib/verify.js b/lib/verify.js
index fdfc3b7..58b1c7c 100644
--- a/lib/verify.js
+++ b/lib/verify.js
@@ -30,7 +30,10 @@ export default async (pluginConfig, context) => {
     options: { repositoryUrl },
     logger,
   } = context;
-  const { gitlabToken, gitlabUrl, gitlabApiUrl, proxy, ...options } = resolveConfig(pluginConfig, context);
+  const { gitlabToken, gitlabUrl, gitlabApiUrl, proxy, publishToCatalog, ...options } = resolveConfig(
+    pluginConfig,
+    context
+  );
   const { projectPath, projectApiUrl } = getProjectContext(context, gitlabUrl, gitlabApiUrl, repositoryUrl);
 
   debug("apiUrl: %o", gitlabApiUrl);
@@ -89,6 +92,18 @@ export default async (pluginConfig, context) => {
     }
   }
 
+  if (publishToCatalog === true) {
+    try {
+      logger.log("Verifying that the GitLab CLI is installed");
+      await glab(["version"], {
+        timeout: 5 * 1000,
+      });
+    } catch (error) {
+      logger.error("The GitLab CLI is required but failed to run:\n%O", error);
+      errors.push(getError("EGLABNOTINSTALLED"));
+    }
+  }
+
   if (errors.length > 0) {
     throw new AggregateError(errors);
   }


Test patch:
diff --git a/test/verify.test.js b/test/verify.test.js
index 48f3004..5fdb6f2 100644
--- a/test/verify.test.js
+++ b/test/verify.test.js
@@ -3,6 +3,7 @@ import nock from "nock";
 import { stub } from "sinon";
 import verify from "../lib/verify.js";
 import authenticate from "./helpers/mock-gitlab.js";
+import * as td from "testdouble";
 
 /* eslint camelcase: ["error", {properties: "never"}] */
 
@@ -988,3 +989,34 @@ test.serial(
     t.true(gitlab.isDone());
   }
 );
+
+test.serial(
+  'Throw SemanticReleaseError if "publishToCatalog" option is set and the GitLab CLI is not installed.',
+  async (t) => {
+    const owner = "test_user";
+    const repo = "test_repo";
+    const env = { GITLAB_TOKEN: "gitlab_token" };
+    const gitlab = authenticate(env)
+      .get(`/projects/${owner}%2F${repo}`)
+      .reply(200, { permissions: { project_access: { access_level: 40 } } });
+
+    const execa = (await td.replaceEsm("execa")).execa;
+    td.when(
+      execa("glab", ["version"], {
+        timeout: 5000,
+      })
+    ).thenReject();
+    const verifyWithMockExeca = (await import("../lib/verify.js")).default;
+    const {
+      errors: [error],
+    } = await t.throwsAsync(
+      verifyWithMockExeca(
+        { publishToCatalog: true },
+        { env, options: { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }, logger: t.context.logger }
+      )
+    );
+    t.is(error.name, "SemanticReleaseError");
+    t.is(error.code, "EGLABNOTINSTALLED");
+    t.true(gitlab.isDone());
+  }
+);