mirror of
https://github.com/coollabsio/coolify.git
synced 2026-10-29 10:04:36 +08:00
Co-authored-by: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Co-authored-by: Aditya Tripathi <aditya@climactic.co> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
147 lines
6.9 KiB
PHP
147 lines
6.9 KiB
PHP
<?php
|
|
|
|
use App\Actions\Application\GetRollbackImages;
|
|
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);
|
|
|
|
beforeEach(function () {
|
|
$team = Team::factory()->create();
|
|
// The server is not reachable, so only registry images are listed and no SSH command runs.
|
|
$server = Server::factory()->create(['team_id' => $team->id]);
|
|
$destination = StandaloneDocker::factory()->create(['server_id' => $server->id, 'network' => 'rollback-test']);
|
|
$environment = Environment::factory()->create(['project_id' => Project::factory()->create(['team_id' => $team->id])->id]);
|
|
|
|
$this->application = Application::factory()->create([
|
|
'environment_id' => $environment->id,
|
|
'destination_id' => $destination->id,
|
|
'destination_type' => StandaloneDocker::class,
|
|
'build_pack' => 'nixpacks',
|
|
'docker_registry_image_name' => 'ghcr.io/coollabs/app',
|
|
]);
|
|
});
|
|
|
|
function rollbackDeployment(Application $application, string $commit, string $finishedAt, array $attributes = []): void
|
|
{
|
|
ApplicationDeploymentQueue::create([
|
|
'application_id' => $application->id,
|
|
'deployment_uuid' => fake()->uuid(),
|
|
'commit' => $commit,
|
|
'status' => ApplicationDeploymentStatus::FINISHED->value,
|
|
'pull_request_id' => 0,
|
|
...$attributes,
|
|
])->forceFill(['updated_at' => $finishedAt])->saveQuietly();
|
|
}
|
|
|
|
it('lists the commits of successful deployments as registry images, newest first and once per commit', function () {
|
|
rollbackDeployment($this->application, 'aaaaaaa', '2026-10-01 10:00:00');
|
|
rollbackDeployment($this->application, 'bbbbbbb', '2026-10-02 10:00:00');
|
|
rollbackDeployment($this->application, 'aaaaaaa', '2026-10-03 10:00:00');
|
|
rollbackDeployment($this->application, 'ccccccc', '2026-10-04 10:00:00', ['status' => ApplicationDeploymentStatus::FAILED->value]);
|
|
rollbackDeployment($this->application, 'ddddddd', '2026-10-04 10:00:00', ['pull_request_id' => 5]);
|
|
rollbackDeployment($this->application, 'HEAD', '2026-10-04 10:00:00');
|
|
|
|
$result = GetRollbackImages::run($this->application);
|
|
|
|
expect($result['current'])->toBeNull()
|
|
->and($result['images'])->toBe([
|
|
['tag' => 'aaaaaaa', 'created_at' => '2026-10-03 10:00:00', 'is_current' => false, 'source' => 'registry'],
|
|
['tag' => 'bbbbbbb', 'created_at' => '2026-10-02 10:00:00', 'is_current' => false, 'source' => 'registry'],
|
|
]);
|
|
});
|
|
|
|
it('limits the registry images to the most recent deployments', function () {
|
|
foreach (range(1, GetRollbackImages::REGISTRY_DEPLOYMENTS_LIMIT + 2) as $day) {
|
|
rollbackDeployment($this->application, sprintf('%07d', $day), sprintf('2026-10-%02d 10:00:00', $day));
|
|
}
|
|
|
|
$images = GetRollbackImages::run($this->application)['images'];
|
|
|
|
expect($images)->toHaveCount(GetRollbackImages::REGISTRY_DEPLOYMENTS_LIMIT)
|
|
->and($images[0]['tag'])->toBe(sprintf('%07d', GetRollbackImages::REGISTRY_DEPLOYMENTS_LIMIT + 2));
|
|
});
|
|
|
|
it('lists no registry images when the application does not push commit images', function (array $attributes) {
|
|
$this->application->update($attributes);
|
|
rollbackDeployment($this->application, 'aaaaaaa', '2026-10-01 10:00:00');
|
|
|
|
expect(GetRollbackImages::run($this->application->fresh())['images'])->toBe([]);
|
|
})->with([
|
|
'no registry image name' => [['docker_registry_image_name' => null]],
|
|
'docker image build pack' => [['build_pack' => 'dockerimage']],
|
|
'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(remoteProcessScript($process), '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();
|
|
});
|
|
});
|