From 89a78dedb7d723c56c5ee6c49b8c445cd6fc8867 Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Thu, 19 Feb 2026 11:04:50 +0100 Subject: [PATCH 1/5] feat: add requirements:health task for service health checks --- autoload.php | 1 + deployer/requirements/config/set.php | 4 ++ deployer/requirements/functions.php | 28 +++++++++ deployer/requirements/task/health.php | 83 +++++++++++++++++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 deployer/requirements/task/health.php diff --git a/autoload.php b/autoload.php index b2aa7ba..8ec9d90 100644 --- a/autoload.php +++ b/autoload.php @@ -51,6 +51,7 @@ 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'); +require_once(__DIR__ . '/deployer/requirements/task/health.php'); /* * dev diff --git a/deployer/requirements/config/set.php b/deployer/requirements/config/set.php index e113154..a969271 100644 --- a/deployer/requirements/config/set.php +++ b/deployer/requirements/config/set.php @@ -107,6 +107,10 @@ set('requirements_eol_warn_months', 6); set('requirements_eol_api_timeout', 5); +// Health check +set('requirements_check_health_enabled', true); +set('requirements_health_url', 'http://localhost'); + // User / permissions set('requirements_user_group', 'www-data'); set('requirements_deploy_path_permissions', '2770'); diff --git a/deployer/requirements/functions.php b/deployer/requirements/functions.php index 0b74910..52c29c4 100644 --- a/deployer/requirements/functions.php +++ b/deployer/requirements/functions.php @@ -331,6 +331,34 @@ function checkEolForProduct(string $label, string $product, string $cycle, int $ evaluateEolStatus("$label $cycle", $match, $warnMonths); } +/** + * Check if a service is active via systemctl, with pgrep fallback. + * + * Returns the matched service/process name or null if none found. + */ +function isServiceActive(string ...$names): ?string +{ + $hasSystemctl = test('command -v systemctl > /dev/null 2>&1'); + + foreach ($names as $name) { + try { + if ($hasSystemctl) { + $status = trim(run("systemctl is-active $name 2>/dev/null || true")); + + if ($status === 'active') { + return $name; + } + } elseif (test("pgrep -x $name > /dev/null 2>&1")) { + return $name; + } + } catch (RunException) { + continue; + } + } + + return null; +} + /** * @return array */ diff --git a/deployer/requirements/task/health.php b/deployer/requirements/task/health.php new file mode 100644 index 0000000..e3d6f2f --- /dev/null +++ b/deployer/requirements/task/health.php @@ -0,0 +1,83 @@ +/dev/null')); + $fpmService = isServiceActive("php$phpVersion-fpm", 'php-fpm'); + + if ($fpmService !== null) { + addRequirementRow('PHP-FPM', REQUIREMENT_OK, "Active ($fpmService)"); + } else { + addRequirementRow('PHP-FPM', REQUIREMENT_FAIL, 'Process not found'); + } + } catch (RunException) { + addRequirementRow('PHP-FPM', REQUIREMENT_SKIP, 'Could not determine PHP version'); + } + + // 2. Webserver + $webserver = isServiceActive('nginx', 'apache2', 'httpd'); + + if ($webserver !== null) { + addRequirementRow('Webserver', REQUIREMENT_OK, "Active ($webserver)"); + } else { + addRequirementRow('Webserver', REQUIREMENT_FAIL, 'No nginx, apache2 or httpd process found'); + } + + // 3. Database server + $db = detectDatabaseProduct(); + $dbLabel = $db !== null ? $db['label'] : 'Database'; + $adminCmd = ($db !== null && $db['product'] === 'mariadb') ? 'mariadb-admin' : 'mysqladmin'; + $dbChecked = false; + + try { + run("$adminCmd ping --silent 2>&1 || true"); + addRequirementRow('Database server', REQUIREMENT_OK, "$dbLabel responding"); + $dbChecked = true; + } catch (RunException) { + // Admin tool not available — fall through to process check + } + + if (!$dbChecked) { + $dbProcess = isServiceActive('mysqld', 'mariadbd'); + + if ($dbProcess !== null) { + addRequirementRow('Database server', REQUIREMENT_OK, "$dbLabel process running ($dbProcess)"); + } else { + addRequirementRow('Database server', REQUIREMENT_FAIL, 'No mysqld or mariadbd process found'); + } + } + + // 4. HTTP response + $url = get('requirements_health_url'); + + try { + $httpCode = (int) trim(run( + sprintf("curl -s -o /dev/null -w '%%{http_code}' --max-time 5 %s 2>/dev/null", escapeshellarg($url)) + )); + + if ($httpCode >= 200 && $httpCode < 500) { + addRequirementRow('HTTP response', REQUIREMENT_OK, "HTTP $httpCode from $url"); + } elseif ($httpCode === 0) { + addRequirementRow('HTTP response', REQUIREMENT_FAIL, "No response from $url (connection refused or timeout)"); + } else { + addRequirementRow('HTTP response', REQUIREMENT_FAIL, "HTTP $httpCode from $url"); + } + } catch (RunException) { + addRequirementRow('HTTP response', REQUIREMENT_SKIP, 'curl not available'); + } + + renderRequirementsTable(); +})->desc('Check service health'); From 1f55c50008a258b4cc7054a47bd387a9e3ca682b Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Thu, 19 Feb 2026 12:06:16 +0100 Subject: [PATCH 2/5] docs: add requirements:health section to documentation --- docs/REQUIREMENTS.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index bcd34d7..3b40e49 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -107,6 +107,25 @@ Checks installed PHP and database (MariaDB/MySQL) versions against the [endoflif The warning threshold is configurable (default: 6 months before EOL). +## Health check + +A standalone task that verifies critical services are running on the target host. This is useful as a quick smoke test before or after deployment. + +```bash +$ dep requirements:health [host] +``` + +The task checks four categories: + +| Check | Method | OK | FAIL | +|-------|--------|----|------| +| PHP-FPM | systemctl / pgrep for `php-fpm` | Process active | Process not found | +| Webserver | systemctl / pgrep for nginx, apache2, httpd | Process active | No process found | +| Database server | `mysqladmin ping` / `mariadb-admin ping` with process fallback | Responding or process running | No process found | +| HTTP response | `curl` against configured URL | HTTP 2xx–4xx | HTTP 5xx, timeout, or connection refused | + +Service detection uses `systemctl is-active` with a `pgrep` fallback for systems without systemd. + ## Configuration All settings use the `requirements_` prefix and can be overridden in the consuming project: @@ -162,6 +181,10 @@ set('requirements_eol_api_timeout', 5); // API timeout in seconds // Database grants check set('requirements_check_database_grants_enabled', true); + +// Health check +set('requirements_check_health_enabled', true); +set('requirements_health_url', 'https://example.com'); ``` ## Extending with custom checks From 013e73a7566e1d62fed32c86ae6bfdc29c0d9aed Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Thu, 19 Feb 2026 14:10:29 +0100 Subject: [PATCH 3/5] fix: validate PHP version output and remove || true from database ping --- deployer/requirements/task/health.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/deployer/requirements/task/health.php b/deployer/requirements/task/health.php index e3d6f2f..ba002b9 100644 --- a/deployer/requirements/task/health.php +++ b/deployer/requirements/task/health.php @@ -16,6 +16,11 @@ // 1. PHP-FPM try { $phpVersion = trim(run('php -r "echo PHP_MAJOR_VERSION.\'.\'.PHP_MINOR_VERSION;" 2>/dev/null')); + + if (!preg_match('/^\d+\.\d+$/', $phpVersion)) { + throw new RunException('php', 1, "Unexpected PHP version output: $phpVersion", ''); + } + $fpmService = isServiceActive("php$phpVersion-fpm", 'php-fpm'); if ($fpmService !== null) { @@ -43,7 +48,7 @@ $dbChecked = false; try { - run("$adminCmd ping --silent 2>&1 || true"); + run("$adminCmd ping --silent 2>/dev/null"); addRequirementRow('Database server', REQUIREMENT_OK, "$dbLabel responding"); $dbChecked = true; } catch (RunException) { From be1057da86982f7529309bf05d53505a0a5cc8c2 Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Thu, 19 Feb 2026 14:10:30 +0100 Subject: [PATCH 4/5] fix: always fall through to pgrep when systemctl does not confirm active --- deployer/requirements/functions.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/deployer/requirements/functions.php b/deployer/requirements/functions.php index 788c806..7fd57a1 100644 --- a/deployer/requirements/functions.php +++ b/deployer/requirements/functions.php @@ -369,7 +369,9 @@ function isServiceActive(string ...$names): ?string if ($status === 'active') { return $name; } - } elseif (test("pgrep -x $name > /dev/null 2>&1")) { + } + + if (test("pgrep -x $name > /dev/null 2>&1")) { return $name; } } catch (RunException) { From 9743a1402f6d9d37152ebcda6e0f12ac54b2bf5a Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Thu, 19 Feb 2026 14:10:31 +0100 Subject: [PATCH 5/5] refactor: move health check enable flag to enable-flags group --- deployer/requirements/config/set.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployer/requirements/config/set.php b/deployer/requirements/config/set.php index 8b0bf97..56da57d 100644 --- a/deployer/requirements/config/set.php +++ b/deployer/requirements/config/set.php @@ -18,6 +18,7 @@ set('requirements_check_env_enabled', true); set('requirements_check_eol_enabled', true); set('requirements_check_database_grants_enabled', true); +set('requirements_check_health_enabled', true); // Locales set('requirements_locales', ['de_DE.utf8', 'en_US.utf8']); @@ -109,7 +110,6 @@ set('requirements_eol_api_timeout', 5); // Health check -set('requirements_check_health_enabled', true); set('requirements_health_url', 'http://localhost'); // User / permissions