Bilder in vier Breiten, Releasearchiv fuer Hosting ohne Composer
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

Uploads werden weiterhin nach WebP umgewandelt, jetzt in vier statt drei
Breiten (480/960/1600/2400) und ohne Hochskalieren. Hintergrundbilder
waehlen ihre Variante je Geraet, weil CSS kein srcset kennt.

Dabei kam ein Fehler heraus, den erst der Browsertest zeigte: der
Dokumentpruefer erlaubte drei sources je Bild, also war mit der vierten
Variante jedes Dokument mit einem Bild ungueltig - ohne sichtbare Meldung.
Die Grenze haengt jetzt an MediaLibrary::WIDTHS.

Geteiltes Hosting hat meistens keinen Composer. scripts/build-release.php
baut deshalb auf einem Build-Rechner ein Archiv mit fertigem vendor/ ohne
Entwicklungspakete, samt SHA-256. Der Updater bricht ab, wenn ein Update
composer.lock aendert und Composer auf dem Server nicht aufrufbar ist,
statt mit einem unpassenden vendor/ weiterzulaufen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-12 22:39:08 +02:00
1 parent c046681302
commit c3bff7d936
17 files changed
+414 -26

No files matched your search

+37 -2
View File
@@ -12,6 +12,9 @@ namespace App\Ops;
*/
final class CommandRunner
{
/** @var array<string, list<string>|null> */
private array $resolved = [];
public function available(): bool
{
if (!function_exists('proc_open')) {
@@ -79,9 +82,41 @@ final class CommandRunner
return ['ok' => 0 === $exit, 'exit' => $exit, 'output' => trim($output)];
}
/** Whether an executable can be found at all, so a missing one is reported, not guessed at. */
/**
* Whether an executable can be found at all, so a missing one is reported, not guessed at.
*
* On Windows several of these are batch files, and proc_open without a shell cannot start a
* .bat - which reported "composer is not available" on a machine that has it. The variants are
* tried in order, and the one that answers is remembered for the next call.
*/
public function has(string $executable, string $cwd): bool
{
return $this->run([$executable, '--version'], $cwd, 30)['ok'];
return null !== $this->resolve($executable, $cwd);
}
/** The spelling of a command that actually starts here, or null. */
public function resolve(string $executable, string $cwd): ?array
{
if (array_key_exists($executable, $this->resolved)) {
return $this->resolved[$executable];
}
$candidates = [[$executable]];
if ('\\' === DIRECTORY_SEPARATOR) {
// On Windows composer is a .bat, and CreateProcess cannot start a batch file on its
// own - it needs the command interpreter. The argument list stays fixed, nothing from
// a request ever reaches it.
$candidates[] = [getenv('COMSPEC') ?: 'cmd.exe', '/c', $executable];
}
// A phar in the project is the way on hosting where nothing is installed globally.
if (is_file($cwd.'/'.$executable.'.phar')) {
$candidates[] = [PHP_BINARY, $cwd.'/'.$executable.'.phar'];
}
foreach ($candidates as $candidate) {
if ($this->run([...$candidate, '--version'], $cwd, 30)['ok']) {
return $this->resolved[$executable] = $candidate;
}
}
return $this->resolved[$executable] = null;
}
}
+24 -5
View File
@@ -195,6 +195,9 @@ final class Updater
return ['ok' => false, 'steps' => $steps, 'target' => null];
}
$from = $this->git(['rev-parse', 'HEAD'])['output'];
// Remembered before the checkout: if the dependencies change and Composer is missing,
// this is what tells us - vendor/ is outside Git and would silently stay old.
$lockBefore = $this->lockHash();
$step($steps, 'Ziel bestimmen', true, sprintf('%s (von %s, Fassung %s)', $target, substr($from, 0, 8), $this->version()));
if ($target === 'v'.$this->version()) {
@@ -229,13 +232,20 @@ final class Updater
throw new \RuntimeException('version');
}
if ($this->runner->has('composer', $this->root)) {
$install = $this->runner->run(['composer', 'install', '--no-dev', '--optimize-autoloader', '--no-interaction'], $this->root, 600);
$composer = $this->runner->resolve('composer', $this->root);
if (null !== $composer) {
$install = $this->runner->run([...$composer, 'install', '--no-dev', '--optimize-autoloader', '--no-interaction'], $this->root, 600);
if (!$step($steps, 'Abhängigkeiten', $install['ok'], $install['ok'] ? 'composer install ausgeführt.' : $install['output'])) {
throw new \RuntimeException('composer');
}
} elseif ($lockBefore !== $this->lockHash()) {
// Without Composer the dependencies cannot be brought up to date, and this update
// needs different ones. Going on would leave an installation whose code and
// vendor/ do not match - which fails somewhere else, later, for no visible reason.
$step($steps, 'Abhängigkeiten', false, 'Dieses Update ändert die Abhängigkeiten (composer.lock), und auf diesem Server ist Composer nicht aufrufbar. Spiele stattdessen das Releasearchiv ein, das vendor/ mitbringt.');
throw new \RuntimeException('lock');
} else {
$step($steps, 'Abhängigkeiten', true, 'Composer ist nicht vorhanden; vendor/ kommt aus dem Tag.');
$step($steps, 'Abhängigkeiten', true, 'Composer ist nicht vorhanden; composer.lock ist unverändert, vendor/ bleibt gültig.');
}
$migrated = true;
@@ -312,8 +322,9 @@ final class Updater
: 'Die Sicherung ließ sich nicht zurückschreiben. Die Datenbank liegt unter '.dirname((string) $backup).'.');
}
if ($this->runner->has('composer', $this->root)) {
$this->runner->run(['composer', 'install', '--no-dev', '--optimize-autoloader', '--no-interaction'], $this->root, 600);
$composer = $this->runner->resolve('composer', $this->root);
if (null !== $composer) {
$this->runner->run([...$composer, 'install', '--no-dev', '--optimize-autoloader', '--no-interaction'], $this->root, 600);
}
$this->console(['bin/console', 'cache:clear', '--env=prod'], 300);
$health = $this->console(['bin/console', 'cms:health', '--env=prod'], 120);
@@ -352,6 +363,14 @@ final class Updater
);
}
/** The checksum of composer.lock, or an empty string when there is none. */
private function lockHash(): string
{
$file = $this->root.'/composer.lock';
return is_readable($file) ? (string) hash_file('sha256', $file) : '';
}
/** @return array{ok: bool, exit: int, output: string} */
private function git(array $arguments, int $timeout = 60): array
{