import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contracts"; import * as AzureDevOpsCli from "./AzureDevOpsCli.ts"; import * as SourceControlProvider from "./SourceControlProvider.ts"; import * as SourceControlProviderDiscovery from "./SourceControlProviderDiscovery.ts"; function providerError( operation: string, cause: AzureDevOpsCli.AzureDevOpsCliError, ): SourceControlProviderError { return new SourceControlProviderError({ provider: "azure-devops", operation, detail: cause.detail, cause, }); } function parseAzureAuth(input: SourceControlProviderDiscovery.SourceControlAuthProbeInput) { const account = input.stdout.trim().split(/\r?\n/)[0]?.trim(); if (input.exitCode !== 0) { return SourceControlProviderDiscovery.providerAuth({ status: "unauthenticated", detail: SourceControlProviderDiscovery.firstSafeAuthLine( SourceControlProviderDiscovery.combinedAuthOutput(input), ) ?? "Run `az login` to authenticate Azure CLI.", }); } if (account !== undefined && account.length > 0) { return SourceControlProviderDiscovery.providerAuth({ status: "authenticated", account, host: "dev.azure.com", }); } return SourceControlProviderDiscovery.providerAuth({ status: "unknown", host: "dev.azure.com", detail: "Azure CLI account status could not be parsed.", }); } export const discovery = { type: "cli", kind: "azure-devops", label: "Azure DevOps", executable: "az", versionArgs: ["--version"], authArgs: ["account", "show", "--query", "user.name", "-o", "tsv"], parseAuth: parseAzureAuth, installHint: "Install the Azure command-line tools (`az`), then enable Azure DevOps support with `az extension add --name azure-devops`.", } satisfies SourceControlProviderDiscovery.SourceControlCliDiscoverySpec; function toChangeRequest(summary: { readonly number: number; readonly title: string; readonly url: string; readonly baseRefName: string; readonly headRefName: string; readonly state: "open" | "closed" | "merged"; readonly updatedAt: ChangeRequest["updatedAt"]; }): ChangeRequest { return { provider: "azure-devops", number: summary.number, title: summary.title, url: summary.url, baseRefName: summary.baseRefName, headRefName: summary.headRefName, state: summary.state, updatedAt: summary.updatedAt, isCrossRepository: false, }; } export const make = Effect.fn("makeAzureDevOpsSourceControlProvider")(function* () { const azure = yield* AzureDevOpsCli.AzureDevOpsCli; return SourceControlProvider.SourceControlProvider.of({ kind: "azure-devops", listChangeRequests: (input) => { const source = SourceControlProvider.sourceControlRefFromInput(input); return azure .listPullRequests({ cwd: input.cwd, headSelector: input.headSelector, ...(source !== undefined ? { source } : {}), state: input.state, ...(input.limit !== undefined ? { limit: input.limit } : {}), }) .pipe( Effect.map((items) => items.map(toChangeRequest)), Effect.mapError((error) => providerError("listChangeRequests", error)), ); }, getChangeRequest: (input) => azure.getPullRequest(input).pipe( Effect.map(toChangeRequest), Effect.mapError((error) => providerError("getChangeRequest", error)), ), createChangeRequest: (input) => { const source = SourceControlProvider.sourceControlRefFromInput(input); return azure .createPullRequest({ cwd: input.cwd, baseBranch: input.baseRefName, headSelector: input.headSelector, ...(source !== undefined ? { source } : {}), ...(input.target !== undefined ? { target: input.target } : {}), title: input.title, bodyFile: input.bodyFile, }) .pipe(Effect.mapError((error) => providerError("createChangeRequest", error))); }, getRepositoryCloneUrls: (input) => azure .getRepositoryCloneUrls(input) .pipe(Effect.mapError((error) => providerError("getRepositoryCloneUrls", error))), createRepository: (input) => azure .createRepository(input) .pipe(Effect.mapError((error) => providerError("createRepository", error))), getDefaultBranch: (input) => azure .getDefaultBranch({ cwd: input.cwd }) .pipe(Effect.mapError((error) => providerError("getDefaultBranch", error))), checkoutChangeRequest: (input) => azure .checkoutPullRequest({ cwd: input.cwd, reference: input.reference, ...(input.context !== undefined ? { remoteName: input.context.remoteName } : {}), }) .pipe(Effect.mapError((error) => providerError("checkoutChangeRequest", error))), }); }); export const layer = Layer.effect(SourceControlProvider.SourceControlProvider, make());