gitlab-mcp 0.1.5 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.dockerignore +7 -0
  2. package/.editorconfig +9 -0
  3. package/.env.example +75 -0
  4. package/.github/workflows/nodejs.yml +31 -0
  5. package/.github/workflows/npm-publish.yml +31 -0
  6. package/.husky/pre-commit +1 -0
  7. package/.nvmrc +1 -0
  8. package/.prettierrc.json +6 -0
  9. package/Dockerfile +20 -0
  10. package/README.md +416 -251
  11. package/docker-compose.yml +10 -0
  12. package/docs/architecture.md +310 -0
  13. package/docs/authentication.md +299 -0
  14. package/docs/configuration.md +149 -0
  15. package/docs/deployment.md +336 -0
  16. package/docs/tools.md +294 -0
  17. package/eslint.config.js +23 -0
  18. package/package.json +78 -32
  19. package/scripts/get-oauth-token.example.sh +15 -0
  20. package/src/config/env.ts +171 -0
  21. package/src/http.ts +620 -0
  22. package/src/index.ts +77 -0
  23. package/src/lib/auth-context.ts +19 -0
  24. package/src/lib/gitlab-client.ts +1810 -0
  25. package/src/lib/logger.ts +17 -0
  26. package/src/lib/network.ts +45 -0
  27. package/src/lib/oauth.ts +287 -0
  28. package/src/lib/output.ts +51 -0
  29. package/src/lib/policy.ts +78 -0
  30. package/src/lib/request-runtime.ts +376 -0
  31. package/src/lib/sanitize.ts +25 -0
  32. package/src/lib/session-capacity.ts +14 -0
  33. package/src/server/build-server.ts +17 -0
  34. package/src/tools/gitlab.ts +3135 -0
  35. package/src/tools/health.ts +27 -0
  36. package/src/tools/mr-code-context.ts +473 -0
  37. package/src/types/context.ts +13 -0
  38. package/tests/auth-context.test.ts +102 -0
  39. package/tests/gitlab-client.test.ts +672 -0
  40. package/tests/graphql-guard.test.ts +121 -0
  41. package/tests/integration/agent-loop.integration.test.ts +558 -0
  42. package/tests/integration/server.integration.test.ts +543 -0
  43. package/tests/mr-code-context.test.ts +600 -0
  44. package/tests/oauth.test.ts +43 -0
  45. package/tests/output.test.ts +186 -0
  46. package/tests/policy.test.ts +324 -0
  47. package/tests/request-runtime.test.ts +252 -0
  48. package/tests/sanitize.test.ts +123 -0
  49. package/tests/session-capacity.test.ts +49 -0
  50. package/tests/upload-reference.test.ts +88 -0
  51. package/tsconfig.build.json +11 -0
  52. package/tsconfig.json +21 -0
  53. package/vitest.config.ts +12 -0
  54. package/LICENSE +0 -21
  55. package/build/index.js +0 -1642
  56. package/build/schemas.js +0 -684
  57. package/build/test-note.js +0 -54
