Versioning, déploiement et mise à jour automatique
Gestion des versions :
- Fichier VERSION (1.0.0) comme source de vérité
- config/update.php : URL Gitea, AUTO_UPDATE (false par défaut), rétention des sauvegardes
Artisan commands :
- app:check-update : interroge l'API Gitea, cache Redis 1h, déclenche app:update si AUTO_UPDATE=true
- app:update : télécharge l'archive, sauvegarde pg_dump, rsync, composer install, migrate, reload php-fpm
- app:rollback : liste les sauvegardes et restaure via psql
UpdateService :
- Téléchargement via Http::sink() (streaming, pas de charge mémoire)
- Sauvegarde pg_dump dans storage/app/backups/ avant chaque mise à jour
- Rechargement php-fpm gracieux (kill -USR2 1) sans downtime
- Purge automatique des anciennes sauvegardes (configurable)
Docker (refactor pour volume-mount) :
- Dockerfile : runtime seulement (PHP + extensions + composer + rsync + pg_client)
Le code n'est plus copié dans l'image → les mises à jour ne nécessitent pas de rebuild
- entrypoint.sh : composer install + key:generate + caches au démarrage du container
- docker-compose.prod.yml : montage du code comme volume (.:/var/www/html)
Scripts de déploiement :
- bin/build-release.sh : rsync + tar.gz + sha256, exclut vendor/node_modules/tests
- install.sh : guide d'installation Docker complète (première mise en service)
Interface admin :
- Bandeau "mise à jour disponible" dans le dashboard admin (version courante + cible)
- Badge version + icône "à jour" en pied de tableau de bord
- Commande à copier-coller pour appliquer depuis le container
Planification :
- routes/console.php : Schedule::command('app:check-update')->daily()
- .env.example : variables GITEA_*, AUTO_UPDATE, UPDATE_BACKUPS_TO_KEEP
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\UpdateService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ApplyUpdateCommand extends Command
|
||||
{
|
||||
protected $signature = 'app:update
|
||||
{--force : Pas de confirmation interactive}
|
||||
{--check : Vérifie uniquement, sans appliquer}';
|
||||
|
||||
protected $description = 'Télécharge et applique la dernière mise à jour disponible';
|
||||
|
||||
public function handle(UpdateService $updates): int
|
||||
{
|
||||
$installed = $updates->getInstalledVersion();
|
||||
$this->line("Version installée : <info>v{$installed}</info>");
|
||||
|
||||
$this->line("Vérification des mises à jour...");
|
||||
$latest = $updates->fetchLatestRelease(useCache: false);
|
||||
|
||||
if (! $latest) {
|
||||
$this->error("Impossible de contacter le serveur de mises à jour.");
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if (! version_compare($latest['version'], $installed, '>')) {
|
||||
$this->info("L'application est déjà à jour (v{$installed}).");
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("Mise à jour disponible : v{$installed} → <comment>v{$latest['version']}</comment>");
|
||||
|
||||
if ($latest['body']) {
|
||||
$this->line($latest['body']);
|
||||
}
|
||||
|
||||
if ($this->option('check')) {
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
if (! $this->option('force') && ! $this->confirm('Procéder à la mise à jour ?', true)) {
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->line("");
|
||||
|
||||
try {
|
||||
$updates->applyUpdate($latest, fn ($msg) => $this->line($msg));
|
||||
} catch (\Exception $e) {
|
||||
$this->error("La mise à jour a échoué : " . $e->getMessage());
|
||||
$this->warn("Vérifiez les sauvegardes dans storage/app/backups/ pour une restauration manuelle.");
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\UpdateService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CheckUpdateCommand extends Command
|
||||
{
|
||||
protected $signature = 'app:check-update {--force : Ignore le cache et interroge Gitea directement}';
|
||||
protected $description = 'Vérifie si une nouvelle version de MesRelevés est disponible';
|
||||
|
||||
public function handle(UpdateService $updates): int
|
||||
{
|
||||
$installed = $updates->getInstalledVersion();
|
||||
$this->line("Version installée : <info>v{$installed}</info>");
|
||||
|
||||
$this->line("Interrogation du serveur de mises à jour...");
|
||||
$latest = $updates->fetchLatestRelease(useCache: ! $this->option('force'));
|
||||
|
||||
if (! $latest) {
|
||||
$this->warn("Impossible de contacter le serveur de mises à jour.");
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if (version_compare($latest['version'], $installed, '>')) {
|
||||
$this->info("Nouvelle version disponible : v{$latest['version']}");
|
||||
if ($latest['body']) {
|
||||
$this->line("<comment>Notes de version :</comment>");
|
||||
$this->line($latest['body']);
|
||||
}
|
||||
$this->line("");
|
||||
$this->line("Pour mettre à jour : <comment>php artisan app:update</comment>");
|
||||
|
||||
if (config('update.auto_update')) {
|
||||
$this->line("AUTO_UPDATE=true détecté — application automatique...");
|
||||
$this->call('app:update', ['--force' => true]);
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("L'application est à jour (v{$installed}).");
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\UpdateService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RollbackCommand extends Command
|
||||
{
|
||||
protected $signature = 'app:rollback
|
||||
{--list : Liste les sauvegardes disponibles}';
|
||||
|
||||
protected $description = 'Restaure la base de données depuis une sauvegarde pré-mise à jour';
|
||||
|
||||
public function handle(UpdateService $updates): int
|
||||
{
|
||||
$backups = $updates->listBackups();
|
||||
|
||||
if (empty($backups)) {
|
||||
$this->warn("Aucune sauvegarde disponible dans storage/app/backups/.");
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
// --list : afficher la liste et sortir
|
||||
if ($this->option('list')) {
|
||||
$rows = array_map(fn ($b) => [
|
||||
basename($b),
|
||||
$this->humanSize(filesize($b)),
|
||||
date('d/m/Y H:i:s', filemtime($b)),
|
||||
], $backups);
|
||||
$this->table(['Fichier', 'Taille', 'Date'], $rows);
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
// Sélection interactive
|
||||
$choices = array_map('basename', $backups);
|
||||
$choice = $this->choice('Choisir la sauvegarde à restaurer :', $choices, 0);
|
||||
$path = storage_path("app/backups/{$choice}");
|
||||
|
||||
$this->warn("Sauvegarde sélectionnée : {$choice}");
|
||||
$this->warn("ATTENTION : cette opération remplace la base de données actuelle.");
|
||||
|
||||
if (! $this->confirm('Continuer ?')) {
|
||||
$this->line("Annulé.");
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
try {
|
||||
$updates->restoreBackup($path);
|
||||
$this->info("Base de données restaurée avec succès.");
|
||||
} catch (\Exception $e) {
|
||||
$this->error("Restauration échouée : " . $e->getMessage());
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function humanSize(int $bytes): string
|
||||
{
|
||||
if ($bytes >= 1_048_576) return round($bytes / 1_048_576, 1) . ' Mo';
|
||||
if ($bytes >= 1_024) return round($bytes / 1_024, 0) . ' Ko';
|
||||
return $bytes . ' o';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user