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.
This commit is contained in:
Andras Bacsai 2026-03-12 13:32:43 +01:00
parent 709e5e882e
commit 92c8ad449f
3 changed files with 73 additions and 16 deletions

View file

@ -1087,12 +1087,16 @@ public function dirOnServer()
return application_configuration_dir()."/{$this->uuid}"; 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); $baseDir = $this->generateBaseDir($deployment_uuid);
$escapedBaseDir = escapeshellarg($baseDir); $escapedBaseDir = escapeshellarg($baseDir);
$isShallowCloneEnabled = $this->settings?->is_git_shallow_clone_enabled ?? false; $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. // 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. // Invalid refs will cause the git checkout/fetch command to fail on the remote server.
$commitToUse = $commit ?? $this->git_commit_sha; $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, // If shallow clone is enabled and we need a specific commit,
// we need to fetch that specific commit with depth=1 // we need to fetch that specific commit with depth=1
if ($isShallowCloneEnabled) { 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 { } 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) { 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 // Add shallow submodules flag if shallow clone is enabled
$submoduleFlags = $isShallowCloneEnabled ? '--depth=1' : ''; $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) { 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; return $git_clone_command;
@ -1407,11 +1411,12 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
$private_key = base64_encode($private_key); $private_key = base64_encode($private_key);
$gitlabPort = $gitlabSource->custom_port ?? 22; $gitlabPort = $gitlabSource->custom_port ?? 22;
$escapedCustomRepository = escapeshellarg($customRepository); $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) { if ($only_checkout) {
$git_clone_command = $git_clone_command_base; $git_clone_command = $git_clone_command_base;
} else { } 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) { if ($exec_in_docker) {
$commands = collect([ $commands = collect([
@ -1477,11 +1482,12 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
} }
$private_key = base64_encode($private_key); $private_key = base64_encode($private_key);
$escapedCustomRepository = escapeshellarg($customRepository); $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) { if ($only_checkout) {
$git_clone_command = $git_clone_command_base; $git_clone_command = $git_clone_command_base;
} else { } 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) { if ($exec_in_docker) {
$commands = collect([ $commands = collect([

View file

@ -111,13 +111,6 @@ public function execute_remote_command(...$commands)
$attempt++; $attempt++;
$delay = $this->calculateRetryDelay($attempt - 1); $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 // Add log entry for the retry
if (isset($this->application_deployment_queue)) { if (isset($this->application_deployment_queue)) {
$this->addRetryLogEntry($attempt, $maxRetries, $delay, $errorMessage); $this->addRetryLogEntry($attempt, $maxRetries, $delay, $errorMessage);

View file

@ -85,4 +85,62 @@
expect($result)->not->toContain('advice.detachedHead=false checkout'); 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');
});
}); });