forges 0.0.0 → 0.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 (152) hide show
  1. package/LICENCE +21 -0
  2. package/README.md +72 -0
  3. package/dist/azure-devops/index.d.mts +2 -0
  4. package/dist/azure-devops/index.mjs +1 -0
  5. package/dist/azure-devops-DwQvL4TV.mjs +1355 -0
  6. package/dist/bitbucket/index.d.mts +2 -0
  7. package/dist/bitbucket/index.mjs +1 -0
  8. package/dist/bitbucket-CpcfJlYi.mjs +1171 -0
  9. package/dist/crypto-CQNEyYHD.mjs +156 -0
  10. package/dist/cursor-origin/index.d.mts +2 -0
  11. package/dist/cursor-origin/index.mjs +1 -0
  12. package/dist/cursor-origin-BhqYx2oW.mjs +980 -0
  13. package/dist/define-B2_4ob2i.d.mts +307 -0
  14. package/dist/define-D3yEXtKK.mjs +2276 -0
  15. package/dist/env.d.mts +46 -0
  16. package/dist/env.mjs +283 -0
  17. package/dist/fake/index.d.mts +227 -0
  18. package/dist/fake/index.mjs +1001 -0
  19. package/dist/fetch-D4pSRvvA.d.mts +1723 -0
  20. package/dist/forgejo/index.d.mts +2 -0
  21. package/dist/forgejo/index.mjs +1 -0
  22. package/dist/forgejo-BCefbHUN.mjs +2005 -0
  23. package/dist/gitea/index.d.mts +2 -0
  24. package/dist/gitea/index.mjs +22 -0
  25. package/dist/gitee/index.d.mts +2 -0
  26. package/dist/gitee/index.mjs +1 -0
  27. package/dist/gitee-CA-f7Rs8.mjs +1272 -0
  28. package/dist/github/index.d.mts +2 -0
  29. package/dist/github/index.mjs +1 -0
  30. package/dist/github-B0umL8_f.mjs +3259 -0
  31. package/dist/gitlab/index.d.mts +2 -0
  32. package/dist/gitlab/index.mjs +1 -0
  33. package/dist/gitlab-Bsr__Z0C.mjs +2378 -0
  34. package/dist/graphql-D7uGzZTf.mjs +366 -0
  35. package/dist/index-BQ6VS31s.d.mts +32 -0
  36. package/dist/index-C-jz9Mo3.d.mts +16 -0
  37. package/dist/index-C4fYzmK3.d.mts +102 -0
  38. package/dist/index-CQiIX4SH.d.mts +27 -0
  39. package/dist/index-CTj5gQzl.d.mts +35 -0
  40. package/dist/index-CliBWzOF.d.mts +23 -0
  41. package/dist/index-Cz_y-t3d.d.mts +36 -0
  42. package/dist/index-DEt25E1w.d.mts +77 -0
  43. package/dist/index-DIuS0EOy.d.mts +43 -0
  44. package/dist/index-uSpsMCrC.d.mts +26 -0
  45. package/dist/index.d.mts +41 -0
  46. package/dist/index.mjs +13 -0
  47. package/dist/kit.d.mts +183 -0
  48. package/dist/kit.mjs +4 -0
  49. package/dist/provider-CoZ7cCxp.d.mts +1439 -0
  50. package/dist/provider-Ed1YhkQN.mjs +169 -0
  51. package/dist/pushin/index.d.mts +2 -0
  52. package/dist/pushin/index.mjs +1 -0
  53. package/dist/pushin-CkxgJ2cU.mjs +343 -0
  54. package/dist/schema/Actor.json +96 -0
  55. package/dist/schema/AlertSupport.json +49 -0
  56. package/dist/schema/AuthKind.json +15 -0
  57. package/dist/schema/Branch.json +31 -0
  58. package/dist/schema/ChangedFile.json +58 -0
  59. package/dist/schema/Check.json +169 -0
  60. package/dist/schema/CheckRef.json +114 -0
  61. package/dist/schema/CheckState.json +14 -0
  62. package/dist/schema/ChecksSummary.json +34 -0
  63. package/dist/schema/CiJob.json +186 -0
  64. package/dist/schema/CiJobRef.json +132 -0
  65. package/dist/schema/CiRun.json +234 -0
  66. package/dist/schema/CiRunRef.json +103 -0
  67. package/dist/schema/CodeFragment.json +19 -0
  68. package/dist/schema/CodeMatch.json +128 -0
  69. package/dist/schema/Collaborator.json +169 -0
  70. package/dist/schema/Comment.json +336 -0
  71. package/dist/schema/CommentRef.json +158 -0
  72. package/dist/schema/Commit.json +307 -0
  73. package/dist/schema/CommitRef.json +104 -0
  74. package/dist/schema/CommitSignature.json +118 -0
  75. package/dist/schema/Comparison.json +355 -0
  76. package/dist/schema/Cursor.json +21 -0
  77. package/dist/schema/EventAction.json +33 -0
  78. package/dist/schema/EventDetail.json +649 -0
  79. package/dist/schema/EventKind.json +26 -0
  80. package/dist/schema/FileContent.json +73 -0
  81. package/dist/schema/FileStatus.json +15 -0
  82. package/dist/schema/ForgeCapabilities.json +737 -0
  83. package/dist/schema/ForgeEvent.json +789 -0
  84. package/dist/schema/ForgeInstance.json +7 -0
  85. package/dist/schema/ForgeKind.json +32 -0
  86. package/dist/schema/ForgeWarning.json +49 -0
  87. package/dist/schema/ForgeWarningCode.json +7 -0
  88. package/dist/schema/GetManyResult.json +699 -0
  89. package/dist/schema/Installation.json +131 -0
  90. package/dist/schema/InstallationToken.json +31 -0
  91. package/dist/schema/KnownForgeKind.json +19 -0
  92. package/dist/schema/Label.json +22 -0
  93. package/dist/schema/MergeMethod.json +14 -0
  94. package/dist/schema/Milestone.json +40 -0
  95. package/dist/schema/Notification.json +498 -0
  96. package/dist/schema/NotificationReason.json +28 -0
  97. package/dist/schema/NotificationRef.json +60 -0
  98. package/dist/schema/NotificationSubject.json +376 -0
  99. package/dist/schema/PerKind.json +49 -0
  100. package/dist/schema/PullBranches.json +125 -0
  101. package/dist/schema/PushCommit.json +24 -0
  102. package/dist/schema/Reaction.json +148 -0
  103. package/dist/schema/ReactionContent.json +17 -0
  104. package/dist/schema/ReactionSummary.json +70 -0
  105. package/dist/schema/Release.json +286 -0
  106. package/dist/schema/ReleaseAsset.json +169 -0
  107. package/dist/schema/ReleaseAssetRef.json +138 -0
  108. package/dist/schema/ReleaseRef.json +107 -0
  109. package/dist/schema/Repo.json +354 -0
  110. package/dist/schema/RepoFeatures.json +35 -0
  111. package/dist/schema/RepoPermissions.json +35 -0
  112. package/dist/schema/RepoRef.json +77 -0
  113. package/dist/schema/RepoRole.json +15 -0
  114. package/dist/schema/RepoVisibility.json +12 -0
  115. package/dist/schema/Review.json +371 -0
  116. package/dist/schema/ReviewComment.json +280 -0
  117. package/dist/schema/ReviewCommentInput.json +34 -0
  118. package/dist/schema/ReviewRef.json +158 -0
  119. package/dist/schema/ReviewState.json +15 -0
  120. package/dist/schema/Reviewer.json +133 -0
  121. package/dist/schema/SecurityAlert.json +219 -0
  122. package/dist/schema/SecurityAlertKind.json +14 -0
  123. package/dist/schema/SecurityAlertRef.json +122 -0
  124. package/dist/schema/SecuritySeverity.json +14 -0
  125. package/dist/schema/SourceKind.json +12 -0
  126. package/dist/schema/SubscriptionState.json +12 -0
  127. package/dist/schema/Support.json +23 -0
  128. package/dist/schema/Tag.json +25 -0
  129. package/dist/schema/TextLimits.json +18 -0
  130. package/dist/schema/Thread.json +653 -0
  131. package/dist/schema/ThreadKind.json +14 -0
  132. package/dist/schema/ThreadRef.json +131 -0
  133. package/dist/schema/ThreadStack.json +146 -0
  134. package/dist/schema/ThreadState.json +13 -0
  135. package/dist/schema/TreeEntry.json +46 -0
  136. package/dist/schema/TreeEntryType.json +13 -0
  137. package/dist/schema/UpsertCommentResult.json +355 -0
  138. package/dist/schema/User.json +123 -0
  139. package/dist/schema/Webhook.json +188 -0
  140. package/dist/schema/WebhookDeliveryRecord.json +173 -0
  141. package/dist/schema/WebhookDeliveryRef.json +135 -0
  142. package/dist/schema/WebhookRef.json +105 -0
  143. package/dist/schema/index.d.mts +4 -0
  144. package/dist/schema/index.mjs +3219 -0
  145. package/dist/tangled/index.d.mts +2 -0
  146. package/dist/tangled/index.mjs +1 -0
  147. package/dist/tangled-ChdPmiv-.mjs +1570 -0
  148. package/dist/testing/index.d.mts +123 -0
  149. package/dist/testing/index.mjs +242 -0
  150. package/dist/webhooks-rn9-8lxT.mjs +70 -0
  151. package/package.json +115 -3
  152. package/dist/.gitkeep +0 -0
