From 3d4df5e06fe329f8eb987014207150f62c2a7288 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 23 Oct 2026 23:51:24 +0200 Subject: [PATCH] fix(rollback): find the current image of containers with any name The rollback image list inspected a container named after the bare application UUID, so `current` was null for timestamped, prefixed, and custom-named containers. Find the running production containers by their owner labels instead. Closes #12296 Co-Authored-By: Claude Opus 5.5 --- app/Actions/Application/GetRollbackImages.php | 21 ++++-- tests/Feature/GetRollbackImagesTest.php | 64 +++++++++++++++++++ 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/app/Actions/Application/GetRollbackImages.php b/app/Actions/Application/GetRollbackImages.php index 73ab3931fa..4378fe54cf 100644 --- a/app/Actions/Application/GetRollbackImages.php +++ b/app/Actions/Application/GetRollbackImages.php @@ -4,6 +4,7 @@ namespace App\Actions\Application; use App\Enums\ApplicationDeploymentStatus; use App\Models\Application; +use App\Models\Server; use Lorisleiva\Actions\Concerns\AsAction; /** @@ -27,10 +28,7 @@ class GetRollbackImages $server = $application->destination?->server; if ($server?->isFunctional()) { - $output = instant_remote_process([ - "docker inspect --format='{{.Config.Image}}' {$application->uuid}", - ], $server, throwError: false); - $current = self::imageTag(str($output)->trim()->toString()); + $current = self::currentTag($server, $application); $repository = $application->docker_registry_image_name ?: $application->uuid; $output = instant_remote_process([ @@ -61,6 +59,21 @@ class GetRollbackImages ]; } + /** + * The tag of the running production containers. Container names can have a timestamp or a + * custom name, so find them by their owner labels. Null when no container runs, or when the + * running containers use different tags (for example during a rolling update). + */ + private static function currentTag(Server $server, Application $application): ?string + { + $tags = getCurrentApplicationContainerStatus($server, $application, 0) + ->filter(fn ($container) => data_get($container, 'State') === 'running') + ->map(fn ($container) => self::imageTag((string) data_get($container, 'Image'))) + ->unique(); + + return $tags->count() === 1 ? $tags->first() : null; + } + /** * Each successful deployment pushes `:` to the registry, so the commits of * recent deployments are rollback targets. Docker image applications do not push, and diff --git a/tests/Feature/GetRollbackImagesTest.php b/tests/Feature/GetRollbackImagesTest.php index a9937a70ac..89481424da 100644 --- a/tests/Feature/GetRollbackImagesTest.php +++ b/tests/Feature/GetRollbackImagesTest.php @@ -5,11 +5,14 @@ use App\Enums\ApplicationDeploymentStatus; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; use App\Models\Environment; +use App\Models\InstanceSettings; +use App\Models\PrivateKey; use App\Models\Project; use App\Models\Server; use App\Models\StandaloneDocker; use App\Models\Team; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Process; uses(RefreshDatabase::class); @@ -80,3 +83,64 @@ it('lists no registry images when the application does not push commit images', 'docker compose build pack' => [['build_pack' => 'dockercompose']], 'dockerfile without git' => [['build_pack' => 'dockerfile', 'dockerfile' => 'FROM nginx']], ]); + +describe('on a reachable server', function () { + beforeEach(function () { + Server::flushIdentityMap(); + InstanceSettings::forceCreate(['id' => 0, 'fqdn' => 'https://coolify.test']); + $server = $this->application->destination->server; + $server->update(['private_key_id' => PrivateKey::factory()->create(['team_id' => $server->team_id])->id]); + $server->settings()->update(['is_reachable' => true, 'is_usable' => true]); + $this->application->update(['docker_registry_image_name' => null]); + $this->application = $this->application->fresh(); + }); + + function fakeRollbackServer(Application $application, array $containers): void + { + $uuid = $application->uuid; + $ps = collect($containers)->map(fn (array $container) => json_encode([ + 'ID' => $container['name'], + 'Names' => $container['name'], + 'Image' => $container['image'], + 'State' => $container['state'] ?? 'running', + 'Labels' => "coolify.applicationUuid={$uuid},coolify.pullRequestId=".($container['pr'] ?? 0).',coolify.managed=true', + ]))->implode(PHP_EOL); + $images = implode(PHP_EOL, ["{$uuid}#aaaaaaa#2026-10-01 10:00:00", "{$uuid}#bbbbbbb#2026-10-02 10:00:00"]); + + Process::fake(fn ($process) => Process::result(output: str_contains($process->command, 'docker images') ? $images : $ps)); + } + + it('finds the current image of containers that are not named after the application uuid', function (string $name) { + fakeRollbackServer($this->application, [['name' => $name, 'image' => "{$this->application->uuid}:bbbbbbb"]]); + + $result = GetRollbackImages::run($this->application); + + expect($result['current'])->toBe('bbbbbbb') + ->and(collect($result['images'])->pluck('is_current', 'tag')->all())->toBe(['aaaaaaa' => false, 'bbbbbbb' => true]); + })->with([ + 'timestamped name' => ['app-20261001T100000'], + 'custom prefix' => ['example-app-20261001T100000'], + 'custom internal name' => ['example-app'], + ]); + + it('ignores stopped and preview containers', function () { + $uuid = $this->application->uuid; + fakeRollbackServer($this->application, [ + ['name' => 'example-app', 'image' => "{$uuid}:bbbbbbb"], + ['name' => 'example-app-old', 'image' => "{$uuid}:aaaaaaa", 'state' => 'exited'], + ['name' => "{$uuid}-pr-1", 'image' => "{$uuid}:pr-1", 'pr' => 1], + ]); + + expect(GetRollbackImages::run($this->application)['current'])->toBe('bbbbbbb'); + }); + + it('reports no current image when running containers use different tags', function () { + $uuid = $this->application->uuid; + fakeRollbackServer($this->application, [ + ['name' => 'example-app-20261001T100000', 'image' => "{$uuid}:aaaaaaa"], + ['name' => 'example-app-20261002T100000', 'image' => "{$uuid}:bbbbbbb"], + ]); + + expect(GetRollbackImages::run($this->application)['current'])->toBeNull(); + }); +});