From f94f4d97ffe2f56175890767bfb3440bb6266f74 Mon Sep 17 00:00:00 2001 From: Mathieu JESER Date: Fri, 28 Aug 2026 16:53:10 +0200 Subject: [PATCH 1/6] =?UTF-8?q?refactor:=20rename=20CoolifyTask=20?= =?UTF-8?q?=E2=86=92=20DevForgeTask?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/Jobs/DevForgeTask.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 backend/app/Jobs/DevForgeTask.php diff --git a/backend/app/Jobs/DevForgeTask.php b/backend/app/Jobs/DevForgeTask.php new file mode 100644 index 0000000000..f8257d6117 --- /dev/null +++ b/backend/app/Jobs/DevForgeTask.php @@ -0,0 +1,14 @@ + Date: Fri, 28 Aug 2026 16:55:26 +0200 Subject: [PATCH 2/6] noop --- backend/app/Actions/Server/UpdateDevForge.php | 1 + 1 file changed, 1 insertion(+) create mode 100644 backend/app/Actions/Server/UpdateDevForge.php diff --git a/backend/app/Actions/Server/UpdateDevForge.php b/backend/app/Actions/Server/UpdateDevForge.php new file mode 100644 index 0000000000..b3a425249b --- /dev/null +++ b/backend/app/Actions/Server/UpdateDevForge.php @@ -0,0 +1 @@ +placeholder \ No newline at end of file From 3d43d313e0e93cde1fa9c87cdb017603e5c4479a Mon Sep 17 00:00:00 2001 From: Mathieu JESER Date: Fri, 28 Aug 2026 17:01:30 +0200 Subject: [PATCH 3/6] fix: instance upgrade via docker.sock when SSH is down --- backend/app/Actions/Server/UpdateCoolify.php | 127 +------------ backend/app/Actions/Server/UpdateDevForge.php | 174 +++++++++++++++++- .../DevForge/InstanceUpgradeService.php | 55 +++++- backend/config/constants.php | 11 +- 4 files changed, 236 insertions(+), 131 deletions(-) diff --git a/backend/app/Actions/Server/UpdateCoolify.php b/backend/app/Actions/Server/UpdateCoolify.php index 1530cc2e81..be274b6ce0 100644 --- a/backend/app/Actions/Server/UpdateCoolify.php +++ b/backend/app/Actions/Server/UpdateCoolify.php @@ -2,128 +2,9 @@ namespace App\Actions\Server; -use App\Models\Server; -use Illuminate\Support\Facades\Http; -use Illuminate\Support\Facades\Log; -use Illuminate\Support\Sleep; -use Lorisleiva\Actions\Concerns\AsAction; - -class UpdateCoolify +/** + * @deprecated Use UpdateDevForge. Kept so existing jobs and Livewire keep resolving. + */ +class UpdateCoolify extends UpdateDevForge { - use AsAction; - - public ?Server $server = null; - - public ?string $latestVersion = null; - - public ?string $currentVersion = null; - - public function handle($manual_update = false) - { - if (isDev()) { - Sleep::for(10)->seconds(); - - return; - } - $settings = instanceSettings(); - $this->server = Server::find(0); - if (! $this->server) { - return; - } - - // Fetch fresh version from CDN instead of using cache - try { - $response = Http::retry(3, 1000)->timeout(10) - ->get(config('constants.coolify.versions_url')); - - if ($response->successful()) { - $versions = $response->json(); - $this->latestVersion = data_get($versions, 'coolify.v4.version'); - } else { - // Fallback to cache if CDN unavailable - $cacheVersion = get_latest_version_of_coolify(); - - // Validate cache version against current running version - if ($cacheVersion && version_compare($cacheVersion, config('constants.coolify.version'), '<')) { - Log::error('Failed to fetch fresh version from CDN and cache is corrupted/outdated', [ - 'cached_version' => $cacheVersion, - 'current_version' => config('constants.coolify.version'), - ]); - throw new \Exception( - 'Cannot determine latest version: CDN unavailable and cache version '. - "({$cacheVersion}) is older than running version (".config('constants.coolify.version').')' - ); - } - - $this->latestVersion = $cacheVersion; - Log::warning('Failed to fetch fresh version from CDN (unsuccessful response), using validated cache', [ - 'version' => $cacheVersion, - ]); - } - } catch (\Throwable $e) { - $cacheVersion = get_latest_version_of_coolify(); - - // Validate cache version against current running version - if ($cacheVersion && version_compare($cacheVersion, config('constants.coolify.version'), '<')) { - Log::error('Failed to fetch fresh version from CDN and cache is corrupted/outdated', [ - 'error' => $e->getMessage(), - 'cached_version' => $cacheVersion, - 'current_version' => config('constants.coolify.version'), - ]); - throw new \Exception( - 'Cannot determine latest version: CDN unavailable and cache version '. - "({$cacheVersion}) is older than running version (".config('constants.coolify.version').')' - ); - } - - $this->latestVersion = $cacheVersion; - Log::warning('Failed to fetch fresh version from CDN, using validated cache', [ - 'error' => $e->getMessage(), - 'version' => $cacheVersion, - ]); - } - - $this->currentVersion = config('constants.coolify.version'); - if (! $manual_update) { - if (! $settings->is_auto_update_enabled) { - return; - } - if ($this->latestVersion === $this->currentVersion) { - return; - } - if (version_compare($this->latestVersion, $this->currentVersion, '<')) { - return; - } - } - - // ALWAYS check for downgrades (even for manual updates) - if (version_compare($this->latestVersion, $this->currentVersion, '<')) { - Log::error('Downgrade prevented', [ - 'target_version' => $this->latestVersion, - 'current_version' => $this->currentVersion, - 'manual_update' => $manual_update, - ]); - throw new \Exception( - "Cannot downgrade from {$this->currentVersion} to {$this->latestVersion}. ". - 'If you need to downgrade, please do so manually via Docker commands.' - ); - } - - $this->update(); - $settings->new_version_available = false; - $settings->save(); - } - - private function update() - { - $latestHelperImageVersion = getHelperVersion(); - $upgradeScriptUrl = config('constants.coolify.upgrade_script_url'); - - remote_process([ - "mkdir -p /data/coolify/source /data/devforge /tmp 2>/dev/null || true", - "curl -fsSL {$upgradeScriptUrl} -o /data/coolify/source/upgrade.sh 2>/dev/null || curl -fsSL {$upgradeScriptUrl} -o /data/devforge/upgrade.sh 2>/dev/null || curl -fsSL {$upgradeScriptUrl} -o /tmp/upgrade.sh", - "chmod +x /data/coolify/source/upgrade.sh 2>/dev/null || chmod +x /data/devforge/upgrade.sh 2>/dev/null || chmod +x /tmp/upgrade.sh 2>/dev/null || true", - "bash /data/coolify/source/upgrade.sh $this->latestVersion $latestHelperImageVersion 2>/dev/null || bash /data/devforge/upgrade.sh $this->latestVersion $latestHelperImageVersion 2>/dev/null || bash /tmp/upgrade.sh $this->latestVersion $latestHelperImageVersion", - ], $this->server); - } } diff --git a/backend/app/Actions/Server/UpdateDevForge.php b/backend/app/Actions/Server/UpdateDevForge.php index b3a425249b..706118caef 100644 --- a/backend/app/Actions/Server/UpdateDevForge.php +++ b/backend/app/Actions/Server/UpdateDevForge.php @@ -1 +1,173 @@ -placeholder \ No newline at end of file +seconds(); + + return; + } + $settings = instanceSettings(); + $this->server = Server::find(0); + if (! $this->server) { + return; + } + + // Fetch fresh version from CDN instead of using cache + try { + $response = Http::retry(3, 1000)->timeout(10) + ->get(config('constants.devforge.versions_url')); + + if ($response->successful()) { + $versions = $response->json(); + $this->latestVersion = data_get($versions, 'devforge.version') + ?? data_get($versions, 'coolify.v4.version'); + } else { + // Fallback to cache if CDN unavailable + $cacheVersion = get_latest_version_of_coolify(); + + // Validate cache version against current running version + if ($cacheVersion && version_compare($cacheVersion, config('constants.devforge.version'), '<')) { + Log::error('Failed to fetch fresh version from CDN and cache is corrupted/outdated', [ + 'cached_version' => $cacheVersion, + 'current_version' => config('constants.devforge.version'), + ]); + throw new \Exception( + 'Cannot determine latest version: CDN unavailable and cache version '. + "({$cacheVersion}) is older than running version (".config('constants.devforge.version').')' + ); + } + + $this->latestVersion = $cacheVersion; + Log::warning('Failed to fetch fresh version from CDN (unsuccessful response), using validated cache', [ + 'version' => $cacheVersion, + ]); + } + } catch (\Throwable $e) { + $cacheVersion = get_latest_version_of_coolify(); + + // Validate cache version against current running version + if ($cacheVersion && version_compare($cacheVersion, config('constants.devforge.version'), '<')) { + Log::error('Failed to fetch fresh version from CDN and cache is corrupted/outdated', [ + 'error' => $e->getMessage(), + 'cached_version' => $cacheVersion, + 'current_version' => config('constants.devforge.version'), + ]); + throw new \Exception( + 'Cannot determine latest version: CDN unavailable and cache version '. + "({$cacheVersion}) is older than running version (".config('constants.devforge.version').')' + ); + } + + $this->latestVersion = $cacheVersion; + Log::warning('Failed to fetch fresh version from CDN, using validated cache', [ + 'error' => $e->getMessage(), + 'version' => $cacheVersion, + ]); + } + + $this->currentVersion = config('constants.devforge.version'); + if (! $manual_update) { + if (! $settings->is_auto_update_enabled) { + return; + } + if ($this->latestVersion === $this->currentVersion) { + return; + } + if (version_compare($this->latestVersion, $this->currentVersion, '<')) { + return; + } + } + + // ALWAYS check for downgrades (even for manual updates) + if (version_compare($this->latestVersion, $this->currentVersion, '<')) { + Log::error('Downgrade prevented', [ + 'target_version' => $this->latestVersion, + 'current_version' => $this->currentVersion, + 'manual_update' => $manual_update, + ]); + throw new \Exception( + "Cannot downgrade from {$this->currentVersion} to {$this->latestVersion}. ". + 'If you need to downgrade, please do so manually via Docker commands.' + ); + } + + $this->update(); + $settings->new_version_available = false; + $settings->save(); + } + + private function update() + { + $latestHelperImageVersion = getHelperVersion(); + $upgradeScriptUrl = config('constants.devforge.upgrade_script_url'); + + $commands = [ + 'mkdir -p /data/coolify/source /data/devforge /tmp 2>/dev/null || true', + "curl -fsSL {$upgradeScriptUrl} -o /data/coolify/source/upgrade.sh 2>/dev/null || curl -fsSL {$upgradeScriptUrl} -o /data/devforge/upgrade.sh 2>/dev/null || curl -fsSL {$upgradeScriptUrl} -o /tmp/upgrade.sh", + 'chmod +x /data/coolify/source/upgrade.sh 2>/dev/null || chmod +x /data/devforge/upgrade.sh 2>/dev/null || chmod +x /tmp/upgrade.sh 2>/dev/null || true', + "bash /data/coolify/source/upgrade.sh $this->latestVersion $latestHelperImageVersion 2>/dev/null || bash /data/devforge/upgrade.sh $this->latestVersion $latestHelperImageVersion 2>/dev/null || bash /tmp/upgrade.sh $this->latestVersion $latestHelperImageVersion", + ]; + + $reachable = (bool) data_get($this->server->settings, 'is_reachable', false); + $canLocal = InstanceUpgradeService::canRunLocalDockerUpgrade(); + + // CasaOS / Zima: SSH to host.docker.internal is often broken — prefer docker.sock. + if ($this->server->isLocalhost() && $canLocal && ! $reachable) { + Log::info('Starting DevForge upgrade via local docker.sock (SSH unreachable)'); + $this->updateViaLocalProcess($commands); + + return; + } + + try { + remote_process($commands, $this->server); + } catch (\Throwable $e) { + if ($this->server->isLocalhost() && $canLocal) { + Log::warning('SSH upgrade failed, falling back to local docker.sock', [ + 'error' => $e->getMessage(), + ]); + $this->updateViaLocalProcess($commands); + + return; + } + + throw $e; + } + } + + /** + * @param list $commands + */ + private function updateViaLocalProcess(array $commands): void + { + $script = implode("\n", $commands); + $result = Process::timeout(600)->run(['bash', '-lc', $script]); + + if ($result->failed()) { + $detail = trim($result->errorOutput() ?: $result->output() ?: 'erreur inconnue'); + + throw new \Exception('Mise à jour locale échouée : '.$detail); + } + } +} diff --git a/backend/app/Services/DevForge/InstanceUpgradeService.php b/backend/app/Services/DevForge/InstanceUpgradeService.php index 794bb1f4a9..a58eaa2bd8 100644 --- a/backend/app/Services/DevForge/InstanceUpgradeService.php +++ b/backend/app/Services/DevForge/InstanceUpgradeService.php @@ -2,7 +2,7 @@ namespace App\Services\DevForge; -use App\Actions\Server\UpdateCoolify; +use App\Actions\Server\UpdateDevForge; use App\Models\InstanceSettings; use App\Models\Server; use DateTimeInterface; @@ -34,7 +34,7 @@ public function status(?InstanceSettings $settings = null): array */ public function availability(?InstanceSettings $settings = null): array { - $currentVersion = (string) config('constants.coolify.version'); + $currentVersion = (string) config('constants.devforge.version'); $latestVersion = $this->latestVersion(); $resolved = self::resolveAvailability( $currentVersion, @@ -79,6 +79,17 @@ public static function resolveAvailability( */ public function progress(): array { + foreach (self::statusFileCandidates() as $path) { + if (! is_readable($path)) { + continue; + } + + $parsed = self::parseStatusFile(@file_get_contents($path) ?: null); + if ($parsed['status'] !== 'none') { + return $parsed; + } + } + $server = Server::find(0); if (! $server) { return self::idleProgress(); @@ -97,6 +108,25 @@ public function progress(): array return self::parseStatusFile(is_string($content) ? $content : null); } + /** + * @return list + */ + public static function statusFileCandidates(): array + { + return [ + '/data/coolify/source/.upgrade-status', + '/data/devforge/.upgrade-status', + '/tmp/.upgrade-status', + '/DATA/AppData/devforge/.upgrade-status', + '/media/Docker/AppData/devforge/.upgrade-status', + ]; + } + + public static function canRunLocalDockerUpgrade(): bool + { + return is_readable('/var/run/docker.sock') || is_readable('/run/docker.sock'); + } + /** * @return array{status: string, step: int, message: string|null} */ @@ -168,8 +198,27 @@ public function start(?InstanceSettings $settings = null): array ]); } + $server = Server::find(0); + if (! $server) { + throw ValidationException::withMessages([ + 'upgrade' => ['Serveur localhost introuvable. Impossible de lancer la mise à jour.'], + ]); + } + + $reachable = (bool) data_get($server->settings, 'is_reachable', false); + $canLocal = self::canRunLocalDockerUpgrade(); + + if (! $reachable && ! $canLocal) { + throw ValidationException::withMessages([ + 'upgrade' => [ + 'Serveur localhost injoignable en SSH, et Docker n’est pas accessible depuis le conteneur. ' + .'Réparez le SSH (host.docker.internal:22) ou mettez à jour DevForge depuis CasaOS/ZimaOS.', + ], + ]); + } + try { - UpdateCoolify::run(manual_update: true); + UpdateDevForge::run(manual_update: true); } catch (\Throwable $e) { throw ValidationException::withMessages([ 'upgrade' => [$e->getMessage()], diff --git a/backend/config/constants.php b/backend/config/constants.php index d5db7f7eab..53ce9c9e61 100644 --- a/backend/config/constants.php +++ b/backend/config/constants.php @@ -1,7 +1,6 @@ [ +$devforgeConstants = [ 'version' => '4.1.13', 'helper_version' => 'helper', 'realtime_version' => 'realtime', @@ -17,7 +16,12 @@ 'versions_url' => env('VERSIONS_URL', 'https://raw.githubusercontent.com/bobdivx/devforge-store/main/versions.json'), 'upgrade_script_url' => env('UPGRADE_SCRIPT_URL', 'https://raw.githubusercontent.com/bobdivx/devforge/main/scripts/upgrade.sh'), 'releases_url' => env('RELEASES_URL', 'https://raw.githubusercontent.com/coollabsio/coolify-cdn/main/json/releases.json'), - ], +]; + +return [ + 'devforge' => $devforgeConstants, + // BC alias for legacy config('constants.coolify.*') + 'coolify' => $devforgeConstants, 'urls' => [ 'docs' => 'https://github.com/bobdivx/devforge', @@ -30,7 +34,6 @@ 'official' => 'https://raw.githubusercontent.com/coollabsio/coolify/v4.x/templates/service-templates-latest.json', 'file_name' => 'service-templates-latest.json', ], - 'terminal' => [ 'protocol' => env('TERMINAL_PROTOCOL'), 'host' => env('TERMINAL_HOST'), From c14c5d12e3b76c1df16139b6903472b64d22361c Mon Sep 17 00:00:00 2001 From: Mathieu JESER Date: Fri, 28 Aug 2026 17:03:25 +0200 Subject: [PATCH 4/6] fix: portal Modal + unlock upgrade UI until in_progress --- frontend/src/components/ui/Modal.tsx | 24 +++++++++++++++++++----- frontend/src/lib/use-instance-upgrade.ts | 5 +++-- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/ui/Modal.tsx b/frontend/src/components/ui/Modal.tsx index 64daca070f..632a7bcc60 100644 --- a/frontend/src/components/ui/Modal.tsx +++ b/frontend/src/components/ui/Modal.tsx @@ -1,5 +1,6 @@ import { X } from 'lucide-preact'; import type { ComponentChildren } from 'preact'; +import { createPortal } from 'preact/compat'; import { useEffect } from 'preact/hooks'; type ModalProps = { @@ -42,15 +43,27 @@ export function Modal({ return () => window.removeEventListener('keydown', onKeyDown); }, [open, onClose, dismissible]); - if (!open) { + useEffect(() => { + if (!open || typeof document === 'undefined') { + return; + } + + const previous = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + return () => { + document.body.style.overflow = previous; + }; + }, [open]); + + if (!open || typeof document === 'undefined') { return null; } - return ( -