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 +20 -0
- package/README.md +11 -10
- package/dist/index.js +162 -60
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.cjs +163 -61
- package/dist/mcp-server.cjs.map +1 -1
- package/package.json +1 -1
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
|
|
349
|
-
|
|
|
350
|
-
| `gitlab_get_merge_request`
|
|
351
|
-
| `gitlab_list_merge_requests`
|
|
352
|
-
| `gitlab_create_merge_request`
|
|
353
|
-
| `gitlab_update_merge_request`
|
|
354
|
-
| `gitlab_get_mr_changes`
|
|
355
|
-
| `gitlab_get_mr_details`
|
|
356
|
-
| `gitlab_list_merge_request_diffs`
|
|
357
|
-
| `gitlab_set_mr_auto_merge`
|
|
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
|
-
|
|
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
|
-
|
|
571
|
-
strategy
|
|
655
|
+
override
|
|
572
656
|
});
|
|
573
|
-
if (result.
|
|
574
|
-
throw new Error(
|
|
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.
|
|
577
|
-
throw new Error("Failed to
|
|
662
|
+
if (!result.mergeRequestUpdate.mergeRequest) {
|
|
663
|
+
throw new Error("Failed to override requested changes: No merge request returned");
|
|
578
664
|
}
|
|
579
|
-
return result.
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1228
|
+
const input = {
|
|
1166
1229
|
projectPath: projectId,
|
|
1167
1230
|
title: options.title,
|
|
1168
|
-
workItemTypeId: toGid("WorkItems::Type", options.work_item_type_id)
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
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
|
|
1191
|
-
|
|
1192
|
-
title
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
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.
|