Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 110 additions & 22 deletions src/azure-devops-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,31 @@ const API_VERSION = "7.1"
const FETCH_MAX_ATTEMPTS = 3
const FETCH_RETRY_DELAY_MS = 1000

const DEFAULT_COLLECTION_URL = "https://dev.azure.com"

function isCloudCollectionUrl(collectionUrl: string): boolean {
return collectionUrl.includes("dev.azure.com") || collectionUrl.includes("visualstudio.com")
}

function buildApiUrl(
collectionUrl: string | undefined,
organization: string,
project: string,
path: string
): string {
const base = collectionUrl || DEFAULT_COLLECTION_URL

// For on-prem Azure DevOps Server (both old /tfs/ and new format),
// the collection is already in the base URL
// e.g., http://server:8080/tfs/Collection or http://server:8080/Collection
if (!isCloudCollectionUrl(base)) {
return `${base}/${project}/${path}`
}

// For cloud Azure DevOps Services
return `${base}/${organization}/${project}/${path}`
}

function isRetryableStatus(status: number): boolean {
return status === 408 || status === 429 || status >= 500
}
Expand Down Expand Up @@ -198,7 +223,7 @@ export async function getPullRequest(
project: string,
pullRequestId: number,
personalAccessToken: string,
options?: GetPullRequestOptions
options?: GetPullRequestOptions & { collectionUrl?: string }
): Promise<PullRequest> {
const queryParams = new URLSearchParams({
"api-version": API_VERSION,
Expand All @@ -208,7 +233,12 @@ export async function getPullRequest(
queryParams.set("includeCommits", "true")
}

const url = `https://dev.azure.com/${organization}/${project}/_apis/git/pullrequests/${pullRequestId}?${queryParams.toString()}`
const url = buildApiUrl(
options?.collectionUrl,
organization,
project,
`_apis/git/pullrequests/${pullRequestId}?${queryParams.toString()}`
)
return makeRequest<PullRequest>(url, { method: "GET" }, personalAccessToken)
}

Expand All @@ -220,7 +250,7 @@ export async function createPullRequest(
sourceRefName: string,
targetRefName: string,
title: string,
options?: CreatePullRequestOptions
options?: CreatePullRequestOptions & { collectionUrl?: string }
): Promise<PullRequest> {
const queryParams = new URLSearchParams({
"api-version": API_VERSION,
Expand All @@ -230,7 +260,12 @@ export async function createPullRequest(
queryParams.set("supportsIterations", String(options.supportsIterations))
}

const url = `https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repositoryId}/pullrequests?${queryParams.toString()}`
const url = buildApiUrl(
options?.collectionUrl,
organization,
project,
`_apis/git/repositories/${repositoryId}/pullrequests?${queryParams.toString()}`
)

const body = {
sourceRefName,
Expand All @@ -255,7 +290,8 @@ export async function listPullRequests(
project: string,
personalAccessToken: string,
repositoryId?: string,
searchCriteria?: SearchPullRequestsCriteria
searchCriteria?: SearchPullRequestsCriteria,
collectionUrl?: string
): Promise<{ value: PullRequest[]; count: number }> {
const queryParams = new URLSearchParams({
"api-version": API_VERSION,
Expand Down Expand Up @@ -285,7 +321,12 @@ export async function listPullRequests(
queryParams.set("searchCriteria.repositoryId", repositoryId)
}

const url = `https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repositoryId || ""}/pullrequests?${queryParams.toString()}`
const url = buildApiUrl(
collectionUrl,
organization,
project,
`_apis/git/repositories/${repositoryId || ""}/pullrequests?${queryParams.toString()}`
)
return makeRequest<{ value: PullRequest[]; count: number }>(
url,
{ method: "GET" },
Expand All @@ -300,9 +341,14 @@ export async function createPullRequestThread(
pullRequestId: number,
personalAccessToken: string,
content: string,
options?: CreateThreadOptions
options?: CreateThreadOptions & { collectionUrl?: string }
): Promise<PullRequestThread> {
const url = `https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/threads?api-version=${API_VERSION}`
const url = buildApiUrl(
options?.collectionUrl,
organization,
project,
`_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/threads?api-version=${API_VERSION}`
)

const body: ThreadRequestBody = {
comments: [
Expand Down Expand Up @@ -353,9 +399,15 @@ export async function addPullRequestComment(
threadId: number,
personalAccessToken: string,
content: string,
parentCommentId?: number
parentCommentId?: number,
collectionUrl?: string
): Promise<Comment> {
const url = `https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/threads/${threadId}/comments?api-version=${API_VERSION}`
const url = buildApiUrl(
collectionUrl,
organization,
project,
`_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/threads/${threadId}/comments?api-version=${API_VERSION}`
)

const body = {
content,
Expand All @@ -381,9 +433,15 @@ export async function editPullRequestComment(
threadId: number,
commentId: number,
personalAccessToken: string,
content: string
content: string,
collectionUrl?: string
): Promise<Comment> {
const url = `https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/threads/${threadId}/comments/${commentId}?api-version=${API_VERSION}`
const url = buildApiUrl(
collectionUrl,
organization,
project,
`_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/threads/${threadId}/comments/${commentId}?api-version=${API_VERSION}`
)

return makeRequest<Comment>(
url,
Expand All @@ -400,9 +458,15 @@ export async function getPullRequestThreads(
project: string,
repositoryId: string,
pullRequestId: number,
personalAccessToken: string
personalAccessToken: string,
collectionUrl?: string
): Promise<{ value: PullRequestThread[]; count: number }> {
const url = `https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/threads?api-version=${API_VERSION}`
const url = buildApiUrl(
collectionUrl,
organization,
project,
`_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/threads?api-version=${API_VERSION}`
)
return makeRequest<{ value: PullRequestThread[]; count: number }>(
url,
{ method: "GET" },
Expand All @@ -416,9 +480,15 @@ export async function getPullRequestThread(
repositoryId: string,
pullRequestId: number,
threadId: number,
personalAccessToken: string
personalAccessToken: string,
collectionUrl?: string
): Promise<PullRequestThread> {
const url = `https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/threads/${threadId}?api-version=${API_VERSION}`
const url = buildApiUrl(
collectionUrl,
organization,
project,
`_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/threads/${threadId}?api-version=${API_VERSION}`
)
return makeRequest<PullRequestThread>(url, { method: "GET" }, personalAccessToken)
}

Expand All @@ -427,9 +497,15 @@ export async function getPullRequestCommits(
project: string,
repositoryId: string,
pullRequestId: number,
personalAccessToken: string
personalAccessToken: string,
collectionUrl?: string
): Promise<GitCommitRef[]> {
const url = `https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/commits?api-version=${API_VERSION}`
const url = buildApiUrl(
collectionUrl,
organization,
project,
`_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/commits?api-version=${API_VERSION}`
)
return makeRequest<GitCommitRef[]>(url, { method: "GET" }, personalAccessToken)
}

Expand All @@ -439,9 +515,15 @@ export async function getPullRequestIterationChanges(
repositoryId: string,
pullRequestId: number,
iterationId: number,
personalAccessToken: string
personalAccessToken: string,
collectionUrl?: string
): Promise<{ changeEntries: GitPullRequestChange[] }> {
const url = `https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/iterations/${iterationId}/changes?api-version=${API_VERSION}`
const url = buildApiUrl(
collectionUrl,
organization,
project,
`_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/iterations/${iterationId}/changes?api-version=${API_VERSION}`
)
return makeRequest<{ changeEntries: GitPullRequestChange[] }>(
url,
{ method: "GET" },
Expand All @@ -454,9 +536,15 @@ export async function getPullRequestIterations(
project: string,
repositoryId: string,
pullRequestId: number,
personalAccessToken: string
personalAccessToken: string,
collectionUrl?: string
): Promise<{ value: Array<{ id: number; description: string }>; count: number }> {
const url = `https://dev.azure.com/${organization}/${project}/_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/iterations?api-version=${API_VERSION}`
const url = buildApiUrl(
collectionUrl,
organization,
project,
`_apis/git/repositories/${repositoryId}/pullRequests/${pullRequestId}/iterations?api-version=${API_VERSION}`
)
return makeRequest<{ value: Array<{ id: number; description: string }>; count: number }>(
url,
{ method: "GET" },
Expand Down
39 changes: 29 additions & 10 deletions src/code-review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export async function runCodeReview(config: ResolvedRunConfig): Promise<void> {
triggerContext,
opencodeConfig,
reviewPrompt,
collectionUrl,
} = config
const { organization, project, repositoryId } = repository
const { pullRequestId, threadId, commentId } = context
Expand All @@ -97,7 +98,7 @@ export async function runCodeReview(config: ResolvedRunConfig): Promise<void> {
validateTrigger(comment!.content, "review")
}

const footer = getCommentFooter(organization, project, buildId)
const footer = getCommentFooter(collectionUrl, organization, project, buildId)
let contextThreads: PullRequestThreadType[] = []

if (!commentTrigger) {
Expand All @@ -109,16 +110,27 @@ export async function runCodeReview(config: ResolvedRunConfig): Promise<void> {
threadId!,
pat,
`Reviewing pull request...${footer}`,
commentId
commentId,
collectionUrl
)
replyCommentId = replyComment.id || null
console.log("Added 'reviewing pull request' reply")
}

const [pr, iterationsData, allThreads] = await Promise.all([
getPullRequest(organization, project, pullRequestId, pat, { includeCommits: true }),
getPullRequestIterations(organization, project, repositoryId, pullRequestId, pat),
getPullRequestThreads(organization, project, repositoryId, pullRequestId, pat),
getPullRequest(organization, project, pullRequestId, pat, {
includeCommits: true,
collectionUrl,
}),
getPullRequestIterations(
organization,
project,
repositoryId,
pullRequestId,
pat,
collectionUrl
),
getPullRequestThreads(organization, project, repositoryId, pullRequestId, pat, collectionUrl),
])

contextThreads = allThreads.value
Expand All @@ -130,7 +142,8 @@ export async function runCodeReview(config: ResolvedRunConfig): Promise<void> {
repositoryId,
pullRequestId,
latestIterationId,
pat
pat,
collectionUrl
)

if (skipClone) {
Expand All @@ -149,6 +162,7 @@ export async function runCodeReview(config: ResolvedRunConfig): Promise<void> {
branch: sourceBranch,
pat,
workspacePath,
collectionUrl,
})
cleanupWorkspaceDir = true
}
Expand All @@ -171,6 +185,9 @@ export async function runCodeReview(config: ResolvedRunConfig): Promise<void> {
process.env["AZURE_DEVOPS_REPO_ID"] = repositoryId
process.env["AZURE_DEVOPS_PR_ID"] = String(pullRequestId)
process.env["AZURE_DEVOPS_PAT"] = pat
if (collectionUrl) {
process.env["AZURE_DEVOPS_COLLECTION_URL"] = collectionUrl
}

opencode = createOpencodeInstance(workspace)

Expand All @@ -193,13 +210,14 @@ export async function runCodeReview(config: ResolvedRunConfig): Promise<void> {
threadId,
replyCommentId,
pat,
`${response}${footer}`
`${response}${footer}`,
collectionUrl
)
}
} catch (err) {
console.error("Error during review mode run:", (err as Error).message)

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

if (!commentTrigger && replyCommentId && threadId) {
Expand All @@ -211,7 +229,8 @@ export async function runCodeReview(config: ResolvedRunConfig): Promise<void> {
threadId,
replyCommentId,
pat,
errorMessage
errorMessage,
collectionUrl
)
} else {
await createPullRequestThread(
Expand All @@ -221,7 +240,7 @@ export async function runCodeReview(config: ResolvedRunConfig): Promise<void> {
pullRequestId,
pat,
errorMessage,
{ status: "fixed" }
{ status: "fixed", collectionUrl }
)
}

Expand Down
Loading
Loading