fix(updates): report a failed manual update check instead of "No new version available"

Closes #12266
This commit is contained in:
Andras Bacsai
2026-10-23 22:27:07 +02:00
parent 5131832cd8
commit ab65fdccf9
4 changed files with 72 additions and 7 deletions
+1
View File
@@ -26,6 +26,7 @@
- Do not symlink `vendor` into another worktree. Composer resolves the base path from the real `vendor` path, so the tests load the app classes of the other checkout. Run `composer install --ignore-platform-req=ext-zip` in the worktree.
- To assert that the unsaved-changes bar shows, check `el.classList.contains('is-dirty')`. Playwright treats the hidden bar (`opacity-0`) as visible, and its class attribute always contains the text `is-dirty` from Tailwind variants.
- Do not call `sleep()` in a browser test. The app server runs in the test process, so `sleep()` stops it; use `$page->wait()`.
- `Queue::fake()` also captures `dispatchSync()` of a `ShouldQueue` job, so the job never runs. Fake only the jobs to block (`Queue::fake([SomeJob::class])`).
- In tests, turning on `is_metrics_enabled` (and other Sentinel settings) with `save()`/`update()` runs the settings hook, which starts Sentinel over a real SSH connection on the sync queue. Use `saveQuietly()` or `Queue::fake()`.
- Call `visit()` directly in each `tests/v4/Browser` test body; Pest does not mark a test that only uses helper-wrapped `visit()` as a browser test, so it fails with `sendText() on null`.
- SQLite returns `0`/`1` for boolean columns without a `boolean` cast, while Postgres returns real booleans. A strict `=== false` check on such a column never matches in tests, so tests can pass on behavior that Postgres blocks. Add the cast or compare loosely.
+12 -4
View File
@@ -8,6 +8,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -21,15 +22,18 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
$this->cdnTries = $cdnTries;
}
public function handle(): void
/**
* @return bool Whether the check finished and new_version_available is up to date
*/
public function handle(): bool
{
try {
if (isDev() || isCloud()) {
return;
return false;
}
$versions = refresh_versions_file($this->cdnTries);
if ($versions === null) {
return;
return false;
}
CheckTraefikVersionJob::dispatch();
@@ -37,8 +41,12 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
$latestVersion = data_get($versions, 'coolify.v4.version');
$newVersionAvailable = version_compare($latestVersion, config('constants.coolify.version'), '>');
instanceSettings()->update(['new_version_available' => $newVersionAvailable]);
return true;
} catch (\Throwable $e) {
// Consider implementing a notification to administrators
Log::warning('Could not check for Coolify updates', ['error' => $e->getMessage()]);
return false;
}
}
}
+7 -3
View File
@@ -159,10 +159,14 @@ class Updates extends Component
public function checkManually()
{
$this->authorize('update', $this->settings);
CheckForUpdatesJob::dispatchSync(cdnTries: 1);
$checked = (new CheckForUpdatesJob(cdnTries: 1))->handle();
$this->dispatch('updateAvailable');
$settings = instanceSettings();
if ($settings->new_version_available) {
if (! $checked) {
$this->dispatch('error', 'Could not check for updates. Try again later.');
return;
}
if (instanceSettings()->new_version_available) {
$this->dispatch('success', 'New version available!');
} else {
$this->dispatch('success', 'No new version available.');
@@ -0,0 +1,52 @@
<?php
use App\Jobs\CheckTraefikVersionJob;
use App\Livewire\Settings\Updates;
use App\Models\InstanceSettings;
use App\Models\Server;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Once;
use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function () {
Queue::fake([CheckTraefikVersionJob::class]);
config()->set('constants.coolify.version', '4.4.6');
$rootTeam = Team::find(0) ?? Team::factory()->create(['id' => 0]);
Server::factory()->create(['id' => 0, 'team_id' => $rootTeam->id]);
InstanceSettings::forceCreate(['id' => 0]);
Once::flush();
$user = User::factory()->create();
$rootTeam->members()->attach($user->id, ['role' => 'admin']);
$this->actingAs($user);
session(['currentTeam' => ['id' => $rootTeam->id]]);
});
it('reports a failed CDN request instead of "No new version available"', function () {
Http::fake(['*' => Http::response('error code: 1200', 503)]);
File::partialMock()->shouldReceive('put')->with(base_path('versions.json'), Mockery::any())->never();
Livewire::test(Updates::class)
->call('checkManually')
->assertDispatched('error', 'Could not check for updates. Try again later.')
->assertNotDispatched('success');
});
it('reports a new version when the CDN has a newer version', function () {
Http::fake(['*' => Http::response(['coolify' => ['v4' => ['version' => '4.4.7']]])]);
File::partialMock()->shouldReceive('put')->with(base_path('versions.json'), Mockery::any())->once();
Livewire::test(Updates::class)
->call('checkManually')
->assertDispatched('success', 'New version available!');
expect(InstanceSettings::get()->new_version_available)->toBeTruthy();
});