@@ -0,0 +1,1810 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+
4
+ import { getSessionAuth, type SessionAuth } from "./auth-context.js";
5
+
6
+ export interface GitLabClientOptions {
7
+ timeoutMs?: number;
8
+ apiUrls?: string[];
9
+ beforeRequest?: (
10
+ context: GitLabBeforeRequestContext
11
+ ) => Promise<GitLabBeforeRequestResult | void>;
12
+ }
13
+
14
+ export interface GitLabRequestOptions {
15
+ query?: Record<string, string | number | boolean | undefined | null>;
16
+ body?: BodyInit;
17
+ headers?: HeadersInit;
18
+ token?: string;
19
+ apiUrl?: string;
20
+ }
21
+
22
+ export interface GitLabBeforeRequestContext {
23
+ url: URL;
24
+ method: string;
25
+ headers: Headers;
26
+ body?: BodyInit;
27
+ token?: string;
28
+ }
29
+
30
+ export interface GitLabBeforeRequestResult {
31
+ headers?: Headers;
32
+ body?: BodyInit;
33
+ token?: string;
34
+ fetchImpl?: typeof fetch;
35
+ }
36
+
37
+ export interface GitLabProject {
38
+ id: number;
39
+ name: string;
40
+ description: string | null;
41
+ path_with_namespace: string;
42
+ default_branch: string | null;
43
+ web_url: string;
44
+ visibility: string;
45
+ last_activity_at: string;
46
+ }
47
+
48
+ export interface PushFileAction {
49
+ action: "create" | "delete" | "move" | "update" | "chmod";
50
+ file_path: string;
51
+ previous_path?: string;
52
+ content?: string;
53
+ encoding?: "text" | "base64";
54
+ execute_filemode?: boolean;
55
+ last_commit_id?: string;
56
+ }
57
+
58
+ export interface MergeRequestCodeContextFile {
59
+ old_path: string;
60
+ new_path: string;
61
+ new_file: boolean;
62
+ renamed_file: boolean;
63
+ deleted_file: boolean;
64
+ diff: string;
65
+ }
66
+
67
+ export class GitLabApiError extends Error {
68
+ constructor(
69
+ message: string,
70
+ public readonly status: number,
71
+ public readonly details?: unknown
72
+ ) {
73
+ super(message);
74
+ this.name = "GitLabApiError";
75
+ }
76
+ }
77
+
78
+ export class GitLabClient {
79
+ private readonly baseApiUrl: string;
80
+ private readonly apiUrls: string[];
81
+ private nextApiUrlIndex = 0;
82
+ private readonly defaultToken?: string;
83
+ private readonly timeoutMs: number;
84
+ private readonly beforeRequest?: GitLabClientOptions["beforeRequest"];
85
+
86
+ constructor(baseApiUrl: string, defaultToken?: string, options: GitLabClientOptions = {}) {
87
+ this.baseApiUrl = normalizeApiUrl(baseApiUrl);
88
+ const configuredApiUrls = options.apiUrls
89
+ ?.map((item) => normalizeApiUrl(item))
90
+ .filter((item) => item.length > 0) ?? [this.baseApiUrl];
91
+ this.apiUrls = configuredApiUrls.length > 0 ? configuredApiUrls : [this.baseApiUrl];
92
+ this.defaultToken = defaultToken;
93
+ this.timeoutMs = options.timeoutMs ?? 20_000;
94
+ this.beforeRequest = options.beforeRequest;
95
+ }
96
+
97
+ // projects
98
+ getProject(projectId: string, options?: GitLabRequestOptions): Promise<unknown> {
99
+ return this.get(`/projects/${encode(projectId)}`, options);
100
+ }
101
+
102
+ listProjects(options: GitLabRequestOptions = {}): Promise<unknown> {
103
+ return this.get("/projects", options);
104
+ }
105
+
106
+ createRepository(
107
+ payload: {
108
+ name: string;
109
+ description?: string;
110
+ visibility?: "private" | "internal" | "public";
111
+ initialize_with_readme?: boolean;
112
+ path?: string;
113
+ namespace_id?: string | number;
114
+ default_branch?: string;
115
+ },
116
+ options: GitLabRequestOptions = {}
117
+ ): Promise<unknown> {
118
+ return this.post("/projects", {
119
+ ...options,
120
+ body: JSON.stringify(payload),
121
+ headers: {
122
+ "Content-Type": "application/json",
123
+ ...(options.headers ?? {})
124
+ }
125
+ });
126
+ }
127
+
128
+ listProjectMembers(projectId: string, options: GitLabRequestOptions = {}): Promise<unknown> {
129
+ return this.get(`/projects/${encode(projectId)}/members/all`, options);
130
+ }
131
+
132
+ listGroupProjects(groupId: string, options: GitLabRequestOptions = {}): Promise<unknown> {
133
+ return this.get(`/groups/${encode(groupId)}/projects`, options);
134
+ }
135
+
136
+ forkRepository(
137
+ projectId: string,
138
+ payload: {
139
+ namespace?: string;
140
+ namespace_id?: string | number;
141
+ path?: string;
142
+ name?: string;
143
+ description?: string;
144
+ visibility?: "private" | "internal" | "public";
145
+ default_branch?: string;
146
+ } = {},
147
+ options: GitLabRequestOptions = {}
148
+ ): Promise<unknown> {
149
+ return this.post(`/projects/${encode(projectId)}/fork`, {
150
+ ...options,
151
+ body: JSON.stringify(payload),
152
+ headers: {
153
+ "Content-Type": "application/json",
154
+ ...(options.headers ?? {})
155
+ }
156
+ });
157
+ }
158
+
159
+ searchProjects(search: string, limit = 10, options: GitLabRequestOptions = {}): Promise<unknown> {
160
+ return this.get("/projects", {
161
+ ...options,
162
+ query: {
163
+ search,
164
+ simple: true,
165
+ per_page: limit,
166
+ ...(options.query ?? {})
167
+ }
168
+ });
169
+ }
170
+
171
+ searchRepositories(search: string, options: GitLabRequestOptions = {}): Promise<unknown> {
172
+ return this.get("/search", {
173
+ ...options,
174
+ query: {
175
+ scope: "projects",
176
+ search,
177
+ ...(options.query ?? {})
178
+ }
179
+ });
180
+ }
181
+
182
+ searchCodeBlobs(
183
+ projectId: string,
184
+ search: string,
185
+ options: GitLabRequestOptions = {}
186
+ ): Promise<unknown> {
187
+ return this.get(`/projects/${encode(projectId)}/search`, {
188
+ ...options,
189
+ query: {
190
+ scope: "blobs",
191
+ search,
192
+ ...(options.query ?? {})
193
+ }
194
+ });
195
+ }
196
+
197
+ // repository/files
198
+ getRepositoryTree(projectId: string, options: GitLabRequestOptions = {}): Promise<unknown> {
199
+ return this.get(`/projects/${encode(projectId)}/repository/tree`, options);
200
+ }
201
+
202
+ getFileContents(
203
+ projectId: string,
204
+ filePath: string,
205
+ ref: string,
206
+ options: GitLabRequestOptions = {}
207
+ ): Promise<unknown> {
208
+ return this.get(`/projects/${encode(projectId)}/repository/files/${encode(filePath)}`, {
209
+ ...options,
210
+ query: {
211
+ ref,
212
+ ...(options.query ?? {})
213
+ }
214
+ });
215
+ }
216
+
217
+ createOrUpdateFile(
218
+ projectId: string,
219
+ filePath: string,
220
+ payload: {
221
+ branch: string;
222
+ content: string;
223
+ commit_message: string;
224
+ author_email?: string;
225
+ author_name?: string;
226
+ encoding?: "text" | "base64";
227
+ execute_filemode?: boolean;
228
+ start_branch?: string;
229
+ last_commit_id?: string;
230
+ },
231
+ options: GitLabRequestOptions = {}
232
+ ): Promise<unknown> {
233
+ return this.put(`/projects/${encode(projectId)}/repository/files/${encode(filePath)}`, {
234
+ ...options,
235
+ body: JSON.stringify(payload),
236
+ headers: {
237
+ "Content-Type": "application/json",
238
+ ...(options.headers ?? {})
239
+ }
240
+ });
241
+ }
242
+
243
+ pushFiles(
244
+ projectId: string,
245
+ payload: {
246
+ branch: string;
247
+ commit_message: string;
248
+ actions: PushFileAction[];
249
+ start_branch?: string;
250
+ author_name?: string;
251
+ author_email?: string;
252
+ force?: boolean;
253
+ },
254
+ options: GitLabRequestOptions = {}
255
+ ): Promise<unknown> {
256
+ return this.post(`/projects/${encode(projectId)}/repository/commits`, {
257
+ ...options,
258
+ body: JSON.stringify(payload),
259
+ headers: {
260
+ "Content-Type": "application/json",
261
+ ...(options.headers ?? {})
262
+ }
263
+ });
264
+ }
265
+
266
+ createBranch(
267
+ projectId: string,
268
+ payload: {
269
+ branch: string;
270
+ ref: string;
271
+ },
272
+ options: GitLabRequestOptions = {}
273
+ ): Promise<unknown> {
274
+ return this.post(`/projects/${encode(projectId)}/repository/branches`, {
275
+ ...options,
276
+ query: payload
277
+ });
278
+ }
279
+
280
+ getBranchDiffs(
281
+ projectId: string,
282
+ payload: {
283
+ from: string;
284
+ to: string;
285
+ straight?: boolean;
286
+ },
287
+ options: GitLabRequestOptions = {}
288
+ ): Promise<unknown> {
289
+ return this.get(`/projects/${encode(projectId)}/repository/compare`, {
290
+ ...options,
291
+ query: payload
292
+ });
293
+ }
294
+
295
+ listCommits(projectId: string, options: GitLabRequestOptions = {}): Promise<unknown> {
296
+ return this.get(`/projects/${encode(projectId)}/repository/commits`, options);
297
+ }
298
+
299
+ getCommit(projectId: string, sha: string, options: GitLabRequestOptions = {}): Promise<unknown> {
300
+ return this.get(`/projects/${encode(projectId)}/repository/commits/${encode(sha)}`, options);
301
+ }
302
+
303
+ getCommitDiff(
304
+ projectId: string,
305
+ sha: string,
306
+ options: GitLabRequestOptions = {}
307
+ ): Promise<unknown> {
308
+ return this.get(
309
+ `/projects/${encode(projectId)}/repository/commits/${encode(sha)}/diff`,
310
+ options
311
+ );
312
+ }
313
+
314
+ // merge requests
315
+ listMergeRequests(projectId: string, options: GitLabRequestOptions = {}): Promise<unknown> {
316
+ return this.get(`/projects/${encode(projectId)}/merge_requests`, options);
317
+ }
318
+
319
+ listGlobalMergeRequests(options: GitLabRequestOptions = {}): Promise<unknown> {
320
+ return this.get("/merge_requests", options);
321
+ }
322
+
323
+ getMergeRequest(
324
+ projectId: string,
325
+ mergeRequestIid: string,
326
+ options: GitLabRequestOptions = {}
327
+ ): Promise<unknown> {
328
+ return this.get(
329
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}`,
330
+ options
331
+ );
332
+ }
333
+
334
+ createMergeRequest(
335
+ projectId: string,
336
+ payload: {
337
+ source_branch: string;
338
+ target_branch: string;
339
+ title: string;
340
+ description?: string;
341
+ target_project_id?: string | number;
342
+ assignee_ids?: number[];
343
+ reviewer_ids?: number[];
344
+ labels?: string;
345
+ allow_collaboration?: boolean;
346
+ remove_source_branch?: boolean;
347
+ squash?: boolean;
348
+ draft?: boolean;
349
+ },
350
+ options: GitLabRequestOptions = {}
351
+ ): Promise<unknown> {
352
+ return this.post(`/projects/${encode(projectId)}/merge_requests`, {
353
+ ...options,
354
+ body: JSON.stringify(payload),
355
+ headers: {
356
+ "Content-Type": "application/json",
357
+ ...(options.headers ?? {})
358
+ }
359
+ });
360
+ }
361
+
362
+ updateMergeRequest(
363
+ projectId: string,
364
+ mergeRequestIid: string,
365
+ payload: Record<string, unknown>,
366
+ options: GitLabRequestOptions = {}
367
+ ): Promise<unknown> {
368
+ return this.put(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}`, {
369
+ ...options,
370
+ body: JSON.stringify(payload),
371
+ headers: {
372
+ "Content-Type": "application/json",
373
+ ...(options.headers ?? {})
374
+ }
375
+ });
376
+ }
377
+
378
+ mergeMergeRequest(
379
+ projectId: string,
380
+ mergeRequestIid: string,
381
+ payload: Record<string, unknown> = {},
382
+ options: GitLabRequestOptions = {}
383
+ ): Promise<unknown> {
384
+ return this.put(
385
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/merge`,
386
+ {
387
+ ...options,
388
+ body: JSON.stringify(payload),
389
+ headers: {
390
+ "Content-Type": "application/json",
391
+ ...(options.headers ?? {})
392
+ }
393
+ }
394
+ );
395
+ }
396
+
397
+ getMergeRequestDiffs(
398
+ projectId: string,
399
+ mergeRequestIid: string,
400
+ options: GitLabRequestOptions = {}
401
+ ): Promise<unknown> {
402
+ return this.get(
403
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/changes`,
404
+ options
405
+ );
406
+ }
407
+
408
+ listMergeRequestDiffs(
409
+ projectId: string,
410
+ mergeRequestIid: string,
411
+ options: GitLabRequestOptions = {}
412
+ ): Promise<unknown> {
413
+ return this.get(
414
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/diffs`,
415
+ options
416
+ );
417
+ }
418
+
419
+ listMergeRequestVersions(
420
+ projectId: string,
421
+ mergeRequestIid: string,
422
+ options: GitLabRequestOptions = {}
423
+ ): Promise<unknown> {
424
+ return this.get(
425
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/versions`,
426
+ options
427
+ );
428
+ }
429
+
430
+ getMergeRequestVersion(
431
+ projectId: string,
432
+ mergeRequestIid: string,
433
+ versionId: string,
434
+ options: GitLabRequestOptions = {}
435
+ ): Promise<unknown> {
436
+ return this.get(
437
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/versions/${encode(versionId)}`,
438
+ options
439
+ );
440
+ }
441
+
442
+ approveMergeRequest(
443
+ projectId: string,
444
+ mergeRequestIid: string,
445
+ payload: Record<string, unknown> = {},
446
+ options: GitLabRequestOptions = {}
447
+ ): Promise<unknown> {
448
+ return this.post(
449
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/approve`,
450
+ {
451
+ ...options,
452
+ body: JSON.stringify(payload),
453
+ headers: {
454
+ "Content-Type": "application/json",
455
+ ...(options.headers ?? {})
456
+ }
457
+ }
458
+ );
459
+ }
460
+
461
+ unapproveMergeRequest(
462
+ projectId: string,
463
+ mergeRequestIid: string,
464
+ options: GitLabRequestOptions = {}
465
+ ): Promise<unknown> {
466
+ return this.post(
467
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/unapprove`,
468
+ {
469
+ ...options,
470
+ body: JSON.stringify({}),
471
+ headers: {
472
+ "Content-Type": "application/json",
473
+ ...(options.headers ?? {})
474
+ }
475
+ }
476
+ );
477
+ }
478
+
479
+ getMergeRequestApprovalState(
480
+ projectId: string,
481
+ mergeRequestIid: string,
482
+ options: GitLabRequestOptions = {}
483
+ ): Promise<unknown> {
484
+ return this.get(
485
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/approval_state`,
486
+ options
487
+ );
488
+ }
489
+
490
+ listMergeRequestDiscussions(
491
+ projectId: string,
492
+ mergeRequestIid: string,
493
+ options: GitLabRequestOptions = {}
494
+ ): Promise<unknown> {
495
+ return this.get(
496
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/discussions`,
497
+ options
498
+ );
499
+ }
500
+
501
+ createMergeRequestDiscussionNote(
502
+ projectId: string,
503
+ mergeRequestIid: string,
504
+ discussionId: string,
505
+ payload: {
506
+ body: string;
507
+ created_at?: string;
508
+ },
509
+ options: GitLabRequestOptions = {}
510
+ ): Promise<unknown> {
511
+ return this.post(
512
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/discussions/${encode(discussionId)}/notes`,
513
+ {
514
+ ...options,
515
+ body: JSON.stringify(payload),
516
+ headers: {
517
+ "Content-Type": "application/json",
518
+ ...(options.headers ?? {})
519
+ }
520
+ }
521
+ );
522
+ }
523
+
524
+ createMergeRequestThread(
525
+ projectId: string,
526
+ mergeRequestIid: string,
527
+ payload: {
528
+ body: string;
529
+ position?: Record<string, unknown>;
530
+ created_at?: string;
531
+ },
532
+ options: GitLabRequestOptions = {}
533
+ ): Promise<unknown> {
534
+ return this.post(
535
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/discussions`,
536
+ {
537
+ ...options,
538
+ body: JSON.stringify(payload),
539
+ headers: {
540
+ "Content-Type": "application/json",
541
+ ...(options.headers ?? {})
542
+ }
543
+ }
544
+ );
545
+ }
546
+
547
+ updateMergeRequestDiscussionNote(
548
+ projectId: string,
549
+ mergeRequestIid: string,
550
+ discussionId: string,
551
+ noteId: string,
552
+ payload: {
553
+ body?: string;
554
+ resolved?: boolean;
555
+ },
556
+ options: GitLabRequestOptions = {}
557
+ ): Promise<unknown> {
558
+ return this.put(
559
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/discussions/${encode(discussionId)}/notes/${encode(noteId)}`,
560
+ {
561
+ ...options,
562
+ body: JSON.stringify(payload),
563
+ headers: {
564
+ "Content-Type": "application/json",
565
+ ...(options.headers ?? {})
566
+ }
567
+ }
568
+ );
569
+ }
570
+
571
+ deleteMergeRequestDiscussionNote(
572
+ projectId: string,
573
+ mergeRequestIid: string,
574
+ discussionId: string,
575
+ noteId: string,
576
+ options: GitLabRequestOptions = {}
577
+ ): Promise<unknown> {
578
+ return this.delete(
579
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/discussions/${encode(discussionId)}/notes/${encode(noteId)}`,
580
+ options
581
+ );
582
+ }
583
+
584
+ resolveMergeRequestThread(
585
+ projectId: string,
586
+ mergeRequestIid: string,
587
+ discussionId: string,
588
+ noteId: string,
589
+ resolved: boolean,
590
+ options: GitLabRequestOptions = {}
591
+ ): Promise<unknown> {
592
+ return this.put(
593
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/discussions/${encode(discussionId)}/notes/${encode(noteId)}`,
594
+ {
595
+ ...options,
596
+ body: JSON.stringify({ resolved }),
597
+ headers: {
598
+ "Content-Type": "application/json",
599
+ ...(options.headers ?? {})
600
+ }
601
+ }
602
+ );
603
+ }
604
+
605
+ listMergeRequestNotes(
606
+ projectId: string,
607
+ mergeRequestIid: string,
608
+ options: GitLabRequestOptions = {}
609
+ ): Promise<unknown> {
610
+ return this.get(
611
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/notes`,
612
+ options
613
+ );
614
+ }
615
+
616
+ getMergeRequestNote(
617
+ projectId: string,
618
+ mergeRequestIid: string,
619
+ noteId: string,
620
+ options: GitLabRequestOptions = {}
621
+ ): Promise<unknown> {
622
+ return this.get(
623
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/notes/${encode(noteId)}`,
624
+ options
625
+ );
626
+ }
627
+
628
+ createMergeRequestNote(
629
+ projectId: string,
630
+ mergeRequestIid: string,
631
+ body: string,
632
+ options: GitLabRequestOptions = {}
633
+ ): Promise<unknown> {
634
+ return this.post(
635
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/notes`,
636
+ {
637
+ ...options,
638
+ body: JSON.stringify({ body }),
639
+ headers: {
640
+ "Content-Type": "application/json",
641
+ ...(options.headers ?? {})
642
+ }
643
+ }
644
+ );
645
+ }
646
+
647
+ getDraftNote(
648
+ projectId: string,
649
+ mergeRequestIid: string,
650
+ draftNoteId: string,
651
+ options: GitLabRequestOptions = {}
652
+ ): Promise<unknown> {
653
+ return this.get(
654
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/draft_notes/${encode(draftNoteId)}`,
655
+ options
656
+ );
657
+ }
658
+
659
+ listDraftNotes(
660
+ projectId: string,
661
+ mergeRequestIid: string,
662
+ options: GitLabRequestOptions = {}
663
+ ): Promise<unknown> {
664
+ return this.get(
665
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/draft_notes`,
666
+ options
667
+ );
668
+ }
669
+
670
+ createDraftNote(
671
+ projectId: string,
672
+ mergeRequestIid: string,
673
+ payload: {
674
+ body: string;
675
+ position?: Record<string, unknown>;
676
+ resolve_discussion?: boolean;
677
+ },
678
+ options: GitLabRequestOptions = {}
679
+ ): Promise<unknown> {
680
+ return this.post(
681
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/draft_notes`,
682
+ {
683
+ ...options,
684
+ body: JSON.stringify({
685
+ note: payload.body,
686
+ position: payload.position,
687
+ resolve_discussion: payload.resolve_discussion
688
+ }),
689
+ headers: {
690
+ "Content-Type": "application/json",
691
+ ...(options.headers ?? {})
692
+ }
693
+ }
694
+ );
695
+ }
696
+
697
+ updateDraftNote(
698
+ projectId: string,
699
+ mergeRequestIid: string,
700
+ draftNoteId: string,
701
+ payload: {
702
+ body?: string;
703
+ position?: Record<string, unknown>;
704
+ resolve_discussion?: boolean;
705
+ },
706
+ options: GitLabRequestOptions = {}
707
+ ): Promise<unknown> {
708
+ return this.put(
709
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/draft_notes/${encode(draftNoteId)}`,
710
+ {
711
+ ...options,
712
+ body: JSON.stringify({
713
+ note: payload.body,
714
+ position: payload.position,
715
+ resolve_discussion: payload.resolve_discussion
716
+ }),
717
+ headers: {
718
+ "Content-Type": "application/json",
719
+ ...(options.headers ?? {})
720
+ }
721
+ }
722
+ );
723
+ }
724
+
725
+ deleteDraftNote(
726
+ projectId: string,
727
+ mergeRequestIid: string,
728
+ draftNoteId: string,
729
+ options: GitLabRequestOptions = {}
730
+ ): Promise<unknown> {
731
+ return this.delete(
732
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/draft_notes/${encode(draftNoteId)}`,
733
+ options
734
+ );
735
+ }
736
+
737
+ publishDraftNote(
738
+ projectId: string,
739
+ mergeRequestIid: string,
740
+ draftNoteId: string,
741
+ options: GitLabRequestOptions = {}
742
+ ): Promise<unknown> {
743
+ return this.put(
744
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/draft_notes/${encode(draftNoteId)}/publish`,
745
+ {
746
+ ...options,
747
+ body: JSON.stringify({}),
748
+ headers: {
749
+ "Content-Type": "application/json",
750
+ ...(options.headers ?? {})
751
+ }
752
+ }
753
+ );
754
+ }
755
+
756
+ bulkPublishDraftNotes(
757
+ projectId: string,
758
+ mergeRequestIid: string,
759
+ options: GitLabRequestOptions = {}
760
+ ): Promise<unknown> {
761
+ return this.post(
762
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/draft_notes/bulk_publish`,
763
+ {
764
+ ...options,
765
+ body: JSON.stringify({}),
766
+ headers: {
767
+ "Content-Type": "application/json",
768
+ ...(options.headers ?? {})
769
+ }
770
+ }
771
+ );
772
+ }
773
+
774
+ createNote(
775
+ projectId: string,
776
+ noteableType: "issue" | "merge_request",
777
+ noteableIid: string,
778
+ body: string,
779
+ options: GitLabRequestOptions = {}
780
+ ): Promise<unknown> {
781
+ return this.post(
782
+ `/projects/${encode(projectId)}/${noteableType}s/${encode(noteableIid)}/notes`,
783
+ {
784
+ ...options,
785
+ body: JSON.stringify({ body }),
786
+ headers: {
787
+ "Content-Type": "application/json",
788
+ ...(options.headers ?? {})
789
+ }
790
+ }
791
+ );
792
+ }
793
+
794
+ updateMergeRequestNote(
795
+ projectId: string,
796
+ mergeRequestIid: string,
797
+ noteId: string,
798
+ body: string,
799
+ options: GitLabRequestOptions = {}
800
+ ): Promise<unknown> {
801
+ return this.put(
802
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/notes/${encode(noteId)}`,
803
+ {
804
+ ...options,
805
+ body: JSON.stringify({ body }),
806
+ headers: {
807
+ "Content-Type": "application/json",
808
+ ...(options.headers ?? {})
809
+ }
810
+ }
811
+ );
812
+ }
813
+
814
+ deleteMergeRequestNote(
815
+ projectId: string,
816
+ mergeRequestIid: string,
817
+ noteId: string,
818
+ options: GitLabRequestOptions = {}
819
+ ): Promise<unknown> {
820
+ return this.delete(
821
+ `/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/notes/${encode(noteId)}`,
822
+ options
823
+ );
824
+ }
825
+
826
+ // issues
827
+ listIssues(projectId: string, options: GitLabRequestOptions = {}): Promise<unknown> {
828
+ return this.get(`/projects/${encode(projectId)}/issues`, options);
829
+ }
830
+
831
+ listGlobalIssues(options: GitLabRequestOptions = {}): Promise<unknown> {
832
+ return this.get("/issues", options);
833
+ }
834
+
835
+ getIssue(
836
+ projectId: string,
837
+ issueIid: string,
838
+ options: GitLabRequestOptions = {}
839
+ ): Promise<unknown> {
840
+ return this.get(`/projects/${encode(projectId)}/issues/${encode(issueIid)}`, options);
841
+ }
842
+
843
+ createIssue(
844
+ projectId: string,
845
+ payload: {
846
+ title: string;
847
+ description?: string;
848
+ assignee_ids?: number[];
849
+ labels?: string;
850
+ milestone_id?: number;
851
+ due_date?: string;
852
+ confidential?: boolean;
853
+ issue_type?: string;
854
+ },
855
+ options: GitLabRequestOptions = {}
856
+ ): Promise<unknown> {
857
+ return this.post(`/projects/${encode(projectId)}/issues`, {
858
+ ...options,
859
+ body: JSON.stringify(payload),
860
+ headers: {
861
+ "Content-Type": "application/json",
862
+ ...(options.headers ?? {})
863
+ }
864
+ });
865
+ }
866
+
867
+ updateIssue(
868
+ projectId: string,
869
+ issueIid: string,
870
+ payload: Record<string, unknown>,
871
+ options: GitLabRequestOptions = {}
872
+ ): Promise<unknown> {
873
+ return this.put(`/projects/${encode(projectId)}/issues/${encode(issueIid)}`, {
874
+ ...options,
875
+ body: JSON.stringify(payload),
876
+ headers: {
877
+ "Content-Type": "application/json",
878
+ ...(options.headers ?? {})
879
+ }
880
+ });
881
+ }
882
+
883
+ deleteIssue(
884
+ projectId: string,
885
+ issueIid: string,
886
+ options: GitLabRequestOptions = {}
887
+ ): Promise<unknown> {
888
+ return this.delete(`/projects/${encode(projectId)}/issues/${encode(issueIid)}`, options);
889
+ }
890
+
891
+ myIssues(
892
+ payload: {
893
+ project_id?: string;
894
+ state?: "opened" | "closed" | "all";
895
+ labels?: string;
896
+ milestone?: string;
897
+ search?: string;
898
+ created_after?: string;
899
+ created_before?: string;
900
+ updated_after?: string;
901
+ updated_before?: string;
902
+ per_page?: number;
903
+ page?: number;
904
+ scope?: string;
905
+ },
906
+ options: GitLabRequestOptions = {}
907
+ ): Promise<unknown> {
908
+ const { project_id: projectId, ...queryPayload } = payload;
909
+ const path = projectId ? `/projects/${encode(projectId)}/issues` : "/issues";
910
+
911
+ return this.get(path, {
912
+ ...options,
913
+ query: {
914
+ scope: queryPayload.scope ?? "assigned_to_me",
915
+ ...queryPayload,
916
+ ...(options.query ?? {})
917
+ }
918
+ });
919
+ }
920
+
921
+ listIssueDiscussions(
922
+ projectId: string,
923
+ issueIid: string,
924
+ options: GitLabRequestOptions = {}
925
+ ): Promise<unknown> {
926
+ return this.get(
927
+ `/projects/${encode(projectId)}/issues/${encode(issueIid)}/discussions`,
928
+ options
929
+ );
930
+ }
931
+
932
+ createIssueNote(
933
+ projectId: string,
934
+ issueIid: string,
935
+ payload: {
936
+ body: string;
937
+ discussion_id?: string;
938
+ created_at?: string;
939
+ },
940
+ options: GitLabRequestOptions = {}
941
+ ): Promise<unknown> {
942
+ const discussionPath = payload.discussion_id
943
+ ? `/discussions/${encode(payload.discussion_id)}/notes`
944
+ : "/notes";
945
+ return this.post(`/projects/${encode(projectId)}/issues/${encode(issueIid)}${discussionPath}`, {
946
+ ...options,
947
+ body: JSON.stringify({
948
+ body: payload.body,
949
+ created_at: payload.created_at
950
+ }),
951
+ headers: {
952
+ "Content-Type": "application/json",
953
+ ...(options.headers ?? {})
954
+ }
955
+ });
956
+ }
957
+
958
+ updateIssueNote(
959
+ projectId: string,
960
+ issueIid: string,
961
+ discussionId: string,
962
+ noteId: string,
963
+ payload: {
964
+ body?: string;
965
+ resolved?: boolean;
966
+ },
967
+ options: GitLabRequestOptions = {}
968
+ ): Promise<unknown> {
969
+ return this.put(
970
+ `/projects/${encode(projectId)}/issues/${encode(issueIid)}/discussions/${encode(discussionId)}/notes/${encode(noteId)}`,
971
+ {
972
+ ...options,
973
+ body: JSON.stringify(payload),
974
+ headers: {
975
+ "Content-Type": "application/json",
976
+ ...(options.headers ?? {})
977
+ }
978
+ }
979
+ );
980
+ }
981
+
982
+ listIssueLinks(
983
+ projectId: string,
984
+ issueIid: string,
985
+ options: GitLabRequestOptions = {}
986
+ ): Promise<unknown> {
987
+ return this.get(`/projects/${encode(projectId)}/issues/${encode(issueIid)}/links`, options);
988
+ }
989
+
990
+ getIssueLink(
991
+ projectId: string,
992
+ issueIid: string,
993
+ issueLinkId: string,
994
+ options: GitLabRequestOptions = {}
995
+ ): Promise<unknown> {
996
+ return this.get(
997
+ `/projects/${encode(projectId)}/issues/${encode(issueIid)}/links/${encode(issueLinkId)}`,
998
+ options
999
+ );
1000
+ }
1001
+
1002
+ createIssueLink(
1003
+ projectId: string,
1004
+ issueIid: string,
1005
+ payload: {
1006
+ target_project_id: string;
1007
+ target_issue_iid: string;
1008
+ link_type?: "relates_to" | "blocks" | "is_blocked_by";
1009
+ },
1010
+ options: GitLabRequestOptions = {}
1011
+ ): Promise<unknown> {
1012
+ return this.post(`/projects/${encode(projectId)}/issues/${encode(issueIid)}/links`, {
1013
+ ...options,
1014
+ body: JSON.stringify(payload),
1015
+ headers: {
1016
+ "Content-Type": "application/json",
1017
+ ...(options.headers ?? {})
1018
+ }
1019
+ });
1020
+ }
1021
+
1022
+ deleteIssueLink(
1023
+ projectId: string,
1024
+ issueIid: string,
1025
+ issueLinkId: string,
1026
+ options: GitLabRequestOptions = {}
1027
+ ): Promise<unknown> {
1028
+ return this.delete(
1029
+ `/projects/${encode(projectId)}/issues/${encode(issueIid)}/links/${encode(issueLinkId)}`,
1030
+ options
1031
+ );
1032
+ }
1033
+
1034
+ // wiki
1035
+ listWikiPages(projectId: string, options: GitLabRequestOptions = {}): Promise<unknown> {
1036
+ return this.get(`/projects/${encode(projectId)}/wikis`, options);
1037
+ }
1038
+
1039
+ getWikiPage(
1040
+ projectId: string,
1041
+ slug: string,
1042
+ options: GitLabRequestOptions = {}
1043
+ ): Promise<unknown> {
1044
+ return this.get(`/projects/${encode(projectId)}/wikis/${encode(slug)}`, options);
1045
+ }
1046
+
1047
+ createWikiPage(
1048
+ projectId: string,
1049
+ payload: {
1050
+ title: string;
1051
+ content: string;
1052
+ format?: "markdown" | "rdoc" | "asciidoc" | "org";
1053
+ },
1054
+ options: GitLabRequestOptions = {}
1055
+ ): Promise<unknown> {
1056
+ return this.post(`/projects/${encode(projectId)}/wikis`, {
1057
+ ...options,
1058
+ body: JSON.stringify(payload),
1059
+ headers: {
1060
+ "Content-Type": "application/json",
1061
+ ...(options.headers ?? {})
1062
+ }
1063
+ });
1064
+ }
1065
+
1066
+ updateWikiPage(
1067
+ projectId: string,
1068
+ slug: string,
1069
+ payload: {
1070
+ content: string;
1071
+ title?: string;
1072
+ format?: "markdown" | "rdoc" | "asciidoc" | "org";
1073
+ },
1074
+ options: GitLabRequestOptions = {}
1075
+ ): Promise<unknown> {
1076
+ return this.put(`/projects/${encode(projectId)}/wikis/${encode(slug)}`, {
1077
+ ...options,
1078
+ body: JSON.stringify(payload),
1079
+ headers: {
1080
+ "Content-Type": "application/json",
1081
+ ...(options.headers ?? {})
1082
+ }
1083
+ });
1084
+ }
1085
+
1086
+ deleteWikiPage(
1087
+ projectId: string,
1088
+ slug: string,
1089
+ options: GitLabRequestOptions = {}
1090
+ ): Promise<unknown> {
1091
+ return this.delete(`/projects/${encode(projectId)}/wikis/${encode(slug)}`, options);
1092
+ }
1093
+
1094
+ // pipelines
1095
+ listPipelines(projectId: string, options: GitLabRequestOptions = {}): Promise<unknown> {
1096
+ return this.get(`/projects/${encode(projectId)}/pipelines`, options);
1097
+ }
1098
+
1099
+ getPipeline(
1100
+ projectId: string,
1101
+ pipelineId: string,
1102
+ options: GitLabRequestOptions = {}
1103
+ ): Promise<unknown> {
1104
+ return this.get(`/projects/${encode(projectId)}/pipelines/${encode(pipelineId)}`, options);
1105
+ }
1106
+
1107
+ listPipelineJobs(
1108
+ projectId: string,
1109
+ pipelineId: string,
1110
+ options: GitLabRequestOptions = {}
1111
+ ): Promise<unknown> {
1112
+ return this.get(`/projects/${encode(projectId)}/pipelines/${encode(pipelineId)}/jobs`, options);
1113
+ }
1114
+
1115
+ listPipelineTriggerJobs(
1116
+ projectId: string,
1117
+ pipelineId: string,
1118
+ options: GitLabRequestOptions = {}
1119
+ ): Promise<unknown> {
1120
+ return this.get(
1121
+ `/projects/${encode(projectId)}/pipelines/${encode(pipelineId)}/bridges`,
1122
+ options
1123
+ );
1124
+ }
1125
+
1126
+ getPipelineJob(
1127
+ projectId: string,
1128
+ jobId: string,
1129
+ options: GitLabRequestOptions = {}
1130
+ ): Promise<unknown> {
1131
+ return this.get(`/projects/${encode(projectId)}/jobs/${encode(jobId)}`, options);
1132
+ }
1133
+
1134
+ getPipelineJobOutput(
1135
+ projectId: string,
1136
+ jobId: string,
1137
+ options: GitLabRequestOptions = {}
1138
+ ): Promise<unknown> {
1139
+ return this.get(`/projects/${encode(projectId)}/jobs/${encode(jobId)}/trace`, options);
1140
+ }
1141
+
1142
+ createPipeline(
1143
+ projectId: string,
1144
+ payload: {
1145
+ ref: string;
1146
+ variables?: Array<{ key: string; value: string; variable_type?: "env_var" | "file" }>;
1147
+ },
1148
+ options: GitLabRequestOptions = {}
1149
+ ): Promise<unknown> {
1150
+ return this.post(`/projects/${encode(projectId)}/pipeline`, {
1151
+ ...options,
1152
+ body: JSON.stringify(payload),
1153
+ headers: {
1154
+ "Content-Type": "application/json",
1155
+ ...(options.headers ?? {})
1156
+ }
1157
+ });
1158
+ }
1159
+
1160
+ retryPipeline(
1161
+ projectId: string,
1162
+ pipelineId: string,
1163
+ options: GitLabRequestOptions = {}
1164
+ ): Promise<unknown> {
1165
+ return this.post(
1166
+ `/projects/${encode(projectId)}/pipelines/${encode(pipelineId)}/retry`,
1167
+ options
1168
+ );
1169
+ }
1170
+
1171
+ cancelPipeline(
1172
+ projectId: string,
1173
+ pipelineId: string,
1174
+ options: GitLabRequestOptions = {}
1175
+ ): Promise<unknown> {
1176
+ return this.post(
1177
+ `/projects/${encode(projectId)}/pipelines/${encode(pipelineId)}/cancel`,
1178
+ options
1179
+ );
1180
+ }
1181
+
1182
+ retryPipelineJob(
1183
+ projectId: string,
1184
+ jobId: string,
1185
+ options: GitLabRequestOptions = {}
1186
+ ): Promise<unknown> {
1187
+ return this.post(`/projects/${encode(projectId)}/jobs/${encode(jobId)}/retry`, options);
1188
+ }
1189
+
1190
+ cancelPipelineJob(
1191
+ projectId: string,
1192
+ jobId: string,
1193
+ options: GitLabRequestOptions = {}
1194
+ ): Promise<unknown> {
1195
+ return this.post(`/projects/${encode(projectId)}/jobs/${encode(jobId)}/cancel`, options);
1196
+ }
1197
+
1198
+ playPipelineJob(
1199
+ projectId: string,
1200
+ jobId: string,
1201
+ options: GitLabRequestOptions = {}
1202
+ ): Promise<unknown> {
1203
+ return this.post(`/projects/${encode(projectId)}/jobs/${encode(jobId)}/play`, options);
1204
+ }
1205
+
1206
+ // milestones
1207
+ listMilestones(projectId: string, options: GitLabRequestOptions = {}): Promise<unknown> {
1208
+ return this.get(`/projects/${encode(projectId)}/milestones`, options);
1209
+ }
1210
+
1211
+ getMilestone(
1212
+ projectId: string,
1213
+ milestoneId: string,
1214
+ options: GitLabRequestOptions = {}
1215
+ ): Promise<unknown> {
1216
+ return this.get(`/projects/${encode(projectId)}/milestones/${encode(milestoneId)}`, options);
1217
+ }
1218
+
1219
+ createMilestone(
1220
+ projectId: string,
1221
+ payload: {
1222
+ title: string;
1223
+ description?: string;
1224
+ due_date?: string;
1225
+ start_date?: string;
1226
+ },
1227
+ options: GitLabRequestOptions = {}
1228
+ ): Promise<unknown> {
1229
+ return this.post(`/projects/${encode(projectId)}/milestones`, {
1230
+ ...options,
1231
+ body: JSON.stringify(payload),
1232
+ headers: {
1233
+ "Content-Type": "application/json",
1234
+ ...(options.headers ?? {})
1235
+ }
1236
+ });
1237
+ }
1238
+
1239
+ updateMilestone(
1240
+ projectId: string,
1241
+ milestoneId: string,
1242
+ payload: Record<string, unknown>,
1243
+ options: GitLabRequestOptions = {}
1244
+ ): Promise<unknown> {
1245
+ return this.put(`/projects/${encode(projectId)}/milestones/${encode(milestoneId)}`, {
1246
+ ...options,
1247
+ body: JSON.stringify(payload),
1248
+ headers: {
1249
+ "Content-Type": "application/json",
1250
+ ...(options.headers ?? {})
1251
+ }
1252
+ });
1253
+ }
1254
+
1255
+ deleteMilestone(
1256
+ projectId: string,
1257
+ milestoneId: string,
1258
+ options: GitLabRequestOptions = {}
1259
+ ): Promise<unknown> {
1260
+ return this.delete(`/projects/${encode(projectId)}/milestones/${encode(milestoneId)}`, options);
1261
+ }
1262
+
1263
+ getMilestoneIssues(
1264
+ projectId: string,
1265
+ milestoneId: string,
1266
+ options: GitLabRequestOptions = {}
1267
+ ): Promise<unknown> {
1268
+ return this.get(
1269
+ `/projects/${encode(projectId)}/milestones/${encode(milestoneId)}/issues`,
1270
+ options
1271
+ );
1272
+ }
1273
+
1274
+ getMilestoneMergeRequests(
1275
+ projectId: string,
1276
+ milestoneId: string,
1277
+ options: GitLabRequestOptions = {}
1278
+ ): Promise<unknown> {
1279
+ return this.get(
1280
+ `/projects/${encode(projectId)}/milestones/${encode(milestoneId)}/merge_requests`,
1281
+ options
1282
+ );
1283
+ }
1284
+
1285
+ promoteMilestone(
1286
+ projectId: string,
1287
+ milestoneId: string,
1288
+ options: GitLabRequestOptions = {}
1289
+ ): Promise<unknown> {
1290
+ return this.post(
1291
+ `/projects/${encode(projectId)}/milestones/${encode(milestoneId)}/promote`,
1292
+ options
1293
+ );
1294
+ }
1295
+
1296
+ getMilestoneBurndownEvents(
1297
+ projectId: string,
1298
+ milestoneId: string,
1299
+ options: GitLabRequestOptions = {}
1300
+ ): Promise<unknown> {
1301
+ return this.get(
1302
+ `/projects/${encode(projectId)}/milestones/${encode(milestoneId)}/burndown_events`,
1303
+ options
1304
+ );
1305
+ }
1306
+
1307
+ // releases
1308
+ listReleases(projectId: string, options: GitLabRequestOptions = {}): Promise<unknown> {
1309
+ return this.get(`/projects/${encode(projectId)}/releases`, options);
1310
+ }
1311
+
1312
+ getRelease(
1313
+ projectId: string,
1314
+ tagName: string,
1315
+ options: GitLabRequestOptions = {}
1316
+ ): Promise<unknown> {
1317
+ return this.get(`/projects/${encode(projectId)}/releases/${encode(tagName)}`, options);
1318
+ }
1319
+
1320
+ createRelease(
1321
+ projectId: string,
1322
+ payload: Record<string, unknown>,
1323
+ options: GitLabRequestOptions = {}
1324
+ ): Promise<unknown> {
1325
+ return this.post(`/projects/${encode(projectId)}/releases`, {
1326
+ ...options,
1327
+ body: JSON.stringify(payload),
1328
+ headers: {
1329
+ "Content-Type": "application/json",
1330
+ ...(options.headers ?? {})
1331
+ }
1332
+ });
1333
+ }
1334
+
1335
+ updateRelease(
1336
+ projectId: string,
1337
+ tagName: string,
1338
+ payload: Record<string, unknown>,
1339
+ options: GitLabRequestOptions = {}
1340
+ ): Promise<unknown> {
1341
+ return this.put(`/projects/${encode(projectId)}/releases/${encode(tagName)}`, {
1342
+ ...options,
1343
+ body: JSON.stringify(payload),
1344
+ headers: {
1345
+ "Content-Type": "application/json",
1346
+ ...(options.headers ?? {})
1347
+ }
1348
+ });
1349
+ }
1350
+
1351
+ deleteRelease(
1352
+ projectId: string,
1353
+ tagName: string,
1354
+ options: GitLabRequestOptions = {}
1355
+ ): Promise<unknown> {
1356
+ return this.delete(`/projects/${encode(projectId)}/releases/${encode(tagName)}`, options);
1357
+ }
1358
+
1359
+ createReleaseEvidence(
1360
+ projectId: string,
1361
+ tagName: string,
1362
+ options: GitLabRequestOptions = {}
1363
+ ): Promise<unknown> {
1364
+ return this.post(
1365
+ `/projects/${encode(projectId)}/releases/${encode(tagName)}/evidence`,
1366
+ options
1367
+ );
1368
+ }
1369
+
1370
+ downloadReleaseAsset(
1371
+ projectId: string,
1372
+ tagName: string,
1373
+ directAssetPath: string,
1374
+ options: GitLabRequestOptions = {}
1375
+ ): Promise<unknown> {
1376
+ const safePath = encodeSlashPath(directAssetPath);
1377
+ return this.get(
1378
+ `/projects/${encode(projectId)}/releases/${encode(tagName)}/downloads/${safePath}`,
1379
+ options
1380
+ );
1381
+ }
1382
+
1383
+ // labels
1384
+ listLabels(projectId: string, options: GitLabRequestOptions = {}): Promise<unknown> {
1385
+ return this.get(`/projects/${encode(projectId)}/labels`, options);
1386
+ }
1387
+
1388
+ getLabel(
1389
+ projectId: string,
1390
+ labelId: string,
1391
+ options: GitLabRequestOptions = {}
1392
+ ): Promise<unknown> {
1393
+ return this.get(`/projects/${encode(projectId)}/labels/${encode(labelId)}`, options);
1394
+ }
1395
+
1396
+ createLabel(
1397
+ projectId: string,
1398
+ payload: Record<string, unknown>,
1399
+ options: GitLabRequestOptions = {}
1400
+ ): Promise<unknown> {
1401
+ return this.post(`/projects/${encode(projectId)}/labels`, {
1402
+ ...options,
1403
+ body: JSON.stringify(payload),
1404
+ headers: {
1405
+ "Content-Type": "application/json",
1406
+ ...(options.headers ?? {})
1407
+ }
1408
+ });
1409
+ }
1410
+
1411
+ updateLabel(
1412
+ projectId: string,
1413
+ payload: Record<string, unknown>,
1414
+ options: GitLabRequestOptions = {}
1415
+ ): Promise<unknown> {
1416
+ return this.put(`/projects/${encode(projectId)}/labels`, {
1417
+ ...options,
1418
+ body: JSON.stringify(payload),
1419
+ headers: {
1420
+ "Content-Type": "application/json",
1421
+ ...(options.headers ?? {})
1422
+ }
1423
+ });
1424
+ }
1425
+
1426
+ deleteLabel(
1427
+ projectId: string,
1428
+ labelName: string,
1429
+ options: GitLabRequestOptions = {}
1430
+ ): Promise<unknown> {
1431
+ return this.delete(`/projects/${encode(projectId)}/labels`, {
1432
+ ...options,
1433
+ query: {
1434
+ name: labelName,
1435
+ ...(options.query ?? {})
1436
+ }
1437
+ });
1438
+ }
1439
+
1440
+ // namespaces/users/events
1441
+ listNamespaces(options: GitLabRequestOptions = {}): Promise<unknown> {
1442
+ return this.get("/namespaces", options);
1443
+ }
1444
+
1445
+ listGroupIterations(groupId: string, options: GitLabRequestOptions = {}): Promise<unknown> {
1446
+ return this.get(`/groups/${encode(groupId)}/iterations`, options);
1447
+ }
1448
+
1449
+ getNamespace(namespaceIdOrPath: string, options: GitLabRequestOptions = {}): Promise<unknown> {
1450
+ return this.get(`/namespaces/${encode(namespaceIdOrPath)}`, options);
1451
+ }
1452
+
1453
+ verifyNamespace(pathName: string, options: GitLabRequestOptions = {}): Promise<unknown> {
1454
+ return this.get(`/namespaces/${encode(pathName)}/exists`, options);
1455
+ }
1456
+
1457
+ getUsers(options: GitLabRequestOptions = {}): Promise<unknown> {
1458
+ return this.get("/users", options);
1459
+ }
1460
+
1461
+ listEvents(options: GitLabRequestOptions = {}): Promise<unknown> {
1462
+ return this.get("/events", options);
1463
+ }
1464
+
1465
+ getProjectEvents(projectId: string, options: GitLabRequestOptions = {}): Promise<unknown> {
1466
+ return this.get(`/projects/${encode(projectId)}/events`, options);
1467
+ }
1468
+
1469
+ // attachments / markdown
1470
+ uploadMarkdown(
1471
+ projectId: string,
1472
+ content: string,
1473
+ filename: string,
1474
+ options: GitLabRequestOptions = {}
1475
+ ): Promise<unknown> {
1476
+ const form = new FormData();
1477
+ form.append("file", new Blob([content], { type: "text/markdown" }), filename);
1478
+
1479
+ return this.post(`/projects/${encode(projectId)}/uploads`, {
1480
+ ...options,
1481
+ body: form,
1482
+ headers: {
1483
+ Accept: "*/*",
1484
+ ...(options.headers ?? {})
1485
+ }
1486
+ });
1487
+ }
1488
+
1489
+ async uploadMarkdownFile(
1490
+ projectId: string,
1491
+ filePath: string,
1492
+ options: GitLabRequestOptions = {}
1493
+ ): Promise<unknown> {
1494
+ const content = await fs.readFile(filePath);
1495
+ const filename = path.basename(filePath);
1496
+ const form = new FormData();
1497
+ form.append("file", new Blob([content], { type: "application/octet-stream" }), filename);
1498
+
1499
+ return this.post(`/projects/${encode(projectId)}/uploads`, {
1500
+ ...options,
1501
+ body: form,
1502
+ headers: {
1503
+ Accept: "*/*",
1504
+ ...(options.headers ?? {})
1505
+ }
1506
+ });
1507
+ }
1508
+
1509
+ async downloadAttachment(
1510
+ urlOrPath: string,
1511
+ options: GitLabRequestOptions = {}
1512
+ ): Promise<{ fileName: string; contentType: string; base64: string }> {
1513
+ const requestConfig = this.resolveRequestConfig(options);
1514
+ const url = this.resolveAttachmentUrl(urlOrPath, requestConfig.apiUrl);
1515
+
1516
+ let headers = new Headers(options.headers);
1517
+ let token = requestConfig.token;
1518
+ let fetchImpl: typeof fetch = fetch;
1519
+
1520
+ if (this.beforeRequest) {
1521
+ const override = await this.beforeRequest({
1522
+ url,
1523
+ method: "GET",
1524
+ headers,
1525
+ token
1526
+ });
1527
+
1528
+ if (override?.headers) {
1529
+ headers = override.headers;
1530
+ }
1531
+ if (override?.token !== undefined) {
1532
+ token = override.token;
1533
+ }
1534
+ if (override?.fetchImpl) {
1535
+ fetchImpl = override.fetchImpl;
1536
+ }
1537
+ }
1538
+
1539
+ this.attachAuth(headers, token);
1540
+
1541
+ const response = await fetchImpl(url, {
1542
+ method: "GET",
1543
+ headers,
1544
+ signal: AbortSignal.timeout(this.timeoutMs)
1545
+ });
1546
+
1547
+ if (!response.ok) {
1548
+ throw new GitLabApiError(
1549
+ `GitLab attachment download failed: ${response.status} ${response.statusText}`,
1550
+ response.status,
1551
+ await this.parseResponseBody(response)
1552
+ );
1553
+ }
1554
+
1555
+ const contentType = response.headers.get("content-type") ?? "application/octet-stream";
1556
+ const disposition = response.headers.get("content-disposition") ?? "";
1557
+ const fileName = extractFileName(disposition) ?? `attachment-${Date.now()}`;
1558
+ const bytes = Buffer.from(await response.arrayBuffer());
1559
+
1560
+ return {
1561
+ fileName,
1562
+ contentType,
1563
+ base64: bytes.toString("base64")
1564
+ };
1565
+ }
1566
+
1567
+ // graphql
1568
+ executeGraphql(
1569
+ query: string,
1570
+ variables: Record<string, unknown> | undefined,
1571
+ options: GitLabRequestOptions = {}
1572
+ ): Promise<unknown> {
1573
+ const requestConfig = this.resolveRequestConfig(options);
1574
+ const endpoint = buildGraphqlEndpoint(requestConfig.apiUrl);
1575
+
1576
+ return this.rawRequest(endpoint, {
1577
+ method: "POST",
1578
+ headers: {
1579
+ "Content-Type": "application/json",
1580
+ ...(options.headers ?? {})
1581
+ },
1582
+ body: JSON.stringify({ query, variables }),
1583
+ token: requestConfig.token
1584
+ });
1585
+ }
1586
+
1587
+ // generic methods
1588
+ get(path: string, options: GitLabRequestOptions = {}): Promise<unknown> {
1589
+ return this.request("GET", path, options);
1590
+ }
1591
+
1592
+ post(path: string, options: GitLabRequestOptions = {}): Promise<unknown> {
1593
+ return this.request("POST", path, options);
1594
+ }
1595
+
1596
+ put(path: string, options: GitLabRequestOptions = {}): Promise<unknown> {
1597
+ return this.request("PUT", path, options);
1598
+ }
1599
+
1600
+ delete(path: string, options: GitLabRequestOptions = {}): Promise<unknown> {
1601
+ return this.request("DELETE", path, options);
1602
+ }
1603
+
1604
+ private async request(
1605
+ method: "GET" | "POST" | "PUT" | "DELETE",
1606
+ path: string,
1607
+ options: GitLabRequestOptions = {}
1608
+ ): Promise<unknown> {
1609
+ const config = this.resolveRequestConfig(options);
1610
+ const url = new URL(path.replace(/^\//, ""), `${config.apiUrl}/`);
1611
+
1612
+ for (const [key, value] of Object.entries(options.query ?? {})) {
1613
+ if (value !== undefined && value !== null) {
1614
+ url.searchParams.set(key, String(value));
1615
+ }
1616
+ }
1617
+
1618
+ return this.rawRequest(url, {
1619
+ method,
1620
+ body: options.body,
1621
+ headers: options.headers,
1622
+ token: config.token
1623
+ });
1624
+ }
1625
+
1626
+ private async rawRequest(
1627
+ url: URL,
1628
+ options: {
1629
+ method: string;
1630
+ body?: BodyInit;
1631
+ headers?: HeadersInit;
1632
+ token?: string;
1633
+ }
1634
+ ): Promise<unknown> {
1635
+ let headers = new Headers(options.headers);
1636
+ if (!headers.has("Accept")) {
1637
+ headers.set("Accept", "application/json");
1638
+ }
1639
+ let requestBody = options.body;
1640
+ let token = options.token;
1641
+ let fetchImpl: typeof fetch = fetch;
1642
+
1643
+ if (this.beforeRequest) {
1644
+ const override = await this.beforeRequest({
1645
+ url,
1646
+ method: options.method,
1647
+ headers,
1648
+ body: requestBody,
1649
+ token
1650
+ });
1651
+
1652
+ if (override?.headers) {
1653
+ headers = override.headers;
1654
+ }
1655
+ if (override?.body !== undefined) {
1656
+ requestBody = override.body;
1657
+ }
1658
+ if (override?.token !== undefined) {
1659
+ token = override.token;
1660
+ }
1661
+ if (override?.fetchImpl) {
1662
+ fetchImpl = override.fetchImpl;
1663
+ }
1664
+ }
1665
+
1666
+ this.attachAuth(headers, token);
1667
+
1668
+ const response = await fetchImpl(url, {
1669
+ method: options.method,
1670
+ body: requestBody,
1671
+ headers,
1672
+ signal: AbortSignal.timeout(this.timeoutMs)
1673
+ });
1674
+
1675
+ const body = await this.parseResponseBody(response);
1676
+
1677
+ if (!response.ok) {
1678
+ throw new GitLabApiError(
1679
+ `GitLab API request failed: ${response.status} ${response.statusText}`,
1680
+ response.status,
1681
+ body
1682
+ );
1683
+ }
1684
+
1685
+ return body;
1686
+ }
1687
+
1688
+ private async parseResponseBody(response: Response): Promise<unknown> {
1689
+ const contentType = response.headers.get("content-type") ?? "";
1690
+
1691
+ if (contentType.includes("application/json")) {
1692
+ return response.json();
1693
+ }
1694
+
1695
+ return response.text();
1696
+ }
1697
+
1698
+ private resolveRequestConfig(options: GitLabRequestOptions): { apiUrl: string; token?: string } {
1699
+ const sessionAuth = getSessionAuth();
1700
+ const apiUrl = options.apiUrl ?? sessionAuth?.apiUrl ?? this.pickApiUrl();
1701
+ const token = options.token ?? sessionAuth?.token ?? this.defaultToken;
1702
+
1703
+ return {
1704
+ apiUrl: normalizeApiUrl(apiUrl),
1705
+ token
1706
+ };
1707
+ }
1708
+
1709
+ private pickApiUrl(): string {
1710
+ if (this.apiUrls.length <= 1) {
1711
+ return this.baseApiUrl;
1712
+ }
1713
+
1714
+ const index = this.nextApiUrlIndex % this.apiUrls.length;
1715
+ this.nextApiUrlIndex = (this.nextApiUrlIndex + 1) % this.apiUrls.length;
1716
+ return this.apiUrls[index] ?? this.baseApiUrl;
1717
+ }
1718
+
1719
+ private resolveAbsoluteUrl(raw: string, apiUrl: string): URL {
1720
+ if (/^https?:\/\//i.test(raw)) {
1721
+ return new URL(raw);
1722
+ }
1723
+
1724
+ const base = new URL(apiUrl);
1725
+ return new URL(raw.replace(/^\//, ""), `${base.origin}/`);
1726
+ }
1727
+
1728
+ private resolveAttachmentUrl(raw: string, apiUrl: string): URL {
1729
+ const base = new URL(apiUrl);
1730
+ const resolved = this.resolveAbsoluteUrl(raw, apiUrl);
1731
+
1732
+ if (resolved.origin !== base.origin) {
1733
+ throw new Error(
1734
+ `Refusing to download cross-origin attachment URL '${resolved.origin}'. Only '${base.origin}' is allowed.`
1735
+ );
1736
+ }
1737
+
1738
+ return resolved;
1739
+ }
1740
+
1741
+ private attachAuth(headers: Headers, token?: string): void {
1742
+ if (!token) {
1743
+ return;
1744
+ }
1745
+
1746
+ headers.set("PRIVATE-TOKEN", token);
1747
+ }
1748
+ }
1749
+
1750
+ export function getEffectiveSessionAuth(
1751
+ defaultToken?: string,
1752
+ defaultApiUrl?: string
1753
+ ): SessionAuth {
1754
+ const auth = getSessionAuth();
1755
+
1756
+ return {
1757
+ token: auth?.token ?? defaultToken,
1758
+ apiUrl: auth?.apiUrl ?? defaultApiUrl,
1759
+ header: auth?.header,
1760
+ sessionId: auth?.sessionId,
1761
+ updatedAt: auth?.updatedAt ?? Date.now()
1762
+ };
1763
+ }
1764
+
1765
+ function encode(value: string): string {
1766
+ return encodeURIComponent(value);
1767
+ }
1768
+
1769
+ function encodeSlashPath(pathValue: string): string {
1770
+ const trimmed = pathValue.replace(/^\/+/, "").trim();
1771
+ if (!trimmed) {
1772
+ return "";
1773
+ }
1774
+
1775
+ return trimmed
1776
+ .split("/")
1777
+ .filter((segment) => segment.length > 0)
1778
+ .map((segment) => encode(segment))
1779
+ .join("/");
1780
+ }
1781
+
1782
+ function normalizeApiUrl(rawUrl: string): string {
1783
+ const url = new URL(rawUrl);
1784
+ const pathname = url.pathname.replace(/\/+$/, "");
1785
+
1786
+ if (pathname.endsWith("/api/v4")) {
1787
+ url.pathname = pathname;
1788
+ return url.toString();
1789
+ }
1790
+
1791
+ url.pathname = `${pathname}/api/v4`.replace(/\/\//g, "/");
1792
+
1793
+ return url.toString();
1794
+ }
1795
+
1796
+ function buildGraphqlEndpoint(apiUrl: string): URL {
1797
+ const url = new URL(apiUrl);
1798
+ const prefix = url.pathname.replace(/\/api\/v4\/?$/, "");
1799
+ return new URL(`${prefix || "/"}/api/graphql`, url.origin);
1800
+ }
1801
+
1802
+ function extractFileName(contentDisposition: string): string | undefined {
1803
+ const quoted = /filename\*?=(?:UTF-8''|")?([^";]+)/i.exec(contentDisposition);
1804
+
1805
+ if (!quoted) {
1806
+ return undefined;
1807
+ }
1808
+
1809
+ return decodeURIComponent(quoted[1] ?? "");
1810
+ }