195 lines · 5.8 KB · TypeScript
RawDownload
import path, { join } from "node:path";import { paths } from "@dokploy/server/constants";import {	findSSHKeyById,	updateSSHKeyById,} from "@dokploy/server/services/ssh-key";import { execAsync, execAsyncRemote } from "../process/execAsync";import { shellWord } from "./utils"; interface CloneGitRepository {	appName: string;	customGitUrl?: string | null;	customGitBranch?: string | null;	customGitSSHKeyId?: string | null;	enableSubmodules?: boolean;	serverId: string | null;	type?: "application" | "compose";	outputPathOverride?: string;} export const cloneGitRepository = async ({	type = "application",	...entity}: CloneGitRepository) => {	let command = "set -e;";	const {		appName,		customGitUrl,		customGitBranch,		customGitSSHKeyId,		enableSubmodules,		serverId,		outputPathOverride,	} = entity;	const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId); 	if (!customGitUrl || !customGitBranch) {		command += `echo "Error: ❌ Repository not found"; exit 1;`;		return command;	} 	const temporalKeyPath = path.join("/tmp", "id_rsa"); 	if (customGitSSHKeyId) {		const sshKey = await findSSHKeyById(customGitSSHKeyId); 		command += `			echo "${sshKey.privateKey}" > ${temporalKeyPath}			chmod 600 ${temporalKeyPath};			`;	}	const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;	const outputPath = outputPathOverride ?? join(basePath, appName, "code");	const knownHostsPath = path.join(SSH_PATH, "known_hosts"); 	if (!isHttpOrHttps(customGitUrl)) {		if (!customGitSSHKeyId) {			command += `echo "Error: ❌ You are trying to clone a ssh repository without a ssh key, please set a ssh key"; exit 1;`;			return command;		}		command += addHostToKnownHostsCommand(customGitUrl);	}	command += `rm -rf ${outputPath};`;	command += `mkdir -p ${outputPath};`;	command += `echo ${shellWord(`Cloning Repo Custom ${customGitUrl} to ${outputPath}: ✅`)};`; 	if (customGitSSHKeyId) {		await updateSSHKeyById({			sshKeyId: customGitSSHKeyId,			lastUsedAt: new Date().toISOString(),		});	} 	if (customGitSSHKeyId) {		const sshKey = await findSSHKeyById(customGitSSHKeyId);		const { port } = sanitizeRepoPathSSH(customGitUrl);		const gitSshCommand = `ssh -i /tmp/id_rsa${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath} -o StrictHostKeyChecking=accept-new`;		command += `echo "${sshKey.privateKey}" > /tmp/id_rsa;`;		command += "chmod 600 /tmp/id_rsa;";		command += `export GIT_SSH_COMMAND="${gitSshCommand}";`;	}	command += `if ! git clone --branch ${shellWord(customGitBranch)} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${shellWord(customGitUrl)} ${shellWord(outputPath)}; then				echo ${shellWord(`❌ [ERROR] Fail to clone the repository ${customGitUrl}`)};				exit 1;			fi			`; 	return command;}; const isHttpOrHttps = (url: string): boolean => {	const regex = /^https?:\/\//;	return regex.test(url);}; // const addHostToKnownHosts = async (repositoryURL: string) => {// 	const { SSH_PATH } = paths();// 	const { domain, port } = sanitizeRepoPathSSH(repositoryURL);// 	const knownHostsPath = path.join(SSH_PATH, "known_hosts"); // 	const command = `ssh-keyscan -p ${port} ${domain} >> ${knownHostsPath}`;// 	try {// 		await execAsync(command);// 	} catch (error) {// 		console.error(`Error adding host to known_hosts: ${error}`);// 		throw error;// 	}// }; const addHostToKnownHostsCommand = (repositoryURL: string) => {	const { SSH_PATH } = paths(true);	const { domain, port } = sanitizeRepoPathSSH(repositoryURL);	const knownHostsPath = path.join(SSH_PATH, "known_hosts"); 	// ssh-keyscan is best-effort: some Git hosts (e.g. Hugging Face) never answer	// it, and its exit code must not abort the clone under `set -e`. The clone's	// own host-key check (StrictHostKeyChecking=accept-new) is the real boundary.	return `ssh-keyscan -p ${Number(port)} ${shellWord(domain)} >> ${knownHostsPath} || true;`;};const sanitizeRepoPathSSH = (input: string) => {	const SSH_PATH_RE = new RegExp(		[			/^\s*/,			/(?:(?<proto>[a-z]+):\/\/)?/,			/(?:(?<user>[a-z_][a-z0-9_-]+)@)?/,			/(?<domain>[^\s/?#:]+)/,			/(?::(?<port>[0-9]{1,5}))?/,			/(?:[/:](?<owner>[^\s/?#:]+))?/,			/(?:[/:](?<repo>(?:[^\s?#:.]|\.(?!git\/?\s*$))+))/,			/(?:.git)?\/?\s*$/,		]			.map((r) => r.source)			.join(""),		"i",	); 	const found = input.match(SSH_PATH_RE);	if (!found) {		throw new Error(`Malformatted SSH path: ${input}`);	} 	return {		user: found.groups?.user ?? "git",		domain: found.groups?.domain,		port: Number(found.groups?.port ?? 22),		owner: found.groups?.owner ?? "",		repo: found.groups?.repo,		get repoPath() {			return `ssh://${this.user}@${this.domain}:${this.port}/${this.owner}${				this.owner && "/"			}${this.repo}.git`;		},	};}; interface Props {	appName: string;	type?: "application" | "compose";	serverId: string | null;} export const getGitCommitInfo = async ({	appName,	type = "application",	serverId,}: Props) => {	const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId);	const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH;	const outputPath = join(basePath, appName, "code");	let stdoutResult = "";	const result = {		message: "",		hash: "",	};	try {		const gitCommand = `git -C ${outputPath} log -1 --pretty=format:"%H---DELIMITER---%B"`;		if (serverId) {			const { stdout } = await execAsyncRemote(serverId, gitCommand);			stdoutResult = stdout.trim();		} else {			const { stdout } = await execAsync(gitCommand);			stdoutResult = stdout.trim();		} 		const parts = stdoutResult.split("---DELIMITER---");		if (parts && parts.length === 2) {			result.hash = parts[0]?.trim() || "";			result.message = parts[1]?.trim() || "";		}	} catch (error) {		console.error(`Error getting git commit info: ${error}`);		return null;	}	return result;};