#!/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_mirror.php * BRIEF: Mirror a Gitea release (with assets) to a GitHub repository * * Usage: * php release_mirror.php --version 09.01.00 --tag stable --token TOKEN --api-base URL \ * --gh-token GH_TOKEN --gh-repo MokoConsulting/MokoWaaS * * Mirrors a Gitea release (title, body, assets) to a corresponding GitHub release. * If the GitHub release already exists at the same tag, its title is updated via PATCH. * All assets from the Gitea release are downloaded and uploaded to the GitHub release. */ declare(strict_types=1); // ── Argument parsing ───────────────────────────────────────────────────────── $version = null; $tag = null; $token = null; $apiBase = null; $ghToken = null; $ghRepo = null; $branch = 'main'; foreach ($argv as $i => $arg) { if ($arg === '--version' && isset($argv[$i + 1])) { $version = $argv[$i + 1]; } if ($arg === '--tag' && isset($argv[$i + 1])) { $tag = $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 === '--gh-token' && isset($argv[$i + 1])) { $ghToken = $argv[$i + 1]; } if ($arg === '--gh-repo' && isset($argv[$i + 1])) { $ghRepo = $argv[$i + 1]; } if ($arg === '--branch' && isset($argv[$i + 1])) { $branch = $argv[$i + 1]; } } // Allow tokens from environment $token = $token ?: (getenv('GA_TOKEN') ?: (getenv('GITEA_TOKEN') ?: null)); $ghToken = $ghToken ?: (getenv('GH_TOKEN') ?: null); if ( $version === null || $tag === null || $token === null || $apiBase === null || $ghToken === null || $ghRepo === null ) { fwrite(STDERR, "Usage: release_mirror.php --version VER --tag TAG --token TOKEN " . "--api-base URL --gh-token GH_TOKEN --gh-repo org/repo [--branch main]\n"); fwrite(STDERR, " --token: Gitea token (or GA_TOKEN / GITEA_TOKEN env)\n"); fwrite(STDERR, " --gh-token: GitHub token (or GH_TOKEN env)\n"); exit(1); } // ── Helper: Gitea API request ──────────────────────────────────────────────── /** * Send a request to the Gitea API. * * @param string $url Full Gitea API URL * @param string $token Gitea API token * @param string $method HTTP method (GET, POST, PATCH, DELETE) * @param string|null $body JSON request body or null * * @return array|null Decoded response or null on failure */ function giteaApi(string $url, string $token, string $method = 'GET', ?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_TIMEOUT => 30, CURLOPT_CUSTOMREQUEST => $method, ]); 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 < 200 || $httpCode >= 300 || empty($response)) { return null; } return json_decode($response, true) ?: null; } /** * Download a file from Gitea to a local path. * * @param string $url Download URL * @param string $token Gitea API token * @param string $dest Local destination path * * @return bool True on success */ function giteaDownload(string $url, string $token, string $dest): bool { $ch = curl_init($url); $fp = fopen($dest, 'wb'); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ["Authorization: token {$token}"], CURLOPT_FILE => $fp, CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => 120, ]); curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); fclose($fp); return $httpCode >= 200 && $httpCode < 300; } /** * Send a request to the GitHub API. * * @param string $url Full GitHub API URL * @param string $token GitHub personal access token * @param string $method HTTP method (GET, POST, PATCH, DELETE) * @param string|null $body JSON request body or null * * @return array|null Decoded response or null on failure */ function githubApi(string $url, string $token, string $method = 'GET', ?string $body = null): ?array { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: token {$token}", 'Accept: application/vnd.github+json', 'User-Agent: moko-platform', 'Content-Type: application/json', ], CURLOPT_TIMEOUT => 30, CURLOPT_CUSTOMREQUEST => $method, ]); 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 < 200 || $httpCode >= 300 || empty($response)) { return null; } return json_decode($response, true) ?: null; } /** * Upload a binary asset to a GitHub release. * * @param string $uploadUrl GitHub upload URL (uploads.github.com) * @param string $token GitHub personal access token * @param string $filePath Local file path to upload * @param string $name Asset filename for GitHub * * @return int HTTP status code */ function githubUploadAsset(string $uploadUrl, string $token, string $filePath, string $name): int { $url = $uploadUrl . '?name=' . urlencode($name); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ "Authorization: token {$token}", 'Accept: application/vnd.github+json', 'User-Agent: moko-platform', 'Content-Type: application/octet-stream', ], CURLOPT_POSTFIELDS => file_get_contents($filePath), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 120, ]); curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); return $httpCode; } // ── Step 1: Get Gitea release by tag ───────────────────────────────────────── echo "Fetching Gitea release: {$tag}\n"; $giteaRelease = giteaApi("{$apiBase}/releases/tags/{$tag}", $token); if (!$giteaRelease || empty($giteaRelease['id'])) { fwrite(STDERR, "No Gitea release found with tag: {$tag}\n"); exit(1); } $giteaId = $giteaRelease['id']; $releaseName = $giteaRelease['name'] ?? "{$version}"; $releaseBody = $giteaRelease['body'] ?? ''; $assets = $giteaRelease['assets'] ?? []; echo " Name: {$releaseName}\n"; echo " Assets: " . count($assets) . " file(s)\n"; // ── Step 2: Check / create GitHub release ──────────────────────────────────── $ghApiBase = "https://api.github.com/repos/{$ghRepo}"; $ghUploadBase = "https://uploads.github.com/repos/{$ghRepo}"; echo "Checking GitHub release: {$tag}\n"; $ghRelease = githubApi("{$ghApiBase}/releases/tags/{$tag}", $ghToken); if ($ghRelease && !empty($ghRelease['id'])) { // Update existing release title $ghReleaseId = $ghRelease['id']; echo " GitHub release exists (id: {$ghReleaseId}), updating title\n"; $patchPayload = json_encode([ 'name' => $releaseName, 'body' => $releaseBody, ]); githubApi("{$ghApiBase}/releases/{$ghReleaseId}", $ghToken, 'PATCH', $patchPayload); } else { // Create new release echo " Creating GitHub release\n"; $createPayload = json_encode([ 'tag_name' => $tag, 'target_commitish' => $branch, 'name' => $releaseName, 'body' => $releaseBody, 'draft' => false, 'prerelease' => ($tag !== 'stable'), ]); $ghRelease = githubApi("{$ghApiBase}/releases", $ghToken, 'POST', $createPayload); if (!$ghRelease || empty($ghRelease['id'])) { fwrite(STDERR, "Failed to create GitHub release\n"); exit(1); } $ghReleaseId = $ghRelease['id']; echo " Created GitHub release (id: {$ghReleaseId})\n"; } // ── Step 3: Download assets from Gitea ─────────────────────────────────────── $tmpDir = sys_get_temp_dir() . '/moko-mirror-' . getmypid(); @mkdir($tmpDir, 0755, true); $uploadUrl = "{$ghUploadBase}/releases/{$ghReleaseId}/assets"; foreach ($assets as $asset) { $name = $asset['name'] ?? ''; $downloadUrl = $asset['browser_download_url'] ?? ''; if ($name === '' || $downloadUrl === '') { continue; } $localPath = "{$tmpDir}/{$name}"; echo " Downloading: {$name}\n"; if (!giteaDownload($downloadUrl, $token, $localPath)) { fwrite(STDERR, " Failed to download: {$name}\n"); continue; } // ── Step 4: Upload asset to GitHub ─────────────────────────────────────── echo " Uploading: {$name}\n"; $code = githubUploadAsset($uploadUrl, $ghToken, $localPath, $name); $status = ($code >= 200 && $code < 300) ? 'OK' : "FAILED ({$code})"; echo " {$status}\n"; } // ── Cleanup ────────────────────────────────────────────────────────────────── array_map('unlink', glob("{$tmpDir}/*") ?: []); @rmdir($tmpDir); // ── Summary ────────────────────────────────────────────────────────────────── echo "\nMirror complete: {$tag} -> github.com/{$ghRepo}\n"; echo " Version: {$version}\n"; echo " Assets: " . count($assets) . " file(s)\n"; exit(0);