#!/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_create.php * BRIEF: Create or overwrite a Gitea release with proper naming */ declare(strict_types=1); require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; use MokoEnterprise\CliFramework; class ReleaseCreateCli extends CliFramework { protected function configure(): void { $this->setDescription('Create or overwrite a Gitea release with proper naming'); $this->addArgument('--path', 'Repo root for manifest detection (default: .)', '.'); $this->addArgument('--version', 'Version string (required)', ''); $this->addArgument('--tag', 'Release tag name (required)', ''); $this->addArgument('--token', 'Gitea API token (required)', ''); $this->addArgument('--api-base', 'Gitea API base URL for the repo (required)', ''); $this->addArgument('--branch', 'Target commitish (default: main)', 'main'); $this->addArgument('--repo', 'Repo name for fallback element detection', ''); $this->addArgument('--prerelease', 'Mark release as prerelease', false); } protected function run(): int { $path = $this->getArgument('--path'); $version = $this->getArgument('--version'); $tag = $this->getArgument('--tag'); $token = $this->getArgument('--token'); $apiBase = $this->getArgument('--api-base'); $branch = $this->getArgument('--branch'); $repoName = $this->getArgument('--repo'); $prerelease = (bool) $this->getArgument('--prerelease'); // Allow token from environment if ($token === '') { $envToken = getenv('MOKOGITEA_TOKEN'); if ($envToken === false || $envToken === '') { $envToken = getenv('GITEA_TOKEN'); } if ($envToken !== false && $envToken !== '') { $token = $envToken; } } if ($version === '' || $tag === '' || $token === '' || $apiBase === '') { $this->log('ERROR', "Usage: release_create.php --version VER --tag TAG --token TOKEN --api-base URL [options]"); $this->log('ERROR', " --path . Repo root for manifest detection (default: .)"); $this->log('ERROR', " --branch main Target commitish (default: main)"); $this->log('ERROR', " --repo REPO Repo name for fallback element detection"); $this->log('ERROR', " --prerelease Mark release as prerelease"); $this->log('ERROR', " Token can also be set via MOKOGITEA_TOKEN or GITEA_TOKEN env var"); return 1; } // ── Detect element metadata ───────────────────────────────────────────── $root = realpath($path) ?: $path; $extElement = ''; $extType = ''; $extFolder = ''; $extName = ''; $typePrefix = ''; // Detect platform and display name from manifest.xml $platform = 'generic'; $prettyName = ''; $manifestXml = "{$root}/.mokogitea/manifest.xml"; if (file_exists($manifestXml)) { $content = file_get_contents($manifestXml); if ($content !== false) { if (preg_match('/([^<]+)<\/platform>/', $content, $pm)) { $platform = trim($pm[1]); } if (preg_match('/([^<]+)<\/display-name>/', $content, $dn)) { $prettyName = trim($dn[1]); } elseif (preg_match('/([^<]+)<\/name>/', $content, $nm)) { $prettyName = trim($nm[1]); } } } // Find extension manifest (Joomla XML) $extManifest = null; $manifestFiles = array_merge( glob("{$root}/src/pkg_*.xml") ?: [], glob("{$root}/src/*.xml") ?: [], glob("{$root}/*.xml") ?: [] ); foreach ($manifestFiles as $file) { $c = file_get_contents($file); if ($c !== false && strpos($c, '([^<]+)<\/element>/', $xml, $em)) { $extElement = $em[1]; } if (empty($extElement) && preg_match('/plugin="([^"]*)"/', $xml, $pm2)) { $extElement = $pm2[1]; } if ($extType === 'package' && preg_match('/([^<]+)<\/packagename>/', $xml, $pn)) { $extElement = $pn[1]; } if (empty($extElement)) { $extElement = strtolower(basename($extManifest, '.xml')); if (in_array($extElement, ['templatedetails', 'manifest'], true)) { $extElement = strtolower(str_replace([' ', '-'], '', $repoName !== '' ? $repoName : basename($root))); } } if (preg_match('/([^<]+)<\/name>/', $xml, $nm)) { $extName = trim($nm[1]); } break; case in_array($platform, ['dolibarr', 'crm-module'], true) && $modFile !== null: $extType = 'dolibarr-module'; $modBasename = basename($modFile, '.class.php'); $extElement = strtolower(preg_replace('/^mod/', '', $modBasename) ?? $modBasename); $modContent = file_get_contents($modFile); if ($modContent !== false && preg_match('/\$this->name\s*=\s*[\'"]([^\'"]+)[\'"]/', $modContent, $nm2)) { $extName = $nm2[1]; } break; default: $extElement = strtolower(str_replace([' ', '-'], '', $repoName !== '' ? $repoName : basename($root))); $extType = 'generic'; break; } // Strip existing type prefix from element to prevent duplication $extElement = preg_replace('/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)/', '', $extElement) ?? $extElement; // Compute type prefix switch ($extType) { case 'plugin': $typePrefix = "plg_{$extFolder}_"; break; case 'module': $typePrefix = 'mod_'; break; case 'component': $typePrefix = 'com_'; break; case 'template': $typePrefix = 'tpl_'; break; case 'library': $typePrefix = 'lib_'; break; case 'package': $typePrefix = 'pkg_'; break; } // Fallback name if (empty($extName)) { $extName = $repoName !== '' ? $repoName : basename($root); } echo "Element: {$extElement}, Type: {$extType}, Prefix: {$typePrefix}, Name: {$extName}\n"; // ── Build release name ────────────────────────────────────────────────────── $displayName = !empty($prettyName) ? $prettyName : $extName; $releaseName = "{$displayName} (VERSION: {$version})"; echo "Release name: {$releaseName}\n"; // ── Generate release notes ────────────────────────────────────────────────── $releaseNotes = "Release {$version}"; $releaseNotesScript = dirname(__DIR__) . '/cli/release_notes.php'; if (file_exists($releaseNotesScript)) { $cmd = sprintf( 'php %s --path %s --version %s', escapeshellarg($releaseNotesScript), escapeshellarg($root), escapeshellarg($version) ); $output = []; $exitCode = 0; exec($cmd, $output, $exitCode); if ($exitCode === 0 && count($output) > 0) { $notes = implode("\n", $output); if (trim($notes) !== '') { $releaseNotes = $notes; echo "Release notes: generated from CHANGELOG.md\n"; } } } // ── Delete existing release at tag (if present) ───────────────────────────── $existing = $this->giteaApi("{$apiBase}/releases/tags/{$tag}", $token); if ($existing !== null && !empty($existing['id'])) { $existingId = $existing['id']; echo "Deleting existing release: {$tag} (id: {$existingId})\n"; // Delete release $this->giteaApi("{$apiBase}/releases/{$existingId}", $token, 'DELETE'); // Delete tag $this->giteaApi("{$apiBase}/tags/{$tag}", $token, 'DELETE'); } // ── Create new release ────────────────────────────────────────────────────── $payload = json_encode([ 'tag_name' => $tag, 'target_commitish' => $branch, 'name' => $releaseName, 'body' => $releaseNotes, 'prerelease' => $prerelease, ]); $newRelease = $this->giteaApi("{$apiBase}/releases", $token, 'POST', $payload !== false ? $payload : '{}'); if ($newRelease === null || empty($newRelease['id'])) { $this->log('ERROR', "Failed to create release at tag: {$tag}"); return 1; } $releaseId = $newRelease['id']; echo "Created release: {$tag} (id: {$releaseId})\n"; // Output release_id to stdout for CI consumption echo "release_id={$releaseId}\n"; return 0; } private function giteaApi(string $url, string $token, string $method = 'GET', ?string $body = null): ?array { $ch = curl_init($url); if ($ch === false) { return null; } 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) || !is_string($response)) { return null; } $decoded = json_decode($response, true); return is_array($decoded) ? $decoded : null; } } $app = new ReleaseCreateCli(); exit($app->execute());