Repository: semantic-release/gitlab (js)

Issue:
Response code 429 (Too Many Requests) on successComment
We are running into the gitlab API limit on some projects when multiple semantic-releases are running at the same time from share IPs.

```
[8:07:42 AM] [semantic-release] › ℹ  Start step "success" of plugin "@semantic-release/gitlab"
[8:07:44 AM] [semantic-release] [@semantic-release/gitlab] › ✖  An error occurred while posting comments to related issues and merge requests:
HTTPError: Response code 429 (Too Many Requests)
    at Request.<anonymous> (/usr/local/lib/node_modules/@semantic-release/gitlab/node_modules/got/dist/source/as-promise/index.js:117:42)
    at processTicksAndRejections (node:internal/process/task_queues:96:5) {
  code: 'ERR_NON_2XX_3XX_RESPONSE',
```

As far as I understand the code https://github.com/semantic-release/gitlab/blob/3051799160efc127585d30a4ef776ccf34185fee/lib/success.js#L72 for very Commit the script is looking for a MR, with those MR the script is searching for an resolved issue. So there will be at least  commits * MR Requests. As we do not use gitlab issues (but like the MR comment), what about a configuration to disable the Issue Comments and hence those requests? This will limit the requests to count of commits. 

Another Option would be rate limiting the requests, but that seems like a lot more work but more sustainable solution.

Pull request:
fix: increase retry limit and make it configurable
Resolves #361 

Patch:
diff --git a/README.md b/README.md
index 3691435..52c3429 100644
--- a/README.md
+++ b/README.md
@@ -95,6 +95,7 @@ If you need to bypass the proxy for some hosts, configure the `NO_PROXY` environ
 | `failCommentCondition`    | Use this as condition, when to comment on or create an issues in case of failures. See [failCommentCondition](#failCommentCondition).                                                                                                                                                                                      | -                                                                                                                                                                       |
 | `labels`                  | The [labels](https://docs.gitlab.com/ee/user/project/labels.html#labels) to add to the issue created when a release fails. Set to `false` to not add any label. Labels should be comma-separated as described in the [official docs](https://docs.gitlab.com/ee/api/issues.html#new-issue), e.g. `"semantic-release,bot"`. | `semantic-release`                                                                                                                                                      |
 | `assignee`                | The [assignee](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#assignee) to add to the issue created when a release fails.                                                                                                                                                                             | -                                                                                                                                                                       |
+| `retryLimit`              | The maximum number of retries for failing HTTP requests.                                                                                                                                                                                                                                                                   | `3`                                                                                                                                                                     |
 
 #### assets
 
diff --git a/lib/fail.js b/lib/fail.js
index 018f451..d33a0c9 100644
--- a/lib/fail.js
+++ b/lib/fail.js
@@ -14,11 +14,23 @@ export default async (pluginConfig, context) => {
     errors,
     logger,
   } = context;
-  const { gitlabToken, gitlabUrl, gitlabApiUrl, failComment, failTitle, failCommentCondition, labels, assignee } =
-    resolveConfig(pluginConfig, context);
+  const {
+    gitlabToken,
+    gitlabUrl,
+    gitlabApiUrl,
+    failComment,
+    failTitle,
+    failCommentCondition,
+    labels,
+    assignee,
+    retryLimit,
+  } = resolveConfig(pluginConfig, context);
   const repoId = getRepoId(context, gitlabUrl, repositoryUrl);
   const encodedRepoId = encodeURIComponent(repoId);
-  const apiOptions = { headers: { "PRIVATE-TOKEN": gitlabToken } };
+  const apiOptions = {
+    headers: { "PRIVATE-TOKEN": gitlabToken },
+    retry: { limit: retryLimit },
+  };
 
   if (failComment === false || failTitle === false) {
     logger.log("Skip issue creation.");
diff --git a/lib/publish.js b/lib/publish.js
index 12b60ad..f3a1cc5 100644
--- a/lib/publish.js
+++ b/lib/publish.js
@@ -22,7 +22,10 @@ export default async (pluginConfig, context) => {
     nextRelease: { gitTag, gitHead, notes, version },
     logger,
   } = context;
-  const { gitlabToken, gitlabUrl, gitlabApiUrl, assets, milestones, proxy } = resolveConfig(pluginConfig, context);
+  const { gitlabToken, gitlabUrl, gitlabApiUrl, assets, milestones, proxy, retryLimit } = resolveConfig(
+    pluginConfig,
+    context
+  );
   const assetsList = [];
   const repoId = getRepoId(context, gitlabUrl, repositoryUrl);
   const encodedRepoId = encodeURIComponent(repoId);
@@ -46,6 +49,7 @@ export default async (pluginConfig, context) => {
         },
       ],
     },
+    retry: { limit: retryLimit },
   };
 
   debug("repoId: %o", repoId);
diff --git a/lib/resolve-config.js b/lib/resolve-config.js
index 295fce3..671fcd1 100644
--- a/lib/resolve-config.js
+++ b/lib/resolve-config.js
@@ -15,6 +15,7 @@ export default (
     failCommentCondition,
     labels,
     assignee,
+    retryLimit,
   },
   {
     envCi: { service } = {},
@@ -34,6 +35,7 @@ export default (
     },
   }
 ) => {
+  const DEFAULT_RETRY_LIMIT = 3;
   const userGitlabApiPathPrefix = isNil(gitlabApiPathPrefix)
     ? isNil(GL_PREFIX)
       ? GITLAB_PREFIX
@@ -64,6 +66,7 @@ export default (
     failCommentCondition,
     labels: isNil(labels) ? "semantic-release" : labels === false ? false : labels,
     assignee,
+    retryLimit: retryLimit ?? DEFAULT_RETRY_LIMIT,
   };
 };
 
