mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-02 10:12:15 +00:00
Merge branch 'canary' into fix/gitlab-branches-pagination
This commit is contained in:
@@ -37,6 +37,7 @@ export const cloneBitbucketRepository = async (
|
||||
bitbucketBranch,
|
||||
bitbucketId,
|
||||
bitbucket,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!bitbucketId) {
|
||||
@@ -53,28 +54,26 @@ export const cloneBitbucketRepository = async (
|
||||
const cloneUrl = `https://${bitbucket?.bitbucketUsername}:${bitbucket?.appPassword}@${repoclone}`;
|
||||
try {
|
||||
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
bitbucketBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
"--recurse-submodules",
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
);
|
||||
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 Clonning: ${error}: ❌`);
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
@@ -89,6 +88,7 @@ export const cloneRawBitbucketRepository = async (entity: Compose) => {
|
||||
bitbucketOwner,
|
||||
bitbucketBranch,
|
||||
bitbucketId,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!bitbucketId) {
|
||||
@@ -106,17 +106,19 @@ export const cloneRawBitbucketRepository = async (entity: Compose) => {
|
||||
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
||||
|
||||
try {
|
||||
await spawnAsync("git", [
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
bitbucketBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
"--recurse-submodules",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
]);
|
||||
];
|
||||
|
||||
await spawnAsync("git", cloneArgs);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -131,6 +133,7 @@ export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
|
||||
bitbucketBranch,
|
||||
bitbucketId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = compose;
|
||||
|
||||
if (!serverId) {
|
||||
@@ -153,11 +156,11 @@ export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
|
||||
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
||||
|
||||
try {
|
||||
const command = `
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
git clone --branch ${bitbucketBranch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath}
|
||||
git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
await execAsyncRemote(serverId, cloneCommand);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -176,7 +179,7 @@ export const getBitbucketCloneCommand = async (
|
||||
bitbucketBranch,
|
||||
bitbucketId,
|
||||
serverId,
|
||||
bitbucket,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!serverId) {
|
||||
@@ -208,7 +211,7 @@ export const getBitbucketCloneCommand = async (
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
if ! git clone --branch ${bitbucketBranch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
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
|
||||
@@ -276,7 +279,7 @@ export const getBitbucketBranches = async (
|
||||
}
|
||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||
const { owner, repo } = input;
|
||||
const url = `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/refs/branches`;
|
||||
const url = `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/refs/branches?pagelen=100`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
|
||||
@@ -53,7 +53,7 @@ export const buildRemoteDocker = async (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { sourceType, dockerImage, username, password } = application;
|
||||
const { registryUrl, dockerImage, username, password } = application;
|
||||
|
||||
try {
|
||||
if (!dockerImage) {
|
||||
@@ -65,7 +65,7 @@ echo "Pulling ${dockerImage}" >> ${logPath};
|
||||
|
||||
if (username && password) {
|
||||
command += `
|
||||
if ! docker login --username ${username} --password ${password} https://index.docker.io/v1/ >> ${logPath} 2>&1; then
|
||||
if ! echo "${password}" | docker login --username "${username}" --password-stdin "${registryUrl || ""}" >> ${logPath} 2>&1; then
|
||||
echo "❌ Login failed" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
@@ -17,12 +17,19 @@ export const cloneGitRepository = async (
|
||||
customGitUrl?: string | null;
|
||||
customGitBranch?: string | null;
|
||||
customGitSSHKeyId?: string | null;
|
||||
enableSubmodules?: boolean;
|
||||
},
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths();
|
||||
const { appName, customGitUrl, customGitBranch, customGitSSHKeyId } = entity;
|
||||
const {
|
||||
appName,
|
||||
customGitUrl,
|
||||
customGitBranch,
|
||||
customGitSSHKeyId,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!customGitUrl || !customGitBranch) {
|
||||
throw new TRPCError({
|
||||
@@ -69,19 +76,22 @@ export const cloneGitRepository = async (
|
||||
});
|
||||
}
|
||||
|
||||
const { port } = sanitizeRepoPathSSH(customGitUrl);
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
customGitBranch,
|
||||
"--depth",
|
||||
"1",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
customGitUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
];
|
||||
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
customGitBranch,
|
||||
"--depth",
|
||||
"1",
|
||||
"--recurse-submodules",
|
||||
customGitUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
],
|
||||
cloneArgs,
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
@@ -91,7 +101,7 @@ export const cloneGitRepository = async (
|
||||
env: {
|
||||
...process.env,
|
||||
...(customGitSSHKeyId && {
|
||||
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath} -o UserKnownHostsFile=${knownHostsPath}`,
|
||||
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`,
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -113,6 +123,7 @@ export const getCustomGitCloneCommand = async (
|
||||
customGitBranch?: string | null;
|
||||
customGitSSHKeyId?: string | null;
|
||||
serverId: string | null;
|
||||
enableSubmodules: boolean;
|
||||
},
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
@@ -124,6 +135,7 @@ export const getCustomGitCloneCommand = async (
|
||||
customGitBranch,
|
||||
customGitSSHKeyId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!customGitUrl || !customGitBranch) {
|
||||
@@ -168,7 +180,8 @@ export const getCustomGitCloneCommand = async (
|
||||
);
|
||||
if (customGitSSHKeyId) {
|
||||
const sshKey = await findSSHKeyById(customGitSSHKeyId);
|
||||
const gitSshCommand = `ssh -i /tmp/id_rsa -o UserKnownHostsFile=${knownHostsPath}`;
|
||||
const { port } = sanitizeRepoPathSSH(customGitUrl);
|
||||
const gitSshCommand = `ssh -i /tmp/id_rsa${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`;
|
||||
command.push(
|
||||
`
|
||||
echo "${sshKey.privateKey}" > /tmp/id_rsa
|
||||
@@ -179,7 +192,7 @@ export const getCustomGitCloneCommand = async (
|
||||
}
|
||||
|
||||
command.push(
|
||||
`if ! git clone --branch ${customGitBranch} --depth 1 --recurse-submodules --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
`if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
@@ -259,8 +272,15 @@ export const cloneGitRawRepository = async (entity: {
|
||||
customGitUrl?: string | null;
|
||||
customGitBranch?: string | null;
|
||||
customGitSSHKeyId?: string | null;
|
||||
enableSubmodules?: boolean;
|
||||
}) => {
|
||||
const { appName, customGitUrl, customGitBranch, customGitSSHKeyId } = entity;
|
||||
const {
|
||||
appName,
|
||||
customGitUrl,
|
||||
customGitBranch,
|
||||
customGitSSHKeyId,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!customGitUrl || !customGitBranch) {
|
||||
throw new TRPCError({
|
||||
@@ -304,29 +324,27 @@ export const cloneGitRawRepository = async (entity: {
|
||||
});
|
||||
}
|
||||
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
customGitBranch,
|
||||
"--depth",
|
||||
"1",
|
||||
"--recurse-submodules",
|
||||
customGitUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
],
|
||||
(data) => {},
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
...(customGitSSHKeyId && {
|
||||
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath} -o UserKnownHostsFile=${knownHostsPath}`,
|
||||
}),
|
||||
},
|
||||
const { port } = sanitizeRepoPathSSH(customGitUrl);
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
customGitBranch,
|
||||
"--depth",
|
||||
"1",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
customGitUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
];
|
||||
|
||||
await spawnAsync("git", cloneArgs, (_data) => {}, {
|
||||
env: {
|
||||
...process.env,
|
||||
...(customGitSSHKeyId && {
|
||||
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`,
|
||||
}),
|
||||
},
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -339,6 +357,7 @@ export const cloneRawGitRepositoryRemote = async (compose: Compose) => {
|
||||
customGitUrl,
|
||||
customGitSSHKeyId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = compose;
|
||||
|
||||
if (!serverId) {
|
||||
@@ -381,7 +400,8 @@ export const cloneRawGitRepositoryRemote = async (compose: Compose) => {
|
||||
command.push(`mkdir -p ${outputPath};`);
|
||||
if (customGitSSHKeyId) {
|
||||
const sshKey = await findSSHKeyById(customGitSSHKeyId);
|
||||
const gitSshCommand = `ssh -i /tmp/id_rsa -o UserKnownHostsFile=${knownHostsPath}`;
|
||||
const { port } = sanitizeRepoPathSSH(customGitUrl);
|
||||
const gitSshCommand = `ssh -i /tmp/id_rsa${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`;
|
||||
command.push(
|
||||
`
|
||||
echo "${sshKey.privateKey}" > /tmp/id_rsa
|
||||
@@ -392,7 +412,7 @@ export const cloneRawGitRepositoryRemote = async (compose: Compose) => {
|
||||
}
|
||||
|
||||
command.push(
|
||||
`if ! git clone --branch ${customGitBranch} --depth 1 --recurse-submodules --progress ${customGitUrl} ${outputPath} ; then
|
||||
`if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath} ; then
|
||||
echo "[ERROR] Fail to clone the repository ";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,516 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import type { Compose } from "@dokploy/server/services/compose";
|
||||
import {
|
||||
type Gitea,
|
||||
findGiteaById,
|
||||
updateGitea,
|
||||
} from "@dokploy/server/services/gitea";
|
||||
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 const getErrorCloneRequirements = (entity: {
|
||||
giteaRepository?: string | null;
|
||||
giteaOwner?: string | null;
|
||||
giteaBranch?: string | null;
|
||||
}) => {
|
||||
const reasons: string[] = [];
|
||||
const { giteaBranch, giteaOwner, giteaRepository } = entity;
|
||||
|
||||
if (!giteaRepository) reasons.push("1. Repository not assigned.");
|
||||
if (!giteaOwner) reasons.push("2. Owner not specified.");
|
||||
if (!giteaBranch) reasons.push("3. Branch not defined.");
|
||||
|
||||
return reasons;
|
||||
};
|
||||
|
||||
export const refreshGiteaToken = async (giteaProviderId: string) => {
|
||||
try {
|
||||
const giteaProvider = await findGiteaById(giteaProviderId);
|
||||
|
||||
if (
|
||||
!giteaProvider?.clientId ||
|
||||
!giteaProvider?.clientSecret ||
|
||||
!giteaProvider?.refreshToken
|
||||
) {
|
||||
return giteaProvider?.accessToken || null;
|
||||
}
|
||||
|
||||
// Check if token is still valid (add some buffer time, e.g., 5 minutes)
|
||||
const currentTimeSeconds = Math.floor(Date.now() / 1000);
|
||||
const bufferTimeSeconds = 300; // 5 minutes
|
||||
|
||||
if (
|
||||
giteaProvider.expiresAt &&
|
||||
giteaProvider.expiresAt > currentTimeSeconds + bufferTimeSeconds &&
|
||||
giteaProvider.accessToken
|
||||
) {
|
||||
// Token is still valid, no need to refresh
|
||||
return giteaProvider.accessToken;
|
||||
}
|
||||
|
||||
// Token is expired or about to expire, refresh it
|
||||
const tokenEndpoint = `${giteaProvider.giteaUrl}/login/oauth/access_token`;
|
||||
const params = new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: giteaProvider.refreshToken,
|
||||
client_id: giteaProvider.clientId,
|
||||
client_secret: giteaProvider.clientSecret,
|
||||
});
|
||||
|
||||
const response = await fetch(tokenEndpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: params.toString(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return giteaProvider?.accessToken || null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const { access_token, refresh_token, expires_in } = data;
|
||||
|
||||
if (!access_token) {
|
||||
return giteaProvider?.accessToken || null;
|
||||
}
|
||||
|
||||
const expiresAt = Date.now() + (expires_in || 3600) * 1000;
|
||||
const expiresAtSeconds = Math.floor(expiresAt / 1000);
|
||||
|
||||
await updateGitea(giteaProviderId, {
|
||||
accessToken: access_token,
|
||||
refreshToken: refresh_token || giteaProvider.refreshToken,
|
||||
expiresAt: expiresAtSeconds,
|
||||
});
|
||||
|
||||
return access_token;
|
||||
} catch (error) {
|
||||
console.error("Error refreshing Gitea token:", error);
|
||||
const giteaProvider = await findGiteaById(giteaProviderId);
|
||||
return giteaProvider?.accessToken || null;
|
||||
}
|
||||
};
|
||||
|
||||
export type ApplicationWithGitea = InferResultType<
|
||||
"applications",
|
||||
{ gitea: true }
|
||||
>;
|
||||
|
||||
export type ComposeWithGitea = InferResultType<"compose", { gitea: true }>;
|
||||
|
||||
export const getGiteaCloneCommand = async (
|
||||
entity: ApplicationWithGitea | ComposeWithGitea,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const {
|
||||
appName,
|
||||
giteaBranch,
|
||||
giteaId,
|
||||
giteaOwner,
|
||||
giteaRepository,
|
||||
serverId,
|
||||
gitea,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (!giteaId) {
|
||||
const command = `
|
||||
echo "Error: ❌ Gitlab Provider not found" >> ${logPath};
|
||||
exit 1;
|
||||
`;
|
||||
|
||||
await execAsyncRemote(serverId, command);
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Use paths(true) for remote operations
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
|
||||
await refreshGiteaToken(giteaId);
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
|
||||
const baseUrl = gitea?.giteaUrl.replace(/^https?:\/\//, "");
|
||||
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitea?.accessToken}@${baseUrl}/${repoClone}`;
|
||||
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
|
||||
if ! git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Failed to clone the repository ${repoClone}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
echo "Cloned ${repoClone} to ${outputPath}: ✅" >> ${logPath};
|
||||
`;
|
||||
|
||||
return cloneCommand;
|
||||
};
|
||||
|
||||
export const cloneGiteaRepository = async (
|
||||
entity: ApplicationWithGitea | ComposeWithGitea,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
|
||||
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const {
|
||||
appName,
|
||||
giteaBranch,
|
||||
giteaId,
|
||||
giteaOwner,
|
||||
giteaRepository,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!giteaId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
await refreshGiteaToken(giteaId);
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
if (!giteaProvider) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea provider not found in the database",
|
||||
});
|
||||
}
|
||||
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
|
||||
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, "");
|
||||
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`;
|
||||
|
||||
writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}...\n`);
|
||||
|
||||
try {
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
giteaBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
);
|
||||
writeStream.write(`\nCloned ${repoClone}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawGiteaRepository = async (entity: Compose) => {
|
||||
const {
|
||||
appName,
|
||||
giteaRepository,
|
||||
giteaOwner,
|
||||
giteaBranch,
|
||||
giteaId,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
const { COMPOSE_PATH } = paths();
|
||||
|
||||
if (!giteaId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea Provider not found",
|
||||
});
|
||||
}
|
||||
await refreshGiteaToken(giteaId);
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
if (!giteaProvider) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea provider not found in the database",
|
||||
});
|
||||
}
|
||||
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
|
||||
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, "");
|
||||
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`;
|
||||
|
||||
try {
|
||||
await spawnAsync("git", [
|
||||
"clone",
|
||||
"--branch",
|
||||
giteaBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
]);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawGiteaRepositoryRemote = async (compose: Compose) => {
|
||||
const {
|
||||
appName,
|
||||
giteaRepository,
|
||||
giteaOwner,
|
||||
giteaBranch,
|
||||
giteaId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = compose;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
if (!giteaId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea Provider not found",
|
||||
});
|
||||
}
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const repoClone = `${giteaOwner}/${giteaRepository}.git`;
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, "");
|
||||
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`;
|
||||
try {
|
||||
const command = `
|
||||
rm -rf ${outputPath};
|
||||
git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const haveGiteaRequirements = (giteaProvider: Gitea) => {
|
||||
return !!(giteaProvider?.clientId && giteaProvider?.clientSecret);
|
||||
};
|
||||
|
||||
export const testGiteaConnection = async (input: { giteaId: string }) => {
|
||||
try {
|
||||
const { giteaId } = input;
|
||||
|
||||
if (!giteaId) {
|
||||
throw new Error("Gitea provider not found");
|
||||
}
|
||||
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
if (!giteaProvider) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitea provider not found in the database",
|
||||
});
|
||||
}
|
||||
|
||||
await refreshGiteaToken(giteaId);
|
||||
|
||||
const provider = await findGiteaById(giteaId);
|
||||
if (!provider || !provider.accessToken) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "No access token available. Please authorize with Gitea.",
|
||||
});
|
||||
}
|
||||
|
||||
const baseUrl = provider.giteaUrl.replace(/\/+$/, "");
|
||||
const limit = 30;
|
||||
let allRepos = 0;
|
||||
let nextUrl = `${baseUrl}/api/v1/repos/search?limit=${limit}`;
|
||||
|
||||
while (nextUrl) {
|
||||
const response = await fetch(nextUrl, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `token ${provider.accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to connect to Gitea API: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const repos = await response.json();
|
||||
allRepos += repos.data.length;
|
||||
|
||||
const linkHeader = response.headers.get("link");
|
||||
nextUrl = "";
|
||||
|
||||
if (linkHeader) {
|
||||
const nextLink = linkHeader
|
||||
.split(",")
|
||||
.find((link) => link.includes('rel="next"'));
|
||||
if (nextLink) {
|
||||
const matches = nextLink.match(/<([^>]+)>/);
|
||||
if (matches?.[1]) {
|
||||
nextUrl = matches[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await updateGitea(giteaId, {
|
||||
lastAuthenticatedAt: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
|
||||
return allRepos;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getGiteaRepositories = async (giteaId?: string) => {
|
||||
if (!giteaId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
await refreshGiteaToken(giteaId);
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
|
||||
const limit = 30;
|
||||
let allRepositories: any[] = [];
|
||||
let nextUrl = `${baseUrl}/api/v1/repos/search?limit=${limit}`;
|
||||
|
||||
while (nextUrl) {
|
||||
const response = await fetch(nextUrl, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `token ${giteaProvider.accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
allRepositories = [...allRepositories, ...result.data];
|
||||
|
||||
const linkHeader = response.headers.get("link");
|
||||
nextUrl = "";
|
||||
|
||||
if (linkHeader) {
|
||||
const nextLink = linkHeader
|
||||
.split(",")
|
||||
.find((link) => link.includes('rel="next"'));
|
||||
if (nextLink) {
|
||||
const matches = nextLink.match(/<([^>]+)>/);
|
||||
if (matches?.[1]) {
|
||||
nextUrl = matches[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
allRepositories?.map((repo: any) => ({
|
||||
id: repo.id,
|
||||
name: repo.name,
|
||||
url: repo.full_name,
|
||||
owner: {
|
||||
username: repo.owner.login,
|
||||
},
|
||||
})) || []
|
||||
);
|
||||
};
|
||||
|
||||
export const getGiteaBranches = async (input: {
|
||||
giteaId?: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
}) => {
|
||||
if (!input.giteaId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
await refreshGiteaToken(input.giteaId);
|
||||
|
||||
const giteaProvider = await findGiteaById(input.giteaId);
|
||||
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
|
||||
const url = `${baseUrl}/api/v1/repos/${input.owner}/${input.repo}/branches`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `token ${giteaProvider.accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch branches: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const branches = await response.json();
|
||||
|
||||
if (!branches) {
|
||||
return [];
|
||||
}
|
||||
return branches?.map((branch: any) => ({
|
||||
id: branch.name,
|
||||
name: branch.name,
|
||||
commit: {
|
||||
id: branch.commit.id,
|
||||
},
|
||||
})) as {
|
||||
id: string;
|
||||
name: string;
|
||||
commit: {
|
||||
id: string;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
@@ -45,6 +45,49 @@ export const getGithubToken = async (
|
||||
return installation.token;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a GitHub user has write/admin permissions on a repository
|
||||
* This is used to validate PR authors before allowing preview deployments
|
||||
*/
|
||||
export const checkUserRepositoryPermissions = async (
|
||||
githubProvider: Github,
|
||||
owner: string,
|
||||
repo: string,
|
||||
username: string,
|
||||
): Promise<{ hasWriteAccess: boolean; permission: string | null }> => {
|
||||
try {
|
||||
const octokit = authGithub(githubProvider);
|
||||
|
||||
// Check if user is a collaborator with write permissions
|
||||
const { data: permission } =
|
||||
await octokit.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner,
|
||||
repo,
|
||||
username,
|
||||
});
|
||||
|
||||
// Allow only users with 'write', 'admin', or 'maintain' permissions
|
||||
// Currently exists Read, Triage, Write, Maintain, Admin
|
||||
const allowedPermissions = ["write", "admin", "maintain"];
|
||||
const hasWriteAccess = allowedPermissions.includes(permission.permission);
|
||||
|
||||
return {
|
||||
hasWriteAccess,
|
||||
permission: permission.permission,
|
||||
};
|
||||
} catch (error) {
|
||||
// If user is not a collaborator, GitHub API returns 404
|
||||
console.warn(
|
||||
`User ${username} is not a collaborator of ${owner}/${repo}:`,
|
||||
error,
|
||||
);
|
||||
return {
|
||||
hasWriteAccess: false,
|
||||
permission: null,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const haveGithubRequirements = (githubProvider: Github) => {
|
||||
return !!(
|
||||
githubProvider?.githubAppId &&
|
||||
@@ -83,6 +126,7 @@ interface CloneGithubRepository {
|
||||
repository: string | null;
|
||||
logPath: string;
|
||||
type?: "application" | "compose";
|
||||
enableSubmodules: boolean;
|
||||
}
|
||||
export const cloneGithubRepository = async ({
|
||||
logPath,
|
||||
@@ -92,7 +136,8 @@ export const cloneGithubRepository = async ({
|
||||
const isCompose = type === "compose";
|
||||
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const { appName, repository, owner, branch, githubId } = entity;
|
||||
const { appName, repository, owner, branch, githubId, enableSubmodules } =
|
||||
entity;
|
||||
|
||||
if (!githubId) {
|
||||
throw new TRPCError({
|
||||
@@ -128,28 +173,26 @@ export const cloneGithubRepository = async ({
|
||||
|
||||
try {
|
||||
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
branch!,
|
||||
"--depth",
|
||||
"1",
|
||||
"--recurse-submodules",
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
);
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
branch!,
|
||||
"--depth",
|
||||
"1",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
];
|
||||
|
||||
await spawnAsync("git", cloneArgs, (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
});
|
||||
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
@@ -161,7 +204,15 @@ export const getGithubCloneCommand = async ({
|
||||
type = "application",
|
||||
...entity
|
||||
}: CloneGithubRepository & { serverId: string }) => {
|
||||
const { appName, repository, owner, branch, githubId, serverId } = entity;
|
||||
const {
|
||||
appName,
|
||||
repository,
|
||||
owner,
|
||||
branch,
|
||||
githubId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
const isCompose = type === "compose";
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
@@ -216,7 +267,7 @@ export const getGithubCloneCommand = async ({
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
if ! git clone --branch ${branch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
if ! git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Fail to clone repository ${repoclone}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
@@ -227,7 +278,8 @@ echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
|
||||
};
|
||||
|
||||
export const cloneRawGithubRepository = async (entity: Compose) => {
|
||||
const { appName, repository, owner, branch, githubId } = entity;
|
||||
const { appName, repository, owner, branch, githubId, enableSubmodules } =
|
||||
entity;
|
||||
|
||||
if (!githubId) {
|
||||
throw new TRPCError({
|
||||
@@ -245,24 +297,33 @@ export const cloneRawGithubRepository = async (entity: Compose) => {
|
||||
await recreateDirectory(outputPath);
|
||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
||||
try {
|
||||
await spawnAsync("git", [
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
branch!,
|
||||
"--depth",
|
||||
"1",
|
||||
"--recurse-submodules",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
]);
|
||||
];
|
||||
await spawnAsync("git", cloneArgs);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
|
||||
const { appName, repository, owner, branch, githubId, serverId } = compose;
|
||||
const {
|
||||
appName,
|
||||
repository,
|
||||
owner,
|
||||
branch,
|
||||
githubId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = compose;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
@@ -288,7 +349,7 @@ export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
|
||||
try {
|
||||
const command = `
|
||||
rm -rf ${outputPath};
|
||||
git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}
|
||||
git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
} catch (error) {
|
||||
|
||||
@@ -4,8 +4,8 @@ import { paths } from "@dokploy/server/constants";
|
||||
import type { apiGitlabTestConnection } from "@dokploy/server/db/schema";
|
||||
import type { Compose } from "@dokploy/server/services/compose";
|
||||
import {
|
||||
type Gitlab,
|
||||
findGitlabById,
|
||||
type Gitlab,
|
||||
updateGitlab,
|
||||
} from "@dokploy/server/services/gitlab";
|
||||
import type { InferResultType } from "@dokploy/server/types/with";
|
||||
@@ -26,7 +26,7 @@ export const refreshGitlabToken = async (gitlabProviderId: string) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch("https://gitlab.com/oauth/token", {
|
||||
const response = await fetch(`${gitlabProvider.gitlabUrl}/oauth/token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
@@ -84,14 +84,38 @@ export type ApplicationWithGitlab = InferResultType<
|
||||
|
||||
export type ComposeWithGitlab = InferResultType<"compose", { gitlab: true }>;
|
||||
|
||||
export type GitlabInfo =
|
||||
| ApplicationWithGitlab["gitlab"]
|
||||
| ComposeWithGitlab["gitlab"];
|
||||
|
||||
const getGitlabRepoClone = (
|
||||
gitlab: GitlabInfo,
|
||||
gitlabPathNamespace: string | null,
|
||||
) => {
|
||||
const repoClone = `${gitlab?.gitlabUrl.replace(/^https?:\/\//, "")}/${gitlabPathNamespace}.git`;
|
||||
return repoClone;
|
||||
};
|
||||
|
||||
const getGitlabCloneUrl = (gitlab: GitlabInfo, repoClone: string) => {
|
||||
const isSecure = gitlab?.gitlabUrl.startsWith("https://");
|
||||
const cloneUrl = `http${isSecure ? "s" : ""}://oauth2:${gitlab?.accessToken}@${repoClone}`;
|
||||
return cloneUrl;
|
||||
};
|
||||
|
||||
export const cloneGitlabRepository = async (
|
||||
entity: ApplicationWithGitlab | ComposeWithGitlab,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const { appName, gitlabBranch, gitlabId, gitlab, gitlabPathNamespace } =
|
||||
entity;
|
||||
const {
|
||||
appName,
|
||||
gitlabBranch,
|
||||
gitlabId,
|
||||
gitlab,
|
||||
gitlabPathNamespace,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!gitlabId) {
|
||||
throw new TRPCError({
|
||||
@@ -122,33 +146,30 @@ export const cloneGitlabRepository = async (
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitlab?.accessToken}@${repoclone}`;
|
||||
|
||||
const repoClone = getGitlabRepoClone(gitlab, gitlabPathNamespace);
|
||||
const cloneUrl = getGitlabCloneUrl(gitlab, repoClone);
|
||||
try {
|
||||
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
gitlabBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
"--recurse-submodules",
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
);
|
||||
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
|
||||
writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}: ✅\n`);
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
gitlabBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
];
|
||||
|
||||
await spawnAsync("git", cloneArgs, (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
});
|
||||
writeStream.write(`\nCloned ${repoClone}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
@@ -162,13 +183,12 @@ export const getGitlabCloneCommand = async (
|
||||
) => {
|
||||
const {
|
||||
appName,
|
||||
gitlabRepository,
|
||||
gitlabOwner,
|
||||
gitlabPathNamespace,
|
||||
gitlabBranch,
|
||||
gitlabId,
|
||||
serverId,
|
||||
gitlab,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!serverId) {
|
||||
@@ -218,17 +238,16 @@ export const getGitlabCloneCommand = async (
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitlab?.accessToken}@${repoclone}`;
|
||||
|
||||
const repoClone = getGitlabRepoClone(gitlab, gitlabPathNamespace);
|
||||
const cloneUrl = getGitlabCloneUrl(gitlab, repoClone);
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
if ! git clone --branch ${gitlabBranch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
|
||||
if ! git clone --branch ${gitlabBranch} --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};
|
||||
echo "Cloned ${repoClone} to ${outputPath}: ✅" >> ${logPath};
|
||||
`;
|
||||
|
||||
return cloneCommand;
|
||||
@@ -243,30 +262,18 @@ export const getGitlabRepositories = async (gitlabId?: string) => {
|
||||
|
||||
const gitlabProvider = await findGitlabById(gitlabId);
|
||||
|
||||
const response = await fetch(
|
||||
`https://gitlab.com/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
const allProjects = await validateGitlabProvider(gitlabProvider);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const repositories = await response.json();
|
||||
|
||||
const filteredRepos = repositories.filter((repo: any) => {
|
||||
const filteredRepos = allProjects.filter((repo: any) => {
|
||||
const { full_path, kind } = repo.namespace;
|
||||
const groupName = gitlabProvider.groupName?.toLowerCase();
|
||||
|
||||
if (groupName) {
|
||||
return full_path.toLowerCase().includes(groupName) && kind === "group";
|
||||
return groupName
|
||||
.split(",")
|
||||
.some((name) =>
|
||||
full_path.toLowerCase().startsWith(name.trim().toLowerCase()),
|
||||
);
|
||||
}
|
||||
return kind === "user";
|
||||
});
|
||||
@@ -318,7 +325,9 @@ export const getGitlabBranches = async (input: {
|
||||
);
|
||||
|
||||
if (!branchesResponse.ok) {
|
||||
throw new Error(`Failed to fetch branches: ${branchesResponse.statusText}`);
|
||||
throw new Error(
|
||||
`Failed to fetch branches: ${branchesResponse.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const branches = await branchesResponse.json();
|
||||
@@ -349,11 +358,10 @@ export const getGitlabBranches = async (input: {
|
||||
export const cloneRawGitlabRepository = async (entity: Compose) => {
|
||||
const {
|
||||
appName,
|
||||
gitlabRepository,
|
||||
gitlabOwner,
|
||||
gitlabBranch,
|
||||
gitlabId,
|
||||
gitlabPathNamespace,
|
||||
enableSubmodules,
|
||||
} = entity;
|
||||
|
||||
if (!gitlabId) {
|
||||
@@ -369,28 +377,35 @@ export const cloneRawGitlabRepository = async (entity: Compose) => {
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
|
||||
|
||||
const repoClone = getGitlabRepoClone(gitlabProvider, gitlabPathNamespace);
|
||||
const cloneUrl = getGitlabCloneUrl(gitlabProvider, repoClone);
|
||||
try {
|
||||
await spawnAsync("git", [
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
gitlabBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
"--recurse-submodules",
|
||||
...(enableSubmodules ? ["--recurse-submodules"] : []),
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
]);
|
||||
];
|
||||
await spawnAsync("git", cloneArgs);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
|
||||
const { appName, gitlabPathNamespace, branch, gitlabId, serverId } = compose;
|
||||
const {
|
||||
appName,
|
||||
gitlabPathNamespace,
|
||||
branch,
|
||||
gitlabId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
} = compose;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
@@ -409,12 +424,12 @@ export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
|
||||
await refreshGitlabToken(gitlabId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
|
||||
const repoClone = getGitlabRepoClone(gitlabProvider, gitlabPathNamespace);
|
||||
const cloneUrl = getGitlabCloneUrl(gitlabProvider, repoClone);
|
||||
try {
|
||||
const command = `
|
||||
rm -rf ${outputPath};
|
||||
git clone --branch ${branch} --depth 1 --recurse-submodules ${cloneUrl} ${outputPath}
|
||||
git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
} catch (error) {
|
||||
@@ -435,32 +450,64 @@ export const testGitlabConnection = async (
|
||||
|
||||
const gitlabProvider = await findGitlabById(gitlabId);
|
||||
|
||||
const response = await fetch(
|
||||
`https://gitlab.com/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const repositories = await response.json();
|
||||
const repositories = await validateGitlabProvider(gitlabProvider);
|
||||
|
||||
const filteredRepos = repositories.filter((repo: any) => {
|
||||
const { full_path, kind } = repo.namespace;
|
||||
|
||||
if (groupName) {
|
||||
return full_path.toLowerCase().includes(groupName) && kind === "group";
|
||||
return groupName
|
||||
.split(",")
|
||||
.some((name) =>
|
||||
full_path.toLowerCase().startsWith(name.trim().toLowerCase()),
|
||||
);
|
||||
}
|
||||
return kind === "user";
|
||||
});
|
||||
|
||||
return filteredRepos.length;
|
||||
};
|
||||
|
||||
export const validateGitlabProvider = async (gitlabProvider: Gitlab) => {
|
||||
try {
|
||||
const allProjects = [];
|
||||
let page = 1;
|
||||
const perPage = 100; // GitLab's max per page is 100
|
||||
|
||||
while (true) {
|
||||
const response = await fetch(
|
||||
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${page}&per_page=${perPage}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const projects = await response.json();
|
||||
|
||||
if (projects.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
allProjects.push(...projects);
|
||||
page++;
|
||||
|
||||
const total = response.headers.get("x-total");
|
||||
if (total && allProjects.length >= Number.parseInt(total)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return allProjects;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user