#!/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 */ declare(strict_types=1); require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; use MokoEnterprise\CliFramework; class VersionBumpRemoteCli extends CliFramework { protected function configure(): void { $this->setDescription('Bump version in manifest XML and CHANGELOG.md on a remote branch via Gitea API'); $this->addArgument('--path', 'Repository root', '.'); $this->addArgument('--branch', 'Target branch to bump (required)', null); $this->addArgument('--bump', 'Bump type: patch | minor | major', 'minor'); $this->addArgument('--token', 'Gitea API token (or MOKOGITEA_TOKEN env var)', null); $this->addArgument('--api-base', 'Gitea API base URL for the repo', null); $this->addArgument('--no-changelog', 'Skip CHANGELOG.md bump', false); $this->addArgument('--repo', 'Repository path (owner/repo)', null); $this->addArgument('--gitea-url', 'Gitea instance URL', null); } protected function run(): int { $path = $this->getArgument('--path'); $branch = $this->getArgument('--branch'); $bumpType = $this->getArgument('--bump'); $token = $this->getArgument('--token'); $apiBase = $this->getArgument('--api-base'); $noChangelog = (bool) $this->getArgument('--no-changelog'); $repo = $this->getArgument('--repo'); $giteaUrl = $this->getArgument('--gitea-url'); if ($token === null) { $token = getenv('MOKOGITEA_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) { $this->log('ERROR', "Usage: version_bump_remote.php --branch BRANCH --token TOKEN --api-base URL [--bump minor|patch|major]"); return 1; } $root = realpath($path) ?: $path; $version = null; $manifestFile = null; foreach (["{$root}/src", $root] 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) { $this->log('ERROR', "No version found in manifest XML"); return 1; } if (!preg_match('/^(\d{2})\.(\d{2})\.(\d{2})$/', $version, $parts)) { $this->log('ERROR', "Invalid version format: {$version}"); return 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"; $manifestPaths = []; if ($manifestFile !== null) { $manifestPaths[] = "src/{$manifestFile}"; } $manifestPaths = array_merge($manifestPaths, ['src/templateDetails.xml', 'src/manifest.xml']); $manifestUpdated = false; foreach ($manifestPaths as $mPath) { $result = $this->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) { $this->log('WARN', "could not update manifest on {$branch}"); } if (!$noChangelog) { $this->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) { $header = "## [{$nextVersion}] - Unreleased\n\n" . "### Added\n\n### Changed\n\n" . "### Fixed\n\n"; $content = str_replace( $marker, $header . $marker, $content ); } } return $content; }, "chore(version): bump CHANGELOG {$version} -> {$nextVersion} [skip ci]"); } return 0; } private 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) ?: []; } private function updateRemoteFile( string $apiBase, string $token, string $filePath, string $branch, callable $transform, string $commitMessage ): bool { $file = $this->giteaApi('GET', "{$apiBase}/contents/{$filePath}?ref={$branch}", $token); if ($file === null || !isset($file['sha']) || !isset($file['content'])) { return false; } $content = base64_decode($file['content']); $newContent = $transform($content); if ($newContent === $content) { $this->log('INFO', "{$filePath}: no changes needed"); return true; } $payload = json_encode(['content' => base64_encode($newContent), 'sha' => $file['sha'], 'message' => $commitMessage, 'branch' => $branch]); $result = $this->giteaApi('PUT', "{$apiBase}/contents/{$filePath}", $token, $payload); if ($result === null) { $this->log('ERROR', "{$filePath}: failed to update"); return false; } echo " {$filePath}: updated on {$branch}\n"; return true; } } $app = new VersionBumpRemoteCli(); exit($app->execute());