From 92c8ad449fb07ee4272fa8c3d6600b9e8c044c2d Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 12 Mar 2026 13:32:43 +0100 Subject: [PATCH] feat(git-import): support custom ssh command for fetch, submodule, and lfs Allow passing a custom GIT_SSH_COMMAND to setGitImportSettings() so that git fetch, submodule update, and lfs pull use the same SSH authentication as the initial clone. This is required for git sources like GitLab that use custom ports and identity files. Also remove unnecessary SSH retry event tracking and add test coverage. --- app/Models/Application.php | 24 ++++++---- app/Traits/ExecuteRemoteCommand.php | 7 --- tests/Feature/ApplicationRollbackTest.php | 58 +++++++++++++++++++++++ 3 files changed, 73 insertions(+), 16 deletions(-) diff --git a/app/Models/Application.php b/app/Models/Application.php index 7b46b6f3d..9f59445d8 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -1087,12 +1087,16 @@ public function dirOnServer() return application_configuration_dir()."/{$this->uuid}"; } - public function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false, ?string $commit = null) + public function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false, ?string $commit = null, ?string $git_ssh_command = null) { $baseDir = $this->generateBaseDir($deployment_uuid); $escapedBaseDir = escapeshellarg($baseDir); $isShallowCloneEnabled = $this->settings?->is_git_shallow_clone_enabled ?? false; + // Use the full GIT_SSH_COMMAND (including -i for SSH key and port options) when provided, + // so that git fetch, submodule update, and lfs pull can authenticate the same way as git clone. + $sshCommand = $git_ssh_command ?? 'GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"'; + // Use the explicitly passed commit (e.g. from rollback), falling back to the application's git_commit_sha. // Invalid refs will cause the git checkout/fetch command to fail on the remote server. $commitToUse = $commit ?? $this->git_commit_sha; @@ -1102,9 +1106,9 @@ public function setGitImportSettings(string $deployment_uuid, string $git_clone_ // If shallow clone is enabled and we need a specific commit, // we need to fetch that specific commit with depth=1 if ($isShallowCloneEnabled) { - $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git fetch --depth=1 origin {$escapedCommit} && git -c advice.detachedHead=false checkout {$escapedCommit} >/dev/null 2>&1"; + $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && {$sshCommand} git fetch --depth=1 origin {$escapedCommit} && git -c advice.detachedHead=false checkout {$escapedCommit} >/dev/null 2>&1"; } else { - $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git -c advice.detachedHead=false checkout {$escapedCommit} >/dev/null 2>&1"; + $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && {$sshCommand} git -c advice.detachedHead=false checkout {$escapedCommit} >/dev/null 2>&1"; } } if ($this->settings->is_git_submodules_enabled) { @@ -1115,10 +1119,10 @@ public function setGitImportSettings(string $deployment_uuid, string $git_clone_ } // Add shallow submodules flag if shallow clone is enabled $submoduleFlags = $isShallowCloneEnabled ? '--depth=1' : ''; - $git_clone_command = "{$git_clone_command} git submodule sync && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git submodule update --init --recursive {$submoduleFlags}; fi"; + $git_clone_command = "{$git_clone_command} git submodule sync && {$sshCommand} git submodule update --init --recursive {$submoduleFlags}; fi"; } if ($this->settings->is_git_lfs_enabled) { - $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git lfs pull"; + $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && {$sshCommand} git lfs pull"; } return $git_clone_command; @@ -1407,11 +1411,12 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req $private_key = base64_encode($private_key); $gitlabPort = $gitlabSource->custom_port ?? 22; $escapedCustomRepository = escapeshellarg($customRepository); - $git_clone_command_base = "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_clone_command} {$escapedCustomRepository} {$escapedBaseDir}"; + $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\""; + $git_clone_command_base = "{$gitlabSshCommand} {$git_clone_command} {$escapedCustomRepository} {$escapedBaseDir}"; if ($only_checkout) { $git_clone_command = $git_clone_command_base; } else { - $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base, commit: $commit); + $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base, commit: $commit, git_ssh_command: $gitlabSshCommand); } if ($exec_in_docker) { $commands = collect([ @@ -1477,11 +1482,12 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req } $private_key = base64_encode($private_key); $escapedCustomRepository = escapeshellarg($customRepository); - $git_clone_command_base = "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_clone_command} {$escapedCustomRepository} {$escapedBaseDir}"; + $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\""; + $git_clone_command_base = "{$deployKeySshCommand} {$git_clone_command} {$escapedCustomRepository} {$escapedBaseDir}"; if ($only_checkout) { $git_clone_command = $git_clone_command_base; } else { - $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base, commit: $commit); + $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base, commit: $commit, git_ssh_command: $deployKeySshCommand); } if ($exec_in_docker) { $commands = collect([ diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php index a60a47b93..a4ea6abe5 100644 --- a/app/Traits/ExecuteRemoteCommand.php +++ b/app/Traits/ExecuteRemoteCommand.php @@ -111,13 +111,6 @@ public function execute_remote_command(...$commands) $attempt++; $delay = $this->calculateRetryDelay($attempt - 1); - // Track SSH retry event in Sentry - $this->trackSshRetryEvent($attempt, $maxRetries, $delay, $errorMessage, [ - 'server' => $this->server->name ?? $this->server->ip ?? 'unknown', - 'command' => $this->redact_sensitive_info($command), - 'trait' => 'ExecuteRemoteCommand', - ]); - // Add log entry for the retry if (isset($this->application_deployment_queue)) { $this->addRetryLogEntry($attempt, $maxRetries, $delay, $errorMessage); diff --git a/tests/Feature/ApplicationRollbackTest.php b/tests/Feature/ApplicationRollbackTest.php index f62ad6650..61b3505ae 100644 --- a/tests/Feature/ApplicationRollbackTest.php +++ b/tests/Feature/ApplicationRollbackTest.php @@ -85,4 +85,62 @@ expect($result)->not->toContain('advice.detachedHead=false checkout'); }); + + test('setGitImportSettings uses provided git_ssh_command for fetch', function () { + $this->application->settings->is_git_shallow_clone_enabled = true; + $rollbackCommit = 'abc123def456abc123def456abc123def456abc1'; + $sshCommand = 'GIT_SSH_COMMAND="ssh -o ConnectTimeout=30 -p 22222 -o Port=22222 -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa"'; + + $result = $this->application->setGitImportSettings( + deployment_uuid: 'test-uuid', + git_clone_command: 'git clone', + commit: $rollbackCommit, + git_ssh_command: $sshCommand, + ); + + expect($result) + ->toContain('-i /root/.ssh/id_rsa" git fetch --depth=1 origin') + ->toContain($rollbackCommit); + }); + + test('setGitImportSettings uses provided git_ssh_command for submodule update', function () { + $this->application->settings->is_git_submodules_enabled = true; + $sshCommand = '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"'; + + $result = $this->application->setGitImportSettings( + deployment_uuid: 'test-uuid', + git_clone_command: 'git clone', + git_ssh_command: $sshCommand, + ); + + expect($result) + ->toContain('-i /root/.ssh/id_rsa" git submodule update --init --recursive'); + }); + + test('setGitImportSettings uses provided git_ssh_command for lfs pull', function () { + $this->application->settings->is_git_lfs_enabled = true; + $sshCommand = '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"'; + + $result = $this->application->setGitImportSettings( + deployment_uuid: 'test-uuid', + git_clone_command: 'git clone', + git_ssh_command: $sshCommand, + ); + + expect($result)->toContain('-i /root/.ssh/id_rsa" git lfs pull'); + }); + + test('setGitImportSettings uses default ssh command when git_ssh_command not provided', function () { + $this->application->settings->is_git_lfs_enabled = true; + + $result = $this->application->setGitImportSettings( + deployment_uuid: 'test-uuid', + git_clone_command: 'git clone', + public: true, + ); + + expect($result) + ->toContain('GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" git lfs pull') + ->not->toContain('-i /root/.ssh/id_rsa'); + }); });