From 4dfbcf4fd2068cf14ba8b5289fefd4f1cf5a22df Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 21 May 2026 21:38:54 -0500 Subject: [PATCH] =?UTF-8?q?feat(cli):=20add=20updates=5Fxml=5Fsync.php=20?= =?UTF-8?q?=E2=80=94=20replaces=20inline=20workflow=20sync=20(#34)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New CLI tool syncs updates.xml to target branches via Gitea API. Pre-release workflow now calls the CLI instead of inline bash. Co-Authored-By: Claude Opus 4.6 (1M context) --- .mokogitea/workflows/pre-release.yml | 23 +--- cli/updates_xml_sync.php | 169 +++++++++++++++++++++++++++ 2 files changed, 170 insertions(+), 22 deletions(-) create mode 100644 cli/updates_xml_sync.php diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index 623bb57..c872554 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -335,28 +335,7 @@ jobs: - name: "Sync updates.xml to all branches" if: steps.platform.outputs.platform == 'joomla' run: | - CURRENT_BRANCH="${{ github.ref_name }}" - TOKEN="${{ secrets.GA_TOKEN }}" - API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - VERSION="${{ steps.meta.outputs.version }}" - - # Sync updates.xml to main and dev via API (avoids git checkout conflicts) - for BRANCH in main dev; do - [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue - - echo "Syncing updates.xml -> ${BRANCH}" - - FILE_SHA=$(curl -sf -H "Authorization: token ${TOKEN}" "${API}/contents/updates.xml?ref=${BRANCH}" | jq -r '.sha // empty' 2>/dev/null || true) - - if [ -z "$FILE_SHA" ]; then - echo " WARNING: could not get updates.xml SHA from ${BRANCH}" - continue - fi - - CONTENT=$(base64 -w0 updates.xml) - curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API}/contents/updates.xml" -d "$(jq -n --arg content \"$CONTENT\" --arg sha \"$FILE_SHA\" --arg msg \"chore: sync updates.xml ${VERSION} from ${CURRENT_BRANCH} [skip ci]\" --arg branch \"$BRANCH\" '{content: $content, sha: $sha, message: $msg, branch: $branch}' - )" > /dev/null 2>&1 && echo " Synced to ${BRANCH}" || echo " WARNING: push to ${BRANCH} failed" - done + php /tmp/moko-platform-api/cli/updates_xml_sync.php --path . --current "${{ github.ref_name }}" --branches main,dev --version "${{ steps.meta.outputs.version }}" --token "${{ secrets.GA_TOKEN }}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" --gitea-url "${GITEA_URL}" - name: "Delete lesser pre-release channels (cascade)" continue-on-error: true diff --git a/cli/updates_xml_sync.php b/cli/updates_xml_sync.php new file mode 100644 index 0000000..46efbc1 --- /dev/null +++ b/cli/updates_xml_sync.php @@ -0,0 +1,169 @@ +#!/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/updates_xml_sync.php + * VERSION: 05.00.01 + * BRIEF: Sync updates.xml to target branches via Gitea API + * NOTE: Called by pre-release and auto-release workflows after updates.xml + * is modified on the current branch. Pushes the file to other branches + * without requiring a git checkout (avoids merge conflicts). + * + * Usage: + * php updates_xml_sync.php --path /repo --branches main,dev --current dev + * php updates_xml_sync.php --path /repo --branches main --current dev --version 02.01.27 + * + * Options: + * --path Repository root containing updates.xml (default: .) + * --branches Comma-separated target branches to sync to (default: main,dev) + * --current Current branch to skip (required) + * --version Version string for commit message (optional) + * --token Gitea API token (default: env GA_TOKEN) + * --gitea-url Gitea instance URL (default: env GITEA_URL or https://git.mokoconsulting.tech) + * --org Organization (default: env GITEA_ORG) + * --repo Repository name (default: env GITEA_REPO) + */ + +declare(strict_types=1); + +// ── Argument parsing ──────────────────────────────────────────────────── +$path = '.'; +$branches = 'main,dev'; +$current = ''; +$version = ''; +$token = getenv('GA_TOKEN') ?: ''; +$giteaUrl = getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech'; +$org = getenv('GITEA_ORG') ?: ''; +$repo = getenv('GITEA_REPO') ?: ''; + +foreach ($argv as $i => $arg) { + if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1]; + if ($arg === '--branches' && isset($argv[$i + 1])) $branches = $argv[$i + 1]; + if ($arg === '--current' && isset($argv[$i + 1])) $current = $argv[$i + 1]; + if ($arg === '--version' && isset($argv[$i + 1])) $version = $argv[$i + 1]; + if ($arg === '--token' && isset($argv[$i + 1])) $token = $argv[$i + 1]; + if ($arg === '--gitea-url' && isset($argv[$i + 1])) $giteaUrl = $argv[$i + 1]; + if ($arg === '--org' && isset($argv[$i + 1])) $org = $argv[$i + 1]; + if ($arg === '--repo' && isset($argv[$i + 1])) $repo = $argv[$i + 1]; +} + +if ($current === '') { + fwrite(STDERR, "Error: --current is required\n"); + exit(1); +} + +if ($token === '') { + fwrite(STDERR, "Error: --token or GA_TOKEN env is required\n"); + exit(1); +} + +if ($org === '' || $repo === '') { + fwrite(STDERR, "Error: --org and --repo (or GITEA_ORG/GITEA_REPO env) are required\n"); + exit(1); +} + +$updatesFile = rtrim($path, '/') . '/updates.xml'; +if (!file_exists($updatesFile)) { + fwrite(STDERR, "No updates.xml found at {$updatesFile}\n"); + exit(0); +} + +$content = file_get_contents($updatesFile); +$encoded = base64_encode($content); +$giteaUrl = rtrim($giteaUrl, '/'); +$apiBase = "{$giteaUrl}/api/v1/repos/{$org}/{$repo}"; +$vLabel = $version !== '' ? " {$version}" : ''; + +$targets = array_filter( + array_map('trim', explode(',', $branches)), + fn($b) => $b !== '' && $b !== $current +); + +if (empty($targets)) { + fwrite(STDERR, "No target branches to sync to (current: {$current})\n"); + exit(0); +} + +$synced = 0; +$failed = 0; + +foreach ($targets as $branch) { + fwrite(STDERR, "Syncing updates.xml -> {$branch}...\n"); + + $sha = getFileSha($apiBase, $token, $branch); + + if ($sha === null) { + fwrite(STDERR, " WARNING: could not get SHA from {$branch}\n"); + $failed++; + continue; + } + + $ok = putFile($apiBase, $token, $branch, $encoded, $sha, + "chore: sync updates.xml{$vLabel} from {$current} [skip ci]"); + + if ($ok) { + fwrite(STDERR, " Synced to {$branch}\n"); + $synced++; + } else { + fwrite(STDERR, " WARNING: push to {$branch} failed\n"); + $failed++; + } +} + +fwrite(STDERR, "Done: {$synced} synced, {$failed} failed\n"); +exit($failed > 0 ? 1 : 0); + +// ═══════════════════════════════════════════════════════════════════════ + +function getFileSha(string $apiBase, string $token, string $branch): ?string +{ + $resp = apiCall('GET', "{$apiBase}/contents/updates.xml?ref={$branch}", $token); + return $resp['sha'] ?? null; +} + +function putFile(string $apiBase, string $token, string $branch, + string $encoded, string $sha, string $msg): bool +{ + $resp = apiCall('PUT', "{$apiBase}/contents/updates.xml", $token, [ + 'content' => $encoded, + 'sha' => $sha, + 'message' => $msg, + 'branch' => $branch, + ]); + return $resp !== null; +} + +function apiCall(string $method, string $url, string $token, ?array $data = null): ?array +{ + $headers = [ + "Authorization: token {$token}", + 'Content-Type: application/json', + 'Accept: application/json', + ]; + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + + if ($data !== null) { + curl_setopt($ch, CURLOPT_POSTFIELDS, + json_encode($data, JSON_UNESCAPED_SLASHES)); + } + + $body = curl_exec($ch); + $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + return ($code >= 200 && $code < 300) + ? (json_decode($body, true) ?: []) + : null; +}