#!/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_build.php * BRIEF: Generate Joomla updates.xml from extension manifest metadata */ declare(strict_types=1); require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; use MokoEnterprise\CliFramework; class UpdatesXmlBuildCli extends CliFramework { protected function configure(): void { $this->setDescription('Generate Joomla updates.xml from extension manifest metadata'); $this->addArgument('--path', 'Repository root (default: .)', '.'); $this->addArgument('--version', 'Version string (required)', ''); $this->addArgument('--stability', 'One of: stable, rc, beta, alpha, development (default: stable)', 'stable'); $this->addArgument('--sha', 'SHA-256 hash of the ZIP package', ''); $this->addArgument('--gitea-url', 'Gitea instance URL', ''); $this->addArgument('--org', 'Organization', ''); $this->addArgument('--repo', 'Repository name', ''); $this->addArgument('--output', 'Output file path (default: updates.xml in --path)', ''); $this->addArgument('--github-output', 'Export ext_element, ext_name, ext_type, ext_folder to $GITHUB_OUTPUT', false); } protected function run(): int { $path = $this->getArgument('--path'); $version = $this->getArgument('--version'); $stability = $this->getArgument('--stability'); $sha = $this->getArgument('--sha') ?: null; $giteaUrl = $this->getArgument('--gitea-url') ?: (getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech'); $org = $this->getArgument('--org') ?: (getenv('GITEA_ORG') ?: ''); $repo = $this->getArgument('--repo') ?: (getenv('GITEA_REPO') ?: ''); $outputFile = $this->getArgument('--output') ?: null; $githubOutput = $this->getArgument('--github-output'); if ($version === '') { $this->log('ERROR', 'Usage: updates_xml_build.php --path . --version XX.YY.ZZ [--stability stable] [--sha SHA]'); return 1; } // Strip suffix — stability is applied via --stability parameter $version = preg_replace('/-(dev|alpha|beta|rc)$/', '', $version); $root = realpath($path) ?: $path; // -- Read platform from .mokogitea/manifest.xml -------------------------------- $detectedPlatform = 'joomla'; $detectedName = $repo; $detectedPackageType = ''; $detectedDisplayName = ''; $mokoManifest = "{$root}/.mokogitea/manifest.xml"; if (file_exists($mokoManifest)) { $mokoXml = @simplexml_load_file($mokoManifest); if ($mokoXml !== false) { $rawPlatform = (string)($mokoXml->governance->platform ?? ''); if ($rawPlatform !== '') { $detectedPlatform = match ($rawPlatform) { 'waas-component' => 'joomla', 'crm-module' => 'dolibarr', default => $rawPlatform, }; } $detectedName = (string)($mokoXml->identity->name ?? $repo); $detectedDisplayName = (string)($mokoXml->identity->{"display-name"} ?? ''); $detectedPackageType = (string)($mokoXml->build->{"package-type"} ?? ''); if (empty($org)) { $manifestOrg = (string)($mokoXml->identity->org ?? ''); if ($manifestOrg !== '') { $org = $manifestOrg; } } if (empty($repo)) { $manifestName = (string)($mokoXml->identity->name ?? ''); if ($manifestName !== '') { $repo = $manifestName; } } } } // -- Fallback: detect org/repo from git remote -------------------------------- if (empty($org) || empty($repo)) { $remoteUrl = trim(shell_exec("git -C " . escapeshellarg($root) . " remote get-url origin 2>/dev/null") ?? ''); if (preg_match('#[/:]([^/:]+)/([^/]+?)(?:\.git)?$#', $remoteUrl, $m)) { if (empty($org)) { $org = $m[1]; } if (empty($repo)) { $repo = $m[2]; } } } // -- Locate Joomla manifest --------------------------------------------------- $manifest = null; $candidates = glob("{$root}/src/pkg_*.xml") ?: []; foreach ($candidates as $f) { if (strpos(file_get_contents($f), 'log('ERROR', "No Joomla XML manifest found in {$root}"); return 1; } // -- Parse extension metadata ------------------------------------------------- $extName = ''; $extType = ''; $extElement = ''; $extClient = ''; $extFolder = ''; $targetPlatform = ''; $phpMinimum = ''; if ($manifest !== null) { $xml = file_get_contents($manifest); if (preg_match('/([^<]+)<\/name>/', $xml, $m)) { $extName = $m[1]; } if (preg_match('/]*type="([^"]+)"/', $xml, $m)) { $extType = $m[1]; } if (preg_match('/([^<]+)<\/element>/', $xml, $m)) { $extElement = $m[1]; } if (empty($extElement) && preg_match('/([^<]+)<\/packagename>/', $xml, $m)) { $extElement = $m[1]; } if (empty($extElement) && preg_match('/plugin="([^"]+)"/', $xml, $m)) { $extElement = $m[1]; } if (empty($extElement) && preg_match('/module="([^"]+)"/', $xml, $m)) { $extElement = $m[1]; } if (empty($extElement)) { $fname = strtolower(pathinfo($manifest, PATHINFO_FILENAME)); if (in_array($fname, ['templatedetails', 'manifest'])) { $extElement = strtolower(str_replace([' ', '-'], '', $repo ?: basename($root))); } else { $extElement = $fname; } } $extElement = preg_replace('/^(pkg_|com_|mod_|plg_\w+_|tpl_|lib_)/', '', $extElement); if (preg_match('/]*client="([^"]+)"/', $xml, $m)) { $extClient = $m[1]; } if (preg_match('/]*group="([^"]+)"/', $xml, $m)) { $extFolder = $m[1]; } if (preg_match('/()/', $xml, $m)) { $targetPlatform = $m[1]; } if (empty($targetPlatform)) { $targetPlatform = ''; } if (preg_match('/([^<]+)<\/php_minimum>/', $xml, $m)) { $phpMinimum = $m[1]; } } else { $extName = $detectedName ?: ($repo ?: basename($root)); $extElement = strtolower(str_replace([' ', '-'], '', $extName)); $extType = $detectedPackageType ?: 'generic'; $targetPlatform = ""; } if (empty($extName)) { $extName = $repo ?: basename($root); } if (empty($extType)) { $extType = 'component'; } if (!empty($detectedDisplayName)) { $displayName = $detectedDisplayName; } elseif (!empty($detectedName)) { $displayName = $detectedName; } else { $displayName = $extName; } // -- Build type prefix -------------------------------------------------------- $typePrefix = ''; 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; } // -- Export to GITHUB_OUTPUT if requested ------------------------------------- if ($githubOutput) { $ghOutput = getenv('GITHUB_OUTPUT'); $lines = [ "ext_element={$extElement}", "ext_name={$extName}", "ext_type={$extType}", "ext_folder={$extFolder}", "type_prefix={$typePrefix}", ]; if ($ghOutput) { file_put_contents($ghOutput, implode("\n", $lines) . "\n", FILE_APPEND); $this->log('INFO', "Exported " . count($lines) . " fields to GITHUB_OUTPUT"); } else { foreach ($lines as $line) { echo "{$line}\n"; } } } // -- Stability suffix map ----------------------------------------------------- $stabilitySuffixMap = [ 'stable' => '', 'rc' => '-rc', 'beta' => '-beta', 'alpha' => '-alpha', 'development' => '-dev', 'dev' => '-dev', ]; $stabilityTagMap = [ 'stable' => 'stable', 'rc' => 'rc', 'beta' => 'beta', 'alpha' => 'alpha', 'development' => 'dev', 'dev' => 'dev', ]; $releaseTagMap = [ 'stable' => 'stable', 'rc' => 'release-candidate', 'beta' => 'beta', 'alpha' => 'alpha', 'development' => 'development', 'dev' => 'development', ]; $primarySuffix = $stabilitySuffixMap[$stability] ?? ''; $primaryVersion = $version . $primarySuffix; $clientTag = ''; if (!empty($extClient)) { $clientTag = " {$extClient}"; } else { $clientTag = ' site'; } $folderTag = ''; if (!empty($extFolder) && $extType === 'plugin') { $folderTag = " {$extFolder}"; } $phpTag = ''; if (!empty($phpMinimum)) { $phpTag = " {$phpMinimum}"; } $shaTag = ''; if (!empty($sha)) { $shaTag = " {$sha}"; } // -- Write ONLY the single channel being released -------------------------------- $entries = []; $giteaTag = $releaseTagMap[$stability] ?? $stability; $channelVersion = $version . ($stabilitySuffixMap[$stability] ?? ''); $channelDownloadUrl = "{$giteaUrl}/{$org}/{$repo}/releases/download/{$giteaTag}/{$typePrefix}{$extElement}-{$channelVersion}.zip"; $channelInfoUrl = "{$giteaUrl}/{$org}/{$repo}/releases/tag/{$giteaTag}"; $joomlaTag = $stabilityTagMap[$stability] ?? $stability; $changelogUrl = "{$giteaUrl}/{$org}/{$repo}/raw/branch/main/CHANGELOG.md"; $entries[] = $this->buildEntry( $joomlaTag, $channelVersion, $channelDownloadUrl, $displayName, $stability, $extElement, $extType, $clientTag, $folderTag, $channelInfoUrl, $targetPlatform, $phpTag, $shaTag, $changelogUrl ); // -- Preserve existing entries for channels not being updated ----------------- $dest = $outputFile ?? "{$root}/updates.xml"; $preservedEntries = []; if (file_exists($dest)) { $existingXml = @simplexml_load_file($dest); if ($existingXml) { $writtenTag = $joomlaTag; $writtenAliases = [$writtenTag]; if ($writtenTag === 'dev') { $writtenAliases[] = 'development'; } if ($writtenTag === 'development') { $writtenAliases[] = 'dev'; } foreach ($existingXml->update as $existingUpdate) { $existingTag = ''; if (isset($existingUpdate->tags->tag)) { $existingTag = (string) $existingUpdate->tags->tag; } if (!empty($existingTag) && !in_array($existingTag, $writtenAliases, true)) { $preservedEntries[] = ' ' . trim($existingUpdate->asXML()); } } } } // -- Write updates.xml -------------------------------------------------------- $year = date('Y'); $output = << XML; $allEntries = array_merge($preservedEntries, $entries); $stabilityOrder = ['dev' => 0, 'development' => 0, 'alpha' => 1, 'beta' => 2, 'rc' => 3, 'stable' => 4]; usort($allEntries, function ($a, $b) use ($stabilityOrder) { preg_match('/([^<]+)<\/tag>/', $a, $ma); preg_match('/([^<]+)<\/tag>/', $b, $mb); return ($stabilityOrder[$ma[1] ?? ''] ?? 99) - ($stabilityOrder[$mb[1] ?? ''] ?? 99); }); $output .= "\n" . implode("\n", $allEntries) . "\n\n"; $dest = $outputFile ?? "{$root}/updates.xml"; file_put_contents($dest, $output); $channelCount = count($entries); echo "updates.xml: {$primaryVersion} ({$channelCount} channel(s), stability={$stability})\n"; echo "Output: {$dest}\n"; return 0; } private function buildEntry( string $tagName, string $entryVersion, string $entryDownloadUrl, string $displayName, string $stabilityLabel, string $extElement, string $extType, string $clientTag, string $folderTag, string $infoUrl, string $targetPlatform, string $phpTag, string $shaTag, string $changelogUrl = '' ): string { $lines = []; $lines[] = ' '; $lines[] = " {$displayName}"; $lines[] = " {$displayName} {$stabilityLabel} build."; $prefixMap = [ 'package' => 'pkg_', 'module' => 'mod_', 'component' => 'com_', 'library' => 'lib_', ]; $dbElement = isset($prefixMap[$extType]) ? $prefixMap[$extType] . $extElement : $extElement; $lines[] = " {$dbElement}"; $lines[] = " {$extType}"; $lines[] = $clientTag; $lines[] = " {$entryVersion}"; $lines[] = " " . date('Y-m-d') . ""; if (!empty($folderTag)) { $lines[] = $folderTag; } $lines[] = " {$infoUrl}"; $lines[] = ' '; $lines[] = " {$entryDownloadUrl}"; $lines[] = ' '; if (!empty($shaTag)) { $lines[] = $shaTag; } $lines[] = " {$tagName}"; if (!empty($changelogUrl)) { $lines[] = " {$changelogUrl}"; } $lines[] = ' Moko Consulting'; $lines[] = ' https://mokoconsulting.tech'; $lines[] = " {$targetPlatform}"; if (!empty($phpTag)) { $lines[] = $phpTag; } $lines[] = ' '; return implode("\n", $lines); } } $app = new UpdatesXmlBuildCli(); exit($app->execute());