1b0d5bd2f3
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Cascade Main → Dev / Cascade main → branches (push) Successful in 3s
Generic: Repo Health / Release configuration (push) Successful in 3s
Generic: Repo Health / Scripts governance (push) Successful in 4s
Generic: Repo Health / Repository health (push) Successful in 11s
Platform: moko-platform CI / Gate 1: Code Quality (push) Successful in 45s
Platform: moko-platform CI / Gate 5: Template Integrity (push) Failing after 5s
Platform: moko-platform CI / Gate 4: Governance (push) Successful in 42s
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Failing after 42s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Failing after 45s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Failing after 45s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Failing after 46s
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Joomla stores packages as pkg_elementname in #__extensions. The element tag in updates.xml must match for the updater to find the extension. The ZIP filename uses the same prefix (already correct), but the XML element tag was using the stripped name. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
368 lines
12 KiB
PHP
368 lines
12 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
*
|
|
* 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
|
|
*
|
|
* Usage:
|
|
* php updates_xml_build.php --path /repo --version 04.01.00 --stability stable
|
|
* php updates_xml_build.php --path /repo --version 04.01.00 --stability stable --sha SHA256
|
|
* php updates_xml_build.php --path /repo --version 04.01.00 --stability stable --github-output
|
|
*
|
|
* Options:
|
|
* --path Repository root (default: .)
|
|
* --version Version string (required)
|
|
* --stability One of: stable, rc, beta, alpha, development (default: stable)
|
|
* --sha SHA-256 hash of the ZIP package (optional)
|
|
* --gitea-url Gitea instance URL (default: env GITEA_URL or https://git.mokoconsulting.tech)
|
|
* --org Organization (default: env GITEA_ORG)
|
|
* --repo Repository name (default: env GITEA_REPO)
|
|
* --output Output file path (default: updates.xml in --path)
|
|
* --github-output Export ext_element, ext_name, ext_type, ext_folder to $GITHUB_OUTPUT
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
// -- Argument parsing ---------------------------------------------------------
|
|
$path = '.';
|
|
$version = null;
|
|
$stability = 'stable';
|
|
$sha = null;
|
|
$giteaUrl = getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech';
|
|
$org = getenv('GITEA_ORG') ?: '';
|
|
$repo = getenv('GITEA_REPO') ?: '';
|
|
$outputFile = null;
|
|
$githubOutput = false;
|
|
|
|
foreach ($argv as $i => $arg) {
|
|
if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1];
|
|
if ($arg === '--version' && isset($argv[$i + 1])) $version = $argv[$i + 1];
|
|
if ($arg === '--stability' && isset($argv[$i + 1])) $stability = $argv[$i + 1];
|
|
if ($arg === '--sha' && isset($argv[$i + 1])) $sha = $argv[$i + 1];
|
|
if ($arg === '--gitea-url' && isset($argv[$i + 1])) $giteaUrl = $argv[$i + 1];
|
|
if ($arg === '--org' && isset($argv[$i + 1])) $org = $argv[$i + 1];
|
|
if ($arg === '--repo' && isset($argv[$i + 1])) $repo = $argv[$i + 1];
|
|
if ($arg === '--output' && isset($argv[$i + 1])) $outputFile = $argv[$i + 1];
|
|
if ($arg === '--github-output') $githubOutput = true;
|
|
}
|
|
|
|
if ($version === null) {
|
|
fwrite(STDERR, "Usage: updates_xml_build.php --path . --version XX.YY.ZZ [--stability stable] [--sha SHA]\n");
|
|
exit(1);
|
|
}
|
|
|
|
$root = realpath($path) ?: $path;
|
|
|
|
// -- Locate Joomla manifest ---------------------------------------------------
|
|
$manifest = null;
|
|
|
|
// Priority: pkg_*.xml in src/ > any extension XML in src/ > any in root
|
|
$candidates = glob("{$root}/src/pkg_*.xml") ?: [];
|
|
foreach ($candidates as $f) {
|
|
if (strpos(file_get_contents($f), '<extension') !== false) {
|
|
$manifest = $f;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($manifest === null) {
|
|
$searchDirs = ["{$root}/src", "{$root}"];
|
|
foreach ($searchDirs as $dir) {
|
|
if (!is_dir($dir)) continue;
|
|
foreach (glob("{$dir}/*.xml") ?: [] as $f) {
|
|
if (strpos(file_get_contents($f), '<extension') !== false) {
|
|
$manifest = $f;
|
|
break 2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($manifest === null) {
|
|
fwrite(STDERR, "No Joomla XML manifest found in {$root}\n");
|
|
exit(1);
|
|
}
|
|
|
|
// -- Parse extension metadata -------------------------------------------------
|
|
$xml = file_get_contents($manifest);
|
|
|
|
// Extract fields via regex (more portable than SimpleXML for malformed manifests)
|
|
$extName = '';
|
|
if (preg_match('/<name>([^<]+)<\/name>/', $xml, $m)) $extName = $m[1];
|
|
|
|
$extType = '';
|
|
if (preg_match('/<extension[^>]*type="([^"]+)"/', $xml, $m)) $extType = $m[1];
|
|
|
|
$extElement = '';
|
|
if (preg_match('/<element>([^<]+)<\/element>/', $xml, $m)) $extElement = $m[1];
|
|
// For packages, prefer <packagename> to avoid pkg_pkg_ duplication
|
|
if (empty($extElement) && preg_match('/<packagename>([^<]+)<\/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;
|
|
}
|
|
}
|
|
// Strip existing type prefix to prevent duplication (e.g. pkg_mokowaas → mokowaas)
|
|
$extElement = preg_replace('/^(pkg_|com_|mod_|plg_\w+_|tpl_|lib_)/', '', $extElement);
|
|
|
|
$extClient = '';
|
|
if (preg_match('/<extension[^>]*client="([^"]+)"/', $xml, $m)) $extClient = $m[1];
|
|
|
|
$extFolder = '';
|
|
if (preg_match('/<extension[^>]*group="([^"]+)"/', $xml, $m)) $extFolder = $m[1];
|
|
|
|
$targetPlatform = '';
|
|
if (preg_match('/(<targetplatform[^\/]*\/>)/', $xml, $m)) $targetPlatform = $m[1];
|
|
if (empty($targetPlatform)) {
|
|
$targetPlatform = '<targetplatform name="joomla" version="(5|6)\..*" />';
|
|
}
|
|
|
|
$phpMinimum = '';
|
|
if (preg_match('/<php_minimum>([^<]+)<\/php_minimum>/', $xml, $m)) $phpMinimum = $m[1];
|
|
|
|
// Resolve language key names (e.g. PLG_SYSTEM_MOKOJOOMTOS)
|
|
if (preg_match('/^[A-Z_]+$/', $extName)) {
|
|
$iniFiles = [];
|
|
$iterator = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS)
|
|
);
|
|
foreach ($iterator as $file) {
|
|
if (preg_match('/\.sys\.ini$/i', $file->getFilename())) {
|
|
$iniFiles[] = $file->getPathname();
|
|
}
|
|
}
|
|
foreach ($iniFiles as $ini) {
|
|
$content = file_get_contents($ini);
|
|
if (preg_match('/^' . preg_quote($extName, '/') . '="([^"]+)"/m', $content, $m)) {
|
|
$extName = $m[1];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallbacks
|
|
if (empty($extName)) $extName = $repo ?: basename($root);
|
|
if (empty($extType)) $extType = 'component';
|
|
|
|
// -- 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);
|
|
fwrite(STDERR, "Exported " . count($lines) . " fields to GITHUB_OUTPUT\n");
|
|
} else {
|
|
foreach ($lines as $line) echo "{$line}\n";
|
|
}
|
|
}
|
|
|
|
// -- Stability suffix map -----------------------------------------------------
|
|
$stabilitySuffixMap = [
|
|
'stable' => '',
|
|
'rc' => '-rc',
|
|
'beta' => '-beta',
|
|
'alpha' => '-alpha',
|
|
'development' => '-dev',
|
|
];
|
|
|
|
$stabilityTagMap = [
|
|
'stable' => 'stable',
|
|
'rc' => 'rc',
|
|
'beta' => 'beta',
|
|
'alpha' => 'alpha',
|
|
'development' => 'development',
|
|
];
|
|
|
|
// -- Build update entries -----------------------------------------------------
|
|
$releaseTag = $stabilityTagMap[$stability] ?? $stability;
|
|
|
|
// For the primary entry: apply suffix if not stable
|
|
$primarySuffix = $stabilitySuffixMap[$stability] ?? '';
|
|
$primaryVersion = $version . $primarySuffix;
|
|
|
|
$downloadUrl = "{$giteaUrl}/{$org}/{$repo}/releases/download/{$releaseTag}/{$typePrefix}{$extElement}-{$primaryVersion}.zip";
|
|
$infoUrl = "{$giteaUrl}/{$org}/{$repo}/releases/tag/{$releaseTag}";
|
|
|
|
// Build client tag
|
|
$clientTag = '';
|
|
if (!empty($extClient)) {
|
|
$clientTag = " <client>{$extClient}</client>";
|
|
} elseif ($extType === 'module' || $extType === 'plugin') {
|
|
$clientTag = ' <client>site</client>';
|
|
}
|
|
|
|
// Build folder tag
|
|
$folderTag = '';
|
|
if (!empty($extFolder) && $extType === 'plugin') {
|
|
$folderTag = " <folder>{$extFolder}</folder>";
|
|
}
|
|
|
|
// PHP minimum tag
|
|
$phpTag = '';
|
|
if (!empty($phpMinimum)) {
|
|
$phpTag = " <php_minimum>{$phpMinimum}</php_minimum>";
|
|
}
|
|
|
|
// SHA tag
|
|
$shaTag = '';
|
|
if (!empty($sha)) {
|
|
$shaTag = " <sha256>{$sha}</sha256>";
|
|
}
|
|
|
|
/**
|
|
* Build a single <update> entry for a given stability tag
|
|
*/
|
|
function buildEntry(
|
|
string $tagName,
|
|
string $entryVersion,
|
|
string $entryDownloadUrl,
|
|
string $extName,
|
|
string $extElement,
|
|
string $extType,
|
|
string $clientTag,
|
|
string $folderTag,
|
|
string $infoUrl,
|
|
string $targetPlatform,
|
|
string $phpTag,
|
|
string $shaTag
|
|
): string {
|
|
$lines = [];
|
|
$lines[] = ' <update>';
|
|
$lines[] = " <name>{$extName}</name>";
|
|
$lines[] = " <description>{$extName} update</description>";
|
|
// Element in updates.xml must match what Joomla stores in #__extensions
|
|
// For packages: pkg_elementname. For plugins: elementname (folder handles grouping).
|
|
$dbElement = ($extType === 'package') ? "pkg_{$extElement}" : $extElement;
|
|
$lines[] = " <element>{$dbElement}</element>";
|
|
$lines[] = " <type>{$extType}</type>";
|
|
$lines[] = " <version>{$entryVersion}</version>";
|
|
if (!empty($clientTag)) $lines[] = $clientTag;
|
|
if (!empty($folderTag)) $lines[] = $folderTag;
|
|
$lines[] = " <tags><tag>{$tagName}</tag></tags>";
|
|
$lines[] = " <infourl title=\"{$extName}\">{$infoUrl}</infourl>";
|
|
$lines[] = ' <downloads>';
|
|
$lines[] = " <downloadurl type=\"full\" format=\"zip\">{$entryDownloadUrl}</downloadurl>";
|
|
$lines[] = ' </downloads>';
|
|
if (!empty($shaTag)) $lines[] = $shaTag;
|
|
$lines[] = " {$targetPlatform}";
|
|
if (!empty($phpTag)) $lines[] = $phpTag;
|
|
$lines[] = ' <maintainer>Moko Consulting</maintainer>';
|
|
$lines[] = ' <maintainerurl>https://mokoconsulting.tech</maintainerurl>';
|
|
$lines[] = ' </update>';
|
|
return implode("\n", $lines);
|
|
}
|
|
|
|
// -- Determine which channels to write ----------------------------------------
|
|
// Stable cascades to all channels; pre-releases only write their level and below
|
|
// Each channel gets its own suffixed version:
|
|
// development -> 04.01.00-dev
|
|
// alpha -> 04.01.00-alpha
|
|
// beta -> 04.01.00-beta
|
|
// rc -> 04.01.00-rc
|
|
// stable -> 04.01.00
|
|
$allChannels = ['development', 'alpha', 'beta', 'rc', 'stable'];
|
|
$stabilityIndex = array_search($stability === 'development' ? 'development' : $stability, $allChannels);
|
|
if ($stabilityIndex === false) $stabilityIndex = 4; // default to stable
|
|
|
|
// Write only the current channel entry (not cascade)
|
|
// Each channel release only creates its own entry; preserved entries handle other channels
|
|
$entries = [];
|
|
$channelName = $allChannels[$stabilityIndex];
|
|
$channelSuffix = $stabilitySuffixMap[$channelName] ?? '';
|
|
$channelVersion = $version . $channelSuffix;
|
|
$channelTag = $stabilityTagMap[$channelName] ?? $channelName;
|
|
$channelDownloadUrl = "{$giteaUrl}/{$org}/{$repo}/releases/download/{$channelTag}/{$typePrefix}{$extElement}-{$channelVersion}.zip";
|
|
$channelInfoUrl = "{$giteaUrl}/{$org}/{$repo}/releases/tag/{$channelTag}";
|
|
|
|
$entries[] = buildEntry(
|
|
$channelName,
|
|
$channelVersion,
|
|
$channelDownloadUrl,
|
|
$extName,
|
|
$extElement,
|
|
$extType,
|
|
$clientTag,
|
|
$folderTag,
|
|
$channelInfoUrl,
|
|
$targetPlatform,
|
|
$phpTag,
|
|
$shaTag
|
|
);
|
|
|
|
// -- 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) {
|
|
// Channels we're writing — don't preserve these
|
|
$writtenChannels = [];
|
|
for ($i = 0; $i <= $stabilityIndex; $i++) {
|
|
$writtenChannels[] = $allChannels[$i];
|
|
}
|
|
|
|
foreach ($existingXml->update as $existingUpdate) {
|
|
$existingTag = '';
|
|
if (isset($existingUpdate->tags->tag)) {
|
|
$existingTag = (string) $existingUpdate->tags->tag;
|
|
}
|
|
// Keep entries for channels we're NOT overwriting
|
|
if (!empty($existingTag) && !in_array($existingTag, $writtenChannels, true)) {
|
|
$preservedEntries[] = ' ' . trim($existingUpdate->asXML());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// -- Write updates.xml --------------------------------------------------------
|
|
$year = date('Y');
|
|
$output = <<<XML
|
|
<?xml version='1.0' encoding='UTF-8'?>
|
|
<!-- Copyright (C) {$year} Moko Consulting <hello@mokoconsulting.tech>
|
|
SPDX-License-Identifier: GPL-3.0-or-later
|
|
VERSION: {$primaryVersion}
|
|
-->
|
|
|
|
<updates>
|
|
XML;
|
|
$allEntries = array_merge($preservedEntries, $entries);
|
|
$output .= "\n" . implode("\n", $allEntries) . "\n</updates>\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";
|
|
exit(0);
|