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/SourceControlProviderDiscovery.ts (1 of 59 files). Show all

Unified · Split

apps/server/src/sourceControl/SourceControlProviderDiscovery.ts+237−0
@@ -0,0 +1,237 @@
1import type {
2 SourceControlProviderAuth,
3 SourceControlProviderDiscoveryItem,
4 SourceControlProviderKind,
5} from "@t3tools/contracts";
6import { Effect, Option } from "effect";
7
8import type * as VcsProcess from "../vcs/VcsProcess.ts";
9
10export interface SourceControlAuthProbeInput {
11 readonly stdout: string;
12 readonly stderr: string;
13 readonly exitCode: VcsProcess.VcsProcessOutput["exitCode"];
14}
15
16interface SourceControlDiscoverySpecBase {
17 readonly kind: SourceControlProviderKind;
18 readonly label: string;
19 readonly installHint: string;
20}
21
22export type SourceControlCliDiscoverySpec = SourceControlDiscoverySpecBase & {
23 readonly type: "cli";
24 readonly executable: string;
25 readonly versionArgs: ReadonlyArray<string>;
26 readonly authArgs: ReadonlyArray<string>;
27 readonly parseAuth: (input: SourceControlAuthProbeInput) => SourceControlProviderAuth;
28};
29
30export type SourceControlApiDiscoverySpec = SourceControlDiscoverySpecBase & {
31 readonly type: "api";
32 readonly probeAuth: Effect.Effect<SourceControlProviderAuth, never>;
33};
34
35export type SourceControlProviderDiscoverySpec =
36 | SourceControlCliDiscoverySpec
37 | SourceControlApiDiscoverySpec;
38
39interface DiscoveryProbeResult {
40 readonly kind: SourceControlProviderKind;
41 readonly label: string;
42 readonly executable: string;
43 readonly status: "available" | "missing";
44 readonly version: Option.Option<string>;
45 readonly installHint: string;
46 readonly detail: Option.Option<string>;
47}
48
49export function firstNonEmptyLine(text: string): Option.Option<string> {
50 const line = text
51 .split(/\r?\n/)
52 .map((entry) => entry.trim())
53 .find((entry) => entry.length > 0);
54 return line === undefined ? Option.none() : Option.some(line);
55}
56
57export function detailFromCause(cause: unknown): Option.Option<string> {
58 if (cause instanceof Error && cause.message.trim().length > 0) {
59 return Option.some(cause.message.trim());
60 }
61 return Option.none();
62}
63
64function authAccount(account: string | undefined): Option.Option<string> {
65 const trimmed = account?.trim();
66 return trimmed === undefined || trimmed.length === 0 ? Option.none() : Option.some(trimmed);
67}
68
69function authHost(host: string | undefined): Option.Option<string> {
70 const trimmed = host?.trim();
71 return trimmed === undefined || trimmed.length === 0 ? Option.none() : Option.some(trimmed);
72}
73
74function authDetail(detail: string | undefined): Option.Option<string> {
75 const trimmed = detail?.trim();
76 return trimmed === undefined || trimmed.length === 0 ? Option.none() : Option.some(trimmed);
77}
78
79export function providerAuth(input: {
80 readonly status: SourceControlProviderAuth["status"];
81 readonly account?: string | undefined;
82 readonly host?: string | undefined;
83 readonly detail?: string | undefined;
84}): SourceControlProviderAuth {
85 return {
86 status: input.status,
87 account: authAccount(input.account),
88 host: authHost(input.host),
89 detail: authDetail(input.detail),
90 };
91}
92
93export function unknownAuth(detail?: string): SourceControlProviderAuth {
94 return providerAuth({ status: "unknown", detail });
95}
96
97export function combinedAuthOutput(input: SourceControlAuthProbeInput): string {
98 return [input.stdout, input.stderr].filter((entry) => entry.trim().length > 0).join("\n");
99}
100
101function sanitizedAuthLines(text: string): ReadonlyArray<string> {
102 return text
103 .split(/\r?\n/)
104 .map((entry) => entry.trim())
105 .filter((entry) => entry.length > 0)
106 .filter((entry) => !/^[-\s]*token(?:\s+scopes?)?:/iu.test(entry));
107}
108
109export function firstSafeAuthLine(text: string): string | undefined {
110 return sanitizedAuthLines(text)[0];
111}
112
113export function parseCliHost(text: string): string | undefined {
114 return sanitizedAuthLines(text)
115 .map((line) => line.replace(/^[^a-z0-9]+/iu, ""))
116 .find((line) => /^[a-z0-9][a-z0-9.-]*(?::\d+)?$/iu.test(line));
117}
118
119export function matchFirst(text: string, patterns: ReadonlyArray<RegExp>): string | undefined {
120 for (const pattern of patterns) {
121 const match = pattern.exec(text);
122 const value = match?.[1]?.trim();
123 if (value && value.length > 0) return value;
124 }
125 return undefined;
126}
127
128function probeCli(input: {
129 readonly spec: SourceControlCliDiscoverySpec;
130 readonly process: VcsProcess.VcsProcessShape;
131 readonly cwd: string;
132}): Effect.Effect<DiscoveryProbeResult> {
133 return input.process
134 .run({
135 operation: "source-control.discovery.probe",
136 command: input.spec.executable,
137 args: input.spec.versionArgs,
138 cwd: input.cwd,
139 timeoutMs: 5_000,
140 maxOutputBytes: 8_000,
141 truncateOutputAtMaxBytes: true,
142 })
143 .pipe(
144 Effect.map(
145 (result) =>
146 ({
147 kind: input.spec.kind,
148 label: input.spec.label,
149 executable: input.spec.executable,
150 status: "available" as const,
151 version: Option.orElse(firstNonEmptyLine(result.stdout), () =>
152 firstNonEmptyLine(result.stderr),
153 ),
154 installHint: input.spec.installHint,
155 detail: Option.none<string>(),
156 }) satisfies DiscoveryProbeResult,
157 ),
158 Effect.catch((cause) =>
159 Effect.succeed({
160 kind: input.spec.kind,
161 label: input.spec.label,
162 executable: input.spec.executable,
163 status: "missing" as const,
164 version: Option.none<string>(),
165 installHint: input.spec.installHint,
166 detail: detailFromCause(cause),
167 } satisfies DiscoveryProbeResult),
168 ),
169 );
170}
171
172export function probeSourceControlProvider(input: {
173 readonly spec: SourceControlProviderDiscoverySpec;
174 readonly process: VcsProcess.VcsProcessShape;
175 readonly cwd: string;
176}): Effect.Effect<SourceControlProviderDiscoveryItem> {
177 if (input.spec.type === "api") {
178 return input.spec.probeAuth.pipe(
179 Effect.map(
180 (auth) =>
181 ({
182 kind: input.spec.kind,
183 label: input.spec.label,
184 status: "available" as const,
185 version: Option.none<string>(),
186 installHint: input.spec.installHint,
187 detail: Option.none<string>(),
188 auth,
189 }) satisfies SourceControlProviderDiscoveryItem,
190 ),
191 );
192 }
193
194 const spec = input.spec;
195
196 return probeCli({
197 spec,
198 process: input.process,
199 cwd: input.cwd,
200 }).pipe(
201 Effect.flatMap((item) => {
202 if (item.status !== "available") {
203 return Effect.succeed({
204 ...item,
205 auth: unknownAuth("Hosting integration command was not found on the server PATH."),
206 } satisfies SourceControlProviderDiscoveryItem);
207 }
208
209 return input.process
210 .run({
211 operation: "source-control.discovery.auth",
212 command: spec.executable,
213 args: spec.authArgs,
214 cwd: input.cwd,
215 allowNonZeroExit: true,
216 timeoutMs: 5_000,
217 maxOutputBytes: 8_000,
218 truncateOutputAtMaxBytes: true,
219 })
220 .pipe(
221 Effect.map(
222 (result) =>
223 ({
224 ...item,
225 auth: spec.parseAuth(result),
226 }) satisfies SourceControlProviderDiscoveryItem,
227 ),
228 Effect.catch((cause) =>
229 Effect.succeed({
230 ...item,
231 auth: unknownAuth(Option.getOrUndefined(detailFromCause(cause))),
232 } satisfies SourceControlProviderDiscoveryItem),
233 ),
234 );
235 }),
236 );
237}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t