#!/usr/bin/env php * * SPDX-License-Identifier: GPL-3.0-or-later * * FILE INFORMATION * DEFGROUP: moko-platform.CLI * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/client_health_check.php * BRIEF: Verify a client site's update server, installed version, and release availability */ declare(strict_types=1); require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; use MokoEnterprise\CliFramework; class ClientHealthCheckCli extends CliFramework { protected function configure(): void { $this->setDescription('Verify a client site\'s update server, installed version, and release availability'); $this->addArgument('--path', 'Repository root (reads update server URL from manifest)', '.'); $this->addArgument('--update-url', 'Update server XML URL (overrides manifest)', ''); $this->addArgument('--site-url', 'Live site URL for version checking via Joomla API', ''); $this->addArgument('--api-token', 'Joomla API token for site-url', ''); $this->addArgument('--github-output', 'Export results to $GITHUB_OUTPUT', false); } protected function run(): int { $path = $this->getArgument('--path'); $updateUrl = $this->getArgument('--update-url'); $siteUrl = $this->getArgument('--site-url'); $apiToken = $this->getArgument('--api-token'); $ghOutput = $this->getArgument('--github-output'); $root = realpath($path) ?: $path; $checks = []; // -- Resolve update server URL from manifest -- if ($updateUrl === '') { $updateUrl = null; $searchDirs = ["{$root}/src", $root]; foreach ($searchDirs as $dir) { if (!is_dir($dir)) { continue; } foreach (glob("{$dir}/*.xml") ?: [] as $f) { $xml = file_get_contents($f); if (preg_match('/]*>([^<]+)<\/server>/', $xml, $m)) { $updateUrl = trim($m[1]); break 2; } } } } if ($updateUrl === null || $updateUrl === '') { $this->log('ERROR', 'No update server URL found. Use --update-url or provide a manifest with .'); return 1; } echo "Update server: {$updateUrl}\n\n"; // -- Check 1: Update server accessible -- echo "--- Update Server ---\n"; $ch = curl_init($updateUrl); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_FOLLOWLOCATION => true, CURLOPT_HTTPHEADER => ['User-Agent: MokoHealthCheck/1.0'], ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode === 200 && !empty($response)) { echo " PASS: HTTP {$httpCode}, " . strlen($response) . " bytes\n"; $checks['update_server'] = 'pass'; } else { echo " FAIL: HTTP {$httpCode}\n"; $checks['update_server'] = 'fail'; } // -- Check 2: Parse updates.xml for stable version -- $stableVersion = null; $downloadUrl = null; if (!empty($response)) { $sections = preg_split('//', $response); foreach ($sections as $section) { if (strpos($section, 'stable') !== false) { if (preg_match('/([^<]+)<\/version>/', $section, $m)) { $stableVersion = $m[1]; } if (preg_match('/]*>([^<]+)<\/downloadurl>/', $section, $m)) { $downloadUrl = trim($m[1]); } break; } } if ($stableVersion === null && preg_match('/([^<]+)<\/version>/', $response, $m)) { $stableVersion = $m[1]; } } echo "\n--- Stable Release ---\n"; if ($stableVersion !== null) { echo " Version: {$stableVersion}\n"; $checks['stable_version'] = $stableVersion; } else { echo " FAIL: Could not parse stable version\n"; $checks['stable_version'] = 'fail'; } // -- Check 3: Download URL accessible -- if ($downloadUrl !== null) { echo "\n--- Download URL ---\n"; $ch = curl_init($downloadUrl); curl_setopt_array($ch, [ CURLOPT_NOBODY => true, CURLOPT_TIMEOUT => 15, CURLOPT_FOLLOWLOCATION => true, ]); curl_exec($ch); $dlCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $dlSize = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD); curl_close($ch); if ($dlCode === 200) { $sizeKb = $dlSize > 0 ? round($dlSize / 1024) . 'KB' : 'unknown size'; echo " PASS: HTTP {$dlCode}, {$sizeKb}\n"; $checks['download'] = 'pass'; } else { echo " FAIL: HTTP {$dlCode}\n"; $checks['download'] = 'fail'; } } // -- Check 4: Site version (optional) -- if ($siteUrl !== '' && $apiToken !== '') { echo "\n--- Site Version ---\n"; $apiUrl = rtrim($siteUrl, '/') . '/api/index.php/v1/extensions?filter[type]=file'; $ch = curl_init($apiUrl); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_HTTPHEADER => [ "X-Joomla-Token: {$apiToken}", 'Accept: application/json', ], ]); $siteResponse = curl_exec($ch); $siteCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($siteCode === 200) { echo " API accessible (HTTP {$siteCode})\n"; $checks['site_api'] = 'pass'; } else { echo " WARN: Site API returned HTTP {$siteCode}\n"; $checks['site_api'] = 'warn'; } } // -- Summary -- echo "\n=== Health Check Summary ===\n"; $failed = 0; foreach ($checks as $name => $result) { $icon = ($result === 'fail') ? 'FAIL' : (($result === 'warn') ? 'WARN' : 'OK'); if ($result === 'fail') { $failed++; } echo " {$icon}: {$name} = {$result}\n"; } if ($ghOutput) { $ghFile = getenv('GITHUB_OUTPUT'); if ($ghFile) { file_put_contents($ghFile, "health_status=" . ($failed > 0 ? 'fail' : 'pass') . "\n", FILE_APPEND); file_put_contents($ghFile, "health_version=" . ($stableVersion ?? 'unknown') . "\n", FILE_APPEND); file_put_contents($ghFile, "health_failures={$failed}\n", FILE_APPEND); } } return $failed > 0 ? 1 : 0; } } $app = new ClientHealthCheckCli(); exit($app->execute());