fix(deploy): preserve private key command metadata (#10795)

This commit is contained in:
Andras Bacsai 2026-06-29 10:17:57 +02:00 committed by GitHub
commit fe9b43be7c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 335 additions and 159 deletions

View file

@ -2367,12 +2367,7 @@ private function clone_repository()
if ($this->pull_request_id !== 0) {
$this->application_deployment_queue->addLogEntry("Checking out tag pull/{$this->pull_request_id}/head.");
}
$this->execute_remote_command(
[
$importCommands,
'hidden' => true,
]
);
$this->execute_remote_command(...$this->gitCommandDefinitions($importCommands));
$this->create_workdir();
$this->execute_remote_command(
[
@ -2402,6 +2397,39 @@ private function generate_git_import_commands()
return $commands;
}
private function gitCommandDefinitions(Collection|array|string $commands): array
{
if (is_string($commands)) {
return [
[
$commands,
'hidden' => true,
],
];
}
return collect($commands)
->map(function ($command): array {
if (is_string($command)) {
return [
$command,
'hidden' => true,
];
}
if (is_array($command)) {
return $command + ['hidden' => true];
}
return [
'command' => $command,
'hidden' => true,
];
})
->values()
->all();
}
private function cleanup_git()
{
$this->execute_remote_command(

View file

@ -1338,7 +1338,7 @@ public function getGitRemoteStatus(string $deployment_uuid)
{
try {
['commands' => $lsRemoteCommand] = $this->generateGitLsRemoteCommands(deployment_uuid: $deployment_uuid, exec_in_docker: false);
instant_remote_process([$lsRemoteCommand], $this->destination->server, true);
instant_remote_process([$this->gitCommandsAsShellCommand($lsRemoteCommand)], $this->destination->server, true);
return [
'is_accessible' => true,
@ -1390,13 +1390,13 @@ public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_
}
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $base_command));
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, $base_command)));
} else {
$commands->push($base_command);
$commands->push($this->gitCommand($base_command));
}
return [
'commands' => $commands->implode(' && '),
'commands' => $this->gitCommandsAsShellCommand($commands),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl,
];
@ -1413,29 +1413,16 @@ public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_
$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 {$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 {$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 {$customSshKeyLocation} > /dev/null",
"chmod 600 {$customSshKeyLocation}",
]);
}
$commands = $this->gitSshKeySetupCommands($deployment_uuid, $private_key, $exec_in_docker);
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $base_command));
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, $base_command)));
} else {
$commands->push($base_command);
$commands->push($this->gitCommand($base_command));
}
return [
'commands' => $commands->implode(' && '),
'commands' => $commands,
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl,
];
@ -1447,13 +1434,13 @@ public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_
$base_command = "{$base_command} {$escapedCustomRepository}";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $base_command));
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, $base_command)));
} else {
$commands->push($base_command);
$commands->push($this->gitCommand($base_command));
}
return [
'commands' => $commands->implode(' && '),
'commands' => $this->gitCommandsAsShellCommand($commands),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl,
];
@ -1472,29 +1459,16 @@ public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_
$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 {$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 {$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 {$customSshKeyLocation} > /dev/null",
"chmod 600 {$customSshKeyLocation}",
]);
}
$commands = $this->gitSshKeySetupCommands($deployment_uuid, $private_key, $exec_in_docker);
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $base_command));
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, $base_command)));
} else {
$commands->push($base_command);
$commands->push($this->gitCommand($base_command));
}
return [
'commands' => $commands->implode(' && '),
'commands' => $commands,
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl,
];
@ -1506,19 +1480,60 @@ public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_
$base_command = "{$base_command} {$escapedCustomRepository}";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $base_command));
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, $base_command)));
} else {
$commands->push($base_command);
$commands->push($this->gitCommand($base_command));
}
return [
'commands' => $commands->implode(' && '),
'commands' => $this->gitCommandsAsShellCommand($commands),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl,
];
}
}
private function gitCommand(string $command): array
{
return [
'command' => $command,
'hidden' => true,
];
}
private function gitCommandsAsShellCommand(Collection|array|string $commands): string
{
if (is_string($commands)) {
return $commands;
}
return collect($commands)
->map(fn ($command) => data_get($command, 'command') ?? $command[0] ?? $command)
->implode(' && ');
}
private function gitSshKeySetupCommands(string $deploymentUuid, string $privateKey, bool $execInDocker): Collection
{
$customSshKeyLocation = "/root/.ssh/id_rsa_coolify_{$deploymentUuid}";
$commands = collect([]);
if (! $execInDocker) {
$commands->push($this->gitCommand("trap 'rm -f {$customSshKeyLocation}' EXIT"));
}
$commands->push($this->gitCommand($execInDocker ? executeInDocker($deploymentUuid, 'mkdir -p /root/.ssh') : 'mkdir -p /root/.ssh'));
$commands->push([
'command' => $execInDocker
? executeInDocker($deploymentUuid, "echo '{$privateKey}' | base64 -d | tee {$customSshKeyLocation} > /dev/null")
: "echo '{$privateKey}' | base64 -d | tee {$customSshKeyLocation} > /dev/null",
'hidden' => true,
'skip_command_log' => true,
]);
$commands->push($this->gitCommand($execInDocker ? executeInDocker($deploymentUuid, "chmod 600 {$customSshKeyLocation}") : "chmod 600 {$customSshKeyLocation}"));
return $commands;
}
private function withGitHttpTransportConfig(?string $gitConfigOptions = null): string
{
return trim(($gitConfigOptions ? "{$gitConfigOptions} " : '').'-c http.version=HTTP/1.1');
@ -1590,9 +1605,9 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: true, commit: $commit, gitConfigOptions: $gitConfigOptions);
}
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, $git_clone_command)));
} else {
$commands->push($git_clone_command);
$commands->push($this->gitCommand($git_clone_command));
}
} else {
$github_access_token = generateGithubInstallationToken($this->source);
@ -1619,9 +1634,9 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: false, commit: $commit, gitConfigOptions: $gitConfigOptions);
}
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, $git_clone_command)));
} else {
$commands->push($git_clone_command);
$commands->push($this->gitCommand($git_clone_command));
}
}
if ($pull_request_id !== 0) {
@ -1631,14 +1646,14 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
$gitCommand = isset($gitConfigOptions) ? "git {$gitConfigOptions}" : 'git';
$escapedPrBranch = escapeshellarg($branch);
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "cd {$escapedBaseDir} && {$gitCommand} fetch origin {$escapedPrBranch} && $git_checkout_command"));
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, "cd {$escapedBaseDir} && {$gitCommand} fetch origin {$escapedPrBranch} && $git_checkout_command")));
} else {
$commands->push("cd {$escapedBaseDir} && {$gitCommand} fetch origin {$escapedPrBranch} && $git_checkout_command");
$commands->push($this->gitCommand("cd {$escapedBaseDir} && {$gitCommand} fetch origin {$escapedPrBranch} && $git_checkout_command"));
}
}
return [
'commands' => $commands->implode(' && '),
'commands' => $this->gitCommandsAsShellCommand($commands),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl,
];
@ -1661,39 +1676,26 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
} else {
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base, commit: $commit, gitSshCommand: $gitlabSshCommand);
}
if ($exec_in_docker) {
$commands = collect([
executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh'),
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 {$customSshKeyLocation} > /dev/null",
"chmod 600 {$customSshKeyLocation}",
]);
}
$commands = $this->gitSshKeySetupCommands($deployment_uuid, $private_key, $exec_in_docker);
if ($pull_request_id !== 0) {
$branch = "merge-requests/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")));
} else {
$commands->push("echo 'Checking out $branch'");
$commands->push($this->gitCommand("echo 'Checking out $branch'"));
}
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && {$gitlabGitSshCommand} git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name, $gitlabSshCommand);
}
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, $git_clone_command)));
} else {
$commands->push($git_clone_command);
$commands->push($this->gitCommand($git_clone_command));
}
return [
'commands' => $commands->implode(' && '),
'commands' => $commands,
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl,
];
@ -1710,13 +1712,13 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: true, commit: $commit, gitConfigOptions: $gitConfigOptions);
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, $git_clone_command)));
} else {
$commands->push($git_clone_command);
$commands->push($this->gitCommand($git_clone_command));
}
return [
'commands' => $commands->implode(' && '),
'commands' => $this->gitCommandsAsShellCommand($commands),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl,
];
@ -1738,55 +1740,42 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
} else {
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base, commit: $commit, gitSshCommand: $deployKeySshCommand);
}
if ($exec_in_docker) {
$commands = collect([
executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh'),
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 {$customSshKeyLocation} > /dev/null",
"chmod 600 {$customSshKeyLocation}",
]);
}
$commands = $this->gitSshKeySetupCommands($deployment_uuid, $private_key, $exec_in_docker);
if ($pull_request_id !== 0) {
if ($git_type === 'gitlab') {
$branch = "merge-requests/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")));
} else {
$commands->push("echo 'Checking out $branch'");
$commands->push($this->gitCommand("echo 'Checking out $branch'"));
}
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && {$deployKeyGitSshCommand} git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name, $deployKeySshCommand);
} elseif ($git_type === 'github' || $git_type === 'gitea') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")));
} else {
$commands->push("echo 'Checking out $branch'");
$commands->push($this->gitCommand("echo 'Checking out $branch'"));
}
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && {$deployKeyGitSshCommand} git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name, $deployKeySshCommand);
} elseif ($git_type === 'bitbucket') {
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")));
} else {
$commands->push("echo 'Checking out $branch'");
$commands->push($this->gitCommand("echo 'Checking out $branch'"));
}
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && {$deployKeyGitSshCommand} ".$this->buildGitCheckoutCommand($commit, $deployKeySshCommand);
}
}
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, $git_clone_command)));
} else {
$commands->push($git_clone_command);
$commands->push($this->gitCommand($git_clone_command));
}
return [
'commands' => $commands->implode(' && '),
'commands' => $commands,
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl,
];
@ -1807,37 +1796,37 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
if ($git_type === 'gitlab') {
$branch = "merge-requests/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")));
} else {
$commands->push("echo 'Checking out $branch'");
$commands->push($this->gitCommand("echo 'Checking out $branch'"));
}
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$otherSshCommand}\" {$gitCommand} fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name, $otherSshCommand, $gitConfigOptions);
} elseif ($git_type === 'github' || $git_type === 'gitea') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")));
} else {
$commands->push("echo 'Checking out $branch'");
$commands->push($this->gitCommand("echo 'Checking out $branch'"));
}
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$otherSshCommand}\" {$gitCommand} fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name, $otherSshCommand, $gitConfigOptions);
} elseif ($git_type === 'bitbucket') {
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")));
} else {
$commands->push("echo 'Checking out $branch'");
$commands->push($this->gitCommand("echo 'Checking out $branch'"));
}
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$otherSshCommand}\" ".$this->buildGitCheckoutCommand($commit, $otherSshCommand, $gitConfigOptions);
}
}
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, $git_clone_command)));
} else {
$commands->push($git_clone_command);
$commands->push($this->gitCommand($git_clone_command));
}
return [
'commands' => $commands->implode(' && '),
'commands' => $this->gitCommandsAsShellCommand($commands),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl,
];
@ -1926,6 +1915,7 @@ public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory =
}
$uuid = new_public_id();
['commands' => $cloneCommand] = $this->generateGitImportCommands(deployment_uuid: $uuid, only_checkout: true, exec_in_docker: false, custom_base_dir: 'checkout');
$cloneCommand = $this->gitCommandsAsShellCommand($cloneCommand);
$cloneCommand = str_replace(' clone ', ' clone --quiet ', $cloneCommand);
$workdir = rtrim($this->base_directory, '/');
$composeFile = $this->docker_compose_location;

