@zereight/mcp-gitlab 2.0.30 → 2.0.32
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 +8 -8
- package/build/index.js +614 -24
- package/build/oauth.js +16 -4
- package/build/schemas.js +244 -12
- package/build/test/schema-tests.js +311 -0
- package/build/test/test-deployment-tools.js +366 -0
- package/build/test/test-job-artifacts.js +194 -0
- package/build/test/test-merge-request-approval-state-tools.js +171 -0
- package/build/test/test-toolset-filtering.js +7 -6
- package/package.json +3 -2
package/build/index.js
CHANGED
|
@@ -36,21 +36,21 @@ import { CookieJar, parse as parseCookie } from "tough-cookie";
|
|
|
36
36
|
import { fileURLToPath, URL } from "node:url";
|
|
37
37
|
import { z } from "zod";
|
|
38
38
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
39
|
-
import {
|
|
39
|
+
import { initializeOAuthClient } from "./oauth.js";
|
|
40
40
|
import { GitLabClientPool } from "./gitlab-client-pool.js";
|
|
41
41
|
// Add type imports for proxy agents
|
|
42
42
|
import { Agent } from "node:http";
|
|
43
43
|
import { Agent as HttpsAgent } from "node:https";
|
|
44
44
|
import { BulkPublishDraftNotesSchema, CancelPipelineJobSchema, CancelPipelineSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateLabelSchema, // Added
|
|
45
|
-
CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateRepositorySchema, CreateWikiPageSchema, DeleteDraftNoteSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, DeleteMergeRequestNoteSchema, EditProjectMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetDraftNoteSchema, GetFileContentsSchema, GetIssueLinkSchema, GetIssueSchema, GetLabelSchema, GetMergeRequestDiffsSchema, GetMergeRequestSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetNamespaceSchema,
|
|
45
|
+
CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateRepositorySchema, CreateWikiPageSchema, DeleteDraftNoteSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, DeleteMergeRequestNoteSchema, EditProjectMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetCommitDiffSchema, GetCommitSchema, GetDraftNoteSchema, GetFileContentsSchema, GetIssueLinkSchema, GetIssueSchema, GetLabelSchema, GetMergeRequestDiffsSchema, GetMergeRequestSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetDeploymentSchema, GetEnvironmentSchema, GetNamespaceSchema,
|
|
46
46
|
// pipeline job schemas
|
|
47
47
|
GetPipelineJobOutputSchema, GetPipelineSchema, GetProjectMilestoneSchema, GetProjectSchema, GetRepositoryTreeSchema, GetUsersSchema, GetWikiPageSchema, GitLabCommitSchema, GitLabCompareResultSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabDiffSchema,
|
|
48
48
|
// Discussion Schemas
|
|
49
49
|
GitLabDiscussionNoteSchema, // Added
|
|
50
50
|
GitLabDiscussionSchema,
|
|
51
51
|
// Draft Notes Schemas
|
|
52
|
-
GitLabDraftNoteSchema, GitLabForkSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabPipelineSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchResponseSchema, GitLabTreeItemSchema, GitLabTreeSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabWikiPageSchema, GroupIteration, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
|
|
53
|
-
ListMergeRequestDiscussionsSchema, ListMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListPipelineTriggerJobsSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, MarkdownUploadSchema, DownloadAttachmentSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateLabelSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, } from "./schemas.js";
|
|
52
|
+
GitLabDraftNoteSchema, GitLabForkSchema, GitLabIssueLinkSchema, GitLabIssueSchema, GitLabIssueWithLinkDetailsSchema, GitLabMarkdownUploadSchema, GitLabMergeRequestSchema, GitLabMilestonesSchema, GitLabNamespaceExistsResponseSchema, GitLabNamespaceSchema, GitLabPipelineJobSchema, GitLabDeploymentSchema, GitLabEnvironmentSchema, GitLabPipelineSchema, GitLabPipelineTriggerJobSchema, GitLabProjectMemberSchema, GitLabProjectSchema, GitLabReferenceSchema, GitLabRepositorySchema, GitLabSearchResponseSchema, GitLabTreeItemSchema, GitLabTreeSchema, GitLabUserSchema, GitLabUsersResponseSchema, GitLabWikiPageSchema, GroupIteration, ListCommitsSchema, ListDraftNotesSchema, ListGroupIterationsSchema, ListGroupProjectsSchema, ListIssueDiscussionsSchema, ListIssueLinksSchema, ListIssuesSchema, ListLabelsSchema, ListMergeRequestDiffsSchema, // Added
|
|
53
|
+
ListMergeRequestDiscussionsSchema, ListMergeRequestsSchema, ListMergeRequestVersionsSchema, GetMergeRequestVersionSchema, GitLabMergeRequestVersionSchema, GitLabMergeRequestVersionDetailSchema, ListNamespacesSchema, ListPipelineJobsSchema, ListPipelinesSchema, ListDeploymentsSchema, ListEnvironmentsSchema, ListPipelineTriggerJobsSchema, ListProjectMembersSchema, ListProjectMilestonesSchema, ListProjectsSchema, ListWikiPagesSchema, MarkdownUploadSchema, DownloadAttachmentSchema, DownloadJobArtifactsSchema, GetJobArtifactFileSchema, GitLabArtifactEntrySchema, ListJobArtifactsSchema, MergeMergeRequestSchema, ApproveMergeRequestSchema, UnapproveMergeRequestSchema, GetMergeRequestApprovalStateSchema, GitLabMergeRequestApprovalsResponseSchema, GitLabMergeRequestApprovalStateSchema, MyIssuesSchema, PaginatedDiscussionsResponseSchema, PromoteProjectMilestoneSchema, PublishDraftNoteSchema, PlayPipelineJobSchema, PushFilesSchema, RetryPipelineJobSchema, RetryPipelineSchema, SearchRepositoriesSchema, UpdateDraftNoteSchema, UpdateIssueNoteSchema, UpdateIssueSchema, UpdateLabelSchema, UpdateMergeRequestNoteSchema, UpdateMergeRequestDiscussionNoteSchema, UpdateMergeRequestSchema, UpdateWikiPageSchema, VerifyNamespaceSchema, GitLabEventSchema, ListEventsSchema, GetProjectEventsSchema, ExecuteGraphQLSchema, GitLabReleaseSchema, ListReleasesSchema, GetReleaseSchema, CreateReleaseSchema, UpdateReleaseSchema, DeleteReleaseSchema, CreateReleaseEvidenceSchema, DownloadReleaseAssetSchema, GetMergeRequestNotesSchema, GetMergeRequestNoteSchema, DeleteMergeRequestDiscussionNoteSchema, ResolveMergeRequestThreadSchema, } from "./schemas.js";
|
|
54
54
|
import { randomUUID } from "node:crypto";
|
|
55
55
|
import { pino } from "pino";
|
|
56
56
|
const logger = pino({
|
|
@@ -243,6 +243,28 @@ function validateConfiguration() {
|
|
|
243
243
|
}
|
|
244
244
|
const GITLAB_PERSONAL_ACCESS_TOKEN = getConfig("token", "GITLAB_PERSONAL_ACCESS_TOKEN");
|
|
245
245
|
let OAUTH_ACCESS_TOKEN = null;
|
|
246
|
+
let oauthClient = null;
|
|
247
|
+
/**
|
|
248
|
+
* Ensure the OAuth token is valid before making an API call.
|
|
249
|
+
* Refreshes the token lazily (only when a tool is actually called).
|
|
250
|
+
* This avoids background timers that cause issues with multiple instances.
|
|
251
|
+
*/
|
|
252
|
+
async function ensureValidOAuthToken() {
|
|
253
|
+
if (!oauthClient)
|
|
254
|
+
return;
|
|
255
|
+
if (oauthClient.hasValidToken())
|
|
256
|
+
return;
|
|
257
|
+
try {
|
|
258
|
+
logger.info("OAuth token expired or missing, refreshing...");
|
|
259
|
+
const freshToken = await oauthClient.getAccessToken();
|
|
260
|
+
OAUTH_ACCESS_TOKEN = freshToken;
|
|
261
|
+
logger.info("OAuth token refreshed successfully");
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
logger.error("Failed to refresh OAuth token:", error);
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
246
268
|
const GITLAB_AUTH_COOKIE_PATH = getConfig("cookie-path", "GITLAB_AUTH_COOKIE_PATH");
|
|
247
269
|
const USE_OAUTH = getConfig("use-oauth", "GITLAB_USE_OAUTH") === "true";
|
|
248
270
|
const IS_OLD = getConfig("is-old", "GITLAB_IS_OLD") === "true";
|
|
@@ -457,7 +479,7 @@ const BASE_HEADERS = {
|
|
|
457
479
|
/**
|
|
458
480
|
* Build authentication headers dynamically based on context
|
|
459
481
|
* In REMOTE_AUTHORIZATION mode, reads from AsyncLocalStorage session context
|
|
460
|
-
* Otherwise, uses environment token
|
|
482
|
+
* Otherwise, uses environment token (OAuth token is refreshed lazily before each tool call)
|
|
461
483
|
*/
|
|
462
484
|
function buildAuthHeaders() {
|
|
463
485
|
if (REMOTE_AUTHORIZATION) {
|
|
@@ -572,7 +594,7 @@ const allTools = [
|
|
|
572
594
|
},
|
|
573
595
|
{
|
|
574
596
|
name: "get_merge_request_approval_state",
|
|
575
|
-
description: "Get
|
|
597
|
+
description: "Get merge request approval details including approvers (uses approval_state when available, falls back to approvals endpoint)",
|
|
576
598
|
inputSchema: toJSONSchema(GetMergeRequestApprovalStateSchema),
|
|
577
599
|
},
|
|
578
600
|
{
|
|
@@ -627,7 +649,7 @@ const allTools = [
|
|
|
627
649
|
},
|
|
628
650
|
{
|
|
629
651
|
name: "get_merge_request",
|
|
630
|
-
description: "Get details of a merge request (Either mergeRequestIid or branchName must be provided)",
|
|
652
|
+
description: "Get details of a merge request with compact deployment, commit addition, and approval summaries (Either mergeRequestIid or branchName must be provided)",
|
|
631
653
|
inputSchema: toJSONSchema(GetMergeRequestSchema),
|
|
632
654
|
},
|
|
633
655
|
{
|
|
@@ -915,6 +937,26 @@ const allTools = [
|
|
|
915
937
|
description: "Get details of a specific pipeline in a GitLab project",
|
|
916
938
|
inputSchema: toJSONSchema(GetPipelineSchema),
|
|
917
939
|
},
|
|
940
|
+
{
|
|
941
|
+
name: "list_deployments",
|
|
942
|
+
description: "List deployments in a GitLab project with filtering options",
|
|
943
|
+
inputSchema: toJSONSchema(ListDeploymentsSchema),
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
name: "get_deployment",
|
|
947
|
+
description: "Get details of a specific deployment in a GitLab project",
|
|
948
|
+
inputSchema: toJSONSchema(GetDeploymentSchema),
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
name: "list_environments",
|
|
952
|
+
description: "List environments in a GitLab project",
|
|
953
|
+
inputSchema: toJSONSchema(ListEnvironmentsSchema),
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
name: "get_environment",
|
|
957
|
+
description: "Get details of a specific environment in a GitLab project",
|
|
958
|
+
inputSchema: toJSONSchema(GetEnvironmentSchema),
|
|
959
|
+
},
|
|
918
960
|
{
|
|
919
961
|
name: "list_pipeline_jobs",
|
|
920
962
|
description: "List all jobs in a specific pipeline",
|
|
@@ -965,6 +1007,21 @@ const allTools = [
|
|
|
965
1007
|
description: "Cancel a running pipeline job",
|
|
966
1008
|
inputSchema: toJSONSchema(CancelPipelineJobSchema),
|
|
967
1009
|
},
|
|
1010
|
+
{
|
|
1011
|
+
name: "list_job_artifacts",
|
|
1012
|
+
description: "List artifact files in a job's artifacts archive. Returns file names, paths, types, and sizes.",
|
|
1013
|
+
inputSchema: toJSONSchema(ListJobArtifactsSchema),
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
name: "download_job_artifacts",
|
|
1017
|
+
description: "Download the entire artifact archive (zip) for a job to a local path. Returns the saved file path.",
|
|
1018
|
+
inputSchema: toJSONSchema(DownloadJobArtifactsSchema),
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
name: "get_job_artifact_file",
|
|
1022
|
+
description: "Get the content of a single file from a job's artifacts by its path within the archive",
|
|
1023
|
+
inputSchema: toJSONSchema(GetJobArtifactFileSchema),
|
|
1024
|
+
},
|
|
968
1025
|
{
|
|
969
1026
|
name: "list_merge_requests",
|
|
970
1027
|
description: "List merge requests. Without project_id, lists MRs assigned to the authenticated user by default (use scope='all' for all accessible MRs). With project_id, lists MRs for that specific project.",
|
|
@@ -1122,10 +1179,17 @@ const readOnlyTools = new Set([
|
|
|
1122
1179
|
"list_project_members",
|
|
1123
1180
|
"get_pipeline",
|
|
1124
1181
|
"list_pipelines",
|
|
1182
|
+
"list_deployments",
|
|
1183
|
+
"get_deployment",
|
|
1184
|
+
"list_environments",
|
|
1185
|
+
"get_environment",
|
|
1125
1186
|
"list_pipeline_jobs",
|
|
1126
1187
|
"list_pipeline_trigger_jobs",
|
|
1127
1188
|
"get_pipeline_job",
|
|
1128
1189
|
"get_pipeline_job_output",
|
|
1190
|
+
"list_job_artifacts",
|
|
1191
|
+
"download_job_artifacts",
|
|
1192
|
+
"get_job_artifact_file",
|
|
1129
1193
|
"list_labels",
|
|
1130
1194
|
"get_label",
|
|
1131
1195
|
"list_group_projects",
|
|
@@ -1176,6 +1240,10 @@ const milestoneToolNames = new Set([
|
|
|
1176
1240
|
const pipelineToolNames = new Set([
|
|
1177
1241
|
"list_pipelines",
|
|
1178
1242
|
"get_pipeline",
|
|
1243
|
+
"list_deployments",
|
|
1244
|
+
"get_deployment",
|
|
1245
|
+
"list_environments",
|
|
1246
|
+
"get_environment",
|
|
1179
1247
|
"list_pipeline_jobs",
|
|
1180
1248
|
"list_pipeline_trigger_jobs",
|
|
1181
1249
|
"get_pipeline_job",
|
|
@@ -1186,6 +1254,9 @@ const pipelineToolNames = new Set([
|
|
|
1186
1254
|
"play_pipeline_job",
|
|
1187
1255
|
"retry_pipeline_job",
|
|
1188
1256
|
"cancel_pipeline_job",
|
|
1257
|
+
"list_job_artifacts",
|
|
1258
|
+
"download_job_artifacts",
|
|
1259
|
+
"get_job_artifact_file",
|
|
1189
1260
|
]);
|
|
1190
1261
|
const TOOLSET_DEFINITIONS = [
|
|
1191
1262
|
{
|
|
@@ -1295,10 +1366,14 @@ const TOOLSET_DEFINITIONS = [
|
|
|
1295
1366
|
},
|
|
1296
1367
|
{
|
|
1297
1368
|
id: "pipelines",
|
|
1298
|
-
isDefault:
|
|
1369
|
+
isDefault: true,
|
|
1299
1370
|
tools: new Set([
|
|
1300
1371
|
"list_pipelines",
|
|
1301
1372
|
"get_pipeline",
|
|
1373
|
+
"list_deployments",
|
|
1374
|
+
"get_deployment",
|
|
1375
|
+
"list_environments",
|
|
1376
|
+
"get_environment",
|
|
1302
1377
|
"list_pipeline_jobs",
|
|
1303
1378
|
"list_pipeline_trigger_jobs",
|
|
1304
1379
|
"get_pipeline_job",
|
|
@@ -1309,11 +1384,14 @@ const TOOLSET_DEFINITIONS = [
|
|
|
1309
1384
|
"play_pipeline_job",
|
|
1310
1385
|
"retry_pipeline_job",
|
|
1311
1386
|
"cancel_pipeline_job",
|
|
1387
|
+
"list_job_artifacts",
|
|
1388
|
+
"download_job_artifacts",
|
|
1389
|
+
"get_job_artifact_file",
|
|
1312
1390
|
]),
|
|
1313
1391
|
},
|
|
1314
1392
|
{
|
|
1315
1393
|
id: "milestones",
|
|
1316
|
-
isDefault:
|
|
1394
|
+
isDefault: true,
|
|
1317
1395
|
tools: new Set([
|
|
1318
1396
|
"list_milestones",
|
|
1319
1397
|
"get_milestone",
|
|
@@ -1328,7 +1406,7 @@ const TOOLSET_DEFINITIONS = [
|
|
|
1328
1406
|
},
|
|
1329
1407
|
{
|
|
1330
1408
|
id: "wiki",
|
|
1331
|
-
isDefault:
|
|
1409
|
+
isDefault: true,
|
|
1332
1410
|
tools: new Set([
|
|
1333
1411
|
"list_wiki_pages",
|
|
1334
1412
|
"get_wiki_page",
|
|
@@ -1440,6 +1518,7 @@ if (GITLAB_TOOLSETS_RAW && (USE_PIPELINE || USE_MILESTONE || USE_GITLAB_WIKI)) {
|
|
|
1440
1518
|
logger.warn("GITLAB_TOOLSETS is set alongside legacy flags (USE_PIPELINE, USE_MILESTONE, USE_GITLAB_WIKI). " +
|
|
1441
1519
|
"Legacy flags add tools additively on top of the toolset selection and may produce unexpected results.");
|
|
1442
1520
|
}
|
|
1521
|
+
const MERGE_REQUEST_DEPLOYMENT_SUMMARY_LIMIT = 10;
|
|
1443
1522
|
/**
|
|
1444
1523
|
* Smart URL handling for GitLab API
|
|
1445
1524
|
*
|
|
@@ -1615,12 +1694,12 @@ async function getDefaultBranchRef(projectId) {
|
|
|
1615
1694
|
* @returns {Promise<GitLabContent>} The file content
|
|
1616
1695
|
*/
|
|
1617
1696
|
async function getFileContents(projectId, filePath, ref) {
|
|
1618
|
-
|
|
1619
|
-
const effectiveProjectId = getEffectiveProjectId(
|
|
1697
|
+
const decodedProjectId = projectId ? decodeURIComponent(projectId) : "";
|
|
1698
|
+
const effectiveProjectId = getEffectiveProjectId(decodedProjectId);
|
|
1620
1699
|
const encodedPath = encodeURIComponent(filePath);
|
|
1621
1700
|
// Fall back to default branch if ref is not provided
|
|
1622
1701
|
if (!ref) {
|
|
1623
|
-
ref = await getDefaultBranchRef(
|
|
1702
|
+
ref = await getDefaultBranchRef(decodedProjectId);
|
|
1624
1703
|
}
|
|
1625
1704
|
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/repository/files/${encodedPath}`);
|
|
1626
1705
|
url.searchParams.append("ref", ref);
|
|
@@ -2454,6 +2533,7 @@ async function getMergeRequest(projectId, mergeRequestIid, branchName) {
|
|
|
2454
2533
|
let url;
|
|
2455
2534
|
if (mergeRequestIid) {
|
|
2456
2535
|
url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}`);
|
|
2536
|
+
url.searchParams.append("include_diverged_commits_count", "true");
|
|
2457
2537
|
}
|
|
2458
2538
|
else if (branchName) {
|
|
2459
2539
|
url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests?source_branch=${encodeURIComponent(branchName)}`);
|
|
@@ -2468,10 +2548,219 @@ async function getMergeRequest(projectId, mergeRequestIid, branchName) {
|
|
|
2468
2548
|
const data = await response.json();
|
|
2469
2549
|
// If response is an array (Comes from branchName search), return the first item if exist
|
|
2470
2550
|
if (Array.isArray(data) && data.length > 0) {
|
|
2471
|
-
|
|
2551
|
+
const mergeRequest = GitLabMergeRequestSchema.parse(data[0]);
|
|
2552
|
+
return getMergeRequest(projectId, mergeRequest.iid, undefined);
|
|
2472
2553
|
}
|
|
2473
2554
|
return GitLabMergeRequestSchema.parse(data);
|
|
2474
2555
|
}
|
|
2556
|
+
async function getMergeRequestSourceCommitCount(projectId, mergeRequestIid) {
|
|
2557
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/commits`);
|
|
2558
|
+
url.searchParams.append("per_page", "100");
|
|
2559
|
+
let totalCount = 0;
|
|
2560
|
+
let page = 1;
|
|
2561
|
+
while (true) {
|
|
2562
|
+
url.searchParams.set("page", String(page));
|
|
2563
|
+
const response = await fetch(url.toString(), {
|
|
2564
|
+
...getFetchConfig(),
|
|
2565
|
+
});
|
|
2566
|
+
await handleGitLabError(response);
|
|
2567
|
+
const data = await response.json();
|
|
2568
|
+
if (!Array.isArray(data)) {
|
|
2569
|
+
throw new Error("Unexpected merge request commits response format");
|
|
2570
|
+
}
|
|
2571
|
+
totalCount += data.length;
|
|
2572
|
+
const nextPage = response.headers.get("x-next-page");
|
|
2573
|
+
if (!nextPage) {
|
|
2574
|
+
break;
|
|
2575
|
+
}
|
|
2576
|
+
page = Number.parseInt(nextPage, 10);
|
|
2577
|
+
if (Number.isNaN(page) || page <= 0) {
|
|
2578
|
+
break;
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
return totalCount;
|
|
2582
|
+
}
|
|
2583
|
+
async function getProjectMergeMethod(projectId) {
|
|
2584
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}`);
|
|
2585
|
+
const response = await fetch(url.toString(), {
|
|
2586
|
+
...getFetchConfig(),
|
|
2587
|
+
});
|
|
2588
|
+
await handleGitLabError(response);
|
|
2589
|
+
const data = await response.json();
|
|
2590
|
+
const mergeMethod = z
|
|
2591
|
+
.object({
|
|
2592
|
+
merge_method: z.string().nullable().optional(),
|
|
2593
|
+
})
|
|
2594
|
+
.parse(data).merge_method;
|
|
2595
|
+
return typeof mergeMethod === "string" ? mergeMethod : null;
|
|
2596
|
+
}
|
|
2597
|
+
function estimateMergeCommitCount(mergeMethod, sourceCommitCount) {
|
|
2598
|
+
if (sourceCommitCount === 0) {
|
|
2599
|
+
return 0;
|
|
2600
|
+
}
|
|
2601
|
+
if (mergeMethod === "merge") {
|
|
2602
|
+
return 1;
|
|
2603
|
+
}
|
|
2604
|
+
if (mergeMethod === "ff" || mergeMethod === "rebase_merge") {
|
|
2605
|
+
return 0;
|
|
2606
|
+
}
|
|
2607
|
+
return null;
|
|
2608
|
+
}
|
|
2609
|
+
async function buildMergeRequestCommitAdditionSummary(projectId, mergeRequest) {
|
|
2610
|
+
try {
|
|
2611
|
+
const sourceCommitCount = await getMergeRequestSourceCommitCount(projectId, mergeRequest.iid);
|
|
2612
|
+
const mergeMethod = await getProjectMergeMethod(projectId);
|
|
2613
|
+
const mergeCommitCount = estimateMergeCommitCount(mergeMethod, sourceCommitCount);
|
|
2614
|
+
const summary = mergeCommitCount === null
|
|
2615
|
+
? null
|
|
2616
|
+
: `${sourceCommitCount} commits and ${mergeCommitCount} merge commit${mergeCommitCount === 1 ? "" : "s"} will be added to ${mergeRequest.target_branch}.`;
|
|
2617
|
+
return {
|
|
2618
|
+
target_branch: mergeRequest.target_branch,
|
|
2619
|
+
source_commits_count: sourceCommitCount,
|
|
2620
|
+
merge_method: mergeMethod,
|
|
2621
|
+
merge_commit_count: mergeCommitCount,
|
|
2622
|
+
summary,
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
catch (error) {
|
|
2626
|
+
const unavailableReason = error instanceof Error ? error.message : String(error);
|
|
2627
|
+
return {
|
|
2628
|
+
target_branch: mergeRequest.target_branch,
|
|
2629
|
+
source_commits_count: null,
|
|
2630
|
+
merge_method: null,
|
|
2631
|
+
merge_commit_count: null,
|
|
2632
|
+
summary: null,
|
|
2633
|
+
unavailable_reason: unavailableReason,
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
async function buildMergeRequestApprovalSummary(projectId, mergeRequestIid) {
|
|
2638
|
+
try {
|
|
2639
|
+
const approvalState = await getMergeRequestApprovalState(projectId, mergeRequestIid);
|
|
2640
|
+
const approvedByUsers = approvalState.approved_by || [];
|
|
2641
|
+
const approvedByUsernames = approvalState.approved_by_usernames || approvedByUsers.map(user => user.username);
|
|
2642
|
+
const inferredApproved = inferMergeRequestApproved(approvalState.rules);
|
|
2643
|
+
return {
|
|
2644
|
+
approved: approvalState.approved ?? inferredApproved,
|
|
2645
|
+
user_has_approved: approvalState.user_has_approved ?? null,
|
|
2646
|
+
user_can_approve: approvalState.user_can_approve ?? null,
|
|
2647
|
+
approved_by: approvedByUsers,
|
|
2648
|
+
approved_by_usernames: approvedByUsernames,
|
|
2649
|
+
rules_count: approvalState.rules?.length ?? null,
|
|
2650
|
+
source_endpoint: approvalState.source_endpoint ?? null,
|
|
2651
|
+
};
|
|
2652
|
+
}
|
|
2653
|
+
catch (error) {
|
|
2654
|
+
const unavailableReason = error instanceof Error ? error.message : String(error);
|
|
2655
|
+
return {
|
|
2656
|
+
approved: null,
|
|
2657
|
+
user_has_approved: null,
|
|
2658
|
+
user_can_approve: null,
|
|
2659
|
+
approved_by: [],
|
|
2660
|
+
approved_by_usernames: [],
|
|
2661
|
+
rules_count: null,
|
|
2662
|
+
source_endpoint: null,
|
|
2663
|
+
unavailable_reason: unavailableReason,
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
function toMergeRequestDeploymentSummaryRecord(deployment) {
|
|
2668
|
+
return {
|
|
2669
|
+
id: deployment.id,
|
|
2670
|
+
status: deployment.status,
|
|
2671
|
+
ref: deployment.ref,
|
|
2672
|
+
sha: deployment.sha,
|
|
2673
|
+
created_at: deployment.created_at,
|
|
2674
|
+
updated_at: deployment.updated_at,
|
|
2675
|
+
finished_at: deployment.finished_at,
|
|
2676
|
+
web_url: deployment.web_url,
|
|
2677
|
+
environment: deployment.environment
|
|
2678
|
+
? {
|
|
2679
|
+
id: deployment.environment.id,
|
|
2680
|
+
name: deployment.environment.name,
|
|
2681
|
+
slug: deployment.environment.slug,
|
|
2682
|
+
external_url: deployment.environment.external_url,
|
|
2683
|
+
state: deployment.environment.state,
|
|
2684
|
+
tier: deployment.environment.tier,
|
|
2685
|
+
}
|
|
2686
|
+
: undefined,
|
|
2687
|
+
deployable: deployment.deployable === null
|
|
2688
|
+
? null
|
|
2689
|
+
: deployment.deployable
|
|
2690
|
+
? {
|
|
2691
|
+
id: deployment.deployable.id,
|
|
2692
|
+
name: deployment.deployable.name,
|
|
2693
|
+
status: deployment.deployable.status,
|
|
2694
|
+
stage: deployment.deployable.stage,
|
|
2695
|
+
web_url: deployment.deployable.web_url,
|
|
2696
|
+
pipeline: deployment.deployable.pipeline
|
|
2697
|
+
? {
|
|
2698
|
+
id: deployment.deployable.pipeline.id,
|
|
2699
|
+
status: deployment.deployable.pipeline.status,
|
|
2700
|
+
ref: deployment.deployable.pipeline.ref,
|
|
2701
|
+
sha: deployment.deployable.pipeline.sha,
|
|
2702
|
+
web_url: deployment.deployable.pipeline.web_url,
|
|
2703
|
+
}
|
|
2704
|
+
: undefined,
|
|
2705
|
+
}
|
|
2706
|
+
: undefined,
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2709
|
+
function sortDeploymentsByCreatedAtDesc(deployments) {
|
|
2710
|
+
return [...deployments].sort((a, b) => {
|
|
2711
|
+
const aTime = Date.parse(a.created_at);
|
|
2712
|
+
const bTime = Date.parse(b.created_at);
|
|
2713
|
+
if (Number.isNaN(aTime) || Number.isNaN(bTime)) {
|
|
2714
|
+
return b.created_at.localeCompare(a.created_at);
|
|
2715
|
+
}
|
|
2716
|
+
return bTime - aTime;
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2719
|
+
async function buildMergeRequestDeploymentSummary(projectId, mergeRequest) {
|
|
2720
|
+
const lookupSha = mergeRequest.merge_commit_sha ?? mergeRequest.diff_refs?.head_sha ?? null;
|
|
2721
|
+
if (!lookupSha) {
|
|
2722
|
+
return {
|
|
2723
|
+
lookup_sha: null,
|
|
2724
|
+
sort: "created_at_desc",
|
|
2725
|
+
limit: MERGE_REQUEST_DEPLOYMENT_SUMMARY_LIMIT,
|
|
2726
|
+
total_count: 0,
|
|
2727
|
+
returned_count: 0,
|
|
2728
|
+
records: [],
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
try {
|
|
2732
|
+
const deployments = await listDeployments(projectId, {
|
|
2733
|
+
sha: lookupSha,
|
|
2734
|
+
order_by: "created_at",
|
|
2735
|
+
sort: "desc",
|
|
2736
|
+
per_page: 100,
|
|
2737
|
+
});
|
|
2738
|
+
const sortedDeployments = sortDeploymentsByCreatedAtDesc(deployments);
|
|
2739
|
+
const records = sortedDeployments
|
|
2740
|
+
.slice(0, MERGE_REQUEST_DEPLOYMENT_SUMMARY_LIMIT)
|
|
2741
|
+
.map(toMergeRequestDeploymentSummaryRecord);
|
|
2742
|
+
return {
|
|
2743
|
+
lookup_sha: lookupSha,
|
|
2744
|
+
sort: "created_at_desc",
|
|
2745
|
+
limit: MERGE_REQUEST_DEPLOYMENT_SUMMARY_LIMIT,
|
|
2746
|
+
total_count: sortedDeployments.length,
|
|
2747
|
+
returned_count: records.length,
|
|
2748
|
+
records,
|
|
2749
|
+
};
|
|
2750
|
+
}
|
|
2751
|
+
catch (error) {
|
|
2752
|
+
const unavailableReason = error instanceof Error ? error.message : String(error);
|
|
2753
|
+
return {
|
|
2754
|
+
lookup_sha: lookupSha,
|
|
2755
|
+
sort: "created_at_desc",
|
|
2756
|
+
limit: MERGE_REQUEST_DEPLOYMENT_SUMMARY_LIMIT,
|
|
2757
|
+
total_count: 0,
|
|
2758
|
+
returned_count: 0,
|
|
2759
|
+
records: [],
|
|
2760
|
+
unavailable_reason: unavailableReason,
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2475
2764
|
/**
|
|
2476
2765
|
* Get merge request changes/diffs
|
|
2477
2766
|
* MR 변경사항 조회 함수 (Function to retrieve merge request changes)
|
|
@@ -2666,13 +2955,61 @@ async function unapproveMergeRequest(projectId, mergeRequestIid) {
|
|
|
2666
2955
|
*/
|
|
2667
2956
|
async function getMergeRequestApprovalState(projectId, mergeRequestIid) {
|
|
2668
2957
|
projectId = decodeURIComponent(projectId);
|
|
2669
|
-
const
|
|
2670
|
-
const
|
|
2958
|
+
const approvalStateUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/approval_state`);
|
|
2959
|
+
const approvalStateResponse = await fetch(approvalStateUrl.toString(), {
|
|
2671
2960
|
...getFetchConfig(),
|
|
2672
2961
|
method: "GET",
|
|
2673
2962
|
});
|
|
2674
|
-
|
|
2675
|
-
|
|
2963
|
+
if (approvalStateResponse.status === 404) {
|
|
2964
|
+
return getMergeRequestApprovalsFallback(projectId, mergeRequestIid);
|
|
2965
|
+
}
|
|
2966
|
+
await handleGitLabError(approvalStateResponse);
|
|
2967
|
+
const parsedApprovalState = GitLabMergeRequestApprovalStateSchema.parse(await approvalStateResponse.json());
|
|
2968
|
+
const approvedByUsers = getUniqueApprovalUsers((parsedApprovalState.rules || []).flatMap(rule => rule.approved_by || []));
|
|
2969
|
+
const approvedByUsernames = approvedByUsers.map(user => user.username);
|
|
2970
|
+
return {
|
|
2971
|
+
...parsedApprovalState,
|
|
2972
|
+
approved_by: approvedByUsers,
|
|
2973
|
+
approved_by_usernames: approvedByUsernames,
|
|
2974
|
+
source_endpoint: "approval_state",
|
|
2975
|
+
};
|
|
2976
|
+
}
|
|
2977
|
+
async function getMergeRequestApprovalsFallback(projectId, mergeRequestIid) {
|
|
2978
|
+
const approvalsUrl = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/merge_requests/${mergeRequestIid}/approvals`);
|
|
2979
|
+
const approvalsResponse = await fetch(approvalsUrl.toString(), {
|
|
2980
|
+
...getFetchConfig(),
|
|
2981
|
+
method: "GET",
|
|
2982
|
+
});
|
|
2983
|
+
await handleGitLabError(approvalsResponse);
|
|
2984
|
+
const parsedApprovals = GitLabMergeRequestApprovalsResponseSchema.parse(await approvalsResponse.json());
|
|
2985
|
+
const approvedByUsers = getUniqueApprovalUsers((parsedApprovals.approved_by || []).map(approvedByEntry => approvedByEntry.user));
|
|
2986
|
+
const approvedByUsernames = approvedByUsers.map(user => user.username);
|
|
2987
|
+
return GitLabMergeRequestApprovalStateSchema.parse({
|
|
2988
|
+
approved: parsedApprovals.approved,
|
|
2989
|
+
user_has_approved: parsedApprovals.user_has_approved,
|
|
2990
|
+
user_can_approve: parsedApprovals.user_can_approve,
|
|
2991
|
+
approved_by: approvedByUsers,
|
|
2992
|
+
approved_by_usernames: approvedByUsernames,
|
|
2993
|
+
source_endpoint: "approvals",
|
|
2994
|
+
});
|
|
2995
|
+
}
|
|
2996
|
+
function getUniqueApprovalUsers(users) {
|
|
2997
|
+
const uniqueUsers = new Map();
|
|
2998
|
+
for (const user of users) {
|
|
2999
|
+
if (!uniqueUsers.has(user.id)) {
|
|
3000
|
+
uniqueUsers.set(user.id, user);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
return [...uniqueUsers.values()];
|
|
3004
|
+
}
|
|
3005
|
+
function inferMergeRequestApproved(rules) {
|
|
3006
|
+
if (!rules || rules.length === 0) {
|
|
3007
|
+
return null;
|
|
3008
|
+
}
|
|
3009
|
+
if (rules.some(rule => typeof rule.approved !== "boolean")) {
|
|
3010
|
+
return null;
|
|
3011
|
+
}
|
|
3012
|
+
return rules.every(rule => rule.approved === true);
|
|
2676
3013
|
}
|
|
2677
3014
|
/**
|
|
2678
3015
|
* Create a new note (comment) on an issue or merge request
|
|
@@ -3409,6 +3746,90 @@ async function getPipeline(projectId, pipelineId) {
|
|
|
3409
3746
|
const data = await response.json();
|
|
3410
3747
|
return GitLabPipelineSchema.parse(data);
|
|
3411
3748
|
}
|
|
3749
|
+
/**
|
|
3750
|
+
* List deployments in a GitLab project
|
|
3751
|
+
*
|
|
3752
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
3753
|
+
* @param {ListDeploymentsOptions} options - Options for filtering deployments
|
|
3754
|
+
* @returns {Promise<GitLabDeployment[]>} List of deployments
|
|
3755
|
+
*/
|
|
3756
|
+
async function listDeployments(projectId, options = {}) {
|
|
3757
|
+
projectId = decodeURIComponent(projectId);
|
|
3758
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/deployments`);
|
|
3759
|
+
Object.entries(options).forEach(([key, value]) => {
|
|
3760
|
+
if (value !== undefined) {
|
|
3761
|
+
url.searchParams.append(key, value.toString());
|
|
3762
|
+
}
|
|
3763
|
+
});
|
|
3764
|
+
const response = await fetch(url.toString(), {
|
|
3765
|
+
...getFetchConfig(),
|
|
3766
|
+
});
|
|
3767
|
+
await handleGitLabError(response);
|
|
3768
|
+
const data = await response.json();
|
|
3769
|
+
return z.array(GitLabDeploymentSchema).parse(data);
|
|
3770
|
+
}
|
|
3771
|
+
/**
|
|
3772
|
+
* Get details of a specific deployment
|
|
3773
|
+
*
|
|
3774
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
3775
|
+
* @param {number | string} deploymentId - The ID of the deployment
|
|
3776
|
+
* @returns {Promise<GitLabDeployment>} Deployment details
|
|
3777
|
+
*/
|
|
3778
|
+
async function getDeployment(projectId, deploymentId) {
|
|
3779
|
+
projectId = decodeURIComponent(projectId);
|
|
3780
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/deployments/${deploymentId}`);
|
|
3781
|
+
const response = await fetch(url.toString(), {
|
|
3782
|
+
...getFetchConfig(),
|
|
3783
|
+
});
|
|
3784
|
+
if (response.status === 404) {
|
|
3785
|
+
throw new Error(`Deployment not found`);
|
|
3786
|
+
}
|
|
3787
|
+
await handleGitLabError(response);
|
|
3788
|
+
const data = await response.json();
|
|
3789
|
+
return GitLabDeploymentSchema.parse(data);
|
|
3790
|
+
}
|
|
3791
|
+
/**
|
|
3792
|
+
* List environments in a GitLab project
|
|
3793
|
+
*
|
|
3794
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
3795
|
+
* @param {ListEnvironmentsOptions} options - Options for filtering environments
|
|
3796
|
+
* @returns {Promise<GitLabEnvironment[]>} List of environments
|
|
3797
|
+
*/
|
|
3798
|
+
async function listEnvironments(projectId, options = {}) {
|
|
3799
|
+
projectId = decodeURIComponent(projectId);
|
|
3800
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/environments`);
|
|
3801
|
+
Object.entries(options).forEach(([key, value]) => {
|
|
3802
|
+
if (value !== undefined) {
|
|
3803
|
+
url.searchParams.append(key, value.toString());
|
|
3804
|
+
}
|
|
3805
|
+
});
|
|
3806
|
+
const response = await fetch(url.toString(), {
|
|
3807
|
+
...getFetchConfig(),
|
|
3808
|
+
});
|
|
3809
|
+
await handleGitLabError(response);
|
|
3810
|
+
const data = await response.json();
|
|
3811
|
+
return z.array(GitLabEnvironmentSchema).parse(data);
|
|
3812
|
+
}
|
|
3813
|
+
/**
|
|
3814
|
+
* Get details of a specific environment
|
|
3815
|
+
*
|
|
3816
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
3817
|
+
* @param {number | string} environmentId - The ID of the environment
|
|
3818
|
+
* @returns {Promise<GitLabEnvironment>} Environment details
|
|
3819
|
+
*/
|
|
3820
|
+
async function getEnvironment(projectId, environmentId) {
|
|
3821
|
+
projectId = decodeURIComponent(projectId);
|
|
3822
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/environments/${environmentId}`);
|
|
3823
|
+
const response = await fetch(url.toString(), {
|
|
3824
|
+
...getFetchConfig(),
|
|
3825
|
+
});
|
|
3826
|
+
if (response.status === 404) {
|
|
3827
|
+
throw new Error(`Environment not found`);
|
|
3828
|
+
}
|
|
3829
|
+
await handleGitLabError(response);
|
|
3830
|
+
const data = await response.json();
|
|
3831
|
+
return GitLabEnvironmentSchema.parse(data);
|
|
3832
|
+
}
|
|
3412
3833
|
/**
|
|
3413
3834
|
* List all jobs in a specific pipeline
|
|
3414
3835
|
*
|
|
@@ -3533,21 +3954,107 @@ async function getPipelineJobOutput(projectId, jobId, limit, offset) {
|
|
|
3533
3954
|
}
|
|
3534
3955
|
return fullTrace;
|
|
3535
3956
|
}
|
|
3957
|
+
/**
|
|
3958
|
+
* List artifact files in a job's artifacts archive
|
|
3959
|
+
*
|
|
3960
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
3961
|
+
* @param {string} jobId - The ID of the job
|
|
3962
|
+
* @param {Object} options - Options for listing artifacts
|
|
3963
|
+
* @returns {Promise<GitLabArtifactEntry[]>} List of artifact entries
|
|
3964
|
+
*/
|
|
3965
|
+
async function listJobArtifacts(projectId, jobId, options = {}) {
|
|
3966
|
+
projectId = decodeURIComponent(projectId);
|
|
3967
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${jobId}/artifacts/tree`);
|
|
3968
|
+
Object.entries(options).forEach(([key, value]) => {
|
|
3969
|
+
if (value !== undefined) {
|
|
3970
|
+
if (typeof value === "boolean") {
|
|
3971
|
+
url.searchParams.append(key, value ? "true" : "false");
|
|
3972
|
+
}
|
|
3973
|
+
else {
|
|
3974
|
+
url.searchParams.append(key, value.toString());
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3977
|
+
});
|
|
3978
|
+
const response = await fetch(url.toString(), {
|
|
3979
|
+
...getFetchConfig(),
|
|
3980
|
+
});
|
|
3981
|
+
if (response.status === 404) {
|
|
3982
|
+
throw new Error(`Job artifacts not found. The job may not have produced artifacts or the job ID is invalid.`);
|
|
3983
|
+
}
|
|
3984
|
+
await handleGitLabError(response);
|
|
3985
|
+
const data = await response.json();
|
|
3986
|
+
return z.array(GitLabArtifactEntrySchema).parse(data);
|
|
3987
|
+
}
|
|
3988
|
+
/**
|
|
3989
|
+
* Download the entire artifact archive for a job and save to disk
|
|
3990
|
+
*
|
|
3991
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
3992
|
+
* @param {string} jobId - The ID of the job
|
|
3993
|
+
* @param {string} localPath - Optional local directory to save the archive
|
|
3994
|
+
* @returns {Promise<string>} The path where the artifact archive was saved
|
|
3995
|
+
*/
|
|
3996
|
+
async function downloadJobArtifacts(projectId, jobId, localPath) {
|
|
3997
|
+
projectId = decodeURIComponent(projectId);
|
|
3998
|
+
const effectiveProjectId = getEffectiveProjectId(projectId);
|
|
3999
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/jobs/${jobId}/artifacts`);
|
|
4000
|
+
const response = await fetch(url.toString(), {
|
|
4001
|
+
...getFetchConfig(),
|
|
4002
|
+
});
|
|
4003
|
+
if (response.status === 404) {
|
|
4004
|
+
throw new Error(`Job artifacts not found. The job may not have produced artifacts or the job ID is invalid.`);
|
|
4005
|
+
}
|
|
4006
|
+
await handleGitLabError(response);
|
|
4007
|
+
const buffer = await response.arrayBuffer();
|
|
4008
|
+
const filename = `artifacts_job_${jobId}.zip`;
|
|
4009
|
+
const savePath = localPath ? path.join(localPath, filename) : filename;
|
|
4010
|
+
fs.mkdirSync(path.dirname(savePath), { recursive: true });
|
|
4011
|
+
fs.writeFileSync(savePath, Buffer.from(buffer));
|
|
4012
|
+
return savePath;
|
|
4013
|
+
}
|
|
4014
|
+
/**
|
|
4015
|
+
* Download a single file from a job's artifacts
|
|
4016
|
+
*
|
|
4017
|
+
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
4018
|
+
* @param {string} jobId - The ID of the job
|
|
4019
|
+
* @param {string} artifactPath - Path to the file within the artifacts archive
|
|
4020
|
+
* @returns {Promise<string>} The file content as text
|
|
4021
|
+
*/
|
|
4022
|
+
async function getJobArtifactFile(projectId, jobId, artifactPath) {
|
|
4023
|
+
projectId = decodeURIComponent(projectId);
|
|
4024
|
+
const effectiveProjectId = getEffectiveProjectId(projectId);
|
|
4025
|
+
const encodedArtifactPath = artifactPath
|
|
4026
|
+
.split("/")
|
|
4027
|
+
.map(segment => encodeURIComponent(segment))
|
|
4028
|
+
.join("/");
|
|
4029
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/jobs/${jobId}/artifacts/${encodedArtifactPath}`);
|
|
4030
|
+
const response = await fetch(url.toString(), {
|
|
4031
|
+
...getFetchConfig(),
|
|
4032
|
+
});
|
|
4033
|
+
if (response.status === 404) {
|
|
4034
|
+
throw new Error(`Artifact file not found: ${artifactPath}`);
|
|
4035
|
+
}
|
|
4036
|
+
await handleGitLabError(response);
|
|
4037
|
+
return await response.text();
|
|
4038
|
+
}
|
|
3536
4039
|
/**
|
|
3537
4040
|
* Create a new pipeline
|
|
3538
4041
|
*
|
|
3539
4042
|
* @param {string} projectId - The ID or URL-encoded path of the project
|
|
3540
4043
|
* @param {string} ref - The branch or tag to run the pipeline on
|
|
3541
4044
|
* @param {Array} variables - Optional variables for the pipeline
|
|
4045
|
+
* @param {Record<string, string>} inputs - Optional input parameters for the pipeline
|
|
3542
4046
|
* @returns {Promise<GitLabPipeline>} The created pipeline
|
|
3543
4047
|
*/
|
|
3544
|
-
async function createPipeline(projectId, ref, variables) {
|
|
4048
|
+
async function createPipeline(projectId, ref, variables, inputs) {
|
|
3545
4049
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
3546
4050
|
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipeline`);
|
|
3547
4051
|
const body = { ref };
|
|
3548
4052
|
if (variables && variables.length > 0) {
|
|
3549
4053
|
body.variables = variables;
|
|
3550
4054
|
}
|
|
4055
|
+
if (inputs && Object.keys(inputs).length > 0) {
|
|
4056
|
+
body.inputs = inputs;
|
|
4057
|
+
}
|
|
3551
4058
|
const response = await fetch(url.toString(), {
|
|
3552
4059
|
method: "POST",
|
|
3553
4060
|
headers: {
|
|
@@ -4450,6 +4957,8 @@ async function handleToolCall(params) {
|
|
|
4450
4957
|
if (GITLAB_AUTH_COOKIE_PATH) {
|
|
4451
4958
|
await ensureSessionForRequest();
|
|
4452
4959
|
}
|
|
4960
|
+
// Lazy OAuth token refresh: only validate/refresh when a tool is actually called
|
|
4961
|
+
await ensureValidOAuthToken();
|
|
4453
4962
|
logger.info(params.name);
|
|
4454
4963
|
switch (params.name) {
|
|
4455
4964
|
case "execute_graphql": {
|
|
@@ -4678,8 +5187,22 @@ async function handleToolCall(params) {
|
|
|
4678
5187
|
case "get_merge_request": {
|
|
4679
5188
|
const args = GetMergeRequestSchema.parse(params.arguments);
|
|
4680
5189
|
const mergeRequest = await getMergeRequest(args.project_id, args.merge_request_iid, args.source_branch);
|
|
5190
|
+
const deploymentSummary = await buildMergeRequestDeploymentSummary(args.project_id, mergeRequest);
|
|
5191
|
+
const commitAdditionSummary = await buildMergeRequestCommitAdditionSummary(args.project_id, mergeRequest);
|
|
5192
|
+
const approvalSummary = await buildMergeRequestApprovalSummary(args.project_id, mergeRequest.iid);
|
|
5193
|
+
const mergeRequestWithDeploymentSummary = {
|
|
5194
|
+
...mergeRequest,
|
|
5195
|
+
deployment_summary: deploymentSummary,
|
|
5196
|
+
commit_addition_summary: commitAdditionSummary,
|
|
5197
|
+
approval_summary: approvalSummary,
|
|
5198
|
+
};
|
|
4681
5199
|
return {
|
|
4682
|
-
content: [
|
|
5200
|
+
content: [
|
|
5201
|
+
{
|
|
5202
|
+
type: "text",
|
|
5203
|
+
text: JSON.stringify(mergeRequestWithDeploymentSummary, null, 2),
|
|
5204
|
+
},
|
|
5205
|
+
],
|
|
4683
5206
|
};
|
|
4684
5207
|
}
|
|
4685
5208
|
case "get_merge_request_diffs": {
|
|
@@ -5138,6 +5661,36 @@ async function handleToolCall(params) {
|
|
|
5138
5661
|
],
|
|
5139
5662
|
};
|
|
5140
5663
|
}
|
|
5664
|
+
case "list_deployments": {
|
|
5665
|
+
const args = ListDeploymentsSchema.parse(params.arguments);
|
|
5666
|
+
const { project_id, ...options } = args;
|
|
5667
|
+
const deployments = await listDeployments(project_id, options);
|
|
5668
|
+
return {
|
|
5669
|
+
content: [{ type: "text", text: JSON.stringify(deployments, null, 2) }],
|
|
5670
|
+
};
|
|
5671
|
+
}
|
|
5672
|
+
case "get_deployment": {
|
|
5673
|
+
const { project_id, deployment_id } = GetDeploymentSchema.parse(params.arguments);
|
|
5674
|
+
const deployment = await getDeployment(project_id, deployment_id);
|
|
5675
|
+
return {
|
|
5676
|
+
content: [{ type: "text", text: JSON.stringify(deployment, null, 2) }],
|
|
5677
|
+
};
|
|
5678
|
+
}
|
|
5679
|
+
case "list_environments": {
|
|
5680
|
+
const args = ListEnvironmentsSchema.parse(params.arguments);
|
|
5681
|
+
const { project_id, ...options } = args;
|
|
5682
|
+
const environments = await listEnvironments(project_id, options);
|
|
5683
|
+
return {
|
|
5684
|
+
content: [{ type: "text", text: JSON.stringify(environments, null, 2) }],
|
|
5685
|
+
};
|
|
5686
|
+
}
|
|
5687
|
+
case "get_environment": {
|
|
5688
|
+
const { project_id, environment_id } = GetEnvironmentSchema.parse(params.arguments);
|
|
5689
|
+
const environment = await getEnvironment(project_id, environment_id);
|
|
5690
|
+
return {
|
|
5691
|
+
content: [{ type: "text", text: JSON.stringify(environment, null, 2) }],
|
|
5692
|
+
};
|
|
5693
|
+
}
|
|
5141
5694
|
case "list_pipeline_jobs": {
|
|
5142
5695
|
const { project_id, pipeline_id, ...options } = ListPipelineJobsSchema.parse(params.arguments);
|
|
5143
5696
|
const jobs = await listPipelineJobs(project_id, pipeline_id, options);
|
|
@@ -5187,8 +5740,8 @@ async function handleToolCall(params) {
|
|
|
5187
5740
|
};
|
|
5188
5741
|
}
|
|
5189
5742
|
case "create_pipeline": {
|
|
5190
|
-
const { project_id, ref, variables } = CreatePipelineSchema.parse(params.arguments);
|
|
5191
|
-
const pipeline = await createPipeline(project_id, ref, variables);
|
|
5743
|
+
const { project_id, ref, variables, inputs } = CreatePipelineSchema.parse(params.arguments);
|
|
5744
|
+
const pipeline = await createPipeline(project_id, ref, variables, inputs);
|
|
5192
5745
|
return {
|
|
5193
5746
|
content: [
|
|
5194
5747
|
{
|
|
@@ -5258,6 +5811,42 @@ async function handleToolCall(params) {
|
|
|
5258
5811
|
],
|
|
5259
5812
|
};
|
|
5260
5813
|
}
|
|
5814
|
+
case "list_job_artifacts": {
|
|
5815
|
+
const { project_id, job_id, ...options } = ListJobArtifactsSchema.parse(params.arguments);
|
|
5816
|
+
const artifacts = await listJobArtifacts(project_id, job_id, options);
|
|
5817
|
+
return {
|
|
5818
|
+
content: [
|
|
5819
|
+
{
|
|
5820
|
+
type: "text",
|
|
5821
|
+
text: JSON.stringify(artifacts, null, 2),
|
|
5822
|
+
},
|
|
5823
|
+
],
|
|
5824
|
+
};
|
|
5825
|
+
}
|
|
5826
|
+
case "download_job_artifacts": {
|
|
5827
|
+
const { project_id, job_id, local_path } = DownloadJobArtifactsSchema.parse(params.arguments);
|
|
5828
|
+
const filePath = await downloadJobArtifacts(project_id, job_id, local_path);
|
|
5829
|
+
return {
|
|
5830
|
+
content: [
|
|
5831
|
+
{
|
|
5832
|
+
type: "text",
|
|
5833
|
+
text: JSON.stringify({ success: true, file_path: filePath }, null, 2),
|
|
5834
|
+
},
|
|
5835
|
+
],
|
|
5836
|
+
};
|
|
5837
|
+
}
|
|
5838
|
+
case "get_job_artifact_file": {
|
|
5839
|
+
const { project_id, job_id, artifact_path } = GetJobArtifactFileSchema.parse(params.arguments);
|
|
5840
|
+
const fileContent = await getJobArtifactFile(project_id, job_id, artifact_path);
|
|
5841
|
+
return {
|
|
5842
|
+
content: [
|
|
5843
|
+
{
|
|
5844
|
+
type: "text",
|
|
5845
|
+
text: fileContent,
|
|
5846
|
+
},
|
|
5847
|
+
],
|
|
5848
|
+
};
|
|
5849
|
+
}
|
|
5261
5850
|
case "list_merge_requests": {
|
|
5262
5851
|
const args = ListMergeRequestsSchema.parse(params.arguments);
|
|
5263
5852
|
const mergeRequests = await listMergeRequests(args.project_id, args);
|
|
@@ -6004,9 +6593,10 @@ async function runServer() {
|
|
|
6004
6593
|
logger.info("Using OAuth authentication...");
|
|
6005
6594
|
try {
|
|
6006
6595
|
const gitlabBaseUrl = GITLAB_API_URL.replace(/\/api\/v4$/, "");
|
|
6007
|
-
|
|
6596
|
+
const oauthResult = await initializeOAuthClient(gitlabBaseUrl);
|
|
6597
|
+
oauthClient = oauthResult.client;
|
|
6598
|
+
OAUTH_ACCESS_TOKEN = oauthResult.accessToken;
|
|
6008
6599
|
logger.info("OAuth authentication successful");
|
|
6009
|
-
// Note: Headers are automatically generated by buildAuthHeaders() using OAUTH_ACCESS_TOKEN
|
|
6010
6600
|
}
|
|
6011
6601
|
catch (error) {
|
|
6012
6602
|
logger.error("OAuth authentication failed:", error);
|