From 6f08c766725d5b2b086744e8f52bb8dcb71eabf4 Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Thu, 19 Feb 2026 09:40:52 +0100 Subject: [PATCH 1/8] feat: align requirements:check with TYPO3 v13 system requirements - Update DB minimums (MariaDB 10.4.3, MySQL 8.0.17) - Add PHP minimum version validation (TYPO3: >= 8.2.0) - Update PHP extensions (remove json/soap, add session/tokenizer/filter) - Add pcre.jit setting, extract detectDatabaseProduct() helper --- deployer/requirements/config/set.php | 58 ++++-- deployer/requirements/functions.php | 181 ++++++++++++++++++ deployer/requirements/task/check_database.php | 57 ++---- .../requirements/task/check_php_settings.php | 7 +- 4 files changed, 240 insertions(+), 63 deletions(-) diff --git a/deployer/requirements/config/set.php b/deployer/requirements/config/set.php index 7c18fdc..e113154 100644 --- a/deployer/requirements/config/set.php +++ b/deployer/requirements/config/set.php @@ -10,11 +10,13 @@ // Enable/disable per check category set('requirements_check_locales_enabled', true); set('requirements_check_packages_enabled', true); +set('requirements_check_image_processing_enabled', true); set('requirements_check_php_extensions_enabled', true); set('requirements_check_php_settings_enabled', true); set('requirements_check_database_enabled', true); set('requirements_check_user_enabled', true); set('requirements_check_env_enabled', true); +set('requirements_check_eol_enabled', true); // Locales set('requirements_locales', ['de_DE.utf8', 'en_US.utf8']); @@ -23,7 +25,6 @@ set('requirements_packages', [ 'rsync' => 'rsync', 'curl' => 'curl', - 'graphicsmagick' => 'gm', 'ghostscript' => 'gs', 'git' => 'git', 'gzip' => 'gzip', @@ -34,35 +35,48 @@ 'composer' => 'composer', ]); +// PHP minimum version (framework-aware) +set('requirements_php_min_version', function (): string { + if (has('app_type') && get('app_type') === 'typo3') { + return '8.2.0'; + } + + return '8.1.0'; +}); + // PHP extensions (framework-aware) set('requirements_php_extensions', function (): array { if (has('app_type') && get('app_type') === 'typo3') { return [ - 'curl', - 'gd', - 'mbstring', - 'soap', + 'pdo', + 'session', 'xml', - 'zip', + 'filter', + 'tokenizer', + 'mbstring', 'intl', - 'apcu', - 'pdo', 'pdo_mysql', - 'json', 'fileinfo', + 'gd', + 'zip', 'openssl', + 'curl', + 'apcu', ]; } return [ - 'curl', - 'gd', - 'mbstring', + 'pdo', + 'session', 'xml', - 'zip', + 'filter', + 'tokenizer', + 'mbstring', 'intl', - 'pdo', 'pdo_mysql', + 'curl', + 'gd', + 'zip', ]; }); @@ -71,6 +85,7 @@ 'max_execution_time' => '240', 'memory_limit' => '512M', 'max_input_vars' => '1500', + 'pcre.jit' => '1', 'date.timezone' => 'Europe/Berlin', 'post_max_size' => '31M', 'upload_max_filesize' => '30M', @@ -78,8 +93,19 @@ ]); // Database -set('requirements_mariadb_min_version', '10.2.7'); -set('requirements_mysql_min_version', '8.0.0'); +set('requirements_mariadb_min_version', '10.4.3'); +set('requirements_mysql_min_version', '8.0.17'); + +// Image processing +set('requirements_graphicsmagick_min_version', '1.3'); +set('requirements_imagemagick_min_version', '6.0'); + +// Composer +set('requirements_composer_min_version', '2.1.0'); + +// End-of-life check (endoflife.date API) +set('requirements_eol_warn_months', 6); +set('requirements_eol_api_timeout', 5); // User / permissions set('requirements_user_group', 'www-data'); diff --git a/deployer/requirements/functions.php b/deployer/requirements/functions.php index 56f97ba..146352c 100644 --- a/deployer/requirements/functions.php +++ b/deployer/requirements/functions.php @@ -4,6 +4,7 @@ namespace Deployer; +use Deployer\Exception\RunException; use Symfony\Component\Console\Helper\Table; const REQUIREMENT_OK = 'OK'; @@ -29,6 +30,7 @@ 'max_execution_time', 'max_input_vars', 'opcache.memory_consumption', + 'pcre.jit', // Treated as numeric (>= 1) to ensure enabled ]; function addRequirementRow(string $check, string $status, string $info = ''): void @@ -140,6 +142,185 @@ function meetsPhpRequirement(string $actual, string $expected, string $setting): return $actual === $expected; } +/** + * Try to detect the version of a CLI tool on the remote server. + */ +function detectPackageVersion(string $command): ?string +{ + $versionCmd = match ($command) { + 'exiftool' => 'exiftool -ver 2>&1 | head -1', + default => "$command --version 2>&1 | head -1", + }; + + try { + $output = trim(run($versionCmd)); + + if ($output !== '' && preg_match('/(\d+[\d.]+)/', $output, $matches)) { + return $matches[1]; + } + } catch (RunException) { + // Command doesn't support version flag + } + + return null; +} + +/** + * Detect the database product and version from the remote server. + * + * @return array{product: string, label: string, version: string, cycle: string}|null + */ +function detectDatabaseProduct(): ?array +{ + foreach (['mariadb', 'mysql'] as $command) { + try { + $versionOutput = trim(run("$command --version 2>/dev/null")); + + if ($versionOutput === '') { + continue; + } + + if (preg_match('/Distrib\s+((\d+\.\d+)[\d.]*)/', $versionOutput, $matches) + || preg_match('/((\d+\.\d+)[\d.]*)-MariaDB/', $versionOutput, $matches) + ) { + return ['product' => 'mariadb', 'label' => 'MariaDB', 'version' => $matches[1], 'cycle' => $matches[2]]; + } + + if (preg_match('/Ver\s+((\d+\.\d+)[\d.]*)/', $versionOutput, $matches)) { + return ['product' => 'mysql', 'label' => 'MySQL', 'version' => $matches[1], 'cycle' => $matches[2]]; + } + } catch (RunException) { + continue; + } + } + + return null; +} + +/** + * Fetch release cycles from endoflife.date API. + * + * @return list|null + */ +function fetchEolCycles(string $product, int $timeout = 5): ?array +{ + $url = sprintf('https://endoflife.date/api/v1/products/%s/', urlencode($product)); + + $context = stream_context_create([ + 'http' => [ + 'timeout' => $timeout, + 'header' => "Accept: application/json\r\nUser-Agent: move-elevator/deployer-tools\r\n", + ], + ]); + + $response = @file_get_contents($url, false, $context); + + if ($response === false) { + return null; + } + + $data = json_decode($response, true); + + if (!is_array($data) || !isset($data['result']['releases'])) { + return null; + } + + return $data['result']['releases']; +} + +/** + * Find the matching release cycle for a major.minor version. + * + * @param list $cycles + * @return array{name: string, isEol: bool, eolFrom: ?string, isEoas: ?bool, eoasFrom: ?string, isMaintained: bool}|null + */ +function findEolCycle(array $cycles, string $majorMinor): ?array +{ + foreach ($cycles as $cycle) { + if ($cycle['name'] === $majorMinor) { + return $cycle; + } + } + + return null; +} + +/** + * Evaluate EOL status and add a requirement row. + */ +function evaluateEolStatus(string $label, array $cycle, int $warnMonths): void +{ + $now = new \DateTimeImmutable(); + + if ($cycle['isEol'] ?? false) { + $eolDate = $cycle['eolFrom'] ?? 'unknown'; + addRequirementRow("EOL: $label", REQUIREMENT_FAIL, "End of Life since $eolDate"); + + return; + } + + $eolFrom = $cycle['eolFrom'] ?? null; + + if ($eolFrom !== null) { + try { + $eolDate = new \DateTimeImmutable($eolFrom); + } catch (\Exception) { + addRequirementRow("EOL: $label", REQUIREMENT_SKIP, "Invalid EOL date from API: $eolFrom"); + + return; + } + + $warnDate = $eolDate->modify("-{$warnMonths} months"); + + if ($now >= $warnDate) { + $interval = $now->diff($eolDate); + $months = $interval->y * 12 + $interval->m; + $remaining = $months > 0 ? "in $months month(s)" : 'imminent'; + addRequirementRow("EOL: $label", REQUIREMENT_WARN, "EOL $remaining ($eolFrom)"); + + return; + } + } + + $isEoas = $cycle['isEoas'] ?? false; + + if ($isEoas) { + $info = 'Security support only'; + $info .= $eolFrom !== null ? ", EOL $eolFrom" : ''; + addRequirementRow("EOL: $label", REQUIREMENT_WARN, $info); + + return; + } + + $info = 'Maintained'; + $info .= $eolFrom !== null ? " until $eolFrom" : ''; + addRequirementRow("EOL: $label", REQUIREMENT_OK, $info); +} + +/** + * Check a single product against the endoflife.date API. + */ +function checkEolForProduct(string $label, string $product, string $cycle, int $warnMonths, int $timeout): void +{ + $cycles = fetchEolCycles($product, $timeout); + + if ($cycles === null) { + addRequirementRow("EOL: $label", REQUIREMENT_SKIP, 'Could not reach endoflife.date API'); + + return; + } + + $match = findEolCycle($cycles, $cycle); + + if ($match === null) { + addRequirementRow("EOL: $label", REQUIREMENT_SKIP, "Cycle $cycle not found in API"); + + return; + } + + evaluateEolStatus("$label $cycle", $match, $warnMonths); +} + /** * @return array */ diff --git a/deployer/requirements/task/check_database.php b/deployer/requirements/task/check_database.php index 1b81d1f..e2faaa3 100644 --- a/deployer/requirements/task/check_database.php +++ b/deployer/requirements/task/check_database.php @@ -4,60 +4,27 @@ namespace Deployer; -use Deployer\Exception\RunException; - task('requirements:check:database', function (): void { if (!get('requirements_check_database_enabled')) { return; } - // Try mariadb command first (newer MariaDB installations), then fall back to mysql - $versionOutput = ''; - $clientFound = false; - - foreach (['mariadb', 'mysql'] as $command) { - try { - $versionOutput = trim(run("$command --version 2>/dev/null")); - - if ($versionOutput !== '') { - $clientFound = true; - - break; - } - } catch (RunException) { - continue; - } - } + $db = detectDatabaseProduct(); - if (!$clientFound) { + if ($db === null) { addRequirementRow('Database client', REQUIREMENT_SKIP, 'Neither mariadb nor mysql command available'); return; } - if (preg_match('/Distrib\s+([\d.]+)/', $versionOutput, $matches) - || preg_match('/([\d.]+)-MariaDB/', $versionOutput, $matches) - ) { - $actualVersion = $matches[1]; - $minVersion = get('requirements_mariadb_min_version'); - $meets = version_compare($actualVersion, $minVersion, '>='); - $info = $meets ? "MariaDB $actualVersion" : "MariaDB $actualVersion (required: >= $minVersion)"; - addRequirementRow( - 'Database client', - $meets ? REQUIREMENT_OK : REQUIREMENT_FAIL, - $info - ); - } elseif (preg_match('/Ver\s+([\d.]+)/', $versionOutput, $matches)) { - $actualVersion = $matches[1]; - $minVersion = get('requirements_mysql_min_version'); - $meets = version_compare($actualVersion, $minVersion, '>='); - $info = $meets ? "MySQL $actualVersion" : "MySQL $actualVersion (required: >= $minVersion)"; - addRequirementRow( - 'Database client', - $meets ? REQUIREMENT_OK : REQUIREMENT_FAIL, - $info - ); - } else { - addRequirementRow('Database client', REQUIREMENT_WARN, "Could not parse version: $versionOutput"); - } + $minVersion = $db['product'] === 'mariadb' + ? get('requirements_mariadb_min_version') + : get('requirements_mysql_min_version'); + + $meets = version_compare($db['version'], $minVersion, '>='); + $info = $meets + ? "{$db['label']} {$db['version']}" + : "{$db['label']} {$db['version']} (required: >= $minVersion)"; + + addRequirementRow('Database client', $meets ? REQUIREMENT_OK : REQUIREMENT_FAIL, $info); })->hidden(); diff --git a/deployer/requirements/task/check_php_settings.php b/deployer/requirements/task/check_php_settings.php index 9a9dd40..0d3e80f 100644 --- a/deployer/requirements/task/check_php_settings.php +++ b/deployer/requirements/task/check_php_settings.php @@ -11,10 +11,13 @@ return; } - // Retrieve PHP version + // Retrieve and validate PHP version try { $phpVersion = trim(run('php -r "echo PHP_VERSION;" 2>/dev/null')); - addRequirementRow('PHP version', REQUIREMENT_OK, $phpVersion); + $minVersion = get('requirements_php_min_version'); + $meets = version_compare($phpVersion, $minVersion, '>='); + $info = $meets ? $phpVersion : "$phpVersion (required: >= $minVersion)"; + addRequirementRow('PHP version', $meets ? REQUIREMENT_OK : REQUIREMENT_FAIL, $info); } catch (RunException) { addRequirementRow('PHP version', REQUIREMENT_SKIP, 'Could not determine PHP version'); } From aee681631591e87fc29b659e3f13a6582da72e53 Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Thu, 19 Feb 2026 09:41:01 +0100 Subject: [PATCH 2/8] feat: add image processing check and package version display --- .../task/check_image_processing.php | 60 +++++++++++++++++++ deployer/requirements/task/check_packages.php | 36 ++++++++++- 2 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 deployer/requirements/task/check_image_processing.php diff --git a/deployer/requirements/task/check_image_processing.php b/deployer/requirements/task/check_image_processing.php new file mode 100644 index 0000000..eaaddf8 --- /dev/null +++ b/deployer/requirements/task/check_image_processing.php @@ -0,0 +1,60 @@ +/dev/null | head -1')); + + if ($gmOutput !== '' && preg_match('/GraphicsMagick\s+([\d.]+)/', $gmOutput, $matches)) { + $actualVersion = $matches[1]; + $meets = version_compare($actualVersion, $gmMinVersion, '>='); + $info = $meets + ? "GraphicsMagick $actualVersion" + : "GraphicsMagick $actualVersion (required: >= $gmMinVersion)"; + addRequirementRow('Image processing', $meets ? REQUIREMENT_OK : REQUIREMENT_FAIL, $info); + + return; + } + } catch (RunException) { + // GraphicsMagick not available, try ImageMagick + } + + // Fallback to ImageMagick (magick for IM7+, convert for legacy) + foreach (['magick', 'convert'] as $imCommand) { + try { + $imOutput = trim(run("$imCommand -version 2>/dev/null | head -1")); + + if ($imOutput !== '' && preg_match('/ImageMagick\s+([\d.]+)/', $imOutput, $matches)) { + $actualVersion = $matches[1]; + $meets = version_compare($actualVersion, $imMinVersion, '>='); + $info = $meets + ? "ImageMagick $actualVersion" + : "ImageMagick $actualVersion (required: >= $imMinVersion)"; + addRequirementRow('Image processing', $meets ? REQUIREMENT_OK : REQUIREMENT_FAIL, $info); + + return; + } + } catch (RunException) { + continue; + } + } + + addRequirementRow( + 'Image processing', + REQUIREMENT_FAIL, + "Neither GraphicsMagick (>= $gmMinVersion) nor ImageMagick (>= $imMinVersion) found" + ); +})->hidden(); diff --git a/deployer/requirements/task/check_packages.php b/deployer/requirements/task/check_packages.php index 0a1c2d0..d1160ba 100644 --- a/deployer/requirements/task/check_packages.php +++ b/deployer/requirements/task/check_packages.php @@ -24,10 +24,40 @@ continue; } - if ($found) { - addRequirementRow("Package: $displayName", REQUIREMENT_OK, 'Installed'); - } else { + if (!$found) { addRequirementRow("Package: $displayName", REQUIREMENT_FAIL, "Command '$command' not found"); + + continue; + } + + // Composer has version validation against a minimum + if ($command === 'composer') { + try { + $versionOutput = trim(run('composer --version 2>/dev/null')); + $minVersion = get('requirements_composer_min_version'); + + if (preg_match('/Composer\s+(?:version\s+)?([\d.]+)/', $versionOutput, $matches)) { + $actualVersion = $matches[1]; + $meets = version_compare($actualVersion, $minVersion, '>='); + $info = $meets + ? "Composer $actualVersion" + : "Composer $actualVersion (required: >= $minVersion)"; + addRequirementRow("Package: $displayName", $meets ? REQUIREMENT_OK : REQUIREMENT_FAIL, $info); + + continue; + } + } catch (RunException) { + // Fall through to generic version detection + } + } + + // Try to retrieve version for informational display + $version = detectPackageVersion($command); + + if ($version !== null) { + addRequirementRow("Package: $displayName", REQUIREMENT_OK, "$displayName $version"); + } else { + addRequirementRow("Package: $displayName", REQUIREMENT_OK, 'Installed'); } } })->hidden(); From ec93c75828da2a9a3319cdbe5dbf7cf26e24b375 Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Thu, 19 Feb 2026 09:41:10 +0100 Subject: [PATCH 3/8] feat: add EOL check against endoflife.date API --- deployer/requirements/task/check_eol.php | 37 ++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 deployer/requirements/task/check_eol.php diff --git a/deployer/requirements/task/check_eol.php b/deployer/requirements/task/check_eol.php new file mode 100644 index 0000000..b59748a --- /dev/null +++ b/deployer/requirements/task/check_eol.php @@ -0,0 +1,37 @@ +/dev/null')); + } catch (RunException) { + $phpCycle = ''; + } + + if ($phpCycle !== '') { + checkEolForProduct('PHP', 'php', $phpCycle, $warnMonths, $timeout); + } + + // Check database EOL + $db = detectDatabaseProduct(); + + if ($db !== null) { + checkEolForProduct($db['label'], $db['product'], $db['cycle'], $warnMonths, $timeout); + } +})->hidden(); From c2ff90eb0dc5626c215836e29f02f73fab6aa9e8 Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Thu, 19 Feb 2026 09:41:19 +0100 Subject: [PATCH 4/8] feat: add requirements:list command for human-readable output --- deployer/requirements/task/list.php | 111 ++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 deployer/requirements/task/list.php diff --git a/deployer/requirements/task/list.php b/deployer/requirements/task/list.php new file mode 100644 index 0000000..d4fef27 --- /dev/null +++ b/deployer/requirements/task/list.php @@ -0,0 +1,111 @@ +Server Requirements ($label)"); + writeln(str_repeat('=', 50)); + + // PHP + if (get('requirements_check_php_settings_enabled') || get('requirements_check_php_extensions_enabled')) { + writeln(''); + writeln('PHP'); + writeln(sprintf(' Version: >= %s', get('requirements_php_min_version'))); + writeln(sprintf(' Extensions: %s', implode(', ', get('requirements_php_extensions')))); + + writeln(' Settings:'); + + foreach (get('requirements_php_settings') as $setting => $value) { + $isComparison = in_array($setting, REQUIREMENT_BYTE_SETTINGS, true) + || in_array($setting, REQUIREMENT_NUMERIC_SETTINGS, true); + $operator = $isComparison ? '>=' : '='; + writeln(sprintf(' %-30s %s %s', $setting, $operator, $value)); + } + } + + // Database + if (get('requirements_check_database_enabled')) { + writeln(''); + writeln('Database'); + writeln(sprintf(' MariaDB: >= %s', get('requirements_mariadb_min_version'))); + writeln(sprintf(' MySQL: >= %s', get('requirements_mysql_min_version'))); + } + + // Image Processing + if (get('requirements_check_image_processing_enabled')) { + writeln(''); + writeln('Image Processing'); + writeln(sprintf( + ' GraphicsMagick >= %s (recommended) or ImageMagick >= %s', + get('requirements_graphicsmagick_min_version'), + get('requirements_imagemagick_min_version') + )); + } + + // System Packages + if (get('requirements_check_packages_enabled')) { + writeln(''); + writeln('System Packages'); + + $packages = get('requirements_packages'); + $packageNames = []; + + foreach ($packages as $displayName => $command) { + $name = is_int($displayName) ? $command : $displayName; + + if ($command === 'composer') { + $name .= sprintf(' (>= %s)', get('requirements_composer_min_version')); + } + + $packageNames[] = $name; + } + + writeln(' ' . implode(', ', $packageNames)); + } + + // Locales + if (get('requirements_check_locales_enabled')) { + writeln(''); + writeln('Locales'); + writeln(' ' . implode(', ', get('requirements_locales'))); + } + + // User & Permissions + if (get('requirements_check_user_enabled')) { + writeln(''); + writeln('User & Permissions'); + writeln(sprintf(' Group: %s', get('requirements_user_group'))); + writeln(sprintf(' Permissions: %s (deploy path)', get('requirements_deploy_path_permissions'))); + } + + // End-of-life checks + if (get('requirements_check_eol_enabled')) { + writeln(''); + writeln('End-of-Life Checks'); + writeln(' PHP and database versions checked against endoflife.date API'); + writeln(sprintf(' Warning threshold: %d months before EOL', (int) get('requirements_eol_warn_months'))); + } + + // Environment Variables + if (get('requirements_check_env_enabled')) { + $envVars = get('requirements_env_vars'); + + if (!empty($envVars)) { + writeln(''); + writeln('Environment Variables'); + writeln(sprintf(' File: {{deploy_path}}/shared/%s', get('requirements_env_file'))); + + foreach ($envVars as $var) { + writeln(" - $var"); + } + } + } + + writeln(''); +})->desc('List server requirements'); From fd40cb85475788336450f395bbff2ad142820aa0 Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Thu, 19 Feb 2026 09:41:28 +0100 Subject: [PATCH 5/8] chore: wire new tasks into autoload and task chain --- autoload.php | 3 +++ deployer/requirements/task/requirements.php | 2 ++ 2 files changed, 5 insertions(+) diff --git a/autoload.php b/autoload.php index 32d0d0d..b2aa7ba 100644 --- a/autoload.php +++ b/autoload.php @@ -43,11 +43,14 @@ require_once(__DIR__ . '/deployer/requirements/task/requirements.php'); require_once(__DIR__ . '/deployer/requirements/task/check_locales.php'); require_once(__DIR__ . '/deployer/requirements/task/check_packages.php'); +require_once(__DIR__ . '/deployer/requirements/task/check_image_processing.php'); require_once(__DIR__ . '/deployer/requirements/task/check_php_extensions.php'); require_once(__DIR__ . '/deployer/requirements/task/check_php_settings.php'); require_once(__DIR__ . '/deployer/requirements/task/check_database.php'); require_once(__DIR__ . '/deployer/requirements/task/check_user.php'); require_once(__DIR__ . '/deployer/requirements/task/check_env.php'); +require_once(__DIR__ . '/deployer/requirements/task/check_eol.php'); +require_once(__DIR__ . '/deployer/requirements/task/list.php'); /* * dev diff --git a/deployer/requirements/task/requirements.php b/deployer/requirements/task/requirements.php index 24b5598..52eb5d1 100644 --- a/deployer/requirements/task/requirements.php +++ b/deployer/requirements/task/requirements.php @@ -7,11 +7,13 @@ task('requirements:check', [ 'requirements:check:locales', 'requirements:check:packages', + 'requirements:check:image_processing', 'requirements:check:php_extensions', 'requirements:check:php_settings', 'requirements:check:database', 'requirements:check:user', 'requirements:check:env', + 'requirements:check:eol', 'requirements:check:summary', ])->desc('Check server requirements'); From 32d4904f385472a065171fb118f6942b5d397d42 Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Thu, 19 Feb 2026 09:41:36 +0100 Subject: [PATCH 6/8] docs: update requirements documentation --- docs/REQUIREMENTS.md | 55 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index 576d765..835fdc0 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -12,6 +12,12 @@ The default settings can be found within the [set.php](../deployer/requirements/ $ dep requirements:check [host] ``` +To display a human-readable list of all requirements (without running remote checks): + +```bash +$ dep requirements:list [host] +``` + ## Checks ### Locales @@ -20,7 +26,15 @@ Verifies that required system locales are available (default: `de_DE.utf8`, `en_ ### System packages -Checks for required CLI tools: rsync, curl, graphicsmagick, ghostscript, git, gzip, mariadb-client, unzip, patch, exiftool, composer. +Checks for required CLI tools: rsync, curl, ghostscript, git, gzip, mariadb-client, unzip, patch, exiftool, composer (with version validation >= 2.1.0). + +### Image processing + +Checks for GraphicsMagick (>= 1.3, recommended) or ImageMagick (>= 6.0) with version validation. Either one is sufficient. + +### PHP version + +Validates the PHP version against the configured minimum (TYPO3: >= 8.2.0, default: >= 8.1.0). ### PHP extensions @@ -35,6 +49,7 @@ Checks PHP CLI configuration values against expected minimums: | max_execution_time | >= 240 | | memory_limit | >= 512M | | max_input_vars | >= 1500 | +| pcre.jit | >= 1 | | date.timezone | Europe/Berlin | | post_max_size | >= 31M | | upload_max_filesize | >= 30M | @@ -42,7 +57,7 @@ Checks PHP CLI configuration values against expected minimums: ### Database client -Checks for the availability of the `mariadb` or `mysql` client and validates the version against client-specific minimums (MariaDB: >= 10.2.7, MySQL: >= 8.0.0). +Checks for the availability of the `mariadb` or `mysql` client and validates the version against client-specific minimums (MariaDB: >= 10.4.3, MySQL: >= 8.0.17). ### User and permissions @@ -52,6 +67,20 @@ Validates that the SSH user belongs to the expected web server group (default: ` Checks that the `.env` file exists in the shared directory and that all required environment variables are present. The required variables adapt automatically based on `app_type`. +### End-of-life (EOL) + +Checks installed PHP and database (MariaDB/MySQL) versions against the [endoflife.date](https://endoflife.date) API. The API call runs locally from the deployer machine. + +| Condition | Status | Example | +|-----------|--------|---------| +| End of Life | FAIL | `End of Life since 2024-12-31` | +| EOL approaching | WARN | `EOL in 3 month(s) (2026-06-30)` | +| Security support only | WARN | `Security support only, EOL 2027-12-31` | +| Fully maintained | OK | `Maintained until 2028-12-31` | +| API unreachable | SKIP | `Could not reach endoflife.date API` | + +The warning threshold is configurable (default: 6 months before EOL). + ## Configuration All settings use the `requirements_` prefix and can be overridden in the consuming project: @@ -59,9 +88,13 @@ All settings use the `requirements_` prefix and can be overridden in the consumi ```php // Disable specific checks set('requirements_check_database_enabled', false); +set('requirements_check_image_processing_enabled', false); + +// Override PHP minimum version +set('requirements_php_min_version', '8.3.0'); // Override PHP extensions list -set('requirements_php_extensions', ['curl', 'gd', 'mbstring', 'xml', 'intl']); +set('requirements_php_extensions', ['pdo', 'session', 'xml', 'mbstring', 'intl']); // Override PHP settings thresholds set('requirements_php_settings', [ @@ -76,6 +109,17 @@ set('requirements_packages', [ 'composer' => 'composer', ]); +// Override database minimum versions +set('requirements_mariadb_min_version', '10.6.0'); +set('requirements_mysql_min_version', '8.0.30'); + +// Override image processing minimum versions +set('requirements_graphicsmagick_min_version', '1.3.30'); +set('requirements_imagemagick_min_version', '7.0'); + +// Override composer minimum version +set('requirements_composer_min_version', '2.6.0'); + // Override locales set('requirements_locales', ['de_DE.utf8', 'en_US.utf8', 'fr_FR.utf8']); @@ -84,6 +128,11 @@ set('requirements_user_group', 'apache'); // Override required env variables set('requirements_env_vars', ['DATABASE_URL', 'APP_SECRET']); + +// EOL check configuration +set('requirements_check_eol_enabled', true); +set('requirements_eol_warn_months', 6); // Warn X months before EOL +set('requirements_eol_api_timeout', 5); // API timeout in seconds ``` ## Extending with custom checks From 5170e8c501da7e10caeafaace201c33d19dabe35 Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Thu, 19 Feb 2026 10:42:07 +0100 Subject: [PATCH 7/8] fix: address CodeRabbit review findings --- deployer/requirements/functions.php | 16 +++++++++++++--- deployer/requirements/task/check_eol.php | 2 +- .../task/check_image_processing.php | 10 +++++----- deployer/requirements/task/list.php | 19 ++++++++++++------- 4 files changed, 31 insertions(+), 16 deletions(-) diff --git a/deployer/requirements/functions.php b/deployer/requirements/functions.php index 146352c..0b74910 100644 --- a/deployer/requirements/functions.php +++ b/deployer/requirements/functions.php @@ -180,13 +180,16 @@ function detectDatabaseProduct(): ?array continue; } - if (preg_match('/Distrib\s+((\d+\.\d+)[\d.]*)/', $versionOutput, $matches) + if (str_contains($versionOutput, 'MariaDB') && ( + preg_match('/Distrib\s+((\d+\.\d+)[\d.]*)/', $versionOutput, $matches) || preg_match('/((\d+\.\d+)[\d.]*)-MariaDB/', $versionOutput, $matches) - ) { + )) { return ['product' => 'mariadb', 'label' => 'MariaDB', 'version' => $matches[1], 'cycle' => $matches[2]]; } - if (preg_match('/Ver\s+((\d+\.\d+)[\d.]*)/', $versionOutput, $matches)) { + if (preg_match('/Distrib\s+((\d+\.\d+)[\d.]*)/', $versionOutput, $matches) + || preg_match('/Ver\s+((\d+\.\d+)[\d.]*)/', $versionOutput, $matches) + ) { return ['product' => 'mysql', 'label' => 'MySQL', 'version' => $matches[1], 'cycle' => $matches[2]]; } } catch (RunException) { @@ -274,6 +277,13 @@ function evaluateEolStatus(string $label, array $cycle, int $warnMonths): void if ($now >= $warnDate) { $interval = $now->diff($eolDate); + + if ($interval->invert) { + addRequirementRow("EOL: $label", REQUIREMENT_FAIL, "End of Life since $eolFrom"); + + return; + } + $months = $interval->y * 12 + $interval->m; $remaining = $months > 0 ? "in $months month(s)" : 'imminent'; addRequirementRow("EOL: $label", REQUIREMENT_WARN, "EOL $remaining ($eolFrom)"); diff --git a/deployer/requirements/task/check_eol.php b/deployer/requirements/task/check_eol.php index b59748a..d15d730 100644 --- a/deployer/requirements/task/check_eol.php +++ b/deployer/requirements/task/check_eol.php @@ -15,7 +15,7 @@ } $warnMonths = max(1, (int) get('requirements_eol_warn_months')); - $timeout = (int) get('requirements_eol_api_timeout'); + $timeout = max(1, (int) get('requirements_eol_api_timeout')); // Check PHP EOL try { diff --git a/deployer/requirements/task/check_image_processing.php b/deployer/requirements/task/check_image_processing.php index eaaddf8..c68763e 100644 --- a/deployer/requirements/task/check_image_processing.php +++ b/deployer/requirements/task/check_image_processing.php @@ -21,12 +21,12 @@ if ($gmOutput !== '' && preg_match('/GraphicsMagick\s+([\d.]+)/', $gmOutput, $matches)) { $actualVersion = $matches[1]; $meets = version_compare($actualVersion, $gmMinVersion, '>='); - $info = $meets - ? "GraphicsMagick $actualVersion" - : "GraphicsMagick $actualVersion (required: >= $gmMinVersion)"; - addRequirementRow('Image processing', $meets ? REQUIREMENT_OK : REQUIREMENT_FAIL, $info); - return; + if ($meets) { + addRequirementRow('Image processing', REQUIREMENT_OK, "GraphicsMagick $actualVersion"); + + return; + } } } catch (RunException) { // GraphicsMagick not available, try ImageMagick diff --git a/deployer/requirements/task/list.php b/deployer/requirements/task/list.php index d4fef27..ed88195 100644 --- a/deployer/requirements/task/list.php +++ b/deployer/requirements/task/list.php @@ -17,15 +17,20 @@ writeln(''); writeln('PHP'); writeln(sprintf(' Version: >= %s', get('requirements_php_min_version'))); - writeln(sprintf(' Extensions: %s', implode(', ', get('requirements_php_extensions')))); - writeln(' Settings:'); + if (get('requirements_check_php_extensions_enabled')) { + writeln(sprintf(' Extensions: %s', implode(', ', get('requirements_php_extensions')))); + } + + if (get('requirements_check_php_settings_enabled')) { + writeln(' Settings:'); - foreach (get('requirements_php_settings') as $setting => $value) { - $isComparison = in_array($setting, REQUIREMENT_BYTE_SETTINGS, true) - || in_array($setting, REQUIREMENT_NUMERIC_SETTINGS, true); - $operator = $isComparison ? '>=' : '='; - writeln(sprintf(' %-30s %s %s', $setting, $operator, $value)); + foreach (get('requirements_php_settings') as $setting => $value) { + $isComparison = in_array($setting, REQUIREMENT_BYTE_SETTINGS, true) + || in_array($setting, REQUIREMENT_NUMERIC_SETTINGS, true); + $operator = $isComparison ? '>=' : '='; + writeln(sprintf(' %-30s %s %s', $setting, $operator, $value)); + } } } From 37d443ee923b89ca7ad57ac801aa892bd7c562ad Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Thu, 19 Feb 2026 11:23:43 +0100 Subject: [PATCH 8/8] fix: address remaining CodeRabbit review findings --- deployer/requirements/functions.php | 2 +- deployer/requirements/task/check_image_processing.php | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/deployer/requirements/functions.php b/deployer/requirements/functions.php index 0b74910..c9e3963 100644 --- a/deployer/requirements/functions.php +++ b/deployer/requirements/functions.php @@ -155,7 +155,7 @@ function detectPackageVersion(string $command): ?string try { $output = trim(run($versionCmd)); - if ($output !== '' && preg_match('/(\d+[\d.]+)/', $output, $matches)) { + if ($output !== '' && preg_match('/(\d+[\d.]*)/', $output, $matches)) { return $matches[1]; } } catch (RunException) { diff --git a/deployer/requirements/task/check_image_processing.php b/deployer/requirements/task/check_image_processing.php index c68763e..3a0ade0 100644 --- a/deployer/requirements/task/check_image_processing.php +++ b/deployer/requirements/task/check_image_processing.php @@ -13,6 +13,7 @@ $gmMinVersion = get('requirements_graphicsmagick_min_version'); $imMinVersion = get('requirements_imagemagick_min_version'); + $gmVersionFail = false; // Try GraphicsMagick first (recommended) try { @@ -27,6 +28,8 @@ return; } + + $gmVersionFail = "GraphicsMagick $actualVersion (required: >= $gmMinVersion)"; } } catch (RunException) { // GraphicsMagick not available, try ImageMagick @@ -55,6 +58,8 @@ addRequirementRow( 'Image processing', REQUIREMENT_FAIL, - "Neither GraphicsMagick (>= $gmMinVersion) nor ImageMagick (>= $imMinVersion) found" + $gmVersionFail !== false + ? "$gmVersionFail — ImageMagick (>= $imMinVersion) not found" + : "Neither GraphicsMagick (>= $gmMinVersion) nor ImageMagick (>= $imMinVersion) found" ); })->hidden();