View file

@ -161,7 +161,7 @@ private function executeCommandWithProcess($command, $hidden, $customType, $appe
}
$remote_command = SshMultiplexingHelper::generateSshCommand($this->server, $command);
$process = Process::timeout(config('constants.ssh.command_timeout'))->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $append, $command_hidden) {
$process = Process::timeout(config('constants.ssh.command_timeout'))->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $append, $command_hidden, $skip_command_log) {
$output = str($output)->trim();
if ($output->startsWith('╔')) {
$output = "\n".$output;

View file

@ -7,6 +7,7 @@
use App\Models\Application;
use App\Models\Environment;
use App\Models\GithubApp;
use App\Models\GitlabApp;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
@ -360,6 +361,36 @@ public static function examples(): array
'ports_exposes' => '3000',
'git_branch' => 'v4.x',
],
[
'uuid' => 'railpack-github-deploy-key',
'name' => 'Railpack GitHub Deploy Key Example',
'git_repository' => 'git@github.com:coollabsio/coolify-examples-deploy-key.git',
'git_branch' => 'main',
'ports_exposes' => '80',
'private_key_id' => 1,
],
[
'uuid' => 'railpack-gitlab-deploy-key',
'name' => 'Railpack GitLab Deploy Key Example',
'git_repository' => 'git@gitlab.com:coollabsio/php-example.git',
'git_branch' => 'main',
'ports_exposes' => '80',
'source_id' => 1,
'source_type' => GitlabApp::class,
'private_key_id' => 1,
],
[
'uuid' => 'railpack-gitlab-public-example',
'name' => 'Railpack GitLab Public Example',
'git_repository' => 'https://gitlab.com/andrasbacsai/coolify-examples.git',
'git_branch' => 'main',
'base_directory' => '/astro/static',
'publish_directory' => '/dist',
'ports_exposes' => '80',
'source_id' => 1,
'source_type' => GitlabApp::class,
'is_static' => true,
],
];
}
@ -420,6 +451,7 @@ private function ensureDevelopmentPrerequisitesExist(): void
);
$this->ensurePublicGithubSourceExists();
$this->ensurePublicGitlabSourceExists();
}
private function ensurePublicGithubSourceExists(): void
@ -437,6 +469,21 @@ private function ensurePublicGithubSourceExists(): void
);
}
private function ensurePublicGitlabSourceExists(): void
{
GitlabApp::query()->firstOrCreate(
['id' => 1],
[
'uuid' => 'gitlab-public',
'name' => 'Public GitLab',
'api_url' => 'https://gitlab.com/api/v4',
'html_url' => 'https://gitlab.com',
'is_public' => true,
'team_id' => 0,
],
);
}
private function isDevelopmentEnvironment(): bool
{
return in_array(config('app.env'), ['local', 'development', 'dev'], true);
@ -479,12 +526,12 @@ private function upsertApplication(Environment $environment, StandaloneDocker $d
'name' => $example['name'],
'description' => $example['name'],
'fqdn' => "http://{$example['uuid']}.127.0.0.1.sslip.io",
'repository_project_id' => self::REPOSITORY_PROJECT_ID,
'git_repository' => self::GIT_REPOSITORY,
'repository_project_id' => $example['repository_project_id'] ?? self::REPOSITORY_PROJECT_ID,
'git_repository' => $example['git_repository'] ?? self::GIT_REPOSITORY,
'git_branch' => $example['git_branch'] ?? self::GIT_BRANCH,
'build_pack' => 'railpack',
'ports_exposes' => $example['ports_exposes'],
'base_directory' => $example['base_directory'],
'base_directory' => $example['base_directory'] ?? '/',
'publish_directory' => $example['publish_directory'] ?? null,
'static_image' => 'nginx:alpine',
'install_command' => $example['install_command'] ?? null,
@ -493,8 +540,9 @@ private function upsertApplication(Environment $environment, StandaloneDocker $d
'environment_id' => $environment->id,
'destination_id' => $destination->id,
'destination_type' => StandaloneDocker::class,
'source_id' => 0,
'source_type' => GithubApp::class,
'source_id' => $example['source_id'] ?? 0,
'source_type' => $example['source_type'] ?? GithubApp::class,
'private_key_id' => $example['private_key_id'] ?? null,
]);
$application->save();

View file

@ -2,6 +2,7 @@
use App\Models\Application;
use App\Models\GithubApp;
use App\Models\GitlabApp;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
@ -42,6 +43,7 @@ function seedRailpackExamplePrerequisites(): void
expect(Server::query()->find(0))->not->toBeNull();
expect(StandaloneDocker::query()->find(0))->not->toBeNull();
expect(GithubApp::query()->find(0))->not->toBeNull();
expect(GitlabApp::query()->find(1))->not->toBeNull();
expect(Project::query()->where('uuid', DevelopmentRailpackExamplesSeeder::PROJECT_UUID)->exists())->toBeTrue();
expect(Application::query()->count())->toBe(count(DevelopmentRailpackExamplesSeeder::examples()));
});
@ -66,9 +68,10 @@ function seedRailpackExamplePrerequisites(): void
expect($applications)->toHaveCount(count(DevelopmentRailpackExamplesSeeder::examples()));
expect($applications->every(fn (Application $application) => $application->build_pack === 'railpack'))->toBeTrue();
expect($applications->every(fn (Application $application) => $application->git_repository === DevelopmentRailpackExamplesSeeder::GIT_REPOSITORY))->toBeTrue();
$examples = collect(DevelopmentRailpackExamplesSeeder::examples())->keyBy('uuid');
expect($applications->every(
fn (Application $application) => $application->git_repository === ($examples->get($application->uuid)['git_repository'] ?? DevelopmentRailpackExamplesSeeder::GIT_REPOSITORY)
))->toBeTrue();
expect($applications->every(
fn (Application $application) => $application->git_branch === ($examples->get($application->uuid)['git_branch'] ?? DevelopmentRailpackExamplesSeeder::GIT_BRANCH)
))->toBeTrue();
@ -79,6 +82,9 @@ function seedRailpackExamplePrerequisites(): void
$pythonFlask = $applications->firstWhere('uuid', 'railpack-python-flask');
$goGin = $applications->firstWhere('uuid', 'railpack-go-gin');
$rust = $applications->firstWhere('uuid', 'railpack-rust');
$githubDeployKey = $applications->firstWhere('uuid', 'railpack-github-deploy-key');
$gitlabDeployKey = $applications->firstWhere('uuid', 'railpack-gitlab-deploy-key');
$gitlabPublic = $applications->firstWhere('uuid', 'railpack-gitlab-public-example');
expect($nestjs)
->not->toBeNull()
@ -113,6 +119,33 @@ function seedRailpackExamplePrerequisites(): void
expect($rust)
->not->toBeNull()
->and($rust->ports_exposes)->toBe('8000');
expect($githubDeployKey)
->not->toBeNull()
->and($githubDeployKey->git_repository)->toBe('git@github.com:coollabsio/coolify-examples-deploy-key.git')
->and($githubDeployKey->git_branch)->toBe('main')
->and($githubDeployKey->build_pack)->toBe('railpack')
->and($githubDeployKey->private_key_id)->toBe(1)
->and($githubDeployKey->source_type)->toBe(GithubApp::class)
->and($githubDeployKey->source_id)->toBe(0);
expect($gitlabDeployKey)
->not->toBeNull()
->and($gitlabDeployKey->git_repository)->toBe('git@gitlab.com:coollabsio/php-example.git')
->and($gitlabDeployKey->git_branch)->toBe('main')
->and($gitlabDeployKey->build_pack)->toBe('railpack')
->and($gitlabDeployKey->private_key_id)->toBe(1)
->and($gitlabDeployKey->source_type)->toBe(GitlabApp::class)
->and($gitlabDeployKey->source_id)->toBe(1);
expect($gitlabPublic)
->not->toBeNull()
->and($gitlabPublic->git_repository)->toBe('https://gitlab.com/andrasbacsai/coolify-examples.git')
->and($gitlabPublic->base_directory)->toBe('/astro/static')
->and($gitlabPublic->publish_directory)->toBe('/dist')
->and($gitlabPublic->build_pack)->toBe('railpack')
->and($gitlabPublic->source_type)->toBe(GitlabApp::class)
->and($gitlabPublic->settings->is_static)->toBeTrue();
});
it('skips the railpack examples outside development mode', function () {

View file

@ -4,11 +4,50 @@
use App\Models\ApplicationSetting;
use App\Models\GitlabApp;
use App\Models\PrivateKey;
use Illuminate\Support\Collection;
afterEach(function () {
Mockery::close();
});
function commandStrings(array|Collection|string $commands): Collection
{
if (is_string($commands)) {
return collect([$commands]);
}
return collect($commands)->map(fn ($command) => data_get($command, 'command') ?? $command[0] ?? $command);
}
function privateKeyMaterializationCommands(array|Collection|string $commands): Collection
{
if (is_string($commands)) {
$commands = [$commands];
}
return collect($commands)->filter(fn ($command) => str(commandStrings([$command])->first())->contains('base64 -d | tee /root/.ssh/id_rsa_coolify_'));
}
function expectCommandListToContain(array|Collection|string $commands, string $expected): void
{
expect(commandStrings($commands)->implode(' && '))->toContain($expected);
}
function expectCommandListNotToContain(array|Collection|string $commands, string $expected): void
{
expect(commandStrings($commands)->implode(' && '))->not->toContain($expected);
}
function expectPrivateKeyMaterializationCommandsSkipLogging(array|Collection|string $commands): void
{
$keyCommands = privateKeyMaterializationCommands($commands);
expect($keyCommands)->not->toBeEmpty();
$keyCommands->each(function ($command): void {
expect(data_get($command, 'skip_command_log'))->toBeTrue();
});
}
/**
* 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
@ -33,6 +72,7 @@
expect($source)
->toContain('$skip_command_log = data_get($single_command, \'skip_command_log\', false);')
->toContain('if ($command_hidden && ! $skip_command_log && isset($this->application_deployment_queue))')
->toContain('use ($command, $hidden, $customType, $append, $command_hidden, $skip_command_log)')
->toContain('\'command\' => $skip_command_log || $command_hidden ? null : $this->redact_sensitive_info($command),');
});
@ -48,11 +88,11 @@
$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
expectCommandListToContain($result['commands'], "tee {$keyPath}");
expectCommandListToContain($result['commands'], "-i {$keyPath} -o IdentitiesOnly=yes");
expectCommandListToContain($result['commands'], "trap 'rm -f {$keyPath}' EXIT"); // removed when the shell exits
expectCommandListNotToContain($result['commands'], 'tee /root/.ssh/id_rsa >'); // never overwrites the host root's own key
expectPrivateKeyMaterializationCommandsSkipLogging($result['commands']);
});
it('writes a deploy key to a per-deployment path for ls-remote inside docker without a trap', function () use ($keyPath) {
@ -67,11 +107,11 @@
$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 >');
expectCommandListToContain($result['commands'], "tee {$keyPath}");
expectCommandListToContain($result['commands'], "-i {$keyPath} -o IdentitiesOnly=yes");
expectCommandListNotToContain($result['commands'], 'trap '); // ephemeral container, no cleanup needed
expectCommandListNotToContain($result['commands'], 'tee /root/.ssh/id_rsa >');
expectPrivateKeyMaterializationCommandsSkipLogging($result['commands']);
});
it('writes a GitLab source private key to a per-deployment path with cleanup on the host', function () use ($keyPath) {
@ -94,11 +134,11 @@
$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 >');
expectCommandListToContain($result['commands'], "tee {$keyPath}");
expectCommandListToContain($result['commands'], "-i {$keyPath} -o IdentitiesOnly=yes");
expectCommandListToContain($result['commands'], "trap 'rm -f {$keyPath}' EXIT");
expectCommandListNotToContain($result['commands'], 'tee /root/.ssh/id_rsa >');
expectPrivateKeyMaterializationCommandsSkipLogging($result['commands']);
});
it('writes a deploy key to a per-deployment path and cleans it up when cloning on the host', function () use ($keyPath) {
@ -121,11 +161,11 @@
// 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 >');
expectCommandListToContain($result['commands'], "tee {$keyPath}");
expectCommandListToContain($result['commands'], "-i {$keyPath} -o IdentitiesOnly=yes");
expectCommandListToContain($result['commands'], "trap 'rm -f {$keyPath}' EXIT");
expectCommandListNotToContain($result['commands'], 'tee /root/.ssh/id_rsa >');
expectPrivateKeyMaterializationCommandsSkipLogging($result['commands']);
});
it('writes a GitLab source private key to a per-deployment path and cleans it up when cloning on the host', function () use ($keyPath) {
@ -154,11 +194,11 @@
$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 >');
expectCommandListToContain($result['commands'], "tee {$keyPath}");
expectCommandListToContain($result['commands'], "-i {$keyPath} -o IdentitiesOnly=yes");
expectCommandListToContain($result['commands'], "trap 'rm -f {$keyPath}' EXIT");
expectCommandListNotToContain($result['commands'], 'tee /root/.ssh/id_rsa >');
expectPrivateKeyMaterializationCommandsSkipLogging($result['commands']);
});
it('uses the per-deployment deploy key for pull request fetches', function () use ($keyPath) {
@ -180,9 +220,9 @@
$result = $application->generateGitImportCommands('test-deployment-uuid', 123, 'github', false);
expect($result['commands'])
->toContain("GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p 22 -o Port=22 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {$keyPath} -o IdentitiesOnly=yes\" git fetch origin pull/123/head:pr-123-coolify")
->not->toContain('GIT_SSH_COMMAND="ssh -o ConnectTimeout=30 -p 22 -o Port=22 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa" git fetch origin pull/123/head:pr-123-coolify');
expectCommandListToContain($result['commands'], "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p 22 -o Port=22 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {$keyPath} -o IdentitiesOnly=yes\" git fetch origin pull/123/head:pr-123-coolify");
expectCommandListNotToContain($result['commands'], 'GIT_SSH_COMMAND="ssh -o ConnectTimeout=30 -p 22 -o Port=22 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa" git fetch origin pull/123/head:pr-123-coolify');
expectPrivateKeyMaterializationCommandsSkipLogging($result['commands']);
});
it('does not force a missing per-deployment key for other repository pull request fetches', function () use ($keyPath) {
@ -200,7 +240,6 @@
$result = $application->generateGitImportCommands('test-deployment-uuid', 123, 'github', false);
expect($result['commands'])
->toContain('GIT_SSH_COMMAND="ssh -o ConnectTimeout=30 -p 22 -o Port=22 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa" git fetch origin pull/123/head:pr-123-coolify')
->not->toContain($keyPath);
expectCommandListToContain($result['commands'], 'GIT_SSH_COMMAND="ssh -o ConnectTimeout=30 -p 22 -o Port=22 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa" git fetch origin pull/123/head:pr-123-coolify');
expectCommandListNotToContain($result['commands'], $keyPath);
});

View file

@ -3,11 +3,45 @@
use App\Models\Application;
use App\Models\GitlabApp;
use App\Models\PrivateKey;
use Illuminate\Support\Collection;
afterEach(function () {
Mockery::close();
});
function gitlabCommandStrings(array|Collection|string $commands): Collection
{
if (is_string($commands)) {
return collect([$commands]);
}
return collect($commands)->map(fn ($command) => data_get($command, 'command') ?? $command[0] ?? $command);
}
function expectGitlabCommandListToContain(array|Collection|string $commands, string $expected): void
{
expect(gitlabCommandStrings($commands)->implode(' && '))->toContain($expected);
}
function expectGitlabCommandListNotToContain(array|Collection|string $commands, string $expected): void
{
expect(gitlabCommandStrings($commands)->implode(' && '))->not->toContain($expected);
}
function expectGitlabPrivateKeyMaterializationCommandsSkipLogging(array|Collection|string $commands): void
{
if (is_string($commands)) {
$commands = [$commands];
}
$keyCommands = collect($commands)->filter(fn ($command) => str(data_get($command, 'command') ?? $command[0] ?? $command)->contains('base64 -d | tee /root/.ssh/id_rsa_coolify_'));
expect($keyCommands)->not->toBeEmpty();
$keyCommands->each(function ($command): void {
expect(data_get($command, 'skip_command_log'))->toBeTrue();
});
}
it('generates ls-remote commands for GitLab source with private key', function () {
$deploymentUuid = 'test-deployment-uuid';
@ -15,7 +49,8 @@
$privateKey->shouldReceive('getAttribute')->with('private_key')->andReturn('fake-private-key');
$gitlabSource = Mockery::mock(GitlabApp::class)->makePartial();
$gitlabSource->shouldReceive('getMorphClass')->andReturn(\App\Models\GitlabApp::class);
$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);
@ -34,16 +69,18 @@
expect($result)->toBeArray();
expect($result)->toHaveKey('commands');
expect($result['commands'])->toContain('git ls-remote');
expect($result['commands'])->toContain('id_rsa');
expect($result['commands'])->toContain('mkdir -p /root/.ssh');
expectGitlabCommandListToContain($result['commands'], 'git ls-remote');
expectGitlabCommandListToContain($result['commands'], 'id_rsa');
expectGitlabCommandListToContain($result['commands'], 'mkdir -p /root/.ssh');
expectGitlabPrivateKeyMaterializationCommandsSkipLogging($result['commands']);
});
it('generates ls-remote commands for GitLab source without private key', function () {
$deploymentUuid = 'test-deployment-uuid';
$gitlabSource = Mockery::mock(GitlabApp::class)->makePartial();
$gitlabSource->shouldReceive('getMorphClass')->andReturn(\App\Models\GitlabApp::class);
$gitlabSource->shouldReceive('getMorphClass')->andReturn(GitlabApp::class);
$gitlabSource->shouldReceive('getAttribute')->with('html_url')->andReturn('https://gitlab.com');
$gitlabSource->shouldReceive('getAttribute')->with('privateKey')->andReturn(null);
$gitlabSource->shouldReceive('getAttribute')->with('private_key_id')->andReturn(null);
@ -61,17 +98,18 @@
expect($result)->toBeArray();
expect($result)->toHaveKey('commands');
expect($result['commands'])->toContain('git ls-remote');
expect($result['commands'])->toContain('https://gitlab.com/user/repo.git');
expectGitlabCommandListToContain($result['commands'], 'git ls-remote');
expectGitlabCommandListToContain($result['commands'], 'https://gitlab.com/user/repo.git');
// Should NOT contain SSH key setup
expect($result['commands'])->not->toContain('id_rsa');
expectGitlabCommandListNotToContain($result['commands'], 'id_rsa');
});
it('does not return null for GitLab source type', function () {
$deploymentUuid = 'test-deployment-uuid';
$gitlabSource = Mockery::mock(GitlabApp::class)->makePartial();
$gitlabSource->shouldReceive('getMorphClass')->andReturn(\App\Models\GitlabApp::class);
$gitlabSource->shouldReceive('getMorphClass')->andReturn(GitlabApp::class);
$gitlabSource->shouldReceive('getAttribute')->with('html_url')->andReturn('https://gitlab.com');
$gitlabSource->shouldReceive('getAttribute')->with('privateKey')->andReturn(null);
$gitlabSource->shouldReceive('getAttribute')->with('private_key_id')->andReturn(null);