mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-20 14:45:42 +02:00
281 lines
7.2 KiB
TypeScript
281 lines
7.2 KiB
TypeScript
import { join } from "node:path";
|
|
import { paths } from "@dokploy/server/constants";
|
|
import type {
|
|
apiBitbucketTestConnection,
|
|
apiFindBitbucketBranches,
|
|
} from "@dokploy/server/db/schema";
|
|
import {
|
|
type Bitbucket,
|
|
findBitbucketById,
|
|
} from "@dokploy/server/services/bitbucket";
|
|
import type { InferResultType } from "@dokploy/server/types/with";
|
|
import { TRPCError } from "@trpc/server";
|
|
import type { z } from "zod";
|
|
|
|
export type ApplicationWithBitbucket = InferResultType<
|
|
"applications",
|
|
{ bitbucket: true }
|
|
>;
|
|
|
|
export type ComposeWithBitbucket = InferResultType<
|
|
"compose",
|
|
{ bitbucket: true }
|
|
>;
|
|
|
|
export const getBitbucketCloneUrl = (
|
|
bitbucketProvider: {
|
|
apiToken?: string | null;
|
|
bitbucketUsername?: string | null;
|
|
appPassword?: string | null;
|
|
bitbucketEmail?: string | null;
|
|
bitbucketWorkspaceName?: string | null;
|
|
} | null,
|
|
repoClone: string,
|
|
) => {
|
|
if (!bitbucketProvider) {
|
|
throw new Error("Bitbucket provider is required");
|
|
}
|
|
|
|
if (bitbucketProvider.apiToken) {
|
|
return `https://x-bitbucket-api-token-auth:${bitbucketProvider.apiToken}@${repoClone}`;
|
|
}
|
|
|
|
// For app passwords, use username:app_password format
|
|
if (!bitbucketProvider.bitbucketUsername || !bitbucketProvider.appPassword) {
|
|
throw new Error(
|
|
"Username and app password are required when not using API token",
|
|
);
|
|
}
|
|
return `https://${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}@${repoClone}`;
|
|
};
|
|
|
|
export const getBitbucketHeaders = (bitbucketProvider: Bitbucket) => {
|
|
if (bitbucketProvider.apiToken) {
|
|
// According to Bitbucket official docs, for API calls with API tokens:
|
|
// "You will need both your Atlassian account email and an API token"
|
|
// Use: {atlassian_account_email}:{api_token}
|
|
|
|
if (!bitbucketProvider.bitbucketEmail) {
|
|
throw new Error(
|
|
"Atlassian account email is required when using API token for API calls",
|
|
);
|
|
}
|
|
|
|
return {
|
|
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketEmail}:${bitbucketProvider.apiToken}`).toString("base64")}`,
|
|
};
|
|
}
|
|
|
|
// For app passwords, use HTTP Basic auth with username and app password
|
|
if (!bitbucketProvider.bitbucketUsername || !bitbucketProvider.appPassword) {
|
|
throw new Error(
|
|
"Username and app password are required when not using API token",
|
|
);
|
|
}
|
|
return {
|
|
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
|
|
};
|
|
};
|
|
|
|
interface CloneBitbucketRepository {
|
|
appName: string;
|
|
bitbucketRepository: string | null;
|
|
bitbucketRepositorySlug?: string | null;
|
|
bitbucketOwner: string | null;
|
|
bitbucketBranch: string | null;
|
|
bitbucketId: string | null;
|
|
enableSubmodules: boolean;
|
|
serverId: string | null;
|
|
type?: "application" | "compose";
|
|
outputPathOverride?: string;
|
|
}
|
|
|
|
export const cloneBitbucketRepository = async ({
|
|
type = "application",
|
|
...entity
|
|
}: CloneBitbucketRepository) => {
|
|
let command = "set -e;";
|
|
const {
|
|
appName,
|
|
bitbucketRepository,
|
|
bitbucketOwner,
|
|
bitbucketBranch,
|
|
bitbucketId,
|
|
enableSubmodules,
|
|
serverId,
|
|
outputPathOverride,
|
|
} = entity;
|
|
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId);
|
|
|
|
if (!bitbucketId) {
|
|
command += `echo "Error: ❌ Bitbucket Provider not found"; exit 1;`;
|
|
return command;
|
|
}
|
|
const bitbucket = await findBitbucketById(bitbucketId);
|
|
|
|
if (!bitbucket) {
|
|
command += `echo "Error: ❌ Bitbucket Provider not found"; exit 1;`;
|
|
return command;
|
|
}
|
|
const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;
|
|
const outputPath = outputPathOverride ?? join(basePath, appName, "code");
|
|
command += `rm -rf ${outputPath};`;
|
|
command += `mkdir -p ${outputPath};`;
|
|
const repoToUse = entity.bitbucketRepositorySlug || bitbucketRepository;
|
|
const repoclone = `bitbucket.org/${bitbucketOwner}/${repoToUse}.git`;
|
|
const cloneUrl = getBitbucketCloneUrl(bitbucket, repoclone);
|
|
command += `echo "Cloning Repo ${repoclone} to ${outputPath}: ✅";`;
|
|
command += `git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`;
|
|
return command;
|
|
};
|
|
|
|
export const getBitbucketRepositories = async (bitbucketId?: string) => {
|
|
if (!bitbucketId) {
|
|
return [];
|
|
}
|
|
const bitbucketProvider = await findBitbucketById(bitbucketId);
|
|
|
|
const username =
|
|
bitbucketProvider.bitbucketWorkspaceName ||
|
|
bitbucketProvider.bitbucketUsername;
|
|
let url = `https://api.bitbucket.org/2.0/repositories/${username}?pagelen=100`;
|
|
let repositories: {
|
|
name: string;
|
|
url: string;
|
|
slug: string;
|
|
owner: { username: string };
|
|
}[] = [];
|
|
|
|
try {
|
|
while (url) {
|
|
const response = await fetch(url, {
|
|
method: "GET",
|
|
headers: getBitbucketHeaders(bitbucketProvider),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: `Failed to fetch repositories: ${response.statusText}`,
|
|
});
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
const mappedData = data.values.map((repo: any) => ({
|
|
name: repo.name,
|
|
url: repo.links.html.href,
|
|
slug: repo.slug,
|
|
owner: {
|
|
username: repo.workspace.slug,
|
|
},
|
|
}));
|
|
repositories = repositories.concat(mappedData);
|
|
url = data.next || null;
|
|
}
|
|
return repositories;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
export const getBitbucketBranches = async (
|
|
input: z.infer<typeof apiFindBitbucketBranches>,
|
|
) => {
|
|
if (!input.bitbucketId) {
|
|
return [];
|
|
}
|
|
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
|
const { owner, repo } = input;
|
|
let url = `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/refs/branches?pagelen=1`;
|
|
let allBranches: {
|
|
name: string;
|
|
commit: {
|
|
sha: string;
|
|
};
|
|
}[] = [];
|
|
|
|
try {
|
|
while (url) {
|
|
const response = await fetch(url, {
|
|
method: "GET",
|
|
headers: getBitbucketHeaders(bitbucketProvider),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: `HTTP error! status: ${response.status}`,
|
|
});
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
const mappedData = data.values.map((branch: any) => {
|
|
return {
|
|
name: branch.name,
|
|
commit: {
|
|
sha: branch.target.hash,
|
|
},
|
|
};
|
|
});
|
|
allBranches = allBranches.concat(mappedData);
|
|
url = data.next || null;
|
|
}
|
|
|
|
return allBranches as {
|
|
name: string;
|
|
commit: {
|
|
sha: string;
|
|
};
|
|
}[];
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
export const testBitbucketConnection = async (
|
|
input: z.infer<typeof apiBitbucketTestConnection>,
|
|
) => {
|
|
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
|
|
|
if (!bitbucketProvider) {
|
|
throw new Error("Bitbucket provider not found");
|
|
}
|
|
|
|
const { bitbucketUsername, workspaceName } = input;
|
|
|
|
const username = workspaceName || bitbucketUsername;
|
|
|
|
const url = `https://api.bitbucket.org/2.0/repositories/${username}`;
|
|
try {
|
|
const response = await fetch(url, {
|
|
method: "GET",
|
|
headers: getBitbucketHeaders(bitbucketProvider),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: `Failed to fetch repositories: ${response.statusText}`,
|
|
});
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
const mappedData = data.values.map((repo: any) => {
|
|
return {
|
|
name: repo.name,
|
|
url: repo.links.html.href,
|
|
owner: {
|
|
username: repo.workspace.slug,
|
|
},
|
|
};
|
|
}) as [];
|
|
|
|
return mappedData.length;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
};
|