fix(git): write deploy key to per-deployment path, not root's id_rsa

This commit is contained in:
Ömer Faruk ŞAHİN 2026-05-27 00:57:07 +03:00
parent dd8a0d501d
commit ddd84e5adc
3 changed files with 149 additions and 30 deletions

View file

@ -2261,18 +2261,19 @@ private function check_git_if_build_needed()
$private_key = data_get($this->application, 'private_key.private_key');
if ($private_key) {
$private_key = base64_encode($private_key);
$customSshKeyLocation = "/root/.ssh/id_rsa_coolify_{$this->deployment_uuid}";
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, 'mkdir -p /root/.ssh'),
],
[
executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null"),
executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d | tee {$customSshKeyLocation} > /dev/null"),
],
[
executeInDocker($this->deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
executeInDocker($this->deployment_uuid, "chmod 600 {$customSshKeyLocation}"),
],
[
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->fullRepoUrl} {$lsRemoteRef}"),
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {$customSshKeyLocation} -o IdentitiesOnly=yes\" git ls-remote {$this->fullRepoUrl} {$lsRemoteRef}"),
'hidden' => true,
'save' => 'git_commit_sha',
]

View file

@ -1343,6 +1343,7 @@ public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_
$branch = $this->git_branch;
['repository' => $customRepository, 'port' => $customPort] = $this->customRepository();
$commands = collect([]);
$customSshKeyLocation = "/root/.ssh/id_rsa_coolify_{$deployment_uuid}";
$base_command = 'git ls-remote';
if ($this->deploymentType() === 'source') {
@ -1396,19 +1397,20 @@ public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_
$private_key = base64_encode($private_key);
$gitlabPort = $gitlabSource->custom_port ?? 22;
$escapedCustomRepository = str_replace("'", "'\\''", $customRepository);
$base_command = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$gitlabPort} -o Port={$gitlabPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$base_command} '{$escapedCustomRepository}'";
$base_command = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$gitlabPort} -o Port={$gitlabPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {$customSshKeyLocation} -o IdentitiesOnly=yes\" {$base_command} '{$escapedCustomRepository}'";
if ($exec_in_docker) {
$commands = collect([
executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh'),
executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null"),
executeInDocker($deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d | tee {$customSshKeyLocation} > /dev/null"),
executeInDocker($deployment_uuid, "chmod 600 {$customSshKeyLocation}"),
]);
} else {
$commands = collect([
"trap 'rm -f {$customSshKeyLocation}' EXIT",
'mkdir -p /root/.ssh',
"echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null",
'chmod 600 /root/.ssh/id_rsa',
"echo '{$private_key}' | base64 -d | tee {$customSshKeyLocation} > /dev/null",
"chmod 600 {$customSshKeyLocation}",
]);
}
@ -1454,19 +1456,20 @@ public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_
// When used with executeInDocker (which uses bash -c '...'), we need to escape for bash context
// Replace ' with '\'' to safely escape within single-quoted bash strings
$escapedCustomRepository = str_replace("'", "'\\''", $customRepository);
$base_command = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$base_command} '{$escapedCustomRepository}'";
$base_command = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {$customSshKeyLocation} -o IdentitiesOnly=yes\" {$base_command} '{$escapedCustomRepository}'";
if ($exec_in_docker) {
$commands = collect([
executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh'),
executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null"),
executeInDocker($deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d | tee {$customSshKeyLocation} > /dev/null"),
executeInDocker($deployment_uuid, "chmod 600 {$customSshKeyLocation}"),
]);
} else {
$commands = collect([
"trap 'rm -f {$customSshKeyLocation}' EXIT",
'mkdir -p /root/.ssh',
"echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null",
'chmod 600 /root/.ssh/id_rsa',
"echo '{$private_key}' | base64 -d | tee {$customSshKeyLocation} > /dev/null",
"chmod 600 {$customSshKeyLocation}",
]);
}
@ -1507,6 +1510,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
$branch = $this->git_branch;
['repository' => $customRepository, 'port' => $customPort] = $this->customRepository();
$baseDir = $custom_base_dir ?? $this->generateBaseDir($deployment_uuid);
$customSshKeyLocation = "/root/.ssh/id_rsa_coolify_{$deployment_uuid}";
// Escape shell arguments for safety to prevent command injection
$escapedBranch = escapeshellarg($branch);
@ -1603,7 +1607,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
$private_key = base64_encode($private_key);
$gitlabPort = $gitlabSource->custom_port ?? 22;
$escapedCustomRepository = escapeshellarg($customRepository);
$gitlabSshCommand = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$gitlabPort} -o Port={$gitlabPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\"";
$gitlabSshCommand = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$gitlabPort} -o Port={$gitlabPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {$customSshKeyLocation} -o IdentitiesOnly=yes\"";
$git_clone_command_base = "{$gitlabSshCommand} {$git_clone_command} {$escapedCustomRepository} {$escapedBaseDir}";
if ($only_checkout) {
$git_clone_command = $git_clone_command_base;
@ -1613,14 +1617,15 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
if ($exec_in_docker) {
$commands = collect([
executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh'),
executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null"),
executeInDocker($deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d | tee {$customSshKeyLocation} > /dev/null"),
executeInDocker($deployment_uuid, "chmod 600 {$customSshKeyLocation}"),
]);
} else {
$commands = collect([
"trap 'rm -f {$customSshKeyLocation}' EXIT",
'mkdir -p /root/.ssh',
"echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null",
'chmod 600 /root/.ssh/id_rsa',
"echo '{$private_key}' | base64 -d | tee {$customSshKeyLocation} > /dev/null",
"chmod 600 {$customSshKeyLocation}",
]);
}
@ -1631,7 +1636,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$gitlabPort} -o Port={$gitlabPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$gitlabPort} -o Port={$gitlabPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {$customSshKeyLocation} -o IdentitiesOnly=yes\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
}
if ($exec_in_docker) {
@ -1674,7 +1679,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
}
$private_key = base64_encode($private_key);
$escapedCustomRepository = escapeshellarg($customRepository);
$deployKeySshCommand = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\"";
$deployKeySshCommand = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {$customSshKeyLocation} -o IdentitiesOnly=yes\"";
$git_clone_command_base = "{$deployKeySshCommand} {$git_clone_command} {$escapedCustomRepository} {$escapedBaseDir}";
if ($only_checkout) {
$git_clone_command = $git_clone_command_base;
@ -1684,14 +1689,15 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
if ($exec_in_docker) {
$commands = collect([
executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh'),
executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null"),
executeInDocker($deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d | tee {$customSshKeyLocation} > /dev/null"),
executeInDocker($deployment_uuid, "chmod 600 {$customSshKeyLocation}"),
]);
} else {
$commands = collect([
"trap 'rm -f {$customSshKeyLocation}' EXIT",
'mkdir -p /root/.ssh',
"echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null",
'chmod 600 /root/.ssh/id_rsa',
"echo '{$private_key}' | base64 -d | tee {$customSshKeyLocation} > /dev/null",
"chmod 600 {$customSshKeyLocation}",
]);
}
if ($pull_request_id !== 0) {
@ -1702,7 +1708,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {$customSshKeyLocation} -o IdentitiesOnly=yes\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
} elseif ($git_type === 'github' || $git_type === 'gitea') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
@ -1710,14 +1716,14 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {$customSshKeyLocation} -o IdentitiesOnly=yes\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
} elseif ($git_type === 'bitbucket') {
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit);
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {$customSshKeyLocation} -o IdentitiesOnly=yes\" ".$this->buildGitCheckoutCommand($commit);
}
}
@ -1747,7 +1753,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {$customSshKeyLocation} -o IdentitiesOnly=yes\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
} elseif ($git_type === 'github' || $git_type === 'gitea') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
@ -1755,14 +1761,14 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {$customSshKeyLocation} -o IdentitiesOnly=yes\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
} elseif ($git_type === 'bitbucket') {
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit);
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {$customSshKeyLocation} -o IdentitiesOnly=yes\" ".$this->buildGitCheckoutCommand($commit);
}
}

