329 lines
13 KiB
PHP
329 lines
13 KiB
PHP
<?php
|
|
namespace App\Console\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
use Illuminate\Support\Facades\Process;
|
|
|
|
class NetgesconNodeRefreshCommand extends Command
|
|
{
|
|
protected $signature = 'netgescon:node-refresh
|
|
{--remote=origin : Remote Git da usare}
|
|
{--branch=main : Branch da allineare}
|
|
{--admin-id= : ID amministratore per eventuale backup Drive}
|
|
{--drive : Carica il backup pre-update anche su Google Drive}
|
|
{--force : Continua anche con working tree sporco lato sync}
|
|
{--require-drive : Fallisce se il backup Drive non viene completato}
|
|
{--skip-backup : Salta backup pre-update (solo test/debug)}
|
|
{--skip-migrate : Salta php artisan migrate --force}
|
|
{--skip-maintenance : Salta php artisan down/up (solo test/debug)}
|
|
{--skip-view-cache : Salta rebuild view cache}
|
|
{--progress-file= : File JSON dove scrivere lo stato avanzamento}';
|
|
|
|
protected $description = 'Aggiorna il nodo da Gitea e applica le manutenzioni post-sync senza usare il terminale';
|
|
|
|
public function handle(): int
|
|
{
|
|
$remote = trim((string) $this->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));
|
|
}
|
|
}
|