@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/build/oauth.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as fs from "fs";
2
+ import * as os from "os";
2
3
  import * as path from "path";
3
4
  import * as http from "http";
4
5
  import * as net from "net";
@@ -82,7 +83,7 @@ export class GitLabOAuth {
82
83
  constructor(config) {
83
84
  this.config = config;
84
85
  this.tokenStoragePath =
85
- config.tokenStoragePath || path.join(process.env.HOME || "", ".gitlab-mcp-token.json");
86
+ config.tokenStoragePath || path.join(os.homedir(), ".gitlab-mcp-token.json");
86
87
  }
87
88
  /**
88
89
  * Get the authorization URL for OAuth flow
@@ -505,9 +506,11 @@ export class GitLabOAuth {
505
506
  }
506
507
  }
507
508
  /**
508
- * Initialize OAuth authentication for GitLab MCP server
509
+ * Create and initialize a GitLabOAuth client.
510
+ * Performs initial authentication (triggers browser flow if needed).
511
+ * Returns the client instance and the initial access token.
509
512
  */
510
- export async function initializeOAuth(gitlabUrl = "https://gitlab.com") {
513
+ export async function initializeOAuthClient(gitlabUrl = "https://gitlab.com") {
511
514
  const clientId = process.env.GITLAB_OAUTH_CLIENT_ID;
512
515
  const clientSecret = process.env.GITLAB_OAUTH_CLIENT_SECRET;
513
516
  const redirectUri = process.env.GITLAB_OAUTH_REDIRECT_URI || "http://127.0.0.1:8888/callback";
@@ -523,5 +526,14 @@ export async function initializeOAuth(gitlabUrl = "https://gitlab.com") {
523
526
  scopes: ["api"],
524
527
  tokenStoragePath,
525
528
  });
526
- return await oauth.getAccessToken();
529
+ // Single call: triggers browser flow if needed, or reads cached token
530
+ const accessToken = await oauth.getAccessToken();
531
+ return { client: oauth, accessToken };
532
+ }
533
+ /**
534
+ * Initialize OAuth authentication for GitLab MCP server
535
+ */
536
+ export async function initializeOAuth(gitlabUrl = "https://gitlab.com") {
537
+ const { accessToken } = await initializeOAuthClient(gitlabUrl);
538
+ return accessToken;
527
539
  }
package/build/schemas.js CHANGED
@@ -62,6 +62,8 @@ export const GitLabPipelineJobSchema = z.object({
62
62
  started_at: z.string().nullable().optional(),
63
63
  finished_at: z.string().nullable().optional(),
64
64
  duration: z.number().nullable().optional(),
65
+ queued_duration: z.number().nullable().optional(),
66
+ failure_reason: z.string().nullable().optional(),
65
67
  user: z
66
68
  .object({
67
69
  id: z.coerce.string(),
@@ -89,6 +91,19 @@ export const GitLabPipelineJobSchema = z.object({
89
91
  })
90
92
  .optional(),
91
93
  web_url: z.string().optional(),
94
+ allow_failure: z.boolean().optional(),
95
+ retried: z.boolean().optional(),
96
+ tag_list: z.array(z.string()).optional(),
97
+ runner: z
98
+ .object({
99
+ id: z.coerce.string().optional(),
100
+ description: z.string().nullable().optional(),
101
+ active: z.boolean().optional(),
102
+ is_shared: z.boolean().optional(),
103
+ runner_type: z.string().optional(),
104
+ })
105
+ .nullable()
106
+ .optional(),
92
107
  });
93
108
  // Pipeline trigger job (bridge) schema
94
109
  export const GitLabPipelineTriggerJobSchema = z.object({
@@ -247,6 +262,126 @@ export const ListPipelineTriggerJobsSchema = z
247
262
  .describe("The scope of trigger jobs to show"),
248
263
  })
249
264
  .merge(PaginationOptionsSchema);
265
+ // Deployment related schemas
266
+ export const GitLabDeploymentSchema = z.object({
267
+ id: z.coerce.string(),
268
+ iid: z.coerce.string().optional(),
269
+ status: z.string(),
270
+ ref: z.string().optional(),
271
+ sha: z.string(),
272
+ created_at: z.string(),
273
+ updated_at: z.string().optional(),
274
+ finished_at: z.string().nullable().optional(),
275
+ environment: z
276
+ .object({
277
+ id: z.coerce.string().optional(),
278
+ name: z.string(),
279
+ slug: z.string().optional(),
280
+ external_url: z.string().nullable().optional(),
281
+ state: z.string().optional(),
282
+ tier: z.string().optional(),
283
+ })
284
+ .optional(),
285
+ deployable: z
286
+ .object({
287
+ id: z.coerce.string().optional(),
288
+ name: z.string().optional(),
289
+ status: z.string().optional(),
290
+ stage: z.string().optional(),
291
+ web_url: z.string().optional(),
292
+ pipeline: z
293
+ .object({
294
+ id: z.coerce.string().optional(),
295
+ status: z.string().optional(),
296
+ ref: z.string().optional(),
297
+ sha: z.string().optional(),
298
+ web_url: z.string().optional(),
299
+ })
300
+ .optional(),
301
+ })
302
+ .nullable()
303
+ .optional(),
304
+ user: z
305
+ .object({
306
+ id: z.coerce.string().optional(),
307
+ username: z.string().optional(),
308
+ name: z.string().optional(),
309
+ avatar_url: z.string().nullable().optional(),
310
+ })
311
+ .optional(),
312
+ web_url: z.string().optional(),
313
+ });
314
+ export const ListDeploymentsSchema = z
315
+ .object({
316
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
317
+ environment: z.string().optional().describe("Filter by environment name"),
318
+ ref: z.string().optional().describe("Filter by ref"),
319
+ sha: z
320
+ .string()
321
+ .optional()
322
+ .describe("Filter by commit SHA (if supported by your GitLab version)"),
323
+ status: z.string().optional().describe("Filter by deployment status"),
324
+ updated_after: z
325
+ .string()
326
+ .optional()
327
+ .describe("Return deployments updated after the specified date"),
328
+ updated_before: z
329
+ .string()
330
+ .optional()
331
+ .describe("Return deployments updated before the specified date"),
332
+ order_by: z
333
+ .enum(["id", "iid", "created_at", "updated_at", "ref", "status", "environment"])
334
+ .optional()
335
+ .describe("Order deployments by"),
336
+ sort: z.enum(["asc", "desc"]).optional().describe("Sort deployments"),
337
+ })
338
+ .merge(PaginationOptionsSchema);
339
+ export const GetDeploymentSchema = z.object({
340
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
341
+ deployment_id: z.coerce.string().describe("The ID of the deployment"),
342
+ });
343
+ // Environment related schemas
344
+ const GitLabEnvironmentLastDeploymentSchema = z.object({
345
+ id: z.coerce.string().optional(),
346
+ iid: z.coerce.string().optional(),
347
+ status: z.string().optional(),
348
+ ref: z.string().optional(),
349
+ sha: z.string().optional(),
350
+ created_at: z.string().optional(),
351
+ updated_at: z.string().optional(),
352
+ web_url: z.string().optional(),
353
+ });
354
+ export const GitLabEnvironmentSchema = z.object({
355
+ id: z.coerce.string(),
356
+ name: z.string(),
357
+ slug: z.string().optional(),
358
+ external_url: z.string().nullable().optional(),
359
+ state: z.string().optional(),
360
+ tier: z.string().optional(),
361
+ environment_type: z.string().optional(),
362
+ created_at: z.string().optional(),
363
+ updated_at: z.string().optional(),
364
+ auto_stop_at: z.string().nullable().optional(),
365
+ enable_advanced_logs_querying: z.boolean().optional(),
366
+ logs_api_path: z.string().optional(),
367
+ web_url: z.string().optional(),
368
+ last_deployment: GitLabEnvironmentLastDeploymentSchema.nullable().optional(),
369
+ });
370
+ export const ListEnvironmentsSchema = z
371
+ .object({
372
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
373
+ name: z.string().optional().describe("Return environments with this exact name"),
374
+ search: z.string().optional().describe("Search environments by name"),
375
+ states: z
376
+ .enum(["available", "stopped"])
377
+ .optional()
378
+ .describe("Filter environments by state"),
379
+ })
380
+ .merge(PaginationOptionsSchema);
381
+ export const GetEnvironmentSchema = z.object({
382
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
383
+ environment_id: z.coerce.string().describe("The ID of the environment"),
384
+ });
250
385
  // Schema for creating a new pipeline
251
386
  export const CreatePipelineSchema = z.object({
252
387
  project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
@@ -258,6 +393,10 @@ export const CreatePipelineSchema = z.object({
258
393
  }))
259
394
  .optional()
260
395
  .describe("An array of variables to use for the pipeline"),
396
+ inputs: z
397
+ .record(z.string(), z.string())
398
+ .optional()
399
+ .describe("Input parameters for the pipeline (key-value pairs for spec:inputs)"),
261
400
  });
262
401
  // Schema for retrying a pipeline
263
402
  export const RetryPipelineSchema = z.object({
@@ -436,17 +575,17 @@ export const GitLabRepositorySchema = z.object({
436
575
  export const GitLabProjectSchema = GitLabRepositorySchema;
437
576
  // File content schemas
438
577
  export const GitLabFileContentSchema = z.object({
439
- file_name: z.string(), // Changed from name to match GitLab API
440
- file_path: z.string(), // Changed from path to match GitLab API
441
- size: z.number(),
578
+ file_name: z.string().optional(),
579
+ file_path: z.string(),
580
+ size: z.coerce.number().optional(),
442
581
  encoding: z.string(),
443
582
  content: z.string(),
444
- content_sha256: z.string(), // Changed from sha to match GitLab API
445
- ref: z.string(), // Added as GitLab requires branch reference
446
- blob_id: z.string(), // Added to match GitLab API
447
- commit_id: z.string(), // ID of the current file version
448
- last_commit_id: z.string(), // Added to match GitLab API
449
- execute_filemode: z.boolean().optional(), // Added to match GitLab API
583
+ content_sha256: z.string().optional(),
584
+ ref: z.string().optional(),
585
+ blob_id: z.string().optional(),
586
+ commit_id: z.string().optional(),
587
+ last_commit_id: z.string().optional(),
588
+ execute_filemode: z.boolean().optional(),
450
589
  });
451
590
  export const GitLabDirectoryContentSchema = z.object({
452
591
  name: z.string(),
@@ -713,6 +852,15 @@ export const GitLabMergeRequestSchema = z.object({
713
852
  allow_collaboration: z.boolean().optional(),
714
853
  allow_maintainer_to_push: z.boolean().optional(),
715
854
  changes_count: z.string().nullable().optional(),
855
+ diverged_commits_count: z.coerce
856
+ .number()
857
+ .nullable()
858
+ .optional()
859
+ .describe("Number of commits the source branch is behind the target branch"),
860
+ rebase_in_progress: z
861
+ .boolean()
862
+ .optional()
863
+ .describe("Whether rebase is currently in progress for this merge request"),
716
864
  merge_when_pipeline_succeeds: z.boolean().optional(),
717
865
  squash: z.boolean().optional(),
718
866
  labels: z.array(z.string()).optional(),
@@ -953,10 +1101,43 @@ export const CreateRepositorySchema = z.object({
953
1101
  .describe("Repository visibility level"),
954
1102
  initialize_with_readme: z.boolean().optional().describe("Initialize with README.md"),
955
1103
  });
956
- export const GetFileContentsSchema = ProjectParamsSchema.extend({
957
- file_path: z.string().describe("Path to the file or directory"),
1104
+ export const GetFileContentsSchema = z
1105
+ .object({
1106
+ project_id: z.coerce
1107
+ .string()
1108
+ .optional()
1109
+ .describe("Project ID or URL-encoded path (optional; falls back to env)"),
1110
+ file_path: z
1111
+ .string()
1112
+ .optional()
1113
+ .describe("Path to the file or directory. Takes precedence over 'path' when both are provided"),
1114
+ path: z.string().optional().describe("Alias of file_path"),
958
1115
  ref: z.string().optional().describe("Branch/tag/commit to get contents from"),
959
- });
1116
+ })
1117
+ .superRefine((data, ctx) => {
1118
+ const fp = data.file_path?.trim();
1119
+ const p = data.path?.trim();
1120
+ if (!fp && !p) {
1121
+ ctx.addIssue({
1122
+ code: z.ZodIssueCode.custom,
1123
+ message: "Either 'file_path' or 'path' must be provided",
1124
+ path: ["file_path"],
1125
+ });
1126
+ }
1127
+ const finalPath = fp && fp.length > 0 ? fp : p ?? "";
1128
+ if (finalPath.trim().length === 0) {
1129
+ ctx.addIssue({
1130
+ code: z.ZodIssueCode.custom,
1131
+ message: "file_path cannot be empty or whitespace",
1132
+ path: ["file_path"],
1133
+ });
1134
+ }
1135
+ })
1136
+ .transform(data => ({
1137
+ project_id: (data.project_id ?? "").trim() || undefined,
1138
+ file_path: ((data.file_path ?? "").trim() || (data.path ?? "").trim()).trim(),
1139
+ ref: (data.ref ?? "").trim() || undefined,
1140
+ }));
960
1141
  export const PushFilesSchema = ProjectParamsSchema.extend({
961
1142
  branch: z.string().describe("Branch to push to"),
962
1143
  files: z
@@ -1129,9 +1310,25 @@ export const GitLabApprovalRuleSchema = z.object({
1129
1310
  .optional(),
1130
1311
  approved: z.boolean().optional(),
1131
1312
  });
1313
+ export const GitLabMergeRequestApprovalsResponseSchema = z.object({
1314
+ approved: z.boolean().optional(),
1315
+ user_has_approved: z.boolean().optional(),
1316
+ user_can_approve: z.boolean().optional(),
1317
+ approved_by: z
1318
+ .array(z.object({
1319
+ user: GitLabApprovalUserSchema,
1320
+ }))
1321
+ .optional(),
1322
+ });
1132
1323
  export const GitLabMergeRequestApprovalStateSchema = z.object({
1133
1324
  approval_rules_overwritten: z.boolean().optional(),
1134
1325
  rules: z.array(GitLabApprovalRuleSchema).optional(),
1326
+ approved: z.boolean().optional(),
1327
+ user_has_approved: z.boolean().optional(),
1328
+ user_can_approve: z.boolean().optional(),
1329
+ approved_by: z.array(GitLabApprovalUserSchema).optional(),
1330
+ approved_by_usernames: z.array(z.string()).optional(),
1331
+ source_endpoint: z.enum(["approval_state", "approvals"]).optional(),
1135
1332
  });
1136
1333
  export const GetMergeRequestApprovalStateSchema = ProjectParamsSchema.extend({
1137
1334
  merge_request_iid: z.coerce.string().describe("The IID of the merge request"),
@@ -2210,6 +2407,41 @@ export const CreateReleaseEvidenceSchema = z.object({
2210
2407
  project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
2211
2408
  tag_name: z.string().describe("The Git tag the release is associated with"),
2212
2409
  });
2410
+ // Job Artifacts schemas
2411
+ export const ListJobArtifactsSchema = z.object({
2412
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
2413
+ job_id: z.coerce.string().describe("The ID of the job"),
2414
+ path: z
2415
+ .string()
2416
+ .optional()
2417
+ .describe("Directory path within the artifacts archive (defaults to root)"),
2418
+ recursive: z
2419
+ .boolean()
2420
+ .optional()
2421
+ .describe("Whether to list artifacts recursively"),
2422
+ });
2423
+ export const GitLabArtifactEntrySchema = z.object({
2424
+ name: z.string(),
2425
+ path: z.string(),
2426
+ type: z.enum(["file", "directory"]),
2427
+ size: z.number().optional(),
2428
+ mode: z.string().optional(),
2429
+ });
2430
+ export const DownloadJobArtifactsSchema = z.object({
2431
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
2432
+ job_id: z.coerce.string().describe("The ID of the job"),
2433
+ local_path: z
2434
+ .string()
2435
+ .optional()
2436
+ .describe("Local directory to save the artifact archive (defaults to current directory)"),
2437
+ });
2438
+ export const GetJobArtifactFileSchema = z.object({
2439
+ project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
2440
+ job_id: z.coerce.string().describe("The ID of the job"),
2441
+ artifact_path: z
2442
+ .string()
2443
+ .describe("Path to the file within the artifacts archive"),
2444
+ });
2213
2445
  export const DownloadReleaseAssetSchema = z.object({
2214
2446
  project_id: z.coerce.string().describe("Project ID or URL-encoded path"),
2215
2447
  tag_name: z.string().describe("The Git tag the release is associated with"),
@@ -0,0 +1,311 @@
1
+ #!/usr/bin/env ts-node
2
+ import { GetFileContentsSchema, GitLabFileContentSchema, CreatePipelineSchema } from '../schemas.js';
3
+ function runGetFileContentsSchemaTests() {
4
+ console.log('🧪 Testing GetFileContentsSchema...');
5
+ const cases = [
6
+ {
7
+ name: 'schema:get_file_contents:path-only',
8
+ input: { path: 'package.json' },
9
+ expected: { file_path: 'package.json', project_id: undefined, ref: undefined }
10
+ },
11
+ {
12
+ name: 'schema:get_file_contents:file-path-precedence',
13
+ input: { file_path: ' README.md ', path: 'package.json' },
14
+ expected: { file_path: 'README.md', project_id: undefined, ref: undefined }
15
+ },
16
+ {
17
+ name: 'schema:get_file_contents:project-id-trim',
18
+ input: { project_id: ' 123 ', file_path: 'a.txt' },
19
+ expected: { project_id: '123', file_path: 'a.txt', ref: undefined }
20
+ },
21
+ {
22
+ name: 'schema:get_file_contents:ref-trim-to-undefined',
23
+ input: { file_path: 'a.txt', ref: ' ' },
24
+ expected: { file_path: 'a.txt', project_id: undefined, ref: undefined }
25
+ },
26
+ {
27
+ name: 'schema:get_file_contents:file-path-empty-fallback-to-path',
28
+ input: { file_path: ' ', path: ' src/index.ts ' },
29
+ expected: { file_path: 'src/index.ts', project_id: undefined, ref: undefined }
30
+ },
31
+ {
32
+ name: 'schema:get_file_contents:project-id-omitted-remains-undefined',
33
+ input: { file_path: 'README.md' },
34
+ expected: { file_path: 'README.md', project_id: undefined, ref: undefined }
35
+ },
36
+ {
37
+ name: 'schema:get_file_contents:reject-empty-path',
38
+ input: { path: ' ' },
39
+ shouldFail: true
40
+ },
41
+ {
42
+ name: 'schema:get_file_contents:reject-both-empty-after-trim',
43
+ input: { file_path: ' ', path: ' ' },
44
+ shouldFail: true
45
+ }
46
+ ];
47
+ let passed = 0;
48
+ let failed = 0;
49
+ cases.forEach(testCase => {
50
+ const result = {
51
+ name: testCase.name,
52
+ status: 'failed'
53
+ };
54
+ const parsed = GetFileContentsSchema.safeParse(testCase.input);
55
+ if (testCase.shouldFail) {
56
+ if (parsed.success) {
57
+ result.error = 'Expected schema validation to fail';
58
+ }
59
+ else {
60
+ result.status = 'passed';
61
+ }
62
+ }
63
+ else if (parsed.success) {
64
+ const { project_id, file_path, ref } = parsed.data;
65
+ const expected = testCase.expected || {};
66
+ const matches = project_id === expected.project_id && file_path === expected.file_path && ref === expected.ref;
67
+ if (matches) {
68
+ result.status = 'passed';
69
+ }
70
+ else {
71
+ result.error = `Unexpected parsed result: ${JSON.stringify(parsed.data)}`;
72
+ }
73
+ }
74
+ else {
75
+ result.error = parsed.error?.message || 'Schema validation failed';
76
+ }
77
+ if (result.status === 'passed') {
78
+ passed++;
79
+ console.log(`✅ ${result.name}`);
80
+ }
81
+ else {
82
+ failed++;
83
+ console.log(`❌ ${result.name}: ${result.error}`);
84
+ }
85
+ });
86
+ console.log(`\nResults: ${passed} passed, ${failed} failed`);
87
+ return { passed, failed };
88
+ }
89
+ function runGitLabFileContentSchemaTests() {
90
+ console.log('\n🧪 Testing GitLabFileContentSchema...');
91
+ const cases = [
92
+ {
93
+ name: 'schema:gitlab_file_content:minimal-required-fields',
94
+ input: {
95
+ file_path: 'README.md',
96
+ encoding: 'base64',
97
+ content: 'IyBSRUFETUU='
98
+ },
99
+ expected: {
100
+ file_path: 'README.md',
101
+ encoding: 'base64',
102
+ content: 'IyBSRUFETUU='
103
+ }
104
+ },
105
+ {
106
+ name: 'schema:gitlab_file_content:optional-size-coerces-to-number',
107
+ input: {
108
+ file_name: 'README.md',
109
+ file_path: 'README.md',
110
+ size: '42',
111
+ encoding: 'base64',
112
+ content: 'IyBSRUFETUU=',
113
+ ref: 'main'
114
+ },
115
+ expected: {
116
+ file_name: 'README.md',
117
+ file_path: 'README.md',
118
+ size: 42,
119
+ encoding: 'base64',
120
+ content: 'IyBSRUFETUU=',
121
+ ref: 'main'
122
+ }
123
+ },
124
+ {
125
+ name: 'schema:gitlab_file_content:reject-missing-content',
126
+ input: {
127
+ file_path: 'README.md',
128
+ encoding: 'base64'
129
+ },
130
+ shouldFail: true
131
+ },
132
+ {
133
+ name: 'schema:gitlab_file_content:reject-missing-encoding',
134
+ input: {
135
+ file_path: 'README.md',
136
+ content: 'IyBSRUFETUU='
137
+ },
138
+ shouldFail: true
139
+ },
140
+ {
141
+ name: 'schema:gitlab_file_content:reject-missing-file-path',
142
+ input: {
143
+ encoding: 'base64',
144
+ content: 'IyBSRUFETUU='
145
+ },
146
+ shouldFail: true
147
+ }
148
+ ];
149
+ let passed = 0;
150
+ let failed = 0;
151
+ cases.forEach(testCase => {
152
+ const result = {
153
+ name: testCase.name,
154
+ status: 'failed'
155
+ };
156
+ const parsed = GitLabFileContentSchema.safeParse(testCase.input);
157
+ if (testCase.shouldFail) {
158
+ if (parsed.success) {
159
+ result.error = 'Expected schema validation to fail';
160
+ }
161
+ else {
162
+ result.status = 'passed';
163
+ }
164
+ }
165
+ else if (parsed.success) {
166
+ const expected = testCase.expected || {};
167
+ const matches = Object.entries(expected).every(([key, value]) => {
168
+ return parsed.data[key] === value;
169
+ });
170
+ if (matches) {
171
+ result.status = 'passed';
172
+ }
173
+ else {
174
+ result.error = `Unexpected parsed result: ${JSON.stringify(parsed.data)}`;
175
+ }
176
+ }
177
+ else {
178
+ result.error = parsed.error?.message || 'Schema validation failed';
179
+ }
180
+ if (result.status === 'passed') {
181
+ passed++;
182
+ console.log(`✅ ${result.name}`);
183
+ }
184
+ else {
185
+ failed++;
186
+ console.log(`❌ ${result.name}: ${result.error}`);
187
+ }
188
+ });
189
+ console.log(`\nResults: ${passed} passed, ${failed} failed`);
190
+ return { passed, failed };
191
+ }
192
+ function runCreatePipelineSchemaTests() {
193
+ console.log('\n🧪 Testing CreatePipelineSchema...');
194
+ const cases = [
195
+ {
196
+ name: 'schema:create_pipeline:minimal-required-fields',
197
+ input: { project_id: 'my/project', ref: 'main' },
198
+ expected: { project_id: 'my/project', ref: 'main' }
199
+ },
200
+ {
201
+ name: 'schema:create_pipeline:with-variables-only',
202
+ input: {
203
+ project_id: 'my/project',
204
+ ref: 'main',
205
+ variables: [{ key: 'ENV', value: 'production' }]
206
+ },
207
+ expected: {
208
+ project_id: 'my/project',
209
+ ref: 'main',
210
+ variables: [{ key: 'ENV', value: 'production' }]
211
+ }
212
+ },
213
+ {
214
+ name: 'schema:create_pipeline:with-inputs-only',
215
+ input: {
216
+ project_id: 'my/project',
217
+ ref: 'main',
218
+ inputs: { deploy_target: 'staging', version: '1.0.0' }
219
+ },
220
+ expected: {
221
+ project_id: 'my/project',
222
+ ref: 'main',
223
+ inputs: { deploy_target: 'staging', version: '1.0.0' }
224
+ }
225
+ },
226
+ {
227
+ name: 'schema:create_pipeline:with-variables-and-inputs',
228
+ input: {
229
+ project_id: 'my/project',
230
+ ref: 'develop',
231
+ variables: [{ key: 'CI', value: 'true' }],
232
+ inputs: { env: 'test' }
233
+ },
234
+ expected: {
235
+ project_id: 'my/project',
236
+ ref: 'develop',
237
+ variables: [{ key: 'CI', value: 'true' }],
238
+ inputs: { env: 'test' }
239
+ }
240
+ },
241
+ {
242
+ name: 'schema:create_pipeline:project-id-coercion',
243
+ input: { project_id: 123, ref: 'main' },
244
+ expected: { project_id: '123', ref: 'main' }
245
+ },
246
+ {
247
+ name: 'schema:create_pipeline:reject-missing-ref',
248
+ input: { project_id: 'my/project' },
249
+ shouldFail: true
250
+ },
251
+ {
252
+ name: 'schema:create_pipeline:reject-invalid-inputs-type',
253
+ input: { project_id: 'my/project', ref: 'main', inputs: 'not-an-object' },
254
+ shouldFail: true
255
+ }
256
+ ];
257
+ let passed = 0;
258
+ let failed = 0;
259
+ cases.forEach(testCase => {
260
+ const result = {
261
+ name: testCase.name,
262
+ status: 'failed'
263
+ };
264
+ const parsed = CreatePipelineSchema.safeParse(testCase.input);
265
+ if (testCase.shouldFail) {
266
+ if (parsed.success) {
267
+ result.error = 'Expected schema validation to fail';
268
+ }
269
+ else {
270
+ result.status = 'passed';
271
+ }
272
+ }
273
+ else if (parsed.success) {
274
+ const expected = testCase.expected || {};
275
+ const matches = Object.entries(expected).every(([key, value]) => {
276
+ const actual = parsed.data[key];
277
+ return JSON.stringify(actual) === JSON.stringify(value);
278
+ });
279
+ if (matches) {
280
+ result.status = 'passed';
281
+ }
282
+ else {
283
+ result.error = `Unexpected parsed result: ${JSON.stringify(parsed.data)}`;
284
+ }
285
+ }
286
+ else {
287
+ result.error = parsed.error?.message || 'Schema validation failed';
288
+ }
289
+ if (result.status === 'passed') {
290
+ passed++;
291
+ console.log(`✅ ${result.name}`);
292
+ }
293
+ else {
294
+ failed++;
295
+ console.log(`❌ ${result.name}: ${result.error}`);
296
+ }
297
+ });
298
+ console.log(`\nResults: ${passed} passed, ${failed} failed`);
299
+ return { passed, failed };
300
+ }
301
+ if (import.meta.url === `file://${process.argv[1]}`) {
302
+ const getFileContentsResult = runGetFileContentsSchemaTests();
303
+ const fileContentResult = runGitLabFileContentSchemaTests();
304
+ const createPipelineResult = runCreatePipelineSchemaTests();
305
+ const totalPassed = getFileContentsResult.passed + fileContentResult.passed + createPipelineResult.passed;
306
+ const totalFailed = getFileContentsResult.failed + fileContentResult.failed + createPipelineResult.failed;
307
+ console.log(`\nTotal Results: ${totalPassed} passed, ${totalFailed} failed`);
308
+ if (totalFailed > 0) {
309
+ process.exit(1);
310
+ }
311
+ }