Files
Jonathan Miller 11eb1e2649
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Universal: Auto Version Bump / Version Bump (push) Failing after 5s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 36s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 37s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
chore(release): bump to 09.23.00 — plugin commands, audit query, version fix
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 14:18:26 -05:00

217 lines
6.3 KiB
PHP

#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* 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.23.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 <site-url> [--timeout <seconds>] [--checks <http,admin,api>]');
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());