fix(service): defer stop when pulling latest images

Ensure restart actions flow through StartService so pull-latest restarts can
avoid stopping the service before image pulls. Also raise the changelog modal
above the desktop sidebar toggle.
This commit is contained in:
Andras Bacsai
2026-05-31 21:19:18 +02:00
parent 38e855b20e
commit c9fcc0bc44
6 changed files with 94 additions and 11 deletions
+5 -3
View File
@@ -13,8 +13,10 @@ class RestartService
public function handle(Service $service, bool $pullLatestImages)
{
StopService::run($service);
return StartService::run($service, $pullLatestImages);
return StartService::run(
service: $service,
pullLatestImages: $pullLatestImages,
stopBeforeStart: true,
);
}
}
+6 -1
View File
@@ -19,7 +19,7 @@ class StartService
public function handle(Service $service, bool $pullLatestImages = false, bool $stopBeforeStart = false)
{
$service->parse();
if ($stopBeforeStart) {
if ($this->shouldStopBeforeStarting($pullLatestImages, $stopBeforeStart)) {
StopService::run(service: $service, dockerCleanup: false);
}
$service->saveComposeConfigs();
@@ -53,4 +53,9 @@ class StartService
return remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged');
}
private function shouldStopBeforeStarting(bool $pullLatestImages, bool $stopBeforeStart): bool
{
return $stopBeforeStart && ! $pullLatestImages;
}
}
+2 -6
View File
@@ -1261,8 +1261,7 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/clsx": {
"version": "2.1.1",
@@ -1752,7 +1751,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -1946,8 +1944,7 @@
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.0",
@@ -1992,7 +1989,6 @@
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -198,7 +198,7 @@
<!-- What's New Modal -->
@if ($showWhatsNewModal)
<div class="fixed inset-0 z-50 flex items-center justify-center py-6 px-4"
<div class="fixed inset-0 z-[60] flex items-center justify-center py-6 px-4"
@keydown.escape.window="$wire.closeWhatsNewModal()">
<!-- Background overlay -->
<div class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs" wire:click="closeWhatsNewModal">
+44
View File
@@ -0,0 +1,44 @@
<?php
use App\Livewire\SettingsDropdown;
use App\Models\User;
use App\Services\ChangelogService;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Livewire\Livewire;
it('renders the changelog modal above the desktop sidebar toggle', function () {
$user = new User(['email' => 'test@example.com']);
$user->id = 1;
Auth::setUser($user);
app()->instance(ChangelogService::class, new class extends ChangelogService
{
public function getEntriesForUser(User $user): Collection
{
return collect([
(object) [
'tag_name' => 'v1.0.0',
'title' => 'Test Release',
'content' => 'Release notes',
'content_html' => '<p>Release notes</p>',
'published_at' => Carbon::parse('2026-05-01'),
'is_read' => false,
],
]);
}
public function getUnreadCountForUser(User $user): int
{
return 1;
}
});
Livewire::test(SettingsDropdown::class, ['trigger' => 'changelog-sidebar'])
->call('openWhatsNewModal')
->assertSee('Changelog')
->assertSee('z-[60]', false)
->assertSee('closeWhatsNewModal', false);
});
@@ -0,0 +1,36 @@
<?php
use App\Actions\Service\RestartService;
use App\Actions\Service\StartService;
use App\Actions\Service\StopService;
use App\Models\Service;
it('does not stop a service before pulling latest images', function () {
$method = new ReflectionMethod(StartService::class, 'shouldStopBeforeStarting');
expect($method->invoke(new StartService, pullLatestImages: true, stopBeforeStart: true))->toBeFalse();
});
it('still stops a service before a regular restart', function () {
$method = new ReflectionMethod(StartService::class, 'shouldStopBeforeStarting');
expect($method->invoke(new StartService, pullLatestImages: false, stopBeforeStart: true))->toBeTrue()
->and($method->invoke(new StartService, pullLatestImages: false, stopBeforeStart: false))->toBeFalse();
});
it('routes service restart actions through start service with deferred stop semantics', function () {
$service = Mockery::mock(Service::class);
$stopService = Mockery::mock(StopService::class);
$stopService->shouldNotReceive('handle');
app()->instance(StopService::class, $stopService);
$startService = Mockery::mock(StartService::class);
$startService->shouldReceive('handle')
->once()
->with($service, true, true)
->andReturn('restart queued');
app()->instance(StartService::class, $startService);
expect(RestartService::run($service, true))->toBe('restart queued');
});