opencode-gitlab-plugin 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
4
4
 
5
+ ## [2.7.0](https://gitlab.com/vglafirov/opencode-gitlab-plugin/compare/v2.6.1...v2.7.0) (2026-08-17)
6
+
7
+
8
+ ### ✨ Features
9
+
10
+ * **merge-requests:** add gitlab_override_requested_changes tool ([50e17d6](https://gitlab.com/vglafirov/opencode-gitlab-plugin/commit/50e17d6119c1373bec8c131b01114206f9486793))
11
+
12
+
13
+ ### 🐛 Bug Fixes
14
+
15
+ * **ci:** regenerate lockfile and reformat with Node 25.2.1 prettier ([58629e2](https://gitlab.com/vglafirov/opencode-gitlab-plugin/commit/58629e2e4e82b33422ecc69d64c903dc55ec101e))
16
+
17
+ ## [2.6.1](https://gitlab.com/vglafirov/opencode-gitlab-plugin/compare/v2.6.0...v2.6.1) (2026-08-17)
18
+
19
+
20
+ ### 🐛 Bug Fixes
21
+
22
+ * **merge-requests:** resolve MWPS 'checking' race in smartMerge/setAutoMerge ([b9a992f](https://gitlab.com/vglafirov/opencode-gitlab-plugin/commit/b9a992f6ebec655fba271623bf5fc963cfe16f09))
23
+ * **work-items:** avoid nullability mismatch on widget inputs ([1ae3cd7](https://gitlab.com/vglafirov/opencode-gitlab-plugin/commit/1ae3cd79627c6cca88955f90af13694472939331))
24
+
5
25
  ## [2.6.0](https://gitlab.com/vglafirov/opencode-gitlab-plugin/compare/v2.5.0...v2.6.0) (2026-05-20)
6
26
 
7
27
 
package/README.md CHANGED
@@ -345,16 +345,17 @@ The plugin provides **64 tools** organized into the following categories:
345
345
 
346
346
  ### Merge Request Tools (8 tools)
347
347
 
348
- | Tool | Description |
349
- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
350
- | `gitlab_get_merge_request` | Get details of a specific merge request with title, description, state, author, assignees, reviewers, labels, and diff stats |
351
- | `gitlab_list_merge_requests` | List merge requests with filtering by state, scope, and labels |
352
- | `gitlab_create_merge_request` | Create a new merge request |
353
- | `gitlab_update_merge_request` | Update merge request title, description, state, assignees, reviewers, and labels |
354
- | `gitlab_get_mr_changes` | Get file changes/diffs for a merge request |
355
- | `gitlab_get_mr_details` | Get additional MR details (commits or pipelines) with detail_type parameter |
356
- | `gitlab_list_merge_request_diffs` | List file diffs with pagination support for large changesets |
357
- | `gitlab_set_mr_auto_merge` | Enable auto-merge (MWPS) using GraphQL API when pipeline succeeds |
348
+ | Tool | Description |
349
+ | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
350
+ | `gitlab_get_merge_request` | Get details of a specific merge request with title, description, state, author, assignees, reviewers, labels, and diff stats |
351
+ | `gitlab_list_merge_requests` | List merge requests with filtering by state, scope, and labels |
352
+ | `gitlab_create_merge_request` | Create a new merge request |
353
+ | `gitlab_update_merge_request` | Update merge request title, description, state, assignees, reviewers, and labels |
354
+ | `gitlab_get_mr_changes` | Get file changes/diffs for a merge request |
355
+ | `gitlab_get_mr_details` | Get additional MR details (commits or pipelines) with detail_type parameter |
356
+ | `gitlab_list_merge_request_diffs` | List file diffs with pagination support for large changesets |
357
+ | `gitlab_set_mr_auto_merge` | Enable auto-merge (MWPS) using GraphQL API when pipeline succeeds |
358
+ | `gitlab_override_requested_changes` | Bypass a "requested changes" merge block (the widget's Bypass button) by setting the override_requested_changes flag |
358
359
 
359
360
  ### Issue Tools (3 tools)
360
361
 
package/dist/index.js CHANGED
@@ -324,6 +324,29 @@ var SET_AUTO_MERGE_MUTATION = `
324
324
  }
325
325
  }
326
326
  `;
327
+ var OVERRIDE_REQUESTED_CHANGES_MUTATION = `
328
+ mutation overrideRequestedChanges(
329
+ $projectPath: ID!
330
+ $iid: String!
331
+ $override: Boolean
332
+ ) {
333
+ mergeRequestUpdate(
334
+ input: {
335
+ projectPath: $projectPath
336
+ iid: $iid
337
+ overrideRequestedChanges: $override
338
+ }
339
+ ) {
340
+ mergeRequest {
341
+ id
342
+ iid
343
+ title
344
+ detailedMergeStatus
345
+ }
346
+ errors
347
+ }
348
+ }
349
+ `;
327
350
  var RESOLVE_DISCUSSION_MUTATION = `
328
351
  mutation resolveDiscussion($discussionId: DiscussionID!, $resolve: Boolean!) {
329
352
  discussionToggleResolve(input: { id: $discussionId, resolve: $resolve }) {
@@ -341,7 +364,7 @@ var RESOLVE_DISCUSSION_MUTATION = `
341
364
  }
342
365
  }
343
366
  `;
344
- var MergeRequestsClient = class extends GitLabApiClient {
367
+ var MergeRequestsClient = class _MergeRequestsClient extends GitLabApiClient {
345
368
  async getMergeRequest(projectId, mrIid, includeChanges) {
346
369
  const encodedProject = this.encodeProjectId(projectId);
347
370
  let path2 = `/projects/${encodedProject}/merge_requests/${mrIid}`;
@@ -563,20 +586,83 @@ var MergeRequestsClient = class extends GitLabApiClient {
563
586
  * Set auto-merge (MWPS) on a merge request using GraphQL API
564
587
  * Uses the mergeRequestAccept mutation with a merge strategy
565
588
  */
566
- async setAutoMerge(projectId, mrIid, sha, strategy = "MERGE_WHEN_CHECKS_PASS") {
567
- const result = await this.fetchGraphQL(SET_AUTO_MERGE_MUTATION, {
589
+ async setAutoMerge(projectId, mrIid, sha, strategy = "MERGE_WHEN_CHECKS_PASS", retries = 3) {
590
+ let lastError = null;
591
+ for (let attempt = 0; attempt <= retries; attempt++) {
592
+ const result = await this.fetchGraphQL(SET_AUTO_MERGE_MUTATION, {
593
+ projectPath: projectId,
594
+ iid: String(mrIid),
595
+ sha,
596
+ strategy
597
+ });
598
+ if (result.mergeRequestAccept.errors.length > 0) {
599
+ const message = result.mergeRequestAccept.errors.join(", ");
600
+ lastError = new Error(`Failed to set auto-merge: ${message}`);
601
+ if (attempt < retries && _MergeRequestsClient.isTransientMergeError(message)) {
602
+ await _MergeRequestsClient.delay(750 * (attempt + 1));
603
+ continue;
604
+ }
605
+ throw lastError;
606
+ }
607
+ if (!result.mergeRequestAccept.mergeRequest) {
608
+ lastError = new Error("Failed to set auto-merge: No merge request returned");
609
+ if (attempt < retries) {
610
+ await _MergeRequestsClient.delay(750 * (attempt + 1));
611
+ continue;
612
+ }
613
+ throw lastError;
614
+ }
615
+ return result.mergeRequestAccept.mergeRequest;
616
+ }
617
+ throw lastError ?? new Error("Failed to set auto-merge: exhausted retries");
618
+ }
619
+ /**
620
+ * True for GitLab merge errors that are transient because mergeability is
621
+ * still being (re)computed. Such errors clear on their own within a second
622
+ * or two, so callers should retry rather than treat them as real failures.
623
+ */
624
+ static isTransientMergeError(message) {
625
+ const m = message.toLowerCase();
626
+ return m.includes("being checked") || m.includes("being recomputed") || m.includes("cannot be merged") || m.includes("not mergeable") || m.includes("merge failed") || m.includes("try again");
627
+ }
628
+ /** Small awaitable delay helper (ms). */
629
+ static delay(ms) {
630
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
631
+ }
632
+ /**
633
+ * Override (or clear an override of) requested changes on a merge request.
634
+ *
635
+ * When a reviewer formally requests changes, the "Change requests must be
636
+ * approved by the requesting user" merge check blocks the merge. Setting the
637
+ * persistent `override_requested_changes` flag downgrades that check from a
638
+ * blocking failure to a non-blocking warning, which is what the "Bypass"
639
+ * button in the merge request widget does. This is distinct from
640
+ * `mergeRequestDestroyRequestedChanges`, which only clears the calling user's
641
+ * own requested-changes review.
642
+ *
643
+ * Requires permission to merge to the target branch (otherwise GitLab
644
+ * silently ignores the flag).
645
+ *
646
+ * @param projectId Project ID or URL-encoded path
647
+ * @param mrIid Internal ID of the merge request
648
+ * @param override true to bypass requested changes, false to re-enable the block
649
+ * @returns The updated merge request (id, iid, title, detailedMergeStatus)
650
+ */
651
+ async overrideRequestedChanges(projectId, mrIid, override = true) {
652
+ const result = await this.fetchGraphQL(OVERRIDE_REQUESTED_CHANGES_MUTATION, {
568
653
  projectPath: projectId,
569
654
  iid: String(mrIid),
570
- sha,
571
- strategy
655
+ override
572
656
  });
573
- if (result.mergeRequestAccept.errors.length > 0) {
574
- throw new Error(`Failed to set auto-merge: ${result.mergeRequestAccept.errors.join(", ")}`);
657
+ if (result.mergeRequestUpdate.errors.length > 0) {
658
+ throw new Error(
659
+ `Failed to override requested changes: ${result.mergeRequestUpdate.errors.join(", ")}`
660
+ );
575
661
  }
576
- if (!result.mergeRequestAccept.mergeRequest) {
577
- throw new Error("Failed to set auto-merge: No merge request returned");
662
+ if (!result.mergeRequestUpdate.mergeRequest) {
663
+ throw new Error("Failed to override requested changes: No merge request returned");
578
664
  }
579
- return result.mergeRequestAccept.mergeRequest;
665
+ return result.mergeRequestUpdate.mergeRequest;
580
666
  }
581
667
  /**
582
668
  * Approve a merge request
@@ -624,7 +710,13 @@ var MergeRequestsClient = class extends GitLabApiClient {
624
710
  * Returns detailed context about what happened
625
711
  */
626
712
  async smartMerge(projectId, mrIid, sha, strategy = "MERGE_WHEN_CHECKS_PASS") {
627
- const mr = await this.getMergeRequest(projectId, mrIid);
713
+ let mr = await this.getMergeRequest(projectId, mrIid);
714
+ if (mr.detailed_merge_status === "checking") {
715
+ for (let attempt = 0; attempt < 5 && mr.detailed_merge_status === "checking"; attempt++) {
716
+ await _MergeRequestsClient.delay(1e3 * (attempt + 1));
717
+ mr = await this.getMergeRequest(projectId, mrIid);
718
+ }
719
+ }
628
720
  const context = {
629
721
  state: mr.state,
630
722
  detailedMergeStatus: mr.detailed_merge_status,
@@ -663,14 +755,13 @@ var MergeRequestsClient = class extends GitLabApiClient {
663
755
  };
664
756
  }
665
757
  }
666
- const autoMergeStatuses = ["ci_still_running", "not_approved", "checking"];
758
+ const autoMergeStatuses = ["ci_still_running", "not_approved"];
667
759
  if (autoMergeStatuses.includes(mr.detailed_merge_status)) {
668
760
  try {
669
761
  const result = await this.setAutoMerge(projectId, mrIid, sha, strategy);
670
762
  const autoMergeMessages = {
671
763
  ci_still_running: "pipeline passes",
672
- not_approved: "approved",
673
- checking: "checks complete"
764
+ not_approved: "approved"
674
765
  };
675
766
  return {
676
767
  action: "auto_merge_enabled",
@@ -1029,22 +1120,8 @@ var GET_WORK_ITEM_NOTES_QUERY = `
1029
1120
  }
1030
1121
  `;
1031
1122
  var CREATE_WORK_ITEM_MUTATION = `
1032
- mutation createWorkItem(
1033
- $projectPath: ID!
1034
- $title: String!
1035
- $workItemTypeId: WorkItemsTypeID!
1036
- $description: String
1037
- $labelIds: [LabelID!]
1038
- $assigneeIds: [UserID!]
1039
- ) {
1040
- workItemCreate(input: {
1041
- projectPath: $projectPath
1042
- title: $title
1043
- workItemTypeId: $workItemTypeId
1044
- descriptionWidget: { description: $description }
1045
- labelsWidget: { labelIds: $labelIds }
1046
- assigneesWidget: { assigneeIds: $assigneeIds }
1047
- }) {
1123
+ mutation createWorkItem($input: WorkItemCreateInput!) {
1124
+ workItemCreate(input: $input) {
1048
1125
  workItem {
1049
1126
  ${WORK_ITEM_FIELDS}
1050
1127
  }
@@ -1053,22 +1130,8 @@ var CREATE_WORK_ITEM_MUTATION = `
1053
1130
  }
1054
1131
  `;
1055
1132
  var UPDATE_WORK_ITEM_MUTATION = `
1056
- mutation updateWorkItem(
1057
- $id: WorkItemID!
1058
- $title: String
1059
- $description: String
1060
- $stateEvent: WorkItemStateEvent
1061
- $labelIds: [LabelID!]
1062
- $assigneeIds: [UserID!]
1063
- ) {
1064
- workItemUpdate(input: {
1065
- id: $id
1066
- title: $title
1067
- descriptionWidget: { description: $description }
1068
- stateEvent: $stateEvent
1069
- labelsWidget: { labelIds: $labelIds }
1070
- assigneesWidget: { assigneeIds: $assigneeIds }
1071
- }) {
1133
+ mutation updateWorkItem($input: WorkItemUpdateInput!) {
1134
+ workItemUpdate(input: $input) {
1072
1135
  workItem {
1073
1136
  ${WORK_ITEM_FIELDS}
1074
1137
  }
@@ -1162,14 +1225,21 @@ var WorkItemsClient = class extends GitLabApiClient {
1162
1225
  async createWorkItem(projectId, options) {
1163
1226
  const labelIds = options.labels?.map((l) => toGid("Label", l));
1164
1227
  const assigneeIds = options.assignee_ids?.map((id) => toGid("User", id));
1165
- const result = await this.fetchGraphQL(CREATE_WORK_ITEM_MUTATION, {
1228
+ const input = {
1166
1229
  projectPath: projectId,
1167
1230
  title: options.title,
1168
- workItemTypeId: toGid("WorkItems::Type", options.work_item_type_id),
1169
- description: options.description,
1170
- labelIds: labelIds?.length ? labelIds : void 0,
1171
- assigneeIds: assigneeIds?.length ? assigneeIds : void 0
1172
- });
1231
+ workItemTypeId: toGid("WorkItems::Type", options.work_item_type_id)
1232
+ };
1233
+ if (options.description !== void 0) {
1234
+ input.descriptionWidget = { description: options.description };
1235
+ }
1236
+ if (labelIds?.length) {
1237
+ input.labelsWidget = { labelIds };
1238
+ }
1239
+ if (assigneeIds?.length) {
1240
+ input.assigneesWidget = { assigneeIds };
1241
+ }
1242
+ const result = await this.fetchGraphQL(CREATE_WORK_ITEM_MUTATION, { input });
1173
1243
  if (result.workItemCreate.errors.length > 0) {
1174
1244
  throw new Error(`Failed to create work item: ${result.workItemCreate.errors.join(", ")}`);
1175
1245
  }
@@ -1187,14 +1257,23 @@ var WorkItemsClient = class extends GitLabApiClient {
1187
1257
  const stateEvent = options.state_event ? options.state_event === "close" ? "CLOSE" : "REOPEN" : void 0;
1188
1258
  const labelIds = options.labels?.map((l) => toGid("Label", l));
1189
1259
  const assigneeIds = options.assignee_ids?.map((id) => toGid("User", id));
1190
- const result = await this.fetchGraphQL(UPDATE_WORK_ITEM_MUTATION, {
1191
- id: gid,
1192
- title: options.title,
1193
- description: options.description,
1194
- stateEvent,
1195
- labelIds: labelIds?.length ? labelIds : void 0,
1196
- assigneeIds: assigneeIds?.length ? assigneeIds : void 0
1197
- });
1260
+ const input = { id: gid };
1261
+ if (options.title !== void 0) {
1262
+ input.title = options.title;
1263
+ }
1264
+ if (options.description !== void 0) {
1265
+ input.descriptionWidget = { description: options.description };
1266
+ }
1267
+ if (stateEvent !== void 0) {
1268
+ input.stateEvent = stateEvent;
1269
+ }
1270
+ if (labelIds?.length) {
1271
+ input.labelsWidget = { addLabelIds: labelIds };
1272
+ }
1273
+ if (assigneeIds?.length) {
1274
+ input.assigneesWidget = { assigneeIds };
1275
+ }
1276
+ const result = await this.fetchGraphQL(UPDATE_WORK_ITEM_MUTATION, { input });
1198
1277
  if (result.workItemUpdate.errors.length > 0) {
1199
1278
  throw new Error(`Failed to update work item: ${result.workItemUpdate.errors.join(", ")}`);
1200
1279
  }
@@ -2966,6 +3045,29 @@ Always provide sha when you want to ensure you're merging the exact code that wa
2966
3045
  return JSON.stringify(result, null, 2);
2967
3046
  }
2968
3047
  }),
3048
+ gitlab_override_requested_changes: tool({
3049
+ description: `Bypass (or re-enable) a "requested changes" merge block on a merge request.
3050
+
3051
+ When a reviewer formally requests changes, the merge check "Change requests must be approved by the requesting user" blocks the merge (detailed_merge_status: requested_changes). This tool sets the persistent override_requested_changes flag, which is exactly what the "Bypass" button in the MR widget does: it downgrades that check from a blocking failure to a non-blocking warning so the MR can be merged.
3052
+
3053
+ Use this when the requesting reviewer is unavailable (e.g. on PTO) and their concerns have been addressed. The acting user must have permission to merge to the target branch, otherwise GitLab silently ignores the flag.
3054
+
3055
+ Note: this is NOT the same as clearing your own requested-changes review. It overrides another user's request. Set override=false to restore the block.`,
3056
+ args: {
3057
+ project_id: z.string().describe("The project ID or URL-encoded path"),
3058
+ mr_iid: z.number().describe("The internal ID of the merge request"),
3059
+ override: z.boolean().optional().describe("true (default) to bypass requested changes, false to restore the block")
3060
+ },
3061
+ execute: async (args, _ctx) => {
3062
+ const client = getGitLabClient();
3063
+ const result = await client.overrideRequestedChanges(
3064
+ args.project_id,
3065
+ args.mr_iid,
3066
+ args.override ?? true
3067
+ );
3068
+ return JSON.stringify(result, null, 2);
3069
+ }
3070
+ }),
2969
3071
  gitlab_approve_merge_request: tool({
2970
3072
  description: `Approve a merge request.
2971
3073
  Adds the current user's approval to the merge request.