#!/usr/bin/env php * * This file is part of a Moko Consulting project. * * SPDX-License-Identifier: GPL-3.0-or-later * * FILE INFORMATION * DEFGROUP: MokoPlatform.Scripts.Deploy * INGROUP: MokoPlatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /deploy/health-check.php * VERSION: 09.25.00 * BRIEF: Post-deploy health check — verify a Joomla site is responding correctly */ declare(strict_types=1); require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; use MokoEnterprise\CliFramework; class HealthCheckCli extends CliFramework { private string $url = ''; private int $timeout = 30; private array $checks = ['http']; private int $passed = 0; private int $failed = 0; protected function configure(): void { $this->setDescription('Post-deploy health check — verify a Joomla site is responding correctly'); $this->addArgument('--url', 'Site URL to check', ''); $this->addArgument('--timeout', 'Request timeout in seconds', '30'); $this->addArgument('--checks', 'Comma-separated list of checks: http,admin,api', 'http'); } protected function run(): int { $this->url = $this->getArgument('--url'); $this->timeout = (int) $this->getArgument('--timeout'); $checksRaw = $this->getArgument('--checks'); $this->checks = array_map('trim', explode(',', $checksRaw)); if ($this->url === '') { $this->log('ERROR', 'Usage: health-check.php --url [--timeout ] [--checks ]'); return 1; } $this->url = rtrim($this->url, '/'); $this->log('INFO', "Health check for: {$this->url}"); $this->log('INFO', "Timeout: {$this->timeout}s"); $this->log('INFO', "Checks: " . implode(', ', $this->checks)); $this->log('INFO', ''); foreach ($this->checks as $check) { switch ($check) { case 'http': $this->checkHttp(); break; case 'admin': $this->checkAdmin(); break; case 'api': $this->checkApi(); break; default: $this->log('WARN', "UNKNOWN CHECK: {$check} — skipping"); break; } } $this->log('INFO', ''); $this->log('INFO', "Results: {$this->passed} passed, {$this->failed} failed"); return $this->failed > 0 ? 1 : 0; } private function checkHttp(): void { $this->log('INFO', '[http] GET ' . $this->url); $result = $this->curlGet($this->url); if ($result === null) { $this->fail('http', 'Request failed — could not connect'); return; } if ($result['http_code'] !== 200) { $this->fail('http', "Expected HTTP 200, got {$result['http_code']}"); return; } if ($this->containsFatalError($result['body'])) { $this->fail('http', 'Response body contains PHP fatal error'); return; } $this->pass('http', "HTTP 200 OK ({$result['time_ms']}ms)"); } private function checkAdmin(): void { $adminUrl = $this->url . '/administrator/'; $this->log('INFO', '[admin] GET ' . $adminUrl); $result = $this->curlGet($adminUrl); if ($result === null) { $this->fail('admin', 'Request failed — could not connect'); return; } if ($result['http_code'] !== 200) { $this->fail('admin', "Expected HTTP 200, got {$result['http_code']}"); return; } $this->pass('admin', "HTTP 200 OK ({$result['time_ms']}ms)"); } private function checkApi(): void { $apiUrl = $this->url . '/api/index.php/v1'; $this->log('INFO', '[api] GET ' . $apiUrl); $result = $this->curlGet($apiUrl); if ($result === null) { $this->fail('api', 'Request failed — could not connect'); return; } if ($result['http_code'] !== 200 && $result['http_code'] !== 401) { $this->fail('api', "Expected HTTP 200 or 401, got {$result['http_code']}"); return; } $this->pass('api', "HTTP {$result['http_code']} — API is alive ({$result['time_ms']}ms)"); } private function curlGet(string $url): ?array { $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, CURLOPT_TIMEOUT => $this->timeout, CURLOPT_CONNECTTIMEOUT => $this->timeout, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_USERAGENT => 'MokoHealthCheck/1.0', ]); $body = curl_exec($ch); if (curl_errno($ch)) { $error = curl_error($ch); $this->log('ERROR', " cURL error: {$error}"); curl_close($ch); return null; } $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); $totalTime = curl_getinfo($ch, CURLINFO_TOTAL_TIME); curl_close($ch); return [ 'http_code' => $httpCode, 'body' => is_string($body) ? $body : '', 'time_ms' => (int) round($totalTime * 1000), ]; } private function containsFatalError(string $body): bool { $patterns = [ 'Fatal error:', 'Fatal Error', 'Parse error:', 'Uncaught Error:', 'Uncaught Exception:', ]; foreach ($patterns as $pattern) { if (stripos($body, $pattern) !== false) { return true; } } return false; } private function pass(string $check, string $message): void { $this->passed++; $this->log('INFO', "[{$check}] PASS: {$message}"); } private function fail(string $check, string $message): void { $this->failed++; $this->log('ERROR', "[{$check}] FAIL: {$message}"); } } $app = new HealthCheckCli(); exit($app->execute());