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.
- package/LICENCE +21 -0
- package/README.md +72 -0
- package/dist/azure-devops/index.d.mts +2 -0
- package/dist/azure-devops/index.mjs +1 -0
- package/dist/azure-devops-DwQvL4TV.mjs +1355 -0
- package/dist/bitbucket/index.d.mts +2 -0
- package/dist/bitbucket/index.mjs +1 -0
- package/dist/bitbucket-CpcfJlYi.mjs +1171 -0
- package/dist/crypto-CQNEyYHD.mjs +156 -0
- package/dist/cursor-origin/index.d.mts +2 -0
- package/dist/cursor-origin/index.mjs +1 -0
- package/dist/cursor-origin-BhqYx2oW.mjs +980 -0
- package/dist/define-B2_4ob2i.d.mts +307 -0
- package/dist/define-D3yEXtKK.mjs +2276 -0
- package/dist/env.d.mts +46 -0
- package/dist/env.mjs +283 -0
- package/dist/fake/index.d.mts +227 -0
- package/dist/fake/index.mjs +1001 -0
- package/dist/fetch-D4pSRvvA.d.mts +1723 -0
- package/dist/forgejo/index.d.mts +2 -0
- package/dist/forgejo/index.mjs +1 -0
- package/dist/forgejo-BCefbHUN.mjs +2005 -0
- package/dist/gitea/index.d.mts +2 -0
- package/dist/gitea/index.mjs +22 -0
- package/dist/gitee/index.d.mts +2 -0
- package/dist/gitee/index.mjs +1 -0
- package/dist/gitee-CA-f7Rs8.mjs +1272 -0
- package/dist/github/index.d.mts +2 -0
- package/dist/github/index.mjs +1 -0
- package/dist/github-B0umL8_f.mjs +3259 -0
- package/dist/gitlab/index.d.mts +2 -0
- package/dist/gitlab/index.mjs +1 -0
- package/dist/gitlab-Bsr__Z0C.mjs +2378 -0
- package/dist/graphql-D7uGzZTf.mjs +366 -0
- package/dist/index-BQ6VS31s.d.mts +32 -0
- package/dist/index-C-jz9Mo3.d.mts +16 -0
- package/dist/index-C4fYzmK3.d.mts +102 -0
- package/dist/index-CQiIX4SH.d.mts +27 -0
- package/dist/index-CTj5gQzl.d.mts +35 -0
- package/dist/index-CliBWzOF.d.mts +23 -0
- package/dist/index-Cz_y-t3d.d.mts +36 -0
- package/dist/index-DEt25E1w.d.mts +77 -0
- package/dist/index-DIuS0EOy.d.mts +43 -0
- package/dist/index-uSpsMCrC.d.mts +26 -0
- package/dist/index.d.mts +41 -0
- package/dist/index.mjs +13 -0
- package/dist/kit.d.mts +183 -0
- package/dist/kit.mjs +4 -0
- package/dist/provider-CoZ7cCxp.d.mts +1439 -0
- package/dist/provider-Ed1YhkQN.mjs +169 -0
- package/dist/pushin/index.d.mts +2 -0
- package/dist/pushin/index.mjs +1 -0
- package/dist/pushin-CkxgJ2cU.mjs +343 -0
- package/dist/schema/Actor.json +96 -0
- package/dist/schema/AlertSupport.json +49 -0
- package/dist/schema/AuthKind.json +15 -0
- package/dist/schema/Branch.json +31 -0
- package/dist/schema/ChangedFile.json +58 -0
- package/dist/schema/Check.json +169 -0
- package/dist/schema/CheckRef.json +114 -0
- package/dist/schema/CheckState.json +14 -0
- package/dist/schema/ChecksSummary.json +34 -0
- package/dist/schema/CiJob.json +186 -0
- package/dist/schema/CiJobRef.json +132 -0
- package/dist/schema/CiRun.json +234 -0
- package/dist/schema/CiRunRef.json +103 -0
- package/dist/schema/CodeFragment.json +19 -0
- package/dist/schema/CodeMatch.json +128 -0
- package/dist/schema/Collaborator.json +169 -0
- package/dist/schema/Comment.json +336 -0
- package/dist/schema/CommentRef.json +158 -0
- package/dist/schema/Commit.json +307 -0
- package/dist/schema/CommitRef.json +104 -0
- package/dist/schema/CommitSignature.json +118 -0
- package/dist/schema/Comparison.json +355 -0
- package/dist/schema/Cursor.json +21 -0
- package/dist/schema/EventAction.json +33 -0
- package/dist/schema/EventDetail.json +649 -0
- package/dist/schema/EventKind.json +26 -0
- package/dist/schema/FileContent.json +73 -0
- package/dist/schema/FileStatus.json +15 -0
- package/dist/schema/ForgeCapabilities.json +737 -0
- package/dist/schema/ForgeEvent.json +789 -0
- package/dist/schema/ForgeInstance.json +7 -0
- package/dist/schema/ForgeKind.json +32 -0
- package/dist/schema/ForgeWarning.json +49 -0
- package/dist/schema/ForgeWarningCode.json +7 -0
- package/dist/schema/GetManyResult.json +699 -0
- package/dist/schema/Installation.json +131 -0
- package/dist/schema/InstallationToken.json +31 -0
- package/dist/schema/KnownForgeKind.json +19 -0
- package/dist/schema/Label.json +22 -0
- package/dist/schema/MergeMethod.json +14 -0
- package/dist/schema/Milestone.json +40 -0
- package/dist/schema/Notification.json +498 -0
- package/dist/schema/NotificationReason.json +28 -0
- package/dist/schema/NotificationRef.json +60 -0
- package/dist/schema/NotificationSubject.json +376 -0
- package/dist/schema/PerKind.json +49 -0
- package/dist/schema/PullBranches.json +125 -0
- package/dist/schema/PushCommit.json +24 -0
- package/dist/schema/Reaction.json +148 -0
- package/dist/schema/ReactionContent.json +17 -0
- package/dist/schema/ReactionSummary.json +70 -0
- package/dist/schema/Release.json +286 -0
- package/dist/schema/ReleaseAsset.json +169 -0
- package/dist/schema/ReleaseAssetRef.json +138 -0
- package/dist/schema/ReleaseRef.json +107 -0
- package/dist/schema/Repo.json +354 -0
- package/dist/schema/RepoFeatures.json +35 -0
- package/dist/schema/RepoPermissions.json +35 -0
- package/dist/schema/RepoRef.json +77 -0
- package/dist/schema/RepoRole.json +15 -0
- package/dist/schema/RepoVisibility.json +12 -0
- package/dist/schema/Review.json +371 -0
- package/dist/schema/ReviewComment.json +280 -0
- package/dist/schema/ReviewCommentInput.json +34 -0
- package/dist/schema/ReviewRef.json +158 -0
- package/dist/schema/ReviewState.json +15 -0
- package/dist/schema/Reviewer.json +133 -0
- package/dist/schema/SecurityAlert.json +219 -0
- package/dist/schema/SecurityAlertKind.json +14 -0
- package/dist/schema/SecurityAlertRef.json +122 -0
- package/dist/schema/SecuritySeverity.json +14 -0
- package/dist/schema/SourceKind.json +12 -0
- package/dist/schema/SubscriptionState.json +12 -0
- package/dist/schema/Support.json +23 -0
- package/dist/schema/Tag.json +25 -0
- package/dist/schema/TextLimits.json +18 -0
- package/dist/schema/Thread.json +653 -0
- package/dist/schema/ThreadKind.json +14 -0
- package/dist/schema/ThreadRef.json +131 -0
- package/dist/schema/ThreadStack.json +146 -0
- package/dist/schema/ThreadState.json +13 -0
- package/dist/schema/TreeEntry.json +46 -0
- package/dist/schema/TreeEntryType.json +13 -0
- package/dist/schema/UpsertCommentResult.json +355 -0
- package/dist/schema/User.json +123 -0
- package/dist/schema/Webhook.json +188 -0
- package/dist/schema/WebhookDeliveryRecord.json +173 -0
- package/dist/schema/WebhookDeliveryRef.json +135 -0
- package/dist/schema/WebhookRef.json +105 -0
- package/dist/schema/index.d.mts +4 -0
- package/dist/schema/index.mjs +3219 -0
- package/dist/tangled/index.d.mts +2 -0
- package/dist/tangled/index.mjs +1 -0
- package/dist/tangled-ChdPmiv-.mjs +1570 -0
- package/dist/testing/index.d.mts +123 -0
- package/dist/testing/index.mjs +242 -0
- package/dist/webhooks-rn9-8lxT.mjs +70 -0
- package/package.json +115 -3
- 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 };
|