From e03b29983afa645f18ecbc7f6dae7f5f64a3e6a0 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 24 May 2026 03:46:37 -0500 Subject: [PATCH 1/5] fix: updates_xml_build preserves existing channel entries When building a dev release, the CLI was overwriting the entire updates.xml with only the dev entry, wiping the stable channel. Now reads existing entries and preserves channels not being updated. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- cli/updates_xml_build.php | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/cli/updates_xml_build.php b/cli/updates_xml_build.php index bfebc69..76ba235 100644 --- a/cli/updates_xml_build.php +++ b/cli/updates_xml_build.php @@ -316,6 +316,32 @@ for ($i = 0; $i <= $stabilityIndex; $i++) { ); } +// -- Preserve existing entries for channels not being updated ----------------- +$dest = $outputFile ?? "{$root}/updates.xml"; +$preservedEntries = []; + +if (file_exists($dest)) { + $existingXml = @simplexml_load_file($dest); + if ($existingXml) { + // Channels we're writing — don't preserve these + $writtenChannels = []; + for ($i = 0; $i <= $stabilityIndex; $i++) { + $writtenChannels[] = $allChannels[$i]; + } + + foreach ($existingXml->update as $existingUpdate) { + $existingTag = ''; + if (isset($existingUpdate->tags->tag)) { + $existingTag = (string) $existingUpdate->tags->tag; + } + // Keep entries for channels we're NOT overwriting + if (!empty($existingTag) && !in_array($existingTag, $writtenChannels, true)) { + $preservedEntries[] = ' ' . trim($existingUpdate->asXML()); + } + } + } +} + // -- Write updates.xml -------------------------------------------------------- $year = date('Y'); $output = << XML; -$output .= "\n" . implode("\n", $entries) . "\n\n"; +$allEntries = array_merge($preservedEntries, $entries); +$output .= "\n" . implode("\n", $allEntries) . "\n\n"; $dest = $outputFile ?? "{$root}/updates.xml"; file_put_contents($dest, $output); From bd2799c76130421950053648a28b7ebd5186eec8 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 24 May 2026 04:02:33 -0500 Subject: [PATCH 2/5] fix: targetplatform regex simplified to avoid Gitea XML parse errors The complex regex ((5.[0-9])|(6.[0-9])) caused Gitea's web view to return 500 when rendering the XML. Simplified to (5|6)\..* which is Joomla-compatible and XML-safe. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- cli/updates_xml_build.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/updates_xml_build.php b/cli/updates_xml_build.php index 76ba235..bbeb53d 100644 --- a/cli/updates_xml_build.php +++ b/cli/updates_xml_build.php @@ -126,7 +126,7 @@ if (preg_match('/]*group="([^"]+)"/', $xml, $m)) $extFolder = $m[1] $targetPlatform = ''; if (preg_match('/()/', $xml, $m)) $targetPlatform = $m[1]; if (empty($targetPlatform)) { - $targetPlatform = ''; + $targetPlatform = ''; } $phpMinimum = ''; From a888b6c9c765c94144201b3928abe254ab74e4ea Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 24 May 2026 09:12:23 +0000 Subject: [PATCH 3/5] feat(cli): add version_bump_remote.php for API-based version bumping Closes #80 --- cli/version_bump_remote.php | 233 ++++++++++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 cli/version_bump_remote.php diff --git a/cli/version_bump_remote.php b/cli/version_bump_remote.php new file mode 100644 index 0000000..557d66a --- /dev/null +++ b/cli/version_bump_remote.php @@ -0,0 +1,233 @@ +#!/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/version_bump_remote.php + * BRIEF: Bump version in manifest XML and CHANGELOG.md on a remote branch via Gitea API + * + * Usage: + * php version_bump_remote.php --path . --branch dev --bump minor --token TOKEN --api-base URL + * php version_bump_remote.php --path . --branch dev --bump patch --token TOKEN --api-base URL + * php version_bump_remote.php --path . --branch dev --bump minor --no-changelog --token TOKEN --api-base URL + * + * Options: + * --path Repository root (reads current version from local manifest) + * --branch Target branch to bump (required, e.g. dev) + * --bump Bump type: patch | minor | major (default: minor) + * --token Gitea API token (or GA_TOKEN env var) + * --api-base Gitea API base URL for the repo + * --no-changelog Skip CHANGELOG.md bump + * --repo Repository path (owner/repo) for API base construction + * --gitea-url Gitea instance URL (default: env GITEA_URL) + */ + +declare(strict_types=1); + +$path = '.'; +$branch = null; +$bumpType = 'minor'; +$token = null; +$apiBase = null; +$noChangelog = false; +$repo = null; +$giteaUrl = null; + +foreach ($argv as $i => $arg) { + if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1]; + if ($arg === '--branch' && isset($argv[$i + 1])) $branch = $argv[$i + 1]; + if ($arg === '--bump' && isset($argv[$i + 1])) $bumpType = $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 === '--no-changelog') $noChangelog = true; + if ($arg === '--repo' && isset($argv[$i + 1])) $repo = $argv[$i + 1]; + if ($arg === '--gitea-url' && isset($argv[$i + 1])) $giteaUrl = $argv[$i + 1]; +} + +if ($token === null) $token = getenv('GA_TOKEN') ?: getenv('GITEA_TOKEN') ?: null; +if ($giteaUrl === null) $giteaUrl = getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech'; + +if ($apiBase === null && $repo !== null) { + $apiBase = rtrim($giteaUrl, '/') . '/api/v1/repos/' . $repo; +} + +if ($branch === null || $token === null || $apiBase === null) { + fwrite(STDERR, "Usage: version_bump_remote.php --branch BRANCH --token TOKEN --api-base URL [--bump minor|patch|major]\n"); + fwrite(STDERR, " or: version_bump_remote.php --branch BRANCH --token TOKEN --repo owner/repo\n"); + exit(1); +} + +$root = realpath($path) ?: $path; + +// ── Read current version from local manifest ──────────────────────────── +$version = null; +$manifestFile = 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 (strpos($xml, '') !== false) { + if (preg_match('|(\d{2}\.\d{2}\.\d{2})|', $xml, $m)) { + if ($version === null || version_compare($m[1], $version, '>')) { + $version = $m[1]; + $manifestFile = basename($f); + } + } + } + } +} + +if ($version === null) { + fwrite(STDERR, "No version found in manifest XML\n"); + exit(1); +} + +// ── Compute next version ──────────────────────────────────────────────── +if (!preg_match('/^(\d{2})\.(\d{2})\.(\d{2})$/', $version, $parts)) { + fwrite(STDERR, "Invalid version format: {$version}\n"); + exit(1); +} + +$major = (int)$parts[1]; +$minor = (int)$parts[2]; +$patch = (int)$parts[3]; + +switch ($bumpType) { + case 'major': $major++; $minor = 0; $patch = 0; break; + case 'minor': $minor++; $patch = 0; break; + default: $patch++; break; +} + +$nextVersion = sprintf('%02d.%02d.%02d', $major, $minor, $patch); +echo "{$version} -> {$nextVersion} ({$branch})\n"; + +// ── Helper: Gitea API request ─────────────────────────────────────────── +function giteaApi(string $method, string $url, string $token, ?string $body = null): ?array +{ + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => [ + "Authorization: token {$token}", + 'Content-Type: application/json', + ], + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_TIMEOUT => 30, + ]); + if ($body !== null) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + } + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode >= 400 || $response === false) { + return null; + } + return json_decode($response, true) ?: []; +} + +// ── Helper: Update a file on a remote branch ──────────────────────────── +function updateRemoteFile( + string $apiBase, + string $token, + string $filePath, + string $branch, + callable $transform, + string $commitMessage +): bool { + $url = "{$apiBase}/contents/{$filePath}?ref={$branch}"; + $file = giteaApi('GET', $url, $token); + if ($file === null || !isset($file['sha']) || !isset($file['content'])) { + return false; + } + + $content = base64_decode($file['content']); + $newContent = $transform($content); + + if ($newContent === $content) { + fwrite(STDERR, " {$filePath}: no changes needed\n"); + return true; + } + + $payload = json_encode([ + 'content' => base64_encode($newContent), + 'sha' => $file['sha'], + 'message' => $commitMessage, + 'branch' => $branch, + ]); + + $result = giteaApi('PUT', "{$apiBase}/contents/{$filePath}", $token, $payload); + if ($result === null) { + fwrite(STDERR, " {$filePath}: failed to update\n"); + return false; + } + + echo " {$filePath}: updated on {$branch}\n"; + return true; +} + +// ── Update manifest XML on the remote branch ──────────────────────────── +$manifestPaths = []; +if ($manifestFile !== null) { + $manifestPaths[] = "src/{$manifestFile}"; +} +$manifestPaths = array_merge($manifestPaths, [ + 'src/templateDetails.xml', + 'src/manifest.xml', +]); + +$manifestUpdated = false; +foreach ($manifestPaths as $mPath) { + $result = updateRemoteFile( + $apiBase, $token, $mPath, $branch, + function (string $content) use ($version, $nextVersion): string { + return str_replace( + "{$version}", + "{$nextVersion}", + $content + ); + }, + "chore(version): bump {$version} -> {$nextVersion} [skip ci]" + ); + if ($result) { + $manifestUpdated = true; + break; + } +} + +if (!$manifestUpdated) { + fwrite(STDERR, "WARNING: could not update manifest on {$branch}\n"); +} + +// ── Update CHANGELOG.md on the remote branch ──────────────────────────── +if (!$noChangelog) { + updateRemoteFile( + $apiBase, $token, 'CHANGELOG.md', $branch, + function (string $content) use ($version, $nextVersion): string { + $content = str_replace("VERSION: {$version}", "VERSION: {$nextVersion}", $content); + + if (strpos($content, '[Unreleased]') === false + && strpos($content, "## [{$nextVersion}]") === false + ) { + $marker = "## [{$version}]"; + if (strpos($content, $marker) !== false) { + $unreleased = "## [{$nextVersion}] - Unreleased\n\n### Added\n\n### Changed\n\n### Fixed\n\n"; + $content = str_replace($marker, $unreleased . $marker, $content); + } + } + + return $content; + }, + "chore(version): bump CHANGELOG {$version} -> {$nextVersion} [skip ci]" + ); +} + +exit(0); From ac8c22f183ce0cdb993bbcc30b2814a7d6b60fcf Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Sun, 24 May 2026 22:54:43 +0000 Subject: [PATCH 4/5] Add RC pre-release trigger to PR check workflow Automatically triggers a release-candidate build when a PR passes branch policy and validation checks. Authored-by: Moko Consulting --- .mokogitea/workflows/pr-check.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index bc1a001..b045d2f 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -194,3 +194,21 @@ jobs: FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) echo "Source: ${FILE_COUNT} files" [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } + + # ── Pre-Release RC Build ───────────────────────────────────────────────── + pre-release: + name: Build RC Package + runs-on: ubuntu-latest + needs: [branch-policy, validate] + + steps: + - name: Trigger RC pre-release + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + REPO: ${{ github.repository }} + BRANCH: ${{ github.head_ref }} + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" + echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY + echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY From 63b0baceede733d28e4aa0fef3b3b094311bd5e5 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 24 May 2026 23:03:19 -0500 Subject: [PATCH 5/5] fix: updates_xml_build writes only the current channel entry Was cascading entries for all lower channels on stable release, producing wrong download URLs for non-existent channel releases. Now writes only the entry for the current stability level; the preserve logic retains entries from other channels. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- cli/updates_xml_build.php | 45 +++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/cli/updates_xml_build.php b/cli/updates_xml_build.php index bbeb53d..7d42617 100644 --- a/cli/updates_xml_build.php +++ b/cli/updates_xml_build.php @@ -290,31 +290,30 @@ $allChannels = ['development', 'alpha', 'beta', 'rc', 'stable']; $stabilityIndex = array_search($stability === 'development' ? 'development' : $stability, $allChannels); if ($stabilityIndex === false) $stabilityIndex = 4; // default to stable -// Write entries for this stability and all below it +// Write only the current channel entry (not cascade) +// Each channel release only creates its own entry; preserved entries handle other channels $entries = []; -for ($i = 0; $i <= $stabilityIndex; $i++) { - $channelName = $allChannels[$i]; - $channelSuffix = $stabilitySuffixMap[$channelName] ?? ''; - $channelVersion = $version . $channelSuffix; - $channelTag = $stabilityTagMap[$channelName] ?? $channelName; - $channelDownloadUrl = "{$giteaUrl}/{$org}/{$repo}/releases/download/{$channelTag}/{$typePrefix}{$extElement}-{$channelVersion}.zip"; - $channelInfoUrl = "{$giteaUrl}/{$org}/{$repo}/releases/tag/{$channelTag}"; +$channelName = $allChannels[$stabilityIndex]; +$channelSuffix = $stabilitySuffixMap[$channelName] ?? ''; +$channelVersion = $version . $channelSuffix; +$channelTag = $stabilityTagMap[$channelName] ?? $channelName; +$channelDownloadUrl = "{$giteaUrl}/{$org}/{$repo}/releases/download/{$channelTag}/{$typePrefix}{$extElement}-{$channelVersion}.zip"; +$channelInfoUrl = "{$giteaUrl}/{$org}/{$repo}/releases/tag/{$channelTag}"; - $entries[] = buildEntry( - $channelName, - $channelVersion, - $channelDownloadUrl, - $extName, - $extElement, - $extType, - $clientTag, - $folderTag, - $channelInfoUrl, - $targetPlatform, - $phpTag, - $shaTag - ); -} +$entries[] = buildEntry( + $channelName, + $channelVersion, + $channelDownloadUrl, + $extName, + $extElement, + $extType, + $clientTag, + $folderTag, + $channelInfoUrl, + $targetPlatform, + $phpTag, + $shaTag +); // -- Preserve existing entries for channels not being updated ----------------- $dest = $outputFile ?? "{$root}/updates.xml";