diff --git a/.ai/lessons.md b/.ai/lessons.md index f4c7c22956..711eb544a8 100644 --- a/.ai/lessons.md +++ b/.ai/lessons.md @@ -91,3 +91,7 @@ ## Prefer named API values over numeric sentinels - When an API option means an unbounded or special mode, expose a clear named value such as `all`. - Keep an existing numeric sentinel such as `-1` only as a compatibility alias unless the user requests a breaking change. + +## Do not auto-heal existing deployments without a request +- When a parser or label fix can apply only after container recreation, keep the change limited to new deployments and later user-initiated redeployments unless the user explicitly asks for live reconciliation. +- Do not add status lookup fallbacks that alter existing deployment behavior when the requested scope is new deployments only. diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index e50b852eca..067a27bc20 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -2546,7 +2546,7 @@ function serviceParser(Service $resource): Collection projectName: $resource->project()->name, resourceName: $resource->name, type: 'service', - subType: $isDatabase ? 'database' : 'application', + subType: $savedService instanceof ServiceDatabase ? 'database' : 'application', subId: $savedService->id, subName: $savedService->human_name ?? $savedService->name, environment: $resource->environment->name, diff --git a/tests/Feature/TraefikServiceDockerNetworkLabelTest.php b/tests/Feature/TraefikServiceDockerNetworkLabelTest.php index c098a2e93a..cdd4136e65 100644 --- a/tests/Feature/TraefikServiceDockerNetworkLabelTest.php +++ b/tests/Feature/TraefikServiceDockerNetworkLabelTest.php @@ -47,3 +47,38 @@ YAML, expect($labels->values()->all())->toContain("traefik.docker.network={$service->uuid}"); }); + +it('labels an existing service application from its stored type after its image changes', function () { + Bus::fake(); + + $team = Team::factory()->create(); + $server = Server::factory()->create(['team_id' => $team->id]); + $destination = StandaloneDocker::where('server_id', $server->id)->firstOrFail(); + $project = Project::factory()->create(['team_id' => $team->id]); + $environment = Environment::factory()->create(['project_id' => $project->id]); + $service = Service::factory()->create([ + 'environment_id' => $environment->id, + 'server_id' => $server->id, + 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), + 'docker_compose_raw' => <<<'YAML' +services: + app: + image: postgres:17 +YAML, + ]); + + $serviceApplication = ServiceApplication::create([ + 'name' => 'app', + 'service_id' => $service->id, + 'image' => 'nginx:latest', + ]); + + $parsedCompose = serviceParser($service); + $labels = collect(data_get($parsedCompose, 'services.app.labels')); + + expect($serviceApplication->fresh()->image)->toBe('postgres:17') + ->and($labels->values()->all()) + ->toContain('coolify.service.subType=application') + ->not->toContain('coolify.service.subType=database'); +});