View file

@ -0,0 +1,112 @@
<?php
use App\Models\Application;
use App\Models\ApplicationSetting;
use App\Models\GitlabApp;
use App\Models\PrivateKey;
afterEach(function () {
Mockery::close();
});
/**
* Git operations authenticate with the SSH key assigned in the UI. Coolify writes that key to a
* per-deployment path (/root/.ssh/id_rsa_coolify_<deployment_uuid>) instead of the shared
* /root/.ssh/id_rsa, so it can neither overwrite the server root's own key nor race with other
* concurrent operations on the same host. `-o IdentitiesOnly=yes` makes ssh offer only that key.
* On the host path an EXIT trap removes the key when the shell finishes; the docker path runs in an
* ephemeral container, so it needs no cleanup.
*/
$keyPath = '/root/.ssh/id_rsa_coolify_test-deployment-uuid';
it('writes a deploy key to a per-deployment path and cleans it up for ls-remote on the host', function () use ($keyPath) {
$privateKey = Mockery::mock(PrivateKey::class)->makePartial();
$privateKey->shouldReceive('getAttribute')->with('private_key')->andReturn('fake-private-key');
$application = Mockery::mock(Application::class)->makePartial();
$application->git_branch = 'main';
$application->shouldReceive('deploymentType')->andReturn('deploy_key');
$application->shouldReceive('customRepository')->andReturn(['repository' => 'git@gitlab.com:user/repo.git', 'port' => 22]);
$application->shouldReceive('getAttribute')->with('private_key')->andReturn($privateKey);
$result = $application->generateGitLsRemoteCommands('test-deployment-uuid', false);
expect($result['commands'])
->toContain("tee {$keyPath}")
->toContain("-i {$keyPath} -o IdentitiesOnly=yes")
->toContain("trap 'rm -f {$keyPath}' EXIT") // removed when the shell exits
->not->toContain('tee /root/.ssh/id_rsa >'); // never overwrites the host root's own key
});
it('writes a deploy key to a per-deployment path for ls-remote inside docker without a trap', function () use ($keyPath) {
$privateKey = Mockery::mock(PrivateKey::class)->makePartial();
$privateKey->shouldReceive('getAttribute')->with('private_key')->andReturn('fake-private-key');
$application = Mockery::mock(Application::class)->makePartial();
$application->git_branch = 'main';
$application->shouldReceive('deploymentType')->andReturn('deploy_key');
$application->shouldReceive('customRepository')->andReturn(['repository' => 'git@gitlab.com:user/repo.git', 'port' => 22]);
$application->shouldReceive('getAttribute')->with('private_key')->andReturn($privateKey);
$result = $application->generateGitLsRemoteCommands('test-deployment-uuid', true);
expect($result['commands'])
->toContain("tee {$keyPath}")
->toContain("-i {$keyPath} -o IdentitiesOnly=yes")
->not->toContain('trap ') // ephemeral container, no cleanup needed
->not->toContain('tee /root/.ssh/id_rsa >');
});
it('writes a GitLab source private key to a per-deployment path with cleanup on the host', function () use ($keyPath) {
$privateKey = Mockery::mock(PrivateKey::class)->makePartial();
$privateKey->shouldReceive('getAttribute')->with('private_key')->andReturn('fake-private-key');
$gitlabSource = Mockery::mock(GitlabApp::class)->makePartial();
$gitlabSource->shouldReceive('getMorphClass')->andReturn(GitlabApp::class);
$gitlabSource->shouldReceive('getAttribute')->with('html_url')->andReturn('https://gitlab.com');
$gitlabSource->shouldReceive('getAttribute')->with('privateKey')->andReturn($privateKey);
$gitlabSource->shouldReceive('getAttribute')->with('private_key_id')->andReturn(1);
$gitlabSource->shouldReceive('getAttribute')->with('custom_port')->andReturn(22);
$application = Mockery::mock(Application::class)->makePartial();
$application->git_branch = 'main';
$application->shouldReceive('deploymentType')->andReturn('source');
$application->shouldReceive('customRepository')->andReturn(['repository' => 'git@gitlab.com:user/repo.git', 'port' => 22]);
$application->shouldReceive('getAttribute')->with('source')->andReturn($gitlabSource);
$application->source = $gitlabSource;
$result = $application->generateGitLsRemoteCommands('test-deployment-uuid', false);
expect($result['commands'])
->toContain("tee {$keyPath}")
->toContain("-i {$keyPath} -o IdentitiesOnly=yes")
->toContain("trap 'rm -f {$keyPath}' EXIT")
->not->toContain('tee /root/.ssh/id_rsa >');
});
it('writes a deploy key to a per-deployment path and cleans it up when cloning on the host', function () use ($keyPath) {
$privateKey = Mockery::mock(PrivateKey::class)->makePartial();
$privateKey->shouldReceive('getAttribute')->with('private_key')->andReturn('fake-private-key');
$settings = Mockery::mock(ApplicationSetting::class)->makePartial();
$settings->shouldReceive('getAttribute')->with('is_git_shallow_clone_enabled')->andReturn(false);
$settings->shouldReceive('getAttribute')->with('is_git_submodules_enabled')->andReturn(false);
$settings->shouldReceive('getAttribute')->with('is_git_lfs_enabled')->andReturn(false);
$application = Mockery::mock(Application::class)->makePartial();
$application->git_branch = 'main';
$application->shouldReceive('deploymentType')->andReturn('deploy_key');
$application->shouldReceive('customRepository')->andReturn(['repository' => 'git@gitlab.com:user/repo.git', 'port' => 22]);
$application->shouldReceive('getAttribute')->with('private_key')->andReturn($privateKey);
$application->shouldReceive('getAttribute')->with('settings')->andReturn($settings);
$application->shouldReceive('getAttribute')->with('git_commit_sha')->andReturn('HEAD');
// exec_in_docker = false → the loadComposeFile / host clone path
$result = $application->generateGitImportCommands('test-deployment-uuid', 0, null, false);
expect($result['commands'])
->toContain("tee {$keyPath}")
->toContain("-i {$keyPath} -o IdentitiesOnly=yes")
->toContain("trap 'rm -f {$keyPath}' EXIT")
->not->toContain('tee /root/.ssh/id_rsa >');
});