Make webhooks work, add workspace selection, rename BitBucket to

Bitbucket
This commit is contained in:
x032205
2025-07-03 18:49:29 -04:00
parent 8c6b903204
commit 6a6f08fc4d
61 changed files with 789 additions and 444 deletions
@@ -28,7 +28,7 @@ export enum AppConnection {
Flyio = "flyio",
GitLab = "gitlab",
Cloudflare = "cloudflare",
BitBucket = "bitbucket"
Bitbucket = "bitbucket"
}
export enum AWSRegion {
@@ -51,9 +51,9 @@ import {
validateAzureKeyVaultConnectionCredentials
} from "./azure-key-vault";
import {
BitBucketConnectionMethod,
getBitBucketConnectionListItem,
validateBitBucketConnectionCredentials
BitbucketConnectionMethod,
getBitbucketConnectionListItem,
validateBitbucketConnectionCredentials
} from "./bitbucket";
import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda";
import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum";
@@ -142,7 +142,7 @@ export const listAppConnectionOptions = () => {
getFlyioConnectionListItem(),
getGitLabConnectionListItem(),
getCloudflareConnectionListItem(),
getBitBucketConnectionListItem()
getBitbucketConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@@ -223,7 +223,7 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.BitBucket]: validateBitBucketConnectionCredentials as TAppConnectionCredentialsValidator
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator
};
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
@@ -260,7 +260,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case VercelConnectionMethod.ApiToken:
case OnePassConnectionMethod.ApiToken:
case CloudflareConnectionMethod.APIToken:
case BitBucketConnectionMethod.ApiToken:
case BitbucketConnectionMethod.ApiToken:
return "API Token";
case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword:
@@ -341,7 +341,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Flyio]: platformManagedCredentialsNotSupported,
[AppConnection.GitLab]: platformManagedCredentialsNotSupported,
[AppConnection.Cloudflare]: platformManagedCredentialsNotSupported,
[AppConnection.BitBucket]: platformManagedCredentialsNotSupported
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported
};
export const enterpriseAppCheck = async (
@@ -30,7 +30,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Flyio]: "Fly.io",
[AppConnection.GitLab]: "GitLab",
[AppConnection.Cloudflare]: "Cloudflare",
[AppConnection.BitBucket]: "BitBucket"
[AppConnection.Bitbucket]: "Bitbucket"
};
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
@@ -63,5 +63,5 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.Flyio]: AppConnectionPlanType.Regular,
[AppConnection.GitLab]: AppConnectionPlanType.Regular,
[AppConnection.Cloudflare]: AppConnectionPlanType.Regular,
[AppConnection.BitBucket]: AppConnectionPlanType.Regular
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular
};
@@ -45,7 +45,7 @@ import { azureClientSecretsConnectionService } from "./azure-client-secrets/azur
import { ValidateAzureDevOpsConnectionCredentialsSchema } from "./azure-devops/azure-devops-schemas";
import { azureDevOpsConnectionService } from "./azure-devops/azure-devops-service";
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
import { ValidateBitBucketConnectionCredentialsSchema } from "./bitbucket";
import { ValidateBitbucketConnectionCredentialsSchema } from "./bitbucket";
import { bitBucketConnectionService } from "./bitbucket/bitbucket-connection-service";
import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
import { camundaConnectionService } from "./camunda/camunda-connection-service";
@@ -122,7 +122,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema,
[AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema,
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema,
[AppConnection.BitBucket]: ValidateBitBucketConnectionCredentialsSchema
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
@@ -57,10 +57,10 @@ import {
TValidateAzureKeyVaultConnectionCredentialsSchema
} from "./azure-key-vault";
import {
TBitBucketConnection,
TBitBucketConnectionConfig,
TBitBucketConnectionInput,
TValidateBitBucketConnectionCredentialsSchema
TBitbucketConnection,
TBitbucketConnectionConfig,
TBitbucketConnectionInput,
TValidateBitbucketConnectionCredentialsSchema
} from "./bitbucket";
import {
TCamundaConnection,
@@ -202,7 +202,7 @@ export type TAppConnection = { id: string } & (
| TFlyioConnection
| TGitLabConnection
| TCloudflareConnection
| TBitBucketConnection
| TBitbucketConnection
);
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -239,7 +239,7 @@ export type TAppConnectionInput = { id: string } & (
| TFlyioConnectionInput
| TGitLabConnectionInput
| TCloudflareConnectionInput
| TBitBucketConnectionInput
| TBitbucketConnectionInput
);
export type TSqlConnectionInput =
@@ -284,7 +284,7 @@ export type TAppConnectionConfig =
| TFlyioConnectionConfig
| TGitLabConnectionConfig
| TCloudflareConnectionConfig
| TBitBucketConnectionConfig;
| TBitbucketConnectionConfig;
export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema
@@ -316,7 +316,7 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateFlyioConnectionCredentialsSchema
| TValidateGitLabConnectionCredentialsSchema
| TValidateCloudflareConnectionCredentialsSchema
| TValidateBitBucketConnectionCredentialsSchema;
| TValidateBitbucketConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {
connectionId: string;
@@ -1,3 +1,3 @@
export enum BitBucketConnectionMethod {
export enum BitbucketConnectionMethod {
ApiToken = "api-token"
}
@@ -5,18 +5,23 @@ import { BadRequestError } from "@app/lib/errors";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { BitBucketConnectionMethod } from "./bitbucket-connection-enums";
import { TBitBucketConnection, TBitBucketConnectionConfig, TBitBucketRepo } from "./bitbucket-connection-types";
import { BitbucketConnectionMethod } from "./bitbucket-connection-enums";
import {
TBitbucketConnection,
TBitbucketConnectionConfig,
TBitbucketRepo,
TBitbucketWorkspace
} from "./bitbucket-connection-types";
export const getBitBucketConnectionListItem = () => {
export const getBitbucketConnectionListItem = () => {
return {
name: "BitBucket" as const,
app: AppConnection.BitBucket as const,
methods: Object.values(BitBucketConnectionMethod) as [BitBucketConnectionMethod.ApiToken]
name: "Bitbucket" as const,
app: AppConnection.Bitbucket as const,
methods: Object.values(BitbucketConnectionMethod) as [BitbucketConnectionMethod.ApiToken]
};
};
export const getBitBucketUser = async ({ email, apiToken }: { email: string; apiToken: string }) => {
export const getBitbucketUser = async ({ email, apiToken }: { email: string; apiToken: string }) => {
try {
const { data } = await request.get<{ username: string }>(`${IntegrationUrls.BITBUCKET_API_URL}/2.0/user`, {
headers: {
@@ -38,12 +43,17 @@ export const getBitBucketUser = async ({ email, apiToken }: { email: string; api
}
};
export const validateBitBucketConnectionCredentials = async (config: TBitBucketConnectionConfig) => {
await getBitBucketUser(config.credentials);
export const validateBitbucketConnectionCredentials = async (config: TBitbucketConnectionConfig) => {
await getBitbucketUser(config.credentials);
return config.credentials;
};
export const listBitBucketRepositories = async (appConnection: TBitBucketConnection) => {
interface BitbucketWorkspacesResponse {
values: TBitbucketWorkspace[];
next?: string;
}
export const listBitbucketWorkspaces = async (appConnection: TBitbucketConnection) => {
const { email, apiToken } = appConnection.credentials;
const headers = {
@@ -51,19 +61,52 @@ export const listBitBucketRepositories = async (appConnection: TBitBucketConnect
Accept: "application/json"
};
let allRepos: TBitBucketRepo[] = [];
let nextUrl: string | undefined = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories?role=member&pagelen=100`;
let allWorkspaces: TBitbucketWorkspace[] = [];
let nextUrl: string | undefined = `${IntegrationUrls.BITBUCKET_API_URL}/2.0/workspaces?pagelen=100`;
let iterationCount = 0;
// Limit to 10 iterations, fetching at most 10 * 100 = 1000 workspaces
while (nextUrl && iterationCount < 10) {
// eslint-disable-next-line no-await-in-loop
const { data }: { data: BitbucketWorkspacesResponse } = await request.get<BitbucketWorkspacesResponse>(nextUrl, {
headers
});
allWorkspaces = allWorkspaces.concat(data.values.map((workspace) => ({ slug: workspace.slug })));
nextUrl = data.next;
iterationCount += 1;
}
return allWorkspaces;
};
interface BitbucketRepositoriesResponse {
values: TBitbucketRepo[];
next?: string;
}
export const listBitbucketRepositories = async (appConnection: TBitbucketConnection, workspaceSlug: string) => {
const { email, apiToken } = appConnection.credentials;
const headers = {
Authorization: `Basic ${Buffer.from(`${email}:${apiToken}`).toString("base64")}`,
Accept: "application/json"
};
let allRepos: TBitbucketRepo[] = [];
let nextUrl: string | undefined =
`${IntegrationUrls.BITBUCKET_API_URL}/2.0/repositories/${workspaceSlug}?pagelen=100`;
let iterationCount = 0;
// Limit to 10 iterations, fetching at most 10 * 100 = 1000 repositories
while (nextUrl && iterationCount < 10) {
// eslint-disable-next-line no-await-in-loop
const { data }: { data: { values: TBitBucketRepo[]; next?: string } } = await request.get<{
values: TBitBucketRepo[];
next?: string;
}>(nextUrl, {
headers
});
const { data }: { data: BitbucketRepositoriesResponse } = await request.get<BitbucketRepositoriesResponse>(
nextUrl,
{
headers
}
);
allRepos = allRepos.concat(data.values);
nextUrl = data.next;
@@ -8,54 +8,54 @@ import {
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { BitBucketConnectionMethod } from "./bitbucket-connection-enums";
import { BitbucketConnectionMethod } from "./bitbucket-connection-enums";
export const BitBucketConnectionAccessTokenCredentialsSchema = z.object({
export const BitbucketConnectionAccessTokenCredentialsSchema = z.object({
apiToken: z.string().trim().min(1, "API Token required").describe(AppConnections.CREDENTIALS.BITBUCKET.apiToken),
email: z.string().email().trim().min(1, "Email required").describe(AppConnections.CREDENTIALS.BITBUCKET.email)
});
const BaseBitBucketConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.BitBucket) });
const BaseBitbucketConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Bitbucket) });
export const BitBucketConnectionSchema = BaseBitBucketConnectionSchema.extend({
method: z.literal(BitBucketConnectionMethod.ApiToken),
credentials: BitBucketConnectionAccessTokenCredentialsSchema
export const BitbucketConnectionSchema = BaseBitbucketConnectionSchema.extend({
method: z.literal(BitbucketConnectionMethod.ApiToken),
credentials: BitbucketConnectionAccessTokenCredentialsSchema
});
export const SanitizedBitBucketConnectionSchema = z.discriminatedUnion("method", [
BaseBitBucketConnectionSchema.extend({
method: z.literal(BitBucketConnectionMethod.ApiToken),
credentials: BitBucketConnectionAccessTokenCredentialsSchema.pick({
export const SanitizedBitbucketConnectionSchema = z.discriminatedUnion("method", [
BaseBitbucketConnectionSchema.extend({
method: z.literal(BitbucketConnectionMethod.ApiToken),
credentials: BitbucketConnectionAccessTokenCredentialsSchema.pick({
email: true
})
})
]);
export const ValidateBitBucketConnectionCredentialsSchema = z.discriminatedUnion("method", [
export const ValidateBitbucketConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(BitBucketConnectionMethod.ApiToken)
.describe(AppConnections.CREATE(AppConnection.BitBucket).method),
credentials: BitBucketConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.BitBucket).credentials
.literal(BitbucketConnectionMethod.ApiToken)
.describe(AppConnections.CREATE(AppConnection.Bitbucket).method),
credentials: BitbucketConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Bitbucket).credentials
)
})
]);
export const CreateBitBucketConnectionSchema = ValidateBitBucketConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.BitBucket)
export const CreateBitbucketConnectionSchema = ValidateBitbucketConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Bitbucket)
);
export const UpdateBitBucketConnectionSchema = z
export const UpdateBitbucketConnectionSchema = z
.object({
credentials: BitBucketConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.BitBucket).credentials
credentials: BitbucketConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Bitbucket).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.BitBucket));
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Bitbucket));
export const BitBucketConnectionListItemSchema = z.object({
name: z.literal("BitBucket"),
app: z.literal(AppConnection.BitBucket),
methods: z.nativeEnum(BitBucketConnectionMethod).array()
export const BitbucketConnectionListItemSchema = z.object({
name: z.literal("Bitbucket"),
app: z.literal(AppConnection.Bitbucket),
methods: z.nativeEnum(BitbucketConnectionMethod).array()
});
@@ -1,25 +1,33 @@
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listBitBucketRepositories } from "./bitbucket-connection-fns";
import { TBitBucketConnection } from "./bitbucket-connection-types";
import { listBitbucketRepositories, listBitbucketWorkspaces } from "./bitbucket-connection-fns";
import { TBitbucketConnection, TGetBitbucketRepositoriesDTO } from "./bitbucket-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TBitBucketConnection>;
) => Promise<TBitbucketConnection>;
export const bitBucketConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listRepositories = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.BitBucket, connectionId, actor);
const listWorkspaces = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Bitbucket, connectionId, actor);
const workspaces = await listBitbucketWorkspaces(appConnection);
return workspaces;
};
const repositories = await listBitBucketRepositories(appConnection);
return repositories.map((repo) => ({ id: repo.slug, name: repo.full_name }));
const listRepositories = async (
{ connectionId, workspaceSlug }: TGetBitbucketRepositoriesDTO,
actor: OrgServiceActor
) => {
const appConnection = await getAppConnection(AppConnection.Bitbucket, connectionId, actor);
const repositories = await listBitbucketRepositories(appConnection, workspaceSlug);
return repositories;
};
return {
listWorkspaces,
listRepositories
};
};
@@ -4,27 +4,36 @@ import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
BitBucketConnectionSchema,
CreateBitBucketConnectionSchema,
ValidateBitBucketConnectionCredentialsSchema
BitbucketConnectionSchema,
CreateBitbucketConnectionSchema,
ValidateBitbucketConnectionCredentialsSchema
} from "./bitbucket-connection-schemas";
export type TBitBucketConnection = z.infer<typeof BitBucketConnectionSchema>;
export type TBitbucketConnection = z.infer<typeof BitbucketConnectionSchema>;
export type TBitBucketConnectionInput = z.infer<typeof CreateBitBucketConnectionSchema> & {
app: AppConnection.BitBucket;
export type TBitbucketConnectionInput = z.infer<typeof CreateBitbucketConnectionSchema> & {
app: AppConnection.Bitbucket;
};
export type TValidateBitBucketConnectionCredentialsSchema = typeof ValidateBitBucketConnectionCredentialsSchema;
export type TValidateBitbucketConnectionCredentialsSchema = typeof ValidateBitbucketConnectionCredentialsSchema;
export type TBitBucketConnectionConfig = DiscriminativePick<
TBitBucketConnectionInput,
export type TBitbucketConnectionConfig = DiscriminativePick<
TBitbucketConnectionInput,
"method" | "app" | "credentials"
> & {
orgId: string;
};
export type TBitBucketRepo = {
export type TGetBitbucketRepositoriesDTO = {
connectionId: string;
workspaceSlug: string;
};
export type TBitbucketWorkspace = {
slug: string;
};
export type TBitbucketRepo = {
full_name: string; // workspace-slug/repo-slug
slug: string;
};