#!/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/release_verify.php * BRIEF: Verify a built release artifact — version, SHA256, disallowed files * * Usage: * php release_verify.php --zip-path /tmp/pkg.zip --version 04.01.00 * php release_verify.php --zip-path /tmp/pkg.zip --version 04.01.00 --updates-xml updates.xml * php release_verify.php --zip-path /tmp/pkg.zip --version 04.01.00 --output-summary * * Options: * --zip-path Path to ZIP file (required) * --version Expected version string (required) * --platform joomla|dolibarr|generic (default: joomla) * --updates-xml Path to updates.xml for SHA256 comparison * --github-output Export verify_pass, verify_fail to $GITHUB_OUTPUT * --output-summary Write markdown table to $GITHUB_STEP_SUMMARY */ declare(strict_types=1); $zipPath = null; $version = null; $platform = 'joomla'; $updatesXml = null; $githubOutput = false; $outputSummary = false; foreach ($argv as $i => $arg) { if ($arg === '--zip-path' && isset($argv[$i + 1])) $zipPath = $argv[$i + 1]; if ($arg === '--version' && isset($argv[$i + 1])) $version = $argv[$i + 1]; if ($arg === '--platform' && isset($argv[$i + 1])) $platform = $argv[$i + 1]; if ($arg === '--updates-xml' && isset($argv[$i + 1])) $updatesXml = $argv[$i + 1]; if ($arg === '--github-output') $githubOutput = true; if ($arg === '--output-summary') $outputSummary = true; } if ($zipPath === null || $version === null) { fwrite(STDERR, "Usage: release_verify.php --zip-path FILE --version XX.YY.ZZ [--platform joomla] [--updates-xml FILE]\n"); exit(1); } $pass = 0; $fail = 0; $warn = 0; $results = []; function addResult(string $check, string $status, string $details): void { global $pass, $fail, $warn, $results; $results[] = ['check' => $check, 'status' => $status, 'details' => $details]; if ($status === 'PASS') $pass++; elseif ($status === 'FAIL') $fail++; elseif ($status === 'WARN') $warn++; } // 1. ZIP exists and is readable if (!file_exists($zipPath) || !is_readable($zipPath)) { addResult('ZIP exists', 'FAIL', "Not found or not readable: {$zipPath}"); } else { addResult('ZIP exists', 'PASS', basename($zipPath)); // 2. Extract ZIP $tmpDir = sys_get_temp_dir() . '/release-verify-' . uniqid(); mkdir($tmpDir, 0755, true); $zip = new ZipArchive(); if ($zip->open($zipPath) !== true) { addResult('ZIP extract', 'FAIL', 'ZipArchive could not open file'); } else { $zip->extractTo($tmpDir); $zip->close(); addResult('ZIP extract', 'PASS', 'Extracted successfully'); // 3. Manifest version check (Joomla) if ($platform === 'joomla') { $manifest = null; foreach (glob("{$tmpDir}/*.xml") as $xmlFile) { $content = file_get_contents($xmlFile); if (strpos($content, '([^<]+)<\/version>/', file_get_contents($manifest), $m)) { $manifestVer = trim($m[1]); if ($manifestVer === $version) { addResult('Manifest version', 'PASS', "`{$manifestVer}` matches release"); } else { addResult('Manifest version', 'FAIL', "`{$manifestVer}` != `{$version}`"); } } else { addResult('Manifest version', 'WARN', 'No tag in manifest'); } } else { addResult('Manifest version', 'WARN', 'No XML manifest found in ZIP'); } } // 4. SHA256 vs updates.xml $zipSha = hash_file('sha256', $zipPath); if ($updatesXml !== null && file_exists($updatesXml)) { $uxContent = file_get_contents($updatesXml); if (preg_match('/([^<]+)<\/sha256>/', $uxContent, $m)) { $expectedSha = trim($m[1]); if ($zipSha === $expectedSha) { addResult('SHA256 vs updates.xml', 'PASS', '`' . substr($zipSha, 0, 16) . '...`'); } else { addResult('SHA256 vs updates.xml', 'FAIL', "ZIP=`" . substr($zipSha, 0, 16) . "...` updates.xml=`" . substr($expectedSha, 0, 16) . "...`"); } } else { addResult('SHA256 vs updates.xml', 'WARN', 'No in updates.xml'); } } // 5. Disallowed files $disallowed = ['.claude', '.mcp.json', 'TODO.md', 'todo.md', '.git', 'node_modules', '.env']; $found = []; $rit = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($tmpDir, RecursiveDirectoryIterator::SKIP_DOTS)); foreach ($rit as $file) { $name = $file->getFilename(); if (in_array($name, $disallowed, true)) { $found[] = $name; } } if (count($found) > 0) { addResult('Disallowed files', 'FAIL', 'Found: ' . implode(', ', array_unique($found))); } else { addResult('Disallowed files', 'PASS', 'None found'); } // 6. Non-vendor .min files $minCount = 0; $rit = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($tmpDir, RecursiveDirectoryIterator::SKIP_DOTS)); foreach ($rit as $file) { $rel = str_replace($tmpDir . '/', '', $file->getPathname()); if (strpos($rel, 'vendor/') !== false) continue; if (preg_match('/\.(min\.css|min\.js)$/', $file->getFilename())) { $minCount++; } } if ($minCount > 0) { addResult('Non-vendor .min files', 'WARN', "{$minCount} file(s) — should be generated at runtime"); } else { addResult('Non-vendor .min files', 'PASS', 'None shipped'); } // Clean up $rit = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($tmpDir, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST); foreach ($rit as $file) { $file->isDir() ? rmdir($file->getPathname()) : unlink($file->getPathname()); } rmdir($tmpDir); } } // Output $table = "| Check | Result | Details |\n|-------|--------|--------|\n"; foreach ($results as $r) { $table .= "| {$r['check']} | {$r['status']} | {$r['details']} |\n"; } $table .= "\n**Verification: {$pass} passed, {$fail} failed, {$warn} warnings**\n"; echo $table; if ($outputSummary) { $summaryFile = getenv('GITHUB_STEP_SUMMARY'); if ($summaryFile) { file_put_contents($summaryFile, "### Release Verification\n\n{$table}\n", FILE_APPEND); } } if ($githubOutput) { $outputFile = getenv('GITHUB_OUTPUT'); if ($outputFile) { file_put_contents($outputFile, "verify_pass={$pass}\nverify_fail={$fail}\nverify_warn={$warn}\n", FILE_APPEND); } } exit($fail > 0 ? 1 : 0);