Files
Jonathan Miller 6a29fbd99e refactor: rename GiteaAdapter to MokoGiteaAdapter
Rename class, file, and all references across the codebase to align
with the moko-platform naming convention.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 17:14:29 -05:00

228 lines
5.1 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: MokoStandards.Scripts.Deploy
* INGROUP: MokoStandards
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /deploy/health-check.php
* VERSION: 01.00.00
* BRIEF: Post-deploy health check — verify a Joomla site is responding correctly
*/
declare(strict_types=1);
class HealthCheck
{
private string $url = '';
private int $timeout = 30;
private array $checks = ['http'];
private int $passed = 0;
private int $failed = 0;
public function run(): int
{
$this->parseArgs();
if ($this->url === '') {
$this->log('Usage: health-check.php --url <site-url> [--timeout <seconds>] [--checks <http,admin,api>]');
return 1;
}
$this->url = rtrim($this->url, '/');
$this->log("Health check for: {$this->url}");
$this->log("Timeout: {$this->timeout}s");
$this->log("Checks: " . implode(', ', $this->checks));
$this->log('');
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("UNKNOWN CHECK: {$check} — skipping");
break;
}
}
$this->log('');
$this->log("Results: {$this->passed} passed, {$this->failed} failed");
return $this->failed > 0 ? 1 : 0;
}
private function parseArgs(): void
{
$args = $_SERVER['argv'] ?? [];
$count = count($args);
for ($i = 1; $i < $count; $i++) {
switch ($args[$i]) {
case '--url':
$this->url = $args[++$i] ?? '';
break;
case '--timeout':
$this->timeout = (int) ($args[++$i] ?? 30);
break;
case '--checks':
$raw = $args[++$i] ?? 'http';
$this->checks = array_map('trim', explode(',', $raw));
break;
}
}
}
private function checkHttp(): void
{
$this->log('[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('[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('[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(" 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("[{$check}] PASS: {$message}");
}
private function fail(string $check, string $message): void
{
$this->failed++;
$this->log("[{$check}] FAIL: {$message}");
}
private function log(string $message): void
{
$timestamp = date('Y-m-d H:i:s');
fwrite(STDERR, "[{$timestamp}] {$message}" . PHP_EOL);
}
}
$app = new HealthCheck();
exit($app->run());