#!/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_promote.php * BRIEF: Promote a Gitea release from one channel to another (rename release, tag, assets) * * Usage: * php release_promote.php --from development --to release-candidate --token TOKEN --api-base URL * php release_promote.php --from release-candidate --to stable --token TOKEN --api-base URL --path . * * When promoting to stable, --path detects extension type prefix for asset renaming. * When --from is "auto", checks beta > alpha > development and uses the first found. */ declare(strict_types=1); $from = null; $to = null; $token = null; $apiBase = null; $path = '.'; $branch = 'main'; foreach ($argv as $i => $arg) { if ($arg === '--from' && isset($argv[$i + 1])) { $from = $argv[$i + 1]; } if ($arg === '--to' && isset($argv[$i + 1])) { $to = $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 === '--path' && isset($argv[$i + 1])) { $path = $argv[$i + 1]; } if ($arg === '--branch' && isset($argv[$i + 1])) { $branch = $argv[$i + 1]; } } $token = $token ?: (getenv('GA_TOKEN') ?: (getenv('GITEA_TOKEN') ?: null)); if ($to === null || $token === null || $apiBase === null) { fwrite(STDERR, "Usage: release_promote.php --from --to --token TOKEN --api-base URL [--path .]\n"); fwrite(STDERR, " --from auto: checks beta > alpha > development\n"); exit(1); } // ── Suffix maps ────────────────────────────────────────────────────────────── $suffixMap = [ 'development' => '-dev', 'alpha' => '-alpha', 'beta' => '-beta', 'release-candidate' => '-rc', 'stable' => '', ]; // ── Channel hierarchy (highest first) ──────────────────────────────────────── $channelOrder = ['beta', 'alpha', 'development']; // ── Helper: Gitea API request ──────────────────────────────────────────────── /** @return array|null */ 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; } 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; } // ── Resolve --from auto ────────────────────────────────────────────────────── if ($from === 'auto') { foreach ($channelOrder as $candidate) { $data = giteaApi("{$apiBase}/releases/tags/{$candidate}", $token); if ($data && !empty($data['id'])) { $from = $candidate; echo "Auto-detected source channel: {$from}\n"; break; } } if ($from === 'auto') { echo "No pre-release found to promote\n"; exit(0); } } // ── Find source release ────────────────────────────────────────────────────── $sourceRelease = giteaApi("{$apiBase}/releases/tags/{$from}", $token); if (!$sourceRelease || empty($sourceRelease['id'])) { fwrite(STDERR, "No release found with tag: {$from}\n"); exit(1); } $sourceId = $sourceRelease['id']; $sourceName = $sourceRelease['name'] ?? ''; $sourceBody = $sourceRelease['body'] ?? ''; echo "Source: {$from} (id: {$sourceId}) — {$sourceName}\n"; // ── Get source assets ──────────────────────────────────────────────────────── $assets = giteaApi("{$apiBase}/releases/{$sourceId}/assets", $token) ?: []; echo "Assets: " . count($assets) . " file(s)\n"; // ── Download assets to temp ────────────────────────────────────────────────── $tmpDir = sys_get_temp_dir() . '/moko-promote-' . getmypid(); @mkdir($tmpDir, 0755, true); foreach ($assets as $asset) { $name = $asset['name']; $downloadUrl = $asset['browser_download_url']; echo " Downloading: {$name}\n"; giteaDownload($downloadUrl, $token, "{$tmpDir}/{$name}"); } // ── Detect type prefix for stable promotion ────────────────────────────────── $typePrefix = ''; if ($to === 'stable') { $root = realpath($path) ?: $path; $manifestFiles = array_merge( glob("{$root}/src/pkg_*.xml") ?: [], glob("{$root}/src/*.xml") ?: [], glob("{$root}/*.xml") ?: [] ); foreach ($manifestFiles as $xmlFile) { $xmlContent = file_get_contents($xmlFile); if (strpos($xmlContent, ' $oldName, 'new' => $newName]; if ($oldName !== $newName) { echo " Rename: {$oldName} → {$newName}\n"; } } // ── Delete source release + tag ────────────────────────────────────────────── giteaApi("{$apiBase}/releases/{$sourceId}", $token, 'DELETE'); giteaApi("{$apiBase}/tags/{$from}", $token, 'DELETE'); echo "Deleted source: {$from} release + tag\n"; // ── Delete existing target release + tag (if any) ──────────────────────────── $existingTarget = giteaApi("{$apiBase}/releases/tags/{$to}", $token); if ($existingTarget && !empty($existingTarget['id'])) { giteaApi("{$apiBase}/releases/{$existingTarget['id']}", $token, 'DELETE'); giteaApi("{$apiBase}/tags/{$to}", $token, 'DELETE'); echo "Deleted existing target: {$to} release + tag\n"; } // ── Create target release ──────────────────────────────────────────────────── $isPrerelease = ($to !== 'stable'); $newName = preg_replace('/\(' . preg_quote($from, '/') . '\)/', "({$to})", $sourceName); if ($newName === $sourceName) { $newName = str_ireplace($from, $to, $sourceName); } $newBody = str_ireplace($from, $to, $sourceBody); $payload = json_encode([ 'tag_name' => $to, 'target_commitish' => $branch, 'name' => $newName, 'body' => $newBody, 'prerelease' => $isPrerelease, ]); $newRelease = giteaApi("{$apiBase}/releases", $token, 'POST', $payload); if (!$newRelease || empty($newRelease['id'])) { fwrite(STDERR, "Failed to create {$to} release\n"); exit(1); } $newId = $newRelease['id']; echo "Created: {$to} release (id: {$newId})\n"; // ── Upload renamed assets ──────────────────────────────────────────────────── foreach ($renamedAssets as $entry) { $localFile = "{$tmpDir}/{$entry['old']}"; if (!file_exists($localFile)) { continue; } $uploadName = urlencode($entry['new']); $url = "{$apiBase}/releases/{$newId}/assets?name={$uploadName}"; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ "Authorization: token {$token}", 'Content-Type: application/octet-stream', ], CURLOPT_POSTFIELDS => file_get_contents($localFile), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 120, ]); curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $status = ($code >= 200 && $code < 300) ? 'OK' : "FAILED ({$code})"; echo " Upload: {$entry['new']} — {$status}\n"; } // ── Cleanup temp ───────────────────────────────────────────────────────────── array_map('unlink', glob("{$tmpDir}/*") ?: []); @rmdir($tmpDir); echo "Promoted: {$from} → {$to}\n"; exit(0);