Skip to content

Commit 045425b

Browse files
authored
feat: Add support for custom collection URLs in Azure DevOps API interactions (#37)
1 parent 4a3371f commit 045425b

9 files changed

Lines changed: 400 additions & 81 deletions

File tree

src/azure-devops-api.ts

Lines changed: 110 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,31 @@ const API_VERSION = "7.1"
44
const FETCH_MAX_ATTEMPTS = 3
55
const FETCH_RETRY_DELAY_MS = 1000
66

7+
const DEFAULT_COLLECTION_URL = "https://dev.azure.com"
8+
9+
function isCloudCollectionUrl(collectionUrl: string): boolean {
10+
return collectionUrl.includes("dev.azure.com") || collectionUrl.includes("visualstudio.com")
11+
}
12+
13+
function buildApiUrl(
14+
collectionUrl: string | undefined,
15+
organization: string,
16+
project: string,
17+
path: string
18+
): string {
19+
const base = collectionUrl || DEFAULT_COLLECTION_URL
20+
21+
// For on-prem Azure DevOps Server (both old /tfs/ and new format),
22+
// the collection is already in the base URL
23+
// e.g., http://server:8080/tfs/Collection or http://server:8080/Collection
24+
if (!isCloudCollectionUrl(base)) {
25+
return `${base}/${project}/${path}`
26+
}
27+
28+
// For cloud Azure DevOps Services
29+
return `${base}/${organization}/${project}/${path}`
30+
}
31+
732
function isRetryableStatus(status: number): boolean {
833
return status === 408 || status === 429 || status >= 500
934
}
@@ -198,7 +223,7 @@ export async function getPullRequest(
198223
project: string,
199224
pullRequestId: number,
200225
personalAccessToken: string,
201-
options?: GetPullRequestOptions
226+
options?: GetPullRequestOptions & { collectionUrl?: string }
202227
): Promise<PullRequest> {
203228
const queryParams = new URLSearchParams({
204229
"api-version": API_VERSION,
@@ -208,7 +233,12 @@ export async function getPullRequest(
208233
queryParams.set("includeCommits", "true")
209234
}
210235

211-
const url = `https://dev.azure.com/${organization}/${project}/_apis/git/pullrequests/${pullRequestId}?${queryParams.toString()}`
236+
const url = buildApiUrl(
237+
options?.collectionUrl,
238+
organization,
239+
project,
240+
`_apis/git/pullrequests/${pullRequestId}?${queryParams.toString()}`
241+
)
212242
return makeRequest<PullRequest>(url, { method: "GET" }, personalAccessToken)
213243
}
214244

@@ -220,7 +250,7 @@ export async function createPullRequest(
220250
sourceRefName: string,
221251
targetRefName: string,
222252
title: string,
223-
options?: CreatePullRequestOptions
253+
options?: CreatePullRequestOptions & { collectionUrl?: string }
224254
): Promise<PullRequest> {
225255
const queryParams = new URLSearchParams({
226256
"api-version": API_VERSION,
@@ -230,7 +260,12 @@ export async function createPullRequest(
230260
queryParams.set("supportsIterations", String(options.supportsIterations))
231261
}
232262

233-
const url = `https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repositoryId}/pullrequests?${queryParams.toString()}`
263+
const url = buildApiUrl(
264+
options?.collectionUrl,
265+
organization,
266+
project,
267+
`_apis/git/repositories/${repositoryId}/pullrequests?${queryParams.toString()}`
268+
)
234269

235270
const body = {
236271
sourceRefName,
@@ -255,7 +290,8 @@ export async function listPullRequests(
255290
project: string,
256291
personalAccessToken: string,
257292
repositoryId?: string,
258-
searchCriteria?: SearchPullRequestsCriteria
293+
searchCriteria?: SearchPullRequestsCriteria,
294+
collectionUrl?: string
259295
): Promise<{ value: PullRequest[]; count: number }> {
260296
const queryParams = new URLSearchParams({
261297
"api-version": API_VERSION,
@@ -285,7 +321,12 @@ export async function listPullRequests(
285321
queryParams.set("searchCriteria.repositoryId", repositoryId)
286322
}
287323

288-
const url = `https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repositoryId || ""}/pullrequests?${queryParams.toString()}`
324+
const url = buildApiUrl(
325+
collectionUrl,
326+
organization,
327+
project,
328+
`_apis/git/repositories/${repositoryId || ""}/pullrequests?${queryParams.toString()}`
329+
)
289330
return makeRequest<{ value: PullRequest[]; count: number }>(
290331
url,
291332
{ method: "GET" },
@@ -300,9 +341,14 @@ export async function createPullRequestThread(
300341
pullRequestId: number,
301342
personalAccessToken: string,
302343
content: string,
303-
options?: CreateThreadOptions
344+
options?: CreateThreadOptions & { collectionUrl?: string }
304345
): Promise<PullRequestThread> {
305-
const url = `https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/threads?api-version=${API_VERSION}`
346+
const url = buildApiUrl(
347+
options?.collectionUrl,
348+
organization,
349+
project,
350+
`_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/threads?api-version=${API_VERSION}`
351+
)
306352

307353
const body: ThreadRequestBody = {
308354
comments: [
@@ -353,9 +399,15 @@ export async function addPullRequestComment(
353399
threadId: number,
354400
personalAccessToken: string,
355401
content: string,
356-
parentCommentId?: number
402+
parentCommentId?: number,
403+
collectionUrl?: string
357404
): Promise<Comment> {
358-
const url = `https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/threads/${threadId}/comments?api-version=${API_VERSION}`
405+
const url = buildApiUrl(
406+
collectionUrl,
407+
organization,
408+
project,
409+
`_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/threads/${threadId}/comments?api-version=${API_VERSION}`
410+
)
359411

360412
const body = {
361413
content,
@@ -381,9 +433,15 @@ export async function editPullRequestComment(
381433
threadId: number,
382434
commentId: number,
383435
personalAccessToken: string,
384-
content: string
436+
content: string,
437+
collectionUrl?: string
385438
): Promise<Comment> {
386-
const url = `https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/threads/${threadId}/comments/${commentId}?api-version=${API_VERSION}`
439+
const url = buildApiUrl(
440+
collectionUrl,
441+
organization,
442+
project,
443+
`_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/threads/${threadId}/comments/${commentId}?api-version=${API_VERSION}`
444+
)
387445

388446
return makeRequest<Comment>(
389447
url,
@@ -400,9 +458,15 @@ export async function getPullRequestThreads(
400458
project: string,
401459
repositoryId: string,
402460
pullRequestId: number,
403-
personalAccessToken: string
461+
personalAccessToken: string,
462+
collectionUrl?: string
404463
): Promise<{ value: PullRequestThread[]; count: number }> {
405-
const url = `https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/threads?api-version=${API_VERSION}`
464+
const url = buildApiUrl(
465+
collectionUrl,
466+
organization,
467+
project,
468+
`_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/threads?api-version=${API_VERSION}`
469+
)
406470
return makeRequest<{ value: PullRequestThread[]; count: number }>(
407471
url,
408472
{ method: "GET" },
@@ -416,9 +480,15 @@ export async function getPullRequestThread(
416480
repositoryId: string,
417481
pullRequestId: number,
418482
threadId: number,
419-
personalAccessToken: string
483+
personalAccessToken: string,
484+
collectionUrl?: string
420485
): Promise<PullRequestThread> {
421-
const url = `https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/threads/${threadId}?api-version=${API_VERSION}`
486+
const url = buildApiUrl(
487+
collectionUrl,
488+
organization,
489+
project,
490+
`_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/threads/${threadId}?api-version=${API_VERSION}`
491+
)
422492
return makeRequest<PullRequestThread>(url, { method: "GET" }, personalAccessToken)
423493
}
424494

@@ -427,9 +497,15 @@ export async function getPullRequestCommits(
427497
project: string,
428498
repositoryId: string,
429499
pullRequestId: number,
430-
personalAccessToken: string
500+
personalAccessToken: string,
501+
collectionUrl?: string
431502
): Promise<GitCommitRef[]> {
432-
const url = `https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/commits?api-version=${API_VERSION}`
503+
const url = buildApiUrl(
504+
collectionUrl,
505+
organization,
506+
project,
507+
`_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/commits?api-version=${API_VERSION}`
508+
)
433509
return makeRequest<GitCommitRef[]>(url, { method: "GET" }, personalAccessToken)
434510
}
435511

@@ -439,9 +515,15 @@ export async function getPullRequestIterationChanges(
439515
repositoryId: string,
440516
pullRequestId: number,
441517
iterationId: number,
442-
personalAccessToken: string
518+
personalAccessToken: string,
519+
collectionUrl?: string
443520
): Promise<{ changeEntries: GitPullRequestChange[] }> {
444-
const url = `https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/iterations/${iterationId}/changes?api-version=${API_VERSION}`
521+
const url = buildApiUrl(
522+
collectionUrl,
523+
organization,
524+
project,
525+
`_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/iterations/${iterationId}/changes?api-version=${API_VERSION}`
526+
)
445527
return makeRequest<{ changeEntries: GitPullRequestChange[] }>(
446528
url,
447529
{ method: "GET" },
@@ -454,9 +536,15 @@ export async function getPullRequestIterations(
454536
project: string,
455537
repositoryId: string,
456538
pullRequestId: number,
457-
personalAccessToken: string
539+
personalAccessToken: string,
540+
collectionUrl?: string
458541
): Promise<{ value: Array<{ id: number; description: string }>; count: number }> {
459-
const url = `https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/iterations?api-version=${API_VERSION}`
542+
const url = buildApiUrl(
543+
collectionUrl,
544+
organization,
545+
project,
546+
`_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/iterations?api-version=${API_VERSION}`
547+
)
460548
return makeRequest<{ value: Array<{ id: number; description: string }>; count: number }>(
461549
url,
462550
{ method: "GET" },

src/code-review.ts

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export async function runCodeReview(config: ResolvedRunConfig): Promise<void> {
7979
triggerContext,
8080
opencodeConfig,
8181
reviewPrompt,
82+
collectionUrl,
8283
} = config
8384
const { organization, project, repositoryId } = repository
8485
const { pullRequestId, threadId, commentId } = context
@@ -97,7 +98,7 @@ export async function runCodeReview(config: ResolvedRunConfig): Promise<void> {
9798
validateTrigger(comment!.content, "review")
9899
}
99100

100-
const footer = getCommentFooter(organization, project, buildId)
101+
const footer = getCommentFooter(collectionUrl, organization, project, buildId)
101102
let contextThreads: PullRequestThreadType[] = []
102103

103104
if (!commentTrigger) {
@@ -109,16 +110,27 @@ export async function runCodeReview(config: ResolvedRunConfig): Promise<void> {
109110
threadId!,
110111
pat,
111112
`Reviewing pull request...${footer}`,
112-
commentId
113+
commentId,
114+
collectionUrl
113115
)
114116
replyCommentId = replyComment.id || null
115117
console.log("Added 'reviewing pull request' reply")
116118
}
117119

118120
const [pr, iterationsData, allThreads] = await Promise.all([
119-
getPullRequest(organization, project, pullRequestId, pat, { includeCommits: true }),
120-
getPullRequestIterations(organization, project, repositoryId, pullRequestId, pat),
121-
getPullRequestThreads(organization, project, repositoryId, pullRequestId, pat),
121+
getPullRequest(organization, project, pullRequestId, pat, {
122+
includeCommits: true,
123+
collectionUrl,
124+
}),
125+
getPullRequestIterations(
126+
organization,
127+
project,
128+
repositoryId,
129+
pullRequestId,
130+
pat,
131+
collectionUrl
132+
),
133+
getPullRequestThreads(organization, project, repositoryId, pullRequestId, pat, collectionUrl),
122134
])
123135

124136
contextThreads = allThreads.value
@@ -130,7 +142,8 @@ export async function runCodeReview(config: ResolvedRunConfig): Promise<void> {
130142
repositoryId,
131143
pullRequestId,
132144
latestIterationId,
133-
pat
145+
pat,
146+
collectionUrl
134147
)
135148

136149
if (skipClone) {
@@ -149,6 +162,7 @@ export async function runCodeReview(config: ResolvedRunConfig): Promise<void> {
149162
branch: sourceBranch,
150163
pat,
151164
workspacePath,
165+
collectionUrl,
152166
})
153167
cleanupWorkspaceDir = true
154168
}
@@ -171,6 +185,9 @@ export async function runCodeReview(config: ResolvedRunConfig): Promise<void> {
171185
process.env["AZURE_DEVOPS_REPO_ID"] = repositoryId
172186
process.env["AZURE_DEVOPS_PR_ID"] = String(pullRequestId)
173187
process.env["AZURE_DEVOPS_PAT"] = pat
188+
if (collectionUrl) {
189+
process.env["AZURE_DEVOPS_COLLECTION_URL"] = collectionUrl
190+
}
174191

175192
opencode = createOpencodeInstance(workspace)
176193

@@ -193,13 +210,14 @@ export async function runCodeReview(config: ResolvedRunConfig): Promise<void> {
193210
threadId,
194211
replyCommentId,
195212
pat,
196-
`${response}${footer}`
213+
`${response}${footer}`,
214+
collectionUrl
197215
)
198216
}
199217
} catch (err) {
200218
console.error("Error during review mode run:", (err as Error).message)
201219

202-
const footer = getCommentFooter(organization, project, buildId)
220+
const footer = getCommentFooter(collectionUrl, organization, project, buildId)
203221
const errorMessage = `## OpenCode Review Summary\n\nReview failed: ${(err as Error).message}${footer}`
204222

205223
if (!commentTrigger && replyCommentId && threadId) {
@@ -211,7 +229,8 @@ export async function runCodeReview(config: ResolvedRunConfig): Promise<void> {
211229
threadId,
212230
replyCommentId,
213231
pat,
214-
errorMessage
232+
errorMessage,
233+
collectionUrl
215234
)
216235
} else {
217236
await createPullRequestThread(
@@ -221,7 +240,7 @@ export async function runCodeReview(config: ResolvedRunConfig): Promise<void> {
221240
pullRequestId,
222241
pat,
223242
errorMessage,
224-
{ status: "fixed" }
243+
{ status: "fixed", collectionUrl }
225244
)
226245
}
227246

0 commit comments

Comments
 (0)