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

Unified · Split

apps/server/src/sourceControl/GitHubSourceControlProvider.ts+115−67
@@ -5,11 +5,15 @@ import { · 4 unmodified lines
55 type ChangeRequestState,
66 } from "@t3tools/contracts";
77
8−import { GitHubCli, type GitHubCliError, type GitHubPullRequestSummary } from "./GitHubCli.ts";
9−import { decodeGitHubPullRequestListJson } from "./gitHubPullRequests.ts";
10−import { SourceControlProvider, type SourceControlProviderShape } from "./SourceControlProvider.ts";
8+import * as GitHubCli from "./GitHubCli.ts";
9+import * as GitHubPullRequests from "./gitHubPullRequests.ts";
10+import * as SourceControlProvider from "./SourceControlProvider.ts";
11+import * as SourceControlProviderDiscovery from "./SourceControlProviderDiscovery.ts";
1112
12−function providerError(operation: string, cause: GitHubCliError): SourceControlProviderError {
13+function providerError(
14+ operation: string,
15+ cause: GitHubCli.GitHubCliError,
16+): SourceControlProviderError {
1317 return new SourceControlProviderError({
1418 provider: "github",
1519 operation,
@@ -18,7 +22,7 @@ function providerError(operation: string, cause: GitHubCliError): SourceControlP · 2 unmodified lines
1822 });
1923 }
2024
21−function toChangeRequest(summary: GitHubPullRequestSummary): ChangeRequest {
25+function toChangeRequest(summary: GitHubCli.GitHubPullRequestSummary): ChangeRequest {
2226 return {
2327 provider: "github",
2428 number: summary.number,
@@ -40,75 +44,119 @@ function toChangeRequest(summary: GitHubPullRequestSummary): ChangeRequest { · 15 unmodified lines
4044 };
4145 }
4246
47+function parseGitHubAuth(input: SourceControlProviderDiscovery.SourceControlAuthProbeInput) {
48+ const output = SourceControlProviderDiscovery.combinedAuthOutput(input);
49+ const account = SourceControlProviderDiscovery.matchFirst(output, [
50+ /Logged in to .* account\s+([^\s(]+)/iu,
51+ /Logged in to .* as\s+([^\s(]+)/iu,
52+ ]);
53+ const host = SourceControlProviderDiscovery.parseCliHost(output);
54+
55+ if (input.exitCode !== 0) {
56+ return SourceControlProviderDiscovery.providerAuth({
57+ status: "unauthenticated",
58+ host,
59+ detail:
60+ SourceControlProviderDiscovery.firstSafeAuthLine(output) ??
61+ "Run `gh auth login` to authenticate GitHub CLI.",
62+ });
63+ }
64+
65+ if (account) {
66+ return SourceControlProviderDiscovery.providerAuth({ status: "authenticated", account, host });
67+ }
68+
69+ return SourceControlProviderDiscovery.providerAuth({
70+ status: "unknown",
71+ host,
72+ detail:
73+ SourceControlProviderDiscovery.firstSafeAuthLine(output) ??
74+ "GitHub CLI auth status could not be parsed.",
75+ });
76+}
77+
78+export const discovery = {
79+ type: "cli",
80+ kind: "github",
81+ label: "GitHub",
82+ executable: "gh",
83+ versionArgs: ["--version"],
84+ authArgs: ["auth", "status"],
85+ parseAuth: parseGitHubAuth,
86+ installHint:
87+ "Install the GitHub command-line tool (`gh`) via https://cli.github.com/ or your package manager (for example `brew install gh`).",
88+} satisfies SourceControlProviderDiscovery.SourceControlCliDiscoverySpec;
89+
4390 export const make = Effect.fn("makeGitHubSourceControlProvider")(function* () {
44− const github = yield* GitHubCli;
91+ const github = yield* GitHubCli.GitHubCli;
4592
46− const listChangeRequests: SourceControlProviderShape["listChangeRequests"] = (input) => {
47− if (input.state === "open") {
93+ const listChangeRequests: SourceControlProvider.SourceControlProviderShape["listChangeRequests"] =
94+ (input) => {
95+ if (input.state === "open") {
96+ return github
97+ .listOpenPullRequests({
98+ cwd: input.cwd,
99+ headSelector: input.headSelector,
100+ ...(input.limit !== undefined ? { limit: input.limit } : {}),
101+ })
102+ .pipe(
103+ Effect.map((items) => items.map(toChangeRequest)),
104+ Effect.mapError((error) => providerError("listChangeRequests", error)),
105+ );
106+ }
107+
108+ const stateArg: ChangeRequestState | "all" = input.state;
48109 return github
49− .listOpenPullRequests({
110+ .execute({
50111 cwd: input.cwd,
51− headSelector: input.headSelector,
52− ...(input.limit !== undefined ? { limit: input.limit } : {}),
112+ args: [
113+ "pr",
114+ "list",
115+ "--head",
116+ input.headSelector,
117+ "--state",
118+ stateArg,
119+ "--limit",
120+ String(input.limit ?? 20),
121+ "--json",
122+ "number,title,url,baseRefName,headRefName,state,mergedAt,updatedAt,isCrossRepository,headRepository,headRepositoryOwner",
123+ ],
53124 })
54125 .pipe(
55− Effect.map((items) => items.map(toChangeRequest)),
56− Effect.mapError((error) => providerError("listChangeRequests", error)),
126+ Effect.flatMap((result) => {
127+ const raw = result.stdout.trim();
128+ if (raw.length === 0) {
129+ return Effect.succeed([]);
130+ }
131+ return Effect.sync(() => GitHubPullRequests.decodeGitHubPullRequestListJson(raw)).pipe(
132+ Effect.flatMap((decoded) =>
133+ Result.isSuccess(decoded)
134+ ? Effect.succeed(
135+ decoded.success.map((item) => ({
136+ ...toChangeRequest(item),
137+ updatedAt: item.updatedAt,
138+ })),
139+ )
140+ : Effect.fail(
141+ new SourceControlProviderError({
142+ provider: "github",
143+ operation: "listChangeRequests",
144+ detail: "GitHub CLI returned invalid change request JSON.",
145+ cause: decoded.failure,
146+ }),
147+ ),
148+ ),
149+ );
150+ }),
151+ Effect.mapError((error) =>
152+ Schema.is(SourceControlProviderError)(error)
153+ ? error
154+ : providerError("listChangeRequests", error),
155+ ),
57156 );
58− }
59−
60− const stateArg: ChangeRequestState | "all" = input.state;
61− return github
62− .execute({
63− cwd: input.cwd,
64− args: [
65− "pr",
66− "list",
67− "--head",
68− input.headSelector,
69− "--state",
70− stateArg,
71− "--limit",
72− String(input.limit ?? 20),
73− "--json",
74− "number,title,url,baseRefName,headRefName,state,mergedAt,updatedAt,isCrossRepository,headRepository,headRepositoryOwner",
75− ],
76− })
77− .pipe(
78− Effect.flatMap((result) => {
79− const raw = result.stdout.trim();
80− if (raw.length === 0) {
81− return Effect.succeed([]);
82− }
83− return Effect.sync(() => decodeGitHubPullRequestListJson(raw)).pipe(
84− Effect.flatMap((decoded) =>
85− Result.isSuccess(decoded)
86− ? Effect.succeed(
87− decoded.success.map((item) => ({
88− ...toChangeRequest(item),
89− updatedAt: item.updatedAt,
90− })),
91− )
92− : Effect.fail(
93− new SourceControlProviderError({
94− provider: "github",
95− operation: "listChangeRequests",
96− detail: "GitHub CLI returned invalid change request JSON.",
97− cause: decoded.failure,
98− }),
99− ),
100− ),
101− );
102− }),
103− Effect.mapError((error) =>
104− Schema.is(SourceControlProviderError)(error)
105− ? error
106− : providerError("listChangeRequests", error),
107− ),
108− );
109− };
157+ };
110158
111− return SourceControlProvider.of({
159+ return SourceControlProvider.SourceControlProvider.of({
112160 kind: "github",
113161 listChangeRequests,
114162 getChangeRequest: (input) =>
@@ -145,4 +193,4 @@ export const make = Effect.fn("makeGitHubSourceControlProvider")(function* () { · 30 unmodified lines
145193 });
146194 });
147195
148−export const layer = Layer.effect(SourceControlProvider, make());
196+export const layer = Layer.effect(SourceControlProvider.SourceControlProvider, make());