Files
KantSites/src/Command/UpdateCommand.php
T
kantarciandClaude Opus 5 52f0ab3807
CMS quality / php (8.2) (push) Canceled after 0s
CMS quality / php (8.4) (push) Canceled after 0s
CMS quality / browser (push) Canceled after 0s
CMS quality / mysql (push) Canceled after 0s
Updater: neue Fassung über Git, mit Vorprüfung und Rückweg
Aus dem Plan in docs/UPDATE.md ist Code geworden: cms:update, cms:health, der
Bildschirm "System" und der Wartungszustand.

Die Vorprüfung ist die halbe Arbeit. Sie fragt nach proc_open, Git, sauberer
Arbeitskopie, Herkunft, Schreibrechten und Platz - und danach, ob Datenbank
und Medienordner wirklich von Git ausgenommen sind. Das ist der Punkt, an dem
ein Update Inhalte zerstören könnte, also wird er geprüft und nicht
angenommen. Jede Prüfung sagt, was zu tun ist, nicht bloß "fehlgeschlagen".

Der Rückweg ist Teil des Wegs: scheitert ein Schritt, wird auf den vorherigen
Commit zurückgesetzt. Die Datenbank nur dann zurückgeschrieben, wenn die
Migration schon begonnen hatte - sonst verlöre der Rückweg Inhalte, die
während des Updates entstanden sind. Antwortet der alte Stand im Rauchtest,
geht die Website wieder online; antwortet er nicht, bleibt der Wartungszustand,
weil eine kaputte Seite schlechter ist als eine ehrliche Meldung.

Der Rauchtest läuft als eigener Prozess, denn nur ein frischer Prozess führt
den neuen Code aus. Ein Test im laufenden Prozess hätte den alten geprüft und
"alles in Ordnung" gemeldet.

Kein symfony/process: der Runner sind dreißig Zeilen über proc_open mit dem
Befehl als Array - kein Shell, nichts zu maskieren, und "proc_open ist
abgeschaltet" wird die erste Antwort statt der Überraschung im dritten
Schritt.

285 PHP-Tests / 1244 Assertions. Sechs davon fahren den Updater gegen echte
Git-Repositorys in einem Temp-Verzeichnis, mit Platzhalterskripten für die
zwei Befehle - inklusive des Fehlschlags, nach dem Code und Datenbank
zurückgesetzt sein müssen. Nicht gegen dieses Projekt: ein Test, der die
Arbeitskopie auscheckt, in der er läuft, ist ein Weg, Arbeit zu verlieren.

Offen bleibt die Sicherung einer MySQL-Datenbank per mysqldump - dafür fehlt
die Installation; der Updater sagt das im Schritt "Sicherung".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 20:08:48 +02:00

90 lines
3.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Command;
use App\Ops\Updater;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* The way that always works, also without a web interface - and the one to reach for when the
* administration itself is what is broken.
*/
#[AsCommand(name: 'cms:update', description: 'Pulls a new version from the Git repository: preflight, backup, maintenance, checkout, migration, smoke test.')]
final class UpdateCommand extends Command
{
public function __construct(private Updater $updater)
{
parent::__construct();
}
protected function configure(): void
{
$this->addOption('check', null, InputOption::VALUE_NONE, 'Nur die Vorprüfung und die verfügbare Fassung zeigen.')
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Alles prüfen, aber nichts verändern.')
->addOption('to', null, InputOption::VALUE_REQUIRED, 'Ein bestimmter Tag, zum Beispiel v1.2.0.')
->addOption('maintenance', null, InputOption::VALUE_REQUIRED, 'Wartungszustand von Hand setzen: an oder aus.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$switch = $input->getOption('maintenance');
if (null !== $switch) {
if (!in_array($switch, ['an', 'aus'], true)) {
$io->error('--maintenance erwartet „an“ oder „aus“.');
return Command::INVALID;
}
$this->updater->maintenance('an' === $switch);
$io->success(sprintf('Wartungszustand ist %s.', 'an' === $switch ? 'eingeschaltet – die Website antwortet mit 503' : 'aufgehoben'));
return Command::SUCCESS;
}
$state = $this->updater->state();
$io->definitionList(
['Fassung' => $state['version']],
['Stand' => $state['describe'].' ('.$state['commit'].')'],
['Wartung' => $state['maintenance'] ? 'eingeschaltet' : 'aus'],
);
$checks = $this->updater->check();
$io->table(['Prüfung', '', 'Befund'], array_map(
static fn (array $check): array => [$check['name'], $check['ok'] ? 'ok' : 'FEHLT', $check['detail']],
$checks,
));
$failed = array_values(array_filter($checks, static fn (array $check): bool => !$check['ok']));
if ($input->getOption('check')) {
$available = $this->updater->availableVersion();
$io->writeln(sprintf('Verfügbar: %s', $available ?? 'kein Tag der Form v1.2.3 gefunden'));
return [] === $failed ? Command::SUCCESS : Command::FAILURE;
}
if ([] !== $failed) {
$io->error('Die Vorprüfung ist nicht bestanden; es wird nichts verändert.');
return Command::FAILURE;
}
$target = $input->getOption('to');
$result = $this->updater->update(is_string($target) ? $target : null, (bool) $input->getOption('dry-run'));
foreach ($result['steps'] as $step) {
$io->writeln(sprintf('%s %s: %s', $step['ok'] ? ' ok ' : ' FEHLER', $step['step'], $step['detail']));
}
if ($result['ok']) {
$io->success(sprintf('Update auf %s abgeschlossen.', $result['target'] ?? 'die aktuelle Fassung'));
return Command::SUCCESS;
}
$io->error('Das Update ist gescheitert. Die Schritte oben sagen, wo.');
return Command::FAILURE;
}
}