#!/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: 09.21.07 * 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). */ declare(strict_types=1); require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; use MokoEnterprise\CliFramework; class UpdatesXmlSyncCli extends CliFramework { protected function configure(): void { $this->setDescription('Sync updates.xml to target branches via Gitea API'); $this->addArgument('--path', 'Repository root containing updates.xml', '.'); $this->addArgument('--branches', 'Comma-separated target branches to sync to', 'main,dev'); $this->addArgument('--all', 'Auto-discover all branches via Gitea API', false); $this->addArgument('--current', 'Current branch to skip (required)', ''); $this->addArgument('--version', 'Version string for commit message', ''); $this->addArgument('--token', 'Gitea API token', ''); $this->addArgument('--gitea-url', 'Gitea instance URL', ''); $this->addArgument('--org', 'Organization', ''); $this->addArgument('--repo', 'Repository name', ''); } protected function run(): int { $path = $this->getArgument('--path'); $branches = $this->getArgument('--branches'); $discoverAll = $this->getArgument('--all'); $current = $this->getArgument('--current'); $version = $this->getArgument('--version'); $token = $this->getArgument('--token'); $giteaUrl = $this->getArgument('--gitea-url'); $org = $this->getArgument('--org'); $repo = $this->getArgument('--repo'); // Fall back to environment variables if ($token === '') { $token = getenv('MOKOGITEA_TOKEN') ?: ''; } if ($giteaUrl === '') { $giteaUrl = getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech'; } if ($org === '') { $org = getenv('GITEA_ORG') ?: ''; } if ($repo === '') { $repo = getenv('GITEA_REPO') ?: ''; } if ($current === '') { $this->log('ERROR', '--current is required'); return 1; } if ($token === '') { $this->log('ERROR', '--token or MOKOGITEA_TOKEN env is required'); return 1; } if ($org === '' || $repo === '') { $this->log('ERROR', '--org and --repo (or GITEA_ORG/GITEA_REPO env) are required'); return 1; } // Auto-discover branches if --all flag is set if ($discoverAll) { $apiUrl = "{$giteaUrl}/api/v1/repos/{$org}/{$repo}/branches?limit=50"; $ch = curl_init($apiUrl); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: token {$token}", 'Accept: application/json'], CURLOPT_TIMEOUT => 15, ]); $response = curl_exec($ch); curl_close($ch); $branchList = json_decode($response ?: '[]', true) ?: []; $discovered = []; foreach ($branchList as $b) { $name = $b['name'] ?? ''; if ($name !== '' && $name !== $current && !str_starts_with($name, 'version/') && !str_starts_with($name, 'feature/') && !str_starts_with($name, 'patch/') ) { $discovered[] = $name; } } if (!empty($discovered)) { $branches = implode(',', $discovered); echo "Discovered branches: {$branches}\n"; } } $updatesFile = rtrim($path, '/') . '/updates.xml'; if (!file_exists($updatesFile)) { $this->log('ERROR', "No updates.xml found at {$updatesFile}"); return 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)) { $this->log('ERROR', "No target branches to sync to (current: {$current})"); return 0; } $synced = 0; $failed = 0; foreach ($targets as $branch) { $this->log('INFO', "Syncing updates.xml -> {$branch}..."); $sha = $this->getFileSha($apiBase, $token, $branch); if ($sha === null) { $this->warning("could not get SHA from {$branch}"); $failed++; continue; } $ok = $this->putFile($apiBase, $token, $branch, $encoded, $sha, "chore: sync updates.xml{$vLabel} from {$current} [skip ci]"); if ($ok) { $this->log('INFO', "Synced to {$branch}"); $synced++; } else { $this->warning("push to {$branch} failed"); $failed++; } } $this->log('INFO', "Done: {$synced} synced, {$failed} failed"); return $failed > 0 ? 1 : 0; } private function getFileSha(string $apiBase, string $token, string $branch): ?string { $resp = $this->apiCall('GET', "{$apiBase}/contents/updates.xml?ref={$branch}", $token); return $resp['sha'] ?? null; } private function putFile(string $apiBase, string $token, string $branch, string $encoded, string $sha, string $msg): bool { $resp = $this->apiCall('PUT', "{$apiBase}/contents/updates.xml", $token, [ 'content' => $encoded, 'sha' => $sha, 'message' => $msg, 'branch' => $branch, ]); return $resp !== null; } private 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; } } $app = new UpdatesXmlSyncCli(); exit($app->execute());