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,1355 @@
1
+ import { C as requireIssueOrPull, Ct as MergeMethodRequiredError, D as syntheticReview, E as summariseChecks, O as toDate, Pt as toMergeError, S as phased, T as resolveToken, _t as ForgeError, b as mergeCommitMessage, f as getManyConcurrently, ft as toFileContent, j as toWarning, jt as UnsupportedOperationError, n as perKind, r as verb, t as defineForgeProvider, v as memo, w as requireThread, wt as NotFoundError } from "./define-D3yEXtKK.mjs";
2
+ import { c as timingSafeEqual, n as headerValue, t as bodyText } from "./crypto-CQNEyYHD.mjs";
3
+ import { i as refEvent } from "./webhooks-rn9-8lxT.mjs";
4
+ //#region src/azure-devops/normalise.ts
5
+ const FORGE = "azure-devops";
6
+ function toActor(instance, identity) {
7
+ if (!identity) return;
8
+ const login = identity.uniqueName ?? identity.displayName ?? identity.id;
9
+ return {
10
+ forge: FORGE,
11
+ instance,
12
+ login,
13
+ id: identity.id,
14
+ name: identity.displayName,
15
+ avatarUrl: identity.imageUrl,
16
+ typeRaw: identity.isContainer ? "group" : "user",
17
+ isBotHint: /Build Service|Project Collection Build Service/.test(identity.displayName ?? "") || /^Build\\/.test(identity.uniqueName ?? "")
18
+ };
19
+ }
20
+ /** Repositories live in a project in an organization; the owner is `org/project`. */
21
+ function toRepoRef(instance, organization, repo) {
22
+ return {
23
+ forge: FORGE,
24
+ instance,
25
+ owner: `${organization}/${repo.project.name}`,
26
+ name: repo.name,
27
+ externalId: repo.id
28
+ };
29
+ }
30
+ /** Work items belong to a project rather than a repository. */
31
+ function projectRef(instance, organization, project) {
32
+ return {
33
+ forge: FORGE,
34
+ instance,
35
+ owner: `${organization}/${project}`,
36
+ name: "",
37
+ kind: "namespace"
38
+ };
39
+ }
40
+ function toRepo(instance, organization, raw) {
41
+ return {
42
+ ref: toRepoRef(instance, organization, raw),
43
+ defaultBranch: raw.defaultBranch?.replace(/^refs\/heads\//, ""),
44
+ visibility: "private",
45
+ isFork: raw.isFork ?? false,
46
+ isArchived: raw.isDisabled ?? false,
47
+ parent: raw.parentRepository?.project ? toRepoRef(instance, organization, {
48
+ ...raw.parentRepository,
49
+ project: raw.parentRepository.project
50
+ }) : void 0,
51
+ topics: [],
52
+ url: raw.webUrl,
53
+ cloneUrls: {
54
+ https: raw.remoteUrl,
55
+ ssh: raw.sshUrl
56
+ },
57
+ raw
58
+ };
59
+ }
60
+ const VOTES = {
61
+ 10: "approved",
62
+ 5: "approved",
63
+ 0: "pending",
64
+ [-5]: "changes_requested",
65
+ [-10]: "changes_requested"
66
+ };
67
+ function toReviewer(instance, raw) {
68
+ return {
69
+ actor: toActor(instance, raw),
70
+ state: VOTES[raw.vote] ?? "unknown",
71
+ stateRaw: `vote:${raw.vote}`,
72
+ ...raw.isContainer ? { isTeam: true } : {}
73
+ };
74
+ }
75
+ const branch = (ref) => ref.replace(/^refs\/heads\//, "");
76
+ function toPullThread(ref, raw) {
77
+ const instance = ref.instance;
78
+ const state = raw.status === "active" ? "open" : raw.status === "completed" ? "merged" : raw.status === "abandoned" ? "closed" : "unknown";
79
+ return {
80
+ ref: {
81
+ ...ref,
82
+ kind: "pull_request",
83
+ number: String(raw.pullRequestId),
84
+ externalId: raw.artifactId
85
+ },
86
+ kind: "pull_request",
87
+ title: raw.title,
88
+ body: raw.description || void 0,
89
+ state,
90
+ stateRaw: raw.status,
91
+ isDraft: raw.isDraft ?? false,
92
+ author: toActor(instance, raw.createdBy),
93
+ assignees: [],
94
+ reviewers: (raw.reviewers ?? []).map((reviewer) => toReviewer(instance, reviewer)),
95
+ labels: (raw.labels ?? []).filter((label) => label.active !== false).map((label) => ({ name: label.name })),
96
+ url: raw._links?.web?.href,
97
+ createdAt: toDate(raw.creationDate),
98
+ closedAt: toDate(raw.closedDate),
99
+ lastActivityAt: toDate(raw.closedDate ?? raw.creationDate),
100
+ branches: {
101
+ head: {
102
+ ref: branch(raw.sourceRefName),
103
+ sha: raw.lastMergeSourceCommit?.commitId
104
+ },
105
+ base: {
106
+ ref: branch(raw.targetRefName),
107
+ sha: raw.lastMergeTargetCommit?.commitId
108
+ },
109
+ mergeCommitSha: raw.status === "completed" ? raw.lastMergeCommit?.commitId : void 0
110
+ },
111
+ raw
112
+ };
113
+ }
114
+ const field = (item, name) => item.fields[name];
115
+ /** State categories `Completed` and `Removed` are closed; every other category is open. */
116
+ function toWorkItemThread(ref, raw, category) {
117
+ const instance = ref.instance;
118
+ const assignee = field(raw, "System.AssignedTo");
119
+ const tags = field(raw, "System.Tags");
120
+ const state = category === void 0 ? "unknown" : category === "Completed" || category === "Removed" ? "closed" : "open";
121
+ return {
122
+ ref: {
123
+ ...ref,
124
+ kind: "issue",
125
+ number: String(raw.id),
126
+ typeRaw: field(raw, "System.WorkItemType")
127
+ },
128
+ kind: "issue",
129
+ title: field(raw, "System.Title") ?? "",
130
+ body: field(raw, "System.Description"),
131
+ state,
132
+ stateRaw: field(raw, "System.State"),
133
+ stateReason: field(raw, "System.Reason"),
134
+ isDraft: false,
135
+ author: toActor(instance, field(raw, "System.CreatedBy")),
136
+ assignees: assignee ? [toActor(instance, assignee)] : [],
137
+ reviewers: [],
138
+ labels: (tags ?? "").split(";").map((tag) => tag.trim()).filter(Boolean).map((name) => ({ name })),
139
+ url: raw._links?.html?.href,
140
+ createdAt: toDate(field(raw, "System.CreatedDate")),
141
+ updatedAt: toDate(field(raw, "System.ChangedDate")),
142
+ closedAt: toDate(field(raw, "Microsoft.VSTS.Common.ClosedDate")),
143
+ lastActivityAt: toDate(field(raw, "System.ChangedDate")),
144
+ commentCount: field(raw, "System.CommentCount"),
145
+ raw
146
+ };
147
+ }
148
+ /** Conversation threads have no file anchor and carry text comments; system threads record votes and updates. */
149
+ function isConversationThread(thread) {
150
+ return !thread.threadContext?.filePath && thread.comments.some((comment) => comment.commentType !== "system");
151
+ }
152
+ /** Pull request comment ids are unique within a thread only, so a comment id is `<thread>/<comment>`. */
153
+ function commentId(thread, comment) {
154
+ return `${thread.id}/${comment.id}`;
155
+ }
156
+ function toPullComment(ref, thread, raw) {
157
+ return {
158
+ ref: {
159
+ forge: FORGE,
160
+ instance: ref.instance,
161
+ thread: ref,
162
+ id: commentId(thread, raw)
163
+ },
164
+ body: raw.content ?? "",
165
+ author: toActor(ref.instance, raw.author),
166
+ createdAt: toDate(raw.publishedDate),
167
+ updatedAt: toDate(raw.lastUpdatedDate),
168
+ ...thread.status && { thread: {
169
+ id: String(thread.id),
170
+ resolved: thread.status !== "active" && thread.status !== "pending"
171
+ } },
172
+ raw
173
+ };
174
+ }
175
+ function toThreadEvents(ref, thread) {
176
+ return thread.comments.filter((comment) => !comment.isDeleted).map((comment) => {
177
+ const actor = toActor(ref.instance, comment.author);
178
+ const system = comment.commentType === "system";
179
+ const kind = thread.properties?.CodeReviewThreadType?.$value === "VoteUpdate" ? "review" : system ? "other" : thread.threadContext?.filePath ? "review_comment" : "comment";
180
+ return {
181
+ forge: FORGE,
182
+ instance: ref.instance,
183
+ id: commentId(thread, comment),
184
+ kind,
185
+ kindRaw: thread.properties?.CodeReviewThreadType?.$value ?? comment.commentType,
186
+ summary: `${actor?.login ?? "someone"} ${system ? comment.content ?? "updated the pull request" : "commented"}`,
187
+ occurredAt: toDate(comment.publishedDate) ?? /* @__PURE__ */ new Date(0),
188
+ actor,
189
+ repo: ref.repo,
190
+ thread: ref,
191
+ source: "poll",
192
+ payload: comment
193
+ };
194
+ });
195
+ }
196
+ function toWorkItemComment(ref, raw) {
197
+ return {
198
+ ref: {
199
+ forge: FORGE,
200
+ instance: ref.instance,
201
+ thread: ref,
202
+ id: String(raw.id)
203
+ },
204
+ body: raw.text,
205
+ author: toActor(ref.instance, raw.createdBy),
206
+ createdAt: toDate(raw.createdDate),
207
+ updatedAt: toDate(raw.modifiedDate),
208
+ raw
209
+ };
210
+ }
211
+ function toWorkItemEvents(ref, updates, comments) {
212
+ const events = updates.flatMap((update) => {
213
+ const fields = update.fields ?? {};
214
+ const actor = toActor(ref.instance, update.revisedBy);
215
+ const kind = fields["System.State"] ? "state_change" : fields["System.AssignedTo"] ? "assignment" : fields["System.Tags"] ? "label" : void 0;
216
+ if (!kind || update.id === 1) return [];
217
+ return [{
218
+ forge: FORGE,
219
+ instance: ref.instance,
220
+ id: `update:${update.id}`,
221
+ kind,
222
+ kindRaw: Object.keys(fields).join(","),
223
+ summary: `${actor?.login ?? "someone"} changed ${kind === "state_change" ? `state to ${String(fields["System.State"]?.newValue)}` : kind === "assignment" ? "the assignee" : "the tags"}`,
224
+ occurredAt: toDate(update.revisedDate) ?? /* @__PURE__ */ new Date(0),
225
+ actor,
226
+ repo: ref.repo,
227
+ thread: ref,
228
+ source: "poll",
229
+ payload: update
230
+ }];
231
+ });
232
+ for (const comment of comments) {
233
+ const actor = toActor(ref.instance, comment.createdBy);
234
+ events.push({
235
+ forge: FORGE,
236
+ instance: ref.instance,
237
+ id: `comment:${comment.id}`,
238
+ kind: "comment",
239
+ kindRaw: "comment",
240
+ summary: `${actor?.login ?? "someone"} commented`,
241
+ occurredAt: toDate(comment.createdDate) ?? /* @__PURE__ */ new Date(0),
242
+ actor,
243
+ repo: ref.repo,
244
+ thread: ref,
245
+ source: "poll",
246
+ payload: comment
247
+ });
248
+ }
249
+ return events.sort((a, b) => a.occurredAt.getTime() - b.occurredAt.getTime());
250
+ }
251
+ const STATUS_STATES = {
252
+ notSet: "unknown",
253
+ pending: "pending",
254
+ succeeded: "success",
255
+ failed: "failure",
256
+ error: "failure",
257
+ notApplicable: "neutral"
258
+ };
259
+ function toStatusCheck(repo, raw) {
260
+ const state = STATUS_STATES[raw.state] ?? "unknown";
261
+ return {
262
+ ref: {
263
+ forge: FORGE,
264
+ instance: repo.instance,
265
+ repo,
266
+ id: String(raw.id),
267
+ type: "status"
268
+ },
269
+ name: raw.context.genre ? `${raw.context.genre}/${raw.context.name}` : raw.context.name,
270
+ state,
271
+ stateRaw: raw.state,
272
+ url: raw.targetUrl,
273
+ startedAt: toDate(raw.creationDate),
274
+ completedAt: state === "pending" ? void 0 : toDate(raw.updatedDate),
275
+ raw
276
+ };
277
+ }
278
+ const POLICY_STATES = {
279
+ queued: "pending",
280
+ running: "pending",
281
+ approved: "success",
282
+ rejected: "failure",
283
+ broken: "failure",
284
+ notApplicable: "neutral"
285
+ };
286
+ /** Branch policy evaluations: required reviewers, builds, linked work items and the like. Optional policies that fail are neutral. */
287
+ function toPolicyCheck(repo, raw) {
288
+ const blocking = raw.configuration?.isBlocking !== false;
289
+ const state = POLICY_STATES[raw.status] ?? "unknown";
290
+ return {
291
+ ref: {
292
+ forge: FORGE,
293
+ instance: repo.instance,
294
+ repo,
295
+ id: raw.evaluationId,
296
+ type: "policy"
297
+ },
298
+ name: raw.configuration?.settings?.displayName ?? raw.configuration?.type?.displayName ?? "Policy",
299
+ state: state === "failure" && !blocking ? "neutral" : state,
300
+ stateRaw: raw.status,
301
+ startedAt: toDate(raw.startedDate),
302
+ completedAt: toDate(raw.completedDate),
303
+ raw
304
+ };
305
+ }
306
+ /** Azure names a change `add`, `edit`, `delete`, `rename` or a comma-joined combination. */
307
+ function toChangedFile(raw) {
308
+ const path = raw.item?.path?.replace(/^\//, "") ?? "";
309
+ const types = raw.changeType?.split(",").map((type) => type.trim().toLowerCase()) ?? [];
310
+ const status = types.includes("delete") ? "removed" : types.includes("rename") ? "renamed" : types.includes("add") ? "added" : types.includes("edit") ? "modified" : "changed";
311
+ return {
312
+ path,
313
+ previousPath: raw.sourceServerItem?.replace(/^\//, ""),
314
+ status,
315
+ statusRaw: raw.changeType,
316
+ sha: raw.item?.objectId
317
+ };
318
+ }
319
+ function toCommit(repo, raw, files) {
320
+ const counts = raw.changeCounts;
321
+ return {
322
+ ref: {
323
+ forge: FORGE,
324
+ instance: repo.instance,
325
+ repo,
326
+ sha: raw.commitId
327
+ },
328
+ sha: raw.commitId,
329
+ message: raw.comment ?? "",
330
+ author: {
331
+ name: raw.author?.name,
332
+ email: raw.author?.email,
333
+ date: toDate(raw.author?.date)
334
+ },
335
+ committer: {
336
+ name: raw.committer?.name,
337
+ email: raw.committer?.email,
338
+ date: toDate(raw.committer?.date)
339
+ },
340
+ parents: raw.parents ?? [],
341
+ url: raw.remoteUrl,
342
+ ...counts ? { stats: {
343
+ additions: counts.Add ?? 0,
344
+ deletions: counts.Delete ?? 0
345
+ } } : {},
346
+ ...files ? { files } : {},
347
+ raw
348
+ };
349
+ }
350
+ function toBranch(raw) {
351
+ return {
352
+ name: raw.name.replace(/^refs\/heads\//, ""),
353
+ sha: raw.objectId ?? "",
354
+ url: raw.url,
355
+ raw
356
+ };
357
+ }
358
+ function toTag(raw) {
359
+ return {
360
+ name: raw.name.replace(/^refs\/tags\//, ""),
361
+ sha: raw.peeledObjectId ?? raw.objectId ?? "",
362
+ url: raw.url,
363
+ raw
364
+ };
365
+ }
366
+ function toTreeEntry(raw) {
367
+ return {
368
+ path: raw.path.replace(/^\//, ""),
369
+ type: raw.isFolder || raw.gitObjectType === "tree" ? "directory" : raw.gitObjectType === "commit" ? "submodule" : "file",
370
+ sha: raw.objectId,
371
+ size: raw.size
372
+ };
373
+ }
374
+ //#endregion
375
+ //#region src/azure-devops/web.ts
376
+ const encode = (path) => path.split("/").map(encodeURIComponent).join("/");
377
+ const gitPath = (repo) => `/${encode(repo.owner)}/_git/${encodeURIComponent(repo.name)}`;
378
+ function azureWeb(origin) {
379
+ return {
380
+ origin,
381
+ repo: (repo) => repo.kind === "namespace" ? `/${encode(repo.owner)}` : gitPath(repo),
382
+ thread: (ref) => {
383
+ if (ref.kind === "issue") return `/${encode(ref.repo.owner)}/_workitems/edit/${encodeURIComponent(ref.number)}`;
384
+ if (ref.kind === "pull_request") return `${gitPath(ref.repo)}/pullrequest/${encodeURIComponent(ref.number)}`;
385
+ return ref.kind === "commit" ? `${gitPath(ref.repo)}/commit/${encodeURIComponent(ref.number)}` : void 0;
386
+ },
387
+ file: (repo, path, at, line) => `${gitPath(repo)}?path=${encodeURIComponent(`/${path}`)}&version=GB${encodeURIComponent(at)}${line ? `&line=${line}` : ""}`,
388
+ compare: (repo, base, head) => `${gitPath(repo)}/branchCompare?baseVersion=GB${encodeURIComponent(base)}&targetVersion=GB${encodeURIComponent(head)}`,
389
+ parse: (segments, _url, from) => {
390
+ const [org, project, section, ...rest] = segments;
391
+ if (!org || !project) return;
392
+ const owner = `${org}/${project}`;
393
+ if (section === "_workitems" && rest[0] === "edit" && rest[1]) {
394
+ const repo = {
395
+ ...from,
396
+ owner,
397
+ name: "",
398
+ kind: "namespace"
399
+ };
400
+ return {
401
+ repo,
402
+ thread: {
403
+ ...from,
404
+ repo,
405
+ kind: "issue",
406
+ number: rest[1]
407
+ }
408
+ };
409
+ }
410
+ if (section !== "_git" || !rest[0]) return { repo: {
411
+ ...from,
412
+ owner,
413
+ name: "",
414
+ kind: "namespace"
415
+ } };
416
+ const repo = {
417
+ ...from,
418
+ owner,
419
+ name: rest[0]
420
+ };
421
+ const kind = rest[1] === "pullrequest" ? "pull_request" : rest[1] === "commit" ? "commit" : void 0;
422
+ if (!kind || !rest[2]) return { repo };
423
+ return {
424
+ repo,
425
+ thread: {
426
+ ...from,
427
+ repo,
428
+ kind,
429
+ number: rest[2]
430
+ }
431
+ };
432
+ },
433
+ reference: (ref) => ref.kind === "issue" ? `#${ref.number}` : ref.kind === "pull_request" ? `!${ref.number}` : void 0
434
+ };
435
+ }
436
+ //#endregion
437
+ //#region src/azure-devops/webhooks.ts
438
+ /**
439
+ * Service hooks carry no signature. A subscription can send basic
440
+ * credentials, so `secret` is `username:password` and the delivery's
441
+ * `Authorization: Basic` header must match it.
442
+ */
443
+ function verifyAzureBasicAuth(delivery, secret) {
444
+ const key = delivery.secret ?? secret;
445
+ const header = headerValue(delivery.headers, "authorization");
446
+ if (!key || !header?.startsWith("Basic ")) return false;
447
+ return timingSafeEqual(header, `Basic ${btoa(key)}`);
448
+ }
449
+ const PULL_KINDS = {
450
+ "git.pullrequest.created": "state_change",
451
+ "git.pullrequest.merged": "state_change",
452
+ "git.pullrequest.updated": "other",
453
+ "ms.vss-code.git-pullrequest-comment-event": "comment"
454
+ };
455
+ function translateAzureWebhook(instance, organization, delivery) {
456
+ const event = JSON.parse(bodyText(delivery.body));
457
+ const resource = event.resource;
458
+ const pull = resource.pullRequest ?? (event.eventType.startsWith("git.pullrequest") ? resource : void 0);
459
+ const repository = pull?.repository ?? resource.repository;
460
+ const repo = repository?.project ? toRepoRef(instance, organization, repository) : void 0;
461
+ const base = {
462
+ forge: FORGE,
463
+ instance,
464
+ id: event.id,
465
+ kindRaw: event.eventType,
466
+ occurredAt: toDate(event.createdDate) ?? /* @__PURE__ */ new Date(),
467
+ repo,
468
+ source: "webhook",
469
+ payload: event
470
+ };
471
+ if (pull && repo) {
472
+ const thread = {
473
+ forge: FORGE,
474
+ instance,
475
+ repo,
476
+ kind: "pull_request",
477
+ number: String(pull.pullRequestId)
478
+ };
479
+ const comment = resource.comment;
480
+ const actor = toActor(instance, comment?.author ?? pull.createdBy);
481
+ return [{
482
+ ...base,
483
+ kind: PULL_KINDS[event.eventType] ?? "other",
484
+ actor,
485
+ thread,
486
+ summary: event.message?.text ?? `${actor?.login ?? "someone"} ${event.eventType}`
487
+ }];
488
+ }
489
+ if (event.eventType.startsWith("workitem.")) {
490
+ const fields = resource.fields ?? resource.revision?.fields ?? {};
491
+ const id = String(resource.workItemId ?? resource.id);
492
+ const project = fields["System.TeamProject"];
493
+ const projectName = typeof project === "string" ? project : project?.newValue;
494
+ const thread = projectName ? {
495
+ forge: FORGE,
496
+ instance,
497
+ repo: projectRef(instance, organization, projectName),
498
+ kind: "issue",
499
+ number: id
500
+ } : void 0;
501
+ const kind = event.eventType === "workitem.commented" ? "comment" : event.eventType === "workitem.updated" ? "other" : "state_change";
502
+ return [{
503
+ ...base,
504
+ kind,
505
+ thread,
506
+ repo: thread?.repo,
507
+ actor: toActor(instance, resource.revisedBy),
508
+ summary: event.message?.text ?? event.eventType
509
+ }];
510
+ }
511
+ if (event.eventType === "git.push" && repo) {
512
+ const updates = resource.refUpdates ?? [];
513
+ const commits = (resource.commits ?? []).map((commit) => ({
514
+ sha: commit.commitId,
515
+ message: commit.comment ?? "",
516
+ author: commit.author?.name,
517
+ url: commit.url
518
+ }));
519
+ const actor = toActor(instance, resource.pushedBy);
520
+ const zero = /^0+$/;
521
+ return updates.map((update, index) => ({
522
+ ...base,
523
+ id: `${event.id}:${index}`,
524
+ actor,
525
+ ...refEvent(actor?.login ?? "someone", {
526
+ ref: update.name,
527
+ before: update.oldObjectId,
528
+ after: update.newObjectId,
529
+ created: zero.test(update.oldObjectId ?? ""),
530
+ deleted: zero.test(update.newObjectId ?? ""),
531
+ commits
532
+ })
533
+ }));
534
+ }
535
+ if (event.eventType === "git.repo.renamed" && repo) return [{
536
+ ...base,
537
+ kind: "repo",
538
+ action: "renamed",
539
+ detail: {
540
+ type: "repo_renamed",
541
+ from: String(resource.oldName ?? ""),
542
+ to: repository.name
543
+ },
544
+ summary: event.message?.text ?? "Repository renamed"
545
+ }];
546
+ return [{
547
+ ...base,
548
+ kind: "other",
549
+ summary: event.message?.text ?? event.eventType
550
+ }];
551
+ }
552
+ const AZURE_WEBHOOK_EVENTS = [
553
+ {
554
+ kind: "comment",
555
+ action: "created"
556
+ },
557
+ {
558
+ kind: "state_change",
559
+ action: "opened"
560
+ },
561
+ {
562
+ kind: "state_change",
563
+ action: "closed"
564
+ },
565
+ {
566
+ kind: "state_change",
567
+ action: "merged"
568
+ },
569
+ {
570
+ kind: "review",
571
+ action: "submitted"
572
+ },
573
+ { kind: "push" }
574
+ ];
575
+ const azureDevOpsWebhooks = ({ options, instance }) => ({
576
+ events: AZURE_WEBHOOK_EVENTS,
577
+ verify: async (delivery) => verifyAzureBasicAuth(delivery, options.webhookSecret),
578
+ translate: (delivery) => translateAzureWebhook(instance, options.organization, delivery)
579
+ });
580
+ //#endregion
581
+ //#region src/azure-devops/index.ts
582
+ /** The REST API version sent with every request; preview endpoints set their own. */
583
+ const AZURE_DEVOPS_API_VERSION = "7.1";
584
+ const POLICY_API_VERSION = "7.1-preview.1";
585
+ const COMMENTS_API_VERSION = "7.1-preview.4";
586
+ const ISSUE_AND_PULL = {
587
+ issue: true,
588
+ pull_request: true
589
+ };
590
+ const PER_PAGE = 50;
591
+ /** System work item types that are not issues: test management and code review records. */
592
+ const NOT_ISSUES = [
593
+ "Test Case",
594
+ "Test Plan",
595
+ "Test Suite",
596
+ "Shared Steps",
597
+ "Shared Parameter",
598
+ "Code Review Request",
599
+ "Code Review Response",
600
+ "Feedback Request",
601
+ "Feedback Response"
602
+ ];
603
+ const MERGE_STRATEGIES = {
604
+ merge: "noFastForward",
605
+ squash: "squash",
606
+ rebase: "rebase",
607
+ rebase_merge: "rebaseMerge"
608
+ };
609
+ function versionType(ref) {
610
+ return /^[0-9a-f]{40}$/i.test(ref) ? "commit" : "branch";
611
+ }
612
+ function setupAzure({ options, instance, origin: context, fetcher, baseUrl }) {
613
+ const enc = encodeURIComponent;
614
+ /** Pull request statuses, policy evaluations and WIQL queries redirect a request without credentials to sign in. */
615
+ const anonymous = options.auth?.type === "anonymous";
616
+ const STATUS_STATES = {
617
+ pending: "pending",
618
+ success: "succeeded",
619
+ failure: "failed",
620
+ neutral: "notApplicable",
621
+ unknown: "notSet"
622
+ };
623
+ function scope(repo) {
624
+ const slash = repo.owner.indexOf("/");
625
+ if (slash < 0) return {
626
+ org: options.organization,
627
+ project: repo.owner
628
+ };
629
+ return {
630
+ org: repo.owner.slice(0, slash),
631
+ project: repo.owner.slice(slash + 1)
632
+ };
633
+ }
634
+ const projectPath = (repo) => {
635
+ const { org, project } = scope(repo);
636
+ return `/${enc(org)}/${enc(project)}/_apis`;
637
+ };
638
+ const repoPath = (repo) => `${projectPath(repo)}/git/repositories/${enc(repo.externalId ?? repo.name)}`;
639
+ const pullPath = (ref) => `${repoPath(ref.repo)}/pullRequests/${ref.number}`;
640
+ const workItemPath = (ref) => `${projectPath(ref.repo)}/wit/workitems/${ref.number}`;
641
+ const issueRef = (repo, id) => {
642
+ const { org, project } = scope(repo);
643
+ return {
644
+ forge: FORGE,
645
+ instance,
646
+ repo: projectRef(instance, org, project),
647
+ kind: "issue",
648
+ number: id
649
+ };
650
+ };
651
+ const categories = /* @__PURE__ */ new Map();
652
+ function stateCategories(repo, type) {
653
+ const key = `${repo.owner}:${type}`;
654
+ let found = categories.get(key);
655
+ if (!found) {
656
+ found = fetcher.json(`${projectPath(repo)}/wit/workitemtypes/${enc(type)}/states`).then(({ data }) => new Map(data.value.map((state) => [state.name, state.category])));
657
+ found.catch(() => categories.delete(key));
658
+ categories.set(key, found);
659
+ }
660
+ return found;
661
+ }
662
+ async function workItemThread(ref, raw) {
663
+ const type = raw.fields["System.WorkItemType"];
664
+ return toWorkItemThread(ref, raw, type ? (await stateCategories(ref.repo, type)).get(raw.fields["System.State"]) : void 0);
665
+ }
666
+ const myId = memo(() => fetcher.json(`/${enc(options.organization)}/_apis/connectionData`, { query: { "api-version": "7.1-preview.1" } }).then(({ data }) => data.authenticatedUser.id));
667
+ async function pullChecks(ref, pull, signal) {
668
+ const [statuses, evaluations] = await Promise.all([fetcher.json(`${pullPath(ref)}/statuses`, { signal }), fetcher.json(`${projectPath(ref.repo)}/policy/evaluations`, {
669
+ query: {
670
+ "artifactId": `vstfs:///CodeReview/CodeReviewId/${pull.repository.project.id}/${pull.pullRequestId}`,
671
+ "api-version": POLICY_API_VERSION
672
+ },
673
+ signal
674
+ })]);
675
+ return [...statuses.data.value.map((raw) => toStatusCheck(ref.repo, raw)), ...evaluations.data.value.map((raw) => toPolicyCheck(ref.repo, raw))];
676
+ }
677
+ /** Azure DevOps has reviewer votes, not reviews: one synthesised review per voter. */
678
+ function toVoteReview(ref, reviewer) {
679
+ const state = reviewer.vote >= 5 ? "approved" : reviewer.vote <= -5 ? "changes_requested" : "pending";
680
+ return syntheticReview(ref, `vote:${reviewer.id}`, state, {
681
+ author: toActor(instance, reviewer),
682
+ stateRaw: `vote:${reviewer.vote}`,
683
+ raw: reviewer
684
+ });
685
+ }
686
+ async function createReview(thread, input, options) {
687
+ const ref = requireThread(thread, context);
688
+ if (ref.kind !== "pull_request") throw new UnsupportedOperationError("Only pull requests can be reviewed", context);
689
+ if (input.event !== "approve" && input.event !== "request_changes") throw new UnsupportedOperationError("Azure DevOps records votes, not review bodies", context);
690
+ if (input.body || input.comments?.length) throw new UnsupportedOperationError("Azure DevOps votes carry no body or inline comments; post them as comment threads", context);
691
+ const id = await myId();
692
+ const vote = input.event === "approve" ? 10 : -10;
693
+ const { data } = await fetcher.json(`${pullPath(ref)}/reviewers/${enc(id)}`, {
694
+ method: "PUT",
695
+ json: { vote },
696
+ signal: options?.signal
697
+ });
698
+ return toVoteReview(ref, {
699
+ ...data,
700
+ id: data.id ?? id,
701
+ vote: data.vote ?? vote
702
+ });
703
+ }
704
+ async function setThreadStatus(thread, id, status, options) {
705
+ const ref = requireThread(thread, context);
706
+ await fetcher.raw(`${pullPath(ref)}/threads/${enc(id)}`, {
707
+ method: "PATCH",
708
+ json: { status },
709
+ signal: options?.signal
710
+ });
711
+ }
712
+ async function get(thread, options) {
713
+ const ref = requireIssueOrPull(thread, context, "read");
714
+ if (ref.kind === "issue") return workItemThread(issueRef(ref.repo, ref.number), (await fetcher.json(workItemPath(ref), {
715
+ query: { $expand: "links" },
716
+ signal: options?.signal
717
+ })).data);
718
+ const { data } = await fetcher.json(pullPath(ref), { signal: options?.signal });
719
+ const result = toPullThread({
720
+ ...ref,
721
+ repo: toRepoRef(instance, scope(ref.repo).org, data.repository)
722
+ }, data);
723
+ if (anonymous) return result;
724
+ try {
725
+ result.checks = summariseChecks((await pullChecks(ref, data, options?.signal)).map((check) => check.state));
726
+ } catch (error) {
727
+ if (!(error instanceof ForgeError)) throw error;
728
+ result.warnings = [...result.warnings ?? [], toWarning("checks_unreadable", error, ref.number)];
729
+ }
730
+ return result;
731
+ }
732
+ async function pullPage(repo, status, query, cursor) {
733
+ const skip = Number(cursor?.token ?? 0);
734
+ const top = Math.min(query.perPage ?? PER_PAGE, 1e3);
735
+ const { data } = await fetcher.json(`${repoPath(repo)}/pullrequests`, {
736
+ query: {
737
+ "searchCriteria.status": status,
738
+ "searchCriteria.minTime": query.createdAfter?.toISOString(),
739
+ "searchCriteria.queryTimeRangeType": query.createdAfter ? "created" : void 0,
740
+ "$top": top,
741
+ "$skip": skip
742
+ },
743
+ signal: query.signal
744
+ });
745
+ const { org } = scope(repo);
746
+ return {
747
+ items: data.value.filter((raw) => !query.author || raw.createdBy?.uniqueName === query.author || raw.createdBy?.id === query.author).filter((raw) => !query.labels?.length || query.labels.every((label) => raw.labels?.some((item) => item.name === label))).map((raw) => toPullThread({
748
+ forge: FORGE,
749
+ instance,
750
+ repo: toRepoRef(instance, org, raw.repository),
751
+ kind: "pull_request",
752
+ number: String(raw.pullRequestId)
753
+ }, raw)),
754
+ cursor: data.value.length === top ? { token: String(skip + top) } : void 0
755
+ };
756
+ }
757
+ function wiql(query) {
758
+ const literal = (value) => `'${value.replaceAll("'", "''")}'`;
759
+ const clauses = [
760
+ "[System.TeamProject] = @project",
761
+ `[System.WorkItemType] NOT IN (${NOT_ISSUES.map(literal).join(", ")})`,
762
+ ...(query.labels ?? []).map((label) => `[System.Tags] CONTAINS ${literal(label)}`),
763
+ ...query.author ? [`[System.CreatedBy] = ${literal(query.author)}`] : [],
764
+ ...query.assignee ? [`[System.AssignedTo] = ${literal(query.assignee)}`] : [],
765
+ ...query.since ? [`[System.ChangedDate] >= ${literal(query.since.toISOString())}`] : [],
766
+ ...query.createdAfter ? [`[System.CreatedDate] >= ${literal(query.createdAfter.toISOString())}`] : [],
767
+ ...query.text ? [`[System.Title] CONTAINS ${literal(query.text)}`] : []
768
+ ];
769
+ const order = query.sort === "updated" ? "[System.ChangedDate]" : "[System.CreatedDate]";
770
+ return `SELECT [System.Id] FROM WorkItems WHERE ${clauses.join(" AND ")} ORDER BY ${order} ${(query.direction ?? "desc").toUpperCase()}`;
771
+ }
772
+ /** WIQL returns every matching id; each page reads its slice of them. */
773
+ async function workItemPage(repo, query, cursor) {
774
+ const offset = Number(cursor?.token ?? 0);
775
+ const size = Math.min(query.perPage ?? PER_PAGE, 200);
776
+ const { data } = await fetcher.json(`${projectPath(repo)}/wit/wiql`, {
777
+ method: "POST",
778
+ json: { query: wiql(query) },
779
+ signal: query.signal
780
+ });
781
+ const ids = data.workItems.slice(offset, offset + size).map((item) => item.id);
782
+ if (!ids.length) return { items: [] };
783
+ const { data: batch } = await fetcher.json(`${projectPath(repo)}/wit/workitemsbatch`, {
784
+ method: "POST",
785
+ json: {
786
+ ids,
787
+ $expand: "Links"
788
+ },
789
+ signal: query.signal
790
+ });
791
+ const state = query.state ?? "open";
792
+ return {
793
+ items: (await Promise.all(batch.value.map((raw) => workItemThread(issueRef(repo, String(raw.id)), raw)))).filter((thread) => state === "all" || thread.state === state),
794
+ cursor: offset + size < data.workItems.length ? { token: String(offset + size) } : void 0
795
+ };
796
+ }
797
+ async function listPage(repo, query = {}) {
798
+ if (query.kind === "discussion") return {
799
+ items: [],
800
+ warnings: [{
801
+ code: "kind_unsupported",
802
+ message: "Azure DevOps has no discussions"
803
+ }]
804
+ };
805
+ const warnings = [];
806
+ if (!query.cursor && query.kind !== "issue") {
807
+ if (query.sort && query.sort !== "created") warnings.push({
808
+ code: "sort_unsupported",
809
+ message: "Azure DevOps lists pull requests newest first only"
810
+ });
811
+ if (query.assignee || query.since) warnings.push({
812
+ code: "filter_unsupported",
813
+ message: "Azure DevOps pull requests have no assignees or update-time filter; those filters were ignored"
814
+ });
815
+ }
816
+ const state = query.state ?? "open";
817
+ const pullStates = state === "open" ? ["active"] : state === "merged" ? ["completed"] : state === "closed" ? ["completed", "abandoned"] : ["all"];
818
+ const phases = [];
819
+ if (query.kind !== "pull_request") {
820
+ if (!anonymous) phases.push((cursor) => workItemPage(repo, query, cursor));
821
+ else if (!query.cursor) warnings.push({
822
+ code: "kind_unsupported",
823
+ message: "Azure DevOps lists work items only to signed-in users; this listing has pull requests only"
824
+ });
825
+ }
826
+ if (query.kind !== "issue") phases.push(...pullStates.map((status) => (cursor) => pullPage(repo, status, query, cursor)));
827
+ const page = await phased(phases, query.cursor);
828
+ return warnings.length ? {
829
+ ...page,
830
+ warnings: [...warnings, ...page.warnings ?? []]
831
+ } : page;
832
+ }
833
+ async function threads(ref, options) {
834
+ return (await fetcher.json(`${pullPath(ref)}/threads`, { signal: options?.signal })).data.value.filter((thread) => !thread.isDeleted);
835
+ }
836
+ /** Pull request threads come unpaged, so comments are one complete page; work item comments page by continuation token. */
837
+ async function commentsPage(thread, listOptions = {}) {
838
+ const ref = requireIssueOrPull(thread, context, "list comments on");
839
+ if (ref.kind === "pull_request") return { items: (await threads(ref, listOptions)).filter(isConversationThread).flatMap((item) => item.comments.filter((comment) => !comment.isDeleted && comment.commentType !== "system").map((comment) => toPullComment(ref, item, comment))) };
840
+ const issue = issueRef(ref.repo, ref.number);
841
+ const { data } = await fetcher.json(`${workItemPath(ref)}/comments`, {
842
+ query: {
843
+ "api-version": COMMENTS_API_VERSION,
844
+ "$top": listOptions.perPage ?? PER_PAGE,
845
+ "continuationToken": listOptions.cursor?.token
846
+ },
847
+ signal: listOptions.signal
848
+ });
849
+ return {
850
+ items: data.comments.filter((comment) => !comment.isDeleted).map((raw) => toWorkItemComment(issue, raw)),
851
+ cursor: data.continuationToken ? { token: data.continuationToken } : void 0
852
+ };
853
+ }
854
+ async function eventsPage(thread, listOptions = {}) {
855
+ const ref = requireIssueOrPull(thread, context, "list events on");
856
+ if (ref.kind === "pull_request") return { items: (await threads(ref, listOptions)).flatMap((item) => toThreadEvents(ref, item)).sort((a, b) => a.occurredAt.getTime() - b.occurredAt.getTime()) };
857
+ const issue = issueRef(ref.repo, ref.number);
858
+ const [{ data: updates }, { data: comments }] = await Promise.all([fetcher.json(`${workItemPath(ref)}/updates`, { signal: listOptions.signal }), fetcher.json(`${workItemPath(ref)}/comments`, {
859
+ query: { "api-version": COMMENTS_API_VERSION },
860
+ signal: listOptions.signal
861
+ })]);
862
+ return { items: toWorkItemEvents(issue, updates.value, comments.comments.filter((comment) => !comment.isDeleted)) };
863
+ }
864
+ async function patchWorkItem(ref, fields, signal) {
865
+ const { data } = await fetcher.json(workItemPath(ref), {
866
+ method: "PATCH",
867
+ headers: { "content-type": "application/json-patch+json" },
868
+ body: JSON.stringify(Object.entries(fields).map(([name, value]) => ({
869
+ op: "add",
870
+ path: `/fields/${name}`,
871
+ value
872
+ }))),
873
+ signal
874
+ });
875
+ return data;
876
+ }
877
+ async function stateIn(ref, wanted, options) {
878
+ const { data } = await fetcher.json(workItemPath(ref), { signal: options?.signal });
879
+ const type = data.fields["System.WorkItemType"];
880
+ for (const [name, category] of await stateCategories(ref.repo, type)) if (wanted.includes(category)) return name;
881
+ throw new UnsupportedOperationError(`The ${type} work item type has no ${wanted.join(" or ")} state`, context);
882
+ }
883
+ async function setState(thread, state, options) {
884
+ const ref = requireIssueOrPull(thread, context, state === "open" ? "reopen" : "close");
885
+ if (ref.kind === "pull_request") {
886
+ await fetcher.raw(pullPath(ref), {
887
+ method: "PATCH",
888
+ json: { status: state === "open" ? "active" : "abandoned" },
889
+ signal: options?.signal
890
+ });
891
+ return;
892
+ }
893
+ await patchWorkItem(ref, { "System.State": await stateIn(ref, state === "open" ? ["Proposed", "InProgress"] : ["Completed"], options) }, options?.signal);
894
+ }
895
+ function splitCommentId(ref) {
896
+ const [thread = "", comment = ""] = ref.id.split("/");
897
+ return {
898
+ thread,
899
+ comment
900
+ };
901
+ }
902
+ /** WIQL runs against one project, and reaches work items only: Azure DevOps has no pull request search. */
903
+ async function searchThreadsPage(query) {
904
+ if (!query.repo) throw new UnsupportedOperationError("Azure DevOps work item search is scoped to a project; pass `repo`", context);
905
+ if (query.kind === "pull_request") return {
906
+ items: [],
907
+ warnings: [{
908
+ code: "kind_unsupported",
909
+ message: "Azure DevOps has no pull request search"
910
+ }]
911
+ };
912
+ const warnings = query.involves && !query.cursor ? [{
913
+ code: "filter_unsupported",
914
+ message: "Azure DevOps WIQL has no involves filter; it was ignored"
915
+ }] : [];
916
+ const page = await workItemPage(query.repo, {
917
+ text: query.text,
918
+ state: query.state ?? "all",
919
+ labels: query.labels,
920
+ author: query.author,
921
+ assignee: query.assignee,
922
+ since: query.since,
923
+ sort: query.sort === "created" ? "created" : "updated",
924
+ direction: query.direction,
925
+ perPage: query.perPage,
926
+ signal: query.signal
927
+ }, query.cursor);
928
+ return warnings.length ? {
929
+ ...page,
930
+ warnings: [...warnings, ...page.warnings ?? []]
931
+ } : page;
932
+ }
933
+ return {
934
+ traits: {
935
+ eventKinds: "native",
936
+ authKinds: [
937
+ "token",
938
+ "basic",
939
+ "anonymous"
940
+ ]
941
+ },
942
+ search: { threadsPage: verb(!anonymous, searchThreadsPage) },
943
+ repos: {
944
+ get: verb(true, async (ref, options) => {
945
+ const { data } = await fetcher.json(repoPath(ref), { signal: options?.signal });
946
+ return toRepo(instance, scope(ref).org, data);
947
+ }),
948
+ listPage: verb(true, async (listOptions) => {
949
+ const { data } = await fetcher.json(`/${enc(options.organization)}/_apis/git/repositories`, { signal: listOptions?.signal });
950
+ return { items: data.value.map((raw) => toRepo(instance, options.organization, raw)) };
951
+ })
952
+ },
953
+ contents: {
954
+ file: verb(true, async (repo, path, fileOptions = {}) => {
955
+ const query = {
956
+ path,
957
+ download: "true",
958
+ $format: "octetStream",
959
+ ...fileOptions.ref ? {
960
+ "versionDescriptor.version": fileOptions.ref,
961
+ "versionDescriptor.versionType": versionType(fileOptions.ref)
962
+ } : {}
963
+ };
964
+ const response = await fetcher.raw(`${repoPath(repo)}/items`, {
965
+ query,
966
+ signal: fileOptions.signal
967
+ });
968
+ return toFileContent(new Uint8Array(await response.arrayBuffer()), { path: path.replace(/^\//, "") }, fileOptions, context);
969
+ }),
970
+ treePage: verb(true, async (repo, treeOptions = {}) => {
971
+ const { data } = await fetcher.json(`${repoPath(repo)}/items`, {
972
+ query: {
973
+ scopePath: treeOptions.path ? `/${treeOptions.path.replace(/^\//, "")}` : "/",
974
+ recursionLevel: treeOptions.recursive ? "full" : "oneLevel",
975
+ ...treeOptions.ref ? {
976
+ "versionDescriptor.version": treeOptions.ref,
977
+ "versionDescriptor.versionType": versionType(treeOptions.ref)
978
+ } : {}
979
+ },
980
+ signal: treeOptions.signal
981
+ });
982
+ return { items: data.value.filter((item) => item.path !== "/").map(toTreeEntry) };
983
+ }),
984
+ branchesPage: verb(true, async (repo, listOptions = {}) => {
985
+ const { data } = await fetcher.json(`${repoPath(repo)}/refs`, {
986
+ query: { filter: "heads/" },
987
+ signal: listOptions.signal
988
+ });
989
+ return { items: data.value.map(toBranch) };
990
+ }),
991
+ tagsPage: verb(true, async (repo, listOptions = {}) => {
992
+ const { data } = await fetcher.json(`${repoPath(repo)}/refs`, {
993
+ query: {
994
+ filter: "tags/",
995
+ peelTags: "true"
996
+ },
997
+ signal: listOptions.signal
998
+ });
999
+ return { items: data.value.map(toTag) };
1000
+ }),
1001
+ resolveRef: verb(true, async (repo, ref, options) => {
1002
+ const { data } = await fetcher.json(`${repoPath(repo)}/commits`, {
1003
+ query: {
1004
+ "searchCriteria.itemVersion.version": ref,
1005
+ "$top": 1
1006
+ },
1007
+ signal: options?.signal
1008
+ });
1009
+ const sha = data.value[0]?.commitId;
1010
+ if (!sha) throw new NotFoundError(`Azure DevOps has no commit for ${ref}`, 404, "", context);
1011
+ return sha;
1012
+ }),
1013
+ commitsPage: verb(true, async (repo, query = {}) => {
1014
+ const { data } = await fetcher.json(`${repoPath(repo)}/commits`, {
1015
+ query: {
1016
+ "searchCriteria.itemVersion.version": query.ref,
1017
+ "searchCriteria.itemPath": query.path,
1018
+ "searchCriteria.author": query.author,
1019
+ "searchCriteria.fromDate": query.since?.toISOString(),
1020
+ "searchCriteria.toDate": query.until?.toISOString(),
1021
+ "$top": query.perPage
1022
+ },
1023
+ signal: query.signal
1024
+ });
1025
+ return { items: data.value.map((raw) => toCommit(repo, raw)) };
1026
+ }),
1027
+ commit: verb(true, async (repo, sha, options) => {
1028
+ const { data } = await fetcher.json(`${repoPath(repo)}/commits/${sha}`, {
1029
+ query: { changeCount: 100 },
1030
+ signal: options?.signal
1031
+ });
1032
+ return toCommit(repo, data, (data.changes ?? []).filter((change) => !change.item?.isFolder).map(toChangedFile));
1033
+ }),
1034
+ compare: verb(true, async (repo, base, head, options) => {
1035
+ const { data } = await fetcher.json(`${repoPath(repo)}/diffs/commits`, {
1036
+ query: {
1037
+ baseVersion: base,
1038
+ baseVersionType: versionType(base),
1039
+ targetVersion: head,
1040
+ targetVersionType: versionType(head),
1041
+ $top: 1e3
1042
+ },
1043
+ signal: options?.signal
1044
+ });
1045
+ return {
1046
+ base,
1047
+ head,
1048
+ aheadBy: data.aheadCount,
1049
+ behindBy: data.behindCount,
1050
+ mergeBaseSha: data.commonCommit,
1051
+ commits: [],
1052
+ files: (data.changes ?? []).filter((change) => !change.item?.isFolder).map(toChangedFile),
1053
+ raw: data
1054
+ };
1055
+ })
1056
+ },
1057
+ checks: {
1058
+ list: verb(true, async (repo, sha, options) => ({ items: (await fetcher.json(`${repoPath(repo)}/commits/${sha}/statuses`, { signal: options?.signal })).data.value.map((raw) => toStatusCheck(repo, raw)) })),
1059
+ report: verb(true, async (repo, sha, input, options) => toStatusCheck(repo, (await fetcher.json(`${repoPath(repo)}/commits/${sha}/statuses`, {
1060
+ method: "POST",
1061
+ json: {
1062
+ state: STATUS_STATES[input.state],
1063
+ description: input.description,
1064
+ targetUrl: input.url,
1065
+ context: {
1066
+ name: input.name,
1067
+ genre: input.externalId ?? "forges"
1068
+ }
1069
+ },
1070
+ signal: options?.signal
1071
+ })).data))
1072
+ },
1073
+ threads: {
1074
+ get: perKind({
1075
+ issue: anonymous ? "unverified" : true,
1076
+ pull_request: true
1077
+ }, get),
1078
+ getMany: verb(true, (refs, options) => getManyConcurrently(refs, get, options)),
1079
+ listPage: perKind({
1080
+ issue: !anonymous,
1081
+ pull_request: true
1082
+ }, listPage),
1083
+ eventsPage: verb(true, eventsPage),
1084
+ commitsPage: verb(true, async (thread, listOptions = {}) => {
1085
+ const ref = requireThread(thread, context);
1086
+ if (ref.kind !== "pull_request") throw new UnsupportedOperationError("Only pull requests can be read for commits", context);
1087
+ const { data } = await fetcher.json(`${pullPath(ref)}/commits`, {
1088
+ query: { $top: listOptions.perPage },
1089
+ signal: listOptions.signal
1090
+ });
1091
+ return { items: data.value.map((raw) => toCommit(ref.repo, raw)) };
1092
+ }),
1093
+ commentsPage: perKind({
1094
+ issue: anonymous ? "unverified" : "experimental",
1095
+ pull_request: true
1096
+ }, commentsPage),
1097
+ comment: perKind({
1098
+ issue: "experimental",
1099
+ pull_request: true
1100
+ }, async (thread, body, options) => {
1101
+ const ref = requireIssueOrPull(thread, context, "comment on");
1102
+ if (ref.kind === "issue") {
1103
+ const { data } = await fetcher.json(`${workItemPath(ref)}/comments`, {
1104
+ method: "POST",
1105
+ json: { text: body },
1106
+ query: { "api-version": COMMENTS_API_VERSION },
1107
+ signal: options?.signal
1108
+ });
1109
+ return toWorkItemComment(issueRef(ref.repo, ref.number), data);
1110
+ }
1111
+ const { data } = await fetcher.json(`${pullPath(ref)}/threads`, {
1112
+ method: "POST",
1113
+ json: {
1114
+ comments: [{
1115
+ parentCommentId: 0,
1116
+ content: body,
1117
+ commentType: "text"
1118
+ }],
1119
+ status: "active"
1120
+ },
1121
+ signal: options?.signal
1122
+ });
1123
+ return toPullComment(ref, data, data.comments[0]);
1124
+ }),
1125
+ editComment: perKind({
1126
+ issue: "experimental",
1127
+ pull_request: true
1128
+ }, async (ref, body, options) => {
1129
+ const thread = requireIssueOrPull(ref.thread, context, "edit comments on");
1130
+ if (thread.kind === "issue") {
1131
+ const { data } = await fetcher.json(`${workItemPath(thread)}/comments/${ref.id}`, {
1132
+ method: "PATCH",
1133
+ json: { text: body },
1134
+ query: { "api-version": COMMENTS_API_VERSION },
1135
+ signal: options?.signal
1136
+ });
1137
+ return toWorkItemComment(issueRef(thread.repo, thread.number), data);
1138
+ }
1139
+ const ids = splitCommentId(ref);
1140
+ const { data } = await fetcher.json(`${pullPath(thread)}/threads/${ids.thread}/comments/${ids.comment}`, {
1141
+ method: "PATCH",
1142
+ json: { content: body },
1143
+ signal: options?.signal
1144
+ });
1145
+ return toPullComment(thread, {
1146
+ id: Number(ids.thread),
1147
+ comments: []
1148
+ }, data);
1149
+ }),
1150
+ deleteComment: perKind({
1151
+ issue: "experimental",
1152
+ pull_request: true
1153
+ }, async (ref, options) => {
1154
+ const thread = requireIssueOrPull(ref.thread, context, "delete comments on");
1155
+ if (thread.kind === "issue") {
1156
+ await fetcher.raw(`${workItemPath(thread)}/comments/${ref.id}`, {
1157
+ method: "DELETE",
1158
+ query: { "api-version": COMMENTS_API_VERSION },
1159
+ signal: options?.signal
1160
+ });
1161
+ return;
1162
+ }
1163
+ const ids = splitCommentId(ref);
1164
+ await fetcher.raw(`${pullPath(thread)}/threads/${ids.thread}/comments/${ids.comment}`, {
1165
+ method: "DELETE",
1166
+ signal: options?.signal
1167
+ });
1168
+ }),
1169
+ create: perKind({
1170
+ issue: true,
1171
+ pull_request: true
1172
+ }, async (repo, input, writeOptions) => {
1173
+ if (input.kind === "issue") {
1174
+ if ((input.assignees?.length ?? 0) > 1) throw new UnsupportedOperationError("Azure DevOps work items take a single assignee", context);
1175
+ const fields = { "System.Title": input.title };
1176
+ if (input.body) fields["System.Description"] = input.body;
1177
+ if (input.labels?.length) fields["System.Tags"] = input.labels.join("; ");
1178
+ if (input.assignees?.[0]) fields["System.AssignedTo"] = typeof input.assignees[0] === "string" ? input.assignees[0] : input.assignees[0].login;
1179
+ const { data } = await fetcher.json(`${projectPath(repo)}/wit/workitems/$${enc(options.issueType ?? "Issue")}`, {
1180
+ method: "POST",
1181
+ headers: { "content-type": "application/json-patch+json" },
1182
+ body: JSON.stringify(Object.entries(fields).map(([name, value]) => ({
1183
+ op: "add",
1184
+ path: `/fields/${name}`,
1185
+ value
1186
+ }))),
1187
+ signal: writeOptions?.signal
1188
+ });
1189
+ return workItemThread(issueRef(repo, String(data.id)), data);
1190
+ }
1191
+ if (!input.head || !input.base) throw new UnsupportedOperationError("Creating a pull request needs head and base branches", context);
1192
+ const { data } = await fetcher.json(`${repoPath(repo)}/pullrequests`, {
1193
+ method: "POST",
1194
+ json: {
1195
+ sourceRefName: `refs/heads/${input.head}`,
1196
+ targetRefName: `refs/heads/${input.base}`,
1197
+ title: input.title,
1198
+ description: input.body,
1199
+ isDraft: input.draft ?? false,
1200
+ labels: input.labels?.map((name) => ({ name })),
1201
+ reviewers: input.assignees?.map((reviewer) => ({ id: typeof reviewer === "string" ? reviewer : reviewer.id }))
1202
+ },
1203
+ signal: writeOptions?.signal
1204
+ });
1205
+ return toPullThread({
1206
+ forge: FORGE,
1207
+ instance,
1208
+ repo,
1209
+ kind: "pull_request",
1210
+ number: String(data.pullRequestId)
1211
+ }, data);
1212
+ }),
1213
+ update: perKind({
1214
+ issue: true,
1215
+ pull_request: true
1216
+ }, async (thread, input, options) => {
1217
+ const ref = requireIssueOrPull(thread, context, "update");
1218
+ if (ref.kind === "issue") {
1219
+ const fields = {};
1220
+ if (input.title !== void 0) fields["System.Title"] = input.title;
1221
+ if (input.body !== void 0) fields["System.Description"] = input.body;
1222
+ return workItemThread(issueRef(ref.repo, ref.number), await patchWorkItem(ref, fields, options?.signal));
1223
+ }
1224
+ const { data } = await fetcher.json(pullPath(ref), {
1225
+ method: "PATCH",
1226
+ json: {
1227
+ title: input.title,
1228
+ description: input.body
1229
+ },
1230
+ signal: options?.signal
1231
+ });
1232
+ return toPullThread(ref, data);
1233
+ }),
1234
+ close: perKind(ISSUE_AND_PULL, (ref, options) => setState(ref, "closed", options)),
1235
+ reopen: perKind({
1236
+ issue: true,
1237
+ pull_request: true
1238
+ }, (ref, options) => setState(ref, "open", options)),
1239
+ setLabels: perKind({
1240
+ issue: true,
1241
+ pull_request: true
1242
+ }, async (thread, labels, options) => {
1243
+ const ref = requireIssueOrPull(thread, context, "label");
1244
+ if (ref.kind === "issue") {
1245
+ await patchWorkItem(ref, { "System.Tags": labels.join("; ") }, options?.signal);
1246
+ return;
1247
+ }
1248
+ const { data } = await fetcher.json(`${pullPath(ref)}/labels`, { signal: options?.signal });
1249
+ const current = data.value.map((label) => label.name);
1250
+ for (const label of data.value.filter((item) => !labels.includes(item.name))) await fetcher.raw(`${pullPath(ref)}/labels/${enc(label.id)}`, {
1251
+ method: "DELETE",
1252
+ signal: options?.signal
1253
+ });
1254
+ for (const name of labels.filter((item) => !current.includes(item))) await fetcher.raw(`${pullPath(ref)}/labels`, {
1255
+ method: "POST",
1256
+ json: { name },
1257
+ signal: options?.signal
1258
+ });
1259
+ }),
1260
+ setAssignees: perKind({ issue: true }, async (thread, assignees, options) => {
1261
+ if (assignees.length > 1) throw new UnsupportedOperationError("Azure DevOps work items take a single assignee", context);
1262
+ const assignee = assignees[0];
1263
+ await patchWorkItem(requireIssueOrPull(thread, context, "assign"), { "System.AssignedTo": assignee === void 0 ? "" : typeof assignee === "string" ? assignee : assignee.login }, options?.signal);
1264
+ }),
1265
+ requestReview: perKind({ pull_request: true }, async (thread, reviewers, options) => {
1266
+ const ref = requireThread(thread, context);
1267
+ for (const reviewer of reviewers) {
1268
+ const id = typeof reviewer === "string" ? reviewer : reviewer.id;
1269
+ await fetcher.raw(`${pullPath(ref)}/reviewers/${enc(id)}`, {
1270
+ method: "PUT",
1271
+ json: { vote: 0 },
1272
+ signal: options?.signal
1273
+ });
1274
+ }
1275
+ }),
1276
+ merge: verb(true, async (thread, mergeOptions = {}, hooks = {}) => {
1277
+ const ref = requireThread(thread, context);
1278
+ if (ref.kind !== "pull_request") throw new UnsupportedOperationError("Only pull requests can be completed", context);
1279
+ if (!mergeOptions.method) throw new MergeMethodRequiredError(Object.keys(MERGE_STRATEGIES), context);
1280
+ if (mergeOptions.method === "fast_forward_only") throw new UnsupportedOperationError("Azure DevOps does not support fast-forward-only completion", context);
1281
+ const message = mergeCommitMessage(mergeOptions);
1282
+ const completionOptions = {
1283
+ mergeStrategy: MERGE_STRATEGIES[mergeOptions.method],
1284
+ ...message ? { mergeCommitMessage: message } : {}
1285
+ };
1286
+ const id = await myId();
1287
+ await hooks.beforeMerge?.();
1288
+ if (mergeOptions.whenChecksPass) {
1289
+ await fetcher.raw(pullPath(ref), {
1290
+ method: "PATCH",
1291
+ json: {
1292
+ autoCompleteSetBy: { id },
1293
+ completionOptions
1294
+ },
1295
+ mapError: toMergeError,
1296
+ signal: mergeOptions.signal
1297
+ });
1298
+ return;
1299
+ }
1300
+ const sha = mergeOptions.sha ?? (await fetcher.json(pullPath(ref), { signal: mergeOptions.signal })).data.lastMergeSourceCommit?.commitId;
1301
+ await fetcher.raw(pullPath(ref), {
1302
+ method: "PATCH",
1303
+ json: {
1304
+ status: "completed",
1305
+ lastMergeSourceCommit: { commitId: sha },
1306
+ completionOptions
1307
+ },
1308
+ mapError: toMergeError,
1309
+ signal: mergeOptions.signal
1310
+ });
1311
+ }),
1312
+ checks: perKind({ pull_request: !anonymous && "experimental" }, async (thread, options) => {
1313
+ const ref = requireThread(thread, context);
1314
+ return { items: await pullChecks(ref, (await fetcher.json(pullPath(ref), { signal: options?.signal })).data, options?.signal) };
1315
+ }),
1316
+ reviewsPage: verb("emulated", async (thread, listOptions) => {
1317
+ const ref = requireThread(thread, context);
1318
+ const { data } = await fetcher.json(pullPath(ref), { signal: listOptions?.signal });
1319
+ return { items: (data.reviewers ?? []).filter((reviewer) => reviewer.vote !== 0).map((reviewer) => toVoteReview(ref, reviewer)) };
1320
+ }),
1321
+ createReview: verb("emulated", createReview),
1322
+ reviewThreads: verb(true, {
1323
+ resolveReviewThread: (thread, id, options) => setThreadStatus(thread, id, "closed", options),
1324
+ unresolveReviewThread: (thread, id, options) => setThreadStatus(thread, id, "active", options)
1325
+ })
1326
+ },
1327
+ web: azureWeb(baseUrl),
1328
+ webhooks: {},
1329
+ scopes: (verb) => verb.startsWith("webhooks.") ? {
1330
+ token: ["vso.work_full"],
1331
+ note: "Service hook subscriptions need project administrator rights"
1332
+ } : { token: ["vso.code", "vso.work"] }
1333
+ };
1334
+ }
1335
+ const AZURE_DEVOPS = {
1336
+ forge: FORGE,
1337
+ baseUrl: "https://dev.azure.com",
1338
+ anonymous: true,
1339
+ headers: { accept: "application/json" },
1340
+ query: { "api-version": "7.1" },
1341
+ authHeaders: ({ options: { auth } }) => {
1342
+ if (!auth || auth.type === "anonymous") return;
1343
+ return async () => ({ authorization: `Basic ${btoa(auth.type === "token" ? `:${await resolveToken(auth)}` : `${auth.username}:${auth.password}`)}` });
1344
+ },
1345
+ setup: setupAzure
1346
+ };
1347
+ /** Creates an Azure DevOps provider for one organization on dev.azure.com, or a collection on Azure DevOps Server. */
1348
+ const azureDevOps = /* @__PURE__ */ defineForgeProvider({
1349
+ ...AZURE_DEVOPS,
1350
+ webhooks: azureDevOpsWebhooks
1351
+ });
1352
+ /** `azureDevOps()` without webhook ingestion, for bundles that never receive a delivery. */
1353
+ const azureDevOpsLite = /* @__PURE__ */ defineForgeProvider(AZURE_DEVOPS);
1354
+ //#endregion
1355
+ export { azureDevOps as n, azureDevOpsLite as r, AZURE_DEVOPS_API_VERSION as t };