diff --git a/VERSION b/VERSION index 5bc23cd..98390b6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.23 +1.3.24 diff --git a/bootstrap/app.php b/bootstrap/app.php index 5d5d787..175d502 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -21,6 +21,21 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { + // The one endpoint that must answer WHILE the application is down. + // + // It is the deployment's own status, read by the overlay every three + // seconds. Behind maintenance mode it answered 503 for the whole run, + // so the overlay had nothing to show but "still waiting" and could not + // name the step it was on — and any Livewire request that fired in the + // same window swapped the console for Laravel's 503 page. + // + // Safe to exempt: it reports version, commit and a step name, it is + // behind the console's host and network guards, and it writes nothing. + $middleware->preventRequestsDuringMaintenance(except: [ + 'update/state', + '*/update/state', + ]); + $middleware->alias([ 'admin' => EnsureAdmin::class, 'admin.host' => RestrictAdminHost::class, diff --git a/resources/js/app.js b/resources/js/app.js index 033ee5b..5c7b2d5 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -134,6 +134,23 @@ function recoverFrom419() { document.addEventListener('livewire:init', () => { window.Livewire.hook('request', ({ fail }) => { fail(({ status, preventDefault }) => { + // 503: the application is in maintenance mode, which during a + // deployment is not a fault — it is the event being watched. + // + // Livewire's default is to replace the document with the response + // body, so a wire:poll that happened to fire mid-update swapped the + // console for Laravel's 503 page: the operator saw the overlay, + // then the error page, then the reload. Reported as "the 503 must + // not appear at all during the update", and it must not. + // + // Swallowed for every Livewire request, not just the poll: any + // action taken in that window would do the same thing. + if (status === 503) { + preventDefault(); + + return; + } + if (status !== 419) return; preventDefault(); diff --git a/resources/views/livewire/admin/settings.blade.php b/resources/views/livewire/admin/settings.blade.php index f4229f1..df1c13d 100644 --- a/resources/views/livewire/admin/settings.blade.php +++ b/resources/views/livewire/admin/settings.blade.php @@ -34,8 +34,13 @@ open. wire:poll below is unrelated and stays: it is Livewire's own refresh of this card's own details (log tail, elapsed time) for as long as a Livewire connection exists. --}} + {{-- The poll stops while a run is in flight. The overlay's own + watcher is what follows a deployment — a plain fetch with no + component state behind it, which is why it survives the + restart. This one cannot, and every attempt during maintenance + mode was a Livewire request answering 503. --}}
+ @if (! $update['running']) wire:poll.{{ $update['checking'] ? '3s' : '30s' }} @endif>
diff --git a/tests/Feature/Admin/UpdateButtonTest.php b/tests/Feature/Admin/UpdateButtonTest.php index 3076494..69aa1d7 100644 --- a/tests/Feature/Admin/UpdateButtonTest.php +++ b/tests/Feature/Admin/UpdateButtonTest.php @@ -951,3 +951,29 @@ it('runs the workers as the same user as the web process', function () { "\nnormalise_ownership\n", ))->toBe(2); }); + +it('keeps answering its own status while the application is down', function () { + // The deployment's status is the one thing that must be readable DURING + // the deployment. Behind maintenance mode it answered 503 for the whole + // run, so the overlay had nothing to show but "still waiting" and could + // never name the step it was on. + expect(Illuminate\Support\Facades\File::get(base_path('bootstrap/app.php'))) + ->toContain("preventRequestsDuringMaintenance(except: [") + ->toContain("'update/state'") + ->toContain("'*/update/state'"); +}); + +it('does not let a Livewire request during maintenance replace the page', function () { + // Reported with the network panel open: `update` and `state` both 503, and + // Livewire's default on a failed request is to render the response body — + // so a wire:poll that happened to fire mid-deployment swapped the console + // for Laravel's 503 page. Overlay, then error page, then reload. + // + // 503 is not a fault here. It IS the event being watched. + $app = Illuminate\Support\Facades\File::get(resource_path('js/app.js')); + + expect($app)->toContain('if (status === 503) {') + // And the poll that used to fire it stops for the duration. + ->and(Illuminate\Support\Facades\File::get(resource_path('views/livewire/admin/settings.blade.php'))) + ->toContain("@if (! \$update['running']) wire:poll"); +});