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

Unified · Split

apps/server/src/sourceControl/BitbucketSourceControlProvider.ts+126−0
@@ -0,0 +1,126 @@
1+import { Effect, Layer, Option } from "effect";
2+import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contracts";
3+
4+import * as BitbucketApi from "./BitbucketApi.ts";
5+import * as BitbucketPullRequests from "./bitbucketPullRequests.ts";
6+import * as SourceControlProvider from "./SourceControlProvider.ts";
7+import type * as SourceControlProviderDiscovery from "./SourceControlProviderDiscovery.ts";
8+
9+function providerError(
10+ operation: string,
11+ cause: BitbucketApi.BitbucketApiError,
12+): SourceControlProviderError {
13+ return new SourceControlProviderError({
14+ provider: "bitbucket",
15+ operation,
16+ detail: cause.detail,
17+ cause,
18+ });
19+}
20+
21+function toChangeRequest(
22+ summary: BitbucketPullRequests.NormalizedBitbucketPullRequestRecord,
23+): ChangeRequest {
24+ return {
25+ provider: "bitbucket",
26+ number: summary.number,
27+ title: summary.title,
28+ url: summary.url,
29+ baseRefName: summary.baseRefName,
30+ headRefName: summary.headRefName,
31+ state: summary.state,
32+ updatedAt: summary.updatedAt ?? Option.none(),
33+ ...(summary.isCrossRepository !== undefined
34+ ? { isCrossRepository: summary.isCrossRepository }
35+ : {}),
36+ ...(summary.headRepositoryNameWithOwner !== undefined
37+ ? { headRepositoryNameWithOwner: summary.headRepositoryNameWithOwner }
38+ : {}),
39+ ...(summary.headRepositoryOwnerLogin !== undefined
40+ ? { headRepositoryOwnerLogin: summary.headRepositoryOwnerLogin }
41+ : {}),
42+ };
43+}
44+
45+export const make = Effect.fn("makeBitbucketSourceControlProvider")(function* () {
46+ const bitbucket = yield* BitbucketApi.BitbucketApi;
47+
48+ return SourceControlProvider.SourceControlProvider.of({
49+ kind: "bitbucket",
50+ listChangeRequests: (input) => {
51+ const source = SourceControlProvider.sourceControlRefFromInput(input);
52+ return bitbucket
53+ .listPullRequests({
54+ cwd: input.cwd,
55+ ...(input.context ? { context: input.context } : {}),
56+ headSelector: input.headSelector,
57+ ...(source ? { source } : {}),
58+ state: input.state,
59+ ...(input.limit !== undefined ? { limit: input.limit } : {}),
60+ })
61+ .pipe(
62+ Effect.map((items) => items.map(toChangeRequest)),
63+ Effect.mapError((error) => providerError("listChangeRequests", error)),
64+ );
65+ },
66+ getChangeRequest: (input) =>
67+ bitbucket.getPullRequest(input).pipe(
68+ Effect.map(toChangeRequest),
69+ Effect.mapError((error) => providerError("getChangeRequest", error)),
70+ ),
71+ createChangeRequest: (input) => {
72+ const source = SourceControlProvider.sourceControlRefFromInput(input);
73+ return bitbucket
74+ .createPullRequest({
75+ cwd: input.cwd,
76+ ...(input.context ? { context: input.context } : {}),
77+ baseBranch: input.baseRefName,
78+ headSelector: input.headSelector,
79+ ...(source ? { source } : {}),
80+ ...(input.target ? { target: input.target } : {}),
81+ title: input.title,
82+ bodyFile: input.bodyFile,
83+ })
84+ .pipe(Effect.mapError((error) => providerError("createChangeRequest", error)));
85+ },
86+ getRepositoryCloneUrls: (input) =>
87+ bitbucket
88+ .getRepositoryCloneUrls(input)
89+ .pipe(Effect.mapError((error) => providerError("getRepositoryCloneUrls", error))),
90+ createRepository: (input) =>
91+ bitbucket
92+ .createRepository(input)
93+ .pipe(Effect.mapError((error) => providerError("createRepository", error))),
94+ getDefaultBranch: (input) =>
95+ bitbucket
96+ .getDefaultBranch({
97+ cwd: input.cwd,
98+ ...(input.context ? { context: input.context } : {}),
99+ })
100+ .pipe(Effect.mapError((error) => providerError("getDefaultBranch", error))),
101+ checkoutChangeRequest: (input) =>
102+ bitbucket
103+ .checkoutPullRequest({
104+ cwd: input.cwd,
105+ ...(input.context ? { context: input.context } : {}),
106+ reference: input.reference,
107+ ...(input.force !== undefined ? { force: input.force } : {}),
108+ })
109+ .pipe(Effect.mapError((error) => providerError("checkoutChangeRequest", error))),
110+ });
111+});
112+
113+export const layer = Layer.effect(SourceControlProvider.SourceControlProvider, make());
114+
115+export const makeDiscovery = Effect.fn("makeBitbucketSourceControlProviderDiscovery")(function* () {
116+ const bitbucket = yield* BitbucketApi.BitbucketApi;
117+
118+ return {
119+ type: "api",
120+ kind: "bitbucket",
121+ label: "Bitbucket",
122+ installHint:
123+ "Set T3CODE_BITBUCKET_EMAIL and T3CODE_BITBUCKET_API_TOKEN on the server (use a Bitbucket API token with pull request and repository scopes).",
124+ probeAuth: bitbucket.probeAuth,
125+ } satisfies SourceControlProviderDiscovery.SourceControlApiDiscoverySpec;
126+});