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 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-10-23 23:51:24 +02:00
co-authored by Claude Opus 5.5
parent f97398c83c
commit 3d4df5e06f
2 changed files with 81 additions and 4 deletions
+17 -4
View File
@@ -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 `<image name>:<commit>` to the registry, so the commits of
* recent deployments are rollback targets. Docker image applications do not push, and
+64
View File
@@ -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();
});
});