gitlab-mcp 1.4.1 → 1.5.1
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/README.md +45 -34
- package/dist/config/env.d.ts +7 -1
- package/dist/config/env.js +17 -0
- package/dist/config/env.js.map +1 -1
- package/dist/http-app.js +422 -17
- package/dist/http-app.js.map +1 -1
- package/dist/http.js +10 -1
- package/dist/http.js.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/download-token.d.ts +24 -0
- package/dist/lib/download-token.js +84 -0
- package/dist/lib/download-token.js.map +1 -0
- package/dist/lib/gitlab-client.d.ts +78 -1
- package/dist/lib/gitlab-client.js +452 -8
- package/dist/lib/gitlab-client.js.map +1 -1
- package/dist/lib/http-auth-guard.d.ts +8 -0
- package/dist/lib/http-auth-guard.js +19 -0
- package/dist/lib/http-auth-guard.js.map +1 -0
- package/dist/lib/mcp-oauth-provider.d.ts +2 -0
- package/dist/lib/mcp-oauth-provider.js +61 -0
- package/dist/lib/mcp-oauth-provider.js.map +1 -0
- package/dist/lib/oauth.d.ts +3 -1
- package/dist/lib/oauth.js +5 -5
- package/dist/lib/oauth.js.map +1 -1
- package/dist/lib/patch-helper.d.ts +13 -0
- package/dist/lib/patch-helper.js +156 -0
- package/dist/lib/patch-helper.js.map +1 -0
- package/dist/lib/request-runtime.d.ts +1 -0
- package/dist/lib/request-runtime.js +42 -4
- package/dist/lib/request-runtime.js.map +1 -1
- package/dist/tools/gitlab.js +2754 -53
- package/dist/tools/gitlab.js.map +1 -1
- package/docs/authentication.md +37 -8
- package/docs/configuration.md +7 -4
- package/docs/tools.md +153 -51
- package/package.json +1 -1
package/dist/tools/gitlab.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
1
2
|
import { Kind, parse } from "graphql";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { GitLabApiError } from "../lib/gitlab-client.js";
|
|
5
|
+
import { applySearchReplace, applyUnifiedDiff, parseSearchReplaceBlocks } from "../lib/patch-helper.js";
|
|
4
6
|
import { bodySchema, displayNameSchema, nullableOptional, optionalBodySchema, optionalDisplayNameSchema, optionalProjectIdSchema, optionalRefLikeSchema, optionalUrlOrPathSchema, projectIdSchema, refLikeSchema, slugSchema } from "../lib/tool-schema.js";
|
|
5
7
|
import { getSessionAuth } from "../lib/auth-context.js";
|
|
8
|
+
import { createDownloadToken } from "../lib/download-token.js";
|
|
6
9
|
import { stripNullsDeep } from "../lib/sanitize.js";
|
|
7
10
|
import { getMergeRequestCodeContext, mergeRequestCodeContextSchema } from "./mr-code-context.js";
|
|
8
11
|
const readCapabilities = ["read"];
|
|
@@ -30,6 +33,40 @@ const paginationShape = {
|
|
|
30
33
|
page: optionalNumber,
|
|
31
34
|
per_page: optionalNumber
|
|
32
35
|
};
|
|
36
|
+
const emojiNameSchema = z.string().min(1);
|
|
37
|
+
const awardEmojiIdSchema = z.string().min(1);
|
|
38
|
+
const workItemTypes = [
|
|
39
|
+
"issue",
|
|
40
|
+
"task",
|
|
41
|
+
"incident",
|
|
42
|
+
"test_case",
|
|
43
|
+
"epic",
|
|
44
|
+
"key_result",
|
|
45
|
+
"objective",
|
|
46
|
+
"requirement",
|
|
47
|
+
"ticket"
|
|
48
|
+
];
|
|
49
|
+
const workItemTypeSchema = z.preprocess((value) => (typeof value === "string" ? value.toLowerCase() : value), z.enum(workItemTypes));
|
|
50
|
+
const optionalWorkItemType = nullableOptional(workItemTypeSchema);
|
|
51
|
+
const workItemIidSchema = z.coerce.number().int().positive();
|
|
52
|
+
const optionalCoercedNumber = nullableOptional(z.coerce.number());
|
|
53
|
+
const optionalCoercedBoolean = nullableOptional(z.coerce.boolean());
|
|
54
|
+
const workItemReferenceSchema = z.object({
|
|
55
|
+
project_id: projectIdSchema,
|
|
56
|
+
iid: workItemIidSchema
|
|
57
|
+
});
|
|
58
|
+
const linkedWorkItemReferenceSchema = z.object({
|
|
59
|
+
project_id: projectIdSchema,
|
|
60
|
+
iid: workItemIidSchema,
|
|
61
|
+
link_type: z.enum(["RELATED", "BLOCKED_BY", "BLOCKS"]).optional()
|
|
62
|
+
});
|
|
63
|
+
const customFieldValueSchema = z.object({
|
|
64
|
+
custom_field_id: z.string().min(1),
|
|
65
|
+
text_value: optionalString,
|
|
66
|
+
number_value: optionalCoercedNumber,
|
|
67
|
+
selected_option_ids: optionalStringArray,
|
|
68
|
+
date_value: optionalString
|
|
69
|
+
});
|
|
33
70
|
export function registerGitLabTools(server, context) {
|
|
34
71
|
const definitions = getGitLabToolDefinitions();
|
|
35
72
|
const disableGraphqlTools = shouldDisableGraphqlTools(context.env.GITLAB_ALLOWED_PROJECT_IDS, context.env.GITLAB_ALLOW_GRAPHQL_WITH_PROJECT_SCOPE);
|
|
@@ -113,6 +150,7 @@ function getGitLabToolDefinitions() {
|
|
|
113
150
|
capabilities: readCapabilities,
|
|
114
151
|
inputSchema: {
|
|
115
152
|
search: optionalString,
|
|
153
|
+
topic: optionalString,
|
|
116
154
|
search_namespaces: optionalBoolean,
|
|
117
155
|
membership: optionalBoolean,
|
|
118
156
|
owned: optionalBoolean,
|
|
@@ -154,6 +192,26 @@ function getGitLabToolDefinitions() {
|
|
|
154
192
|
default_branch: getOptionalString(args, "default_branch")
|
|
155
193
|
})
|
|
156
194
|
},
|
|
195
|
+
{
|
|
196
|
+
name: "gitlab_create_group",
|
|
197
|
+
title: "Create Group",
|
|
198
|
+
description: "Create a new GitLab group or subgroup.",
|
|
199
|
+
capabilities: adminCapabilities,
|
|
200
|
+
inputSchema: {
|
|
201
|
+
name: displayNameSchema,
|
|
202
|
+
path: z.string().min(1),
|
|
203
|
+
description: optionalString,
|
|
204
|
+
visibility: z.enum(["private", "internal", "public"]).optional(),
|
|
205
|
+
parent_id: optionalNumber
|
|
206
|
+
},
|
|
207
|
+
handler: async (args, context) => context.gitlab.createGroup({
|
|
208
|
+
name: getString(args, "name"),
|
|
209
|
+
path: getString(args, "path"),
|
|
210
|
+
description: getOptionalString(args, "description"),
|
|
211
|
+
visibility: getOptionalString(args, "visibility"),
|
|
212
|
+
parent_id: getOptionalNumber(args, "parent_id")
|
|
213
|
+
})
|
|
214
|
+
},
|
|
157
215
|
{
|
|
158
216
|
name: "gitlab_list_project_members",
|
|
159
217
|
title: "List Project Members",
|
|
@@ -183,6 +241,7 @@ function getGitLabToolDefinitions() {
|
|
|
183
241
|
group_id: z.string(),
|
|
184
242
|
include_subgroups: optionalBoolean,
|
|
185
243
|
search: optionalString,
|
|
244
|
+
topic: optionalString,
|
|
186
245
|
order_by: z
|
|
187
246
|
.enum(["name", "path", "created_at", "updated_at", "last_activity_at"])
|
|
188
247
|
.optional(),
|
|
@@ -253,10 +312,62 @@ function getGitLabToolDefinitions() {
|
|
|
253
312
|
project_id: optionalProjectIdSchema,
|
|
254
313
|
search: z.string().min(1),
|
|
255
314
|
ref: optionalRefLikeSchema,
|
|
315
|
+
filename: optionalString,
|
|
316
|
+
path: optionalString,
|
|
317
|
+
extension: optionalString,
|
|
318
|
+
...paginationShape
|
|
319
|
+
},
|
|
320
|
+
handler: async (args, context) => context.gitlab.searchCodeBlobs(resolveProjectId(args, context, true), getString(args, "search"), { query: toQuery(omit(args, ["project_id", "search"])) })
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: "gitlab_search_code",
|
|
324
|
+
title: "Search Code",
|
|
325
|
+
description: "Search code across all projects on the GitLab instance. Requires GitLab code search support.",
|
|
326
|
+
capabilities: readCapabilities,
|
|
327
|
+
inputSchema: {
|
|
328
|
+
search: z.string().min(1),
|
|
329
|
+
filename: optionalString,
|
|
330
|
+
path: optionalString,
|
|
331
|
+
extension: optionalString,
|
|
332
|
+
...paginationShape
|
|
333
|
+
},
|
|
334
|
+
handler: async (args, context) => context.gitlab.searchCode(getString(args, "search"), {
|
|
335
|
+
query: toQuery(omit(args, ["search"]))
|
|
336
|
+
})
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
name: "gitlab_search_project_code",
|
|
340
|
+
title: "Search Project Code",
|
|
341
|
+
description: "Search code in a specific project.",
|
|
342
|
+
capabilities: readCapabilities,
|
|
343
|
+
inputSchema: {
|
|
344
|
+
project_id: optionalProjectIdSchema,
|
|
345
|
+
search: z.string().min(1),
|
|
346
|
+
ref: optionalRefLikeSchema,
|
|
347
|
+
filename: optionalString,
|
|
348
|
+
path: optionalString,
|
|
349
|
+
extension: optionalString,
|
|
256
350
|
...paginationShape
|
|
257
351
|
},
|
|
258
352
|
handler: async (args, context) => context.gitlab.searchCodeBlobs(resolveProjectId(args, context, true), getString(args, "search"), { query: toQuery(omit(args, ["project_id", "search"])) })
|
|
259
353
|
},
|
|
354
|
+
{
|
|
355
|
+
name: "gitlab_search_group_code",
|
|
356
|
+
title: "Search Group Code",
|
|
357
|
+
description: "Search code in a specific group.",
|
|
358
|
+
capabilities: readCapabilities,
|
|
359
|
+
inputSchema: {
|
|
360
|
+
group_id: projectIdSchema,
|
|
361
|
+
search: z.string().min(1),
|
|
362
|
+
filename: optionalString,
|
|
363
|
+
path: optionalString,
|
|
364
|
+
extension: optionalString,
|
|
365
|
+
...paginationShape
|
|
366
|
+
},
|
|
367
|
+
handler: async (args, context) => context.gitlab.searchGroupCodeBlobs(getString(args, "group_id"), getString(args, "search"), {
|
|
368
|
+
query: toQuery(omit(args, ["group_id", "search"]))
|
|
369
|
+
})
|
|
370
|
+
},
|
|
260
371
|
{
|
|
261
372
|
name: "gitlab_get_repository_tree",
|
|
262
373
|
title: "Get Repository Tree",
|
|
@@ -284,7 +395,8 @@ function getGitLabToolDefinitions() {
|
|
|
284
395
|
inputSchema: {
|
|
285
396
|
project_id: optionalProjectIdSchema,
|
|
286
397
|
file_path: z.string().min(1),
|
|
287
|
-
ref: optionalRefLikeSchema
|
|
398
|
+
ref: optionalRefLikeSchema,
|
|
399
|
+
decode_base64: optionalBoolean
|
|
288
400
|
},
|
|
289
401
|
handler: async (args, context) => {
|
|
290
402
|
const projectId = resolveProjectId(args, context, true);
|
|
@@ -293,7 +405,37 @@ function getGitLabToolDefinitions() {
|
|
|
293
405
|
const project = (await context.gitlab.getProject(projectId));
|
|
294
406
|
ref = typeof project.default_branch === "string" ? project.default_branch : "main";
|
|
295
407
|
}
|
|
296
|
-
|
|
408
|
+
const result = await context.gitlab.getFileContents(projectId, getString(args, "file_path"), ref);
|
|
409
|
+
return maybeDecodeRepositoryFileContents(result, getOptionalBoolean(args, "decode_base64"));
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
name: "gitlab_get_file_blame",
|
|
414
|
+
title: "Get File Blame",
|
|
415
|
+
description: "Get git blame for a repository file at a given ref. Optional line range must provide both range_start and range_end.",
|
|
416
|
+
capabilities: readCapabilities,
|
|
417
|
+
inputSchema: {
|
|
418
|
+
project_id: optionalProjectIdSchema,
|
|
419
|
+
file_path: z.string().min(1),
|
|
420
|
+
ref: refLikeSchema,
|
|
421
|
+
range_start: z.number().int().positive().optional(),
|
|
422
|
+
range_end: z.number().int().positive().optional()
|
|
423
|
+
},
|
|
424
|
+
handler: async (args, context) => {
|
|
425
|
+
const rangeStart = getOptionalNumber(args, "range_start");
|
|
426
|
+
const rangeEnd = getOptionalNumber(args, "range_end");
|
|
427
|
+
if ((rangeStart === undefined) !== (rangeEnd === undefined)) {
|
|
428
|
+
throw new Error("range_start and range_end must be provided together");
|
|
429
|
+
}
|
|
430
|
+
if (rangeStart !== undefined && rangeEnd !== undefined && rangeStart > rangeEnd) {
|
|
431
|
+
throw new Error("range_start must be less than or equal to range_end");
|
|
432
|
+
}
|
|
433
|
+
return context.gitlab.getFileBlame(resolveProjectId(args, context, true), getString(args, "file_path"), getString(args, "ref"), {
|
|
434
|
+
query: toQuery({
|
|
435
|
+
"range[start]": rangeStart,
|
|
436
|
+
"range[end]": rangeEnd
|
|
437
|
+
})
|
|
438
|
+
});
|
|
297
439
|
}
|
|
298
440
|
},
|
|
299
441
|
{
|
|
@@ -417,6 +559,44 @@ function getGitLabToolDefinitions() {
|
|
|
417
559
|
});
|
|
418
560
|
}
|
|
419
561
|
},
|
|
562
|
+
{
|
|
563
|
+
name: "gitlab_list_branches",
|
|
564
|
+
title: "List Branches",
|
|
565
|
+
description: "List repository branches.",
|
|
566
|
+
capabilities: readCapabilities,
|
|
567
|
+
inputSchema: {
|
|
568
|
+
project_id: optionalProjectIdSchema,
|
|
569
|
+
search: optionalString,
|
|
570
|
+
regex: optionalString,
|
|
571
|
+
sort: z.enum(["name_asc", "updated_asc", "updated_desc"]).optional(),
|
|
572
|
+
...paginationShape
|
|
573
|
+
},
|
|
574
|
+
handler: async (args, context) => context.gitlab.listBranches(resolveProjectId(args, context, true), {
|
|
575
|
+
query: toQuery(omit(args, ["project_id"]))
|
|
576
|
+
})
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
name: "gitlab_get_branch",
|
|
580
|
+
title: "Get Branch",
|
|
581
|
+
description: "Get details for one repository branch.",
|
|
582
|
+
capabilities: readCapabilities,
|
|
583
|
+
inputSchema: {
|
|
584
|
+
project_id: optionalProjectIdSchema,
|
|
585
|
+
branch: refLikeSchema
|
|
586
|
+
},
|
|
587
|
+
handler: async (args, context) => context.gitlab.getBranch(resolveProjectId(args, context, true), getString(args, "branch"))
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
name: "gitlab_delete_branch",
|
|
591
|
+
title: "Delete Branch",
|
|
592
|
+
description: "Delete a repository branch permanently. Requires branch. Recommended pre-check: gitlab_get_branch.",
|
|
593
|
+
capabilities: deleteCapabilities,
|
|
594
|
+
inputSchema: {
|
|
595
|
+
project_id: optionalProjectIdSchema,
|
|
596
|
+
branch: refLikeSchema
|
|
597
|
+
},
|
|
598
|
+
handler: async (args, context) => context.gitlab.deleteBranch(resolveProjectId(args, context, true), getString(args, "branch"))
|
|
599
|
+
},
|
|
420
600
|
{
|
|
421
601
|
name: "gitlab_get_branch_diffs",
|
|
422
602
|
title: "Get Branch Diffs",
|
|
@@ -502,6 +682,49 @@ function getGitLabToolDefinitions() {
|
|
|
502
682
|
});
|
|
503
683
|
}
|
|
504
684
|
},
|
|
685
|
+
{
|
|
686
|
+
name: "gitlab_list_commit_statuses",
|
|
687
|
+
title: "List Commit Statuses",
|
|
688
|
+
description: "List statuses for a commit.",
|
|
689
|
+
capabilities: readCapabilities,
|
|
690
|
+
inputSchema: {
|
|
691
|
+
project_id: optionalProjectIdSchema,
|
|
692
|
+
sha: z.string().min(1),
|
|
693
|
+
ref: optionalRefLikeSchema,
|
|
694
|
+
stage: optionalString,
|
|
695
|
+
name: optionalString,
|
|
696
|
+
pipeline_id: optionalNumber,
|
|
697
|
+
order_by: z.enum(["id", "pipeline_id"]).optional(),
|
|
698
|
+
sort: z.enum(["asc", "desc"]).optional(),
|
|
699
|
+
all: optionalBoolean,
|
|
700
|
+
...paginationShape
|
|
701
|
+
},
|
|
702
|
+
handler: async (args, context) => context.gitlab.listCommitStatuses(resolveProjectId(args, context, true), getString(args, "sha"), { query: toQuery(omit(args, ["project_id", "sha"])) })
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
name: "gitlab_create_commit_status",
|
|
706
|
+
title: "Create Commit Status",
|
|
707
|
+
description: "Create or update the status of a commit.",
|
|
708
|
+
capabilities: writeCapabilities,
|
|
709
|
+
inputSchema: {
|
|
710
|
+
project_id: optionalProjectIdSchema,
|
|
711
|
+
sha: z.string().min(1),
|
|
712
|
+
state: z.enum(["pending", "running", "success", "failed", "canceled", "skipped"]),
|
|
713
|
+
ref: optionalRefLikeSchema,
|
|
714
|
+
name: optionalString,
|
|
715
|
+
context: optionalString,
|
|
716
|
+
target_url: optionalString,
|
|
717
|
+
description: optionalString,
|
|
718
|
+
coverage: optionalNumber,
|
|
719
|
+
pipeline_id: optionalNumber
|
|
720
|
+
},
|
|
721
|
+
handler: async (args, context) => {
|
|
722
|
+
if (getOptionalString(args, "name") && getOptionalString(args, "context")) {
|
|
723
|
+
throw new Error("Use either name or context when creating a commit status, not both");
|
|
724
|
+
}
|
|
725
|
+
return context.gitlab.createCommitStatus(resolveProjectId(args, context, true), getString(args, "sha"), toQuery(omit(args, ["project_id", "sha"])));
|
|
726
|
+
}
|
|
727
|
+
},
|
|
505
728
|
{
|
|
506
729
|
name: "gitlab_list_merge_requests",
|
|
507
730
|
title: "List Merge Requests",
|
|
@@ -564,7 +787,8 @@ function getGitLabToolDefinitions() {
|
|
|
564
787
|
const projectId = resolveProjectId(args, context, true);
|
|
565
788
|
const mergeRequestIid = getOptionalString(args, "merge_request_iid");
|
|
566
789
|
if (mergeRequestIid) {
|
|
567
|
-
|
|
790
|
+
const mergeRequest = await context.gitlab.getMergeRequest(projectId, mergeRequestIid);
|
|
791
|
+
return withMergeRequestSummaries(projectId, mergeRequest, context);
|
|
568
792
|
}
|
|
569
793
|
const sourceBranch = getOptionalString(args, "source_branch");
|
|
570
794
|
if (!sourceBranch) {
|
|
@@ -580,9 +804,22 @@ function getGitLabToolDefinitions() {
|
|
|
580
804
|
const match = pickMergeRequestForSourceBranch(candidates, sourceBranch, {
|
|
581
805
|
requireOpened: false
|
|
582
806
|
});
|
|
583
|
-
|
|
807
|
+
const mergeRequest = await getDetailedMergeRequestFromMatch(projectId, match, context);
|
|
808
|
+
return withMergeRequestSummaries(projectId, mergeRequest, context);
|
|
584
809
|
}
|
|
585
810
|
},
|
|
811
|
+
{
|
|
812
|
+
name: "gitlab_list_merge_request_pipelines",
|
|
813
|
+
title: "List Merge Request Pipelines",
|
|
814
|
+
description: "List pipelines associated with a merge request.",
|
|
815
|
+
capabilities: readCapabilities,
|
|
816
|
+
inputSchema: {
|
|
817
|
+
project_id: optionalProjectIdSchema,
|
|
818
|
+
merge_request_iid: z.string().min(1),
|
|
819
|
+
...paginationShape
|
|
820
|
+
},
|
|
821
|
+
handler: async (args, context) => context.gitlab.listMergeRequestPipelines(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), { query: toQuery(omit(args, ["project_id", "merge_request_iid"])) })
|
|
822
|
+
},
|
|
586
823
|
{
|
|
587
824
|
name: "gitlab_create_merge_request",
|
|
588
825
|
title: "Create Merge Request",
|
|
@@ -739,6 +976,33 @@ function getGitLabToolDefinitions() {
|
|
|
739
976
|
},
|
|
740
977
|
handler: async (args, context) => context.gitlab.getMergeRequestDiffs(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), { query: toQuery(omit(args, ["project_id", "merge_request_iid"])) })
|
|
741
978
|
},
|
|
979
|
+
{
|
|
980
|
+
name: "gitlab_list_merge_request_changed_files",
|
|
981
|
+
title: "List Merge Request Changed Files",
|
|
982
|
+
description: "Step 1 for large MR review: return changed file metadata without diff content.",
|
|
983
|
+
capabilities: readCapabilities,
|
|
984
|
+
inputSchema: {
|
|
985
|
+
project_id: optionalProjectIdSchema,
|
|
986
|
+
merge_request_iid: optionalString,
|
|
987
|
+
source_branch: optionalRefLikeSchema,
|
|
988
|
+
excluded_file_patterns: optionalStringArray
|
|
989
|
+
},
|
|
990
|
+
handler: async (args, context) => {
|
|
991
|
+
const projectId = resolveProjectId(args, context, true);
|
|
992
|
+
const mergeRequestIid = await resolveMergeRequestIid(args, context, projectId, {
|
|
993
|
+
requireOpened: false
|
|
994
|
+
});
|
|
995
|
+
const response = await context.gitlab.getMergeRequestDiffs(projectId, mergeRequestIid);
|
|
996
|
+
const files = extractMergeRequestChanges(response).map((item) => ({
|
|
997
|
+
new_path: item.new_path,
|
|
998
|
+
old_path: item.old_path,
|
|
999
|
+
new_file: item.new_file,
|
|
1000
|
+
deleted_file: item.deleted_file,
|
|
1001
|
+
renamed_file: item.renamed_file
|
|
1002
|
+
}));
|
|
1003
|
+
return filterChangedFiles(files, getOptionalStringArray(args, "excluded_file_patterns"));
|
|
1004
|
+
}
|
|
1005
|
+
},
|
|
742
1006
|
{
|
|
743
1007
|
name: "gitlab_list_merge_request_diffs",
|
|
744
1008
|
title: "List Merge Request Diffs",
|
|
@@ -753,6 +1017,67 @@ function getGitLabToolDefinitions() {
|
|
|
753
1017
|
},
|
|
754
1018
|
handler: async (args, context) => context.gitlab.listMergeRequestDiffs(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), { query: toQuery(omit(args, ["project_id", "merge_request_iid"])) })
|
|
755
1019
|
},
|
|
1020
|
+
{
|
|
1021
|
+
name: "gitlab_get_merge_request_file_diff",
|
|
1022
|
+
title: "Get Merge Request File Diff",
|
|
1023
|
+
description: "Step 2 for large MR review: fetch diffs for specific files from a merge request.",
|
|
1024
|
+
capabilities: readCapabilities,
|
|
1025
|
+
inputSchema: {
|
|
1026
|
+
project_id: optionalProjectIdSchema,
|
|
1027
|
+
merge_request_iid: optionalString,
|
|
1028
|
+
source_branch: optionalRefLikeSchema,
|
|
1029
|
+
file_paths: z.array(z.string().min(1)).min(1),
|
|
1030
|
+
unidiff: optionalBoolean
|
|
1031
|
+
},
|
|
1032
|
+
handler: async (args, context) => {
|
|
1033
|
+
const projectId = resolveProjectId(args, context, true);
|
|
1034
|
+
const mergeRequestIid = await resolveMergeRequestIid(args, context, projectId, {
|
|
1035
|
+
requireOpened: false
|
|
1036
|
+
});
|
|
1037
|
+
const requested = getRequiredStringArray(args, "file_paths");
|
|
1038
|
+
const remaining = new Set(requested);
|
|
1039
|
+
const results = [];
|
|
1040
|
+
let page = 1;
|
|
1041
|
+
const perPage = 20;
|
|
1042
|
+
while (remaining.size > 0) {
|
|
1043
|
+
const pageItems = extractMergeRequestDiffRecords(await context.gitlab.listMergeRequestDiffs(projectId, mergeRequestIid, {
|
|
1044
|
+
query: toQuery({
|
|
1045
|
+
page,
|
|
1046
|
+
per_page: perPage,
|
|
1047
|
+
unidiff: getOptionalBoolean(args, "unidiff")
|
|
1048
|
+
})
|
|
1049
|
+
}));
|
|
1050
|
+
if (pageItems.length === 0) {
|
|
1051
|
+
break;
|
|
1052
|
+
}
|
|
1053
|
+
for (const item of pageItems) {
|
|
1054
|
+
const newPath = typeof item.new_path === "string" ? item.new_path : undefined;
|
|
1055
|
+
const oldPath = typeof item.old_path === "string" ? item.old_path : undefined;
|
|
1056
|
+
if ((newPath && remaining.has(newPath)) || (oldPath && remaining.has(oldPath))) {
|
|
1057
|
+
results.push(item);
|
|
1058
|
+
if (newPath) {
|
|
1059
|
+
remaining.delete(newPath);
|
|
1060
|
+
}
|
|
1061
|
+
if (oldPath) {
|
|
1062
|
+
remaining.delete(oldPath);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
if (pageItems.length < perPage) {
|
|
1067
|
+
break;
|
|
1068
|
+
}
|
|
1069
|
+
page += 1;
|
|
1070
|
+
}
|
|
1071
|
+
for (const missing of remaining) {
|
|
1072
|
+
results.push({
|
|
1073
|
+
file_path: missing,
|
|
1074
|
+
error: `File not found in merge request diffs: ${missing}`,
|
|
1075
|
+
hint: "Use gitlab_list_merge_request_changed_files to verify the correct file paths."
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
return results;
|
|
1079
|
+
}
|
|
1080
|
+
},
|
|
756
1081
|
{
|
|
757
1082
|
name: "gitlab_get_merge_request_code_context",
|
|
758
1083
|
title: "Get Merge Request Code Context",
|
|
@@ -1150,6 +1475,92 @@ function getGitLabToolDefinitions() {
|
|
|
1150
1475
|
},
|
|
1151
1476
|
handler: async (args, context) => context.gitlab.deleteMergeRequestNote(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "note_id"))
|
|
1152
1477
|
},
|
|
1478
|
+
{
|
|
1479
|
+
name: "gitlab_list_merge_request_emoji_reactions",
|
|
1480
|
+
title: "List Merge Request Emoji Reactions",
|
|
1481
|
+
description: "List emoji reactions on a merge request.",
|
|
1482
|
+
capabilities: readCapabilities,
|
|
1483
|
+
inputSchema: {
|
|
1484
|
+
project_id: optionalProjectIdSchema,
|
|
1485
|
+
merge_request_iid: z.string().min(1),
|
|
1486
|
+
...paginationShape
|
|
1487
|
+
},
|
|
1488
|
+
handler: async (args, context) => context.gitlab.listMergeRequestEmojiReactions(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), { query: toQuery(omit(args, ["project_id", "merge_request_iid"])) })
|
|
1489
|
+
},
|
|
1490
|
+
{
|
|
1491
|
+
name: "gitlab_list_merge_request_note_emoji_reactions",
|
|
1492
|
+
title: "List MR Note Emoji Reactions",
|
|
1493
|
+
description: "List emoji reactions on a merge request note. Pass discussion_id for discussion replies.",
|
|
1494
|
+
capabilities: readCapabilities,
|
|
1495
|
+
inputSchema: {
|
|
1496
|
+
project_id: optionalProjectIdSchema,
|
|
1497
|
+
merge_request_iid: z.string().min(1),
|
|
1498
|
+
note_id: z.string().min(1),
|
|
1499
|
+
discussion_id: optionalString,
|
|
1500
|
+
...paginationShape
|
|
1501
|
+
},
|
|
1502
|
+
handler: async (args, context) => context.gitlab.listMergeRequestNoteEmojiReactions(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "note_id"), { discussion_id: getOptionalString(args, "discussion_id") }, {
|
|
1503
|
+
query: toQuery(omit(args, ["project_id", "merge_request_iid", "note_id", "discussion_id"]))
|
|
1504
|
+
})
|
|
1505
|
+
},
|
|
1506
|
+
{
|
|
1507
|
+
name: "gitlab_create_merge_request_emoji_reaction",
|
|
1508
|
+
title: "Create Merge Request Emoji Reaction",
|
|
1509
|
+
description: "Add an emoji reaction to a merge request, for example thumbsup, rocket, or eyes.",
|
|
1510
|
+
capabilities: writeCapabilities,
|
|
1511
|
+
inputSchema: {
|
|
1512
|
+
project_id: optionalProjectIdSchema,
|
|
1513
|
+
merge_request_iid: z.string().min(1),
|
|
1514
|
+
name: emojiNameSchema
|
|
1515
|
+
},
|
|
1516
|
+
handler: async (args, context) => context.gitlab.createMergeRequestEmojiReaction(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "name"))
|
|
1517
|
+
},
|
|
1518
|
+
{
|
|
1519
|
+
name: "gitlab_delete_merge_request_emoji_reaction",
|
|
1520
|
+
title: "Delete Merge Request Emoji Reaction",
|
|
1521
|
+
description: "Delete an emoji reaction from a merge request permanently. Irreversible for that reaction. Requires merge_request_iid and award_id. Recommended pre-check: gitlab_list_merge_request_emoji_reactions.",
|
|
1522
|
+
capabilities: deleteCapabilities,
|
|
1523
|
+
inputSchema: {
|
|
1524
|
+
project_id: optionalProjectIdSchema,
|
|
1525
|
+
merge_request_iid: z.string().min(1),
|
|
1526
|
+
award_id: awardEmojiIdSchema
|
|
1527
|
+
},
|
|
1528
|
+
handler: async (args, context) => context.gitlab.deleteMergeRequestEmojiReaction(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "award_id"))
|
|
1529
|
+
},
|
|
1530
|
+
{
|
|
1531
|
+
name: "gitlab_create_merge_request_note_emoji_reaction",
|
|
1532
|
+
title: "Create MR Note Emoji Reaction",
|
|
1533
|
+
description: "Add an emoji reaction to a merge request note. Pass discussion_id for discussion replies.",
|
|
1534
|
+
capabilities: writeCapabilities,
|
|
1535
|
+
inputSchema: {
|
|
1536
|
+
project_id: optionalProjectIdSchema,
|
|
1537
|
+
merge_request_iid: z.string().min(1),
|
|
1538
|
+
note_id: z.string().min(1),
|
|
1539
|
+
discussion_id: optionalString,
|
|
1540
|
+
name: emojiNameSchema
|
|
1541
|
+
},
|
|
1542
|
+
handler: async (args, context) => context.gitlab.createMergeRequestNoteEmojiReaction(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "note_id"), {
|
|
1543
|
+
name: getString(args, "name"),
|
|
1544
|
+
discussion_id: getOptionalString(args, "discussion_id")
|
|
1545
|
+
})
|
|
1546
|
+
},
|
|
1547
|
+
{
|
|
1548
|
+
name: "gitlab_delete_merge_request_note_emoji_reaction",
|
|
1549
|
+
title: "Delete MR Note Emoji Reaction",
|
|
1550
|
+
description: "Delete an emoji reaction from a merge request note permanently. Irreversible for that reaction. Requires merge_request_iid, note_id, and award_id. Recommended pre-check: gitlab_list_merge_request_note_emoji_reactions.",
|
|
1551
|
+
capabilities: deleteCapabilities,
|
|
1552
|
+
inputSchema: {
|
|
1553
|
+
project_id: optionalProjectIdSchema,
|
|
1554
|
+
merge_request_iid: z.string().min(1),
|
|
1555
|
+
note_id: z.string().min(1),
|
|
1556
|
+
discussion_id: optionalString,
|
|
1557
|
+
award_id: awardEmojiIdSchema
|
|
1558
|
+
},
|
|
1559
|
+
handler: async (args, context) => context.gitlab.deleteMergeRequestNoteEmojiReaction(resolveProjectId(args, context, true), getString(args, "merge_request_iid"), getString(args, "note_id"), {
|
|
1560
|
+
award_id: getString(args, "award_id"),
|
|
1561
|
+
discussion_id: getOptionalString(args, "discussion_id")
|
|
1562
|
+
})
|
|
1563
|
+
},
|
|
1153
1564
|
{
|
|
1154
1565
|
name: "gitlab_list_issues",
|
|
1155
1566
|
title: "List Issues",
|
|
@@ -1211,6 +1622,71 @@ function getGitLabToolDefinitions() {
|
|
|
1211
1622
|
});
|
|
1212
1623
|
}
|
|
1213
1624
|
},
|
|
1625
|
+
{
|
|
1626
|
+
name: "gitlab_list_todos",
|
|
1627
|
+
title: "List Todos",
|
|
1628
|
+
description: "List to-do items for the current authenticated user.",
|
|
1629
|
+
capabilities: readCapabilities,
|
|
1630
|
+
inputSchema: {
|
|
1631
|
+
action: z
|
|
1632
|
+
.enum([
|
|
1633
|
+
"assigned",
|
|
1634
|
+
"mentioned",
|
|
1635
|
+
"build_failed",
|
|
1636
|
+
"marked",
|
|
1637
|
+
"approval_required",
|
|
1638
|
+
"unmergeable",
|
|
1639
|
+
"directly_addressed",
|
|
1640
|
+
"merge_train_removed",
|
|
1641
|
+
"member_access_requested"
|
|
1642
|
+
])
|
|
1643
|
+
.optional(),
|
|
1644
|
+
author_id: optionalNumber,
|
|
1645
|
+
project_id: optionalNumber,
|
|
1646
|
+
group_id: optionalNumber,
|
|
1647
|
+
state: z.enum(["pending", "done"]).optional(),
|
|
1648
|
+
type: z
|
|
1649
|
+
.enum([
|
|
1650
|
+
"Issue",
|
|
1651
|
+
"MergeRequest",
|
|
1652
|
+
"Commit",
|
|
1653
|
+
"Epic",
|
|
1654
|
+
"DesignManagement::Design",
|
|
1655
|
+
"AlertManagement::Alert",
|
|
1656
|
+
"Project",
|
|
1657
|
+
"Namespace",
|
|
1658
|
+
"Vulnerability",
|
|
1659
|
+
"WikiPage::Meta"
|
|
1660
|
+
])
|
|
1661
|
+
.optional(),
|
|
1662
|
+
...paginationShape
|
|
1663
|
+
},
|
|
1664
|
+
handler: async (args, context) => context.gitlab.listTodos({ query: toQuery(args) })
|
|
1665
|
+
},
|
|
1666
|
+
{
|
|
1667
|
+
name: "gitlab_mark_todo_done",
|
|
1668
|
+
title: "Mark Todo Done",
|
|
1669
|
+
description: "Mark one to-do item as done.",
|
|
1670
|
+
capabilities: writeCapabilities,
|
|
1671
|
+
inputSchema: {
|
|
1672
|
+
todo_id: z.string().min(1)
|
|
1673
|
+
},
|
|
1674
|
+
handler: async (args, context) => context.gitlab.markTodoDone(getString(args, "todo_id"))
|
|
1675
|
+
},
|
|
1676
|
+
{
|
|
1677
|
+
name: "gitlab_mark_all_todos_done",
|
|
1678
|
+
title: "Mark All Todos Done",
|
|
1679
|
+
description: "Mark all pending to-do items as done for the current authenticated user.",
|
|
1680
|
+
capabilities: writeCapabilities,
|
|
1681
|
+
inputSchema: {},
|
|
1682
|
+
handler: async (_args, context) => {
|
|
1683
|
+
await context.gitlab.markAllTodosDone();
|
|
1684
|
+
return {
|
|
1685
|
+
status: "success",
|
|
1686
|
+
message: "All pending to-do items marked as done"
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
},
|
|
1214
1690
|
{
|
|
1215
1691
|
name: "gitlab_get_issue",
|
|
1216
1692
|
title: "Get Issue",
|
|
@@ -1266,7 +1742,7 @@ function getGitLabToolDefinitions() {
|
|
|
1266
1742
|
confidential: optionalBoolean,
|
|
1267
1743
|
assignee_ids: optionalNumberArray,
|
|
1268
1744
|
discussion_locked: optionalBoolean,
|
|
1269
|
-
weight:
|
|
1745
|
+
weight: optionalCoercedNumber,
|
|
1270
1746
|
issue_type: z.enum(["issue", "incident", "test_case", "task"]).optional()
|
|
1271
1747
|
},
|
|
1272
1748
|
handler: async (args, context) => {
|
|
@@ -1280,6 +1756,79 @@ function getGitLabToolDefinitions() {
|
|
|
1280
1756
|
return context.gitlab.updateIssue(resolveProjectId(args, context, true), getString(args, "issue_iid"), payload);
|
|
1281
1757
|
}
|
|
1282
1758
|
},
|
|
1759
|
+
{
|
|
1760
|
+
name: "gitlab_update_issue_description_patch",
|
|
1761
|
+
title: "Update Issue Description Patch",
|
|
1762
|
+
description: "Apply a search/replace or unified diff patch to an issue description without sending the full replacement text.",
|
|
1763
|
+
capabilities: writeCapabilities,
|
|
1764
|
+
inputSchema: {
|
|
1765
|
+
project_id: optionalProjectIdSchema,
|
|
1766
|
+
issue_iid: z.string().min(1),
|
|
1767
|
+
patch_type: z.enum(["search_replace", "unified_diff"]),
|
|
1768
|
+
patch: z.string().min(1).max(50_000),
|
|
1769
|
+
dry_run: optionalBoolean,
|
|
1770
|
+
create_note: optionalBoolean,
|
|
1771
|
+
allow_multiple: optionalBoolean
|
|
1772
|
+
},
|
|
1773
|
+
handler: async (args, context) => {
|
|
1774
|
+
const projectId = resolveProjectId(args, context, true);
|
|
1775
|
+
const issueIid = getString(args, "issue_iid");
|
|
1776
|
+
const issue = (await context.gitlab.getIssue(projectId, issueIid));
|
|
1777
|
+
const currentDescription = typeof issue.description === "string" ? issue.description : "";
|
|
1778
|
+
const patchType = getString(args, "patch_type");
|
|
1779
|
+
const patch = getString(args, "patch");
|
|
1780
|
+
let result;
|
|
1781
|
+
if (patchType === "search_replace") {
|
|
1782
|
+
const blocks = parseSearchReplaceBlocks(patch);
|
|
1783
|
+
if (blocks.length === 0) {
|
|
1784
|
+
throw new Error("No valid search/replace blocks found. Expected format: <<<<<<< SEARCH\\ntext\\n=======\\nnew text\\n>>>>>>> REPLACE");
|
|
1785
|
+
}
|
|
1786
|
+
result = applySearchReplace(currentDescription, blocks, getOptionalBoolean(args, "allow_multiple") ?? false);
|
|
1787
|
+
}
|
|
1788
|
+
else {
|
|
1789
|
+
result = applyUnifiedDiff(currentDescription, patch);
|
|
1790
|
+
}
|
|
1791
|
+
if (getOptionalBoolean(args, "dry_run")) {
|
|
1792
|
+
return {
|
|
1793
|
+
status: "preview",
|
|
1794
|
+
dry_run: true,
|
|
1795
|
+
changes: result.changes,
|
|
1796
|
+
summary: result.summary,
|
|
1797
|
+
preview: result.preview
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
const updatedIssue = (await context.gitlab.updateIssue(projectId, issueIid, {
|
|
1801
|
+
description: result.description
|
|
1802
|
+
}));
|
|
1803
|
+
let note;
|
|
1804
|
+
if (getOptionalBoolean(args, "create_note")) {
|
|
1805
|
+
try {
|
|
1806
|
+
await context.gitlab.createIssueNote(projectId, issueIid, {
|
|
1807
|
+
body: `Updated issue description using patch-based tool.\n\n${result.summary}`
|
|
1808
|
+
});
|
|
1809
|
+
note = { status: "created" };
|
|
1810
|
+
}
|
|
1811
|
+
catch (error) {
|
|
1812
|
+
note = {
|
|
1813
|
+
status: "failed",
|
|
1814
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
return {
|
|
1819
|
+
status: "success",
|
|
1820
|
+
changes: result.changes,
|
|
1821
|
+
summary: result.summary,
|
|
1822
|
+
note,
|
|
1823
|
+
issue: {
|
|
1824
|
+
iid: updatedIssue.iid,
|
|
1825
|
+
title: updatedIssue.title,
|
|
1826
|
+
web_url: updatedIssue.web_url,
|
|
1827
|
+
updated_at: updatedIssue.updated_at
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
},
|
|
1283
1832
|
{
|
|
1284
1833
|
name: "gitlab_delete_issue",
|
|
1285
1834
|
title: "Delete Issue",
|
|
@@ -1347,25 +1896,109 @@ function getGitLabToolDefinitions() {
|
|
|
1347
1896
|
}
|
|
1348
1897
|
},
|
|
1349
1898
|
{
|
|
1350
|
-
name: "
|
|
1351
|
-
title: "List Issue
|
|
1352
|
-
description: "List
|
|
1899
|
+
name: "gitlab_list_issue_emoji_reactions",
|
|
1900
|
+
title: "List Issue Emoji Reactions",
|
|
1901
|
+
description: "List emoji reactions on an issue.",
|
|
1353
1902
|
capabilities: readCapabilities,
|
|
1354
1903
|
inputSchema: {
|
|
1355
1904
|
project_id: optionalProjectIdSchema,
|
|
1356
|
-
issue_iid: z.string().min(1)
|
|
1905
|
+
issue_iid: z.string().min(1),
|
|
1906
|
+
...paginationShape
|
|
1357
1907
|
},
|
|
1358
|
-
handler: async (args, context) => context.gitlab.
|
|
1908
|
+
handler: async (args, context) => context.gitlab.listIssueEmojiReactions(resolveProjectId(args, context, true), getString(args, "issue_iid"), { query: toQuery(omit(args, ["project_id", "issue_iid"])) })
|
|
1359
1909
|
},
|
|
1360
1910
|
{
|
|
1361
|
-
name: "
|
|
1362
|
-
title: "
|
|
1363
|
-
description: "
|
|
1911
|
+
name: "gitlab_list_issue_note_emoji_reactions",
|
|
1912
|
+
title: "List Issue Note Emoji Reactions",
|
|
1913
|
+
description: "List emoji reactions on an issue note. Pass discussion_id for discussion replies.",
|
|
1364
1914
|
capabilities: readCapabilities,
|
|
1365
1915
|
inputSchema: {
|
|
1366
1916
|
project_id: optionalProjectIdSchema,
|
|
1367
1917
|
issue_iid: z.string().min(1),
|
|
1368
|
-
|
|
1918
|
+
note_id: z.string().min(1),
|
|
1919
|
+
discussion_id: optionalString,
|
|
1920
|
+
...paginationShape
|
|
1921
|
+
},
|
|
1922
|
+
handler: async (args, context) => context.gitlab.listIssueNoteEmojiReactions(resolveProjectId(args, context, true), getString(args, "issue_iid"), getString(args, "note_id"), { discussion_id: getOptionalString(args, "discussion_id") }, { query: toQuery(omit(args, ["project_id", "issue_iid", "note_id", "discussion_id"])) })
|
|
1923
|
+
},
|
|
1924
|
+
{
|
|
1925
|
+
name: "gitlab_create_issue_emoji_reaction",
|
|
1926
|
+
title: "Create Issue Emoji Reaction",
|
|
1927
|
+
description: "Add an emoji reaction to an issue, for example thumbsup, rocket, or eyes.",
|
|
1928
|
+
capabilities: writeCapabilities,
|
|
1929
|
+
inputSchema: {
|
|
1930
|
+
project_id: optionalProjectIdSchema,
|
|
1931
|
+
issue_iid: z.string().min(1),
|
|
1932
|
+
name: emojiNameSchema
|
|
1933
|
+
},
|
|
1934
|
+
handler: async (args, context) => context.gitlab.createIssueEmojiReaction(resolveProjectId(args, context, true), getString(args, "issue_iid"), getString(args, "name"))
|
|
1935
|
+
},
|
|
1936
|
+
{
|
|
1937
|
+
name: "gitlab_delete_issue_emoji_reaction",
|
|
1938
|
+
title: "Delete Issue Emoji Reaction",
|
|
1939
|
+
description: "Delete an emoji reaction from an issue permanently. Irreversible for that reaction. Requires issue_iid and award_id. Recommended pre-check: gitlab_list_issue_emoji_reactions.",
|
|
1940
|
+
capabilities: deleteCapabilities,
|
|
1941
|
+
inputSchema: {
|
|
1942
|
+
project_id: optionalProjectIdSchema,
|
|
1943
|
+
issue_iid: z.string().min(1),
|
|
1944
|
+
award_id: awardEmojiIdSchema
|
|
1945
|
+
},
|
|
1946
|
+
handler: async (args, context) => context.gitlab.deleteIssueEmojiReaction(resolveProjectId(args, context, true), getString(args, "issue_iid"), getString(args, "award_id"))
|
|
1947
|
+
},
|
|
1948
|
+
{
|
|
1949
|
+
name: "gitlab_create_issue_note_emoji_reaction",
|
|
1950
|
+
title: "Create Issue Note Emoji Reaction",
|
|
1951
|
+
description: "Add an emoji reaction to an issue note. Pass discussion_id for discussion replies.",
|
|
1952
|
+
capabilities: writeCapabilities,
|
|
1953
|
+
inputSchema: {
|
|
1954
|
+
project_id: optionalProjectIdSchema,
|
|
1955
|
+
issue_iid: z.string().min(1),
|
|
1956
|
+
note_id: z.string().min(1),
|
|
1957
|
+
discussion_id: optionalString,
|
|
1958
|
+
name: emojiNameSchema
|
|
1959
|
+
},
|
|
1960
|
+
handler: async (args, context) => context.gitlab.createIssueNoteEmojiReaction(resolveProjectId(args, context, true), getString(args, "issue_iid"), getString(args, "note_id"), {
|
|
1961
|
+
name: getString(args, "name"),
|
|
1962
|
+
discussion_id: getOptionalString(args, "discussion_id")
|
|
1963
|
+
})
|
|
1964
|
+
},
|
|
1965
|
+
{
|
|
1966
|
+
name: "gitlab_delete_issue_note_emoji_reaction",
|
|
1967
|
+
title: "Delete Issue Note Emoji Reaction",
|
|
1968
|
+
description: "Delete an emoji reaction from an issue note permanently. Irreversible for that reaction. Requires issue_iid, note_id, and award_id. Recommended pre-check: gitlab_list_issue_note_emoji_reactions.",
|
|
1969
|
+
capabilities: deleteCapabilities,
|
|
1970
|
+
inputSchema: {
|
|
1971
|
+
project_id: optionalProjectIdSchema,
|
|
1972
|
+
issue_iid: z.string().min(1),
|
|
1973
|
+
note_id: z.string().min(1),
|
|
1974
|
+
discussion_id: optionalString,
|
|
1975
|
+
award_id: awardEmojiIdSchema
|
|
1976
|
+
},
|
|
1977
|
+
handler: async (args, context) => context.gitlab.deleteIssueNoteEmojiReaction(resolveProjectId(args, context, true), getString(args, "issue_iid"), getString(args, "note_id"), {
|
|
1978
|
+
award_id: getString(args, "award_id"),
|
|
1979
|
+
discussion_id: getOptionalString(args, "discussion_id")
|
|
1980
|
+
})
|
|
1981
|
+
},
|
|
1982
|
+
{
|
|
1983
|
+
name: "gitlab_list_issue_links",
|
|
1984
|
+
title: "List Issue Links",
|
|
1985
|
+
description: "List related issue links for an issue.",
|
|
1986
|
+
capabilities: readCapabilities,
|
|
1987
|
+
inputSchema: {
|
|
1988
|
+
project_id: optionalProjectIdSchema,
|
|
1989
|
+
issue_iid: z.string().min(1)
|
|
1990
|
+
},
|
|
1991
|
+
handler: async (args, context) => context.gitlab.listIssueLinks(resolveProjectId(args, context, true), getString(args, "issue_iid"))
|
|
1992
|
+
},
|
|
1993
|
+
{
|
|
1994
|
+
name: "gitlab_get_issue_link",
|
|
1995
|
+
title: "Get Issue Link",
|
|
1996
|
+
description: "Get a single issue link by ID.",
|
|
1997
|
+
capabilities: readCapabilities,
|
|
1998
|
+
inputSchema: {
|
|
1999
|
+
project_id: optionalProjectIdSchema,
|
|
2000
|
+
issue_iid: z.string().min(1),
|
|
2001
|
+
issue_link_id: z.string().min(1)
|
|
1369
2002
|
},
|
|
1370
2003
|
handler: async (args, context) => context.gitlab.getIssueLink(resolveProjectId(args, context, true), getString(args, "issue_iid"), getString(args, "issue_link_id"))
|
|
1371
2004
|
},
|
|
@@ -1478,6 +2111,87 @@ function getGitLabToolDefinitions() {
|
|
|
1478
2111
|
},
|
|
1479
2112
|
handler: async (args, context) => context.gitlab.deleteWikiPage(resolveProjectId(args, context, true), getString(args, "slug"))
|
|
1480
2113
|
},
|
|
2114
|
+
{
|
|
2115
|
+
name: "gitlab_list_group_wiki_pages",
|
|
2116
|
+
title: "List Group Wiki Pages",
|
|
2117
|
+
description: "List wiki pages in a group.",
|
|
2118
|
+
capabilities: readCapabilities,
|
|
2119
|
+
requiresFeature: "wiki",
|
|
2120
|
+
inputSchema: {
|
|
2121
|
+
group_id: projectIdSchema,
|
|
2122
|
+
with_content: optionalBoolean,
|
|
2123
|
+
...paginationShape
|
|
2124
|
+
},
|
|
2125
|
+
handler: async (args, context) => context.gitlab.listGroupWikiPages(getString(args, "group_id"), {
|
|
2126
|
+
query: toQuery(omit(args, ["group_id"]))
|
|
2127
|
+
})
|
|
2128
|
+
},
|
|
2129
|
+
{
|
|
2130
|
+
name: "gitlab_get_group_wiki_page",
|
|
2131
|
+
title: "Get Group Wiki Page",
|
|
2132
|
+
description: "Get group wiki page by slug.",
|
|
2133
|
+
capabilities: readCapabilities,
|
|
2134
|
+
requiresFeature: "wiki",
|
|
2135
|
+
inputSchema: {
|
|
2136
|
+
group_id: projectIdSchema,
|
|
2137
|
+
slug: slugSchema,
|
|
2138
|
+
version: optionalString
|
|
2139
|
+
},
|
|
2140
|
+
handler: async (args, context) => context.gitlab.getGroupWikiPage(getString(args, "group_id"), getString(args, "slug"), {
|
|
2141
|
+
query: toQuery(omit(args, ["group_id", "slug"]))
|
|
2142
|
+
})
|
|
2143
|
+
},
|
|
2144
|
+
{
|
|
2145
|
+
name: "gitlab_create_group_wiki_page",
|
|
2146
|
+
title: "Create Group Wiki Page",
|
|
2147
|
+
description: "Create a group wiki page.",
|
|
2148
|
+
capabilities: writeCapabilities,
|
|
2149
|
+
requiresFeature: "wiki",
|
|
2150
|
+
inputSchema: {
|
|
2151
|
+
group_id: projectIdSchema,
|
|
2152
|
+
title: z.string().min(1),
|
|
2153
|
+
content: z.string().min(1),
|
|
2154
|
+
format: optionalString
|
|
2155
|
+
},
|
|
2156
|
+
handler: async (args, context) => context.gitlab.createGroupWikiPage(getString(args, "group_id"), {
|
|
2157
|
+
title: getString(args, "title"),
|
|
2158
|
+
content: getString(args, "content"),
|
|
2159
|
+
format: getOptionalString(args, "format")
|
|
2160
|
+
})
|
|
2161
|
+
},
|
|
2162
|
+
{
|
|
2163
|
+
name: "gitlab_update_group_wiki_page",
|
|
2164
|
+
title: "Update Group Wiki Page",
|
|
2165
|
+
description: "Update group wiki page by slug.",
|
|
2166
|
+
capabilities: writeCapabilities,
|
|
2167
|
+
requiresFeature: "wiki",
|
|
2168
|
+
inputSchema: {
|
|
2169
|
+
group_id: projectIdSchema,
|
|
2170
|
+
slug: slugSchema,
|
|
2171
|
+
title: optionalString,
|
|
2172
|
+
content: optionalString,
|
|
2173
|
+
format: optionalString
|
|
2174
|
+
},
|
|
2175
|
+
handler: async (args, context) => {
|
|
2176
|
+
const payload = toQuery(omit(args, ["group_id", "slug"]));
|
|
2177
|
+
if (Object.keys(payload).length === 0) {
|
|
2178
|
+
throw new Error("At least one of title, content, or format must be provided");
|
|
2179
|
+
}
|
|
2180
|
+
return context.gitlab.updateGroupWikiPage(getString(args, "group_id"), getString(args, "slug"), payload);
|
|
2181
|
+
}
|
|
2182
|
+
},
|
|
2183
|
+
{
|
|
2184
|
+
name: "gitlab_delete_group_wiki_page",
|
|
2185
|
+
title: "Delete Group Wiki Page",
|
|
2186
|
+
description: "Delete a group wiki page permanently. Irreversible. Requires group_id and slug. Recommended pre-check: gitlab_get_group_wiki_page or gitlab_list_group_wiki_pages.",
|
|
2187
|
+
capabilities: deleteCapabilities,
|
|
2188
|
+
requiresFeature: "wiki",
|
|
2189
|
+
inputSchema: {
|
|
2190
|
+
group_id: projectIdSchema,
|
|
2191
|
+
slug: slugSchema
|
|
2192
|
+
},
|
|
2193
|
+
handler: async (args, context) => context.gitlab.deleteGroupWikiPage(getString(args, "group_id"), getString(args, "slug"))
|
|
2194
|
+
},
|
|
1481
2195
|
{
|
|
1482
2196
|
name: "gitlab_list_pipelines",
|
|
1483
2197
|
title: "List Pipelines",
|
|
@@ -1673,6 +2387,38 @@ function getGitLabToolDefinitions() {
|
|
|
1673
2387
|
},
|
|
1674
2388
|
handler: async (args, context) => context.gitlab.getPipelineJobOutput(resolveProjectId(args, context, true), getString(args, "job_id"))
|
|
1675
2389
|
},
|
|
2390
|
+
{
|
|
2391
|
+
name: "gitlab_validate_ci_lint",
|
|
2392
|
+
title: "Validate CI Lint",
|
|
2393
|
+
description: "Validate provided GitLab CI/CD YAML content for a project.",
|
|
2394
|
+
capabilities: readCapabilities,
|
|
2395
|
+
requiresFeature: "pipeline",
|
|
2396
|
+
inputSchema: {
|
|
2397
|
+
project_id: optionalProjectIdSchema,
|
|
2398
|
+
content: z.string().min(1),
|
|
2399
|
+
dry_run: optionalBoolean,
|
|
2400
|
+
include_jobs: optionalBoolean,
|
|
2401
|
+
ref: optionalRefLikeSchema
|
|
2402
|
+
},
|
|
2403
|
+
handler: async (args, context) => withCiLintHttpDiagnostics(() => context.gitlab.validateCiLint(resolveProjectId(args, context, true), toQuery(omit(args, ["project_id"]))))
|
|
2404
|
+
},
|
|
2405
|
+
{
|
|
2406
|
+
name: "gitlab_validate_project_ci_lint",
|
|
2407
|
+
title: "Validate Project CI Lint",
|
|
2408
|
+
description: "Validate an existing project CI/CD configuration.",
|
|
2409
|
+
capabilities: readCapabilities,
|
|
2410
|
+
requiresFeature: "pipeline",
|
|
2411
|
+
inputSchema: {
|
|
2412
|
+
project_id: optionalProjectIdSchema,
|
|
2413
|
+
content_ref: optionalRefLikeSchema,
|
|
2414
|
+
dry_run: optionalBoolean,
|
|
2415
|
+
dry_run_ref: optionalRefLikeSchema,
|
|
2416
|
+
include_jobs: optionalBoolean
|
|
2417
|
+
},
|
|
2418
|
+
handler: async (args, context) => withCiLintHttpDiagnostics(() => context.gitlab.validateProjectCiLint(resolveProjectId(args, context, true), {
|
|
2419
|
+
query: toQuery(omit(args, ["project_id"]))
|
|
2420
|
+
}))
|
|
2421
|
+
},
|
|
1676
2422
|
{
|
|
1677
2423
|
name: "gitlab_list_job_artifacts",
|
|
1678
2424
|
title: "List Job Artifacts",
|
|
@@ -1697,7 +2443,17 @@ function getGitLabToolDefinitions() {
|
|
|
1697
2443
|
project_id: optionalProjectIdSchema,
|
|
1698
2444
|
job_id: z.string().min(1)
|
|
1699
2445
|
},
|
|
1700
|
-
handler: async (args, context) =>
|
|
2446
|
+
handler: async (args, context) => {
|
|
2447
|
+
const projectId = resolveProjectId(args, context, true);
|
|
2448
|
+
const jobId = getString(args, "job_id");
|
|
2449
|
+
if (shouldReturnDownloadProxy(context)) {
|
|
2450
|
+
return buildDownloadProxyResult(context, {
|
|
2451
|
+
type: "job-artifacts",
|
|
2452
|
+
params: { project_id: projectId, job_id: jobId }
|
|
2453
|
+
}, `artifacts_job_${jobId}.zip`);
|
|
2454
|
+
}
|
|
2455
|
+
return context.gitlab.downloadJobArtifacts(projectId, jobId);
|
|
2456
|
+
}
|
|
1701
2457
|
},
|
|
1702
2458
|
{
|
|
1703
2459
|
name: "gitlab_download_job_artifacts_local",
|
|
@@ -2075,7 +2831,84 @@ function getGitLabToolDefinitions() {
|
|
|
2075
2831
|
tag_name: refLikeSchema,
|
|
2076
2832
|
direct_asset_path: z.string().min(1)
|
|
2077
2833
|
},
|
|
2078
|
-
handler: async (args, context) =>
|
|
2834
|
+
handler: async (args, context) => {
|
|
2835
|
+
const projectId = resolveProjectId(args, context, true);
|
|
2836
|
+
const tagName = getString(args, "tag_name");
|
|
2837
|
+
const directAssetPath = getString(args, "direct_asset_path");
|
|
2838
|
+
if (shouldReturnDownloadProxy(context)) {
|
|
2839
|
+
return buildDownloadProxyResult(context, {
|
|
2840
|
+
type: "release-asset",
|
|
2841
|
+
params: {
|
|
2842
|
+
project_id: projectId,
|
|
2843
|
+
tag_name: tagName,
|
|
2844
|
+
direct_asset_path: directAssetPath
|
|
2845
|
+
}
|
|
2846
|
+
}, directAssetPath.split("/").pop() || directAssetPath);
|
|
2847
|
+
}
|
|
2848
|
+
return context.gitlab.downloadReleaseAsset(projectId, tagName, directAssetPath);
|
|
2849
|
+
}
|
|
2850
|
+
},
|
|
2851
|
+
{
|
|
2852
|
+
name: "gitlab_list_tags",
|
|
2853
|
+
title: "List Tags",
|
|
2854
|
+
description: "List repository tags for a project.",
|
|
2855
|
+
capabilities: readCapabilities,
|
|
2856
|
+
inputSchema: {
|
|
2857
|
+
project_id: optionalProjectIdSchema,
|
|
2858
|
+
order_by: z.enum(["name", "updated", "version"]).optional(),
|
|
2859
|
+
sort: z.enum(["asc", "desc"]).optional(),
|
|
2860
|
+
search: optionalString,
|
|
2861
|
+
...paginationShape
|
|
2862
|
+
},
|
|
2863
|
+
handler: async (args, context) => context.gitlab.listTags(resolveProjectId(args, context, true), {
|
|
2864
|
+
query: toQuery(omit(args, ["project_id"]))
|
|
2865
|
+
})
|
|
2866
|
+
},
|
|
2867
|
+
{
|
|
2868
|
+
name: "gitlab_get_tag",
|
|
2869
|
+
title: "Get Tag",
|
|
2870
|
+
description: "Get a repository tag by name.",
|
|
2871
|
+
capabilities: readCapabilities,
|
|
2872
|
+
inputSchema: {
|
|
2873
|
+
project_id: optionalProjectIdSchema,
|
|
2874
|
+
tag_name: refLikeSchema
|
|
2875
|
+
},
|
|
2876
|
+
handler: async (args, context) => context.gitlab.getTag(resolveProjectId(args, context, true), getString(args, "tag_name"))
|
|
2877
|
+
},
|
|
2878
|
+
{
|
|
2879
|
+
name: "gitlab_create_tag",
|
|
2880
|
+
title: "Create Tag",
|
|
2881
|
+
description: "Create a repository tag from a branch, commit SHA, or another tag.",
|
|
2882
|
+
capabilities: writeCapabilities,
|
|
2883
|
+
inputSchema: {
|
|
2884
|
+
project_id: optionalProjectIdSchema,
|
|
2885
|
+
tag_name: refLikeSchema,
|
|
2886
|
+
ref: refLikeSchema,
|
|
2887
|
+
message: optionalString
|
|
2888
|
+
},
|
|
2889
|
+
handler: async (args, context) => context.gitlab.createTag(resolveProjectId(args, context, true), toQuery(omit(args, ["project_id"])))
|
|
2890
|
+
},
|
|
2891
|
+
{
|
|
2892
|
+
name: "gitlab_delete_tag",
|
|
2893
|
+
title: "Delete Tag",
|
|
2894
|
+
description: "Delete a repository tag permanently. Irreversible for tag_name. Requires tag_name. Recommended pre-check: gitlab_get_tag or gitlab_list_tags.",
|
|
2895
|
+
capabilities: deleteCapabilities,
|
|
2896
|
+
inputSchema: {
|
|
2897
|
+
project_id: optionalProjectIdSchema,
|
|
2898
|
+
tag_name: refLikeSchema
|
|
2899
|
+
},
|
|
2900
|
+
handler: async (args, context) => context.gitlab.deleteTag(resolveProjectId(args, context, true), getString(args, "tag_name"))
|
|
2901
|
+
},
|
|
2902
|
+
{
|
|
2903
|
+
name: "gitlab_get_tag_signature",
|
|
2904
|
+
title: "Get Tag Signature",
|
|
2905
|
+
description: "Get the X.509 signature for a signed repository tag.",
|
|
2906
|
+
capabilities: readCapabilities,
|
|
2907
|
+
inputSchema: {
|
|
2908
|
+
project_id: optionalProjectIdSchema,
|
|
2909
|
+
tag_name: refLikeSchema
|
|
2910
|
+
},
|
|
2911
|
+
handler: async (args, context) => context.gitlab.getTagSignature(resolveProjectId(args, context, true), getString(args, "tag_name"))
|
|
2079
2912
|
},
|
|
2080
2913
|
{
|
|
2081
2914
|
name: "gitlab_list_labels",
|
|
@@ -2219,6 +3052,24 @@ function getGitLabToolDefinitions() {
|
|
|
2219
3052
|
},
|
|
2220
3053
|
handler: async (args, context) => context.gitlab.getUsers({ query: toQuery(args) })
|
|
2221
3054
|
},
|
|
3055
|
+
{
|
|
3056
|
+
name: "gitlab_get_user",
|
|
3057
|
+
title: "Get User",
|
|
3058
|
+
description: "Get one user by ID.",
|
|
3059
|
+
capabilities: readCapabilities,
|
|
3060
|
+
inputSchema: {
|
|
3061
|
+
user_id: z.string().min(1)
|
|
3062
|
+
},
|
|
3063
|
+
handler: async (args, context) => context.gitlab.getUser(getString(args, "user_id"))
|
|
3064
|
+
},
|
|
3065
|
+
{
|
|
3066
|
+
name: "gitlab_whoami",
|
|
3067
|
+
title: "Who Am I",
|
|
3068
|
+
description: "Get the current authenticated user.",
|
|
3069
|
+
capabilities: readCapabilities,
|
|
3070
|
+
inputSchema: {},
|
|
3071
|
+
handler: async (_args, context) => context.gitlab.whoami()
|
|
3072
|
+
},
|
|
2222
3073
|
{
|
|
2223
3074
|
name: "gitlab_list_events",
|
|
2224
3075
|
title: "List Events",
|
|
@@ -2253,6 +3104,69 @@ function getGitLabToolDefinitions() {
|
|
|
2253
3104
|
query: toQuery(omit(args, ["project_id"]))
|
|
2254
3105
|
})
|
|
2255
3106
|
},
|
|
3107
|
+
{
|
|
3108
|
+
name: "gitlab_list_webhooks",
|
|
3109
|
+
title: "List Webhooks",
|
|
3110
|
+
description: "List configured webhooks for a project or group.",
|
|
3111
|
+
capabilities: readCapabilities,
|
|
3112
|
+
inputSchema: {
|
|
3113
|
+
project_id: optionalProjectIdSchema,
|
|
3114
|
+
group_id: optionalProjectIdSchema,
|
|
3115
|
+
...paginationShape
|
|
3116
|
+
},
|
|
3117
|
+
handler: async (args, context) => context.gitlab.listWebhooks(resolveWebhookScope(args), {
|
|
3118
|
+
query: toQuery(omit(args, ["project_id", "group_id"]))
|
|
3119
|
+
})
|
|
3120
|
+
},
|
|
3121
|
+
{
|
|
3122
|
+
name: "gitlab_list_webhook_events",
|
|
3123
|
+
title: "List Webhook Events",
|
|
3124
|
+
description: "List recent webhook events for a project or group webhook. Use summary mode for overviews.",
|
|
3125
|
+
capabilities: readCapabilities,
|
|
3126
|
+
inputSchema: {
|
|
3127
|
+
project_id: optionalProjectIdSchema,
|
|
3128
|
+
group_id: optionalProjectIdSchema,
|
|
3129
|
+
hook_id: z.union([z.string(), z.number()]),
|
|
3130
|
+
status: optionalStringOrNumber,
|
|
3131
|
+
summary: optionalBoolean,
|
|
3132
|
+
page: optionalNumber,
|
|
3133
|
+
per_page: z.number().int().min(1).max(20).optional()
|
|
3134
|
+
},
|
|
3135
|
+
handler: async (args, context) => {
|
|
3136
|
+
const events = extractRecords(await context.gitlab.listWebhookEvents(resolveWebhookScope(args), getIdString(args, "hook_id"), {
|
|
3137
|
+
query: toQuery({
|
|
3138
|
+
status: args.status,
|
|
3139
|
+
page: args.page,
|
|
3140
|
+
per_page: args.per_page ?? 20
|
|
3141
|
+
})
|
|
3142
|
+
}));
|
|
3143
|
+
return getOptionalBoolean(args, "summary") ? summarizeWebhookEvents(events) : events;
|
|
3144
|
+
}
|
|
3145
|
+
},
|
|
3146
|
+
{
|
|
3147
|
+
name: "gitlab_get_webhook_event",
|
|
3148
|
+
title: "Get Webhook Event",
|
|
3149
|
+
description: "Find one webhook event by ID. Provide page when known, otherwise scans up to 500 recent events.",
|
|
3150
|
+
capabilities: readCapabilities,
|
|
3151
|
+
inputSchema: {
|
|
3152
|
+
project_id: optionalProjectIdSchema,
|
|
3153
|
+
group_id: optionalProjectIdSchema,
|
|
3154
|
+
hook_id: z.union([z.string(), z.number()]),
|
|
3155
|
+
event_id: z.union([z.string(), z.number()]),
|
|
3156
|
+
page: optionalNumber
|
|
3157
|
+
},
|
|
3158
|
+
handler: async (args, context) => {
|
|
3159
|
+
const event = await findWebhookEvent(context, resolveWebhookScope(args), getIdString(args, "hook_id"), getIdString(args, "event_id"), getOptionalNumber(args, "page"));
|
|
3160
|
+
if (event) {
|
|
3161
|
+
return event;
|
|
3162
|
+
}
|
|
3163
|
+
return {
|
|
3164
|
+
error: `Webhook event ${getIdString(args, "event_id")} not found ${getOptionalNumber(args, "page")
|
|
3165
|
+
? `on page ${getOptionalNumber(args, "page")}`
|
|
3166
|
+
: "in the 500 most recent events"}`
|
|
3167
|
+
};
|
|
3168
|
+
}
|
|
3169
|
+
},
|
|
2256
3170
|
{
|
|
2257
3171
|
name: "gitlab_upload_markdown",
|
|
2258
3172
|
title: "Upload Markdown",
|
|
@@ -2268,6 +3182,9 @@ function getGitLabToolDefinitions() {
|
|
|
2268
3182
|
const projectId = resolveProjectId(args, context, true);
|
|
2269
3183
|
const filePath = getOptionalString(args, "file_path");
|
|
2270
3184
|
if (filePath) {
|
|
3185
|
+
if (!context.allowLocalFileTools) {
|
|
3186
|
+
throw new Error("file_path cannot be used over HTTP. Provide content and filename.");
|
|
3187
|
+
}
|
|
2271
3188
|
return context.gitlab.uploadMarkdownFile(projectId, filePath);
|
|
2272
3189
|
}
|
|
2273
3190
|
const content = getOptionalString(args, "content");
|
|
@@ -2300,10 +3217,30 @@ function getGitLabToolDefinitions() {
|
|
|
2300
3217
|
if (!upload) {
|
|
2301
3218
|
throw new Error("In project-scoped mode, url_or_path must be a GitLab upload URL/path like '/uploads/<secret>/<filename>'");
|
|
2302
3219
|
}
|
|
3220
|
+
if (shouldReturnDownloadProxy(context)) {
|
|
3221
|
+
return buildDownloadProxyResult(context, {
|
|
3222
|
+
type: "attachment",
|
|
3223
|
+
params: {
|
|
3224
|
+
project_id: projectId,
|
|
3225
|
+
secret: upload.secret,
|
|
3226
|
+
filename: upload.filename
|
|
3227
|
+
}
|
|
3228
|
+
}, upload.filename);
|
|
3229
|
+
}
|
|
2303
3230
|
const apiRelativePath = `api/v4/projects/${encodeURIComponent(projectId)}/uploads/${encodeURIComponent(upload.secret)}/${encodeURIComponent(upload.filename)}`;
|
|
2304
3231
|
return context.gitlab.downloadAttachment(apiRelativePath);
|
|
2305
3232
|
}
|
|
2306
3233
|
if (projectId && upload) {
|
|
3234
|
+
if (shouldReturnDownloadProxy(context)) {
|
|
3235
|
+
return buildDownloadProxyResult(context, {
|
|
3236
|
+
type: "attachment",
|
|
3237
|
+
params: {
|
|
3238
|
+
project_id: projectId,
|
|
3239
|
+
secret: upload.secret,
|
|
3240
|
+
filename: upload.filename
|
|
3241
|
+
}
|
|
3242
|
+
}, upload.filename);
|
|
3243
|
+
}
|
|
2307
3244
|
const apiRelativePath = `api/v4/projects/${encodeURIComponent(projectId)}/uploads/${encodeURIComponent(upload.secret)}/${encodeURIComponent(upload.filename)}`;
|
|
2308
3245
|
return context.gitlab.downloadAttachment(apiRelativePath);
|
|
2309
3246
|
}
|
|
@@ -2316,53 +3253,390 @@ function getGitLabToolDefinitions() {
|
|
|
2316
3253
|
}
|
|
2317
3254
|
const projectId = resolveProjectId(args, context, true);
|
|
2318
3255
|
const apiRelativePath = `api/v4/projects/${encodeURIComponent(projectId)}/uploads/${encodeURIComponent(secret)}/${encodeURIComponent(filename)}`;
|
|
3256
|
+
if (shouldReturnDownloadProxy(context)) {
|
|
3257
|
+
return buildDownloadProxyResult(context, {
|
|
3258
|
+
type: "attachment",
|
|
3259
|
+
params: { project_id: projectId, secret, filename }
|
|
3260
|
+
}, filename);
|
|
3261
|
+
}
|
|
2319
3262
|
return context.gitlab.downloadAttachment(apiRelativePath);
|
|
2320
3263
|
}
|
|
2321
3264
|
},
|
|
2322
3265
|
{
|
|
2323
|
-
name: "
|
|
2324
|
-
title: "
|
|
2325
|
-
description: "
|
|
3266
|
+
name: "gitlab_get_work_item",
|
|
3267
|
+
title: "Get Work Item",
|
|
3268
|
+
description: "Get a single work item with full widget details including status, hierarchy, labels, assignees, linked items, custom fields, and development data.",
|
|
2326
3269
|
capabilities: readGraphqlCapabilities,
|
|
2327
3270
|
inputSchema: {
|
|
2328
|
-
|
|
2329
|
-
|
|
3271
|
+
project_id: optionalProjectIdSchema,
|
|
3272
|
+
iid: workItemIidSchema
|
|
2330
3273
|
},
|
|
2331
|
-
handler: async (args, context) =>
|
|
2332
|
-
const query = getString(args, "query");
|
|
2333
|
-
if (containsGraphqlMutation(query)) {
|
|
2334
|
-
throw new Error("Mutation detected. Use gitlab_execute_graphql_mutation for mutation operations.");
|
|
2335
|
-
}
|
|
2336
|
-
return context.gitlab.executeGraphql(query, getOptionalRecord(args, "variables"));
|
|
2337
|
-
}
|
|
3274
|
+
handler: async (args, context) => getWorkItem(context, resolveProjectId(args, context, true), getNumber(args, "iid"))
|
|
2338
3275
|
},
|
|
2339
3276
|
{
|
|
2340
|
-
name: "
|
|
2341
|
-
title: "
|
|
2342
|
-
description: "
|
|
2343
|
-
capabilities:
|
|
3277
|
+
name: "gitlab_list_work_items",
|
|
3278
|
+
title: "List Work Items",
|
|
3279
|
+
description: "List work items in a project with filters for type, state, search, assignees, and labels.",
|
|
3280
|
+
capabilities: readGraphqlCapabilities,
|
|
2344
3281
|
inputSchema: {
|
|
2345
|
-
|
|
2346
|
-
|
|
3282
|
+
project_id: optionalProjectIdSchema,
|
|
3283
|
+
types: nullableOptional(z.array(workItemTypeSchema)),
|
|
3284
|
+
state: z.enum(["opened", "closed"]).optional(),
|
|
3285
|
+
search: optionalString,
|
|
3286
|
+
assignee_usernames: optionalStringArray,
|
|
3287
|
+
label_names: optionalStringArray,
|
|
3288
|
+
first: z.coerce.number().int().positive().max(100).optional(),
|
|
3289
|
+
after: optionalString
|
|
2347
3290
|
},
|
|
2348
|
-
handler: async (args, context) => {
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
3291
|
+
handler: async (args, context) => listWorkItems(context, resolveProjectId(args, context, true), {
|
|
3292
|
+
types: getOptionalStringArray(args, "types"),
|
|
3293
|
+
state: getOptionalString(args, "state"),
|
|
3294
|
+
search: getOptionalString(args, "search"),
|
|
3295
|
+
assigneeUsernames: getOptionalStringArray(args, "assignee_usernames"),
|
|
3296
|
+
labelNames: getOptionalStringArray(args, "label_names"),
|
|
3297
|
+
first: getOptionalNumber(args, "first"),
|
|
3298
|
+
after: getOptionalString(args, "after")
|
|
3299
|
+
})
|
|
2355
3300
|
},
|
|
2356
3301
|
{
|
|
2357
|
-
name: "
|
|
2358
|
-
title: "
|
|
2359
|
-
description: "
|
|
2360
|
-
capabilities:
|
|
3302
|
+
name: "gitlab_create_work_item",
|
|
3303
|
+
title: "Create Work Item",
|
|
3304
|
+
description: "Create a work item of type issue, task, incident, test_case, epic, key_result, objective, requirement, or ticket.",
|
|
3305
|
+
capabilities: writeGraphqlCapabilities,
|
|
2361
3306
|
inputSchema: {
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
3307
|
+
project_id: optionalProjectIdSchema,
|
|
3308
|
+
title: z.string().min(1),
|
|
3309
|
+
type: optionalWorkItemType,
|
|
3310
|
+
description: optionalString,
|
|
3311
|
+
labels: optionalStringArray,
|
|
3312
|
+
assignee_usernames: optionalStringArray,
|
|
3313
|
+
parent_iid: workItemIidSchema.optional(),
|
|
3314
|
+
weight: optionalCoercedNumber,
|
|
3315
|
+
health_status: z.enum(["onTrack", "needsAttention", "atRisk"]).optional(),
|
|
3316
|
+
start_date: optionalString,
|
|
3317
|
+
due_date: optionalString,
|
|
3318
|
+
milestone_id: optionalString,
|
|
3319
|
+
iteration_id: optionalString,
|
|
3320
|
+
confidential: optionalCoercedBoolean
|
|
3321
|
+
},
|
|
3322
|
+
handler: async (args, context) => createWorkItem(context, resolveProjectId(args, context, true), {
|
|
3323
|
+
title: getString(args, "title"),
|
|
3324
|
+
type: getOptionalString(args, "type"),
|
|
3325
|
+
description: getOptionalString(args, "description"),
|
|
3326
|
+
labels: getOptionalStringArray(args, "labels"),
|
|
3327
|
+
assigneeUsernames: getOptionalStringArray(args, "assignee_usernames"),
|
|
3328
|
+
parentIid: getOptionalNumber(args, "parent_iid"),
|
|
3329
|
+
weight: getOptionalNumber(args, "weight"),
|
|
3330
|
+
healthStatus: getOptionalString(args, "health_status"),
|
|
3331
|
+
startDate: getOptionalString(args, "start_date"),
|
|
3332
|
+
dueDate: getOptionalString(args, "due_date"),
|
|
3333
|
+
milestoneId: getOptionalString(args, "milestone_id"),
|
|
3334
|
+
iterationId: getOptionalString(args, "iteration_id"),
|
|
3335
|
+
confidential: getOptionalBoolean(args, "confidential")
|
|
3336
|
+
})
|
|
3337
|
+
},
|
|
3338
|
+
{
|
|
3339
|
+
name: "gitlab_update_work_item",
|
|
3340
|
+
title: "Update Work Item",
|
|
3341
|
+
description: "Update a work item title, description, labels, assignees, state, status, hierarchy, linked items, custom fields, dates, milestone, iteration, and incident metadata.",
|
|
3342
|
+
capabilities: writeGraphqlCapabilities,
|
|
3343
|
+
inputSchema: {
|
|
3344
|
+
project_id: optionalProjectIdSchema,
|
|
3345
|
+
iid: workItemIidSchema,
|
|
3346
|
+
title: optionalString,
|
|
3347
|
+
description: optionalString,
|
|
3348
|
+
add_labels: optionalStringArray,
|
|
3349
|
+
remove_labels: optionalStringArray,
|
|
3350
|
+
assignee_usernames: optionalStringArray,
|
|
3351
|
+
state_event: z.enum(["close", "reopen"]).optional(),
|
|
3352
|
+
weight: optionalNumber,
|
|
3353
|
+
status: optionalString,
|
|
3354
|
+
parent_iid: workItemIidSchema.optional(),
|
|
3355
|
+
parent_project_id: optionalProjectIdSchema,
|
|
3356
|
+
remove_parent: optionalCoercedBoolean,
|
|
3357
|
+
children_to_add: nullableOptional(z.array(workItemReferenceSchema)),
|
|
3358
|
+
children_to_remove: nullableOptional(z.array(workItemReferenceSchema)),
|
|
3359
|
+
health_status: z.enum(["onTrack", "needsAttention", "atRisk"]).optional(),
|
|
3360
|
+
start_date: optionalString,
|
|
3361
|
+
due_date: optionalString,
|
|
3362
|
+
milestone_id: optionalString,
|
|
3363
|
+
iteration_id: optionalString,
|
|
3364
|
+
confidential: optionalCoercedBoolean,
|
|
3365
|
+
linked_items_to_add: nullableOptional(z.array(linkedWorkItemReferenceSchema)),
|
|
3366
|
+
linked_items_to_remove: nullableOptional(z.array(workItemReferenceSchema)),
|
|
3367
|
+
custom_fields: nullableOptional(z.array(customFieldValueSchema)),
|
|
3368
|
+
severity: z.enum(["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL"]).optional(),
|
|
3369
|
+
escalation_status: z.enum(["TRIGGERED", "ACKNOWLEDGED", "RESOLVED", "IGNORED"]).optional()
|
|
3370
|
+
},
|
|
3371
|
+
handler: async (args, context) => updateWorkItem(context, resolveProjectId(args, context, true), getNumber(args, "iid"), {
|
|
3372
|
+
title: getOptionalString(args, "title"),
|
|
3373
|
+
description: getOptionalString(args, "description"),
|
|
3374
|
+
addLabels: getOptionalStringArray(args, "add_labels"),
|
|
3375
|
+
removeLabels: getOptionalStringArray(args, "remove_labels"),
|
|
3376
|
+
assigneeUsernames: getOptionalStringArray(args, "assignee_usernames"),
|
|
3377
|
+
stateEvent: getOptionalString(args, "state_event"),
|
|
3378
|
+
weight: getOptionalNumber(args, "weight"),
|
|
3379
|
+
status: getOptionalString(args, "status"),
|
|
3380
|
+
parentIid: getOptionalNumber(args, "parent_iid"),
|
|
3381
|
+
parentProjectId: getOptionalString(args, "parent_project_id"),
|
|
3382
|
+
removeParent: getOptionalBoolean(args, "remove_parent"),
|
|
3383
|
+
childrenToAdd: getWorkItemReferences(args, "children_to_add", context),
|
|
3384
|
+
childrenToRemove: getWorkItemReferences(args, "children_to_remove", context),
|
|
3385
|
+
healthStatus: getOptionalString(args, "health_status"),
|
|
3386
|
+
startDate: getOptionalString(args, "start_date"),
|
|
3387
|
+
dueDate: getOptionalString(args, "due_date"),
|
|
3388
|
+
milestoneId: getOptionalString(args, "milestone_id"),
|
|
3389
|
+
iterationId: getOptionalString(args, "iteration_id"),
|
|
3390
|
+
confidential: getOptionalBoolean(args, "confidential"),
|
|
3391
|
+
linkedItemsToAdd: getLinkedWorkItemReferences(args, "linked_items_to_add", context),
|
|
3392
|
+
linkedItemsToRemove: getWorkItemReferences(args, "linked_items_to_remove", context),
|
|
3393
|
+
customFields: getOptionalArray(args, "custom_fields"),
|
|
3394
|
+
severity: getOptionalString(args, "severity"),
|
|
3395
|
+
escalationStatus: getOptionalString(args, "escalation_status")
|
|
3396
|
+
})
|
|
3397
|
+
},
|
|
3398
|
+
{
|
|
3399
|
+
name: "gitlab_convert_work_item_type",
|
|
3400
|
+
title: "Convert Work Item Type",
|
|
3401
|
+
description: "Convert a work item to a different type.",
|
|
3402
|
+
capabilities: writeGraphqlCapabilities,
|
|
3403
|
+
inputSchema: {
|
|
3404
|
+
project_id: optionalProjectIdSchema,
|
|
3405
|
+
iid: workItemIidSchema,
|
|
3406
|
+
new_type: workItemTypeSchema
|
|
3407
|
+
},
|
|
3408
|
+
handler: async (args, context) => convertWorkItemType(context, resolveProjectId(args, context, true), getNumber(args, "iid"), getString(args, "new_type"))
|
|
3409
|
+
},
|
|
3410
|
+
{
|
|
3411
|
+
name: "gitlab_list_work_item_statuses",
|
|
3412
|
+
title: "List Work Item Statuses",
|
|
3413
|
+
description: "List available statuses and allowed hierarchy/conversion types for a work item type.",
|
|
3414
|
+
capabilities: readGraphqlCapabilities,
|
|
3415
|
+
inputSchema: {
|
|
3416
|
+
project_id: optionalProjectIdSchema,
|
|
3417
|
+
work_item_type: optionalWorkItemType
|
|
3418
|
+
},
|
|
3419
|
+
handler: async (args, context) => listWorkItemStatuses(context, resolveProjectId(args, context, true), getOptionalString(args, "work_item_type") ?? "issue")
|
|
3420
|
+
},
|
|
3421
|
+
{
|
|
3422
|
+
name: "gitlab_list_custom_field_definitions",
|
|
3423
|
+
title: "List Custom Field Definitions",
|
|
3424
|
+
description: "List custom field definitions for a work item type, including field IDs, types, options, and supported work item types.",
|
|
3425
|
+
capabilities: readGraphqlCapabilities,
|
|
3426
|
+
inputSchema: {
|
|
3427
|
+
project_id: optionalProjectIdSchema,
|
|
3428
|
+
work_item_type: optionalWorkItemType
|
|
3429
|
+
},
|
|
3430
|
+
handler: async (args, context) => listCustomFieldDefinitions(context, resolveProjectId(args, context, true), getOptionalString(args, "work_item_type") ?? "issue")
|
|
3431
|
+
},
|
|
3432
|
+
{
|
|
3433
|
+
name: "gitlab_move_work_item",
|
|
3434
|
+
title: "Move Work Item",
|
|
3435
|
+
description: "Move a work item to a different project.",
|
|
3436
|
+
capabilities: writeGraphqlCapabilities,
|
|
3437
|
+
inputSchema: {
|
|
3438
|
+
project_id: optionalProjectIdSchema,
|
|
3439
|
+
iid: workItemIidSchema,
|
|
3440
|
+
target_project_id: projectIdSchema
|
|
3441
|
+
},
|
|
3442
|
+
handler: async (args, context) => moveWorkItem(context, resolveProjectId(args, context, true), getNumber(args, "iid"), resolveExplicitProjectId(context, getString(args, "target_project_id")))
|
|
3443
|
+
},
|
|
3444
|
+
{
|
|
3445
|
+
name: "gitlab_list_work_item_notes",
|
|
3446
|
+
title: "List Work Item Notes",
|
|
3447
|
+
description: "List threaded discussions and notes on a work item.",
|
|
3448
|
+
capabilities: readGraphqlCapabilities,
|
|
3449
|
+
inputSchema: {
|
|
3450
|
+
project_id: optionalProjectIdSchema,
|
|
3451
|
+
iid: workItemIidSchema,
|
|
3452
|
+
page_size: z.coerce.number().int().positive().max(100).optional(),
|
|
3453
|
+
after: optionalString,
|
|
3454
|
+
sort: z.enum(["CREATED_ASC", "CREATED_DESC"]).optional()
|
|
3455
|
+
},
|
|
3456
|
+
handler: async (args, context) => listWorkItemNotes(context, resolveProjectId(args, context, true), getNumber(args, "iid"), {
|
|
3457
|
+
pageSize: getOptionalNumber(args, "page_size"),
|
|
3458
|
+
after: getOptionalString(args, "after"),
|
|
3459
|
+
sort: getOptionalString(args, "sort")
|
|
3460
|
+
})
|
|
3461
|
+
},
|
|
3462
|
+
{
|
|
3463
|
+
name: "gitlab_create_work_item_note",
|
|
3464
|
+
title: "Create Work Item Note",
|
|
3465
|
+
description: "Add a note or threaded reply to a work item.",
|
|
3466
|
+
capabilities: writeGraphqlCapabilities,
|
|
3467
|
+
inputSchema: {
|
|
3468
|
+
project_id: optionalProjectIdSchema,
|
|
3469
|
+
iid: workItemIidSchema,
|
|
3470
|
+
body: bodySchema,
|
|
3471
|
+
internal: optionalCoercedBoolean,
|
|
3472
|
+
discussion_id: optionalString
|
|
3473
|
+
},
|
|
3474
|
+
handler: async (args, context) => createWorkItemNote(context, resolveProjectId(args, context, true), getNumber(args, "iid"), getString(args, "body"), {
|
|
3475
|
+
internal: getOptionalBoolean(args, "internal"),
|
|
3476
|
+
discussionId: getOptionalString(args, "discussion_id")
|
|
3477
|
+
})
|
|
3478
|
+
},
|
|
3479
|
+
{
|
|
3480
|
+
name: "gitlab_list_work_item_emoji_reactions",
|
|
3481
|
+
title: "List Work Item Emoji Reactions",
|
|
3482
|
+
description: "List emoji reactions on a work item.",
|
|
3483
|
+
capabilities: readGraphqlCapabilities,
|
|
3484
|
+
inputSchema: {
|
|
3485
|
+
project_id: optionalProjectIdSchema,
|
|
3486
|
+
iid: workItemIidSchema
|
|
3487
|
+
},
|
|
3488
|
+
handler: async (args, context) => listGraphqlAwardEmoji(context, (await resolveWorkItemGid(context, resolveProjectId(args, context, true), getNumber(args, "iid"))).workItemGid)
|
|
3489
|
+
},
|
|
3490
|
+
{
|
|
3491
|
+
name: "gitlab_list_work_item_note_emoji_reactions",
|
|
3492
|
+
title: "List Work Item Note Emoji Reactions",
|
|
3493
|
+
description: "List emoji reactions on a work item note by GraphQL note_id.",
|
|
3494
|
+
capabilities: readGraphqlCapabilities,
|
|
3495
|
+
inputSchema: {
|
|
3496
|
+
project_id: optionalProjectIdSchema,
|
|
3497
|
+
iid: workItemIidSchema,
|
|
3498
|
+
note_id: z.string().min(1)
|
|
3499
|
+
},
|
|
3500
|
+
handler: async (args, context) => {
|
|
3501
|
+
const projectId = resolveProjectId(args, context, true);
|
|
3502
|
+
const noteId = await resolveWorkItemNoteAwardableId(context, projectId, getNumber(args, "iid"), getString(args, "note_id"));
|
|
3503
|
+
return listGraphqlAwardEmoji(context, noteId);
|
|
3504
|
+
}
|
|
3505
|
+
},
|
|
3506
|
+
{
|
|
3507
|
+
name: "gitlab_create_work_item_emoji_reaction",
|
|
3508
|
+
title: "Create Work Item Emoji Reaction",
|
|
3509
|
+
description: "Add an emoji reaction to a work item, for example thumbsup, rocket, or eyes.",
|
|
3510
|
+
capabilities: writeGraphqlCapabilities,
|
|
3511
|
+
inputSchema: {
|
|
3512
|
+
project_id: optionalProjectIdSchema,
|
|
3513
|
+
iid: workItemIidSchema,
|
|
3514
|
+
name: emojiNameSchema
|
|
3515
|
+
},
|
|
3516
|
+
handler: async (args, context) => addGraphqlAwardEmoji(context, (await resolveWorkItemGid(context, resolveProjectId(args, context, true), getNumber(args, "iid"))).workItemGid, getString(args, "name"))
|
|
3517
|
+
},
|
|
3518
|
+
{
|
|
3519
|
+
name: "gitlab_delete_work_item_emoji_reaction",
|
|
3520
|
+
title: "Delete Work Item Emoji Reaction",
|
|
3521
|
+
description: "Remove the current user's emoji reaction from a work item by emoji name. Requires iid and name.",
|
|
3522
|
+
capabilities: writeGraphqlCapabilities,
|
|
3523
|
+
inputSchema: {
|
|
3524
|
+
project_id: optionalProjectIdSchema,
|
|
3525
|
+
iid: workItemIidSchema,
|
|
3526
|
+
name: emojiNameSchema
|
|
3527
|
+
},
|
|
3528
|
+
handler: async (args, context) => removeGraphqlAwardEmoji(context, (await resolveWorkItemGid(context, resolveProjectId(args, context, true), getNumber(args, "iid"))).workItemGid, getString(args, "name"))
|
|
3529
|
+
},
|
|
3530
|
+
{
|
|
3531
|
+
name: "gitlab_create_work_item_note_emoji_reaction",
|
|
3532
|
+
title: "Create Work Item Note Emoji Reaction",
|
|
3533
|
+
description: "Add an emoji reaction to a work item note by GraphQL note_id.",
|
|
3534
|
+
capabilities: writeGraphqlCapabilities,
|
|
3535
|
+
inputSchema: {
|
|
3536
|
+
project_id: optionalProjectIdSchema,
|
|
3537
|
+
iid: workItemIidSchema,
|
|
3538
|
+
note_id: z.string().min(1),
|
|
3539
|
+
name: emojiNameSchema
|
|
3540
|
+
},
|
|
3541
|
+
handler: async (args, context) => {
|
|
3542
|
+
const projectId = resolveProjectId(args, context, true);
|
|
3543
|
+
const noteId = await resolveWorkItemNoteAwardableId(context, projectId, getNumber(args, "iid"), getString(args, "note_id"));
|
|
3544
|
+
return addGraphqlAwardEmoji(context, noteId, getString(args, "name"));
|
|
3545
|
+
}
|
|
3546
|
+
},
|
|
3547
|
+
{
|
|
3548
|
+
name: "gitlab_delete_work_item_note_emoji_reaction",
|
|
3549
|
+
title: "Delete Work Item Note Emoji Reaction",
|
|
3550
|
+
description: "Remove the current user's emoji reaction from a work item note by GraphQL note_id and emoji name.",
|
|
3551
|
+
capabilities: writeGraphqlCapabilities,
|
|
3552
|
+
inputSchema: {
|
|
3553
|
+
project_id: optionalProjectIdSchema,
|
|
3554
|
+
iid: workItemIidSchema,
|
|
3555
|
+
note_id: z.string().min(1),
|
|
3556
|
+
name: emojiNameSchema
|
|
3557
|
+
},
|
|
3558
|
+
handler: async (args, context) => {
|
|
3559
|
+
const projectId = resolveProjectId(args, context, true);
|
|
3560
|
+
const noteId = await resolveWorkItemNoteAwardableId(context, projectId, getNumber(args, "iid"), getString(args, "note_id"));
|
|
3561
|
+
return removeGraphqlAwardEmoji(context, noteId, getString(args, "name"));
|
|
3562
|
+
}
|
|
3563
|
+
},
|
|
3564
|
+
{
|
|
3565
|
+
name: "gitlab_get_timeline_events",
|
|
3566
|
+
title: "Get Timeline Events",
|
|
3567
|
+
description: "List timeline events for an incident work item.",
|
|
3568
|
+
capabilities: readGraphqlCapabilities,
|
|
3569
|
+
inputSchema: {
|
|
3570
|
+
project_id: optionalProjectIdSchema,
|
|
3571
|
+
incident_iid: workItemIidSchema
|
|
3572
|
+
},
|
|
3573
|
+
handler: async (args, context) => getTimelineEvents(context, resolveProjectId(args, context, true), getNumber(args, "incident_iid"))
|
|
3574
|
+
},
|
|
3575
|
+
{
|
|
3576
|
+
name: "gitlab_create_timeline_event",
|
|
3577
|
+
title: "Create Timeline Event",
|
|
3578
|
+
description: "Create an incident timeline event with optional known GitLab incident timeline tags.",
|
|
3579
|
+
capabilities: writeGraphqlCapabilities,
|
|
3580
|
+
inputSchema: {
|
|
3581
|
+
project_id: optionalProjectIdSchema,
|
|
3582
|
+
incident_iid: workItemIidSchema,
|
|
3583
|
+
note: bodySchema,
|
|
3584
|
+
occurred_at: z.string().min(1),
|
|
3585
|
+
tag_names: nullableOptional(z.array(z.enum([
|
|
3586
|
+
"Start time",
|
|
3587
|
+
"End time",
|
|
3588
|
+
"Impact detected",
|
|
3589
|
+
"Response initiated",
|
|
3590
|
+
"Impact mitigated",
|
|
3591
|
+
"Cause identified"
|
|
3592
|
+
])))
|
|
3593
|
+
},
|
|
3594
|
+
handler: async (args, context) => createTimelineEvent(context, resolveProjectId(args, context, true), getNumber(args, "incident_iid"), getString(args, "note"), getString(args, "occurred_at"), getOptionalStringArray(args, "tag_names"))
|
|
3595
|
+
},
|
|
3596
|
+
{
|
|
3597
|
+
name: "gitlab_execute_graphql_query",
|
|
3598
|
+
title: "Execute GraphQL Query",
|
|
3599
|
+
description: "Execute read-only GraphQL query.",
|
|
3600
|
+
capabilities: readGraphqlCapabilities,
|
|
3601
|
+
inputSchema: {
|
|
3602
|
+
query: z.string().min(1),
|
|
3603
|
+
variables: optionalRecord
|
|
3604
|
+
},
|
|
3605
|
+
handler: async (args, context) => {
|
|
3606
|
+
const query = getString(args, "query");
|
|
3607
|
+
if (containsGraphqlMutation(query)) {
|
|
3608
|
+
throw new Error("Mutation detected. Use gitlab_execute_graphql_mutation for mutation operations.");
|
|
3609
|
+
}
|
|
3610
|
+
return context.gitlab.executeGraphql(query, getOptionalRecord(args, "variables"));
|
|
3611
|
+
}
|
|
3612
|
+
},
|
|
3613
|
+
{
|
|
3614
|
+
name: "gitlab_execute_graphql_mutation",
|
|
3615
|
+
title: "Execute GraphQL Mutation",
|
|
3616
|
+
description: "Execute GraphQL mutation (disabled in read-only mode).",
|
|
3617
|
+
capabilities: writeGraphqlCapabilities,
|
|
3618
|
+
inputSchema: {
|
|
3619
|
+
query: z.string().min(1),
|
|
3620
|
+
variables: optionalRecord
|
|
3621
|
+
},
|
|
3622
|
+
handler: async (args, context) => {
|
|
3623
|
+
const query = getString(args, "query");
|
|
3624
|
+
if (!containsGraphqlMutation(query)) {
|
|
3625
|
+
throw new Error("No mutation detected. Use gitlab_execute_graphql_query for queries.");
|
|
3626
|
+
}
|
|
3627
|
+
return context.gitlab.executeGraphql(query, getOptionalRecord(args, "variables"));
|
|
3628
|
+
}
|
|
3629
|
+
},
|
|
3630
|
+
{
|
|
3631
|
+
name: "gitlab_execute_graphql",
|
|
3632
|
+
title: "Execute GraphQL (Compat)",
|
|
3633
|
+
description: "Backward-compatible GraphQL executor. Mutation payloads still honor read-only policy.",
|
|
3634
|
+
capabilities: readGraphqlCapabilities,
|
|
3635
|
+
inputSchema: {
|
|
3636
|
+
query: z.string().min(1),
|
|
3637
|
+
variables: optionalRecord
|
|
3638
|
+
},
|
|
3639
|
+
handler: async (args, context) => {
|
|
2366
3640
|
const query = getString(args, "query");
|
|
2367
3641
|
if (containsGraphqlMutation(query)) {
|
|
2368
3642
|
context.policy.assertCanExecute({
|
|
@@ -2375,12 +3649,65 @@ function getGitLabToolDefinitions() {
|
|
|
2375
3649
|
}
|
|
2376
3650
|
];
|
|
2377
3651
|
}
|
|
3652
|
+
function shouldReturnDownloadProxy(context) {
|
|
3653
|
+
return !context.allowLocalFileTools && resolveDownloadTokenAuth(context) !== undefined;
|
|
3654
|
+
}
|
|
3655
|
+
function buildDownloadProxyResult(context, resource, filename) {
|
|
3656
|
+
return {
|
|
3657
|
+
download_url: buildDownloadProxyUrl(context, resource),
|
|
3658
|
+
filename,
|
|
3659
|
+
expires_in_seconds: context.env.GITLAB_DOWNLOAD_TOKEN_TTL_SECONDS
|
|
3660
|
+
};
|
|
3661
|
+
}
|
|
3662
|
+
function buildDownloadProxyUrl(context, resource) {
|
|
3663
|
+
const baseUrl = new URL(context.env.MCP_SERVER_URL ?? `http://${context.env.HTTP_HOST}:${String(context.env.HTTP_PORT)}`);
|
|
3664
|
+
const basePath = baseUrl.pathname.replace(/\/+$/, "");
|
|
3665
|
+
const url = new URL(`${basePath}/downloads/${encodeURIComponent(resource.type)}`, baseUrl.origin);
|
|
3666
|
+
for (const [key, value] of Object.entries(resource.params)) {
|
|
3667
|
+
url.searchParams.set(key, value);
|
|
3668
|
+
}
|
|
3669
|
+
const tokenAuth = resolveDownloadTokenAuth(context);
|
|
3670
|
+
if (tokenAuth) {
|
|
3671
|
+
url.searchParams.set("_token", createDownloadToken(tokenAuth, resource, {
|
|
3672
|
+
secret: context.env.GITLAB_DOWNLOAD_TOKEN_SECRET,
|
|
3673
|
+
ttlSeconds: context.env.GITLAB_DOWNLOAD_TOKEN_TTL_SECONDS
|
|
3674
|
+
}));
|
|
3675
|
+
}
|
|
3676
|
+
return url.toString();
|
|
3677
|
+
}
|
|
3678
|
+
function resolveDownloadTokenAuth(context) {
|
|
3679
|
+
const sessionAuth = getSessionAuth();
|
|
3680
|
+
if (sessionAuth?.token) {
|
|
3681
|
+
return {
|
|
3682
|
+
header: sessionAuth.header ?? "private-token",
|
|
3683
|
+
token: sessionAuth.token,
|
|
3684
|
+
apiUrl: context.env.ENABLE_DYNAMIC_API_URL && sessionAuth.apiUrl !== context.env.GITLAB_API_URL
|
|
3685
|
+
? sessionAuth.apiUrl
|
|
3686
|
+
: undefined
|
|
3687
|
+
};
|
|
3688
|
+
}
|
|
3689
|
+
if (context.env.GITLAB_PERSONAL_ACCESS_TOKEN) {
|
|
3690
|
+
return {
|
|
3691
|
+
header: "private-token",
|
|
3692
|
+
token: context.env.GITLAB_PERSONAL_ACCESS_TOKEN
|
|
3693
|
+
};
|
|
3694
|
+
}
|
|
3695
|
+
if (context.env.GITLAB_JOB_TOKEN) {
|
|
3696
|
+
return {
|
|
3697
|
+
header: "job-token",
|
|
3698
|
+
token: context.env.GITLAB_JOB_TOKEN
|
|
3699
|
+
};
|
|
3700
|
+
}
|
|
3701
|
+
return undefined;
|
|
3702
|
+
}
|
|
2378
3703
|
function assertAuthReady(context) {
|
|
2379
3704
|
const auth = getSessionAuth();
|
|
2380
|
-
if (context.env.REMOTE_AUTHORIZATION) {
|
|
3705
|
+
if (context.env.REMOTE_AUTHORIZATION || context.env.GITLAB_MCP_OAUTH) {
|
|
2381
3706
|
const token = auth?.token;
|
|
2382
3707
|
if (!token) {
|
|
2383
|
-
throw new Error(
|
|
3708
|
+
throw new Error(context.env.REMOTE_AUTHORIZATION
|
|
3709
|
+
? "Missing remote authorization token for this session"
|
|
3710
|
+
: "Missing OAuth authorization token for this session");
|
|
2384
3711
|
}
|
|
2385
3712
|
if (context.env.ENABLE_DYNAMIC_API_URL && !auth?.apiUrl) {
|
|
2386
3713
|
throw new Error("Missing remote API URL for this session");
|
|
@@ -2388,14 +3715,1032 @@ function assertAuthReady(context) {
|
|
|
2388
3715
|
return;
|
|
2389
3716
|
}
|
|
2390
3717
|
const hasFallbackAuth = Boolean(context.env.GITLAB_PERSONAL_ACCESS_TOKEN) ||
|
|
3718
|
+
Boolean(context.env.GITLAB_JOB_TOKEN) ||
|
|
2391
3719
|
Boolean(context.env.GITLAB_USE_OAUTH && context.env.GITLAB_OAUTH_CLIENT_ID) ||
|
|
2392
3720
|
Boolean(context.env.GITLAB_TOKEN_SCRIPT) ||
|
|
2393
3721
|
Boolean(context.env.GITLAB_TOKEN_FILE) ||
|
|
2394
3722
|
Boolean(context.env.GITLAB_AUTH_COOKIE_PATH);
|
|
2395
3723
|
if (!hasFallbackAuth) {
|
|
2396
|
-
throw new Error("Authentication required: set GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_TOKEN_SCRIPT, GITLAB_TOKEN_FILE, or GITLAB_AUTH_COOKIE_PATH");
|
|
3724
|
+
throw new Error("Authentication required: set GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_JOB_TOKEN, GITLAB_TOKEN_SCRIPT, GITLAB_TOKEN_FILE, or GITLAB_AUTH_COOKIE_PATH");
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
const WORK_ITEM_TYPE_NAMES = {
|
|
3728
|
+
issue: "Issue",
|
|
3729
|
+
task: "Task",
|
|
3730
|
+
incident: "Incident",
|
|
3731
|
+
test_case: "Test Case",
|
|
3732
|
+
epic: "Epic",
|
|
3733
|
+
key_result: "Key Result",
|
|
3734
|
+
objective: "Objective",
|
|
3735
|
+
requirement: "Requirement",
|
|
3736
|
+
ticket: "Ticket"
|
|
3737
|
+
};
|
|
3738
|
+
const WORK_ITEM_GRAPHQL_TYPES = {
|
|
3739
|
+
issue: "ISSUE",
|
|
3740
|
+
task: "TASK",
|
|
3741
|
+
incident: "INCIDENT",
|
|
3742
|
+
test_case: "TEST_CASE",
|
|
3743
|
+
epic: "EPIC",
|
|
3744
|
+
key_result: "KEY_RESULT",
|
|
3745
|
+
objective: "OBJECTIVE",
|
|
3746
|
+
requirement: "REQUIREMENT",
|
|
3747
|
+
ticket: "TICKET"
|
|
3748
|
+
};
|
|
3749
|
+
async function executeGraphqlData(context, query, variables = {}) {
|
|
3750
|
+
const response = (await context.gitlab.executeGraphql(query, variables));
|
|
3751
|
+
if (typeof response === "object" &&
|
|
3752
|
+
response !== null &&
|
|
3753
|
+
"errors" in response &&
|
|
3754
|
+
Array.isArray(response.errors)) {
|
|
3755
|
+
const errors = response.errors;
|
|
3756
|
+
throw new Error(`GraphQL errors: ${errors.map((item) => item.message ?? String(item)).join(", ")}`);
|
|
3757
|
+
}
|
|
3758
|
+
if (typeof response === "object" && response !== null && "data" in response) {
|
|
3759
|
+
return response.data;
|
|
3760
|
+
}
|
|
3761
|
+
return response;
|
|
3762
|
+
}
|
|
3763
|
+
function resolveExplicitProjectId(context, projectId) {
|
|
3764
|
+
const allowed = context.env.GITLAB_ALLOWED_PROJECT_IDS;
|
|
3765
|
+
if (allowed.length > 0 && !allowed.includes(projectId)) {
|
|
3766
|
+
throw new Error(`Project '${projectId}' is not in GITLAB_ALLOWED_PROJECT_IDS: ${allowed.join(", ")}`);
|
|
3767
|
+
}
|
|
3768
|
+
return projectId;
|
|
3769
|
+
}
|
|
3770
|
+
async function resolveProjectPathForWorkItem(context, projectId) {
|
|
3771
|
+
const project = (await context.gitlab.getProject(projectId));
|
|
3772
|
+
const pathWithNamespace = project.path_with_namespace;
|
|
3773
|
+
if (typeof pathWithNamespace === "string" && pathWithNamespace.length > 0) {
|
|
3774
|
+
return pathWithNamespace;
|
|
3775
|
+
}
|
|
3776
|
+
if (projectId.includes("/")) {
|
|
3777
|
+
return projectId;
|
|
3778
|
+
}
|
|
3779
|
+
throw new Error(`Project '${projectId}' did not include path_with_namespace`);
|
|
3780
|
+
}
|
|
3781
|
+
async function resolveWorkItemGid(context, projectId, iid) {
|
|
3782
|
+
const projectPath = await resolveProjectPathForWorkItem(context, projectId);
|
|
3783
|
+
const data = await executeGraphqlData(context, `query($path: ID!, $iid: String!) {
|
|
3784
|
+
namespace(fullPath: $path) {
|
|
3785
|
+
workItem(iid: $iid) { id }
|
|
3786
|
+
}
|
|
3787
|
+
}`, { path: projectPath, iid: String(iid) });
|
|
3788
|
+
const workItemGid = data.namespace?.workItem?.id;
|
|
3789
|
+
if (!workItemGid) {
|
|
3790
|
+
throw new Error(`Work item #${iid} not found in project ${projectPath}`);
|
|
3791
|
+
}
|
|
3792
|
+
return { workItemGid, projectPath };
|
|
3793
|
+
}
|
|
3794
|
+
async function resolveWorkItemTypeGid(context, projectPath, type) {
|
|
3795
|
+
const targetName = WORK_ITEM_TYPE_NAMES[type];
|
|
3796
|
+
const data = await executeGraphqlData(context, `query($path: ID!) {
|
|
3797
|
+
namespace(fullPath: $path) {
|
|
3798
|
+
workItemTypes { nodes { id name } }
|
|
3799
|
+
}
|
|
3800
|
+
}`, { path: projectPath });
|
|
3801
|
+
const match = data.namespace?.workItemTypes?.nodes?.find((item) => item.name === targetName);
|
|
3802
|
+
if (!match) {
|
|
3803
|
+
throw new Error(`Work item type '${targetName}' not found in project ${projectPath}`);
|
|
2397
3804
|
}
|
|
3805
|
+
return match.id;
|
|
2398
3806
|
}
|
|
3807
|
+
async function resolveNamesToIds(context, projectPath, labelNames, usernames) {
|
|
3808
|
+
if ((!labelNames || labelNames.length === 0) && (!usernames || usernames.length === 0)) {
|
|
3809
|
+
return { labelIds: [], userIds: [] };
|
|
3810
|
+
}
|
|
3811
|
+
const data = await executeGraphqlData(context, `query($path: ID!, $usernames: [String!]!) {
|
|
3812
|
+
project(fullPath: $path) {
|
|
3813
|
+
labels(includeAncestorGroups: true, first: 250) { nodes { id title } }
|
|
3814
|
+
}
|
|
3815
|
+
users(usernames: $usernames) { nodes { id username } }
|
|
3816
|
+
}`, { path: projectPath, usernames: usernames ?? [] });
|
|
3817
|
+
const labels = data.project?.labels?.nodes ?? [];
|
|
3818
|
+
const users = data.users?.nodes ?? [];
|
|
3819
|
+
const labelIds = (labelNames ?? []).map((name) => {
|
|
3820
|
+
const label = labels.find((item) => item.title === name);
|
|
3821
|
+
if (!label) {
|
|
3822
|
+
throw new Error(`Label '${name}' not found in project ${projectPath}`);
|
|
3823
|
+
}
|
|
3824
|
+
return label.id;
|
|
3825
|
+
});
|
|
3826
|
+
const userIds = (usernames ?? []).map((username) => {
|
|
3827
|
+
const user = users.find((item) => item.username === username);
|
|
3828
|
+
if (!user) {
|
|
3829
|
+
throw new Error(`User '${username}' not found`);
|
|
3830
|
+
}
|
|
3831
|
+
return user.id;
|
|
3832
|
+
});
|
|
3833
|
+
return { labelIds, userIds };
|
|
3834
|
+
}
|
|
3835
|
+
function normalizeGlobalId(value, typeName) {
|
|
3836
|
+
return value.startsWith("gid://") ? value : `gid://gitlab/${typeName}/${value}`;
|
|
3837
|
+
}
|
|
3838
|
+
function toWorkItemGraphqlType(type) {
|
|
3839
|
+
return WORK_ITEM_GRAPHQL_TYPES[type] ?? type.replace(/ /g, "_").toUpperCase();
|
|
3840
|
+
}
|
|
3841
|
+
function workItemTypeName(type) {
|
|
3842
|
+
return WORK_ITEM_TYPE_NAMES[type] ?? "Issue";
|
|
3843
|
+
}
|
|
3844
|
+
function findWidget(widgets, typename) {
|
|
3845
|
+
if (!Array.isArray(widgets)) {
|
|
3846
|
+
return undefined;
|
|
3847
|
+
}
|
|
3848
|
+
return widgets.find((item) => typeof item === "object" && item !== null && item.__typename === typename);
|
|
3849
|
+
}
|
|
3850
|
+
async function getWorkItem(context, projectId, iid) {
|
|
3851
|
+
const projectPath = await resolveProjectPathForWorkItem(context, projectId);
|
|
3852
|
+
const data = await executeGraphqlData(context, `query($path: ID!, $iid: String!) {
|
|
3853
|
+
namespace(fullPath: $path) {
|
|
3854
|
+
workItem(iid: $iid) {
|
|
3855
|
+
id
|
|
3856
|
+
iid
|
|
3857
|
+
title
|
|
3858
|
+
state
|
|
3859
|
+
description
|
|
3860
|
+
webUrl
|
|
3861
|
+
confidential
|
|
3862
|
+
author { username }
|
|
3863
|
+
createdAt
|
|
3864
|
+
closedAt
|
|
3865
|
+
workItemType { name }
|
|
3866
|
+
widgets {
|
|
3867
|
+
__typename
|
|
3868
|
+
... on WorkItemWidgetHierarchy {
|
|
3869
|
+
hasChildren
|
|
3870
|
+
hasParent
|
|
3871
|
+
parent { id iid title webUrl workItemType { name } namespace { fullPath } }
|
|
3872
|
+
children { nodes { id iid title state webUrl workItemType { name } namespace { fullPath } } }
|
|
3873
|
+
}
|
|
3874
|
+
... on WorkItemWidgetStatus { status { id name category color iconName position } }
|
|
3875
|
+
... on WorkItemWidgetCustomFields {
|
|
3876
|
+
customFieldValues {
|
|
3877
|
+
__typename
|
|
3878
|
+
customField { id name fieldType }
|
|
3879
|
+
... on WorkItemNumberFieldValue { value }
|
|
3880
|
+
... on WorkItemTextFieldValue { value }
|
|
3881
|
+
... on WorkItemSelectFieldValue { selectedOptions { id value } }
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
... on WorkItemWidgetLabels { labels { nodes { id title color } } }
|
|
3885
|
+
... on WorkItemWidgetAssignees { assignees { nodes { id username name } } }
|
|
3886
|
+
... on WorkItemWidgetWeight { weight rolledUpWeight rolledUpCompletedWeight }
|
|
3887
|
+
... on WorkItemWidgetHealthStatus { healthStatus }
|
|
3888
|
+
... on WorkItemWidgetStartAndDueDate { startDate dueDate }
|
|
3889
|
+
... on WorkItemWidgetMilestone { milestone { id title } }
|
|
3890
|
+
... on WorkItemWidgetLinkedItems {
|
|
3891
|
+
blocked
|
|
3892
|
+
blockedByCount
|
|
3893
|
+
blockingCount
|
|
3894
|
+
linkedItems { nodes { linkType workItem { id iid title state webUrl workItemType { name } namespace { fullPath } } } }
|
|
3895
|
+
}
|
|
3896
|
+
... on WorkItemWidgetTimeTracking { timeEstimate totalTimeSpent }
|
|
3897
|
+
... on WorkItemWidgetDevelopment {
|
|
3898
|
+
willAutoCloseByMergeRequest
|
|
3899
|
+
relatedBranches { nodes { name } }
|
|
3900
|
+
relatedMergeRequests { nodes { iid title webUrl state sourceBranch } }
|
|
3901
|
+
closingMergeRequests { nodes { mergeRequest { iid title webUrl state sourceBranch } } }
|
|
3902
|
+
featureFlags { nodes { name active } }
|
|
3903
|
+
}
|
|
3904
|
+
... on WorkItemWidgetIteration {
|
|
3905
|
+
iteration { id title startDate dueDate webUrl iterationCadence { id title } }
|
|
3906
|
+
}
|
|
3907
|
+
... on WorkItemWidgetProgress { progress }
|
|
3908
|
+
... on WorkItemWidgetColor { color textColor }
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
}`, { path: projectPath, iid: String(iid) });
|
|
3913
|
+
const workItem = data.namespace?.workItem;
|
|
3914
|
+
if (!workItem) {
|
|
3915
|
+
throw new Error(`Work item #${iid} not found in project ${projectPath}`);
|
|
3916
|
+
}
|
|
3917
|
+
return flattenWorkItem(workItem);
|
|
3918
|
+
}
|
|
3919
|
+
function flattenWorkItem(workItem) {
|
|
3920
|
+
const widgets = workItem.widgets ?? [];
|
|
3921
|
+
const hierarchy = findWidget(widgets, "WorkItemWidgetHierarchy");
|
|
3922
|
+
const status = findWidget(widgets, "WorkItemWidgetStatus");
|
|
3923
|
+
const labels = findWidget(widgets, "WorkItemWidgetLabels");
|
|
3924
|
+
const assignees = findWidget(widgets, "WorkItemWidgetAssignees");
|
|
3925
|
+
const weight = findWidget(widgets, "WorkItemWidgetWeight");
|
|
3926
|
+
const health = findWidget(widgets, "WorkItemWidgetHealthStatus");
|
|
3927
|
+
const dates = findWidget(widgets, "WorkItemWidgetStartAndDueDate");
|
|
3928
|
+
const milestone = findWidget(widgets, "WorkItemWidgetMilestone");
|
|
3929
|
+
const linked = findWidget(widgets, "WorkItemWidgetLinkedItems");
|
|
3930
|
+
const timeTracking = findWidget(widgets, "WorkItemWidgetTimeTracking");
|
|
3931
|
+
const development = findWidget(widgets, "WorkItemWidgetDevelopment");
|
|
3932
|
+
const customFields = findWidget(widgets, "WorkItemWidgetCustomFields");
|
|
3933
|
+
const iteration = findWidget(widgets, "WorkItemWidgetIteration");
|
|
3934
|
+
const progress = findWidget(widgets, "WorkItemWidgetProgress");
|
|
3935
|
+
const color = findWidget(widgets, "WorkItemWidgetColor");
|
|
3936
|
+
const result = {
|
|
3937
|
+
id: workItem.id,
|
|
3938
|
+
iid: workItem.iid,
|
|
3939
|
+
title: workItem.title,
|
|
3940
|
+
state: workItem.state,
|
|
3941
|
+
type: workItem.workItemType?.name,
|
|
3942
|
+
webUrl: workItem.webUrl
|
|
3943
|
+
};
|
|
3944
|
+
if (workItem.description)
|
|
3945
|
+
result.description = workItem.description;
|
|
3946
|
+
if (workItem.confidential)
|
|
3947
|
+
result.confidential = true;
|
|
3948
|
+
if (workItem.author?.username)
|
|
3949
|
+
result.author = workItem.author.username;
|
|
3950
|
+
if (workItem.createdAt)
|
|
3951
|
+
result.createdAt = workItem.createdAt;
|
|
3952
|
+
if (workItem.closedAt)
|
|
3953
|
+
result.closedAt = workItem.closedAt;
|
|
3954
|
+
if (status?.status) {
|
|
3955
|
+
result.status = {
|
|
3956
|
+
id: status.status.id,
|
|
3957
|
+
name: status.status.name,
|
|
3958
|
+
category: status.status.category
|
|
3959
|
+
};
|
|
3960
|
+
}
|
|
3961
|
+
const labelNames = (labels?.labels?.nodes ?? []).map((item) => item.title);
|
|
3962
|
+
if (labelNames.length > 0)
|
|
3963
|
+
result.labels = labelNames;
|
|
3964
|
+
const assigneeNames = (assignees?.assignees?.nodes ?? []).map((item) => item.username);
|
|
3965
|
+
if (assigneeNames.length > 0)
|
|
3966
|
+
result.assignees = assigneeNames;
|
|
3967
|
+
if (weight?.weight != null) {
|
|
3968
|
+
result.weight = weight.weight;
|
|
3969
|
+
if (weight.rolledUpWeight != null)
|
|
3970
|
+
result.rolledUpWeight = weight.rolledUpWeight;
|
|
3971
|
+
if (weight.rolledUpCompletedWeight != null) {
|
|
3972
|
+
result.rolledUpCompletedWeight = weight.rolledUpCompletedWeight;
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3975
|
+
if (health?.healthStatus)
|
|
3976
|
+
result.healthStatus = health.healthStatus;
|
|
3977
|
+
if (dates?.startDate)
|
|
3978
|
+
result.startDate = dates.startDate;
|
|
3979
|
+
if (dates?.dueDate)
|
|
3980
|
+
result.dueDate = dates.dueDate;
|
|
3981
|
+
if (milestone?.milestone)
|
|
3982
|
+
result.milestone = milestone.milestone;
|
|
3983
|
+
if (iteration?.iteration)
|
|
3984
|
+
result.iteration = iteration.iteration;
|
|
3985
|
+
if (progress?.progress != null)
|
|
3986
|
+
result.progress = progress.progress;
|
|
3987
|
+
if (color?.color)
|
|
3988
|
+
result.color = color.color;
|
|
3989
|
+
if (hierarchy?.parent) {
|
|
3990
|
+
result.parent = {
|
|
3991
|
+
iid: hierarchy.parent.iid,
|
|
3992
|
+
title: hierarchy.parent.title,
|
|
3993
|
+
type: hierarchy.parent.workItemType?.name,
|
|
3994
|
+
project: hierarchy.parent.namespace?.fullPath,
|
|
3995
|
+
webUrl: hierarchy.parent.webUrl
|
|
3996
|
+
};
|
|
3997
|
+
}
|
|
3998
|
+
const children = hierarchy?.children?.nodes ?? [];
|
|
3999
|
+
if (children.length > 0) {
|
|
4000
|
+
result.children = children.map((item) => ({
|
|
4001
|
+
iid: item.iid,
|
|
4002
|
+
title: item.title,
|
|
4003
|
+
state: item.state,
|
|
4004
|
+
type: item.workItemType?.name,
|
|
4005
|
+
project: item.namespace?.fullPath,
|
|
4006
|
+
webUrl: item.webUrl
|
|
4007
|
+
}));
|
|
4008
|
+
}
|
|
4009
|
+
if (linked?.blocked)
|
|
4010
|
+
result.blocked = true;
|
|
4011
|
+
if ((linked?.blockedByCount ?? 0) > 0)
|
|
4012
|
+
result.blockedByCount = linked?.blockedByCount;
|
|
4013
|
+
if ((linked?.blockingCount ?? 0) > 0)
|
|
4014
|
+
result.blockingCount = linked?.blockingCount;
|
|
4015
|
+
const linkedItems = linked?.linkedItems?.nodes ?? [];
|
|
4016
|
+
if (linkedItems.length > 0) {
|
|
4017
|
+
result.linkedItems = linkedItems.map((item) => ({
|
|
4018
|
+
linkType: item.linkType,
|
|
4019
|
+
iid: item.workItem?.iid,
|
|
4020
|
+
title: item.workItem?.title,
|
|
4021
|
+
state: item.workItem?.state,
|
|
4022
|
+
type: item.workItem?.workItemType?.name,
|
|
4023
|
+
project: item.workItem?.namespace?.fullPath,
|
|
4024
|
+
webUrl: item.workItem?.webUrl
|
|
4025
|
+
}));
|
|
4026
|
+
}
|
|
4027
|
+
if ((timeTracking?.timeEstimate ?? 0) > 0)
|
|
4028
|
+
result.timeEstimate = timeTracking?.timeEstimate;
|
|
4029
|
+
if ((timeTracking?.totalTimeSpent ?? 0) > 0) {
|
|
4030
|
+
result.totalTimeSpent = timeTracking?.totalTimeSpent;
|
|
4031
|
+
}
|
|
4032
|
+
const relatedMergeRequests = development?.relatedMergeRequests?.nodes ?? [];
|
|
4033
|
+
const closingMergeRequests = (development?.closingMergeRequests?.nodes ?? []).map((item) => item.mergeRequest);
|
|
4034
|
+
const branches = development?.relatedBranches?.nodes ?? [];
|
|
4035
|
+
const flags = development?.featureFlags?.nodes ?? [];
|
|
4036
|
+
if (relatedMergeRequests.length > 0 ||
|
|
4037
|
+
closingMergeRequests.length > 0 ||
|
|
4038
|
+
branches.length > 0 ||
|
|
4039
|
+
flags.length > 0) {
|
|
4040
|
+
result.development = {};
|
|
4041
|
+
if (relatedMergeRequests.length > 0) {
|
|
4042
|
+
result.development.relatedMergeRequests = relatedMergeRequests;
|
|
4043
|
+
}
|
|
4044
|
+
if (closingMergeRequests.length > 0) {
|
|
4045
|
+
result.development.closingMergeRequests = closingMergeRequests;
|
|
4046
|
+
}
|
|
4047
|
+
if (branches.length > 0) {
|
|
4048
|
+
result.development.relatedBranches = branches.map((item) => item.name);
|
|
4049
|
+
}
|
|
4050
|
+
if (flags.length > 0)
|
|
4051
|
+
result.development.featureFlags = flags;
|
|
4052
|
+
}
|
|
4053
|
+
const fieldValues = (customFields?.customFieldValues ?? []).filter((item) => item.value != null || item.selectedOptions != null);
|
|
4054
|
+
if (fieldValues.length > 0) {
|
|
4055
|
+
result.customFields = fieldValues.map((item) => ({
|
|
4056
|
+
name: item.customField?.name,
|
|
4057
|
+
type: item.customField?.fieldType,
|
|
4058
|
+
value: item.value ?? item.selectedOptions ?? null
|
|
4059
|
+
}));
|
|
4060
|
+
}
|
|
4061
|
+
return result;
|
|
4062
|
+
}
|
|
4063
|
+
async function listWorkItems(context, projectId, options) {
|
|
4064
|
+
const projectPath = await resolveProjectPathForWorkItem(context, projectId);
|
|
4065
|
+
const variables = {
|
|
4066
|
+
path: projectPath,
|
|
4067
|
+
first: options.first ?? 20,
|
|
4068
|
+
types: options.types?.map(toWorkItemGraphqlType),
|
|
4069
|
+
state: options.state,
|
|
4070
|
+
search: options.search,
|
|
4071
|
+
assigneeUsernames: options.assigneeUsernames,
|
|
4072
|
+
labelName: options.labelNames,
|
|
4073
|
+
after: options.after
|
|
4074
|
+
};
|
|
4075
|
+
const data = await executeGraphqlData(context, `query($path: ID!, $types: [IssueType!], $state: IssuableState, $search: String, $assigneeUsernames: [String!], $labelName: [String!], $first: Int, $after: String) {
|
|
4076
|
+
project(fullPath: $path) {
|
|
4077
|
+
workItems(types: $types, state: $state, search: $search, assigneeUsernames: $assigneeUsernames, labelName: $labelName, first: $first, after: $after) {
|
|
4078
|
+
nodes {
|
|
4079
|
+
id iid title state webUrl workItemType { name }
|
|
4080
|
+
widgets {
|
|
4081
|
+
__typename
|
|
4082
|
+
... on WorkItemWidgetStatus { status { id name category color } }
|
|
4083
|
+
... on WorkItemWidgetLabels { labels { nodes { title } } }
|
|
4084
|
+
... on WorkItemWidgetAssignees { assignees { nodes { username } } }
|
|
4085
|
+
... on WorkItemWidgetWeight { weight }
|
|
4086
|
+
... on WorkItemWidgetHealthStatus { healthStatus }
|
|
4087
|
+
... on WorkItemWidgetStartAndDueDate { startDate dueDate }
|
|
4088
|
+
... on WorkItemWidgetMilestone { milestone { id title } }
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
pageInfo { hasNextPage endCursor }
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
}`, variables);
|
|
4095
|
+
const nodes = data.project?.workItems?.nodes ?? [];
|
|
4096
|
+
return {
|
|
4097
|
+
items: nodes.map(flattenWorkItemSummary),
|
|
4098
|
+
pageInfo: data.project?.workItems?.pageInfo ?? {}
|
|
4099
|
+
};
|
|
4100
|
+
}
|
|
4101
|
+
function flattenWorkItemSummary(workItem) {
|
|
4102
|
+
const widgets = workItem.widgets ?? [];
|
|
4103
|
+
const status = findWidget(widgets, "WorkItemWidgetStatus");
|
|
4104
|
+
const labels = findWidget(widgets, "WorkItemWidgetLabels");
|
|
4105
|
+
const assignees = findWidget(widgets, "WorkItemWidgetAssignees");
|
|
4106
|
+
const weight = findWidget(widgets, "WorkItemWidgetWeight");
|
|
4107
|
+
const health = findWidget(widgets, "WorkItemWidgetHealthStatus");
|
|
4108
|
+
const dates = findWidget(widgets, "WorkItemWidgetStartAndDueDate");
|
|
4109
|
+
const milestone = findWidget(widgets, "WorkItemWidgetMilestone");
|
|
4110
|
+
const item = {
|
|
4111
|
+
iid: workItem.iid,
|
|
4112
|
+
title: workItem.title,
|
|
4113
|
+
state: workItem.state,
|
|
4114
|
+
type: workItem.workItemType?.name,
|
|
4115
|
+
webUrl: workItem.webUrl
|
|
4116
|
+
};
|
|
4117
|
+
if (status?.status)
|
|
4118
|
+
item.status = status.status.name;
|
|
4119
|
+
const labelNames = (labels?.labels?.nodes ?? []).map((label) => label.title);
|
|
4120
|
+
if (labelNames.length > 0)
|
|
4121
|
+
item.labels = labelNames;
|
|
4122
|
+
const assigneeNames = (assignees?.assignees?.nodes ?? []).map((assignee) => assignee.username);
|
|
4123
|
+
if (assigneeNames.length > 0)
|
|
4124
|
+
item.assignees = assigneeNames;
|
|
4125
|
+
if (weight?.weight != null)
|
|
4126
|
+
item.weight = weight.weight;
|
|
4127
|
+
if (health?.healthStatus)
|
|
4128
|
+
item.healthStatus = health.healthStatus;
|
|
4129
|
+
if (dates?.startDate)
|
|
4130
|
+
item.startDate = dates.startDate;
|
|
4131
|
+
if (dates?.dueDate)
|
|
4132
|
+
item.dueDate = dates.dueDate;
|
|
4133
|
+
if (milestone?.milestone)
|
|
4134
|
+
item.milestone = milestone.milestone.title;
|
|
4135
|
+
return item;
|
|
4136
|
+
}
|
|
4137
|
+
async function createWorkItem(context, projectId, options) {
|
|
4138
|
+
const projectPath = await resolveProjectPathForWorkItem(context, projectId);
|
|
4139
|
+
const typeId = await resolveWorkItemTypeGid(context, projectPath, options.type ?? "issue");
|
|
4140
|
+
const variableDefinitions = ["$projectPath: ID!", "$title: String!", "$typeId: WorkItemsTypeID!"];
|
|
4141
|
+
const inputParts = ["namespacePath: $projectPath", "title: $title", "workItemTypeId: $typeId"];
|
|
4142
|
+
const variables = {
|
|
4143
|
+
projectPath,
|
|
4144
|
+
title: options.title,
|
|
4145
|
+
typeId
|
|
4146
|
+
};
|
|
4147
|
+
if (options.description !== undefined) {
|
|
4148
|
+
variableDefinitions.push("$description: String!");
|
|
4149
|
+
inputParts.push("descriptionWidget: { description: $description }");
|
|
4150
|
+
variables.description = options.description;
|
|
4151
|
+
}
|
|
4152
|
+
const { labelIds, userIds } = await resolveNamesToIds(context, projectPath, options.labels, options.assigneeUsernames);
|
|
4153
|
+
if (labelIds.length > 0) {
|
|
4154
|
+
variableDefinitions.push("$labelIds: [LabelID!]!");
|
|
4155
|
+
inputParts.push("labelsWidget: { labelIds: $labelIds }");
|
|
4156
|
+
variables.labelIds = labelIds;
|
|
4157
|
+
}
|
|
4158
|
+
if (userIds.length > 0) {
|
|
4159
|
+
variableDefinitions.push("$assigneeIds: [UserID!]!");
|
|
4160
|
+
inputParts.push("assigneesWidget: { assigneeIds: $assigneeIds }");
|
|
4161
|
+
variables.assigneeIds = userIds;
|
|
4162
|
+
}
|
|
4163
|
+
if (options.weight !== undefined) {
|
|
4164
|
+
variableDefinitions.push("$weight: Int");
|
|
4165
|
+
inputParts.push("weightWidget: { weight: $weight }");
|
|
4166
|
+
variables.weight = options.weight;
|
|
4167
|
+
}
|
|
4168
|
+
if (options.parentIid !== undefined) {
|
|
4169
|
+
const { workItemGid: parentId } = await resolveWorkItemGid(context, projectId, options.parentIid);
|
|
4170
|
+
variableDefinitions.push("$parentId: WorkItemID");
|
|
4171
|
+
inputParts.push("hierarchyWidget: { parentId: $parentId }");
|
|
4172
|
+
variables.parentId = parentId;
|
|
4173
|
+
}
|
|
4174
|
+
if (options.healthStatus !== undefined) {
|
|
4175
|
+
variableDefinitions.push("$healthStatus: HealthStatus");
|
|
4176
|
+
inputParts.push("healthStatusWidget: { healthStatus: $healthStatus }");
|
|
4177
|
+
variables.healthStatus = options.healthStatus;
|
|
4178
|
+
}
|
|
4179
|
+
appendDateWidget(variableDefinitions, inputParts, variables, options.startDate, options.dueDate);
|
|
4180
|
+
if (options.milestoneId !== undefined) {
|
|
4181
|
+
variableDefinitions.push("$milestoneId: MilestoneID");
|
|
4182
|
+
inputParts.push("milestoneWidget: { milestoneId: $milestoneId }");
|
|
4183
|
+
variables.milestoneId = normalizeGlobalId(options.milestoneId, "Milestone");
|
|
4184
|
+
}
|
|
4185
|
+
if (options.iterationId !== undefined) {
|
|
4186
|
+
variableDefinitions.push("$iterationId: IterationID");
|
|
4187
|
+
inputParts.push("iterationWidget: { iterationId: $iterationId }");
|
|
4188
|
+
variables.iterationId = normalizeGlobalId(options.iterationId, "Iteration");
|
|
4189
|
+
}
|
|
4190
|
+
if (options.confidential !== undefined) {
|
|
4191
|
+
variableDefinitions.push("$confidential: Boolean");
|
|
4192
|
+
inputParts.push("confidential: $confidential");
|
|
4193
|
+
variables.confidential = options.confidential;
|
|
4194
|
+
}
|
|
4195
|
+
const data = await executeGraphqlData(context, `mutation(${variableDefinitions.join(", ")}) {
|
|
4196
|
+
workItemCreate(input: { ${inputParts.join(", ")} }) {
|
|
4197
|
+
workItem { id iid title webUrl workItemType { name } }
|
|
4198
|
+
errors
|
|
4199
|
+
}
|
|
4200
|
+
}`, variables);
|
|
4201
|
+
assertNoGraphqlMutationErrors(data.workItemCreate?.errors, "Failed to create work item");
|
|
4202
|
+
const workItem = data.workItemCreate.workItem;
|
|
4203
|
+
return {
|
|
4204
|
+
id: workItem?.id,
|
|
4205
|
+
iid: workItem?.iid,
|
|
4206
|
+
title: workItem?.title,
|
|
4207
|
+
type: workItem?.workItemType?.name,
|
|
4208
|
+
webUrl: workItem?.webUrl
|
|
4209
|
+
};
|
|
4210
|
+
}
|
|
4211
|
+
function appendDateWidget(variableDefinitions, inputParts, variables, startDate, dueDate) {
|
|
4212
|
+
if (startDate === undefined && dueDate === undefined) {
|
|
4213
|
+
return;
|
|
4214
|
+
}
|
|
4215
|
+
const dateParts = [];
|
|
4216
|
+
if (startDate !== undefined) {
|
|
4217
|
+
variableDefinitions.push("$startDate: Date");
|
|
4218
|
+
dateParts.push("startDate: $startDate");
|
|
4219
|
+
variables.startDate = startDate;
|
|
4220
|
+
}
|
|
4221
|
+
if (dueDate !== undefined) {
|
|
4222
|
+
variableDefinitions.push("$dueDate: Date");
|
|
4223
|
+
dateParts.push("dueDate: $dueDate");
|
|
4224
|
+
variables.dueDate = dueDate;
|
|
4225
|
+
}
|
|
4226
|
+
inputParts.push(`startAndDueDateWidget: { ${dateParts.join(", ")} }`);
|
|
4227
|
+
}
|
|
4228
|
+
async function updateWorkItem(context, projectId, iid, options) {
|
|
4229
|
+
const { workItemGid, projectPath } = await resolveWorkItemGid(context, projectId, iid);
|
|
4230
|
+
const variableDefinitions = ["$id: WorkItemID!"];
|
|
4231
|
+
const inputParts = ["id: $id"];
|
|
4232
|
+
const variables = { id: workItemGid };
|
|
4233
|
+
if (options.title !== undefined) {
|
|
4234
|
+
variableDefinitions.push("$title: String");
|
|
4235
|
+
inputParts.push("title: $title");
|
|
4236
|
+
variables.title = options.title;
|
|
4237
|
+
}
|
|
4238
|
+
if (options.description !== undefined) {
|
|
4239
|
+
variableDefinitions.push("$description: String!");
|
|
4240
|
+
inputParts.push("descriptionWidget: { description: $description }");
|
|
4241
|
+
variables.description = options.description;
|
|
4242
|
+
}
|
|
4243
|
+
const allLabelNames = [...(options.addLabels ?? []), ...(options.removeLabels ?? [])];
|
|
4244
|
+
const needsNameResolution = allLabelNames.length > 0 || !!options.assigneeUsernames?.length;
|
|
4245
|
+
const { labelIds, userIds } = needsNameResolution
|
|
4246
|
+
? await resolveNamesToIds(context, projectPath, allLabelNames.length > 0 ? allLabelNames : undefined, options.assigneeUsernames)
|
|
4247
|
+
: { labelIds: [], userIds: [] };
|
|
4248
|
+
if (options.addLabels || options.removeLabels) {
|
|
4249
|
+
const labelParts = [];
|
|
4250
|
+
if (options.addLabels && options.addLabels.length > 0) {
|
|
4251
|
+
variableDefinitions.push("$addLabelIds: [LabelID!]");
|
|
4252
|
+
labelParts.push("addLabelIds: $addLabelIds");
|
|
4253
|
+
variables.addLabelIds = labelIds.slice(0, options.addLabels.length);
|
|
4254
|
+
}
|
|
4255
|
+
if (options.removeLabels && options.removeLabels.length > 0) {
|
|
4256
|
+
variableDefinitions.push("$removeLabelIds: [LabelID!]");
|
|
4257
|
+
labelParts.push("removeLabelIds: $removeLabelIds");
|
|
4258
|
+
variables.removeLabelIds = labelIds.slice(options.addLabels?.length ?? 0);
|
|
4259
|
+
}
|
|
4260
|
+
if (labelParts.length > 0)
|
|
4261
|
+
inputParts.push(`labelsWidget: { ${labelParts.join(", ")} }`);
|
|
4262
|
+
}
|
|
4263
|
+
if (userIds.length > 0) {
|
|
4264
|
+
variableDefinitions.push("$assigneeIds: [UserID!]!");
|
|
4265
|
+
inputParts.push("assigneesWidget: { assigneeIds: $assigneeIds }");
|
|
4266
|
+
variables.assigneeIds = userIds;
|
|
4267
|
+
}
|
|
4268
|
+
if (options.stateEvent !== undefined) {
|
|
4269
|
+
variableDefinitions.push("$stateEvent: WorkItemStateEvent");
|
|
4270
|
+
inputParts.push("stateEvent: $stateEvent");
|
|
4271
|
+
variables.stateEvent = options.stateEvent === "close" ? "CLOSE" : "REOPEN";
|
|
4272
|
+
}
|
|
4273
|
+
if (options.weight !== undefined) {
|
|
4274
|
+
variableDefinitions.push("$weight: Int");
|
|
4275
|
+
inputParts.push("weightWidget: { weight: $weight }");
|
|
4276
|
+
variables.weight = options.weight;
|
|
4277
|
+
}
|
|
4278
|
+
if (options.status !== undefined) {
|
|
4279
|
+
variableDefinitions.push("$status: WorkItemsStatusesStatusID");
|
|
4280
|
+
inputParts.push("statusWidget: { status: $status }");
|
|
4281
|
+
variables.status = options.status;
|
|
4282
|
+
}
|
|
4283
|
+
if (options.healthStatus !== undefined) {
|
|
4284
|
+
variableDefinitions.push("$healthStatus: HealthStatus");
|
|
4285
|
+
inputParts.push("healthStatusWidget: { healthStatus: $healthStatus }");
|
|
4286
|
+
variables.healthStatus = options.healthStatus;
|
|
4287
|
+
}
|
|
4288
|
+
appendDateWidget(variableDefinitions, inputParts, variables, options.startDate, options.dueDate);
|
|
4289
|
+
if (options.milestoneId !== undefined) {
|
|
4290
|
+
variableDefinitions.push("$milestoneId: MilestoneID");
|
|
4291
|
+
inputParts.push("milestoneWidget: { milestoneId: $milestoneId }");
|
|
4292
|
+
variables.milestoneId = normalizeGlobalId(options.milestoneId, "Milestone");
|
|
4293
|
+
}
|
|
4294
|
+
if (options.iterationId !== undefined) {
|
|
4295
|
+
variableDefinitions.push("$iterationId: IterationID");
|
|
4296
|
+
inputParts.push("iterationWidget: { iterationId: $iterationId }");
|
|
4297
|
+
variables.iterationId = normalizeGlobalId(options.iterationId, "Iteration");
|
|
4298
|
+
}
|
|
4299
|
+
if (options.confidential !== undefined) {
|
|
4300
|
+
variableDefinitions.push("$confidential: Boolean");
|
|
4301
|
+
inputParts.push("confidential: $confidential");
|
|
4302
|
+
variables.confidential = options.confidential;
|
|
4303
|
+
}
|
|
4304
|
+
if (options.customFields && options.customFields.length > 0) {
|
|
4305
|
+
variableDefinitions.push("$customFieldsWidget: [WorkItemWidgetCustomFieldValueInputType!]");
|
|
4306
|
+
inputParts.push("customFieldsWidget: $customFieldsWidget");
|
|
4307
|
+
variables.customFieldsWidget = options.customFields.map((field) => ({
|
|
4308
|
+
customFieldId: normalizeGlobalId(field.custom_field_id, "IssuablesCustomField"),
|
|
4309
|
+
textValue: field.text_value,
|
|
4310
|
+
numberValue: field.number_value,
|
|
4311
|
+
selectedOptionIds: field.selected_option_ids,
|
|
4312
|
+
dateValue: field.date_value
|
|
4313
|
+
}));
|
|
4314
|
+
}
|
|
4315
|
+
if (options.removeParent) {
|
|
4316
|
+
inputParts.push("hierarchyWidget: { parentId: null }");
|
|
4317
|
+
}
|
|
4318
|
+
else if (options.parentIid !== undefined) {
|
|
4319
|
+
const parentProjectId = options.parentProjectId ?? projectId;
|
|
4320
|
+
const { workItemGid: parentId } = await resolveWorkItemGid(context, parentProjectId, options.parentIid);
|
|
4321
|
+
variableDefinitions.push("$parentId: WorkItemID");
|
|
4322
|
+
inputParts.push("hierarchyWidget: { parentId: $parentId }");
|
|
4323
|
+
variables.parentId = parentId;
|
|
4324
|
+
}
|
|
4325
|
+
const data = await executeGraphqlData(context, `mutation(${variableDefinitions.join(", ")}) {
|
|
4326
|
+
workItemUpdate(input: { ${inputParts.join(", ")} }) {
|
|
4327
|
+
workItem {
|
|
4328
|
+
id iid title state webUrl workItemType { name }
|
|
4329
|
+
widgets {
|
|
4330
|
+
__typename
|
|
4331
|
+
... on WorkItemWidgetStatus { status { id name category color } }
|
|
4332
|
+
... on WorkItemWidgetLabels { labels { nodes { title } } }
|
|
4333
|
+
... on WorkItemWidgetAssignees { assignees { nodes { username } } }
|
|
4334
|
+
... on WorkItemWidgetWeight { weight }
|
|
4335
|
+
... on WorkItemWidgetHierarchy { parent { id title workItemType { name } } }
|
|
4336
|
+
... on WorkItemWidgetHealthStatus { healthStatus }
|
|
4337
|
+
... on WorkItemWidgetStartAndDueDate { startDate dueDate }
|
|
4338
|
+
... on WorkItemWidgetMilestone { milestone { id title } }
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
errors
|
|
4342
|
+
}
|
|
4343
|
+
}`, variables);
|
|
4344
|
+
assertNoGraphqlMutationErrors(data.workItemUpdate?.errors, "Failed to update work item");
|
|
4345
|
+
await updateWorkItemRelationships(context, workItemGid, options);
|
|
4346
|
+
if (options.severity !== undefined) {
|
|
4347
|
+
await updateIncidentSeverity(context, projectPath, iid, options.severity);
|
|
4348
|
+
}
|
|
4349
|
+
if (options.escalationStatus !== undefined) {
|
|
4350
|
+
await updateIncidentEscalationStatus(context, projectPath, iid, options.escalationStatus);
|
|
4351
|
+
}
|
|
4352
|
+
const workItem = data.workItemUpdate.workItem ?? {};
|
|
4353
|
+
return {
|
|
4354
|
+
...flattenWorkItemSummary(workItem),
|
|
4355
|
+
id: workItem.id,
|
|
4356
|
+
children_added: options.childrenToAdd?.length ?? 0,
|
|
4357
|
+
children_removed: options.childrenToRemove?.length ?? 0,
|
|
4358
|
+
linked_items_added: options.linkedItemsToAdd?.length ?? 0,
|
|
4359
|
+
linked_items_removed: options.linkedItemsToRemove?.length ?? 0,
|
|
4360
|
+
...(options.severity !== undefined ? { severity: options.severity } : {}),
|
|
4361
|
+
...(options.escalationStatus !== undefined
|
|
4362
|
+
? { escalation_status: options.escalationStatus }
|
|
4363
|
+
: {})
|
|
4364
|
+
};
|
|
4365
|
+
}
|
|
4366
|
+
async function updateWorkItemRelationships(context, workItemGid, options) {
|
|
4367
|
+
if (options.childrenToAdd && options.childrenToAdd.length > 0) {
|
|
4368
|
+
const childIds = [];
|
|
4369
|
+
for (const child of options.childrenToAdd) {
|
|
4370
|
+
const { workItemGid: childId } = await resolveWorkItemGid(context, child.project_id, child.iid);
|
|
4371
|
+
childIds.push(childId);
|
|
4372
|
+
}
|
|
4373
|
+
const data = await executeGraphqlData(context, `mutation($id: WorkItemID!, $childrenIds: [WorkItemID!]!) {
|
|
4374
|
+
workItemUpdate(input: { id: $id, hierarchyWidget: { childrenIds: $childrenIds } }) {
|
|
4375
|
+
errors
|
|
4376
|
+
}
|
|
4377
|
+
}`, { id: workItemGid, childrenIds: childIds });
|
|
4378
|
+
assertNoGraphqlMutationErrors(data.workItemUpdate?.errors, "Failed to add children");
|
|
4379
|
+
}
|
|
4380
|
+
if (options.childrenToRemove) {
|
|
4381
|
+
for (const child of options.childrenToRemove) {
|
|
4382
|
+
await removeWorkItemParent(context, child.project_id, child.iid);
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
if (options.linkedItemsToAdd && options.linkedItemsToAdd.length > 0) {
|
|
4386
|
+
const grouped = {};
|
|
4387
|
+
for (const item of options.linkedItemsToAdd) {
|
|
4388
|
+
const linkType = item.link_type ?? "RELATED";
|
|
4389
|
+
const { workItemGid: targetId } = await resolveWorkItemGid(context, item.project_id, item.iid);
|
|
4390
|
+
grouped[linkType] = [...(grouped[linkType] ?? []), targetId];
|
|
4391
|
+
}
|
|
4392
|
+
for (const [linkType, targetIds] of Object.entries(grouped)) {
|
|
4393
|
+
const data = await executeGraphqlData(context, `mutation($id: WorkItemID!, $workItemsIds: [WorkItemID!]!, $linkType: WorkItemRelatedLinkType!) {
|
|
4394
|
+
workItemAddLinkedItems(input: { id: $id, workItemsIds: $workItemsIds, linkType: $linkType }) {
|
|
4395
|
+
errors
|
|
4396
|
+
}
|
|
4397
|
+
}`, { id: workItemGid, workItemsIds: targetIds, linkType });
|
|
4398
|
+
assertNoGraphqlMutationErrors(data.workItemAddLinkedItems?.errors, "Failed to add linked items");
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
if (options.linkedItemsToRemove && options.linkedItemsToRemove.length > 0) {
|
|
4402
|
+
const targetIds = [];
|
|
4403
|
+
for (const item of options.linkedItemsToRemove) {
|
|
4404
|
+
const { workItemGid: targetId } = await resolveWorkItemGid(context, item.project_id, item.iid);
|
|
4405
|
+
targetIds.push(targetId);
|
|
4406
|
+
}
|
|
4407
|
+
const data = await executeGraphqlData(context, `mutation($id: WorkItemID!, $workItemsIds: [WorkItemID!]!) {
|
|
4408
|
+
workItemRemoveLinkedItems(input: { id: $id, workItemsIds: $workItemsIds }) { errors }
|
|
4409
|
+
}`, { id: workItemGid, workItemsIds: targetIds });
|
|
4410
|
+
assertNoGraphqlMutationErrors(data.workItemRemoveLinkedItems?.errors, "Failed to remove linked items");
|
|
4411
|
+
}
|
|
4412
|
+
}
|
|
4413
|
+
async function removeWorkItemParent(context, projectId, iid) {
|
|
4414
|
+
const { workItemGid } = await resolveWorkItemGid(context, projectId, iid);
|
|
4415
|
+
const data = await executeGraphqlData(context, `mutation($id: WorkItemID!) {
|
|
4416
|
+
workItemUpdate(input: { id: $id, hierarchyWidget: { parentId: null } }) { errors }
|
|
4417
|
+
}`, { id: workItemGid });
|
|
4418
|
+
assertNoGraphqlMutationErrors(data.workItemUpdate?.errors, "Failed to remove parent");
|
|
4419
|
+
}
|
|
4420
|
+
async function convertWorkItemType(context, projectId, iid, newType) {
|
|
4421
|
+
const { workItemGid, projectPath } = await resolveWorkItemGid(context, projectId, iid);
|
|
4422
|
+
const typeId = await resolveWorkItemTypeGid(context, projectPath, newType);
|
|
4423
|
+
const data = await executeGraphqlData(context, `mutation($id: WorkItemID!, $typeId: WorkItemsTypeID!) {
|
|
4424
|
+
workItemConvert(input: { id: $id, workItemTypeId: $typeId }) {
|
|
4425
|
+
workItem { id workItemType { name } }
|
|
4426
|
+
errors
|
|
4427
|
+
}
|
|
4428
|
+
}`, { id: workItemGid, typeId });
|
|
4429
|
+
assertNoGraphqlMutationErrors(data.workItemConvert?.errors, "Conversion failed");
|
|
4430
|
+
return {
|
|
4431
|
+
id: data.workItemConvert.workItem?.id,
|
|
4432
|
+
type: data.workItemConvert.workItem?.workItemType?.name
|
|
4433
|
+
};
|
|
4434
|
+
}
|
|
4435
|
+
async function listWorkItemStatuses(context, projectId, type) {
|
|
4436
|
+
const projectPath = await resolveProjectPathForWorkItem(context, projectId);
|
|
4437
|
+
const typeName = workItemTypeName(type);
|
|
4438
|
+
const data = await executeGraphqlData(context, `query($path: ID!, $typeName: IssueType) {
|
|
4439
|
+
namespace(fullPath: $path) {
|
|
4440
|
+
workItemTypes(name: $typeName) {
|
|
4441
|
+
nodes {
|
|
4442
|
+
id
|
|
4443
|
+
name
|
|
4444
|
+
supportedConversionTypes { id name }
|
|
4445
|
+
widgetDefinitions {
|
|
4446
|
+
__typename
|
|
4447
|
+
... on WorkItemWidgetDefinitionStatus {
|
|
4448
|
+
allowedStatuses { id name iconName color position }
|
|
4449
|
+
}
|
|
4450
|
+
... on WorkItemWidgetDefinitionHierarchy {
|
|
4451
|
+
allowedChildTypes { nodes { id name } }
|
|
4452
|
+
allowedParentTypes { nodes { id name } }
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
}`, { path: projectPath, typeName: typeName.replace(/ /g, "_").toUpperCase() });
|
|
4459
|
+
const typeNode = data.namespace?.workItemTypes?.nodes?.[0];
|
|
4460
|
+
if (!typeNode) {
|
|
4461
|
+
throw new Error(`Work item type '${typeName}' not found in project ${projectPath}`);
|
|
4462
|
+
}
|
|
4463
|
+
const statusWidget = typeNode.widgetDefinitions?.find((widget) => widget.__typename === "WorkItemWidgetDefinitionStatus");
|
|
4464
|
+
const hierarchyWidget = typeNode.widgetDefinitions?.find((widget) => widget.__typename === "WorkItemWidgetDefinitionHierarchy");
|
|
4465
|
+
return {
|
|
4466
|
+
work_item_type: typeNode.name,
|
|
4467
|
+
statuses_available: (statusWidget?.allowedStatuses ?? []).length > 0,
|
|
4468
|
+
statuses: statusWidget?.allowedStatuses ?? [],
|
|
4469
|
+
supported_conversion_types: (typeNode.supportedConversionTypes ?? []).map((item) => item.name),
|
|
4470
|
+
allowed_child_types: (hierarchyWidget?.allowedChildTypes?.nodes ?? []).map((item) => item.name),
|
|
4471
|
+
allowed_parent_types: (hierarchyWidget?.allowedParentTypes?.nodes ?? []).map((item) => item.name)
|
|
4472
|
+
};
|
|
4473
|
+
}
|
|
4474
|
+
async function listCustomFieldDefinitions(context, projectId, type) {
|
|
4475
|
+
const projectPath = await resolveProjectPathForWorkItem(context, projectId);
|
|
4476
|
+
const typeName = workItemTypeName(type);
|
|
4477
|
+
const data = await executeGraphqlData(context, `query($path: ID!, $typeName: IssueType) {
|
|
4478
|
+
namespace(fullPath: $path) {
|
|
4479
|
+
workItemTypes(name: $typeName) {
|
|
4480
|
+
nodes {
|
|
4481
|
+
id
|
|
4482
|
+
name
|
|
4483
|
+
widgetDefinitions {
|
|
4484
|
+
__typename
|
|
4485
|
+
... on WorkItemWidgetDefinitionCustomFields {
|
|
4486
|
+
customFieldValues {
|
|
4487
|
+
customField {
|
|
4488
|
+
id
|
|
4489
|
+
name
|
|
4490
|
+
fieldType
|
|
4491
|
+
selectOptions { id value }
|
|
4492
|
+
workItemTypes { id name }
|
|
4493
|
+
}
|
|
4494
|
+
}
|
|
4495
|
+
}
|
|
4496
|
+
}
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
}
|
|
4500
|
+
}`, { path: projectPath, typeName: typeName.replace(/ /g, "_").toUpperCase() });
|
|
4501
|
+
const typeNode = data.namespace?.workItemTypes?.nodes?.[0];
|
|
4502
|
+
if (!typeNode) {
|
|
4503
|
+
throw new Error(`Work item type '${typeName}' not found in project ${projectPath}`);
|
|
4504
|
+
}
|
|
4505
|
+
const widget = typeNode.widgetDefinitions?.find((item) => item.__typename === "WorkItemWidgetDefinitionCustomFields");
|
|
4506
|
+
return {
|
|
4507
|
+
work_item_type: typeNode.name,
|
|
4508
|
+
custom_fields: (widget?.customFieldValues ?? []).map((item) => {
|
|
4509
|
+
const field = item.customField ?? {};
|
|
4510
|
+
return {
|
|
4511
|
+
id: field.id,
|
|
4512
|
+
name: field.name,
|
|
4513
|
+
type: field.fieldType,
|
|
4514
|
+
...(field.selectOptions?.length ? { selectOptions: field.selectOptions } : {}),
|
|
4515
|
+
...(field.workItemTypes?.length
|
|
4516
|
+
? { workItemTypes: field.workItemTypes.map((workItemType) => workItemType.name) }
|
|
4517
|
+
: {})
|
|
4518
|
+
};
|
|
4519
|
+
})
|
|
4520
|
+
};
|
|
4521
|
+
}
|
|
4522
|
+
async function moveWorkItem(context, projectId, iid, targetProjectId) {
|
|
4523
|
+
const projectPath = await resolveProjectPathForWorkItem(context, projectId);
|
|
4524
|
+
const targetProjectPath = await resolveProjectPathForWorkItem(context, targetProjectId);
|
|
4525
|
+
const data = await executeGraphqlData(context, `mutation($projectPath: ID!, $iid: String!, $targetProjectPath: ID!) {
|
|
4526
|
+
issueMove(input: { projectPath: $projectPath, iid: $iid, targetProjectPath: $targetProjectPath }) {
|
|
4527
|
+
issue { id iid webUrl }
|
|
4528
|
+
errors
|
|
4529
|
+
}
|
|
4530
|
+
}`, { projectPath, iid: String(iid), targetProjectPath });
|
|
4531
|
+
assertNoGraphqlMutationErrors(data.issueMove?.errors, "Failed to move work item");
|
|
4532
|
+
return data.issueMove.issue;
|
|
4533
|
+
}
|
|
4534
|
+
async function listWorkItemNotes(context, projectId, iid, options) {
|
|
4535
|
+
const projectPath = await resolveProjectPathForWorkItem(context, projectId);
|
|
4536
|
+
const data = await executeGraphqlData(context, `query($path: ID!, $iid: String!, $pageSize: Int, $after: String, $sort: WorkItemDiscussionsSort) {
|
|
4537
|
+
namespace(fullPath: $path) {
|
|
4538
|
+
workItem(iid: $iid) {
|
|
4539
|
+
id
|
|
4540
|
+
widgets(onlyTypes: [NOTES]) {
|
|
4541
|
+
... on WorkItemWidgetNotes {
|
|
4542
|
+
discussionLocked
|
|
4543
|
+
discussions(first: $pageSize, after: $after, filter: ALL_NOTES, sort: $sort) {
|
|
4544
|
+
pageInfo { hasNextPage endCursor }
|
|
4545
|
+
nodes {
|
|
4546
|
+
id
|
|
4547
|
+
resolved
|
|
4548
|
+
resolvable
|
|
4549
|
+
notes {
|
|
4550
|
+
nodes {
|
|
4551
|
+
id
|
|
4552
|
+
body
|
|
4553
|
+
system
|
|
4554
|
+
internal
|
|
4555
|
+
createdAt
|
|
4556
|
+
lastEditedAt
|
|
4557
|
+
author { username }
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
}
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4566
|
+
}`, {
|
|
4567
|
+
path: projectPath,
|
|
4568
|
+
iid: String(iid),
|
|
4569
|
+
pageSize: options.pageSize ?? 20,
|
|
4570
|
+
after: options.after ?? null,
|
|
4571
|
+
sort: options.sort ?? "CREATED_ASC"
|
|
4572
|
+
});
|
|
4573
|
+
const workItem = data.namespace?.workItem;
|
|
4574
|
+
if (!workItem) {
|
|
4575
|
+
throw new Error(`Work item #${iid} not found in project ${projectPath}`);
|
|
4576
|
+
}
|
|
4577
|
+
const notesWidget = (workItem.widgets ?? []).find((widget) => widget.discussions);
|
|
4578
|
+
const discussions = notesWidget?.discussions;
|
|
4579
|
+
return {
|
|
4580
|
+
discussions: (discussions?.nodes ?? []).map((discussion) => ({
|
|
4581
|
+
id: discussion.id,
|
|
4582
|
+
resolved: discussion.resolved,
|
|
4583
|
+
resolvable: discussion.resolvable,
|
|
4584
|
+
notes: (discussion.notes?.nodes ?? []).map((note) => ({
|
|
4585
|
+
id: note.id,
|
|
4586
|
+
author: note.author?.username,
|
|
4587
|
+
body: note.body,
|
|
4588
|
+
createdAt: note.createdAt,
|
|
4589
|
+
...(note.system ? { system: true } : {}),
|
|
4590
|
+
...(note.internal ? { internal: true } : {}),
|
|
4591
|
+
...(note.lastEditedAt ? { lastEditedAt: note.lastEditedAt } : {})
|
|
4592
|
+
}))
|
|
4593
|
+
})),
|
|
4594
|
+
pageInfo: discussions?.pageInfo ?? {}
|
|
4595
|
+
};
|
|
4596
|
+
}
|
|
4597
|
+
async function resolveWorkItemNoteAwardableId(context, projectId, iid, noteId) {
|
|
4598
|
+
let after;
|
|
4599
|
+
for (let page = 0; page < 100; page += 1) {
|
|
4600
|
+
const result = (await listWorkItemNotes(context, projectId, iid, {
|
|
4601
|
+
pageSize: 100,
|
|
4602
|
+
after,
|
|
4603
|
+
sort: "CREATED_ASC"
|
|
4604
|
+
}));
|
|
4605
|
+
const found = (result.discussions ?? []).some((discussion) => (discussion.notes ?? []).some((note) => note.id === noteId));
|
|
4606
|
+
if (found) {
|
|
4607
|
+
return noteId;
|
|
4608
|
+
}
|
|
4609
|
+
if (result.pageInfo?.hasNextPage !== true || typeof result.pageInfo.endCursor !== "string") {
|
|
4610
|
+
break;
|
|
4611
|
+
}
|
|
4612
|
+
after = result.pageInfo.endCursor;
|
|
4613
|
+
}
|
|
4614
|
+
throw new Error(`Note '${noteId}' was not found on work item #${iid} in project ${projectId}`);
|
|
4615
|
+
}
|
|
4616
|
+
async function createWorkItemNote(context, projectId, iid, body, options) {
|
|
4617
|
+
const { workItemGid } = await resolveWorkItemGid(context, projectId, iid);
|
|
4618
|
+
const variableDefinitions = ["$noteableId: NoteableID!", "$body: String!"];
|
|
4619
|
+
const inputParts = ["noteableId: $noteableId", "body: $body"];
|
|
4620
|
+
const variables = { noteableId: workItemGid, body };
|
|
4621
|
+
if (options.internal) {
|
|
4622
|
+
variableDefinitions.push("$internal: Boolean");
|
|
4623
|
+
inputParts.push("internal: $internal");
|
|
4624
|
+
variables.internal = true;
|
|
4625
|
+
}
|
|
4626
|
+
if (options.discussionId) {
|
|
4627
|
+
variableDefinitions.push("$discussionId: DiscussionID");
|
|
4628
|
+
inputParts.push("discussionId: $discussionId");
|
|
4629
|
+
variables.discussionId = options.discussionId;
|
|
4630
|
+
}
|
|
4631
|
+
const data = await executeGraphqlData(context, `mutation(${variableDefinitions.join(", ")}) {
|
|
4632
|
+
createNote(input: { ${inputParts.join(", ")} }) {
|
|
4633
|
+
note { id body discussion { id } }
|
|
4634
|
+
errors
|
|
4635
|
+
}
|
|
4636
|
+
}`, variables);
|
|
4637
|
+
assertNoGraphqlMutationErrors(data.createNote?.errors, "Failed to create note");
|
|
4638
|
+
return data.createNote.note;
|
|
4639
|
+
}
|
|
4640
|
+
async function addGraphqlAwardEmoji(context, awardableId, name) {
|
|
4641
|
+
const data = await executeGraphqlData(context, `mutation($awardableId: AwardableID!, $name: String!) {
|
|
4642
|
+
awardEmojiAdd(input: { awardableId: $awardableId, name: $name }) {
|
|
4643
|
+
awardEmoji { name user { username } }
|
|
4644
|
+
errors
|
|
4645
|
+
}
|
|
4646
|
+
}`, { awardableId, name });
|
|
4647
|
+
assertNoGraphqlMutationErrors(data.awardEmojiAdd?.errors, "Failed to add emoji reaction");
|
|
4648
|
+
return data.awardEmojiAdd.awardEmoji;
|
|
4649
|
+
}
|
|
4650
|
+
async function listGraphqlAwardEmoji(context, awardableId) {
|
|
4651
|
+
const data = await executeGraphqlData(context, `query($id: AwardableID!) {
|
|
4652
|
+
awardable(id: $id) {
|
|
4653
|
+
awardEmoji { nodes { name user { username } } }
|
|
4654
|
+
}
|
|
4655
|
+
}`, { id: awardableId });
|
|
4656
|
+
return data.awardable?.awardEmoji?.nodes ?? [];
|
|
4657
|
+
}
|
|
4658
|
+
async function removeGraphqlAwardEmoji(context, awardableId, name) {
|
|
4659
|
+
const data = await executeGraphqlData(context, `mutation($awardableId: AwardableID!, $name: String!) {
|
|
4660
|
+
awardEmojiRemove(input: { awardableId: $awardableId, name: $name }) {
|
|
4661
|
+
awardEmoji { name }
|
|
4662
|
+
errors
|
|
4663
|
+
}
|
|
4664
|
+
}`, { awardableId, name });
|
|
4665
|
+
assertNoGraphqlMutationErrors(data.awardEmojiRemove?.errors, "Failed to remove emoji reaction");
|
|
4666
|
+
return data.awardEmojiRemove.awardEmoji ?? { status: "success" };
|
|
4667
|
+
}
|
|
4668
|
+
async function getTimelineEvents(context, projectId, incidentIid) {
|
|
4669
|
+
const { workItemGid, projectPath } = await resolveWorkItemGid(context, projectId, incidentIid);
|
|
4670
|
+
const incidentId = workItemGid.replace("/WorkItem/", "/Issue/");
|
|
4671
|
+
const data = await executeGraphqlData(context, `query($fullPath: ID!, $incidentId: IssueID!) {
|
|
4672
|
+
project(fullPath: $fullPath) {
|
|
4673
|
+
incidentManagementTimelineEvents(incidentId: $incidentId) {
|
|
4674
|
+
nodes {
|
|
4675
|
+
id
|
|
4676
|
+
note
|
|
4677
|
+
noteHtml
|
|
4678
|
+
action
|
|
4679
|
+
occurredAt
|
|
4680
|
+
createdAt
|
|
4681
|
+
timelineEventTags { nodes { id name } }
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4684
|
+
}
|
|
4685
|
+
}`, { fullPath: projectPath, incidentId });
|
|
4686
|
+
return (data.project?.incidentManagementTimelineEvents?.nodes ?? []).map((event) => ({
|
|
4687
|
+
id: event.id,
|
|
4688
|
+
note: event.note,
|
|
4689
|
+
action: event.action,
|
|
4690
|
+
occurredAt: event.occurredAt,
|
|
4691
|
+
createdAt: event.createdAt,
|
|
4692
|
+
...(event.noteHtml ? { noteHtml: event.noteHtml } : {}),
|
|
4693
|
+
...(event.timelineEventTags?.nodes?.length
|
|
4694
|
+
? { tags: event.timelineEventTags.nodes.map((tag) => tag.name) }
|
|
4695
|
+
: {})
|
|
4696
|
+
}));
|
|
4697
|
+
}
|
|
4698
|
+
async function createTimelineEvent(context, projectId, incidentIid, note, occurredAt, tagNames) {
|
|
4699
|
+
const { workItemGid } = await resolveWorkItemGid(context, projectId, incidentIid);
|
|
4700
|
+
const incidentId = workItemGid.replace("/WorkItem/", "/Issue/");
|
|
4701
|
+
const input = { incidentId, note, occurredAt };
|
|
4702
|
+
if (tagNames && tagNames.length > 0) {
|
|
4703
|
+
input.timelineEventTagNames = tagNames;
|
|
4704
|
+
}
|
|
4705
|
+
const data = await executeGraphqlData(context, `mutation CreateTimelineEvent($input: TimelineEventCreateInput!) {
|
|
4706
|
+
timelineEventCreate(input: $input) {
|
|
4707
|
+
timelineEvent {
|
|
4708
|
+
id
|
|
4709
|
+
note
|
|
4710
|
+
noteHtml
|
|
4711
|
+
action
|
|
4712
|
+
occurredAt
|
|
4713
|
+
createdAt
|
|
4714
|
+
timelineEventTags { nodes { id name } }
|
|
4715
|
+
}
|
|
4716
|
+
errors
|
|
4717
|
+
}
|
|
4718
|
+
}`, { input });
|
|
4719
|
+
assertNoGraphqlMutationErrors(data.timelineEventCreate?.errors, "Failed to create timeline event");
|
|
4720
|
+
return data.timelineEventCreate.timelineEvent;
|
|
4721
|
+
}
|
|
4722
|
+
async function updateIncidentSeverity(context, projectPath, incidentIid, severity) {
|
|
4723
|
+
const data = await executeGraphqlData(context, `mutation($projectPath: ID!, $severity: IssuableSeverity!, $iid: String!) {
|
|
4724
|
+
issueSetSeverity(input: { iid: $iid, severity: $severity, projectPath: $projectPath }) {
|
|
4725
|
+
errors
|
|
4726
|
+
}
|
|
4727
|
+
}`, { projectPath, severity, iid: String(incidentIid) });
|
|
4728
|
+
assertNoGraphqlMutationErrors(data.issueSetSeverity?.errors, "Failed to set severity");
|
|
4729
|
+
}
|
|
4730
|
+
async function updateIncidentEscalationStatus(context, projectPath, incidentIid, status) {
|
|
4731
|
+
const data = await executeGraphqlData(context, `mutation($projectPath: ID!, $status: IssueEscalationStatus!, $iid: String!) {
|
|
4732
|
+
issueSetEscalationStatus(input: { projectPath: $projectPath, status: $status, iid: $iid }) {
|
|
4733
|
+
errors
|
|
4734
|
+
}
|
|
4735
|
+
}`, { projectPath, status, iid: String(incidentIid) });
|
|
4736
|
+
assertNoGraphqlMutationErrors(data.issueSetEscalationStatus?.errors, "Failed to set escalation status");
|
|
4737
|
+
}
|
|
4738
|
+
function assertNoGraphqlMutationErrors(errors, prefix) {
|
|
4739
|
+
if (errors && errors.length > 0) {
|
|
4740
|
+
throw new Error(`${prefix}: ${errors.join(", ")}`);
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4743
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
2399
4744
|
export function containsGraphqlMutation(query) {
|
|
2400
4745
|
if (!query.trim()) {
|
|
2401
4746
|
return false;
|
|
@@ -2459,7 +4804,25 @@ export function shouldDisableGraphqlTools(allowedProjectIds, allowGraphqlWithPro
|
|
|
2459
4804
|
function isGraphqlToolName(name) {
|
|
2460
4805
|
return (name === "gitlab_execute_graphql_query" ||
|
|
2461
4806
|
name === "gitlab_execute_graphql_mutation" ||
|
|
2462
|
-
name === "gitlab_execute_graphql"
|
|
4807
|
+
name === "gitlab_execute_graphql" ||
|
|
4808
|
+
name === "gitlab_get_work_item" ||
|
|
4809
|
+
name === "gitlab_list_work_items" ||
|
|
4810
|
+
name === "gitlab_create_work_item" ||
|
|
4811
|
+
name === "gitlab_update_work_item" ||
|
|
4812
|
+
name === "gitlab_convert_work_item_type" ||
|
|
4813
|
+
name === "gitlab_list_work_item_statuses" ||
|
|
4814
|
+
name === "gitlab_list_custom_field_definitions" ||
|
|
4815
|
+
name === "gitlab_move_work_item" ||
|
|
4816
|
+
name === "gitlab_list_work_item_notes" ||
|
|
4817
|
+
name === "gitlab_create_work_item_note" ||
|
|
4818
|
+
name === "gitlab_list_work_item_emoji_reactions" ||
|
|
4819
|
+
name === "gitlab_list_work_item_note_emoji_reactions" ||
|
|
4820
|
+
name === "gitlab_create_work_item_emoji_reaction" ||
|
|
4821
|
+
name === "gitlab_delete_work_item_emoji_reaction" ||
|
|
4822
|
+
name === "gitlab_create_work_item_note_emoji_reaction" ||
|
|
4823
|
+
name === "gitlab_delete_work_item_note_emoji_reaction" ||
|
|
4824
|
+
name === "gitlab_get_timeline_events" ||
|
|
4825
|
+
name === "gitlab_create_timeline_event");
|
|
2463
4826
|
}
|
|
2464
4827
|
function resolveProjectId(args, context, required) {
|
|
2465
4828
|
const fromArgs = getOptionalString(args, "project_id");
|
|
@@ -2481,6 +4844,69 @@ function resolveProjectId(args, context, required) {
|
|
|
2481
4844
|
}
|
|
2482
4845
|
return fromArgs ?? "";
|
|
2483
4846
|
}
|
|
4847
|
+
async function withCiLintHttpDiagnostics(operation) {
|
|
4848
|
+
try {
|
|
4849
|
+
return await operation();
|
|
4850
|
+
}
|
|
4851
|
+
catch (error) {
|
|
4852
|
+
const lintResult = toCiLintHttpDiagnosticResult(error);
|
|
4853
|
+
if (lintResult) {
|
|
4854
|
+
return lintResult;
|
|
4855
|
+
}
|
|
4856
|
+
throw error;
|
|
4857
|
+
}
|
|
4858
|
+
}
|
|
4859
|
+
function toCiLintHttpDiagnosticResult(error) {
|
|
4860
|
+
if (!(error instanceof GitLabApiError) || error.status !== 400) {
|
|
4861
|
+
return undefined;
|
|
4862
|
+
}
|
|
4863
|
+
const details = redactSensitive(error.details);
|
|
4864
|
+
const errors = extractCiLintMessages(details);
|
|
4865
|
+
if (errors.length === 0 || !hasCiLintDiagnosticSignal(details, errors)) {
|
|
4866
|
+
return undefined;
|
|
4867
|
+
}
|
|
4868
|
+
const result = typeof details === "object" && details !== null && !Array.isArray(details)
|
|
4869
|
+
? { ...details }
|
|
4870
|
+
: {};
|
|
4871
|
+
return {
|
|
4872
|
+
...result,
|
|
4873
|
+
valid: false,
|
|
4874
|
+
errors,
|
|
4875
|
+
warnings: Array.isArray(result.warnings) ? result.warnings : [],
|
|
4876
|
+
status: error.status
|
|
4877
|
+
};
|
|
4878
|
+
}
|
|
4879
|
+
function extractCiLintMessages(value) {
|
|
4880
|
+
if (typeof value === "string") {
|
|
4881
|
+
return [value];
|
|
4882
|
+
}
|
|
4883
|
+
if (Array.isArray(value)) {
|
|
4884
|
+
return value.flatMap((item) => extractCiLintMessages(item));
|
|
4885
|
+
}
|
|
4886
|
+
if (typeof value !== "object" || value === null) {
|
|
4887
|
+
return [];
|
|
4888
|
+
}
|
|
4889
|
+
const record = value;
|
|
4890
|
+
const messages = [];
|
|
4891
|
+
for (const key of ["errors", "message", "error"]) {
|
|
4892
|
+
messages.push(...extractCiLintMessages(record[key]));
|
|
4893
|
+
}
|
|
4894
|
+
return [...new Set(messages.map((item) => item.trim()).filter((item) => item.length > 0))];
|
|
4895
|
+
}
|
|
4896
|
+
function hasCiLintDiagnosticSignal(details, messages) {
|
|
4897
|
+
if (typeof details === "object" && details !== null && !Array.isArray(details)) {
|
|
4898
|
+
const record = details;
|
|
4899
|
+
if (record.valid === false ||
|
|
4900
|
+
record.status === "invalid" ||
|
|
4901
|
+
hasNonEmptyCiLintErrorsArray(record)) {
|
|
4902
|
+
return true;
|
|
4903
|
+
}
|
|
4904
|
+
}
|
|
4905
|
+
return messages.some((message) => /gitlab ci configuration is invalid|jobs config|ci config|config should contain/i.test(message));
|
|
4906
|
+
}
|
|
4907
|
+
function hasNonEmptyCiLintErrorsArray(record) {
|
|
4908
|
+
return Array.isArray(record.errors) && extractCiLintMessages(record.errors).length > 0;
|
|
4909
|
+
}
|
|
2484
4910
|
function toToolError(error, context) {
|
|
2485
4911
|
const detailMode = context?.env.GITLAB_ERROR_DETAIL_MODE ?? "full";
|
|
2486
4912
|
if (error instanceof GitLabApiError) {
|
|
@@ -2542,6 +4968,20 @@ function toStructuredContent(value) {
|
|
|
2542
4968
|
value
|
|
2543
4969
|
};
|
|
2544
4970
|
}
|
|
4971
|
+
function maybeDecodeRepositoryFileContents(value, shouldDecode) {
|
|
4972
|
+
if (!shouldDecode || typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
4973
|
+
return value;
|
|
4974
|
+
}
|
|
4975
|
+
const file = value;
|
|
4976
|
+
if (file.encoding !== "base64" || typeof file.content !== "string") {
|
|
4977
|
+
return value;
|
|
4978
|
+
}
|
|
4979
|
+
return {
|
|
4980
|
+
...file,
|
|
4981
|
+
content: Buffer.from(file.content, "base64").toString("utf8"),
|
|
4982
|
+
encoding: "utf8"
|
|
4983
|
+
};
|
|
4984
|
+
}
|
|
2545
4985
|
function omit(args, keys) {
|
|
2546
4986
|
const result = {};
|
|
2547
4987
|
for (const [key, value] of Object.entries(args)) {
|
|
@@ -2580,6 +5020,88 @@ function toCsvValue(value) {
|
|
|
2580
5020
|
}
|
|
2581
5021
|
return undefined;
|
|
2582
5022
|
}
|
|
5023
|
+
async function resolveMergeRequestIid(args, context, projectId, options) {
|
|
5024
|
+
const mergeRequestIid = getOptionalString(args, "merge_request_iid");
|
|
5025
|
+
if (mergeRequestIid) {
|
|
5026
|
+
return mergeRequestIid;
|
|
5027
|
+
}
|
|
5028
|
+
const sourceBranch = getOptionalString(args, "source_branch");
|
|
5029
|
+
if (!sourceBranch) {
|
|
5030
|
+
throw new Error("Either merge_request_iid or source_branch must be provided");
|
|
5031
|
+
}
|
|
5032
|
+
const candidates = await context.gitlab.listMergeRequests(projectId, {
|
|
5033
|
+
query: {
|
|
5034
|
+
source_branch: sourceBranch,
|
|
5035
|
+
per_page: 100,
|
|
5036
|
+
page: 1
|
|
5037
|
+
}
|
|
5038
|
+
});
|
|
5039
|
+
const match = pickMergeRequestForSourceBranch(candidates, sourceBranch, options);
|
|
5040
|
+
return getMergeRequestIid(match);
|
|
5041
|
+
}
|
|
5042
|
+
function extractMergeRequestChanges(value) {
|
|
5043
|
+
if (typeof value !== "object" || value === null || !("changes" in value)) {
|
|
5044
|
+
return [];
|
|
5045
|
+
}
|
|
5046
|
+
const changes = value.changes;
|
|
5047
|
+
return extractMergeRequestDiffRecords(changes);
|
|
5048
|
+
}
|
|
5049
|
+
function extractMergeRequestDiffRecords(value) {
|
|
5050
|
+
if (!Array.isArray(value)) {
|
|
5051
|
+
return [];
|
|
5052
|
+
}
|
|
5053
|
+
return value.filter((item) => typeof item === "object" && item !== null);
|
|
5054
|
+
}
|
|
5055
|
+
function filterChangedFiles(files, patterns) {
|
|
5056
|
+
if (!patterns || patterns.length === 0) {
|
|
5057
|
+
return files;
|
|
5058
|
+
}
|
|
5059
|
+
const regexes = patterns.map((pattern) => new RegExp(pattern));
|
|
5060
|
+
return files.filter((file) => {
|
|
5061
|
+
const paths = [file.new_path, file.old_path].filter((value) => typeof value === "string");
|
|
5062
|
+
return !regexes.some((regex) => paths.some((filePath) => regex.test(filePath)));
|
|
5063
|
+
});
|
|
5064
|
+
}
|
|
5065
|
+
function resolveWebhookScope(args) {
|
|
5066
|
+
const projectId = getOptionalString(args, "project_id");
|
|
5067
|
+
const groupId = getOptionalString(args, "group_id");
|
|
5068
|
+
if ((projectId ? 1 : 0) + (groupId ? 1 : 0) !== 1) {
|
|
5069
|
+
throw new Error("Provide exactly one of project_id or group_id");
|
|
5070
|
+
}
|
|
5071
|
+
return projectId ? { projectId } : { groupId };
|
|
5072
|
+
}
|
|
5073
|
+
function summarizeWebhookEvents(events) {
|
|
5074
|
+
return events.map((event) => ({
|
|
5075
|
+
id: event.id,
|
|
5076
|
+
url: event.url,
|
|
5077
|
+
trigger: event.trigger,
|
|
5078
|
+
response_status: event.response_status,
|
|
5079
|
+
execution_duration: event.execution_duration
|
|
5080
|
+
}));
|
|
5081
|
+
}
|
|
5082
|
+
async function findWebhookEvent(context, scope, hookId, eventId, page) {
|
|
5083
|
+
const perPage = 20;
|
|
5084
|
+
const pages = page ? [page] : Array.from({ length: 25 }, (_value, index) => index + 1);
|
|
5085
|
+
for (const currentPage of pages) {
|
|
5086
|
+
const events = extractRecords(await context.gitlab.listWebhookEvents(scope, hookId, {
|
|
5087
|
+
query: { page: currentPage, per_page: perPage }
|
|
5088
|
+
}));
|
|
5089
|
+
const match = events.find((event) => String(event.id) === eventId);
|
|
5090
|
+
if (match) {
|
|
5091
|
+
return match;
|
|
5092
|
+
}
|
|
5093
|
+
if (events.length < perPage) {
|
|
5094
|
+
break;
|
|
5095
|
+
}
|
|
5096
|
+
}
|
|
5097
|
+
return undefined;
|
|
5098
|
+
}
|
|
5099
|
+
function extractRecords(value) {
|
|
5100
|
+
if (!Array.isArray(value)) {
|
|
5101
|
+
return [];
|
|
5102
|
+
}
|
|
5103
|
+
return value.filter((item) => typeof item === "object" && item !== null);
|
|
5104
|
+
}
|
|
2583
5105
|
function pickMergeRequestForSourceBranch(value, sourceBranch, options) {
|
|
2584
5106
|
const matches = extractMergeRequestRecords(value).filter((item) => {
|
|
2585
5107
|
const candidateBranch = item.source_branch;
|
|
@@ -2625,6 +5147,122 @@ function getMergeRequestIid(mergeRequest) {
|
|
|
2625
5147
|
}
|
|
2626
5148
|
throw new Error("Matched merge request is missing a valid iid");
|
|
2627
5149
|
}
|
|
5150
|
+
async function getDetailedMergeRequestFromMatch(projectId, mergeRequest, context) {
|
|
5151
|
+
if (typeof context.gitlab.getMergeRequest !== "function") {
|
|
5152
|
+
return mergeRequest;
|
|
5153
|
+
}
|
|
5154
|
+
return context.gitlab.getMergeRequest(projectId, getMergeRequestIid(mergeRequest));
|
|
5155
|
+
}
|
|
5156
|
+
async function withMergeRequestSummaries(projectId, mergeRequest, context) {
|
|
5157
|
+
if (typeof mergeRequest !== "object" || mergeRequest === null || Array.isArray(mergeRequest)) {
|
|
5158
|
+
return mergeRequest;
|
|
5159
|
+
}
|
|
5160
|
+
const record = mergeRequest;
|
|
5161
|
+
const [commitAdditionSummary, approvalSummary] = await Promise.all([
|
|
5162
|
+
buildMergeRequestCommitAdditionSummary(projectId, record, context),
|
|
5163
|
+
buildMergeRequestApprovalSummary(projectId, record, context)
|
|
5164
|
+
]);
|
|
5165
|
+
return {
|
|
5166
|
+
...record,
|
|
5167
|
+
commit_addition_summary: commitAdditionSummary,
|
|
5168
|
+
approval_summary: approvalSummary
|
|
5169
|
+
};
|
|
5170
|
+
}
|
|
5171
|
+
async function buildMergeRequestCommitAdditionSummary(projectId, mergeRequest, context) {
|
|
5172
|
+
const targetBranch = typeof mergeRequest.target_branch === "string" ? mergeRequest.target_branch : null;
|
|
5173
|
+
try {
|
|
5174
|
+
const sourceCommitCount = await context.gitlab.countMergeRequestCommits(projectId, getMergeRequestIid(mergeRequest));
|
|
5175
|
+
const project = (await context.gitlab.getProject(projectId));
|
|
5176
|
+
const mergeMethod = typeof project.merge_method === "string" ? project.merge_method : null;
|
|
5177
|
+
const mergeCommitCount = estimateMergeCommitCount(mergeMethod, sourceCommitCount);
|
|
5178
|
+
const summary = targetBranch && mergeCommitCount !== null
|
|
5179
|
+
? `${sourceCommitCount} commits and ${mergeCommitCount} merge commit${mergeCommitCount === 1 ? "" : "s"} will be added to ${targetBranch}.`
|
|
5180
|
+
: null;
|
|
5181
|
+
return {
|
|
5182
|
+
target_branch: targetBranch,
|
|
5183
|
+
source_commits_count: sourceCommitCount,
|
|
5184
|
+
merge_method: mergeMethod,
|
|
5185
|
+
merge_commit_count: mergeCommitCount,
|
|
5186
|
+
summary
|
|
5187
|
+
};
|
|
5188
|
+
}
|
|
5189
|
+
catch (error) {
|
|
5190
|
+
return {
|
|
5191
|
+
target_branch: targetBranch,
|
|
5192
|
+
source_commits_count: null,
|
|
5193
|
+
merge_method: null,
|
|
5194
|
+
merge_commit_count: null,
|
|
5195
|
+
summary: null,
|
|
5196
|
+
unavailable_reason: error instanceof Error ? error.message : String(error)
|
|
5197
|
+
};
|
|
5198
|
+
}
|
|
5199
|
+
}
|
|
5200
|
+
function estimateMergeCommitCount(mergeMethod, sourceCommitCount) {
|
|
5201
|
+
if (sourceCommitCount === 0) {
|
|
5202
|
+
return 0;
|
|
5203
|
+
}
|
|
5204
|
+
if (mergeMethod === "merge") {
|
|
5205
|
+
return 1;
|
|
5206
|
+
}
|
|
5207
|
+
if (mergeMethod === "ff" || mergeMethod === "rebase_merge") {
|
|
5208
|
+
return 0;
|
|
5209
|
+
}
|
|
5210
|
+
return null;
|
|
5211
|
+
}
|
|
5212
|
+
async function buildMergeRequestApprovalSummary(projectId, mergeRequest, context) {
|
|
5213
|
+
try {
|
|
5214
|
+
const approvalState = (await context.gitlab.getMergeRequestApprovalState(projectId, getMergeRequestIid(mergeRequest)));
|
|
5215
|
+
const approvedBy = extractRecords(approvalState.approved_by);
|
|
5216
|
+
const approvedByUsernames = getApprovalUsernames(approvalState, approvedBy);
|
|
5217
|
+
const rules = extractRecords(approvalState.rules);
|
|
5218
|
+
return {
|
|
5219
|
+
approved: typeof approvalState.approved === "boolean"
|
|
5220
|
+
? approvalState.approved
|
|
5221
|
+
: inferMergeRequestApproved(rules),
|
|
5222
|
+
user_has_approved: typeof approvalState.user_has_approved === "boolean"
|
|
5223
|
+
? approvalState.user_has_approved
|
|
5224
|
+
: null,
|
|
5225
|
+
user_can_approve: typeof approvalState.user_can_approve === "boolean" ? approvalState.user_can_approve : null,
|
|
5226
|
+
approved_by: approvedBy,
|
|
5227
|
+
approved_by_usernames: approvedByUsernames,
|
|
5228
|
+
rules_count: Array.isArray(approvalState.rules) ? approvalState.rules.length : null,
|
|
5229
|
+
source_endpoint: approvalState.source_endpoint === "approval_state" ||
|
|
5230
|
+
approvalState.source_endpoint === "approvals"
|
|
5231
|
+
? approvalState.source_endpoint
|
|
5232
|
+
: null
|
|
5233
|
+
};
|
|
5234
|
+
}
|
|
5235
|
+
catch (error) {
|
|
5236
|
+
return {
|
|
5237
|
+
approved: null,
|
|
5238
|
+
user_has_approved: null,
|
|
5239
|
+
user_can_approve: null,
|
|
5240
|
+
approved_by: [],
|
|
5241
|
+
approved_by_usernames: [],
|
|
5242
|
+
rules_count: null,
|
|
5243
|
+
source_endpoint: null,
|
|
5244
|
+
unavailable_reason: error instanceof Error ? error.message : String(error)
|
|
5245
|
+
};
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
5248
|
+
function getApprovalUsernames(approvalState, approvedBy) {
|
|
5249
|
+
const explicit = approvalState.approved_by_usernames;
|
|
5250
|
+
if (Array.isArray(explicit) && explicit.every((item) => typeof item === "string")) {
|
|
5251
|
+
return explicit;
|
|
5252
|
+
}
|
|
5253
|
+
return approvedBy
|
|
5254
|
+
.map((user) => user.username)
|
|
5255
|
+
.filter((username) => typeof username === "string");
|
|
5256
|
+
}
|
|
5257
|
+
function inferMergeRequestApproved(rules) {
|
|
5258
|
+
if (rules.length === 0) {
|
|
5259
|
+
return null;
|
|
5260
|
+
}
|
|
5261
|
+
if (rules.some((rule) => typeof rule.approved !== "boolean")) {
|
|
5262
|
+
return null;
|
|
5263
|
+
}
|
|
5264
|
+
return rules.every((rule) => rule.approved === true);
|
|
5265
|
+
}
|
|
2628
5266
|
function getString(args, key) {
|
|
2629
5267
|
const value = args[key];
|
|
2630
5268
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -2632,6 +5270,13 @@ function getString(args, key) {
|
|
|
2632
5270
|
}
|
|
2633
5271
|
return value;
|
|
2634
5272
|
}
|
|
5273
|
+
function getIdString(args, key) {
|
|
5274
|
+
const value = args[key];
|
|
5275
|
+
if ((typeof value !== "string" && typeof value !== "number") || String(value).length === 0) {
|
|
5276
|
+
throw new Error(`'${key}' must be a non-empty string or number`);
|
|
5277
|
+
}
|
|
5278
|
+
return String(value);
|
|
5279
|
+
}
|
|
2635
5280
|
function getOptionalString(args, key) {
|
|
2636
5281
|
const value = args[key];
|
|
2637
5282
|
if (value === undefined) {
|
|
@@ -2659,6 +5304,21 @@ function getBoolean(args, key) {
|
|
|
2659
5304
|
}
|
|
2660
5305
|
return value;
|
|
2661
5306
|
}
|
|
5307
|
+
function getNumber(args, key) {
|
|
5308
|
+
const value = args[key];
|
|
5309
|
+
const numericValue = typeof value === "string" ? Number(value) : value;
|
|
5310
|
+
if (typeof numericValue !== "number" || Number.isNaN(numericValue)) {
|
|
5311
|
+
throw new Error(`'${key}' must be number`);
|
|
5312
|
+
}
|
|
5313
|
+
return numericValue;
|
|
5314
|
+
}
|
|
5315
|
+
function getRequiredStringArray(args, key) {
|
|
5316
|
+
const value = getOptionalStringArray(args, key);
|
|
5317
|
+
if (!value || value.length === 0) {
|
|
5318
|
+
throw new Error(`'${key}' must be a non-empty string array`);
|
|
5319
|
+
}
|
|
5320
|
+
return value;
|
|
5321
|
+
}
|
|
2662
5322
|
function getOptionalBoolean(args, key) {
|
|
2663
5323
|
const value = args[key];
|
|
2664
5324
|
if (value === undefined) {
|
|
@@ -2689,6 +5349,47 @@ function getOptionalArray(args, key) {
|
|
|
2689
5349
|
}
|
|
2690
5350
|
return value;
|
|
2691
5351
|
}
|
|
5352
|
+
function getWorkItemReferences(args, key, context) {
|
|
5353
|
+
const values = getOptionalArray(args, key);
|
|
5354
|
+
if (!values) {
|
|
5355
|
+
return undefined;
|
|
5356
|
+
}
|
|
5357
|
+
return values.map((value) => {
|
|
5358
|
+
if (typeof value !== "object" || value === null) {
|
|
5359
|
+
throw new Error(`'${key}' must contain objects`);
|
|
5360
|
+
}
|
|
5361
|
+
const record = value;
|
|
5362
|
+
if (typeof record.project_id !== "string") {
|
|
5363
|
+
throw new Error(`'${key}.project_id' must be string`);
|
|
5364
|
+
}
|
|
5365
|
+
const iid = typeof record.iid === "string" ? Number(record.iid) : record.iid;
|
|
5366
|
+
if (typeof iid !== "number" || Number.isNaN(iid)) {
|
|
5367
|
+
throw new Error(`'${key}.iid' must be number`);
|
|
5368
|
+
}
|
|
5369
|
+
return {
|
|
5370
|
+
project_id: resolveExplicitProjectId(context, record.project_id),
|
|
5371
|
+
iid
|
|
5372
|
+
};
|
|
5373
|
+
});
|
|
5374
|
+
}
|
|
5375
|
+
function getLinkedWorkItemReferences(args, key, context) {
|
|
5376
|
+
const references = getWorkItemReferences(args, key, context);
|
|
5377
|
+
const rawValues = getOptionalArray(args, key);
|
|
5378
|
+
if (!references || !rawValues) {
|
|
5379
|
+
return references;
|
|
5380
|
+
}
|
|
5381
|
+
return references.map((reference, index) => {
|
|
5382
|
+
const raw = rawValues[index];
|
|
5383
|
+
const linkType = raw.link_type;
|
|
5384
|
+
if (linkType !== undefined && typeof linkType !== "string") {
|
|
5385
|
+
throw new Error(`'${key}.link_type' must be string`);
|
|
5386
|
+
}
|
|
5387
|
+
return {
|
|
5388
|
+
...reference,
|
|
5389
|
+
link_type: linkType
|
|
5390
|
+
};
|
|
5391
|
+
});
|
|
5392
|
+
}
|
|
2692
5393
|
function getOptionalNumberArray(args, key) {
|
|
2693
5394
|
const value = args[key];
|
|
2694
5395
|
if (value === undefined) {
|