option('remote')) ?: 'origin'; $branch = trim((string) $this->option('branch')) ?: 'main'; $adminId = (int) $this->option('admin-id'); $force = (bool) $this->option('force'); $driveBackup = (bool) $this->option('drive'); $requireDrive = (bool) $this->option('require-drive'); $skipBackup = (bool) $this->option('skip-backup'); $skipMigrate = (bool) $this->option('skip-migrate'); $skipMaintenance = (bool) $this->option('skip-maintenance'); $skipViewCache = (bool) $this->option('skip-view-cache'); $maintenanceOn = false; $maintenanceError = null; $this->writeProgress(2, 'Avvio refresh nodo da Gitea', 'running'); $fullOutput = []; if (! $skipBackup) { $this->writeProgress(8, 'Backup pre-update in corso', 'running'); $backupArgs = [ '--differential' => true, '--tag' => 'node-refresh-' . now()->format('Ymd-His'), ]; if ($driveBackup) { $backupArgs['--drive'] = true; if ($adminId > 0) { $backupArgs['--admin-id'] = (string) $adminId; } if ($requireDrive) { $backupArgs['--require-drive'] = true; } } $backupExit = Artisan::call('netgescon:preupdate-backup', $backupArgs); $backupOutput = trim((string) Artisan::output()); $fullOutput[] = 'NODE REFRESH: php artisan netgescon:preupdate-backup'; $fullOutput[] = $backupOutput; if ($backupExit !== 0) { $this->error('Backup pre-update fallito.'); $this->line($backupOutput); $this->writeProgress(100, 'Backup pre-update fallito', 'failed', $backupExit); return self::FAILURE; } } if (! $skipMaintenance) { $this->writeProgress(16, 'Blocco accessi e maintenance mode', 'running'); $maintenanceExit = Artisan::call('down', ['--retry' => 60, '--refresh' => 15]); $maintenanceOutput = trim((string) Artisan::output()); $fullOutput[] = 'NODE REFRESH: php artisan down --retry=60 --refresh=15'; $fullOutput[] = $maintenanceOutput; if ($maintenanceExit !== 0) { $this->error('Impossibile attivare la maintenance mode.'); $this->line($maintenanceOutput); $this->writeProgress(100, 'Maintenance mode non attivata', 'failed', $maintenanceExit); return self::FAILURE; } $maintenanceOn = true; } try { $gitParams = [ '--remote' => $remote, '--branch' => $branch, ]; if ($force) { $gitParams['--force'] = true; } $this->info("Sync Git da {$remote}/{$branch}..."); $this->writeProgress(28, 'Sync Git da Gitea in corso', 'running'); $gitExit = Artisan::call('netgescon:git-sync', $gitParams); $gitOutput = trim((string) Artisan::output()); if ($gitExit !== 0) { $this->error('Sync Git fallita.'); $this->line($gitOutput); $this->writeProgress(100, 'Sync Git fallita', 'failed', $gitExit); return self::FAILURE; } $fullOutput[] = 'NODE REFRESH: netgescon:git-sync'; $fullOutput[] = $gitOutput; $this->writeProgress(46, 'Composer install e autoload', 'running'); $composerResult = $this->runShellStep([ 'composer', 'install', '--no-interaction', '--prefer-dist', '--optimize-autoloader', '--no-progress', ]); $fullOutput[] = 'NODE REFRESH: composer install --no-interaction --prefer-dist --optimize-autoloader --no-progress'; $fullOutput[] = $composerResult['output']; if (! $composerResult['success']) { $this->error('Composer install fallito.'); $this->line($composerResult['output']); $this->writeProgress(100, 'Composer install fallito', 'failed', $composerResult['exit_code']); return self::FAILURE; } $this->writeProgress(58, 'Build frontend se disponibile', 'running'); $frontendBuild = $this->attemptFrontendBuild(); if ($frontendBuild !== null) { $fullOutput[] = $frontendBuild['label']; $fullOutput[] = $frontendBuild['output']; } $this->info('Pulizia cache applicative...'); $this->writeProgress(68, 'Pulizia cache applicative', 'running'); $optimizeExit = Artisan::call('optimize:clear'); $optimizeOutput = trim((string) Artisan::output()); $fullOutput[] = 'NODE REFRESH: php artisan optimize:clear'; $fullOutput[] = $optimizeOutput; if ($optimizeExit !== 0) { $this->error('optimize:clear fallito.'); $this->line($optimizeOutput); $this->writeProgress(100, 'Pulizia cache fallita', 'failed', $optimizeExit); return self::FAILURE; } if (! $skipMigrate) { $this->info('Esecuzione migrate --force...'); $this->writeProgress(76, 'Applicazione migration', 'running'); $migrateExit = Artisan::call('migrate', ['--force' => true]); $migrateOutput = trim((string) Artisan::output()); $fullOutput[] = 'NODE REFRESH: php artisan migrate --force'; $fullOutput[] = $migrateOutput; if ($migrateExit !== 0) { $this->error('Migration fallita.'); $this->line($migrateOutput); $this->writeProgress(100, 'Migration fallita', 'failed', $migrateExit); return self::FAILURE; } } if (! $skipViewCache) { $this->info('Ricostruzione view cache...'); $this->writeProgress(84, 'Ricostruzione view cache', 'running'); $viewClearExit = Artisan::call('view:clear'); $viewClearOutput = trim((string) Artisan::output()); $fullOutput[] = 'NODE REFRESH: php artisan view:clear'; $fullOutput[] = $viewClearOutput; if ($viewClearExit !== 0) { $this->error('view:clear fallito.'); $this->line($viewClearOutput); $this->writeProgress(100, 'view:clear fallito', 'failed', $viewClearExit); return self::FAILURE; } $viewCacheExit = Artisan::call('view:cache'); $viewCacheOutput = trim((string) Artisan::output()); $fullOutput[] = 'NODE REFRESH: php artisan view:cache'; $fullOutput[] = $viewCacheOutput; if ($viewCacheExit !== 0) { $this->error('view:cache fallita.'); $this->line($viewCacheOutput); $this->writeProgress(100, 'view:cache fallita', 'failed', $viewCacheExit); return self::FAILURE; } } $this->writeProgress(91, 'Rigenerazione cache applicative finali', 'running'); foreach ([ 'config:cache' => 'NODE REFRESH: php artisan config:cache', 'event:cache' => 'NODE REFRESH: php artisan event:cache', 'queue:restart' => 'NODE REFRESH: php artisan queue:restart', ] as $artisanCommand => $label) { $exit = Artisan::call($artisanCommand); $output = trim((string) Artisan::output()); $fullOutput[] = $label; $fullOutput[] = $output; if ($exit !== 0) { $this->warn($artisanCommand . ' non completato: ' . ($output !== '' ? $output : 'uscita ' . $exit)); } } $this->line(implode(PHP_EOL . PHP_EOL, array_filter($fullOutput, static fn(string $chunk): bool => trim($chunk) !== ''))); $this->writeProgress(98, 'Riapertura nodo e chiusura refresh', 'running'); } finally { if ($maintenanceOn) { $upExit = Artisan::call('up'); $maintenanceUp = trim((string) Artisan::output()); $fullOutput[] = 'NODE REFRESH: php artisan up'; $fullOutput[] = $maintenanceUp; if ($upExit !== 0) { $maintenanceError = $maintenanceUp !== '' ? $maintenanceUp : 'php artisan up fallito'; } $lockPath = storage_path('framework/down'); if (is_file($lockPath)) { @unlink($lockPath); } } } if ($maintenanceError !== null) { $this->error('Refresh completato ma il nodo non e stato riaperto automaticamente.'); $this->line($maintenanceError); $this->writeProgress(100, 'Refresh completato ma nodo ancora in maintenance', 'failed', 1); return self::FAILURE; } $this->writeProgress(100, 'Refresh nodo completato', 'completed', 0); return self::SUCCESS; } /** @return array{success:bool,exit_code:int,output:string} */ private function runShellStep(array $command): array { $result = Process::path(base_path()) ->timeout(3600) ->run($command); return [ 'success' => $result->successful(), 'exit_code' => $result->exitCode(), 'output' => trim((string) ($result->output() !== '' ? $result->output() : $result->errorOutput())), ]; } /** @return array{label:string,output:string}|null */ private function attemptFrontendBuild(): ?array { if (! is_file(base_path('package.json'))) { return null; } $nodeCheck = $this->runShellStep(['bash', '-lc', 'command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1']); if (! $nodeCheck['success']) { return [ 'label' => 'NODE REFRESH: frontend build skipped', 'output' => 'node/npm non disponibili sul nodo: build frontend saltata senza bloccare il deploy.', ]; } $installCommand = is_file(base_path('package-lock.json')) ? ['npm', 'ci', '--no-audit', '--no-fund'] : ['npm', 'install', '--no-audit', '--no-fund']; $installResult = $this->runShellStep($installCommand); if (! $installResult['success']) { return [ 'label' => 'NODE REFRESH: npm install/ci', 'output' => $installResult['output'] !== '' ? $installResult['output'] : 'Installazione dipendenze frontend fallita; build saltata.', ]; } $buildResult = $this->runShellStep(['npm', 'run', 'build']); return [ 'label' => 'NODE REFRESH: npm run build', 'output' => $buildResult['output'] !== '' ? $buildResult['output'] : ($buildResult['success'] ? 'Build frontend completata.' : 'Build frontend fallita.'), ]; } private function writeProgress(int $percent, string $message, string $status, ?int $exitCode = null): void { $path = trim((string) $this->option('progress-file')); if ($path === '') { return; } @file_put_contents($path, json_encode([ 'timestamp' => now()->toIso8601String(), 'percent' => max(0, min(100, $percent)), 'message' => $message, 'status' => $status, 'exit_code' => $exitCode, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); } }