@@ -0,0 +1,2378 @@
1
+ import { A as toPage, At as UnresolvedThreadError, B as isNamespaceRef, C as requireIssueOrPull, D as syntheticReview, M as versionAtLeast, Mt as WebhookVerificationError, Nt as soleMergeMethod, O as toDate, Ot as TokenRevokedError, Pt as toMergeError, S as phased, T as resolveToken, Y as reactionContent, b as mergeCommitMessage, bt as InsufficientScopeError, ct as eventAction, dt as fromBase64, f as getManyConcurrently, ft as toFileContent, j as toWarning, jt as UnsupportedOperationError, l as createListing, m as hexColour, n as perKind, o as webUrlFor, pt as AuthenticationRequiredError, r as verb, t as defineForgeProvider, u as degradesToWarning, v as memo, w as requireThread, wt as NotFoundError, x as milestoneId, y as memoBy } from "./define-D3yEXtKK.mjs";
2
+ import { n as headerValue, t as bodyText } from "./crypto-CQNEyYHD.mjs";
3
+ import { i as refEvent, o as verifySharedToken, r as nativeEventsFor, t as eventKindsOf } from "./webhooks-rn9-8lxT.mjs";
4
+ //#region src/gitlab/webhook-events.ts
5
+ /**
6
+ * GitLab subscribes per event with a boolean flag rather than an event name,
7
+ * so the flag names are the native events.
8
+ */
9
+ const GITLAB_NATIVE_EVENTS = {
10
+ comment: ["note_events", "confidential_note_events"],
11
+ review: ["merge_requests_events"],
12
+ review_comment: ["note_events"],
13
+ state_change: [
14
+ "issues_events",
15
+ "confidential_issues_events",
16
+ "merge_requests_events"
17
+ ],
18
+ label: ["issues_events", "merge_requests_events"],
19
+ assignment: ["issues_events", "merge_requests_events"],
20
+ push: ["push_events"],
21
+ ref: ["tag_push_events", "push_events"],
22
+ release: ["releases_events"],
23
+ membership: ["member_events"]
24
+ };
25
+ /** Normalised kinds and actions GitLab deliveries translate into. */
26
+ const GITLAB_WEBHOOK_EVENTS = [
27
+ {
28
+ kind: "comment",
29
+ action: "created"
30
+ },
31
+ {
32
+ kind: "review",
33
+ action: "submitted"
34
+ },
35
+ {
36
+ kind: "review_comment",
37
+ action: "created"
38
+ },
39
+ {
40
+ kind: "state_change",
41
+ action: "opened"
42
+ },
43
+ {
44
+ kind: "state_change",
45
+ action: "closed"
46
+ },
47
+ {
48
+ kind: "state_change",
49
+ action: "reopened"
50
+ },
51
+ {
52
+ kind: "state_change",
53
+ action: "merged"
54
+ },
55
+ {
56
+ kind: "label",
57
+ action: "labelled"
58
+ },
59
+ {
60
+ kind: "label",
61
+ action: "unlabelled"
62
+ },
63
+ {
64
+ kind: "assignment",
65
+ action: "assigned"
66
+ },
67
+ {
68
+ kind: "assignment",
69
+ action: "unassigned"
70
+ },
71
+ {
72
+ kind: "assignment",
73
+ action: "review_requested"
74
+ },
75
+ { kind: "push" },
76
+ {
77
+ kind: "ref",
78
+ action: "created"
79
+ },
80
+ {
81
+ kind: "ref",
82
+ action: "deleted"
83
+ },
84
+ {
85
+ kind: "release",
86
+ action: "published"
87
+ },
88
+ { kind: "membership" }
89
+ ];
90
+ //#endregion
91
+ //#region src/gitlab/normalise.ts
92
+ const FORGE = "gitlab";
93
+ function toActor(instance, user) {
94
+ if (!user) return;
95
+ return {
96
+ forge: FORGE,
97
+ instance,
98
+ login: user.username,
99
+ id: String(user.id),
100
+ name: user.name,
101
+ avatarUrl: user.avatar_url ?? void 0,
102
+ url: user.web_url,
103
+ typeRaw: user.bot === void 0 ? void 0 : user.bot ? "bot" : "user",
104
+ isBotHint: user.bot === true || /^(?:project|group)_\d+_bot(?:_|$)/.test(user.username)
105
+ };
106
+ }
107
+ /** Splits a full project path into the namespace (which may contain slashes) and the project name. */
108
+ function toRepoRef(instance, pathWithNamespace, id) {
109
+ const index = pathWithNamespace.lastIndexOf("/");
110
+ return {
111
+ forge: FORGE,
112
+ instance,
113
+ owner: pathWithNamespace.slice(0, index),
114
+ name: pathWithNamespace.slice(index + 1),
115
+ externalId: id === void 0 ? void 0 : String(id)
116
+ };
117
+ }
118
+ /** Reads the project path out of a `/-/`-style web URL, which cross-project listings carry instead of a project path. */
119
+ function repoRefFromWebUrl(instance, webUrl, id) {
120
+ const path = webUrl?.match(/^https?:\/\/[^/]+\/(.+?)\/-\//)?.[1];
121
+ return path ? toRepoRef(instance, path, id) : void 0;
122
+ }
123
+ function toNamespaceRef(instance, fullPath, id) {
124
+ return {
125
+ forge: FORGE,
126
+ instance,
127
+ kind: "namespace",
128
+ owner: fullPath,
129
+ name: "",
130
+ externalId: id === void 0 ? void 0 : String(id)
131
+ };
132
+ }
133
+ /** Project id accepted by every `/projects/:id` route. */
134
+ function projectId(repo) {
135
+ return encodeURIComponent(`${repo.owner}/${repo.name}`);
136
+ }
137
+ function toThreadKind(targetType) {
138
+ switch (targetType) {
139
+ case "Issue": return "issue";
140
+ case "MergeRequest": return "pull_request";
141
+ case "Commit": return "commit";
142
+ default: return "other";
143
+ }
144
+ }
145
+ const REASONS = {
146
+ added_approver: "approval_requested",
147
+ approval_required: "approval_requested",
148
+ assigned: "assigned",
149
+ build_failed: "ci_activity",
150
+ directly_addressed: "mention",
151
+ marked: "manual",
152
+ member_access_requested: "access_requested",
153
+ mentioned: "mention",
154
+ merge_train_removed: "merge_blocked",
155
+ review_requested: "review_requested",
156
+ review_submitted: "review_submitted",
157
+ unmergeable: "merge_blocked"
158
+ };
159
+ function toReason(action) {
160
+ return REASONS[action] ?? "unknown";
161
+ }
162
+ function toState(state) {
163
+ switch (state) {
164
+ case "opened": return "open";
165
+ case "closed":
166
+ case "locked": return "closed";
167
+ case "merged": return "merged";
168
+ default: return "unknown";
169
+ }
170
+ }
171
+ function toNotification(instance, raw) {
172
+ const repo = raw.project ? toRepoRef(instance, raw.project.path_with_namespace, raw.project.id) : toNamespaceRef(instance, raw.group?.full_path ?? "", raw.group?.id);
173
+ const kind = toThreadKind(raw.target_type);
174
+ const target = raw.target ?? void 0;
175
+ const number = kind === "commit" || target?.iid === void 0 ? target?.id === void 0 ? void 0 : String(target.id) : String(target.iid);
176
+ let subject;
177
+ if (raw.target_type === "Vulnerability") subject = {
178
+ type: "security_alert",
179
+ alert: {
180
+ forge: FORGE,
181
+ instance,
182
+ repo,
183
+ id: number ?? String(raw.id)
184
+ }
185
+ };
186
+ else if (raw.target_type === "Project" || raw.target_type === "Namespace") subject = {
187
+ type: "repo",
188
+ repo
189
+ };
190
+ else if (kind === "other" && !raw.target?.iid) subject = {
191
+ type: "other",
192
+ typeRaw: raw.target_type,
193
+ repo,
194
+ id: number,
195
+ url: raw.target_url
196
+ };
197
+ else subject = {
198
+ type: "thread",
199
+ thread: {
200
+ forge: FORGE,
201
+ instance,
202
+ repo,
203
+ kind,
204
+ typeRaw: raw.target_type,
205
+ number
206
+ }
207
+ };
208
+ return {
209
+ ref: {
210
+ forge: FORGE,
211
+ instance,
212
+ id: String(raw.id)
213
+ },
214
+ subject,
215
+ subjectTypeRaw: raw.target_type,
216
+ reason: toReason(raw.action_name),
217
+ reasonRaw: raw.action_name,
218
+ unread: raw.state === "pending",
219
+ title: target?.title ?? raw.body ?? "",
220
+ subjectState: toState(target?.state),
221
+ subjectStateRaw: target?.state,
222
+ updatedAt: toDate(raw.updated_at ?? raw.created_at) ?? /* @__PURE__ */ new Date(0),
223
+ url: raw.target_url,
224
+ raw
225
+ };
226
+ }
227
+ const ACCESS_LEVELS$1 = {
228
+ admin: 50,
229
+ maintain: 40,
230
+ push: 30,
231
+ triage: 20,
232
+ pull: 10
233
+ };
234
+ /** GitLab access levels: 50 owner, 40 maintainer, 30 developer, 20 reporter, 10 guest. */
235
+ function toRole(level) {
236
+ return level >= 50 ? "admin" : level >= 40 ? "maintain" : level >= 30 ? "write" : level >= 20 ? "triage" : level >= 10 ? "read" : "none";
237
+ }
238
+ function toLabel(raw) {
239
+ return {
240
+ name: raw.name,
241
+ colour: raw.color?.replace(/^#/, ""),
242
+ description: raw.description ?? void 0
243
+ };
244
+ }
245
+ function toMilestone(raw) {
246
+ return raw ? {
247
+ id: String(raw.id),
248
+ title: raw.title,
249
+ state: raw.state === "closed" ? "closed" : "open",
250
+ description: raw.description ?? void 0,
251
+ dueOn: toDate(raw.due_date),
252
+ url: raw.web_url,
253
+ raw
254
+ } : void 0;
255
+ }
256
+ /** GitLab counts only thumbs up and down on a thread read; the full set needs the award emoji listing. */
257
+ function toVoteReactions(raw) {
258
+ if (raw.upvotes === void 0 && raw.downvotes === void 0) return;
259
+ return {
260
+ total: (raw.upvotes ?? 0) + (raw.downvotes ?? 0),
261
+ counts: {
262
+ ...raw.upvotes ? { "+1": raw.upvotes } : {},
263
+ ...raw.downvotes ? { "-1": raw.downvotes } : {}
264
+ }
265
+ };
266
+ }
267
+ function toRepo(instance, raw) {
268
+ const level = Math.max(raw.permissions?.project_access?.access_level ?? 0, raw.permissions?.group_access?.access_level ?? 0);
269
+ const visibility = raw.visibility === "internal" ? "internal" : raw.visibility === "private" ? "private" : "public";
270
+ return {
271
+ ref: toRepoRef(instance, raw.path_with_namespace, raw.id),
272
+ description: raw.description ?? void 0,
273
+ defaultBranch: raw.default_branch ?? void 0,
274
+ visibility,
275
+ visibilityRaw: raw.visibility,
276
+ isFork: Boolean(raw.forked_from_project),
277
+ isArchived: raw.archived ?? false,
278
+ parent: raw.forked_from_project ? toRepoRef(instance, raw.forked_from_project.path_with_namespace, raw.forked_from_project.id) : void 0,
279
+ topics: raw.topics ?? [],
280
+ url: raw.web_url,
281
+ cloneUrls: raw.http_url_to_repo || raw.ssh_url_to_repo ? {
282
+ https: raw.http_url_to_repo,
283
+ ssh: raw.ssh_url_to_repo
284
+ } : void 0,
285
+ createdAt: toDate(raw.created_at),
286
+ updatedAt: toDate(raw.last_activity_at),
287
+ openIssueCount: raw.open_issues_count,
288
+ owner: toActor(instance, raw.owner),
289
+ licence: raw.license?.key && raw.license.key !== "other" ? raw.license.key : void 0,
290
+ stars: raw.star_count,
291
+ forks: raw.forks_count,
292
+ mergeMethods: raw.merge_method === void 0 ? void 0 : [
293
+ ...raw.merge_method === "merge" ? ["merge"] : [],
294
+ ...raw.merge_method === "rebase_merge" ? ["rebase_merge"] : [],
295
+ ...raw.merge_method === "ff" ? ["fast_forward_only"] : [],
296
+ ...raw.squash_option && raw.squash_option !== "never" ? ["squash"] : []
297
+ ],
298
+ features: raw.issues_access_level === void 0 && raw.issues_enabled === void 0 ? void 0 : {
299
+ issues: raw.issues_enabled ?? raw.issues_access_level !== "disabled",
300
+ pullRequests: raw.merge_requests_enabled ?? raw.merge_requests_access_level !== "disabled",
301
+ discussions: false,
302
+ wiki: raw.wiki_enabled ?? raw.wiki_access_level !== "disabled",
303
+ projects: false,
304
+ releases: raw.releases_access_level !== "disabled"
305
+ },
306
+ permissions: raw.permissions ? {
307
+ admin: level >= ACCESS_LEVELS$1.admin,
308
+ maintain: level >= ACCESS_LEVELS$1.maintain,
309
+ push: level >= ACCESS_LEVELS$1.push,
310
+ triage: level >= ACCESS_LEVELS$1.triage,
311
+ pull: level >= ACCESS_LEVELS$1.pull
312
+ } : void 0,
313
+ raw
314
+ };
315
+ }
316
+ function toThread(ref, raw) {
317
+ const isMerge = ref.kind === "pull_request";
318
+ const reviewers = (raw.reviewers ?? []).flatMap((user) => {
319
+ const actor = toActor(ref.instance, user);
320
+ return actor ? [{
321
+ actor,
322
+ state: "unknown"
323
+ }] : [];
324
+ });
325
+ return {
326
+ ref: {
327
+ ...ref,
328
+ externalId: raw.id === void 0 ? ref.externalId : String(raw.id)
329
+ },
330
+ kind: ref.kind,
331
+ title: raw.title,
332
+ body: raw.description ?? void 0,
333
+ state: toState(raw.state),
334
+ stateRaw: raw.state,
335
+ isDraft: raw.draft ?? false,
336
+ assignees: (raw.assignees ?? []).flatMap((user) => toActor(ref.instance, user) ?? []),
337
+ reviewers,
338
+ lastActivityAt: toDate(raw.updated_at),
339
+ branches: isMerge && raw.source_branch && raw.target_branch ? {
340
+ head: {
341
+ ref: raw.source_branch,
342
+ sha: raw.diff_refs?.head_sha ?? raw.sha,
343
+ repo: raw.source_project_id !== void 0 && raw.source_project_id === raw.target_project_id ? ref.repo : void 0
344
+ },
345
+ base: {
346
+ ref: raw.target_branch,
347
+ sha: raw.diff_refs?.base_sha
348
+ },
349
+ mergeCommitSha: raw.merge_commit_sha ?? raw.squash_commit_sha ?? void 0
350
+ } : void 0,
351
+ author: toActor(ref.instance, raw.author),
352
+ url: raw.web_url,
353
+ createdAt: toDate(raw.created_at),
354
+ updatedAt: toDate(raw.updated_at),
355
+ closedAt: toDate(raw.closed_at),
356
+ labels: (raw.labels ?? []).map((label) => typeof label === "string" ? { name: label } : {
357
+ name: label.name,
358
+ colour: label.color,
359
+ description: label.description ?? void 0
360
+ }),
361
+ locked: raw.discussion_locked ?? void 0,
362
+ commentCount: raw.user_notes_count,
363
+ milestone: toMilestone(raw.milestone),
364
+ reactions: toVoteReactions(raw),
365
+ raw
366
+ };
367
+ }
368
+ function toCommitThread(ref, raw) {
369
+ return {
370
+ ref,
371
+ kind: "commit",
372
+ title: raw.title,
373
+ body: raw.message,
374
+ state: "unknown",
375
+ isDraft: false,
376
+ assignees: [],
377
+ reviewers: [],
378
+ url: raw.web_url,
379
+ createdAt: toDate(raw.created_at),
380
+ lastActivityAt: toDate(raw.created_at),
381
+ labels: [],
382
+ raw
383
+ };
384
+ }
385
+ /** System notes are the only record of label, state and assignment changes in the notes API. */
386
+ const SYSTEM_NOTE_KINDS = [
387
+ [/^approved this merge request/i, "review"],
388
+ [/^added \d+ (?:new )?commits?/i, "commit"],
389
+ [/^(?:added|removed) .*label/i, "label"],
390
+ [/^(?:closed|reopened|merged|marked this merge request as|status changed to)/i, "state_change"],
391
+ [/^(?:assigned to|reassigned to|unassigned|requested review from)/i, "assignment"],
392
+ [/^mentioned in /i, "referenced"]
393
+ ];
394
+ function toNoteKind(note) {
395
+ if (!note.system) return note.type === "DiffNote" ? "review_comment" : "comment";
396
+ return SYSTEM_NOTE_KINDS.find(([pattern]) => pattern.test(note.body))?.[1] ?? "other";
397
+ }
398
+ /**
399
+ * A "mentioned in" system note names the subject that mentioned this thread:
400
+ * `!5` or `acme/widgets!5` for a merge request, `#5` for an issue, or a sha
401
+ * for a commit. The reference is read from that wording, since the note
402
+ * carries no structured source.
403
+ */
404
+ function crossReference(ref, body) {
405
+ const text = body.match(/^mentioned in (?:merge request|issue|commit) (\S+)/i)?.[1];
406
+ if (!text) return {
407
+ type: "referenced",
408
+ text: body
409
+ };
410
+ const thread = text.match(/^(?:([^!#]+)([!#]))?[!#]?(\d+)$/);
411
+ if (thread) {
412
+ const repo = thread[1] ? toRepoRef(ref.instance, thread[1]) : ref.repo;
413
+ return {
414
+ type: "referenced",
415
+ from: {
416
+ forge: FORGE,
417
+ instance: ref.instance,
418
+ repo,
419
+ kind: text.includes("!") ? "pull_request" : "issue",
420
+ number: thread[3]
421
+ },
422
+ fromRepo: repo,
423
+ text
424
+ };
425
+ }
426
+ return /^[0-9a-f]{7,40}$/i.test(text) ? {
427
+ type: "referenced",
428
+ from: {
429
+ forge: FORGE,
430
+ instance: ref.instance,
431
+ repo: ref.repo,
432
+ sha: text
433
+ },
434
+ fromRepo: ref.repo,
435
+ text
436
+ } : {
437
+ type: "referenced",
438
+ text
439
+ };
440
+ }
441
+ function toNoteEvent(ref, note) {
442
+ const actor = toActor(ref.instance, note.author);
443
+ const kind = toNoteKind(note);
444
+ const who = actor?.login ?? "someone";
445
+ return {
446
+ forge: FORGE,
447
+ instance: ref.instance,
448
+ id: String(note.id),
449
+ kind,
450
+ kindRaw: note.system ? "system" : note.type ?? "Note",
451
+ summary: note.system ? `${who} ${note.body.split("\n")[0]}` : `${who} ${kind === "review_comment" ? "commented on a line" : "commented"}`,
452
+ occurredAt: toDate(note.created_at) ?? /* @__PURE__ */ new Date(0),
453
+ actor,
454
+ repo: ref.repo,
455
+ thread: ref,
456
+ ...kind === "referenced" ? { detail: crossReference(ref, note.body) } : {},
457
+ source: "poll",
458
+ payload: note
459
+ };
460
+ }
461
+ function toCommitCommentEvent(ref, comment, index) {
462
+ const actor = toActor(ref.instance, comment.author);
463
+ return {
464
+ forge: FORGE,
465
+ instance: ref.instance,
466
+ id: `${ref.number}:${index}`,
467
+ kind: comment.line_type ? "review_comment" : "comment",
468
+ kindRaw: "CommitComment",
469
+ summary: `${actor?.login ?? "someone"} commented`,
470
+ occurredAt: toDate(comment.created_at) ?? /* @__PURE__ */ new Date(0),
471
+ actor,
472
+ repo: ref.repo,
473
+ thread: ref,
474
+ source: "poll",
475
+ payload: comment
476
+ };
477
+ }
478
+ function toNoteComment(thread, note, discussion) {
479
+ return {
480
+ ref: {
481
+ forge: FORGE,
482
+ instance: thread.instance,
483
+ thread,
484
+ id: String(note.id)
485
+ },
486
+ body: note.body,
487
+ author: toActor(thread.instance, note.author),
488
+ createdAt: toDate(note.created_at),
489
+ updatedAt: toDate(note.updated_at),
490
+ ...discussion && note.resolvable && { thread: {
491
+ id: discussion.id,
492
+ resolved: Boolean(note.resolved)
493
+ } },
494
+ raw: note
495
+ };
496
+ }
497
+ /** GitLab commit comments carry no id, so they are keyed by creation time and author. */
498
+ function toCommitComment(thread, comment) {
499
+ return {
500
+ ref: {
501
+ forge: FORGE,
502
+ instance: thread.instance,
503
+ thread,
504
+ id: `${comment.created_at ?? ""}:${comment.author?.id ?? ""}`
505
+ },
506
+ body: comment.note,
507
+ author: toActor(thread.instance, comment.author),
508
+ createdAt: toDate(comment.created_at),
509
+ raw: comment
510
+ };
511
+ }
512
+ /** Pipeline and job statuses. `manual` jobs wait for a person, so they neither pass nor fail. */
513
+ function pipelineState(status) {
514
+ switch (status) {
515
+ case "success": return "success";
516
+ case "failed":
517
+ case "canceled": return "failure";
518
+ case "skipped":
519
+ case "manual": return "neutral";
520
+ case "created":
521
+ case "waiting_for_resource":
522
+ case "preparing":
523
+ case "pending":
524
+ case "running":
525
+ case "scheduled": return "pending";
526
+ default: return "unknown";
527
+ }
528
+ }
529
+ /** The merge request's head pipeline, which GitLab reports as one combined status. */
530
+ function toPipelineSummary(pipeline) {
531
+ const state = pipelineState(pipeline.status);
532
+ return {
533
+ state: state === "neutral" ? "pending" : state,
534
+ stateRaw: pipeline.status,
535
+ ...pipeline.web_url ? { url: pipeline.web_url } : {}
536
+ };
537
+ }
538
+ function toJobCheck(repo, job) {
539
+ const state = pipelineState(job.status);
540
+ return {
541
+ ref: {
542
+ forge: FORGE,
543
+ instance: repo.instance,
544
+ repo,
545
+ id: String(job.id),
546
+ type: "job"
547
+ },
548
+ name: job.stage ? `${job.stage}: ${job.name}` : job.name,
549
+ state: state === "failure" && job.allow_failure ? "neutral" : state,
550
+ stateRaw: job.status,
551
+ url: job.web_url,
552
+ startedAt: toDate(job.started_at),
553
+ completedAt: toDate(job.finished_at),
554
+ raw: job
555
+ };
556
+ }
557
+ /** GitLab keys releases by tag and has no drafts or prereleases. */
558
+ function toRelease(repo, raw) {
559
+ return {
560
+ ref: {
561
+ forge: FORGE,
562
+ instance: repo.instance,
563
+ repo,
564
+ id: raw.tag_name,
565
+ tag: raw.tag_name
566
+ },
567
+ name: raw.name ?? void 0,
568
+ tag: raw.tag_name,
569
+ body: raw.description ?? void 0,
570
+ isDraft: false,
571
+ isPrerelease: false,
572
+ author: toActor(repo.instance, raw.author),
573
+ publishedAt: raw.upcoming_release ? void 0 : toDate(raw.released_at),
574
+ createdAt: toDate(raw.created_at),
575
+ url: raw._links?.self,
576
+ assets: raw.assets?.links?.map((link) => ({
577
+ name: link.name,
578
+ url: link.direct_asset_url ?? link.url
579
+ })),
580
+ raw
581
+ };
582
+ }
583
+ const REPORT_KINDS = {
584
+ DEPENDENCY_SCANNING: "dependency",
585
+ CONTAINER_SCANNING: "dependency",
586
+ CLUSTER_IMAGE_SCANNING: "dependency",
587
+ SAST: "code_scanning",
588
+ SECRET_DETECTION: "secret"
589
+ };
590
+ const REPORT_TYPES = {
591
+ dependency: [
592
+ "DEPENDENCY_SCANNING",
593
+ "CONTAINER_SCANNING",
594
+ "CLUSTER_IMAGE_SCANNING"
595
+ ],
596
+ code_scanning: ["SAST"],
597
+ secret: ["SECRET_DETECTION"],
598
+ advisory: []
599
+ };
600
+ /** `INFO` findings are informational and carry no severity. */
601
+ function toSeverity(raw) {
602
+ const severity = raw.toLowerCase();
603
+ return severity === "critical" || severity === "high" || severity === "medium" || severity === "low" ? severity : "unknown";
604
+ }
605
+ function toVulnerability(repo, raw) {
606
+ const kind = REPORT_KINDS[raw.reportType] ?? "other";
607
+ const state = raw.state === "DETECTED" || raw.state === "CONFIRMED" ? "open" : raw.state === "RESOLVED" ? "fixed" : raw.state === "DISMISSED" ? "dismissed" : "unknown";
608
+ const id = raw.id.replace(/^gid:\/\/gitlab\/Vulnerability\//, "");
609
+ return {
610
+ ref: {
611
+ forge: FORGE,
612
+ instance: repo.instance,
613
+ repo,
614
+ id,
615
+ kind,
616
+ severityRaw: raw.severity.toLowerCase()
617
+ },
618
+ kind,
619
+ kindRaw: raw.reportType.toLowerCase(),
620
+ severity: toSeverity(raw.severity),
621
+ severityRaw: raw.severity.toLowerCase(),
622
+ state,
623
+ stateRaw: raw.state.toLowerCase(),
624
+ title: raw.title,
625
+ url: raw.webUrl ?? void 0,
626
+ createdAt: toDate(raw.detectedAt),
627
+ updatedAt: toDate(raw.updatedAt),
628
+ dismissedAt: toDate(raw.dismissedAt),
629
+ raw
630
+ };
631
+ }
632
+ function toStatusCheck(repo, raw) {
633
+ const state = pipelineState(raw.status);
634
+ return {
635
+ ref: {
636
+ forge: FORGE,
637
+ instance: repo.instance,
638
+ repo,
639
+ id: String(raw.id),
640
+ type: "status"
641
+ },
642
+ name: raw.name ?? String(raw.id),
643
+ state: state === "failure" && raw.allow_failure ? "neutral" : state,
644
+ stateRaw: raw.status,
645
+ url: raw.target_url ?? void 0,
646
+ startedAt: toDate(raw.created_at),
647
+ completedAt: toDate(raw.finished_at),
648
+ raw
649
+ };
650
+ }
651
+ function toPipeline(repo, raw) {
652
+ return {
653
+ ref: {
654
+ forge: FORGE,
655
+ instance: repo.instance,
656
+ repo,
657
+ id: String(raw.id)
658
+ },
659
+ name: raw.name || raw.source || `pipeline ${raw.id}`,
660
+ state: pipelineState(raw.status),
661
+ stateRaw: raw.status,
662
+ number: raw.iid === void 0 ? void 0 : String(raw.iid),
663
+ eventRaw: raw.source,
664
+ branch: raw.ref,
665
+ sha: raw.sha,
666
+ url: raw.web_url,
667
+ actor: toActor(repo.instance, raw.user),
668
+ createdAt: toDate(raw.created_at),
669
+ startedAt: toDate(raw.started_at),
670
+ completedAt: toDate(raw.finished_at),
671
+ raw
672
+ };
673
+ }
674
+ function toCiJob(run, raw) {
675
+ const state = pipelineState(raw.status);
676
+ return {
677
+ ref: {
678
+ forge: FORGE,
679
+ instance: run.instance,
680
+ repo: run.repo,
681
+ id: String(raw.id),
682
+ run
683
+ },
684
+ name: raw.name,
685
+ state: state === "failure" && raw.allow_failure ? "neutral" : state,
686
+ stateRaw: raw.status,
687
+ stage: raw.stage,
688
+ url: raw.web_url,
689
+ startedAt: toDate(raw.started_at),
690
+ completedAt: toDate(raw.finished_at),
691
+ raw
692
+ };
693
+ }
694
+ function toCommit(repo, raw, files) {
695
+ return {
696
+ ref: {
697
+ forge: FORGE,
698
+ instance: repo.instance,
699
+ repo,
700
+ sha: raw.id
701
+ },
702
+ sha: raw.id,
703
+ message: raw.message ?? raw.title,
704
+ author: {
705
+ name: raw.author_name,
706
+ email: raw.author_email,
707
+ date: toDate(raw.authored_date ?? raw.created_at)
708
+ },
709
+ committer: {
710
+ name: raw.committer_name,
711
+ email: raw.committer_email,
712
+ date: toDate(raw.committed_date)
713
+ },
714
+ parents: raw.parent_ids ?? [],
715
+ url: raw.web_url,
716
+ ...raw.stats ? { stats: {
717
+ additions: raw.stats.additions ?? 0,
718
+ deletions: raw.stats.deletions ?? 0,
719
+ total: raw.stats.total
720
+ } } : {},
721
+ ...files ? { files } : {},
722
+ raw
723
+ };
724
+ }
725
+ /** GitLab reports a change by three booleans and a unified diff, with no counts. */
726
+ function toChangedFile(raw) {
727
+ const path = raw.new_path ?? raw.old_path ?? "";
728
+ const status = raw.new_file ? "added" : raw.deleted_file ? "removed" : raw.renamed_file ? "renamed" : "modified";
729
+ return {
730
+ path,
731
+ previousPath: raw.renamed_file ? raw.old_path : void 0,
732
+ status,
733
+ statusRaw: status,
734
+ patch: raw.diff
735
+ };
736
+ }
737
+ function toBranch(raw) {
738
+ return {
739
+ name: raw.name,
740
+ sha: raw.commit?.id ?? "",
741
+ isDefault: raw.default,
742
+ isProtected: raw.protected,
743
+ url: raw.web_url,
744
+ raw
745
+ };
746
+ }
747
+ function toTag(raw) {
748
+ return {
749
+ name: raw.name,
750
+ sha: raw.commit?.id ?? raw.target ?? "",
751
+ raw
752
+ };
753
+ }
754
+ function toTreeEntry(raw) {
755
+ return {
756
+ path: raw.path,
757
+ type: raw.mode === "120000" ? "symlink" : raw.type === "tree" ? "directory" : raw.type === "commit" ? "submodule" : "file",
758
+ sha: raw.id,
759
+ mode: raw.mode
760
+ };
761
+ }
762
+ function toWebhook(target, raw) {
763
+ const nativeEvents = Object.entries(raw).filter(([key, value]) => key.endsWith("_events") && value === true).map(([key]) => key);
764
+ return {
765
+ ref: {
766
+ forge: FORGE,
767
+ instance: target.instance,
768
+ target,
769
+ id: String(raw.id)
770
+ },
771
+ url: raw.url,
772
+ events: eventKindsOf(GITLAB_NATIVE_EVENTS, nativeEvents),
773
+ nativeEvents,
774
+ active: !raw.disabled_until,
775
+ createdAt: toDate(raw.created_at),
776
+ raw
777
+ };
778
+ }
779
+ function toWebhookDelivery(hook, raw) {
780
+ const status = Number(raw.response_status) || 0;
781
+ return {
782
+ ref: {
783
+ forge: FORGE,
784
+ instance: hook.instance,
785
+ hook,
786
+ id: String(raw.id)
787
+ },
788
+ event: raw.trigger ?? "",
789
+ status,
790
+ ok: status >= 200 && status < 300,
791
+ deliveredAt: toDate(raw.created_at),
792
+ duration: raw.execution_duration === void 0 ? void 0 : Math.round(raw.execution_duration * 1e3),
793
+ raw
794
+ };
795
+ }
796
+ //#endregion
797
+ //#region src/gitlab/web.ts
798
+ const SECTIONS = {
799
+ issue: "issues",
800
+ pull_request: "merge_requests",
801
+ commit: "commit"
802
+ };
803
+ const KINDS = new Map(Object.entries(SECTIONS).map(([kind, section]) => [section, kind]));
804
+ const PREFIXES = {
805
+ issue: "#",
806
+ pull_request: "!"
807
+ };
808
+ const encode = (path) => path.split("/").map(encodeURIComponent).join("/");
809
+ const projectPath = (repo) => `/${encode(repo.owner)}/${encodeURIComponent(repo.name)}`;
810
+ function gitlabWeb(origin) {
811
+ return {
812
+ origin,
813
+ repo: (repo) => repo.kind === "namespace" ? `/${encode(repo.owner)}` : projectPath(repo),
814
+ thread: (ref) => ref.repo.kind === "namespace" || !SECTIONS[ref.kind] ? void 0 : `${projectPath(ref.repo)}/-/${SECTIONS[ref.kind]}/${encodeURIComponent(ref.number)}`,
815
+ comment: (ref) => ref.thread.repo.kind === "namespace" || !SECTIONS[ref.thread.kind] ? void 0 : `${projectPath(ref.thread.repo)}/-/${SECTIONS[ref.thread.kind]}/${encodeURIComponent(ref.thread.number)}#note_${ref.id}`,
816
+ release: (ref) => ref.tag ? `${projectPath(ref.repo)}/-/releases/${encodeURIComponent(ref.tag)}` : void 0,
817
+ file: (repo, path, at, line) => `${projectPath(repo)}/-/blob/${encodeURIComponent(at)}/${encode(path)}${line ? `#L${line}` : ""}`,
818
+ compare: (repo, base, head) => `${projectPath(repo)}/-/compare/${encodeURIComponent(base)}...${encodeURIComponent(head)}`,
819
+ parse: (segments, url, from) => {
820
+ const separator = segments.indexOf("-");
821
+ const path = separator < 0 ? segments : segments.slice(0, separator);
822
+ if (path.length < 2) return;
823
+ const repo = {
824
+ ...from,
825
+ owner: path.slice(0, -1).join("/"),
826
+ name: path.at(-1).replace(/\.git$/, "")
827
+ };
828
+ const [section, id] = separator < 0 ? [] : segments.slice(separator + 1);
829
+ if (section === "releases" && id) return {
830
+ repo,
831
+ release: {
832
+ ...from,
833
+ repo,
834
+ id,
835
+ tag: id
836
+ }
837
+ };
838
+ const kind = section ? KINDS.get(section) : void 0;
839
+ if (!kind || !id) return { repo };
840
+ const thread = {
841
+ ...from,
842
+ repo,
843
+ kind,
844
+ number: id
845
+ };
846
+ const note = url.hash.startsWith("#note_") ? url.hash.slice(6) : void 0;
847
+ return {
848
+ repo,
849
+ thread,
850
+ ...note ? { comment: {
851
+ ...from,
852
+ thread,
853
+ id: note
854
+ } } : {}
855
+ };
856
+ },
857
+ reference: (ref, same, expand) => {
858
+ if (ref.kind === "commit") return `${same ? "" : `${ref.repo.owner}/${ref.repo.name}@`}${ref.number.slice(0, 8)}`;
859
+ const prefix = PREFIXES[ref.kind];
860
+ if (!prefix || ref.repo.kind === "namespace") return;
861
+ return `${same ? "" : `${ref.repo.owner}/${ref.repo.name}`}${prefix}${ref.number}${expand ? "+" : ""}`;
862
+ }
863
+ };
864
+ }
865
+ //#endregion
866
+ //#region src/gitlab/webhooks.ts
867
+ const TOKEN_HEADER = "x-gitlab-token";
868
+ const EVENT_HEADER = "x-gitlab-event";
869
+ /**
870
+ * GitLab sends the configured secret verbatim in `X-Gitlab-Token`; there is no
871
+ * body signature to compute.
872
+ */
873
+ async function verifyGitLabToken(delivery, secret) {
874
+ return verifySharedToken(delivery, secret, TOKEN_HEADER);
875
+ }
876
+ const ZERO_SHA = /^0+$/;
877
+ function repoLevel(instance, payload, who, repo) {
878
+ switch (payload.object_kind ?? payload.event_name) {
879
+ case "push":
880
+ case "tag_push": return refEvent(who, {
881
+ ref: payload.ref ?? "",
882
+ before: payload.before,
883
+ after: payload.after,
884
+ created: Boolean(payload.before && ZERO_SHA.test(payload.before)),
885
+ deleted: Boolean(payload.after && ZERO_SHA.test(payload.after)),
886
+ commits: (payload.commits ?? []).map((commit) => ({
887
+ sha: commit.id,
888
+ message: commit.message,
889
+ author: commit.author?.name,
890
+ url: commit.url
891
+ })),
892
+ commitCount: payload.total_commits_count
893
+ });
894
+ case "release":
895
+ if (payload.action !== "create" || !repo) return;
896
+ return {
897
+ kind: "release",
898
+ action: "published",
899
+ detail: {
900
+ type: "release",
901
+ release: {
902
+ forge: FORGE,
903
+ instance,
904
+ repo,
905
+ id: payload.tag ?? String(payload.id),
906
+ tag: payload.tag
907
+ },
908
+ name: payload.name
909
+ },
910
+ summary: `${who} published ${payload.name ?? payload.tag ?? "a release"}`
911
+ };
912
+ case "project_rename": return {
913
+ kind: "repo",
914
+ action: "renamed",
915
+ detail: {
916
+ type: "repo_renamed",
917
+ from: payload.old_path_with_namespace ?? "",
918
+ to: payload.path_with_namespace ?? ""
919
+ },
920
+ summary: `${who} renamed the project`
921
+ };
922
+ case "project_transfer": return {
923
+ kind: "repo",
924
+ action: "transferred",
925
+ detail: {
926
+ type: "repo_transferred",
927
+ fromOwner: payload.old_path_with_namespace?.split("/").slice(0, -1).join("/"),
928
+ toOwner: payload.path_with_namespace?.split("/").slice(0, -1).join("/")
929
+ },
930
+ summary: `${who} transferred the project`
931
+ };
932
+ case "user_add_to_team":
933
+ case "user_remove_from_team":
934
+ case "user_add_to_group":
935
+ case "user_remove_from_group":
936
+ case "user_access_request_to_project":
937
+ case "user_update_for_team":
938
+ case "user_update_for_group": return {
939
+ kind: "membership",
940
+ action: membershipAction(payload.event_name),
941
+ detail: {
942
+ type: "membership",
943
+ actionRaw: payload.event_name ?? "member",
944
+ member: payload.user_name ? toActor(instance, {
945
+ id: payload.user_id ?? 0,
946
+ username: payload.user_username ?? payload.user_name,
947
+ name: payload.user_name
948
+ }) : void 0
949
+ },
950
+ summary: `${who} changed a membership`
951
+ };
952
+ default: return;
953
+ }
954
+ }
955
+ function membershipAction(eventName) {
956
+ if (eventName?.startsWith("user_add_")) return "added";
957
+ if (eventName?.startsWith("user_remove_")) return "removed";
958
+ return eventName?.startsWith("user_update_") ? "edited" : "other";
959
+ }
960
+ function diff(change, key) {
961
+ const previous = change?.previous ?? [];
962
+ const current = change?.current ?? [];
963
+ const before = new Set(previous.map(key));
964
+ const after = new Set(current.map(key));
965
+ return {
966
+ added: current.filter((item) => !before.has(key(item))),
967
+ removed: previous.filter((item) => !after.has(key(item)))
968
+ };
969
+ }
970
+ /** One event per label, assignee or reviewer an `update` delivery adds or removes. */
971
+ function changeEvents(instance, payload, who) {
972
+ const changes = payload.changes;
973
+ if (payload.object_attributes?.action !== "update" || !changes) return [];
974
+ const events = [];
975
+ const labels = diff(changes.labels, (label) => label.title);
976
+ for (const [items, action, verb] of [[
977
+ labels.added,
978
+ "labelled",
979
+ "added"
980
+ ], [
981
+ labels.removed,
982
+ "unlabelled",
983
+ "removed"
984
+ ]]) for (const { title = "" } of items) events.push({
985
+ kind: "label",
986
+ action,
987
+ detail: {
988
+ type: "label",
989
+ label: title
990
+ },
991
+ summary: `${who} ${verb} label ${title}`.trim()
992
+ });
993
+ const people = (change, added, removed) => {
994
+ const { added: joined, removed: left } = diff(change, (user) => user.username);
995
+ for (const [users, [action, verb]] of [[joined, added], [left, removed]]) for (const user of users) events.push({
996
+ kind: "assignment",
997
+ action,
998
+ detail: {
999
+ type: "assignment",
1000
+ assignee: toActor(instance, user)
1001
+ },
1002
+ summary: `${who} ${verb} ${user.username}`
1003
+ });
1004
+ };
1005
+ people(changes.assignees, ["assigned", "assigned"], ["unassigned", "unassigned"]);
1006
+ people(changes.reviewers, ["review_requested", "requested review from"], ["other", "removed the review request for"]);
1007
+ return events;
1008
+ }
1009
+ function eventKindFor(payload) {
1010
+ const attributes = payload.object_attributes;
1011
+ switch (payload.object_kind) {
1012
+ case "note": return attributes?.type === "DiffNote" || attributes?.position ? "review_comment" : "comment";
1013
+ case "push": return "commit";
1014
+ case "emoji": return "reaction";
1015
+ case "issue":
1016
+ case "merge_request": switch (attributes?.action) {
1017
+ case "approved":
1018
+ case "approval": return "review";
1019
+ case "open":
1020
+ case "close":
1021
+ case "reopen":
1022
+ case "merge": return "state_change";
1023
+ case "update":
1024
+ if (payload.changes?.labels) return "label";
1025
+ if (payload.changes?.assignees || payload.changes?.reviewers) return "assignment";
1026
+ return "other";
1027
+ default: return "other";
1028
+ }
1029
+ default: return "other";
1030
+ }
1031
+ }
1032
+ function translateGitLabWebhook(instance, delivery) {
1033
+ const event = headerValue(delivery.headers, EVENT_HEADER);
1034
+ if (!event) throw new WebhookVerificationError(`Missing ${EVENT_HEADER} header`, {
1035
+ forge: FORGE,
1036
+ instance
1037
+ });
1038
+ const deliveryId = headerValue(delivery.headers, "x-gitlab-event-uuid") ?? crypto.randomUUID();
1039
+ const payload = JSON.parse(bodyText(delivery.body));
1040
+ const repo = payload.project?.path_with_namespace ? toRepoRef(instance, payload.project.path_with_namespace, payload.project.id) : payload.path_with_namespace ? toRepoRef(instance, payload.path_with_namespace, payload.project_id) : void 0;
1041
+ const user = payload.user?.username ? toActor(instance, {
1042
+ id: payload.user.id ?? 0,
1043
+ ...payload.user,
1044
+ username: payload.user.username
1045
+ }) : void 0;
1046
+ const attributes = payload.object_attributes;
1047
+ const subject = payload.object_kind === "note" ? payload.merge_request ? {
1048
+ kind: "pull_request",
1049
+ iid: payload.merge_request.iid
1050
+ } : payload.issue ? {
1051
+ kind: "issue",
1052
+ iid: payload.issue.iid
1053
+ } : void 0 : payload.object_kind === "merge_request" && attributes?.iid !== void 0 ? {
1054
+ kind: "pull_request",
1055
+ iid: attributes.iid
1056
+ } : payload.object_kind === "issue" && attributes?.iid !== void 0 ? {
1057
+ kind: "issue",
1058
+ iid: attributes.iid
1059
+ } : void 0;
1060
+ const thread = repo && subject ? {
1061
+ forge: FORGE,
1062
+ instance,
1063
+ repo,
1064
+ kind: subject.kind,
1065
+ number: String(subject.iid)
1066
+ } : void 0;
1067
+ const level = repoLevel(instance, payload, user?.login ?? payload.user_username ?? "someone", repo);
1068
+ if (level) return [{
1069
+ forge: FORGE,
1070
+ instance,
1071
+ id: deliveryId,
1072
+ kind: level.kind,
1073
+ action: level.action,
1074
+ kindRaw: payload.object_kind ?? payload.event_name ?? event,
1075
+ summary: level.summary,
1076
+ occurredAt: toDate(payload.commits?.at(-1)?.timestamp ?? payload.released_at) ?? /* @__PURE__ */ new Date(),
1077
+ actor: user,
1078
+ repo,
1079
+ detail: level.detail,
1080
+ source: "webhook",
1081
+ payload
1082
+ }];
1083
+ const occurredAt = toDate(attributes?.created_at && payload.object_kind === "note" ? attributes.created_at : attributes?.updated_at) ?? /* @__PURE__ */ new Date();
1084
+ const changes = changeEvents(instance, payload, user?.login ?? "someone");
1085
+ if (changes.length) return changes.map((change, index) => ({
1086
+ forge: FORGE,
1087
+ instance,
1088
+ id: changes.length === 1 ? deliveryId : `${deliveryId}:${index}`,
1089
+ ...change,
1090
+ kindRaw: `${payload.object_kind}.update`,
1091
+ actionRaw: "update",
1092
+ occurredAt,
1093
+ actor: user,
1094
+ repo,
1095
+ thread,
1096
+ source: "webhook",
1097
+ payload
1098
+ }));
1099
+ const kind = eventKindFor(payload);
1100
+ const action = attributes?.action;
1101
+ return [{
1102
+ forge: FORGE,
1103
+ instance,
1104
+ id: deliveryId,
1105
+ kind,
1106
+ kindRaw: action ? `${payload.object_kind}.${action}` : payload.object_kind ?? event,
1107
+ action: action === "update" && attributes?.oldrev ? "synchronised" : action === "merge" ? "merged" : eventAction(action),
1108
+ actionRaw: action,
1109
+ detail: kind === "comment" || kind === "review_comment" ? {
1110
+ type: kind,
1111
+ comment: thread && attributes?.id !== void 0 ? {
1112
+ forge: FORGE,
1113
+ instance,
1114
+ thread,
1115
+ id: String(attributes.id)
1116
+ } : void 0,
1117
+ body: attributes?.note
1118
+ } : kind === "state_change" ? {
1119
+ type: "state_change",
1120
+ state: action === "merge" ? "merged" : action === "close" ? "closed" : "open"
1121
+ } : void 0,
1122
+ summary: payload.object_kind === "note" ? `${user?.login ?? "someone"} commented` : `${user?.login ?? "someone"} ${action ? `${action} ` : ""}${payload.object_kind ?? event}`.trim(),
1123
+ occurredAt,
1124
+ actor: user,
1125
+ repo,
1126
+ thread,
1127
+ source: "webhook",
1128
+ payload
1129
+ }];
1130
+ }
1131
+ const gitlabWebhooks = ({ options, instance }) => ({
1132
+ events: GITLAB_WEBHOOK_EVENTS,
1133
+ verify: (delivery) => verifyGitLabToken(delivery, options.webhookSecret),
1134
+ translate: (delivery) => translateGitLabWebhook(instance, delivery)
1135
+ });
1136
+ //#endregion
1137
+ //#region src/gitlab/index.ts
1138
+ /** GitLab versions with the webhook event log and resend endpoints. */
1139
+ const GITLAB_HOOK_EVENTS = "17.0";
1140
+ const ISSUE_LIKE = {
1141
+ issue: true,
1142
+ pull_request: true
1143
+ };
1144
+ /** Access level per normalised role, as `POST /projects/:id/members` takes it. */
1145
+ const ACCESS_LEVELS = {
1146
+ admin: 50,
1147
+ maintain: 40,
1148
+ write: 30,
1149
+ triage: 20,
1150
+ read: 10,
1151
+ none: 0
1152
+ };
1153
+ /** GitLab awards emoji by name; these are the names behind the normalised reactions. */
1154
+ const AWARD_EMOJI = {
1155
+ "+1": "thumbsup",
1156
+ "-1": "thumbsdown",
1157
+ "laugh": "laughing",
1158
+ "confused": "confused",
1159
+ "heart": "heart",
1160
+ "hooray": "tada",
1161
+ "rocket": "rocket",
1162
+ "eyes": "eyes"
1163
+ };
1164
+ /** The model's name for each award emoji `forges` writes. */
1165
+ const REACTION_NAMES = Object.fromEntries(Object.entries(AWARD_EMOJI).map(([content, name]) => [name, content]));
1166
+ const VULNERABILITIES_QUERY = `query ProjectVulnerabilities($fullPath: ID!, $first: Int, $after: String, $state: [VulnerabilityState!], $reportType: [VulnerabilityReportType!]) {
1167
+ project(fullPath: $fullPath) {
1168
+ vulnerabilities(first: $first, after: $after, state: $state, reportType: $reportType) {
1169
+ pageInfo { hasNextPage endCursor }
1170
+ nodes { id title severity state reportType detectedAt updatedAt dismissedAt resolvedAt webUrl }
1171
+ }
1172
+ }
1173
+ }`;
1174
+ const PROJECTS_QUERY = `query CodeSearchProjects($ids: [ID!]) {
1175
+ projects(ids: $ids) {
1176
+ nodes { id fullPath }
1177
+ }
1178
+ }`;
1179
+ const BASE_METHODS = {
1180
+ merge: "merge",
1181
+ rebase_merge: "rebase_merge",
1182
+ ff: "fast_forward_only"
1183
+ };
1184
+ function threadPath(ref) {
1185
+ const project = `/projects/${projectId(ref.repo)}`;
1186
+ switch (ref.kind) {
1187
+ case "pull_request": return `${project}/merge_requests/${encodeURIComponent(ref.number)}`;
1188
+ case "commit": return `${project}/repository/commits/${encodeURIComponent(ref.number)}`;
1189
+ default: return `${project}/issues/${encodeURIComponent(ref.number)}`;
1190
+ }
1191
+ }
1192
+ const GITLAB = {
1193
+ forge: FORGE,
1194
+ baseUrl: "https://gitlab.com",
1195
+ anonymous: true,
1196
+ apiPath: "/api/v4",
1197
+ headers: { accept: "application/json" },
1198
+ authHeaders: ({ options: { auth } }) => auth?.type === "token" ? async () => ({ authorization: `Bearer ${await resolveToken(auth)}` }) : void 0,
1199
+ setup({ options, instance, baseUrl, origin: context, fetcher }) {
1200
+ /** GitLab serves notes, labels, milestones, members, commit statuses, commit and code search and vulnerabilities only to signed-in users. */
1201
+ const anonymous = options.auth?.type === "anonymous";
1202
+ const web = gitlabWeb(baseUrl.replace(/\/api\/v4$/, ""));
1203
+ /**
1204
+ * GitLab lists pending and done to-dos separately. With `all`, pending
1205
+ * pages are followed by done pages; `cursor.token` marks the switch.
1206
+ */
1207
+ async function notificationPage(listOptions = {}) {
1208
+ const cursor = listOptions.cursor;
1209
+ const state = cursor?.token === "done" ? "done" : "pending";
1210
+ const result = await fetcher.json(cursor?.nextUrl ?? "/todos", {
1211
+ query: cursor?.nextUrl ? void 0 : {
1212
+ state,
1213
+ per_page: listOptions.perPage
1214
+ },
1215
+ signal: listOptions.signal
1216
+ });
1217
+ const since = listOptions.since?.getTime();
1218
+ const items = (result.data ?? []).map((raw) => toNotification(instance, raw)).filter((item) => since === void 0 || item.updatedAt.getTime() >= since);
1219
+ const phase = cursor?.token ?? "pending";
1220
+ return {
1221
+ items,
1222
+ cursor: result.cursor?.nextUrl ? {
1223
+ ...result.cursor,
1224
+ token: phase === "done" ? "done" : void 0
1225
+ } : listOptions.all && phase !== "done" ? { token: "done" } : void 0,
1226
+ notModified: result.notModified
1227
+ };
1228
+ }
1229
+ /** GitLab assigns by user id, so logins are resolved once each. */
1230
+ /** Award emoji hang off the issue, the merge request or a note, all under the same path. */
1231
+ function awardPath(target) {
1232
+ return "thread" in target ? `${threadPath(requireIssueOrPull(target.thread, context, "react to"))}/notes/${target.id}` : threadPath(requireIssueOrPull(target, context, "react to"));
1233
+ }
1234
+ /** The authenticated username, read once; removing an award needs its id, which is found by owner. */
1235
+ const viewerLogin = memo(() => fetcher.json("/user").then((result) => result.data.username, () => void 0));
1236
+ const userIdByName = memoBy((username) => fetcher.json("/users", { query: { username } }).then(({ data }) => {
1237
+ if (!data[0]) throw new UnsupportedOperationError(`No GitLab user named ${username}`, context);
1238
+ return data[0].id;
1239
+ }));
1240
+ function userId(actor) {
1241
+ if (typeof actor !== "string" && /^\d+$/.test(actor.id)) return Promise.resolve(Number(actor.id));
1242
+ return userIdByName(typeof actor === "string" ? actor : actor.login);
1243
+ }
1244
+ const STATUS_STATES = {
1245
+ pending: "pending",
1246
+ success: "success",
1247
+ failure: "failed",
1248
+ neutral: "success",
1249
+ unknown: "pending"
1250
+ };
1251
+ const PIPELINE_STATES = {
1252
+ pending: "running",
1253
+ success: "success",
1254
+ failure: "failed",
1255
+ neutral: "manual",
1256
+ unknown: void 0
1257
+ };
1258
+ async function setDiscussionResolved(thread, id, resolved, options) {
1259
+ const ref = requireThread(thread, context);
1260
+ await fetcher.raw(`${threadPath(ref)}/discussions/${encodeURIComponent(id)}`, {
1261
+ method: "PUT",
1262
+ query: { resolved },
1263
+ signal: options?.signal
1264
+ });
1265
+ }
1266
+ const list = createListing(fetcher, "per_page");
1267
+ function milestonesPage(repo, listOptions = {}, includeAncestors) {
1268
+ return list(`${projectPath(repo)}/milestones`, listOptions, (raw) => toMilestone(raw), { query: {
1269
+ state: (listOptions.state ?? "open") === "all" ? void 0 : listOptions.state === "closed" ? "closed" : "active",
1270
+ include_ancestors: includeAncestors
1271
+ } });
1272
+ }
1273
+ async function readUser(path, signal) {
1274
+ const { data } = await fetcher.json(path, { signal });
1275
+ return {
1276
+ ...toActor(instance, data),
1277
+ bio: data.bio || void 0,
1278
+ company: data.organization || void 0,
1279
+ location: data.location || void 0,
1280
+ websiteUrl: data.website_url || void 0,
1281
+ createdAt: toDate(data.created_at),
1282
+ followers: data.followers,
1283
+ following: data.following,
1284
+ raw: data
1285
+ };
1286
+ }
1287
+ function projectPath(repo) {
1288
+ return `/projects/${projectId(repo)}`;
1289
+ }
1290
+ /** Project hooks, or group hooks for a namespace ref. */
1291
+ function hooksPath(target) {
1292
+ return isNamespaceRef(target) ? `/groups/${encodeURIComponent(target.externalId ?? target.owner)}/hooks` : `${projectPath(target)}/hooks`;
1293
+ }
1294
+ /** GitLab subscribes with one boolean per event group rather than a list of names. */
1295
+ function hookFlags(input) {
1296
+ return Object.fromEntries(nativeEventsFor(GITLAB_NATIVE_EVENTS, input.events, input.nativeEvents).map((flag) => [flag, true]));
1297
+ }
1298
+ function requireMerge(thread, action) {
1299
+ const ref = requireThread(thread, context);
1300
+ if (ref.kind !== "pull_request") throw new UnsupportedOperationError(`Only merge requests can be ${action}`, context);
1301
+ return ref;
1302
+ }
1303
+ /** GitLab's file endpoint needs an explicit ref, so the project's default branch stands in. */
1304
+ async function defaultBranch(repo, options) {
1305
+ const { data } = await fetcher.json(projectPath(repo), { signal: options?.signal });
1306
+ return data.default_branch ?? "HEAD";
1307
+ }
1308
+ async function get(thread, options) {
1309
+ const ref = requireThread(thread, context);
1310
+ if (ref.kind === "discussion") throw new UnsupportedOperationError("GitLab has no discussion threads", context);
1311
+ if (ref.kind === "commit") return toCommitThread(ref, (await fetcher.json(threadPath(ref), { signal: options?.signal })).data);
1312
+ const { data } = await fetcher.json(threadPath(ref), { signal: options?.signal });
1313
+ const result = toThread(ref, data);
1314
+ if (data.head_pipeline) result.checks = toPipelineSummary(data.head_pipeline);
1315
+ return result;
1316
+ }
1317
+ const graphqlUrl = `${baseUrl.replace(/\/api\/v4$/, "")}/api/graphql`;
1318
+ async function vulnerabilitiesPage(repo, listOptions = {}) {
1319
+ const state = listOptions.state ?? "open";
1320
+ const { data } = await fetcher.json(graphqlUrl, {
1321
+ method: "POST",
1322
+ json: {
1323
+ operationName: "ProjectVulnerabilities",
1324
+ query: VULNERABILITIES_QUERY,
1325
+ variables: {
1326
+ fullPath: `${repo.owner}/${repo.name}`,
1327
+ first: Math.min(listOptions.perPage ?? 50, 100),
1328
+ after: listOptions.cursor?.token ?? null,
1329
+ state: state === "open" ? ["DETECTED", "CONFIRMED"] : state === "closed" ? ["RESOLVED", "DISMISSED"] : null,
1330
+ reportType: listOptions.kind ? REPORT_TYPES[listOptions.kind] : null
1331
+ }
1332
+ },
1333
+ signal: listOptions.signal
1334
+ });
1335
+ const vulnerabilities = data.data?.project?.vulnerabilities;
1336
+ if (!vulnerabilities) {
1337
+ const error = new InsufficientScopeError(data.errors?.[0]?.message ?? "The token cannot read vulnerabilities for this project, or its licence has no vulnerability report", 403, "", context);
1338
+ if (listOptions.kind) throw error;
1339
+ return {
1340
+ items: [],
1341
+ warnings: [toWarning("insufficient_scope", error, `${repo.owner}/${repo.name}`)]
1342
+ };
1343
+ }
1344
+ return {
1345
+ items: vulnerabilities.nodes.map((node) => toVulnerability(repo, node)),
1346
+ cursor: vulnerabilities.pageInfo.hasNextPage && vulnerabilities.pageInfo.endCursor ? { token: vulnerabilities.pageInfo.endCursor } : void 0
1347
+ };
1348
+ }
1349
+ async function releasesPage(repo, listOptions = {}) {
1350
+ return list(`${projectPath(repo)}/releases`, listOptions, (raw) => toRelease(repo, raw));
1351
+ }
1352
+ function requirePublishedRelease(input) {
1353
+ if (input.draft) throw new UnsupportedOperationError("GitLab has no draft releases", context);
1354
+ if (input.prerelease) throw new UnsupportedOperationError("GitLab has no prereleases", context);
1355
+ }
1356
+ async function listPage(repo, query = {}) {
1357
+ if (query.kind === "discussion") return {
1358
+ items: [],
1359
+ warnings: [{
1360
+ code: "kind_unsupported",
1361
+ message: "GitLab has no discussion threads"
1362
+ }]
1363
+ };
1364
+ const warnings = !query.cursor && query.sort === "comments" ? [{
1365
+ code: "sort_unsupported",
1366
+ message: "GitLab cannot sort by comment count; sorted by creation time"
1367
+ }] : [];
1368
+ const state = query.state ?? "open";
1369
+ const kinds = query.kind ? [query.kind] : ["issue", "pull_request"];
1370
+ const page = await phased(kinds.map((kind) => async (cursor) => {
1371
+ const merges = kind === "pull_request";
1372
+ const result = await fetcher.page(`${projectPath(repo)}/${merges ? "merge_requests" : "issues"}`, {
1373
+ query: {
1374
+ state: state === "open" ? "opened" : state === "merged" ? "merged" : state === "closed" && !merges ? "closed" : void 0,
1375
+ labels: query.labels?.join(","),
1376
+ author_username: query.author,
1377
+ assignee_username: query.assignee,
1378
+ updated_after: query.since?.toISOString(),
1379
+ created_after: query.createdAfter?.toISOString(),
1380
+ order_by: query.sort === "updated" ? "updated_at" : "created_at",
1381
+ sort: query.direction ?? "desc",
1382
+ per_page: query.perPage
1383
+ },
1384
+ cursor,
1385
+ signal: query.signal
1386
+ });
1387
+ return toPage(result, (raw) => merges && state === "closed" && raw.state === "opened" ? void 0 : toThread({
1388
+ forge: FORGE,
1389
+ instance,
1390
+ repo,
1391
+ kind,
1392
+ number: String(raw.iid)
1393
+ }, raw));
1394
+ }), query.cursor);
1395
+ return warnings.length ? {
1396
+ ...page,
1397
+ warnings
1398
+ } : page;
1399
+ }
1400
+ /** Cross-project search runs on the global issue and merge request listings, which take `search` and `in`. */
1401
+ async function searchThreadsPage(query) {
1402
+ const warnings = [];
1403
+ if (!query.cursor && query.involves) warnings.push({
1404
+ code: "filter_unsupported",
1405
+ message: "GitLab has no involves filter; it was ignored"
1406
+ });
1407
+ if (!query.cursor && query.sort === "comments") warnings.push({
1408
+ code: "sort_unsupported",
1409
+ message: "GitLab cannot sort by comment count; sorted by update time"
1410
+ });
1411
+ const state = query.state ?? "all";
1412
+ const kinds = query.kind ? [query.kind] : ["issue", "pull_request"];
1413
+ const page = await phased(kinds.map((kind) => async (cursor) => {
1414
+ const merges = kind === "pull_request";
1415
+ const base = query.repo ? `${projectPath(query.repo)}/` : "/";
1416
+ const result = await fetcher.page(`${base}${merges ? "merge_requests" : "issues"}`, {
1417
+ query: {
1418
+ scope: query.repo ? void 0 : "all",
1419
+ search: query.text,
1420
+ in: query.text ? "title,description" : void 0,
1421
+ state: state === "open" ? "opened" : state === "closed" ? "closed" : void 0,
1422
+ labels: query.labels?.join(","),
1423
+ author_username: query.author,
1424
+ assignee_username: query.assignee,
1425
+ updated_after: query.since?.toISOString(),
1426
+ order_by: query.sort === "created" ? "created_at" : "updated_at",
1427
+ sort: query.direction ?? "desc",
1428
+ per_page: query.perPage
1429
+ },
1430
+ cursor,
1431
+ signal: query.signal
1432
+ });
1433
+ return toPage(result, (raw) => {
1434
+ const repo = query.repo ?? repoRefFromWebUrl(instance, raw.web_url, merges ? raw.target_project_id : void 0);
1435
+ return repo ? toThread({
1436
+ forge: FORGE,
1437
+ instance,
1438
+ repo,
1439
+ kind,
1440
+ number: String(raw.iid)
1441
+ }, raw) : void 0;
1442
+ });
1443
+ }), query.cursor);
1444
+ return warnings.length ? {
1445
+ ...page,
1446
+ warnings: [...warnings, ...page.warnings ?? []]
1447
+ } : page;
1448
+ }
1449
+ async function searchReposPage(query) {
1450
+ const path = query.owner ? `/groups/${encodeURIComponent(query.owner)}/projects` : "/projects";
1451
+ const result = await fetcher.page(path, {
1452
+ query: {
1453
+ search: query.text,
1454
+ order_by: query.sort === "created" ? "created_at" : query.sort === "stars" ? "star_count" : query.sort === "updated" ? "updated_at" : void 0,
1455
+ sort: query.direction,
1456
+ per_page: query.perPage
1457
+ },
1458
+ cursor: query.cursor,
1459
+ signal: query.signal
1460
+ });
1461
+ return toPage(result, (raw) => toRepo(instance, raw));
1462
+ }
1463
+ /** Needs Advanced Search, which no endpoint reports; an instance without it returns an empty page. */
1464
+ async function searchCommitsPage(query) {
1465
+ const unsupported = [
1466
+ "author",
1467
+ "committer",
1468
+ "since",
1469
+ "until",
1470
+ "sort"
1471
+ ].filter((field) => query[field] !== void 0);
1472
+ const warnings = unsupported.length ? [{
1473
+ code: "filter_unsupported",
1474
+ message: `GitLab commit search cannot filter or sort by ${unsupported.join(", ")}`
1475
+ }] : [];
1476
+ const result = await fetcher.page(query.repo ? `${projectPath(query.repo)}/search` : "/search", {
1477
+ query: {
1478
+ scope: "commits",
1479
+ search: query.text ?? "",
1480
+ per_page: query.perPage
1481
+ },
1482
+ cursor: query.cursor,
1483
+ signal: query.signal
1484
+ });
1485
+ const repo = query.repo;
1486
+ return toPage(result, (raw) => repo && toCommit(repo, raw), warnings);
1487
+ }
1488
+ /** Projects that group and instance code searches named by id, kept so later pages skip the lookup. */
1489
+ const projectRefs = /* @__PURE__ */ new Map();
1490
+ /** A project the token cannot read costs its blobs and a warning, not the page. */
1491
+ async function resolveProjects(ids, signal, warnings) {
1492
+ if (!ids.length) return;
1493
+ try {
1494
+ const { data } = await fetcher.json(graphqlUrl, {
1495
+ method: "POST",
1496
+ json: {
1497
+ operationName: "CodeSearchProjects",
1498
+ query: PROJECTS_QUERY,
1499
+ variables: { ids: ids.map((id) => `gid://gitlab/Project/${id}`) }
1500
+ },
1501
+ signal
1502
+ });
1503
+ for (const node of data.data?.projects?.nodes ?? []) {
1504
+ const id = Number(node.id.slice(node.id.lastIndexOf("/") + 1));
1505
+ projectRefs.set(id, toRepoRef(instance, node.fullPath, id));
1506
+ }
1507
+ } catch (error) {
1508
+ if (!degradesToWarning(error)) throw error;
1509
+ warnings.push(...ids.map((id) => toWarning("record_unreachable", error, `project ${id}`)));
1510
+ return;
1511
+ }
1512
+ for (const id of ids.filter((id) => !projectRefs.has(id))) warnings.push({
1513
+ code: "record_unreachable",
1514
+ message: "GitLab returned no project for this id",
1515
+ subject: `project ${id}`
1516
+ });
1517
+ }
1518
+ /** Any tier searches a project; a group or the instance needs Advanced Search or exact code search. */
1519
+ async function searchCodePage(query) {
1520
+ const path = query.repo ? `${projectPath(query.repo)}/search` : query.owner ? `/groups/${encodeURIComponent(query.owner)}/search` : "/search";
1521
+ const result = await fetcher.page(path, {
1522
+ query: {
1523
+ scope: "blobs",
1524
+ search: query.text ?? "",
1525
+ per_page: query.perPage
1526
+ },
1527
+ cursor: query.cursor,
1528
+ signal: query.signal
1529
+ });
1530
+ const warnings = [];
1531
+ await resolveProjects(query.repo ? [] : [...new Set((result.data ?? []).map((raw) => raw.project_id))].filter((id) => !projectRefs.has(id)), query.signal, warnings);
1532
+ /** GitLab returns one blob per excerpt; basic search lists a file again, with the same first excerpt, when its name matches too. */
1533
+ const files = /* @__PURE__ */ new Map();
1534
+ for (const blob of result.data ?? []) {
1535
+ const key = `${blob.project_id}:${blob.ref}:${blob.path}`;
1536
+ const excerpts = files.get(key);
1537
+ if (!excerpts) files.set(key, [blob]);
1538
+ else if (!excerpts.some((seen) => seen.startline === blob.startline && seen.data === blob.data)) excerpts.push(blob);
1539
+ }
1540
+ return toPage({
1541
+ ...result,
1542
+ data: [...files.values()]
1543
+ }, (excerpts) => {
1544
+ const [first] = excerpts;
1545
+ const repo = query.repo ?? projectRefs.get(first.project_id);
1546
+ if (!repo) return;
1547
+ const url = webUrlFor(web, { file: {
1548
+ repo,
1549
+ path: first.path,
1550
+ at: first.ref,
1551
+ line: first.startline
1552
+ } });
1553
+ return {
1554
+ repo,
1555
+ path: first.path,
1556
+ ref: first.ref,
1557
+ fragments: excerpts.map((raw) => ({
1558
+ text: raw.data,
1559
+ line: raw.startline
1560
+ })),
1561
+ url,
1562
+ raw: excerpts
1563
+ };
1564
+ }, warnings);
1565
+ }
1566
+ async function setSubscribed(thread, subscribed, options) {
1567
+ const ref = requireIssueOrPull(thread, context, subscribed ? "subscribe to" : "unsubscribe from");
1568
+ await fetcher.raw(`${threadPath(ref)}/${subscribed ? "subscribe" : "unsubscribe"}`, {
1569
+ method: "POST",
1570
+ signal: options?.signal
1571
+ });
1572
+ }
1573
+ async function unsubscribe(_ref, writeOptions = {}) {
1574
+ if (!writeOptions.thread) throw new UnresolvedThreadError("GitLab cannot look up a to-do by id; pass { thread } to unsubscribe", context);
1575
+ const thread = requireThread(writeOptions.thread, context);
1576
+ if (thread.kind !== "issue" && thread.kind !== "pull_request") throw new UnsupportedOperationError(`GitLab cannot unsubscribe from a ${thread.kind}`, context);
1577
+ await fetcher.raw(`${threadPath(thread)}/unsubscribe`, {
1578
+ method: "POST",
1579
+ signal: writeOptions.signal
1580
+ });
1581
+ }
1582
+ async function setState(thread, stateEvent, options) {
1583
+ await fetcher.raw(threadPath(requireIssueOrPull(thread, context, stateEvent)), {
1584
+ method: "PUT",
1585
+ json: { state_event: stateEvent },
1586
+ signal: options?.signal
1587
+ });
1588
+ }
1589
+ async function resolveMergeMethod(ref, requested, signal) {
1590
+ if (requested === "squash") return requested;
1591
+ const { data: project } = await fetcher.json(`/projects/${projectId(ref.repo)}`, { signal });
1592
+ const base = BASE_METHODS[project.merge_method ?? "merge"];
1593
+ const squash = project.squash_option ?? "default_off";
1594
+ if (requested) {
1595
+ if (requested !== base || squash === "always") throw new UnsupportedOperationError(`This project merges with ${squash === "always" ? "squash" : base}; GitLab cannot override it per merge request`, context);
1596
+ return requested;
1597
+ }
1598
+ return soleMergeMethod({
1599
+ [base]: squash !== "always",
1600
+ squash: squash !== "never"
1601
+ }, context);
1602
+ }
1603
+ /** GitLab has approvals, not reviews: one synthesised review per approver, with no comments to hang on it. */
1604
+ function toApprovalReviews(ref, approvals) {
1605
+ return (approvals.approved_by ?? []).map(({ user }) => syntheticReview(ref, `approval:${user?.username ?? user?.id ?? ""}`, "approved", {
1606
+ author: toActor(instance, user),
1607
+ stateRaw: "approved",
1608
+ raw: user
1609
+ }));
1610
+ }
1611
+ async function approve(thread, body, options) {
1612
+ const ref = requireThread(thread, context);
1613
+ if (ref.kind !== "pull_request") throw new UnsupportedOperationError("Only merge requests can be approved", context);
1614
+ if (body) throw new UnsupportedOperationError("GitLab approvals carry no body; leave a comment instead", context);
1615
+ const { data } = await fetcher.json(`${threadPath(ref)}/approve`, {
1616
+ method: "POST",
1617
+ signal: options?.signal,
1618
+ mapError: (error) => error instanceof TokenRevokedError ? new InsufficientScopeError("Not allowed to approve this merge request", 401, error.body, {
1619
+ ...context,
1620
+ url: error.url,
1621
+ method: error.method
1622
+ }, { cause: error }) : error
1623
+ });
1624
+ return data;
1625
+ }
1626
+ async function merge(thread, mergeOptions = {}, hooks = {}) {
1627
+ const ref = requireThread(thread, context);
1628
+ if (ref.kind !== "pull_request") throw new UnsupportedOperationError("Only merge requests can be merged", context);
1629
+ const method = await resolveMergeMethod(ref, mergeOptions.method, mergeOptions.signal);
1630
+ const message = mergeCommitMessage(mergeOptions);
1631
+ await hooks.beforeMerge?.();
1632
+ await fetcher.raw(`${threadPath(ref)}/merge`, {
1633
+ method: "PUT",
1634
+ json: {
1635
+ sha: mergeOptions.sha,
1636
+ ...message ? { [method === "squash" ? "squash_commit_message" : "merge_commit_message"]: message } : {},
1637
+ squash: method === "squash",
1638
+ merge_when_pipeline_succeeds: mergeOptions.whenChecksPass ?? false
1639
+ },
1640
+ mapError: toMergeError,
1641
+ signal: mergeOptions.signal
1642
+ });
1643
+ }
1644
+ return {
1645
+ traits: {
1646
+ eventKinds: "heuristic",
1647
+ authKinds: ["token", "anonymous"],
1648
+ limits: {
1649
+ bodyLength: 1048576,
1650
+ commentLength: 1e6,
1651
+ labelLength: 255
1652
+ }
1653
+ },
1654
+ probeVersion: async () => (await fetcher.json("/version")).data.version,
1655
+ web,
1656
+ webhooks: {
1657
+ listPage: verb(true, (target, listOptions = {}) => list(hooksPath(target), listOptions, (raw) => toWebhook(target, raw))),
1658
+ create: verb(true, async (target, input, options) => toWebhook(target, (await fetcher.json(hooksPath(target), {
1659
+ method: "POST",
1660
+ json: {
1661
+ url: input.url,
1662
+ token: input.secret,
1663
+ ...hookFlags(input)
1664
+ },
1665
+ signal: options?.signal
1666
+ })).data)),
1667
+ update: verb(true, async (ref, update, options) => toWebhook(ref.target, (await fetcher.json(`${hooksPath(ref.target)}/${ref.id}`, {
1668
+ method: "PUT",
1669
+ json: {
1670
+ ...update.url ? { url: update.url } : {},
1671
+ ...update.secret ? { token: update.secret } : {},
1672
+ ...hookFlags(update)
1673
+ },
1674
+ signal: options?.signal
1675
+ })).data)),
1676
+ delete: verb(true, async (ref, options) => {
1677
+ await fetcher.raw(`${hooksPath(ref.target)}/${ref.id}`, {
1678
+ method: "DELETE",
1679
+ signal: options?.signal
1680
+ });
1681
+ }),
1682
+ rotateSecret: verb(true, async (ref, secret, options) => toWebhook(ref.target, (await fetcher.json(`${hooksPath(ref.target)}/${ref.id}`, {
1683
+ method: "PUT",
1684
+ json: { token: secret },
1685
+ signal: options?.signal
1686
+ })).data)),
1687
+ deliveriesPage: verb(({ version }) => versionAtLeast(version, GITLAB_HOOK_EVENTS), (ref, listOptions = {}) => list(`${hooksPath(ref.target)}/${ref.id}/events`, listOptions, (raw) => toWebhookDelivery(ref, raw))),
1688
+ redeliver: verb(({ version }) => versionAtLeast(version, GITLAB_HOOK_EVENTS), async (ref, options) => {
1689
+ await fetcher.raw(`${hooksPath(ref.hook.target)}/${ref.hook.id}/events/${ref.id}/resend`, {
1690
+ method: "POST",
1691
+ signal: options?.signal
1692
+ });
1693
+ })
1694
+ },
1695
+ scopes: gitlabScopesFor,
1696
+ users: {
1697
+ get: verb(true, async (login, options) => {
1698
+ const { data: matches } = await fetcher.json("/users", {
1699
+ query: { username: login },
1700
+ signal: options?.signal
1701
+ });
1702
+ const [match] = matches ?? [];
1703
+ if (!match) throw new NotFoundError(`No GitLab user named ${login}`, 404, "", context);
1704
+ try {
1705
+ return await readUser(`/users/${match.id}`, options?.signal);
1706
+ } catch (error) {
1707
+ if (error instanceof InsufficientScopeError || error instanceof AuthenticationRequiredError) return {
1708
+ ...toActor(instance, match),
1709
+ raw: match
1710
+ };
1711
+ throw error;
1712
+ }
1713
+ }),
1714
+ me: verb(true, (options) => readUser("/user", options?.signal))
1715
+ },
1716
+ repos: {
1717
+ get: verb(true, async (ref, options) => {
1718
+ const query = options?.licence ? { license: true } : void 0;
1719
+ return toRepo(instance, (await fetcher.json(projectPath(ref), {
1720
+ query,
1721
+ signal: options?.signal
1722
+ })).data);
1723
+ }),
1724
+ listPage: verb(true, (listOptions = {}) => list("/projects", listOptions, (raw) => toRepo(instance, raw), { query: { membership: true } })),
1725
+ labelsPage: verb(!anonymous, (repo, listOptions = {}) => list(`${projectPath(repo)}/labels`, listOptions, toLabel)),
1726
+ createLabel: verb(true, async (repo, label, options) => toLabel((await fetcher.json(`${projectPath(repo)}/labels`, {
1727
+ method: "POST",
1728
+ json: {
1729
+ name: label.name,
1730
+ color: hexColour(label.colour, "#"),
1731
+ description: label.description
1732
+ },
1733
+ signal: options?.signal
1734
+ })).data)),
1735
+ milestonesPage: verb(!anonymous, milestonesPage),
1736
+ collaboratorsPage: verb(!anonymous, (repo, listOptions = {}) => list(`${projectPath(repo)}/members/all`, listOptions, (raw) => ({
1737
+ actor: toActor(instance, raw),
1738
+ role: toRole(raw.access_level),
1739
+ roleRaw: String(raw.access_level),
1740
+ raw
1741
+ }))),
1742
+ permissionFor: verb(!anonymous, async (repo, actor, options) => {
1743
+ try {
1744
+ const { data } = await fetcher.json(`${projectPath(repo)}/members/all/${await userId(actor)}`, { signal: options?.signal });
1745
+ return toRole(data.access_level);
1746
+ } catch (error) {
1747
+ if (error instanceof NotFoundError) return "none";
1748
+ throw error;
1749
+ }
1750
+ }),
1751
+ addCollaborator: verb(true, async (repo, actor, role, options) => {
1752
+ await fetcher.raw(`${projectPath(repo)}/members`, {
1753
+ method: "POST",
1754
+ json: {
1755
+ user_id: await userId(actor),
1756
+ access_level: ACCESS_LEVELS[role]
1757
+ },
1758
+ signal: options?.signal
1759
+ });
1760
+ }),
1761
+ assignableUsersPage: verb(true, (repo, listOptions = {}) => list(`${projectPath(repo)}/users`, listOptions, (raw) => toActor(instance, raw))),
1762
+ reviewerCandidatesPage: verb("emulated", (thread, listOptions = {}) => list(`${projectPath(requireThread(thread, context).repo)}/users`, listOptions, (raw) => toActor(instance, raw)))
1763
+ },
1764
+ notifications: {
1765
+ listPage: verb(true, notificationPage),
1766
+ markDone: verb(true, async (ref, options) => {
1767
+ await fetcher.raw(`/todos/${ref.id}/mark_as_done`, {
1768
+ method: "POST",
1769
+ signal: options?.signal
1770
+ });
1771
+ }),
1772
+ unsubscribe: verb(true, unsubscribe),
1773
+ markAllDone: verb(true, async (bulk = {}) => {
1774
+ if (bulk.repo || bulk.before) throw new UnsupportedOperationError("GitLab marks every to-do done at once; repo and before filters are not supported", context);
1775
+ await fetcher.raw("/todos/mark_as_done", {
1776
+ method: "POST",
1777
+ signal: bulk.signal
1778
+ });
1779
+ }),
1780
+ unreadCount: verb(true, async (options) => {
1781
+ const total = (await fetcher.raw("/todos", {
1782
+ query: {
1783
+ state: "pending",
1784
+ per_page: 1
1785
+ },
1786
+ signal: options?.signal
1787
+ })).headers.get("x-total");
1788
+ if (total === null) throw new UnsupportedOperationError("GitLab omitted X-Total; the to-do count is too large to report", context);
1789
+ return Number(total);
1790
+ })
1791
+ },
1792
+ contents: {
1793
+ file: verb(true, async (repo, path, fileOptions = {}) => {
1794
+ const ref = fileOptions.ref ?? await defaultBranch(repo, fileOptions);
1795
+ const { data } = await fetcher.json(`${projectPath(repo)}/repository/files/${encodeURIComponent(path.replace(/^\//, ""))}`, {
1796
+ query: { ref },
1797
+ signal: fileOptions.signal
1798
+ });
1799
+ const file = {
1800
+ path: data.file_path,
1801
+ sha: data.blob_id,
1802
+ size: data.size
1803
+ };
1804
+ return toFileContent(fromBase64(data.content ?? ""), file, fileOptions, context);
1805
+ }),
1806
+ treePage: verb(true, (repo, treeOptions = {}) => list(`${projectPath(repo)}/repository/tree`, treeOptions, toTreeEntry, { query: {
1807
+ ref: treeOptions.ref,
1808
+ path: treeOptions.path,
1809
+ recursive: treeOptions.recursive ? "true" : void 0
1810
+ } })),
1811
+ branchesPage: verb(true, (repo, listOptions = {}) => list(`${projectPath(repo)}/repository/branches`, listOptions, toBranch)),
1812
+ tagsPage: verb(true, (repo, listOptions = {}) => list(`${projectPath(repo)}/repository/tags`, listOptions, toTag)),
1813
+ resolveRef: verb(true, async (repo, ref, options) => (await fetcher.json(`${projectPath(repo)}/repository/commits/${encodeURIComponent(ref)}`, { signal: options?.signal })).data.id),
1814
+ commitsPage: verb(true, (repo, query = {}) => list(`${projectPath(repo)}/repository/commits`, query, (raw) => toCommit(repo, raw), { query: {
1815
+ ref_name: query.ref,
1816
+ path: query.path,
1817
+ author: query.author,
1818
+ since: query.since?.toISOString(),
1819
+ until: query.until?.toISOString()
1820
+ } })),
1821
+ commit: verb(true, async (repo, sha, options) => {
1822
+ const [commit, diffs] = await Promise.all([fetcher.json(`${projectPath(repo)}/repository/commits/${sha}`, {
1823
+ query: { stats: "true" },
1824
+ signal: options?.signal
1825
+ }), fetcher.json(`${projectPath(repo)}/repository/commits/${sha}/diff`, { signal: options?.signal })]);
1826
+ return toCommit(repo, commit.data, (diffs.data ?? []).map(toChangedFile));
1827
+ }),
1828
+ compare: verb(true, async (repo, base, head, options) => {
1829
+ const { data } = await fetcher.json(`${projectPath(repo)}/repository/compare`, {
1830
+ query: {
1831
+ from: base,
1832
+ to: head
1833
+ },
1834
+ signal: options?.signal
1835
+ });
1836
+ return {
1837
+ base,
1838
+ head,
1839
+ aheadBy: data.commits?.length,
1840
+ commits: (data.commits ?? []).map((raw) => toCommit(repo, raw)),
1841
+ files: (data.diffs ?? []).map(toChangedFile),
1842
+ raw: data
1843
+ };
1844
+ })
1845
+ },
1846
+ releases: {
1847
+ listPage: verb(true, releasesPage),
1848
+ get: verb(true, async (ref, options) => toRelease(ref.repo, (await fetcher.json(`${projectPath(ref.repo)}/releases/${encodeURIComponent(ref.tag ?? ref.id)}`, { signal: options?.signal })).data)),
1849
+ getByTag: verb(true, async (repo, tag, options) => toRelease(repo, (await fetcher.json(`${projectPath(repo)}/releases/${encodeURIComponent(tag)}`, { signal: options?.signal })).data)),
1850
+ latest: verb(true, async (repo, options) => {
1851
+ const { items } = await releasesPage(repo, {
1852
+ perPage: 20,
1853
+ signal: options?.signal
1854
+ });
1855
+ return items.find((release) => release.publishedAt);
1856
+ }),
1857
+ create: verb(true, async (repo, input, options) => {
1858
+ requirePublishedRelease(input);
1859
+ const { data } = await fetcher.json(`${projectPath(repo)}/releases`, {
1860
+ method: "POST",
1861
+ json: {
1862
+ tag_name: input.tag,
1863
+ ref: input.target,
1864
+ name: input.name,
1865
+ description: input.body
1866
+ },
1867
+ signal: options?.signal
1868
+ });
1869
+ return toRelease(repo, data);
1870
+ }),
1871
+ update: verb(true, async (ref, update, options) => {
1872
+ requirePublishedRelease(update);
1873
+ const tag = ref.tag ?? ref.id;
1874
+ if (update.tag !== void 0 && update.tag !== tag) throw new UnsupportedOperationError("GitLab keys a release by its tag, so the tag cannot change", context);
1875
+ if (update.target !== void 0) throw new UnsupportedOperationError("GitLab cannot change the commit a release points to", context);
1876
+ const { data } = await fetcher.json(`${projectPath(ref.repo)}/releases/${encodeURIComponent(tag)}`, {
1877
+ method: "PUT",
1878
+ json: {
1879
+ name: update.name,
1880
+ description: update.body
1881
+ },
1882
+ signal: options?.signal
1883
+ });
1884
+ return toRelease(ref.repo, data);
1885
+ })
1886
+ },
1887
+ search: {
1888
+ threadsPage: verb(true, searchThreadsPage),
1889
+ reposPage: verb(true, searchReposPage),
1890
+ commitsPage: verb(!anonymous && "experimental", searchCommitsPage),
1891
+ codePage: verb(!anonymous, searchCodePage)
1892
+ },
1893
+ checks: {
1894
+ list: verb(!anonymous, async (repo, sha, options) => ({ items: await Array.fromAsync(fetcher.items(`${projectPath(repo)}/repository/commits/${sha}/statuses`, {
1895
+ query: { per_page: 100 },
1896
+ signal: options?.signal
1897
+ }), (raw) => toStatusCheck(repo, raw)) })),
1898
+ report: verb(true, async (repo, sha, input, options) => toStatusCheck(repo, (await fetcher.json(`${projectPath(repo)}/statuses/${sha}`, {
1899
+ method: "POST",
1900
+ json: {
1901
+ state: STATUS_STATES[input.state],
1902
+ name: input.name,
1903
+ description: input.description,
1904
+ target_url: input.url
1905
+ },
1906
+ signal: options?.signal
1907
+ })).data)),
1908
+ rerun: verb(true, async (ref, options) => {
1909
+ if (ref.type !== "job" && ref.type !== "status") throw new UnsupportedOperationError(`GitLab cannot re-run a ${ref.type}`, context);
1910
+ await fetcher.raw(`${projectPath(ref.repo)}/jobs/${ref.id}/retry`, {
1911
+ method: "POST",
1912
+ signal: options?.signal
1913
+ });
1914
+ })
1915
+ },
1916
+ ci: {
1917
+ runsPage: verb(true, (repo, query = {}) => list(`${projectPath(repo)}/pipelines`, query, (raw) => toPipeline(repo, raw), { query: {
1918
+ ref: query.branch,
1919
+ status: PIPELINE_STATES[query.state ?? "unknown"]
1920
+ } })),
1921
+ run: verb(true, async (ref, options) => toPipeline(ref.repo, (await fetcher.json(`${projectPath(ref.repo)}/pipelines/${ref.id}`, { signal: options?.signal })).data)),
1922
+ jobsPage: verb(true, (ref, listOptions = {}) => list(`${projectPath(ref.repo)}/pipelines/${ref.id}/jobs`, listOptions, (raw) => toCiJob(ref, raw))),
1923
+ log: verb(!anonymous, async (ref, options) => (await fetcher.stream(`${projectPath(ref.repo)}/jobs/${encodeURIComponent(ref.id)}/trace`, { signal: options?.signal })).body)
1924
+ },
1925
+ securityAlerts: {
1926
+ kinds: {
1927
+ dependency: !anonymous,
1928
+ code_scanning: !anonymous,
1929
+ secret: !anonymous
1930
+ },
1931
+ listPage: vulnerabilitiesPage
1932
+ },
1933
+ threads: {
1934
+ filesPage: verb(true, (thread, listOptions = {}) => list(`${threadPath(requireMerge(thread, "read for changed files"))}/diffs`, listOptions, toChangedFile)),
1935
+ commitsPage: verb(true, (thread, listOptions = {}) => list(`${threadPath(requireMerge(thread, "read for commits"))}/commits`, listOptions, (raw) => toCommit(thread.repo, raw))),
1936
+ reviewsPage: verb("emulated", async (thread, listOptions) => {
1937
+ const ref = requireThread(thread, context);
1938
+ if (ref.kind !== "pull_request") throw new UnsupportedOperationError("Only merge requests have approvals", context);
1939
+ const { data } = await fetcher.json(`${threadPath(ref)}/approvals`, { signal: listOptions?.signal });
1940
+ return { items: toApprovalReviews(ref, data) };
1941
+ }),
1942
+ approve: verb(true, async (thread, body, options) => {
1943
+ await approve(thread, body, options);
1944
+ }),
1945
+ createReview: verb("emulated", async (thread, input, options) => {
1946
+ if (input.comments?.length) throw new UnsupportedOperationError("GitLab reviews carry no inline comments yet", context);
1947
+ const ref = requireThread(thread, context);
1948
+ if (input.event === "comment" && input.body) {
1949
+ const { data } = await fetcher.json(`${threadPath(ref)}/discussions`, {
1950
+ method: "POST",
1951
+ json: { body: input.body },
1952
+ signal: options?.signal
1953
+ });
1954
+ const note = data.notes[0];
1955
+ return {
1956
+ ...syntheticReview(ref, `discussion:${data.id}`, "commented", {
1957
+ author: toActor(instance, note.author),
1958
+ stateRaw: "discussion",
1959
+ submittedAt: toDate(note.created_at),
1960
+ raw: data
1961
+ }),
1962
+ body: note.body,
1963
+ comments: [{
1964
+ ...toNoteComment(ref, note, data),
1965
+ raw: note
1966
+ }]
1967
+ };
1968
+ }
1969
+ if (input.event !== "approve") throw new UnsupportedOperationError("GitLab has approvals and discussions, not reviews; create `approve`, or `comment` with a body", context);
1970
+ const approvals = await approve(ref, input.body, options);
1971
+ const viewer = await viewerLogin();
1972
+ const reviews = toApprovalReviews(ref, approvals);
1973
+ return reviews.find((review) => review.author?.login === viewer) ?? reviews.at(-1) ?? {
1974
+ ref: {
1975
+ forge: "gitlab",
1976
+ instance,
1977
+ thread: ref,
1978
+ id: "approval:"
1979
+ },
1980
+ state: "approved",
1981
+ stateRaw: "approved",
1982
+ comments: false,
1983
+ raw: approvals
1984
+ };
1985
+ }),
1986
+ reviewThreads: verb(true, {
1987
+ resolveReviewThread: (thread, id, options) => setDiscussionResolved(thread, id, true, options),
1988
+ unresolveReviewThread: (thread, id, options) => setDiscussionResolved(thread, id, false, options)
1989
+ }),
1990
+ checks: perKind({ pull_request: true }, async (thread, options) => {
1991
+ const ref = requireThread(thread, context);
1992
+ const { data } = await fetcher.json(threadPath(ref), { signal: options?.signal });
1993
+ if (!data.head_pipeline) return { items: [] };
1994
+ const pipelineProject = data.head_pipeline.project_id;
1995
+ const project = pipelineProject === void 0 || pipelineProject === data.target_project_id ? projectPath(ref.repo) : `/projects/${pipelineProject}`;
1996
+ return { items: await Array.fromAsync(fetcher.items(`${project}/pipelines/${data.head_pipeline.id}/jobs`, {
1997
+ query: { per_page: 100 },
1998
+ signal: options?.signal
1999
+ }), (job) => toJobCheck(ref.repo, job)) };
2000
+ }),
2001
+ get: perKind({
2002
+ issue: true,
2003
+ pull_request: true,
2004
+ commit: true
2005
+ }, get),
2006
+ getMany: verb(true, (refs, options) => getManyConcurrently(refs, get, options)),
2007
+ listPage: perKind(ISSUE_LIKE, listPage),
2008
+ eventsPage: verb(!anonymous, async (thread, listOptions = {}) => {
2009
+ const ref = requireThread(thread, context);
2010
+ if (ref.kind === "commit") {
2011
+ const offset = Number(listOptions.cursor?.token ?? 0);
2012
+ const result = await fetcher.page(`${threadPath(ref)}/comments`, {
2013
+ query: { per_page: listOptions.perPage },
2014
+ cursor: listOptions.cursor,
2015
+ signal: listOptions.signal
2016
+ });
2017
+ let index = offset;
2018
+ const page = toPage(result, (comment) => toCommitCommentEvent(ref, comment, index++));
2019
+ return page.cursor ? {
2020
+ ...page,
2021
+ cursor: {
2022
+ ...page.cursor,
2023
+ token: String(index)
2024
+ }
2025
+ } : page;
2026
+ }
2027
+ return list(`${threadPath(ref)}/notes`, listOptions, (note) => toNoteEvent(ref, note), { query: {
2028
+ sort: "asc",
2029
+ order_by: "created_at"
2030
+ } });
2031
+ }),
2032
+ commentsPage: perKind({
2033
+ issue: !anonymous,
2034
+ pull_request: !anonymous,
2035
+ commit: true
2036
+ }, async (thread, listOptions = {}) => {
2037
+ const ref = requireThread(thread, context);
2038
+ if (ref.kind === "commit") return list(`${threadPath(ref)}/comments`, listOptions, (comment) => toCommitComment(ref, comment));
2039
+ const page = await list(`${threadPath(requireIssueOrPull(ref, context, "list comments on"))}/discussions`, listOptions, (discussion) => discussion);
2040
+ return {
2041
+ ...page,
2042
+ items: page.items.flatMap((discussion) => discussion.notes.filter((note) => !note.system).map((note) => toNoteComment(ref, note, discussion)))
2043
+ };
2044
+ }),
2045
+ comment: perKind({
2046
+ issue: true,
2047
+ pull_request: true,
2048
+ commit: true
2049
+ }, async (thread, body, options) => {
2050
+ const ref = requireThread(thread, context);
2051
+ if (ref.kind === "commit") {
2052
+ const { data } = await fetcher.json(`${threadPath(ref)}/comments`, {
2053
+ method: "POST",
2054
+ json: { note: body },
2055
+ signal: options?.signal
2056
+ });
2057
+ return toCommitComment(ref, data);
2058
+ }
2059
+ const { data } = await fetcher.json(`${threadPath(requireIssueOrPull(ref, context, "comment on"))}/notes`, {
2060
+ method: "POST",
2061
+ json: { body },
2062
+ signal: options?.signal
2063
+ });
2064
+ return toNoteComment(ref, data);
2065
+ }),
2066
+ editComment: perKind({
2067
+ issue: true,
2068
+ pull_request: true
2069
+ }, async (ref, body, options) => {
2070
+ const thread = requireIssueOrPull(ref.thread, context, "edit comments on");
2071
+ const { data } = await fetcher.json(`${threadPath(thread)}/notes/${ref.id}`, {
2072
+ method: "PUT",
2073
+ json: { body },
2074
+ signal: options?.signal
2075
+ });
2076
+ return toNoteComment(thread, data);
2077
+ }),
2078
+ deleteComment: perKind({
2079
+ issue: true,
2080
+ pull_request: true
2081
+ }, async (ref, options) => {
2082
+ const thread = requireIssueOrPull(ref.thread, context, "delete comments on");
2083
+ await fetcher.raw(`${threadPath(thread)}/notes/${ref.id}`, {
2084
+ method: "DELETE",
2085
+ signal: options?.signal
2086
+ });
2087
+ }),
2088
+ create: perKind({
2089
+ issue: true,
2090
+ pull_request: true
2091
+ }, async (repo, input, options) => {
2092
+ if (input.kind === "discussion") throw new UnsupportedOperationError("GitLab has no discussion threads", context);
2093
+ const assignee_ids = input.assignees?.length ? await Promise.all(input.assignees.map(userId)) : void 0;
2094
+ const labels = input.labels?.join(",");
2095
+ if (input.kind === "issue") {
2096
+ const { data } = await fetcher.json(`${projectPath(repo)}/issues`, {
2097
+ method: "POST",
2098
+ json: {
2099
+ title: input.title,
2100
+ description: input.body,
2101
+ labels,
2102
+ assignee_ids
2103
+ },
2104
+ signal: options?.signal
2105
+ });
2106
+ return toThread({
2107
+ forge: FORGE,
2108
+ instance,
2109
+ repo,
2110
+ kind: "issue",
2111
+ number: String(data.iid)
2112
+ }, data);
2113
+ }
2114
+ if (!input.head || !input.base) throw new UnsupportedOperationError("Creating a merge request needs head and base branches", context);
2115
+ const { data } = await fetcher.json(`${projectPath(repo)}/merge_requests`, {
2116
+ method: "POST",
2117
+ json: {
2118
+ title: input.draft ? `Draft: ${input.title}` : input.title,
2119
+ description: input.body,
2120
+ source_branch: input.head,
2121
+ target_branch: input.base,
2122
+ labels,
2123
+ assignee_ids
2124
+ },
2125
+ signal: options?.signal
2126
+ });
2127
+ return toThread({
2128
+ forge: FORGE,
2129
+ instance,
2130
+ repo,
2131
+ kind: "pull_request",
2132
+ number: String(data.iid)
2133
+ }, data);
2134
+ }),
2135
+ update: perKind({
2136
+ issue: true,
2137
+ pull_request: true
2138
+ }, async (thread, input, options) => {
2139
+ const ref = requireIssueOrPull(thread, context, "update");
2140
+ const { data } = await fetcher.json(threadPath(ref), {
2141
+ method: "PUT",
2142
+ json: {
2143
+ title: input.title,
2144
+ description: input.body
2145
+ },
2146
+ signal: options?.signal
2147
+ });
2148
+ return toThread(ref, data);
2149
+ }),
2150
+ addLabels: perKind({
2151
+ issue: true,
2152
+ pull_request: true
2153
+ }, async (thread, labels, options) => {
2154
+ const ref = requireIssueOrPull(thread, context, "label");
2155
+ await fetcher.raw(threadPath(ref), {
2156
+ method: "PUT",
2157
+ json: { add_labels: labels.join(",") },
2158
+ signal: options?.signal
2159
+ });
2160
+ }),
2161
+ removeLabels: perKind({
2162
+ issue: true,
2163
+ pull_request: true
2164
+ }, async (thread, labels, options) => {
2165
+ const ref = requireIssueOrPull(thread, context, "label");
2166
+ await fetcher.raw(threadPath(ref), {
2167
+ method: "PUT",
2168
+ json: { remove_labels: labels.join(",") },
2169
+ signal: options?.signal
2170
+ });
2171
+ }),
2172
+ setMilestone: perKind({
2173
+ issue: true,
2174
+ pull_request: true
2175
+ }, async (thread, milestone, options) => {
2176
+ const ref = requireIssueOrPull(thread, context, "set the milestone of");
2177
+ await fetcher.raw(threadPath(ref), {
2178
+ method: "PUT",
2179
+ json: { milestone_id: milestone === void 0 ? 0 : await milestoneId(milestone, (page) => milestonesPage(ref.repo, page, true), context, options) },
2180
+ signal: options?.signal
2181
+ });
2182
+ }),
2183
+ reactionsPage: perKind(ISSUE_LIKE, async (target, listOptions = {}) => {
2184
+ const result = await fetcher.page(`${awardPath(target)}/award_emoji`, {
2185
+ query: { per_page: listOptions.perPage },
2186
+ cursor: listOptions.cursor,
2187
+ signal: listOptions.signal
2188
+ });
2189
+ return toPage(result, (raw) => {
2190
+ const reactor = toActor(instance, raw.user);
2191
+ return reactor && {
2192
+ content: reactionContent(REACTION_NAMES[raw.name] ?? raw.name),
2193
+ contentRaw: raw.name,
2194
+ actor: reactor,
2195
+ createdAt: toDate(raw.created_at),
2196
+ raw
2197
+ };
2198
+ });
2199
+ }),
2200
+ reactions: perKind({
2201
+ issue: true,
2202
+ pull_request: true
2203
+ }, {
2204
+ async react(target, reaction, options) {
2205
+ await fetcher.raw(`${awardPath(target)}/award_emoji`, {
2206
+ method: "POST",
2207
+ json: { name: AWARD_EMOJI[reaction] },
2208
+ signal: options?.signal
2209
+ });
2210
+ },
2211
+ async unreact(target, reaction, options) {
2212
+ const path = `${awardPath(target)}/award_emoji`;
2213
+ const { data } = await fetcher.json(path, {
2214
+ query: { per_page: 100 },
2215
+ signal: options?.signal
2216
+ });
2217
+ const login = await viewerLogin();
2218
+ const mine = data.find((award) => award.name === AWARD_EMOJI[reaction] && award.user?.username === login);
2219
+ if (mine) await fetcher.raw(`${path}/${mine.id}`, {
2220
+ method: "DELETE",
2221
+ signal: options?.signal
2222
+ });
2223
+ }
2224
+ }),
2225
+ transfer: verb(true, async (thread, repo, options) => {
2226
+ const ref = requireIssueOrPull(thread, context, "transfer");
2227
+ if (ref.kind !== "issue") throw new UnsupportedOperationError("GitLab moves issues only", context);
2228
+ const { data: target } = await fetcher.json(`/projects/${projectId(repo)}`, { signal: options?.signal });
2229
+ const { data } = await fetcher.json(`${threadPath(ref)}/move`, {
2230
+ method: "POST",
2231
+ json: { to_project_id: target.id },
2232
+ signal: options?.signal
2233
+ });
2234
+ return {
2235
+ forge: FORGE,
2236
+ instance,
2237
+ repo,
2238
+ kind: "issue",
2239
+ number: String(data.iid),
2240
+ externalId: data.id === void 0 ? void 0 : String(data.id)
2241
+ };
2242
+ }),
2243
+ markDuplicate: verb("emulated", async (thread, canonical, options) => {
2244
+ const ref = requireIssueOrPull(thread, context, "mark duplicate");
2245
+ const reference = canonical.repo.owner === ref.repo.owner && canonical.repo.name === ref.repo.name ? `#${canonical.number}` : `${canonical.repo.owner}/${canonical.repo.name}#${canonical.number}`;
2246
+ await fetcher.raw(`${threadPath(ref)}/notes`, {
2247
+ method: "POST",
2248
+ json: { body: `/duplicate ${reference}` },
2249
+ signal: options?.signal
2250
+ });
2251
+ }),
2252
+ setLabels: perKind({
2253
+ issue: true,
2254
+ pull_request: true
2255
+ }, async (thread, labels, options) => {
2256
+ const ref = requireIssueOrPull(thread, context, "label");
2257
+ await fetcher.raw(threadPath(ref), {
2258
+ method: "PUT",
2259
+ json: { labels: labels.join(",") },
2260
+ signal: options?.signal
2261
+ });
2262
+ }),
2263
+ setAssignees: perKind({
2264
+ issue: true,
2265
+ pull_request: true
2266
+ }, async (thread, assignees, options) => {
2267
+ const ref = requireIssueOrPull(thread, context, "assign");
2268
+ await fetcher.raw(threadPath(ref), {
2269
+ method: "PUT",
2270
+ json: { assignee_ids: await Promise.all(assignees.map(userId)) },
2271
+ signal: options?.signal
2272
+ });
2273
+ }),
2274
+ requestReview: perKind({ pull_request: true }, async (thread, reviewers, options) => {
2275
+ const ref = requireThread(thread, context);
2276
+ if (ref.kind !== "pull_request") throw new UnsupportedOperationError("Only merge requests have reviewers", context);
2277
+ const { data } = await fetcher.json(threadPath(ref), { signal: options?.signal });
2278
+ const ids = /* @__PURE__ */ new Set([...(data.reviewers ?? []).map((user) => user.id), ...await Promise.all(reviewers.map(userId))]);
2279
+ await fetcher.raw(threadPath(ref), {
2280
+ method: "PUT",
2281
+ json: { reviewer_ids: [...ids] },
2282
+ signal: options?.signal
2283
+ });
2284
+ }),
2285
+ close: perKind({
2286
+ issue: true,
2287
+ pull_request: true
2288
+ }, (ref, options) => setState(ref, "close", options)),
2289
+ reopen: perKind({
2290
+ issue: true,
2291
+ pull_request: true
2292
+ }, (ref, options) => setState(ref, "reopen", options)),
2293
+ merge: verb(true, merge),
2294
+ subscriptions: perKind(ISSUE_LIKE, {
2295
+ subscription: async (thread, options) => {
2296
+ const ref = requireIssueOrPull(thread, context, "read the subscription of");
2297
+ return (await fetcher.json(threadPath(ref), { signal: options?.signal })).data.subscribed ? "subscribed" : "none";
2298
+ },
2299
+ subscribe: (thread, options) => setSubscribed(thread, true, options),
2300
+ unsubscribe: (thread, options) => setSubscribed(thread, false, options)
2301
+ })
2302
+ }
2303
+ };
2304
+ }
2305
+ };
2306
+ /** Creates a GitLab provider for gitlab.com or a self-managed instance. */
2307
+ const gitlab = /* @__PURE__ */ defineForgeProvider({
2308
+ ...GITLAB,
2309
+ webhooks: gitlabWebhooks
2310
+ });
2311
+ /** `gitlab()` without webhook ingestion, for bundles that never receive a delivery. */
2312
+ const gitlabLite = /* @__PURE__ */ defineForgeProvider(GITLAB);
2313
+ /** GitLab tokens carry coarse scopes; `api` covers writes and `read_api` reads. */
2314
+ function gitlabScopesFor(verb) {
2315
+ const [group = "", name = ""] = verb.split(".");
2316
+ if (group === "webhooks") return name === "verify" || name === "ingest" ? {} : {
2317
+ token: ["api"],
2318
+ note: "Maintainer on the project, or Owner on the group"
2319
+ };
2320
+ return { token: [(/* @__PURE__ */ new Set([
2321
+ "get",
2322
+ "list",
2323
+ "listPage",
2324
+ "page",
2325
+ "events",
2326
+ "eventsPage",
2327
+ "comments",
2328
+ "commentsPage",
2329
+ "getMany",
2330
+ "reviews",
2331
+ "reviewsPage",
2332
+ "checks",
2333
+ "files",
2334
+ "filesPage",
2335
+ "commits",
2336
+ "commitsPage",
2337
+ "code",
2338
+ "codePage",
2339
+ "subscription",
2340
+ "unreadCount",
2341
+ "threads",
2342
+ "threadsPage",
2343
+ "repos",
2344
+ "reposPage",
2345
+ "tree",
2346
+ "treePage",
2347
+ "branches",
2348
+ "branchesPage",
2349
+ "tags",
2350
+ "tagsPage",
2351
+ "file",
2352
+ "compare",
2353
+ "resolveRef",
2354
+ "commit",
2355
+ "latest",
2356
+ "getByTag",
2357
+ "downloadAsset",
2358
+ "runs",
2359
+ "runsPage",
2360
+ "run",
2361
+ "jobs",
2362
+ "jobsPage",
2363
+ "log",
2364
+ "permissionFor",
2365
+ "labels",
2366
+ "labelsPage",
2367
+ "milestones",
2368
+ "milestonesPage",
2369
+ "collaborators",
2370
+ "collaboratorsPage",
2371
+ "assignableUsers",
2372
+ "assignableUsersPage",
2373
+ "reviewerCandidates",
2374
+ "reviewerCandidatesPage"
2375
+ ])).has(name) ? "read_api" : "api"] };
2376
+ }
2377
+ //#endregion
2378
+ export { gitlabLite as n, gitlabScopesFor as r, gitlab as t };