diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php index a2c3d06da9..5c709f5494 100644 --- a/app/Traits/ExecuteRemoteCommand.php +++ b/app/Traits/ExecuteRemoteCommand.php @@ -162,17 +162,16 @@ trait ExecuteRemoteCommand $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, $skip_command_log) { - $output = str($output)->trim(); - if ($output->startsWith('╔')) { - $output = "\n".$output; - } - // Sanitize output to ensure valid UTF-8 encoding before JSON encoding $sanitized_output = sanitize_utf8_text($output); + $log_output = str($sanitized_output)->trim(); + if ($log_output->startsWith('╔')) { + $log_output = "\n".$log_output; + } $new_log_entry = [ 'command' => $skip_command_log || $command_hidden ? null : $this->redact_sensitive_info($command), - 'output' => $this->redact_sensitive_info($sanitized_output), + 'output' => $this->redact_sensitive_info($log_output), 'type' => $customType ?? ($type === 'err' ? 'stderr' : 'stdout'), 'timestamp' => Carbon::now('UTC'), 'hidden' => $hidden, @@ -206,17 +205,7 @@ trait ExecuteRemoteCommand $this->application_deployment_queue->save(); - if ($this->save) { - if (data_get($this->saved_outputs, $this->save, null) === null) { - $this->saved_outputs->put($this->save, str()); - } - if ($append) { - $current_value = $this->saved_outputs->get($this->save); - $this->saved_outputs->put($this->save, str($current_value.str($sanitized_output)->trim())); - } else { - $this->saved_outputs->put($this->save, str($sanitized_output)->trim()); - } - } + $this->saveCommandOutput($sanitized_output, $append); }); $this->application_deployment_queue->update([ 'current_process_id' => $process->id(), @@ -245,6 +234,22 @@ trait ExecuteRemoteCommand } } + private function saveCommandOutput(string $output, bool $append): void + { + if (! $this->save) { + return; + } + + if ($append) { + $currentValue = $this->saved_outputs->get($this->save, ''); + $this->saved_outputs->put($this->save, str($currentValue.$output)); + + return; + } + + $this->saved_outputs->put($this->save, str($output)); + } + /** * Add a log entry for SSH retry attempts */ diff --git a/tests/Unit/ExecuteRemoteCommandTest.php b/tests/Unit/ExecuteRemoteCommandTest.php new file mode 100644 index 0000000000..a7b8008d45 --- /dev/null +++ b/tests/Unit/ExecuteRemoteCommandTest.php @@ -0,0 +1,47 @@ +save = 'dockerfile'; + $this->saved_outputs = collect(); + } + + public function collectOutput(string $output, bool $append = true): void + { + $this->saveCommandOutput($output, $append); + } + }; +} + +it('preserves whitespace across streamed saved output chunks', function () { + $collector = remoteCommandOutputCollector(); + + foreach (["FROM alpine\nARG FIRST", "\n", 'ARG', ' ', "SECOND\nRUN true\n"] as $chunk) { + $collector->collectOutput($chunk); + } + + expect((string) $collector->saved_outputs->get('dockerfile')) + ->toBe("FROM alpine\nARG FIRST\nARG SECOND\nRUN true\n"); +}); + +it('replaces saved output without trimming it when append is disabled', function () { + $collector = remoteCommandOutputCollector(); + $collector->collectOutput('old'); + + $collector->collectOutput(" new output\n", append: false); + + expect((string) $collector->saved_outputs->get('dockerfile'))->toBe(" new output\n"); +});