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
5 type ChangeRequestState,5 type ChangeRequestState,
6} from "@t3tools/contracts";6} from "@t3tools/contracts";
7 7
8import { GitHubCli, type GitHubCliError, type GitHubPullRequestSummary } from "./GitHubCli.ts";8import * as GitHubCli from "./GitHubCli.ts";
9import { decodeGitHubPullRequestListJson } from "./gitHubPullRequests.ts";9import * as GitHubPullRequests from "./gitHubPullRequests.ts";
10import { SourceControlProvider, type SourceControlProviderShape } from "./SourceControlProvider.ts";10import * as SourceControlProvider from "./SourceControlProvider.ts";
11import * as SourceControlProviderDiscovery from "./SourceControlProviderDiscovery.ts";
11 12
12function providerError(operation: string, cause: GitHubCliError): SourceControlProviderError {13function providerError(
14 operation: string,
15 cause: GitHubCli.GitHubCliError,
16): SourceControlProviderError {
13 return new SourceControlProviderError({17 return new SourceControlProviderError({
14 provider: "github",18 provider: "github",
15 operation,19 operation,
@@ -18,7 +22,7 @@ function providerError(operation: string, cause: GitHubCliError): SourceControlP · 2 unmodified lines
18 });22 });
19}23}
20 24
21function toChangeRequest(summary: GitHubPullRequestSummary): ChangeRequest {25function toChangeRequest(summary: GitHubCli.GitHubPullRequestSummary): ChangeRequest {
22 return {26 return {
23 provider: "github",27 provider: "github",
24 number: summary.number,28 number: summary.number,
@@ -40,75 +44,119 @@ function toChangeRequest(summary: GitHubPullRequestSummary): ChangeRequest { · 15 unmodified lines
40 };44 };
41}45}
42 46
47function 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
78export 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
43export const make = Effect.fn("makeGitHubSourceControlProvider")(function* () {90export const make = Effect.fn("makeGitHubSourceControlProvider")(function* () {
44 const github = yield* GitHubCli;91 const github = yield* GitHubCli.GitHubCli;
45 92
46 const listChangeRequests: SourceControlProviderShape["listChangeRequests"] = (input) => {93 const listChangeRequests: SourceControlProvider.SourceControlProviderShape["listChangeRequests"] =
47 if (input.state === "open") {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;
48 return github109 return github
49 .listOpenPullRequests({110 .execute({
50 cwd: input.cwd,111 cwd: input.cwd,
51 headSelector: input.headSelector,112 args: [
52 ...(input.limit !== undefined ? { limit: input.limit } : {}),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 ],
53 })124 })
54 .pipe(125 .pipe(
55 Effect.map((items) => items.map(toChangeRequest)),126 Effect.flatMap((result) => {
56 Effect.mapError((error) => providerError("listChangeRequests", error)),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 ),
57 );156 );
58 }157 };
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 };
110 158
111 return SourceControlProvider.of({159 return SourceControlProvider.SourceControlProvider.of({
112 kind: "github",160 kind: "github",
113 listChangeRequests,161 listChangeRequests,
114 getChangeRequest: (input) =>162 getChangeRequest: (input) =>
@@ -145,4 +193,4 @@ export const make = Effect.fn("makeGitHubSourceControlProvider")(function* () { · 30 unmodified lines
145 });193 });
146});194});
147 195
148export const layer = Layer.effect(SourceControlProvider, make());196export const layer = Layer.effect(SourceControlProvider.SourceControlProvider, make());