fix(runners): preserve unsaved settings during Sysbox checks

Move Sysbox checks and installation into a lazy Livewire component so its requests do not reset the runner form's unsaved state. Update tests to cover the separate component.
This commit is contained in:
Andras Bacsai committed 2026-11-01 21:03:26 +01:00
1 parent 23eebbf2ad
commit faf0b6fac4
6 files changed
+108 -67

No files matched your search

-25
View File
@@ -55,11 +55,6 @@ class GithubRunners extends Component
public bool $allowPullRequests = false;
/** Null until checked over SSH. */
public ?bool $isSysboxInstalled = null;
protected $listeners = ['sysboxInstalled' => 'checkSysbox'];
public function mount(string $server_uuid)
{
try {
@@ -178,7 +173,6 @@ class GithubRunners extends Component
$githubApp = $this->selectedApp;
$this->authorize('update', $githubApp);
if ($this->dockerMode === GithubRunnerDockerMode::Sysbox->value && ! InstallSysbox::isInstalled($this->server)) {
$this->isSysboxInstalled = false;
throw ValidationException::withMessages(['dockerMode' => 'Sysbox is not installed on this server. Install it, then save again.']);
}
@@ -258,25 +252,6 @@ class GithubRunners extends Component
}
}
public function checkSysbox(): void
{
$this->authorize('view', $this->server);
$this->isSysboxInstalled = InstallSysbox::isInstalled($this->server);
}
public function installSysbox()
{
$this->authorize('update', $this->server);
try {
$activity = InstallSysbox::run($this->server);
$this->dispatch('sysbox-install-started');
$this->dispatch('activityMonitor', $activity->id, 'sysboxInstalled');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
/**
* Creates the runner group when the App can run workflow jobs. An App that is not ready is saved anyway:
* the page lists what it misses, and the runner group is created when the first job is provisioned.
+62
View File
@@ -0,0 +1,62 @@
<?php
namespace App\Livewire\Server;
use App\Actions\Server\InstallSysbox;
use App\Models\Server;
use Illuminate\Contracts\View\View;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Lazy;
use Livewire\Component;
/**
* A separate component, so that its requests do not send the unsaved fields of the runner form.
* Otherwise the check would store a new Docker mode in the form snapshot and hide the unsaved bar.
*/
#[Lazy]
class SysboxStatus extends Component
{
use AuthorizesRequests;
public Server $server;
public bool $isInstalled = false;
protected $listeners = ['sysboxInstalled' => 'checkSysbox'];
public function mount(Server $server): void
{
$this->authorize('view', $server);
$this->server = $server;
$this->checkSysbox();
}
public function checkSysbox(): void
{
$this->authorize('view', $this->server);
$this->isInstalled = InstallSysbox::isInstalled($this->server);
}
public function installSysbox()
{
$this->authorize('update', $this->server);
try {
$activity = InstallSysbox::run($this->server);
$this->dispatch('sysbox-install-started');
$this->dispatch('activityMonitor', $activity->id, 'sysboxInstalled');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function placeholder(): View
{
return view('livewire.server.sysbox-status-placeholder');
}
public function render(): View
{
return view('livewire.server.sysbox-status');
}
}
@@ -171,32 +171,8 @@
:placeholder="config('constants.github_runner.image')"
helper="Empty uses the latest official runner image, which Coolify pulls for every runner. Set a tag, for example ghcr.io/actions/actions-runner:2.337.0, to pin a version, or use a custom image based on the official one to add tools. GitHub stops sending jobs to runners that are more than 30 days old." />
</div>
<div x-cloak x-show="$wire.dockerMode === 'sysbox'" class="mt-4"
x-effect="if ($wire.dockerMode === 'sysbox' && $wire.isSysboxInstalled === null) $wire.checkSysbox()">
@if ($isSysboxInstalled === true)
<x-callout type="success" title="Sysbox is installed">
Jobs get a private Docker daemon in an unprivileged container.
</x-callout>
@elseif ($isSysboxInstalled === false)
<x-callout type="warning" title="Sysbox is not installed">
Isolated Docker needs the Sysbox runtime. Coolify installs Sysbox
{{ config('constants.github_runner.sysbox.version') }} from the official package
(Debian and Ubuntu, kernel 5.12 or newer). Docker reloads its configuration, and
running containers are not restarted.
@can('update', $server)
<div class="mt-3">
<x-forms.button type="button" wire:click="installSysbox"
wire:loading.attr="disabled" wire:target="installSysbox">
Install Sysbox
</x-forms.button>
</div>
@endcan
</x-callout>
@else
<x-callout type="info" title="Checking Sysbox">
Coolify checks if Sysbox is installed on this server.
</x-callout>
@endif
<div x-cloak x-show="$wire.dockerMode === 'sysbox'" class="mt-4">
<livewire:server.sysbox-status :server="$server" />
</div>
</x-application.settings-section>
@@ -0,0 +1,5 @@
<div>
<x-callout type="info" title="Checking Sysbox">
Coolify checks if Sysbox is installed on this server.
</x-callout>
</div>
@@ -0,0 +1,22 @@
<div>
@if ($isInstalled)
<x-callout type="success" title="Sysbox is installed">
Jobs get a private Docker daemon in an unprivileged container.
</x-callout>
@else
<x-callout type="warning" title="Sysbox is not installed">
Isolated Docker needs the Sysbox runtime. Coolify installs Sysbox
{{ config('constants.github_runner.sysbox.version') }} from the official package
(Debian and Ubuntu, kernel 5.12 or newer). Docker reloads its configuration, and
running containers are not restarted.
@can('update', $server)
<div class="mt-3">
<x-forms.button type="button" wire:click="installSysbox"
wire:loading.attr="disabled" wire:target="installSysbox">
Install Sysbox
</x-forms.button>
</div>
@endcan
</x-callout>
@endif
</div>
+17 -16
View File
@@ -8,6 +8,7 @@ use App\Jobs\ProvisionGithubRunnerJob;
use App\Livewire\Server\GithubRunnerExecutions;
use App\Livewire\Server\GithubRunners;
use App\Livewire\Server\Show;
use App\Livewire\Server\SysboxStatus;
use App\Livewire\Source\Github\Change;
use App\Livewire\Source\Github\Create;
use App\Models\GithubApp;
@@ -446,8 +447,7 @@ describe('isolated Docker with Sysbox', function () {
->set('githubAppId', $this->githubApp->id)
->set('dockerMode', 'sysbox')
->call('submit')
->assertHasErrors('dockerMode')
->assertSet('isSysboxInstalled', false);
->assertHasErrors('dockerMode');
expect(GithubRunnerConfig::count())->toBe(0);
});
@@ -469,29 +469,29 @@ describe('isolated Docker with Sysbox', function () {
it('checks that Sysbox can mount its FUSE file system', function () {
Process::fake(['*' => Process::result(output: '')]);
Livewire::actingAs($this->owner)
->test(GithubRunners::class, ['server_uuid' => $this->server->uuid])
->call('checkSysbox');
Livewire::withoutLazyLoading()
->actingAs($this->owner)
->test(SysboxStatus::class, ['server' => $this->server]);
Process::assertRan(fn ($process) => str_contains(remoteProcessScript($process), 'test -x /usr/bin/fusermount3'));
});
it('shows the install button when Sysbox is missing', function () {
Process::fake(['*' => Process::result(output: '')]);
GithubRunnerConfig::create(['server_id' => $this->server->id, 'github_app_id' => $this->githubApp->id, 'labels' => ['coolify']]);
Livewire::actingAs($this->owner)
->test(GithubRunners::class, ['server_uuid' => $this->server->uuid])
->call('checkSysbox')
->assertSet('isSysboxInstalled', false)
Livewire::withoutLazyLoading()
->actingAs($this->owner)
->test(SysboxStatus::class, ['server' => $this->server])
->assertSet('isInstalled', false)
->assertSee('Install Sysbox');
});
it('stops before the installation on servers without apt', function () {
Process::fake(['*' => Process::result(output: '')]);
Livewire::actingAs($this->owner)
->test(GithubRunners::class, ['server_uuid' => $this->server->uuid])
Livewire::withoutLazyLoading()
->actingAs($this->owner)
->test(SysboxStatus::class, ['server' => $this->server])
->call('installSysbox')
->assertNotDispatched('activityMonitor');
@@ -499,14 +499,15 @@ describe('isolated Docker with Sysbox', function () {
});
it('does not let members install Sysbox', function () {
Process::fake();
Process::fake(['*' => Process::result(output: '')]);
Livewire::actingAs($this->member)
->test(GithubRunners::class, ['server_uuid' => $this->server->uuid])
Livewire::withoutLazyLoading()
->actingAs($this->member)
->test(SysboxStatus::class, ['server' => $this->server])
->call('installSysbox')
->assertForbidden();
Process::assertNothingRan();
Process::assertDidntRun(fn ($process) => str_contains(remoteProcessScript($process), 'apt-get'));
});
});