diff --git a/services/nginx/app/classes/coolify_api_client.php b/services/nginx/app/classes/coolify_api_client.php index cfdeb9ee..a3d7ce0f 100644 --- a/services/nginx/app/classes/coolify_api_client.php +++ b/services/nginx/app/classes/coolify_api_client.php @@ -159,6 +159,11 @@ class coolify_api_client return $this->request('GET', '/applications/' . rawurlencode($uuid) . '/restart'); } + public function stopApplication(string $uuid): array + { + return $this->request('GET', '/applications/' . rawurlencode($uuid) . '/stop'); + } + public function deleteService(string $uuid): array { return $this->request('DELETE', '/services/' . rawurlencode($uuid)); diff --git a/services/nginx/app/classes/release_manager.php b/services/nginx/app/classes/release_manager.php index e7fc8124..f327c902 100644 --- a/services/nginx/app/classes/release_manager.php +++ b/services/nginx/app/classes/release_manager.php @@ -1435,6 +1435,7 @@ class release_manager } $expectedCommit = self::normalizeCommitSha((string)($gateInput['expected_commit'] ?? '')); + $enforceExpectedCommit = $expectedCommit !== '' && !$this->releaseGateAutoSyncRequested($gateInput); $checked = []; try { foreach ($gateInput['api_ping_paths'] as $path) { @@ -1444,7 +1445,7 @@ class release_manager throw new RuntimeException(sprintf('%s returned success=false.', $path)); } $actualCommit = $this->releaseGateApiPayloadCommitSha($payload); - if ($expectedCommit !== '' && !$this->releaseGateCommitMatches($actualCommit, $expectedCommit)) { + if ($enforceExpectedCommit && !$this->releaseGateCommitMatches($actualCommit, $expectedCommit)) { throw new RuntimeException(sprintf( '%s returned commit %s, expected %s.', $path, @@ -6257,6 +6258,7 @@ class release_manager } $deployment = $client->deployResource($serviceUuid, $this->releaseCoolifyForceRebuild($context)); + $previousApplications = $this->stopCoolifyPreviousApplications($client, $context, $serviceUuid); return [ 'service_uuid' => $serviceUuid, 'resource_type' => $resourceType, @@ -6266,9 +6268,76 @@ class release_manager 'updated' => self::redactPayload($update ?? []), 'runtime_env' => $runtimeEnvUpdate, 'deployment' => self::redactPayload($deployment), + 'previous_applications' => self::redactPayload($previousApplications), ]; } + private function stopCoolifyPreviousApplications(coolify_api_client $client, array $context, string $activeUuid): array + { + $stopped = []; + foreach ($this->releaseCoolifyPreviousApplicationUuids($context, $activeUuid) as $uuid) { + try { + $stopped[] = [ + 'uuid' => $uuid, + 'status' => 'stop_requested', + 'result' => $client->stopApplication($uuid), + ]; + } catch (Throwable $throwable) { + $stopped[] = [ + 'uuid' => $uuid, + 'status' => 'warning', + 'error' => $throwable->getMessage(), + ]; + } + } + + return $stopped; + } + + private function releaseCoolifyPreviousApplicationUuids(array $context, string $activeUuid): array + { + $values = []; + foreach ([ + 'coolify_previous_application_uuid', + 'coolify_previous_artifact_app_uuid', + 'coolify_previous_artifact_application_uuid', + 'previous_application_uuid', + 'previous_app_uuid', + ] as $key) { + if (is_scalar($context[$key] ?? null)) { + $values[] = (string)$context[$key]; + } + } + + foreach ([ + 'coolify_previous_application_uuids', + 'coolify_previous_artifact_app_uuids', + 'previous_application_uuids', + 'previous_app_uuids', + ] as $key) { + if (!is_array($context[$key] ?? null)) { + continue; + } + foreach ($context[$key] as $value) { + if (is_scalar($value)) { + $values[] = (string)$value; + } + } + } + + $activeUuid = trim($activeUuid); + $uuids = []; + foreach ($values as $value) { + $uuid = trim((string)$value); + if ($uuid === '' || $uuid === $activeUuid || in_array($uuid, $uuids, true)) { + continue; + } + $uuids[] = $uuid; + } + + return $uuids; + } + private function updateCoolifyReleaseRuntimeEnv(coolify_api_client $client, string $resourceUuid, string $resourceType, array $target, array $context): ?array { $env = $this->releaseCoolifyRuntimeEnv($target, $context); diff --git a/services/nginx/app/tests/Unit/ReleaseManager/ReleaseManagerTest.php b/services/nginx/app/tests/Unit/ReleaseManager/ReleaseManagerTest.php index 7e830a6e..99d1abf3 100644 --- a/services/nginx/app/tests/Unit/ReleaseManager/ReleaseManagerTest.php +++ b/services/nginx/app/tests/Unit/ReleaseManager/ReleaseManagerTest.php @@ -1010,6 +1010,7 @@ it('defines release manager schema, routes, permissions, and system-status integ expect($manager)->toContain('createService'); expect($manager)->toContain('updateService'); expect(file_get_contents(app_path('classes/coolify_api_client.php')))->toContain('updateApplicationEnvsBulk'); + expect(file_get_contents(app_path('classes/coolify_api_client.php')))->toContain('stopApplication'); expect($manager)->toContain('channel_presets'); expect($manager)->toContain('target_presets'); @@ -1400,6 +1401,27 @@ it('resolves release deployment endpoints from manual overrides, URLs, health ch ]))->toBe('https://gateway.example.test/beta/frontend'); }); +it('collects previous Coolify application UUIDs for stale route cleanup', function (): void { + $manager = new release_manager(); + $previousApplications = new ReflectionMethod(release_manager::class, 'releaseCoolifyPreviousApplicationUuids'); + $previousApplications->setAccessible(true); + + expect($previousApplications->invoke($manager, [ + 'coolify_previous_artifact_app_uuid' => 'old-artifact-app', + 'coolify_previous_application_uuid' => 'old-application', + 'coolify_previous_application_uuids' => [ + 'old-application', + 'active-application', + 'other-old-application', + '', + ], + ], 'active-application'))->toBe([ + 'old-application', + 'old-artifact-app', + 'other-old-application', + ]); +}); + it('redacts GitHub access metadata from public release versions', function (): void { $manager = new release_manager(); $publicVersion = new ReflectionMethod(release_manager::class, 'publicVersion');