From c314319a7eff0fcd39e3ed0b75e8cd2c19e68e77 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 17 Aug 2026 16:23:30 +0200 Subject: [PATCH] fix(sudo): preserve substitutions in backup shell commands (#11329) --- AGENTS.md | 1 + bootstrap/helpers/sudo.php | 1 + tests/Unit/ParseCommandsByLineForSudoTest.php | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 86fc0f00b..5563a18ec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -179,6 +179,7 @@ Coolify seeds **instance-owned** rows at primary key `0`. That value is a sentin - Run `vendor/bin/pint --dirty --format agent` before finalizing changes - Every change must have tests — write or update tests, then run them. For bug fixes, follow TDD: write a failing test first, then fix the bug (see Test Enforcement below) - Check sibling files for conventions before creating new files +- When adding remote shell commands, account for servers using non-root SSH users: commands pass through `parseCommandsByLineForSudo()`, so test pipelines, redirects, substitutions, and `sh -c`/`bash -c` scripts with the non-root sudo parser. ## Git Workflow diff --git a/bootstrap/helpers/sudo.php b/bootstrap/helpers/sudo.php index b8ef84687..397efc387 100644 --- a/bootstrap/helpers/sudo.php +++ b/bootstrap/helpers/sudo.php @@ -95,6 +95,7 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array $isComplexPipeCommand = ( $line->contains(' | sh') || $line->contains(' | bash') || + $line->contains(' sh -c ') || ($line->contains(' | ') && ($line->contains('||') || $line->contains('&&'))) ); diff --git a/tests/Unit/ParseCommandsByLineForSudoTest.php b/tests/Unit/ParseCommandsByLineForSudoTest.php index f294de35f..b741b875f 100644 --- a/tests/Unit/ParseCommandsByLineForSudoTest.php +++ b/tests/Unit/ParseCommandsByLineForSudoTest.php @@ -24,6 +24,24 @@ test('wraps complex Docker install command with pipes in bash -c', function () { expect($result[0])->toBe("sudo bash -c 'curl https://releases.rancher.com/install-docker/27.3.sh | sh || curl https://get.docker.com | sh'"); }); +test('preserves command substitutions inside database and volume backup scripts', function () { + $script = 'compressor=$(if command -v pigz; then printf pigz; else printf gzip; fi); exec $compressor'; + $command = 'docker exec database pg_dumpall | docker run --rm -i helper sh -c '.escapeshellarg($script); + $volumeCommand = 'docker run --rm helper sh -c '.escapeshellarg($script).' > /data/coolify/backups/volume.tar.gz'; + + $result = parseCommandsByLineForSudo(collect([$command, $volumeCommand]), $this->server); + + expect($result[0]) + ->toStartWith("sudo bash -c '") + ->toContain('compressor=$(if command -v pigz; then') + ->not->toContain('$(sudo if') + ->not->toContain('| sudo docker run') + ->and($result[1]) + ->toStartWith("sudo bash -c '") + ->toContain('compressor=$(if command -v pigz; then') + ->not->toContain('$(sudo if'); +}); + test('wraps complex Docker install command with multiple fallbacks', function () { $commands = collect([ 'curl --max-time 300 https://releases.rancher.com/install-docker/27.3.sh | sh || curl https://get.docker.com | sh -s -- --version 27.3',