From 14de7dbe19ede4e622e2d37dfde58d674968dd57 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 23 May 2026 22:37:37 -0500 Subject: [PATCH 1/2] feat(cli): add 4 release pipeline CLI tools - release_verify.php: Post-release artifact verification (ZIP integrity, manifest version, SHA256 vs updates.xml, disallowed files check) - release_validate.php: Pre-release sanity checks (version consistency across README, CHANGELOG, manifest, updates.xml, composer.json) - release_body_update.php: Update Gitea release body with changelog extract and checksums table via API - dev_branch_reset.php: Delete and recreate dev branch from main via Gitea API Resolves: #56, #60, #62, #64 Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- cli/dev_branch_reset.php | 97 +++++++++++++++++++ cli/release_body_update.php | 152 +++++++++++++++++++++++++++++ cli/release_validate.php | 178 ++++++++++++++++++++++++++++++++++ cli/release_verify.php | 188 ++++++++++++++++++++++++++++++++++++ 4 files changed, 615 insertions(+) create mode 100644 cli/dev_branch_reset.php create mode 100644 cli/release_body_update.php create mode 100644 cli/release_validate.php create mode 100644 cli/release_verify.php diff --git a/cli/dev_branch_reset.php b/cli/dev_branch_reset.php new file mode 100644 index 0000000..605b393 --- /dev/null +++ b/cli/dev_branch_reset.php @@ -0,0 +1,97 @@ +#!/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/dev_branch_reset.php + * BRIEF: Delete and recreate dev branch from main via Gitea API + * + * Usage: + * php dev_branch_reset.php --token TOKEN --api-base URL + * php dev_branch_reset.php --token TOKEN --api-base URL --branch dev --from main + * + * Options: + * --token Gitea API token (required) + * --api-base Gitea API base URL (required) + * --branch Branch to reset (default: dev) + * --from Source branch (default: main) + * --output-summary Write to $GITHUB_STEP_SUMMARY + */ + +declare(strict_types=1); + +$token = null; +$apiBase = null; +$branch = 'dev'; +$from = 'main'; +$outputSummary = false; + +foreach ($argv as $i => $arg) { + if ($arg === '--token' && isset($argv[$i + 1])) $token = $argv[$i + 1]; + if ($arg === '--api-base' && isset($argv[$i + 1])) $apiBase = $argv[$i + 1]; + if ($arg === '--branch' && isset($argv[$i + 1])) $branch = $argv[$i + 1]; + if ($arg === '--from' && isset($argv[$i + 1])) $from = $argv[$i + 1]; + if ($arg === '--output-summary') $outputSummary = true; +} + +if ($token === null) $token = getenv('GA_TOKEN') ?: getenv('GITEA_TOKEN') ?: null; + +if ($token === null || $apiBase === null) { + fwrite(STDERR, "Usage: dev_branch_reset.php --token TOKEN --api-base URL [--branch dev] [--from main]\n"); + exit(1); +} + +// Delete branch (tolerate 404) +$ch = curl_init("{$apiBase}/branches/{$branch}"); +curl_setopt_array($ch, [ + CURLOPT_CUSTOMREQUEST => 'DELETE', + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => ["Authorization: token {$token}"], + CURLOPT_TIMEOUT => 30, +]); +curl_exec($ch); +$delCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +curl_close($ch); + +if ($delCode === 204 || $delCode === 200) { + echo "Deleted branch '{$branch}'\n"; +} elseif ($delCode === 404) { + echo "Branch '{$branch}' did not exist (skipped delete)\n"; +} else { + fwrite(STDERR, "WARNING: Delete branch returned HTTP {$delCode}\n"); +} + +// Create branch from source +$payload = json_encode(['new_branch_name' => $branch, 'old_branch_name' => $from]); +$ch = curl_init("{$apiBase}/branches"); +curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => ["Authorization: token {$token}", "Content-Type: application/json"], + CURLOPT_POSTFIELDS => $payload, + CURLOPT_TIMEOUT => 30, +]); +$response = curl_exec($ch); +$createCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +curl_close($ch); + +if ($createCode === 201) { + echo "Recreated '{$branch}' from '{$from}'\n"; +} else { + fwrite(STDERR, "Failed to create branch '{$branch}' from '{$from}' (HTTP {$createCode})\n"); + exit(1); +} + +if ($outputSummary) { + $summaryFile = getenv('GITHUB_STEP_SUMMARY'); + if ($summaryFile) { + file_put_contents($summaryFile, "Dev branch reset: '{$branch}' recreated from '{$from}'\n", FILE_APPEND); + } +} + +exit(0); diff --git a/cli/release_body_update.php b/cli/release_body_update.php new file mode 100644 index 0000000..43f565e --- /dev/null +++ b/cli/release_body_update.php @@ -0,0 +1,152 @@ +#!/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_body_update.php + * BRIEF: Update Gitea release body with changelog extract and checksums + * + * Usage: + * php release_body_update.php --version 04.01.00 --release-tag stable --token TOKEN --api-base URL + * php release_body_update.php --version 04.01.00 --release-tag stable --token TOKEN --api-base URL --zip-name pkg.zip --zip-sha abc123 + * + * Options: + * --path Repo root for CHANGELOG.md (default: .) + * --version Version string (required) + * --release-tag Gitea release tag (required) + * --token Gitea API token (required) + * --api-base Gitea API base URL (required) + * --zip-name ZIP filename for checksum table + * --tar-name tar.gz filename for checksum table + * --zip-sha SHA256 of ZIP + * --tar-sha SHA256 of tar.gz + * --output-summary Write to $GITHUB_STEP_SUMMARY + */ + +declare(strict_types=1); + +$path = '.'; +$version = null; +$releaseTag = null; +$token = null; +$apiBase = null; +$zipName = null; +$tarName = null; +$zipSha = null; +$tarSha = null; +$outputSummary = false; + +foreach ($argv as $i => $arg) { + if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1]; + if ($arg === '--version' && isset($argv[$i + 1])) $version = $argv[$i + 1]; + if ($arg === '--release-tag' && isset($argv[$i + 1])) $releaseTag = $argv[$i + 1]; + if ($arg === '--token' && isset($argv[$i + 1])) $token = $argv[$i + 1]; + if ($arg === '--api-base' && isset($argv[$i + 1])) $apiBase = $argv[$i + 1]; + if ($arg === '--zip-name' && isset($argv[$i + 1])) $zipName = $argv[$i + 1]; + if ($arg === '--tar-name' && isset($argv[$i + 1])) $tarName = $argv[$i + 1]; + if ($arg === '--zip-sha' && isset($argv[$i + 1])) $zipSha = $argv[$i + 1]; + if ($arg === '--tar-sha' && isset($argv[$i + 1])) $tarSha = $argv[$i + 1]; + if ($arg === '--output-summary') $outputSummary = true; +} + +if ($token === null) $token = getenv('GA_TOKEN') ?: getenv('GITEA_TOKEN') ?: null; + +if ($version === null || $releaseTag === null || $token === null || $apiBase === null) { + fwrite(STDERR, "Usage: release_body_update.php --version VER --release-tag TAG --token TOKEN --api-base URL\n"); + exit(1); +} + +$root = realpath($path) ?: $path; + +// Extract changelog section for this version +$changelog = ''; +$clFile = "{$root}/CHANGELOG.md"; +if (file_exists($clFile)) { + $lines = file($clFile, FILE_IGNORE_NEW_LINES); + $capturing = false; + $clLines = []; + foreach ($lines as $line) { + if (preg_match('/^##\s.*' . preg_quote($version, '/') . '/', $line)) { + $capturing = true; + continue; + } + if ($capturing && preg_match('/^## /', $line)) break; + if ($capturing) $clLines[] = $line; + } + $changelog = trim(implode("\n", $clLines)); +} + +// Build release body +$body = "## {$version} (" . date('Y-m-d') . ")\n\n"; +if (!empty($changelog)) { + $body .= "{$changelog}\n\n"; +} + +if ($zipSha !== null || $tarSha !== null) { + $body .= "---\n\n### Checksums\n\n| File | SHA-256 |\n|------|--------|\n"; + if ($zipName !== null && $zipSha !== null) { + $body .= "| `{$zipName}` | `{$zipSha}` |\n"; + } + if ($tarName !== null && $tarSha !== null) { + $body .= "| `{$tarName}` | `{$tarSha}` |\n"; + } +} + +// Get release ID by tag +$ch = curl_init("{$apiBase}/releases/tags/{$releaseTag}"); +curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => ["Authorization: token {$token}"], + CURLOPT_TIMEOUT => 30, +]); +$response = curl_exec($ch); +$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +curl_close($ch); + +if ($httpCode !== 200 || empty($response)) { + fwrite(STDERR, "Failed to get release for tag '{$releaseTag}' (HTTP {$httpCode})\n"); + exit(1); +} + +$release = json_decode($response, true); +$releaseId = $release['id'] ?? null; + +if ($releaseId === null) { + fwrite(STDERR, "No release ID found for tag '{$releaseTag}'\n"); + exit(1); +} + +// PATCH release body +$payload = json_encode(['body' => $body]); +$ch = curl_init("{$apiBase}/releases/{$releaseId}"); +curl_setopt_array($ch, [ + CURLOPT_CUSTOMREQUEST => 'PATCH', + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => ["Authorization: token {$token}", "Content-Type: application/json"], + CURLOPT_POSTFIELDS => $payload, + CURLOPT_TIMEOUT => 30, +]); +$response = curl_exec($ch); +$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +curl_close($ch); + +if ($httpCode !== 200) { + fwrite(STDERR, "Failed to update release body (HTTP {$httpCode})\n"); + exit(1); +} + +echo "Release body updated for {$releaseTag} (release #{$releaseId})\n"; + +if ($outputSummary) { + $summaryFile = getenv('GITHUB_STEP_SUMMARY'); + if ($summaryFile) { + file_put_contents($summaryFile, "Release body updated with changelog + checksums\n", FILE_APPEND); + } +} + +exit(0); diff --git a/cli/release_validate.php b/cli/release_validate.php new file mode 100644 index 0000000..7a37cac --- /dev/null +++ b/cli/release_validate.php @@ -0,0 +1,178 @@ +#!/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_validate.php + * BRIEF: Pre-release validation — version consistency, required files, manifest checks + * + * Usage: + * php release_validate.php --path /repo --version 04.01.00 + * php release_validate.php --path /repo --version 04.01.00 --platform joomla --output-summary + * + * Options: + * --path Repository root (default: .) + * --version Expected version string (required) + * --platform joomla|dolibarr|generic (default: joomla) + * --output-summary Write markdown table to $GITHUB_STEP_SUMMARY + */ + +declare(strict_types=1); + +$path = '.'; +$version = null; +$platform = 'joomla'; +$outputSummary = false; + +foreach ($argv as $i => $arg) { + if ($arg === '--path' && isset($argv[$i + 1])) $path = $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 === '--output-summary') $outputSummary = true; +} + +if ($version === null) { + fwrite(STDERR, "Usage: release_validate.php --path . --version XX.YY.ZZ [--platform joomla]\n"); + exit(1); +} + +$root = realpath($path) ?: $path; +$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. README.md exists and contains VERSION +if (!file_exists("{$root}/README.md")) { + addResult('README.md', 'FAIL', 'Not found'); +} else { + $readme = file_get_contents("{$root}/README.md"); + if (preg_match('/VERSION:\s*' . preg_quote($version, '/') . '/', $readme) || + strpos($readme, $version) !== false) { + addResult('README.md version', 'PASS', "`{$version}` found"); + } else { + addResult('README.md version', 'FAIL', "`{$version}` not found in README.md"); + } +} + +// 2. CHANGELOG.md exists with matching section +if (!file_exists("{$root}/CHANGELOG.md")) { + addResult('CHANGELOG.md', 'WARN', 'Not found'); +} else { + $cl = file_get_contents("{$root}/CHANGELOG.md"); + if (preg_match('/^##\s.*' . preg_quote($version, '/') . '/m', $cl)) { + addResult('CHANGELOG.md version', 'PASS', "Section for `{$version}` found"); + } else { + addResult('CHANGELOG.md version', 'WARN', "No section header for `{$version}`"); + } +} + +// 3. LICENSE file exists +$licenseFound = false; +foreach (['LICENSE', 'LICENSE.md', 'LICENSE.txt', 'COPYING'] as $lf) { + if (file_exists("{$root}/{$lf}")) { $licenseFound = true; break; } +} +addResult('LICENSE', $licenseFound ? 'PASS' : 'FAIL', $licenseFound ? 'Found' : 'Not found'); + +// 4. Platform-specific checks +if ($platform === 'joomla') { + // Find XML manifest + $manifest = null; + $searchDirs = ["{$root}/src", $root]; + foreach ($searchDirs as $dir) { + if (!is_dir($dir)) continue; + foreach (glob("{$dir}/*.xml") as $xmlFile) { + $content = file_get_contents($xmlFile); + if (strpos($content, '([^<]+)<\/version>/', file_get_contents($manifest), $m)) { + $mVer = trim($m[1]); + if ($mVer === $version) { + addResult('Manifest version', 'PASS', "`{$mVer}` matches"); + } else { + addResult('Manifest version', 'FAIL', "`{$mVer}` != `{$version}`"); + } + } else { + addResult('Manifest version', 'FAIL', 'No tag in manifest'); + } + } + + // updates.xml + if (!file_exists("{$root}/updates.xml")) { + addResult('updates.xml', 'WARN', 'Not found'); + } else { + $ux = file_get_contents("{$root}/updates.xml"); + if (preg_match('/' . preg_quote($version, '/') . '<\/version>/', $ux)) { + addResult('updates.xml version', 'PASS', "`{$version}` found"); + } else { + addResult('updates.xml version', 'FAIL', "`{$version}` not in updates.xml"); + } + } +} elseif ($platform === 'dolibarr') { + $modFile = null; + foreach (['src', 'htdocs'] as $sd) { + $pattern = "{$root}/{$sd}/mod*.class.php"; + $matches = glob($pattern); + if (!empty($matches)) { $modFile = $matches[0]; break; } + } + if ($modFile === null) { + addResult('Dolibarr mod file', 'FAIL', 'No mod*.class.php found'); + } else { + $mc = file_get_contents($modFile); + if (preg_match("/\\\$this->version\s*=\s*'" . preg_quote($version, '/') . "'/", $mc)) { + addResult('Dolibarr version', 'PASS', "`{$version}` matches"); + } else { + addResult('Dolibarr version', 'FAIL', "`{$version}` not found in " . basename($modFile)); + } + } +} + +// 5. composer.json version (if present) +if (file_exists("{$root}/composer.json")) { + $composer = json_decode(file_get_contents("{$root}/composer.json"), true); + if (isset($composer['version'])) { + if ($composer['version'] === $version) { + addResult('composer.json version', 'PASS', "`{$version}` matches"); + } else { + addResult('composer.json version', 'WARN', "`{$composer['version']}` != `{$version}`"); + } + } +} + +// Output +$table = "| Check | Result | Details |\n|-------|--------|--------|\n"; +foreach ($results as $r) { + $table .= "| {$r['check']} | {$r['status']} | {$r['details']} |\n"; +} +$table .= "\n**Validation: {$pass} passed, {$fail} failed, {$warn} warnings**\n"; + +echo $table; + +if ($outputSummary) { + $summaryFile = getenv('GITHUB_STEP_SUMMARY'); + if ($summaryFile) { + file_put_contents($summaryFile, "### Pre-Release Validation\n\n{$table}\n", FILE_APPEND); + } +} + +exit($fail > 0 ? 1 : 0); diff --git a/cli/release_verify.php b/cli/release_verify.php new file mode 100644 index 0000000..7fe0363 --- /dev/null +++ b/cli/release_verify.php @@ -0,0 +1,188 @@ +#!/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); -- 2.52.0 From d9846b1c0141c4997ad97b0d7b55b221eac87a10 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 23 May 2026 22:47:49 -0500 Subject: [PATCH 2/2] fix: version_bump/read check manifest XML, use higher version as base Previously version_bump.php only read from README.md VERSION header, ignoring the actual manifest XML version. When a version was manually bumped in the manifest (e.g. 02.03.00) but README still showed an older version (02.01.45), the CLI would bump from README's version instead, producing wrong release numbers. Now both tools check README.md AND Joomla manifest XML files (pkg_*.xml, src/*.xml, packages/*/*.xml) and use whichever version is higher as the base for bumping/reading. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- cli/version_bump.php | 86 +++++++++++++++++++++++++++++++++++--------- cli/version_read.php | 61 ++++++++++++++++++++++++------- 2 files changed, 118 insertions(+), 29 deletions(-) diff --git a/cli/version_bump.php b/cli/version_bump.php index a7d155f..805e0a4 100644 --- a/cli/version_bump.php +++ b/cli/version_bump.php @@ -9,7 +9,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/version_bump.php - * BRIEF: Auto-increment patch version in README.md — outputs old → new + * BRIEF: Auto-increment patch version — checks both README.md and manifest XML, uses the higher version as base */ declare(strict_types=1); @@ -22,21 +22,69 @@ foreach ($argv as $i => $arg) { if ($arg === '--major') $type = 'major'; } -$readme = realpath($path) . '/README.md'; -if (!file_exists($readme)) { - fwrite(STDERR, "No README.md found at {$path}\n"); +$root = realpath($path) ?: $path; + +// ── Read version from README.md ────────────────────────────────────────────── +$readmeVersion = null; +$readme = "{$root}/README.md"; +$readmeContent = ''; +if (file_exists($readme)) { + $readmeContent = file_get_contents($readme); + if (preg_match('/^\s*VERSION:\s*(\d{2}\.\d{2}\.\d{2})/m', $readmeContent, $m)) { + $readmeVersion = $m[1]; + } +} + +// ── Read version from Joomla manifest XML ──────────────────────────────────── +$manifestVersion = null; + +// Check package manifest first (pkg_*.xml), then sub-extension manifests +$manifestFiles = array_merge( + glob("{$root}/src/pkg_*.xml") ?: [], + glob("{$root}/src/*.xml") ?: [], + glob("{$root}/src/packages/*/mokowaas.xml") ?: [], + glob("{$root}/src/packages/*/*.xml") ?: [], + glob("{$root}/*.xml") ?: [] +); + +foreach ($manifestFiles as $xmlFile) { + $xmlContent = file_get_contents($xmlFile); + if (strpos($xmlContent, '') === false) { + continue; + } + if (preg_match('|(\d{2}\.\d{2}\.\d{2})|', $xmlContent, $xm)) { + $candidate = $xm[1]; + if ($manifestVersion === null || version_compare($candidate, $manifestVersion, '>')) { + $manifestVersion = $candidate; + } + } +} + +// ── Use the higher version as base ─────────────────────────────────────────── +$baseVersion = null; + +if ($readmeVersion !== null && $manifestVersion !== null) { + $baseVersion = version_compare($manifestVersion, $readmeVersion, '>') ? $manifestVersion : $readmeVersion; +} elseif ($manifestVersion !== null) { + $baseVersion = $manifestVersion; +} elseif ($readmeVersion !== null) { + $baseVersion = $readmeVersion; +} + +if ($baseVersion === null) { + fwrite(STDERR, "No version found in README.md or manifest XML\n"); exit(1); } -$content = file_get_contents($readme); -if (!preg_match('/^(\s*VERSION:\s*)(\d{2})\.(\d{2})\.(\d{2})/m', $content, $m)) { - fwrite(STDERR, "No VERSION field found in README.md\n"); +// ── Parse and bump ─────────────────────────────────────────────────────────── +if (!preg_match('/^(\d{2})\.(\d{2})\.(\d{2})$/', $baseVersion, $parts)) { + fwrite(STDERR, "Invalid version format: {$baseVersion}\n"); exit(1); } -$major = (int)$m[2]; -$minor = (int)$m[3]; -$patch = (int)$m[4]; +$major = (int)$parts[1]; +$minor = (int)$parts[2]; +$patch = (int)$parts[3]; $old = sprintf('%02d.%02d.%02d', $major, $minor, $patch); switch ($type) { @@ -50,13 +98,17 @@ switch ($type) { } $new = sprintf('%02d.%02d.%02d', $major, $minor, $patch); -$updated = preg_replace( - '/^(\s*VERSION:\s*)\d{2}\.\d{2}\.\d{2}/m', - '${1}' . $new, - $content, - 1 -); -file_put_contents($readme, $updated); +// ── Update README.md ───────────────────────────────────────────────────────── +if (file_exists($readme) && !empty($readmeContent)) { + $updated = preg_replace( + '/^(\s*VERSION:\s*)\d{2}\.\d{2}\.\d{2}/m', + '${1}' . $new, + $readmeContent, + 1 + ); + file_put_contents($readme, $updated); +} + echo "{$old} → {$new}\n"; exit(0); diff --git a/cli/version_read.php b/cli/version_read.php index 0c0063b..16ae9da 100644 --- a/cli/version_read.php +++ b/cli/version_read.php @@ -9,7 +9,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/version_read.php - * BRIEF: Read VERSION from README.md — outputs just the version string + * BRIEF: Read version from README.md or manifest XML — outputs the higher of the two */ declare(strict_types=1); @@ -21,17 +21,54 @@ foreach ($argv as $i => $arg) { } } -$readme = realpath($path) . '/README.md'; -if (!file_exists($readme)) { - fwrite(STDERR, "No README.md found at {$path}\n"); +$root = realpath($path) ?: $path; + +// ── Read from README.md ────────────────────────────────────────────────────── +$readmeVersion = null; +$readme = "{$root}/README.md"; +if (file_exists($readme)) { + $content = file_get_contents($readme); + if (preg_match('/^\s*VERSION:\s*(\d{2}\.\d{2}\.\d{2})/m', $content, $m)) { + $readmeVersion = $m[1]; + } +} + +// ── Read from Joomla manifest XML ──────────────────────────────────────────── +$manifestVersion = null; +$manifestFiles = array_merge( + glob("{$root}/src/pkg_*.xml") ?: [], + glob("{$root}/src/*.xml") ?: [], + glob("{$root}/src/packages/*/*.xml") ?: [], + glob("{$root}/*.xml") ?: [] +); + +foreach ($manifestFiles as $xmlFile) { + $xmlContent = file_get_contents($xmlFile); + if (strpos($xmlContent, '') === false) { + continue; + } + if (preg_match('|(\d{2}\.\d{2}\.\d{2})|', $xmlContent, $xm)) { + $candidate = $xm[1]; + if ($manifestVersion === null || version_compare($candidate, $manifestVersion, '>')) { + $manifestVersion = $candidate; + } + } +} + +// ── Output the higher version ──────────────────────────────────────────────── +$version = null; +if ($readmeVersion !== null && $manifestVersion !== null) { + $version = version_compare($manifestVersion, $readmeVersion, '>') ? $manifestVersion : $readmeVersion; +} elseif ($manifestVersion !== null) { + $version = $manifestVersion; +} elseif ($readmeVersion !== null) { + $version = $readmeVersion; +} + +if ($version === null) { + fwrite(STDERR, "No version found in README.md or manifest XML\n"); exit(1); } -$content = file_get_contents($readme); -if (preg_match('/^\s*VERSION:\s*(\d{2}\.\d{2}\.\d{2})/m', $content, $m)) { - echo $m[1] . "\n"; - exit(0); -} - -fwrite(STDERR, "No VERSION field found in README.md\n"); -exit(1); +echo $version . "\n"; +exit(0); -- 2.52.0