Files
dokploy/packages/server/src/utils/providers/bitbucket.ts
2025-09-27 08:56:28 +00:00

429 lines
11 KiB
TypeScript

import { createWriteStream } from "node:fs";
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 { Compose } from "@dokploy/server/services/compose";
import type { InferResultType } from "@dokploy/server/types/with";
import { TRPCError } from "@trpc/server";
import { recreateDirectory } from "../filesystem/directory";
import { execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";
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")}`,
};
};
export const cloneBitbucketRepository = async (
entity: ApplicationWithBitbucket | ComposeWithBitbucket,
logPath: string,
isCompose = false,
) => {
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths();
const writeStream = createWriteStream(logPath, { flags: "a" });
const {
appName,
bitbucketRepository,
bitbucketOwner,
bitbucketBranch,
bitbucketId,
bitbucket,
enableSubmodules,
} = entity;
if (!bitbucketId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Bitbucket Provider not found",
});
}
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
const cloneUrl = getBitbucketCloneUrl(bitbucket, repoclone);
try {
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
const cloneArgs = [
"clone",
"--branch",
bitbucketBranch!,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
];
await spawnAsync("git", cloneArgs, (data) => {
if (writeStream.writable) {
writeStream.write(data);
}
});
writeStream.write(`\nCloned ${repoclone} to ${outputPath}: ✅\n`);
} catch (error) {
writeStream.write(`ERROR Cloning: ${error}: ❌`);
throw error;
} finally {
writeStream.end();
}
};
export const cloneRawBitbucketRepository = async (entity: Compose) => {
const { COMPOSE_PATH } = paths();
const {
appName,
bitbucketRepository,
bitbucketOwner,
bitbucketBranch,
bitbucketId,
enableSubmodules,
} = entity;
if (!bitbucketId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Bitbucket Provider not found",
});
}
const bitbucketProvider = await findBitbucketById(bitbucketId);
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone);
try {
const cloneArgs = [
"clone",
"--branch",
bitbucketBranch!,
"--depth",
"1",
...(enableSubmodules ? ["--recurse-submodules"] : []),
cloneUrl,
outputPath,
"--progress",
];
await spawnAsync("git", cloneArgs);
} catch (error) {
throw error;
}
};
export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
const { COMPOSE_PATH } = paths(true);
const {
appName,
bitbucketRepository,
bitbucketOwner,
bitbucketBranch,
bitbucketId,
serverId,
enableSubmodules,
} = compose;
if (!serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server not found",
});
}
if (!bitbucketId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Bitbucket Provider not found",
});
}
const bitbucketProvider = await findBitbucketById(bitbucketId);
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone);
try {
const cloneCommand = `
rm -rf ${outputPath};
git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
`;
await execAsyncRemote(serverId, cloneCommand);
} catch (error) {
throw error;
}
};
export const getBitbucketCloneCommand = async (
entity: ApplicationWithBitbucket | ComposeWithBitbucket,
logPath: string,
isCompose = false,
) => {
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
const {
appName,
bitbucketRepository,
bitbucketOwner,
bitbucketBranch,
bitbucketId,
serverId,
enableSubmodules,
} = entity;
if (!serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server not found",
});
}
if (!bitbucketId) {
const command = `
echo "Error: ❌ Bitbucket Provider not found" >> ${logPath};
exit 1;
`;
await execAsyncRemote(serverId, command);
throw new TRPCError({
code: "NOT_FOUND",
message: "Bitbucket Provider not found",
});
}
const bitbucketProvider = await findBitbucketById(bitbucketId);
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone);
const cloneCommand = `
rm -rf ${outputPath};
mkdir -p ${outputPath};
if ! git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
exit 1;
fi
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
`;
return cloneCommand;
};
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;
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,
owner: {
username: repo.workspace.slug,
},
}));
repositories = repositories.concat(mappedData);
url = data.next || null;
}
return repositories;
} catch (error) {
throw error;
}
};
export const getBitbucketBranches = async (
input: typeof apiFindBitbucketBranches._type,
) => {
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: typeof apiBitbucketTestConnection._type,
) => {
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;
}
};