From 6492d081362c009519481ac70e50873e39ba1861 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:23:29 +0200 Subject: [PATCH] fix(upgrade): use authenticated status polling after restart Remove the version header from the health endpoint and use upgrade status polling to determine completion. --- app/Http/Controllers/Api/OtherController.php | 11 +-- resources/views/livewire/upgrade.blade.php | 86 ++++++------------- .../Feature/HealthcheckVersionHeaderTest.php | 8 +- tests/Unit/CoolifyUpgradeStatusTest.php | 13 +-- 4 files changed, 38 insertions(+), 80 deletions(-) diff --git a/app/Http/Controllers/Api/OtherController.php b/app/Http/Controllers/Api/OtherController.php index 46d0a1a84..392b1fd4c 100644 --- a/app/Http/Controllers/Api/OtherController.php +++ b/app/Http/Controllers/Api/OtherController.php @@ -292,20 +292,13 @@ class OtherController extends Controller #[OA\Get( summary: 'Healthcheck', - description: 'Healthcheck endpoint. Includes the running Coolify version in the X-Coolify-Version header.', + description: 'Healthcheck endpoint.', path: '/health', operationId: 'healthcheck', responses: [ new OA\Response( response: 200, description: 'Healthcheck endpoint.', - headers: [ - new OA\Header( - header: 'X-Coolify-Version', - description: 'Currently running Coolify version.', - schema: new OA\Schema(type: 'string', example: '4.3.1'), - ), - ], content: new OA\MediaType( mediaType: 'text/html', schema: new OA\Schema(type: 'string'), @@ -323,6 +316,6 @@ class OtherController extends Controller )] public function healthcheck(Request $request) { - return response('OK')->header('X-Coolify-Version', (string) config('constants.coolify.version')); + return response('OK'); } } diff --git a/resources/views/livewire/upgrade.blade.php b/resources/views/livewire/upgrade.blade.php index 92b50967a..b476704f7 100644 --- a/resources/views/livewire/upgrade.blade.php +++ b/resources/views/livewire/upgrade.blade.php @@ -256,34 +256,6 @@ return 4; }, - hasReachedTargetVersion(running, target) { - if (!running || !target) { - return false; - } - const normalize = (version) => String(version).replace(/^v/i, ''); - running = normalize(running); - target = normalize(target); - if (running === target) { - return true; - } - - return running.localeCompare(target, undefined, { - numeric: true, - sensitivity: 'base', - }) >= 0; - }, - - isReadyToReload(runningVersion) { - if (this.hasReachedTargetVersion(runningVersion, this.latestVersion)) { - return true; - } - - // Releases before this header existed (e.g. 4.3.1) still - // return a healthy /api/health with no X-Coolify-Version. - // Only treat that as done after the instance actually went down. - return !runningVersion && this.instanceWentDown; - }, - startHealthWatch() { if (this.checkHealthInterval) { return; @@ -293,38 +265,36 @@ }, 2000); }, - probeHealth() { + async probeHealth() { this.healthCheckAttempts++; const elapsedMinutes = Math.floor((Date.now() - this.startTime) / 60000); - return fetch('/api/health') - .then(response => { - const runningVersion = response.headers.get('X-Coolify-Version'); - if (!response.ok) { - this.instanceWentDown = true; - this.currentStep = 4; - this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this.healthCheckAttempts); - return; - } - if (this.isReadyToReload(runningVersion)) { - this.showSuccess(); - return; - } - if (!this.instanceWentDown && this.currentStep < 4) { - return; - } - if (runningVersion) { - this.currentStatus = `Coolify is still on ${runningVersion}. Waiting for ${this.latestVersion}...`; - } else { - this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this.healthCheckAttempts); - } - }) - .catch(error => { - console.error('Health check failed:', error); + try { + const response = await fetch('/api/health'); + if (!response.ok) { this.instanceWentDown = true; this.currentStep = 4; this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this.healthCheckAttempts); - }); + return; + } + if (!this.instanceWentDown && this.currentStep < 4) { + return; + } + + const data = await this.$wire.getUpgradeStatus(); + if (data.status === 'complete') { + this.showSuccess(); + } else if (data.status === 'error') { + this.showError(data.message); + } else { + this.currentStatus = data.message ?? this.getReviveStatusMessage(elapsedMinutes, this.healthCheckAttempts); + } + } catch (error) { + console.error('Health check failed:', error); + this.instanceWentDown = true; + this.currentStep = 4; + this.currentStatus = this.getReviveStatusMessage(elapsedMinutes, this.healthCheckAttempts); + } }, getReviveStatusMessage(elapsedMinutes, attempts) { @@ -434,13 +404,7 @@ this.currentStep = this.mapStepToUI(data.step); this.currentStatus = data.message; } else if (data.status === 'complete') { - if (this.isReadyToReload(data.running_version)) { - this.showSuccess(); - } else { - this.currentStep = 4; - this.currentStatus = `Waiting for Coolify ${this.latestVersion} to come online...`; - this.revive(); - } + this.showSuccess(); } else if (data.status === 'error') { this.showError(data.message); } else if (data.status === 'none' && this.instanceWentDown) { diff --git a/tests/Feature/HealthcheckVersionHeaderTest.php b/tests/Feature/HealthcheckVersionHeaderTest.php index 7acf78c6f..f86f84867 100644 --- a/tests/Feature/HealthcheckVersionHeaderTest.php +++ b/tests/Feature/HealthcheckVersionHeaderTest.php @@ -3,20 +3,20 @@ use App\Http\Controllers\Api\OtherController; use Illuminate\Http\Request; -it('adds the running version header on the healthcheck response', function () { +it('does not expose the running version on the healthcheck response', function () { config(['constants.coolify.version' => '4.3.1']); $response = (new OtherController)->healthcheck(Request::create('/api/health', 'GET')); expect($response->getContent())->toBe('OK') - ->and($response->headers->get('X-Coolify-Version'))->toBe('4.3.1'); + ->and($response->headers->has('X-Coolify-Version'))->toBeFalse(); }); -it('exposes the running Coolify version on the public health endpoint', function () { +it('does not expose the running Coolify version on the public health endpoint', function () { config(['constants.coolify.version' => '4.3.1']); $this->get('/api/health') ->assertSuccessful() ->assertSee('OK') - ->assertHeader('X-Coolify-Version', '4.3.1'); + ->assertHeaderMissing('X-Coolify-Version'); }); diff --git a/tests/Unit/CoolifyUpgradeStatusTest.php b/tests/Unit/CoolifyUpgradeStatusTest.php index 68fa2ee94..0ce2e7e88 100644 --- a/tests/Unit/CoolifyUpgradeStatusTest.php +++ b/tests/Unit/CoolifyUpgradeStatusTest.php @@ -119,23 +119,24 @@ it('ignores stale status files older than ten minutes', function () { expect($result['status'])->toBe('none'); }); -it('waits for the running version to match the target before showing reload', function () { +it('checks the running version through authenticated upgrade status before showing reload', function () { $upgradeView = file_get_contents(__DIR__.'/../../resources/views/livewire/upgrade.blade.php'); expect($upgradeView) - ->toContain('X-Coolify-Version') - ->toContain('hasReachedTargetVersion') + ->not->toContain('X-Coolify-Version') + ->toContain('this.$wire.getUpgradeStatus()') + ->toContain("data.status === 'complete'") ->toContain('livewireFailures'); }); -it('treats a healthy instance without a version header as ready only after downtime', function () { +it('uses the public health endpoint only for liveness during an upgrade', function () { $upgradeView = file_get_contents(__DIR__.'/../../resources/views/livewire/upgrade.blade.php'); expect($upgradeView) ->toContain('instanceWentDown') - ->toContain('isReadyToReload') ->toContain('startHealthWatch') - ->toContain('data.status === \'none\''); + ->toContain("fetch('/api/health')") + ->not->toContain('response.headers.get'); }); it('starts the upgrade after the Livewire response so status polling is not blocked', function () {