diff --git a/lib/success.js b/lib/success.js
index 910f020..f7ed11f 100644
--- a/lib/success.js
+++ b/lib/success.js
@@ -15,13 +15,14 @@ export default async (pluginConfig, context) => {
     commits,
     releases,
   } = context;
-  const { gitlabToken, gitlabUrl, gitlabApiUrl, successComment, successCommentCondition, proxy } = resolveConfig(
-    pluginConfig,
-    context
-  );
+  const { gitlabToken, gitlabUrl, gitlabApiUrl, successComment, successCommentCondition, proxy, retryLimit } =
+    resolveConfig(pluginConfig, context);
   const repoId = getRepoId(context, gitlabUrl, repositoryUrl);
   const encodedRepoId = encodeURIComponent(repoId);
-  const apiOptions = { headers: { "PRIVATE-TOKEN": gitlabToken } };
+  const apiOptions = {
+    headers: { "PRIVATE-TOKEN": gitlabToken },
+    retry: { limit: retryLimit },
+  };
 
   if (successComment === false) {
     logger.log("Skip commenting on issues and pull requests.");


Test patch:
diff --git a/test/resolve-config.test.js b/test/resolve-config.test.js
index 91efa76..525e9e4 100644
--- a/test/resolve-config.test.js
+++ b/test/resolve-config.test.js
@@ -17,6 +17,7 @@ const defaultOptions = {
   labels: "semantic-release",
   assignee: undefined,
   proxy: {},
+  retryLimit: 3,
 };
 
 test("Returns user config", (t) => {
@@ -27,10 +28,11 @@ test("Returns user config", (t) => {
   const postComments = true;
   const proxy = {};
   const labels = false;
+  const retryLimit = 42;
 
   t.deepEqual(
     resolveConfig(
-      { gitlabUrl, gitlabApiPathPrefix, assets, postComments, labels },
+      { gitlabUrl, gitlabApiPathPrefix, assets, postComments, labels, retryLimit },
       { env: { GITLAB_TOKEN: gitlabToken } }
     ),
     {
@@ -40,6 +42,7 @@ test("Returns user config", (t) => {
       gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
       assets,
       labels: false,
+      retryLimit,
     }
   );
 
diff --git a/test/success.test.js b/test/success.test.js
index 9e6272f..2a3f2d4 100644
--- a/test/success.test.js
+++ b/test/success.test.js
@@ -305,3 +305,30 @@ test.serial("Does not post comments when successCommentCondition is set to false
 
   t.true(gitlab.isDone());
 });
+
+test.serial("Retries requests when rate limited", async (t) => {
+  const owner = "test_user";
+  const repo = "test_repo";
+  const env = { GITLAB_TOKEN: "gitlab_token" };
+  const pluginConfig = {};
+  const nextRelease = { version: "1.0.0" };
+  const releases = [{ name: RELEASE_NAME, url: "https://gitlab.com/test_user/test_repo/-/releases/v1.0.0" }];
+  const options = { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` };
+  const encodedRepoId = encodeURIComponent(`${owner}/${repo}`);
+  const commits = [{ hash: "abcdef" }];
+  const retryLimit = 3;
+  const gitlab = authenticate(env)
+    .get(`/projects/${encodedRepoId}/repository/commits/abcdef/merge_requests`)
+    .times(retryLimit)
+    .reply(429)
+    .get(`/projects/${encodedRepoId}/repository/commits/abcdef/merge_requests`)
+    .reply(200, [{ project_id: 100, iid: 1, state: "merged" }])
+    .get(`/projects/100/merge_requests/1/closes_issues`)
+    .reply(200, [])
+    .post(`/projects/100/merge_requests/1/notes`)
+    .reply(200);
+
+  await success(pluginConfig, { env, options, nextRelease, logger: t.context.logger, commits, releases, retryLimit });
+
+  t.true(gitlab.isDone());
+});