mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
fix(azure-devops): retry with -preview api-version and keep project-level Git base for on-prem Server (STA-3494) (#12832)
* fix(azure-devops): retry with -preview api-version and keep project-level Git base for on-prem Server (STA-3494) Azure DevOps Server rejects api-version=7.1 with 400 VssInvalidPreviewVersionException unless the -preview suffix is supplied, so auth and every Git endpoint failed. Retry once with -preview on that rejection and remember the requirement per origin. Also stop letting a same-origin ORCA_AZURE_DEVOPS_API_BASE_URL (collection-level, needed only for the connectionData auth probe) override the project-level base derived from the remote for Git endpoints; cross-origin (proxy) overrides keep working. * fix(azure-devops): constrain preview retry and base override
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
_resetAzureDevOpsPreviewApiVersionCache,
|
||||
requestAzureDevOpsJson,
|
||||
requestAzureDevOpsJsonAtBase
|
||||
} from './azure-devops-api-request'
|
||||
import type { AzureDevOpsRepoRef } from './repository-ref'
|
||||
|
||||
const OLD_ENV = process.env
|
||||
const OLD_FETCH = globalThis.fetch
|
||||
|
||||
const SERVER_BASE = 'https://ado.example.com:8443/tfs/MyCollection'
|
||||
|
||||
function previewRejection(): Response {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
message:
|
||||
'The requested version "7.1" of the resource is under preview. The -preview flag must be supplied in the api-version for such requests. For example: "7.1-preview"',
|
||||
typeKey: 'VssInvalidPreviewVersionException'
|
||||
}),
|
||||
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
}
|
||||
|
||||
function serverRepoRef(): AzureDevOpsRepoRef {
|
||||
return {
|
||||
host: 'ado.example.com',
|
||||
organization: null,
|
||||
project: 'MyProject',
|
||||
repository: 'my-repo',
|
||||
apiBaseUrl: `${SERVER_BASE}/MyProject`,
|
||||
webBaseUrl: `${SERVER_BASE}/MyProject/_git/my-repo`
|
||||
}
|
||||
}
|
||||
|
||||
describe('Azure DevOps API request (STA-3494)', () => {
|
||||
beforeEach(() => {
|
||||
process.env = { ...OLD_ENV, ORCA_AZURE_DEVOPS_TOKEN: 'pat-token' }
|
||||
delete process.env.ORCA_AZURE_DEVOPS_API_BASE_URL
|
||||
_resetAzureDevOpsPreviewApiVersionCache()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env = OLD_ENV
|
||||
globalThis.fetch = OLD_FETCH
|
||||
})
|
||||
|
||||
it('retries with -preview when Azure DevOps Server rejects the api-version', async () => {
|
||||
const versions: (string | null)[] = []
|
||||
globalThis.fetch = vi.fn(async (input: string | URL | Request) => {
|
||||
const url = new URL(String(input))
|
||||
versions.push(url.searchParams.get('api-version'))
|
||||
if (!url.searchParams.get('api-version')?.endsWith('-preview')) {
|
||||
return previewRejection()
|
||||
}
|
||||
return Response.json({ authenticatedUser: { providerDisplayName: 'Server User' } })
|
||||
}) as never
|
||||
|
||||
await expect(
|
||||
requestAzureDevOpsJsonAtBase(SERVER_BASE, '/_apis/connectionData')
|
||||
).resolves.toEqual({ authenticatedUser: { providerDisplayName: 'Server User' } })
|
||||
expect(versions).toEqual(['7.1', '7.1-preview'])
|
||||
})
|
||||
|
||||
it('remembers the -preview requirement per origin after the first rejection', async () => {
|
||||
const versions: (string | null)[] = []
|
||||
globalThis.fetch = vi.fn(async (input: string | URL | Request) => {
|
||||
const url = new URL(String(input))
|
||||
versions.push(url.searchParams.get('api-version'))
|
||||
if (!url.searchParams.get('api-version')?.endsWith('-preview')) {
|
||||
return previewRejection()
|
||||
}
|
||||
return Response.json({ ok: true })
|
||||
}) as never
|
||||
|
||||
const base = 'https://ado-sticky.example.com/tfs/MyCollection'
|
||||
await requestAzureDevOpsJsonAtBase(base, '/_apis/connectionData')
|
||||
await requestAzureDevOpsJsonAtBase(base, '/_apis/connectionData')
|
||||
// First request learns the suffix; the second must not repeat the 400 round trip.
|
||||
expect(versions).toEqual(['7.1', '7.1-preview', '7.1-preview'])
|
||||
})
|
||||
|
||||
it('does not retry a 400 that is not a preview-version rejection', async () => {
|
||||
const fetchMock = vi.fn(async () =>
|
||||
Response.json({ message: 'A project name is required.' }, { status: 400 })
|
||||
)
|
||||
globalThis.fetch = fetchMock as never
|
||||
|
||||
await expect(
|
||||
requestAzureDevOpsJsonAtBase(
|
||||
'https://ado-other.example.com/tfs/Coll',
|
||||
'/_apis/connectionData'
|
||||
)
|
||||
).resolves.toBeNull()
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('uses the remote-derived project base for Git endpoints when the configured base shares its origin', async () => {
|
||||
process.env.ORCA_AZURE_DEVOPS_API_BASE_URL = SERVER_BASE
|
||||
const paths: string[] = []
|
||||
globalThis.fetch = vi.fn(async (input: string | URL | Request) => {
|
||||
paths.push(new URL(String(input)).pathname)
|
||||
return Response.json({ id: 'repo-guid' })
|
||||
}) as never
|
||||
|
||||
await requestAzureDevOpsJson(serverRepoRef(), '/_apis/git/repositories/my-repo')
|
||||
// Collection-level env base must not strip the project segment Git endpoints need.
|
||||
expect(paths).toEqual(['/tfs/MyCollection/MyProject/_apis/git/repositories/my-repo'])
|
||||
})
|
||||
|
||||
it('keeps a cross-origin configured base URL as an override for Git endpoints', async () => {
|
||||
process.env.ORCA_AZURE_DEVOPS_API_BASE_URL = 'http://127.0.0.1:8123/acme/Project'
|
||||
const origins: string[] = []
|
||||
globalThis.fetch = vi.fn(async (input: string | URL | Request) => {
|
||||
origins.push(new URL(String(input)).origin)
|
||||
return Response.json({ id: 'repo-guid' })
|
||||
}) as never
|
||||
|
||||
await requestAzureDevOpsJson(serverRepoRef(), '/_apis/git/repositories/my-repo')
|
||||
expect(origins).toEqual(['http://127.0.0.1:8123'])
|
||||
})
|
||||
|
||||
it('keeps a same-origin non-ancestor base URL as a Git endpoint override', async () => {
|
||||
process.env.ORCA_AZURE_DEVOPS_API_BASE_URL = 'https://ado.example.com:8443/rewrite/MyProject'
|
||||
const paths: string[] = []
|
||||
globalThis.fetch = vi.fn(async (input: string | URL | Request) => {
|
||||
paths.push(new URL(String(input)).pathname)
|
||||
return Response.json({ id: 'repo-guid' })
|
||||
}) as never
|
||||
|
||||
await requestAzureDevOpsJson(serverRepoRef(), '/_apis/git/repositories/my-repo')
|
||||
expect(paths).toEqual(['/rewrite/MyProject/_apis/git/repositories/my-repo'])
|
||||
})
|
||||
})
|
||||
@@ -3,6 +3,42 @@ import type { AzureDevOpsRepoRef } from './repository-ref'
|
||||
import { cancelUnreadResponseBody } from '../lib/unread-response-body'
|
||||
|
||||
const REQUEST_TIMEOUT_MS = 5000
|
||||
const DEFAULT_API_VERSION = '7.1'
|
||||
|
||||
// Why (STA-3494): on-prem Azure DevOps Server rejects versioned requests without
|
||||
// the -preview suffix; remember which origins need it after the first rejection.
|
||||
const previewApiVersionOrigins = new Set<string>()
|
||||
|
||||
/** @internal - exposed for tests only */
|
||||
export function _resetAzureDevOpsPreviewApiVersionCache(): void {
|
||||
previewApiVersionOrigins.clear()
|
||||
}
|
||||
|
||||
export function markAzureDevOpsPreviewApiVersionOrigin(origin: string): void {
|
||||
previewApiVersionOrigins.add(origin)
|
||||
}
|
||||
|
||||
export function azureDevOpsApiVersionForOrigin(
|
||||
origin: string,
|
||||
requested?: string | number
|
||||
): string {
|
||||
const version = String(requested ?? DEFAULT_API_VERSION)
|
||||
return previewApiVersionOrigins.has(origin) && !version.endsWith('-preview')
|
||||
? `${version}-preview`
|
||||
: version
|
||||
}
|
||||
|
||||
export function isAzureDevOpsPreviewVersionRejection(status: number | null, body: string): boolean {
|
||||
if (status !== 400) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(body) as { typeKey?: unknown } | null
|
||||
return parsed?.typeKey === 'VssInvalidPreviewVersionException'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type AzureDevOpsAuthConfig = {
|
||||
apiBaseUrl: string | null
|
||||
@@ -52,9 +88,30 @@ function authHeaders(config: AzureDevOpsAuthConfig): Record<string, string> {
|
||||
return {}
|
||||
}
|
||||
|
||||
function configuredApiBaseUrl(repo: AzureDevOpsRepoRef): string {
|
||||
function isUrlPathAncestor(ancestor: string, descendant: string): boolean {
|
||||
try {
|
||||
const ancestorUrl = new URL(ancestor)
|
||||
const descendantUrl = new URL(descendant)
|
||||
const ancestorPath = ancestorUrl.pathname.replace(/\/+$/, '')
|
||||
const descendantPath = descendantUrl.pathname.replace(/\/+$/, '')
|
||||
return (
|
||||
ancestorUrl.origin === descendantUrl.origin &&
|
||||
(ancestorPath === descendantPath || descendantPath.startsWith(`${ancestorPath}/`))
|
||||
)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveAzureDevOpsGitApiBaseUrl(repo: AzureDevOpsRepoRef): string {
|
||||
const configured = getAzureDevOpsAuthConfig().apiBaseUrl
|
||||
return configured ? normalizeAzureDevOpsApiBaseUrl(configured) : repo.apiBaseUrl
|
||||
if (!configured) {
|
||||
return repo.apiBaseUrl
|
||||
}
|
||||
const normalized = normalizeAzureDevOpsApiBaseUrl(configured)
|
||||
// Why (STA-3494): a configured collection ancestor is the auth-probe URL;
|
||||
// Git endpoints need the project-level base derived from the remote.
|
||||
return isUrlPathAncestor(normalized, repo.apiBaseUrl) ? repo.apiBaseUrl : normalized
|
||||
}
|
||||
|
||||
function apiUrl(
|
||||
@@ -63,13 +120,32 @@ function apiUrl(
|
||||
searchParams?: AzureDevOpsRequestOptions['searchParams']
|
||||
): URL {
|
||||
const url = new URL(`${baseUrl.replace(/\/+$/, '')}${path}`)
|
||||
const params = { ...searchParams, 'api-version': searchParams?.['api-version'] ?? '7.1' }
|
||||
const params = {
|
||||
...searchParams,
|
||||
'api-version': azureDevOpsApiVersionForOrigin(url.origin, searchParams?.['api-version'])
|
||||
}
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
url.searchParams.set(key, String(value))
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
// Reads the body only for a 400 on a non-preview request; consumed either way.
|
||||
async function shouldRetryWithPreviewApiVersion(url: URL, response: Response): Promise<boolean> {
|
||||
if (response.ok || response.status !== 400) {
|
||||
return false
|
||||
}
|
||||
if (url.searchParams.get('api-version')?.endsWith('-preview')) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
const body = (await response.json()) as { typeKey?: string | null } | null
|
||||
return body?.typeKey === 'VssInvalidPreviewVersionException'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function requestAzureDevOpsJsonAtBase<T>(
|
||||
baseUrl: string,
|
||||
path: string,
|
||||
@@ -80,14 +156,21 @@ export async function requestAzureDevOpsJsonAtBase<T>(
|
||||
throwOnFailure = false
|
||||
): Promise<T | null> {
|
||||
const config = getAzureDevOpsAuthConfig()
|
||||
try {
|
||||
const response = await fetch(apiUrl(baseUrl, path, options.searchParams), {
|
||||
const doFetch = (url: URL): Promise<Response> =>
|
||||
fetch(url, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...authHeaders(config)
|
||||
},
|
||||
signal: AbortSignal.timeout(options.timeoutMs ?? REQUEST_TIMEOUT_MS)
|
||||
})
|
||||
try {
|
||||
const url = apiUrl(baseUrl, path, options.searchParams)
|
||||
let response = await doFetch(url)
|
||||
if (await shouldRetryWithPreviewApiVersion(url, response)) {
|
||||
markAzureDevOpsPreviewApiVersionOrigin(url.origin)
|
||||
response = await doFetch(apiUrl(baseUrl, path, options.searchParams))
|
||||
}
|
||||
if (!response.ok) {
|
||||
await cancelUnreadResponseBody(response)
|
||||
if (throwOnFailure) {
|
||||
@@ -110,5 +193,10 @@ export function requestAzureDevOpsJson<T>(
|
||||
options: AzureDevOpsRequestOptions = {},
|
||||
throwOnFailure = false
|
||||
): Promise<T | null> {
|
||||
return requestAzureDevOpsJsonAtBase(configuredApiBaseUrl(repo), path, options, throwOnFailure)
|
||||
return requestAzureDevOpsJsonAtBase(
|
||||
resolveAzureDevOpsGitApiBaseUrl(repo),
|
||||
path,
|
||||
options,
|
||||
throwOnFailure
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
getAzureDevOpsPullRequestForBranchOrThrow,
|
||||
normalizeAzureDevOpsApiBaseUrl
|
||||
} from './client'
|
||||
import { _resetAzureDevOpsPreviewApiVersionCache } from './azure-devops-api-request'
|
||||
import { _resetAzureDevOpsRepoRefCache } from './repository-ref'
|
||||
import { __resetRepoDefaultBranchCacheForTests } from '../source-control/repo-default-branch'
|
||||
|
||||
@@ -39,6 +40,7 @@ describe('Azure DevOps client', () => {
|
||||
process.env = { ...OLD_ENV, ORCA_AZURE_DEVOPS_TOKEN: 'pat-token' }
|
||||
gitExecFileAsyncMock.mockReset()
|
||||
_resetAzureDevOpsRepoRefCache()
|
||||
_resetAzureDevOpsPreviewApiVersionCache()
|
||||
__resetRepoDefaultBranchCacheForTests()
|
||||
})
|
||||
|
||||
@@ -65,6 +67,35 @@ describe('Azure DevOps client', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('authenticates against an on-prem Server that requires -preview api-versions (STA-3494)', async () => {
|
||||
process.env.ORCA_AZURE_DEVOPS_API_BASE_URL = 'https://ado.example.com:8443/tfs/MyCollection'
|
||||
const versions: (string | null)[] = []
|
||||
globalThis.fetch = vi.fn(async (input: string | URL | Request) => {
|
||||
const url = new URL(String(input))
|
||||
expect(url.pathname).toBe('/tfs/MyCollection/_apis/connectionData')
|
||||
versions.push(url.searchParams.get('api-version'))
|
||||
if (!url.searchParams.get('api-version')?.endsWith('-preview')) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
message: 'The requested version "7.1" of the resource is under preview.',
|
||||
typeKey: 'VssInvalidPreviewVersionException'
|
||||
}),
|
||||
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
}
|
||||
return Response.json({ authenticatedUser: { providerDisplayName: 'Server User' } })
|
||||
}) as never
|
||||
|
||||
await expect(getAzureDevOpsAuthStatus()).resolves.toEqual({
|
||||
configured: true,
|
||||
authenticated: true,
|
||||
account: 'Server User',
|
||||
baseUrl: 'https://ado.example.com:8443/tfs/MyCollection',
|
||||
tokenConfigured: true
|
||||
})
|
||||
expect(versions).toEqual(['7.1', '7.1-preview'])
|
||||
})
|
||||
|
||||
it('resolves a PR for a branch through repository, PR, and status REST calls', async () => {
|
||||
gitExecFileAsyncMock.mockResolvedValue({
|
||||
stdout: 'https://dev.azure.com/acme/Project/_git/repo\n'
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
createAzureDevOpsPullRequest,
|
||||
isAzureDevOpsReviewCreationAuthenticated
|
||||
} from './pull-request-creation'
|
||||
import { _resetAzureDevOpsPreviewApiVersionCache } from './azure-devops-api-request'
|
||||
import { _resetAzureDevOpsRepoRefCache } from './repository-ref'
|
||||
import { REMOTE_URL_PROBE_TIMEOUT_MS } from '../git/remote-url-probe'
|
||||
|
||||
@@ -37,6 +38,7 @@ describe('Azure DevOps pull request creation', () => {
|
||||
stderr: ''
|
||||
})
|
||||
_resetAzureDevOpsRepoRefCache()
|
||||
_resetAzureDevOpsPreviewApiVersionCache()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -98,6 +100,77 @@ describe('Azure DevOps pull request creation', () => {
|
||||
expect(fetchMock).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('retries PR creation with -preview when the Server rejects the api-version (STA-3494)', async () => {
|
||||
gitExecFileAsyncMock.mockResolvedValue({
|
||||
stdout: 'https://ado.example.com:8443/tfs/MyCollection/MyProject/_git/my-repo\n',
|
||||
stderr: ''
|
||||
})
|
||||
const versions: (string | null)[] = []
|
||||
const fetchMock = vi.fn(async (input: string | URL | Request) => {
|
||||
const url = new URL(String(input))
|
||||
expect(url.pathname).toBe(
|
||||
'/tfs/MyCollection/MyProject/_apis/git/repositories/my-repo/pullRequests'
|
||||
)
|
||||
versions.push(url.searchParams.get('api-version'))
|
||||
if (!url.searchParams.get('api-version')?.endsWith('-preview')) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
message: 'The requested version "7.1" of the resource is under preview.',
|
||||
typeKey: 'VssInvalidPreviewVersionException'
|
||||
}),
|
||||
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
}
|
||||
return Response.json({
|
||||
pullRequestId: 51,
|
||||
title: 'Server create',
|
||||
status: 'active',
|
||||
creationDate: '2026-06-01T00:00:00Z',
|
||||
_links: {
|
||||
web: {
|
||||
href: 'https://ado.example.com:8443/tfs/MyCollection/MyProject/_git/my-repo/pullrequest/51'
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
globalThis.fetch = fetchMock as never
|
||||
|
||||
await expect(
|
||||
createAzureDevOpsPullRequest('/repo', {
|
||||
provider: 'azure-devops',
|
||||
base: 'main',
|
||||
head: 'feature/server',
|
||||
title: 'Server create',
|
||||
body: 'Body'
|
||||
})
|
||||
).resolves.toEqual({
|
||||
ok: true,
|
||||
number: 51,
|
||||
url: 'https://ado.example.com:8443/tfs/MyCollection/MyProject/_git/my-repo/pullrequest/51'
|
||||
})
|
||||
expect(versions).toEqual(['7.1', '7.1-preview'])
|
||||
})
|
||||
|
||||
it('does not retry PR creation when only the error message names the preview exception', async () => {
|
||||
const fetchMock = vi.fn(async () =>
|
||||
Response.json(
|
||||
{ message: 'Validation failed near VssInvalidPreviewVersionException' },
|
||||
{ status: 400 }
|
||||
)
|
||||
)
|
||||
globalThis.fetch = fetchMock as never
|
||||
|
||||
await expect(
|
||||
createAzureDevOpsPullRequest('/repo', {
|
||||
provider: 'azure-devops',
|
||||
base: 'main',
|
||||
head: 'feature/azure',
|
||||
title: 'Do not retry'
|
||||
})
|
||||
).resolves.toMatchObject({ ok: false, code: 'validation' })
|
||||
expect(fetchMock).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('resolves Azure DevOps remotes through the SSH git provider', async () => {
|
||||
const remoteGit = {
|
||||
exec: vi.fn(async () => ({
|
||||
|
||||
@@ -9,6 +9,12 @@ import {
|
||||
requestHostedReviewJson
|
||||
} from '../source-control/hosted-review-api-request'
|
||||
import { readHostedPullRequestTemplate } from '../source-control/pull-request-template'
|
||||
import {
|
||||
azureDevOpsApiVersionForOrigin,
|
||||
isAzureDevOpsPreviewVersionRejection,
|
||||
markAzureDevOpsPreviewApiVersionOrigin,
|
||||
resolveAzureDevOpsGitApiBaseUrl
|
||||
} from './azure-devops-api-request'
|
||||
import { getAzureDevOpsPullRequestForBranch } from './client'
|
||||
import { mapAzureDevOpsPullRequest, type RawAzureDevOpsPullRequest } from './pull-request-mappers'
|
||||
import { getAzureDevOpsRepoRef, type AzureDevOpsRepoRef } from './repository-ref'
|
||||
@@ -16,7 +22,6 @@ import { getAzureDevOpsRepoRef, type AzureDevOpsRepoRef } from './repository-ref
|
||||
const CREATE_REQUEST_TIMEOUT_MS = 60_000
|
||||
|
||||
type AzureDevOpsCreateAuthConfig = {
|
||||
apiBaseUrl: string | null
|
||||
pat: string | null
|
||||
accessToken: string | null
|
||||
username: string | null
|
||||
@@ -27,16 +32,8 @@ function envValue(name: string): string | null {
|
||||
return value.length > 0 ? value : null
|
||||
}
|
||||
|
||||
function normalizeApiBaseUrl(value: string): string {
|
||||
return value
|
||||
.trim()
|
||||
.replace(/\/+$/, '')
|
||||
.replace(/\/_apis$/i, '')
|
||||
}
|
||||
|
||||
function getAuthConfig(): AzureDevOpsCreateAuthConfig {
|
||||
return {
|
||||
apiBaseUrl: envValue('ORCA_AZURE_DEVOPS_API_BASE_URL'),
|
||||
pat: envValue('ORCA_AZURE_DEVOPS_TOKEN') ?? envValue('ORCA_AZURE_DEVOPS_PAT'),
|
||||
accessToken: envValue('ORCA_AZURE_DEVOPS_ACCESS_TOKEN'),
|
||||
username: envValue('ORCA_AZURE_DEVOPS_USERNAME')
|
||||
@@ -60,13 +57,43 @@ function authHeaders(config: AzureDevOpsCreateAuthConfig): Record<string, string
|
||||
}
|
||||
|
||||
function apiUrl(repo: AzureDevOpsRepoRef, path: string): URL {
|
||||
const config = getAuthConfig()
|
||||
const baseUrl = config.apiBaseUrl ? normalizeApiBaseUrl(config.apiBaseUrl) : repo.apiBaseUrl
|
||||
const baseUrl = resolveAzureDevOpsGitApiBaseUrl(repo)
|
||||
const url = new URL(`${baseUrl.replace(/\/+$/, '')}${path}`)
|
||||
url.searchParams.set('api-version', '7.1')
|
||||
url.searchParams.set('api-version', azureDevOpsApiVersionForOrigin(url.origin))
|
||||
return url
|
||||
}
|
||||
|
||||
// Why (STA-3494): Azure DevOps Server 400s versioned requests without -preview;
|
||||
// the rejection happens before the PR is created, so one retry is safe.
|
||||
async function requestCreatePullRequest(
|
||||
repo: AzureDevOpsRepoRef,
|
||||
path: string,
|
||||
init: Omit<RequestInit, 'signal'>
|
||||
): Promise<RawAzureDevOpsPullRequest> {
|
||||
const url = apiUrl(repo, path)
|
||||
try {
|
||||
return await requestHostedReviewJson<RawAzureDevOpsPullRequest>(
|
||||
url,
|
||||
init,
|
||||
CREATE_REQUEST_TIMEOUT_MS
|
||||
)
|
||||
} catch (error) {
|
||||
if (
|
||||
url.searchParams.get('api-version')?.endsWith('-preview') ||
|
||||
!(error instanceof HostedReviewApiRequestError) ||
|
||||
!isAzureDevOpsPreviewVersionRejection(error.status, error.message)
|
||||
) {
|
||||
throw error
|
||||
}
|
||||
markAzureDevOpsPreviewApiVersionOrigin(url.origin)
|
||||
return requestHostedReviewJson<RawAzureDevOpsPullRequest>(
|
||||
apiUrl(repo, path),
|
||||
init,
|
||||
CREATE_REQUEST_TIMEOUT_MS
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function encodePathSegment(value: string): string {
|
||||
return encodeURIComponent(value)
|
||||
}
|
||||
@@ -192,8 +219,9 @@ export async function createAzureDevOpsPullRequest(
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = await requestHostedReviewJson<RawAzureDevOpsPullRequest>(
|
||||
apiUrl(repo, `/_apis/git/repositories/${encodePathSegment(repo.repository)}/pullRequests`),
|
||||
const raw = await requestCreatePullRequest(
|
||||
repo,
|
||||
`/_apis/git/repositories/${encodePathSegment(repo.repository)}/pullRequests`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -202,8 +230,7 @@ export async function createAzureDevOpsPullRequest(
|
||||
...authHeaders(getAuthConfig())
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
},
|
||||
CREATE_REQUEST_TIMEOUT_MS
|
||||
}
|
||||
)
|
||||
const created = mapAzureDevOpsPullRequest(raw, 'neutral', repo.webBaseUrl)
|
||||
if (created) {
|
||||
|
||||
Reference in New Issue
Block a user