History

feat(source-control): add Bitbucket & Azure Devops providers (#2473)

Julius Marminge committed 6 months agov0.0.22-nightly.20260504.196

Commit 91a03e01 parent d501ebe59 files · +5834 −2327Browse files

Showing the changes to apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.ts (1 of 59 files). Show all

Unified · Split

apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.ts+144−0
@@ -0,0 +1,144 @@
1import { Effect, Layer } from "effect";
2import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contracts";
3
4import * as AzureDevOpsCli from "./AzureDevOpsCli.ts";
5import * as SourceControlProvider from "./SourceControlProvider.ts";
6import * as SourceControlProviderDiscovery from "./SourceControlProviderDiscovery.ts";
7
8function providerError(
9 operation: string,
10 cause: AzureDevOpsCli.AzureDevOpsCliError,
11): SourceControlProviderError {
12 return new SourceControlProviderError({
13 provider: "azure-devops",
14 operation,
15 detail: cause.detail,
16 cause,
17 });
18}
19
20function parseAzureAuth(input: SourceControlProviderDiscovery.SourceControlAuthProbeInput) {
21 const account = input.stdout.trim().split(/\r?\n/)[0]?.trim();
22
23 if (input.exitCode !== 0) {
24 return SourceControlProviderDiscovery.providerAuth({
25 status: "unauthenticated",
26 detail:
27 SourceControlProviderDiscovery.firstSafeAuthLine(
28 SourceControlProviderDiscovery.combinedAuthOutput(input),
29 ) ?? "Run `az login` to authenticate Azure CLI.",
30 });
31 }
32
33 if (account && account.length > 0) {
34 return SourceControlProviderDiscovery.providerAuth({
35 status: "authenticated",
36 account,
37 host: "dev.azure.com",
38 });
39 }
40
41 return SourceControlProviderDiscovery.providerAuth({
42 status: "unknown",
43 host: "dev.azure.com",
44 detail: "Azure CLI account status could not be parsed.",
45 });
46}
47
48export const discovery = {
49 type: "cli",
50 kind: "azure-devops",
51 label: "Azure DevOps",
52 executable: "az",
53 versionArgs: ["--version"],
54 authArgs: ["account", "show", "--query", "user.name", "-o", "tsv"],
55 parseAuth: parseAzureAuth,
56 installHint:
57 "Install the Azure command-line tools (`az`), then enable Azure DevOps support with `az extension add --name azure-devops`.",
58} satisfies SourceControlProviderDiscovery.SourceControlCliDiscoverySpec;
59
60function toChangeRequest(summary: {
61 readonly number: number;
62 readonly title: string;
63 readonly url: string;
64 readonly baseRefName: string;
65 readonly headRefName: string;
66 readonly state: "open" | "closed" | "merged";
67 readonly updatedAt: ChangeRequest["updatedAt"];
68}): ChangeRequest {
69 return {
70 provider: "azure-devops",
71 number: summary.number,
72 title: summary.title,
73 url: summary.url,
74 baseRefName: summary.baseRefName,
75 headRefName: summary.headRefName,
76 state: summary.state,
77 updatedAt: summary.updatedAt,
78 isCrossRepository: false,
79 };
80}
81
82export const make = Effect.fn("makeAzureDevOpsSourceControlProvider")(function* () {
83 const azure = yield* AzureDevOpsCli.AzureDevOpsCli;
84
85 return SourceControlProvider.SourceControlProvider.of({
86 kind: "azure-devops",
87 listChangeRequests: (input) => {
88 const source = SourceControlProvider.sourceControlRefFromInput(input);
89 return azure
90 .listPullRequests({
91 cwd: input.cwd,
92 headSelector: input.headSelector,
93 ...(source ? { source } : {}),
94 state: input.state,
95 ...(input.limit !== undefined ? { limit: input.limit } : {}),
96 })
97 .pipe(
98 Effect.map((items) => items.map(toChangeRequest)),
99 Effect.mapError((error) => providerError("listChangeRequests", error)),
100 );
101 },
102 getChangeRequest: (input) =>
103 azure.getPullRequest(input).pipe(
104 Effect.map(toChangeRequest),
105 Effect.mapError((error) => providerError("getChangeRequest", error)),
106 ),
107 createChangeRequest: (input) => {
108 const source = SourceControlProvider.sourceControlRefFromInput(input);
109 return azure
110 .createPullRequest({
111 cwd: input.cwd,
112 baseBranch: input.baseRefName,
113 headSelector: input.headSelector,
114 ...(source ? { source } : {}),
115 ...(input.target ? { target: input.target } : {}),
116 title: input.title,
117 bodyFile: input.bodyFile,
118 })
119 .pipe(Effect.mapError((error) => providerError("createChangeRequest", error)));
120 },
121 getRepositoryCloneUrls: (input) =>
122 azure
123 .getRepositoryCloneUrls(input)
124 .pipe(Effect.mapError((error) => providerError("getRepositoryCloneUrls", error))),
125 createRepository: (input) =>
126 azure
127 .createRepository(input)
128 .pipe(Effect.mapError((error) => providerError("createRepository", error))),
129 getDefaultBranch: (input) =>
130 azure
131 .getDefaultBranch({ cwd: input.cwd })
132 .pipe(Effect.mapError((error) => providerError("getDefaultBranch", error))),
133 checkoutChangeRequest: (input) =>
134 azure
135 .checkoutPullRequest({
136 cwd: input.cwd,
137 reference: input.reference,
138 ...(input.context ? { remoteName: input.context.remoteName } : {}),
139 })
140 .pipe(Effect.mapError((error) => providerError("checkoutChangeRequest", error))),
141 });
142});
143
144export const layer = Layer.effect(SourceControlProvider.SourceControlProvider, make());