Public Access
Merge dev: release promotion pipeline, manifest-aware CLI, workflow refactoring
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Cascade Main → Dev / Cascade main → branches (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Cascade Main → Dev / Cascade main → branches (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
- 7 new CLI tools (manifest_element, release_create, release_package, release_promote, release_mirror, version_reset_dev, ManifestReader) - Universal workflows: RC promotion, auto-dev-release, paths filter removed - CLI tools now read .mokogitea/manifest.xml for platform-aware behavior - updates_xml_build.php supports non-Joomla platforms - Version 09.01.00
This commit is contained in:
+32
-2
@@ -157,6 +157,26 @@ function giteaUploadAsset(string $url, string $token, string $filePath): int
|
|||||||
return $httpCode;
|
return $httpCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Read platform from .mokogitea/manifest.xml ───────────────────────────────
|
||||||
|
|
||||||
|
$detectedPlatform = 'generic';
|
||||||
|
$detectedEntryPoint = '';
|
||||||
|
$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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
$detectedEntryPoint = (string)($mokoXml->build->{"entry-point"} ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Detect element metadata from manifest XML ────────────────────────────────
|
// ── Detect element metadata from manifest XML ────────────────────────────────
|
||||||
|
|
||||||
$extElement = '';
|
$extElement = '';
|
||||||
@@ -257,9 +277,19 @@ echo "TAR: {$baseName}.tar.gz\n";
|
|||||||
// ── Find source directory ────────────────────────────────────────────────────
|
// ── Find source directory ────────────────────────────────────────────────────
|
||||||
|
|
||||||
$sourceDir = null;
|
$sourceDir = null;
|
||||||
if (is_dir("{$root}/src")) {
|
|
||||||
|
// Use entry-point from manifest.xml if available
|
||||||
|
if ($detectedEntryPoint !== '') {
|
||||||
|
$entryDir = rtrim(dirname($detectedEntryPoint) === '.' ? $detectedEntryPoint : dirname($detectedEntryPoint), '/');
|
||||||
|
if (is_dir("{$root}/{$entryDir}")) {
|
||||||
|
$sourceDir = "{$root}/{$entryDir}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to common directories
|
||||||
|
if ($sourceDir === null && is_dir("{$root}/src")) {
|
||||||
$sourceDir = "{$root}/src";
|
$sourceDir = "{$root}/src";
|
||||||
} elseif (is_dir("{$root}/htdocs")) {
|
} elseif ($sourceDir === null && is_dir("{$root}/htdocs")) {
|
||||||
$sourceDir = "{$root}/htdocs";
|
$sourceDir = "{$root}/htdocs";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+286
-194
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env php
|
#!/usr/bin/env php
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
@@ -42,174 +43,255 @@ $outputFile = null;
|
|||||||
$githubOutput = false;
|
$githubOutput = false;
|
||||||
|
|
||||||
foreach ($argv as $i => $arg) {
|
foreach ($argv as $i => $arg) {
|
||||||
if ($arg === '--path' && isset($argv[$i + 1])) $path = $argv[$i + 1];
|
if ($arg === '--path' && isset($argv[$i + 1])) {
|
||||||
if ($arg === '--version' && isset($argv[$i + 1])) $version = $argv[$i + 1];
|
$path = $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 === '--version' && isset($argv[$i + 1])) {
|
||||||
if ($arg === '--gitea-url' && isset($argv[$i + 1])) $giteaUrl = $argv[$i + 1];
|
$version = $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 === '--stability' && isset($argv[$i + 1])) {
|
||||||
if ($arg === '--output' && isset($argv[$i + 1])) $outputFile = $argv[$i + 1];
|
$stability = $argv[$i + 1];
|
||||||
if ($arg === '--github-output') $githubOutput = true;
|
}
|
||||||
|
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) {
|
if ($version === null) {
|
||||||
fwrite(STDERR, "Usage: updates_xml_build.php --path . --version XX.YY.ZZ [--stability stable] [--sha SHA]\n");
|
fwrite(STDERR, "Usage: updates_xml_build.php --path . --version XX.YY.ZZ [--stability stable] [--sha SHA]\n");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
$root = realpath($path) ?: $path;
|
$root = realpath($path) ?: $path;
|
||||||
|
|
||||||
|
// -- Read platform from .mokogitea/manifest.xml --------------------------------
|
||||||
|
$detectedPlatform = 'joomla'; // default for backward compat
|
||||||
|
$detectedName = $repo;
|
||||||
|
$detectedPackageType = '';
|
||||||
|
$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);
|
||||||
|
$detectedPackageType = (string)($mokoXml->build->{"package-type"} ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -- Locate Joomla manifest ---------------------------------------------------
|
// -- Locate Joomla manifest ---------------------------------------------------
|
||||||
$manifest = null;
|
$manifest = null;
|
||||||
|
|
||||||
// Priority: pkg_*.xml in src/ > any extension XML in src/ > any in root
|
// Priority: pkg_*.xml in src/ > any extension XML in src/ > any in root
|
||||||
$candidates = glob("{$root}/src/pkg_*.xml") ?: [];
|
$candidates = glob("{$root}/src/pkg_*.xml") ?: [];
|
||||||
foreach ($candidates as $f) {
|
foreach ($candidates as $f) {
|
||||||
if (strpos(file_get_contents($f), '<extension') !== false) {
|
if (strpos(file_get_contents($f), '<extension') !== false) {
|
||||||
$manifest = $f;
|
$manifest = $f;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($manifest === null) {
|
if ($manifest === null) {
|
||||||
$searchDirs = ["{$root}/src", "{$root}"];
|
$searchDirs = ["{$root}/src", "{$root}"];
|
||||||
foreach ($searchDirs as $dir) {
|
foreach ($searchDirs as $dir) {
|
||||||
if (!is_dir($dir)) continue;
|
if (!is_dir($dir)) {
|
||||||
foreach (glob("{$dir}/*.xml") ?: [] as $f) {
|
continue;
|
||||||
if (strpos(file_get_contents($f), '<extension') !== false) {
|
}
|
||||||
$manifest = $f;
|
foreach (glob("{$dir}/*.xml") ?: [] as $f) {
|
||||||
break 2;
|
if (strpos(file_get_contents($f), '<extension') !== false) {
|
||||||
}
|
$manifest = $f;
|
||||||
}
|
break 2;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($manifest === null) {
|
if ($manifest === null && $detectedPlatform === 'joomla') {
|
||||||
fwrite(STDERR, "No Joomla XML manifest found in {$root}\n");
|
fwrite(STDERR, "No Joomla XML manifest found in {$root}\n");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Parse extension metadata -------------------------------------------------
|
// -- Parse extension metadata -------------------------------------------------
|
||||||
$xml = file_get_contents($manifest);
|
|
||||||
|
|
||||||
// Extract fields via regex (more portable than SimpleXML for malformed manifests)
|
|
||||||
$extName = '';
|
$extName = '';
|
||||||
if (preg_match('/<name>([^<]+)<\/name>/', $xml, $m)) $extName = $m[1];
|
|
||||||
|
|
||||||
$extType = '';
|
$extType = '';
|
||||||
if (preg_match('/<extension[^>]*type="([^"]+)"/', $xml, $m)) $extType = $m[1];
|
|
||||||
|
|
||||||
$extElement = '';
|
$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 = '';
|
$extClient = '';
|
||||||
if (preg_match('/<extension[^>]*client="([^"]+)"/', $xml, $m)) $extClient = $m[1];
|
|
||||||
|
|
||||||
$extFolder = '';
|
$extFolder = '';
|
||||||
if (preg_match('/<extension[^>]*group="([^"]+)"/', $xml, $m)) $extFolder = $m[1];
|
|
||||||
|
|
||||||
$targetPlatform = '';
|
$targetPlatform = '';
|
||||||
if (preg_match('/(<targetplatform[^\/]*\/>)/', $xml, $m)) $targetPlatform = $m[1];
|
|
||||||
if (empty($targetPlatform)) {
|
|
||||||
$targetPlatform = '<targetplatform name="joomla" version="(5|6)\..*" />';
|
|
||||||
}
|
|
||||||
|
|
||||||
$phpMinimum = '';
|
$phpMinimum = '';
|
||||||
if (preg_match('/<php_minimum>([^<]+)<\/php_minimum>/', $xml, $m)) $phpMinimum = $m[1];
|
|
||||||
|
if ($manifest !== null) {
|
||||||
|
// Joomla manifest found — parse extension metadata from it
|
||||||
|
$xml = file_get_contents($manifest);
|
||||||
|
|
||||||
|
if (preg_match('/<name>([^<]+)<\/name>/', $xml, $m)) {
|
||||||
|
$extName = $m[1];
|
||||||
|
}
|
||||||
|
if (preg_match('/<extension[^>]*type="([^"]+)"/', $xml, $m)) {
|
||||||
|
$extType = $m[1];
|
||||||
|
}
|
||||||
|
if (preg_match('/<element>([^<]+)<\/element>/', $xml, $m)) {
|
||||||
|
$extElement = $m[1];
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$extElement = preg_replace('/^(pkg_|com_|mod_|plg_\w+_|tpl_|lib_)/', '', $extElement);
|
||||||
|
|
||||||
|
if (preg_match('/<extension[^>]*client="([^"]+)"/', $xml, $m)) {
|
||||||
|
$extClient = $m[1];
|
||||||
|
}
|
||||||
|
if (preg_match('/<extension[^>]*group="([^"]+)"/', $xml, $m)) {
|
||||||
|
$extFolder = $m[1];
|
||||||
|
}
|
||||||
|
if (preg_match('/(<targetplatform[^\/]*\/>)/', $xml, $m)) {
|
||||||
|
$targetPlatform = $m[1];
|
||||||
|
}
|
||||||
|
if (empty($targetPlatform)) {
|
||||||
|
$targetPlatform = '<targetplatform name="joomla" version="(5|6)\..*" />';
|
||||||
|
}
|
||||||
|
if (preg_match('/<php_minimum>([^<]+)<\/php_minimum>/', $xml, $m)) {
|
||||||
|
$phpMinimum = $m[1];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Non-Joomla platform — derive metadata from .mokogitea/manifest.xml
|
||||||
|
$extName = $detectedName ?: ($repo ?: basename($root));
|
||||||
|
$extElement = strtolower(str_replace([' ', '-'], '', $extName));
|
||||||
|
$extType = $detectedPackageType ?: 'generic';
|
||||||
|
$targetPlatform = "<targetplatform name=\"{$detectedPlatform}\" version=\".*\" />";
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve language key names (e.g. PLG_SYSTEM_MOKOJOOMTOS)
|
// Resolve language key names (e.g. PLG_SYSTEM_MOKOJOOMTOS)
|
||||||
if (preg_match('/^[A-Z_]+$/', $extName)) {
|
if (preg_match('/^[A-Z_]+$/', $extName)) {
|
||||||
$iniFiles = [];
|
$iniFiles = [];
|
||||||
$iterator = new RecursiveIteratorIterator(
|
$iterator = new RecursiveIteratorIterator(
|
||||||
new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS)
|
new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS)
|
||||||
);
|
);
|
||||||
foreach ($iterator as $file) {
|
foreach ($iterator as $file) {
|
||||||
if (preg_match('/\.sys\.ini$/i', $file->getFilename())) {
|
if (preg_match('/\.sys\.ini$/i', $file->getFilename())) {
|
||||||
$iniFiles[] = $file->getPathname();
|
$iniFiles[] = $file->getPathname();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
foreach ($iniFiles as $ini) {
|
foreach ($iniFiles as $ini) {
|
||||||
$content = file_get_contents($ini);
|
$content = file_get_contents($ini);
|
||||||
if (preg_match('/^' . preg_quote($extName, '/') . '="([^"]+)"/m', $content, $m)) {
|
if (preg_match('/^' . preg_quote($extName, '/') . '="([^"]+)"/m', $content, $m)) {
|
||||||
$extName = $m[1];
|
$extName = $m[1];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallbacks
|
// Fallbacks
|
||||||
if (empty($extName)) $extName = $repo ?: basename($root);
|
if (empty($extName)) {
|
||||||
if (empty($extType)) $extType = 'component';
|
$extName = $repo ?: basename($root);
|
||||||
|
}
|
||||||
|
if (empty($extType)) {
|
||||||
|
$extType = 'component';
|
||||||
|
}
|
||||||
|
|
||||||
// -- Build type prefix --------------------------------------------------------
|
// -- Build type prefix --------------------------------------------------------
|
||||||
$typePrefix = '';
|
$typePrefix = '';
|
||||||
switch ($extType) {
|
switch ($extType) {
|
||||||
case 'plugin': $typePrefix = "plg_{$extFolder}_"; break;
|
case 'plugin':
|
||||||
case 'module': $typePrefix = 'mod_'; break;
|
$typePrefix = "plg_{$extFolder}_";
|
||||||
case 'component': $typePrefix = 'com_'; break;
|
break;
|
||||||
case 'template': $typePrefix = 'tpl_'; break;
|
case 'module':
|
||||||
case 'library': $typePrefix = 'lib_'; break;
|
$typePrefix = 'mod_';
|
||||||
case 'package': $typePrefix = 'pkg_'; break;
|
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 -------------------------------------
|
// -- Export to GITHUB_OUTPUT if requested -------------------------------------
|
||||||
if ($githubOutput) {
|
if ($githubOutput) {
|
||||||
$ghOutput = getenv('GITHUB_OUTPUT');
|
$ghOutput = getenv('GITHUB_OUTPUT');
|
||||||
$lines = [
|
$lines = [
|
||||||
"ext_element={$extElement}",
|
"ext_element={$extElement}",
|
||||||
"ext_name={$extName}",
|
"ext_name={$extName}",
|
||||||
"ext_type={$extType}",
|
"ext_type={$extType}",
|
||||||
"ext_folder={$extFolder}",
|
"ext_folder={$extFolder}",
|
||||||
"type_prefix={$typePrefix}",
|
"type_prefix={$typePrefix}",
|
||||||
];
|
];
|
||||||
if ($ghOutput) {
|
if ($ghOutput) {
|
||||||
file_put_contents($ghOutput, implode("\n", $lines) . "\n", FILE_APPEND);
|
file_put_contents($ghOutput, implode("\n", $lines) . "\n", FILE_APPEND);
|
||||||
fwrite(STDERR, "Exported " . count($lines) . " fields to GITHUB_OUTPUT\n");
|
fwrite(STDERR, "Exported " . count($lines) . " fields to GITHUB_OUTPUT\n");
|
||||||
} else {
|
} else {
|
||||||
foreach ($lines as $line) echo "{$line}\n";
|
foreach ($lines as $line) {
|
||||||
}
|
echo "{$line}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Stability suffix map -----------------------------------------------------
|
// -- Stability suffix map -----------------------------------------------------
|
||||||
$stabilitySuffixMap = [
|
$stabilitySuffixMap = [
|
||||||
'stable' => '',
|
'stable' => '',
|
||||||
'rc' => '-rc',
|
'rc' => '-rc',
|
||||||
'beta' => '-beta',
|
'beta' => '-beta',
|
||||||
'alpha' => '-alpha',
|
'alpha' => '-alpha',
|
||||||
'development' => '-dev',
|
'development' => '-dev',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Joomla <tags><tag> values — maps to Joomla's stabilityTagToInteger()
|
// Joomla <tags><tag> values — maps to Joomla's stabilityTagToInteger()
|
||||||
$stabilityTagMap = [
|
$stabilityTagMap = [
|
||||||
'stable' => 'stable',
|
'stable' => 'stable',
|
||||||
'rc' => 'rc',
|
'rc' => 'rc',
|
||||||
'beta' => 'beta',
|
'beta' => 'beta',
|
||||||
'alpha' => 'alpha',
|
'alpha' => 'alpha',
|
||||||
'development' => 'dev',
|
'development' => 'dev',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Gitea release tag names (used in download/info URLs)
|
// Gitea release tag names (used in download/info URLs)
|
||||||
$releaseTagMap = [
|
$releaseTagMap = [
|
||||||
'stable' => 'stable',
|
'stable' => 'stable',
|
||||||
'rc' => 'release-candidate',
|
'rc' => 'release-candidate',
|
||||||
'beta' => 'beta',
|
'beta' => 'beta',
|
||||||
'alpha' => 'alpha',
|
'alpha' => 'alpha',
|
||||||
'development' => 'development',
|
'development' => 'development',
|
||||||
];
|
];
|
||||||
|
|
||||||
// -- Build update entries -----------------------------------------------------
|
// -- Build update entries -----------------------------------------------------
|
||||||
@@ -221,70 +303,78 @@ $primaryVersion = $version . $primarySuffix;
|
|||||||
// to installed extensions. Without it, extension_id=0 in #__updates.
|
// to installed extensions. Without it, extension_id=0 in #__updates.
|
||||||
$clientTag = '';
|
$clientTag = '';
|
||||||
if (!empty($extClient)) {
|
if (!empty($extClient)) {
|
||||||
$clientTag = " <client>{$extClient}</client>";
|
$clientTag = " <client>{$extClient}</client>";
|
||||||
} else {
|
} else {
|
||||||
$clientTag = ' <client>site</client>';
|
$clientTag = ' <client>site</client>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build folder tag
|
// Build folder tag
|
||||||
$folderTag = '';
|
$folderTag = '';
|
||||||
if (!empty($extFolder) && $extType === 'plugin') {
|
if (!empty($extFolder) && $extType === 'plugin') {
|
||||||
$folderTag = " <folder>{$extFolder}</folder>";
|
$folderTag = " <folder>{$extFolder}</folder>";
|
||||||
}
|
}
|
||||||
|
|
||||||
// PHP minimum tag
|
// PHP minimum tag
|
||||||
$phpTag = '';
|
$phpTag = '';
|
||||||
if (!empty($phpMinimum)) {
|
if (!empty($phpMinimum)) {
|
||||||
$phpTag = " <php_minimum>{$phpMinimum}</php_minimum>";
|
$phpTag = " <php_minimum>{$phpMinimum}</php_minimum>";
|
||||||
}
|
}
|
||||||
|
|
||||||
// SHA tag
|
// SHA tag
|
||||||
$shaTag = '';
|
$shaTag = '';
|
||||||
if (!empty($sha)) {
|
if (!empty($sha)) {
|
||||||
$shaTag = " <sha256>{$sha}</sha256>";
|
$shaTag = " <sha256>{$sha}</sha256>";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a single <update> entry for a given stability tag
|
* Build a single <update> entry for a given stability tag
|
||||||
*/
|
*/
|
||||||
function buildEntry(
|
function buildEntry(
|
||||||
string $tagName,
|
string $tagName,
|
||||||
string $entryVersion,
|
string $entryVersion,
|
||||||
string $entryDownloadUrl,
|
string $entryDownloadUrl,
|
||||||
string $extName,
|
string $extName,
|
||||||
string $extElement,
|
string $extElement,
|
||||||
string $extType,
|
string $extType,
|
||||||
string $clientTag,
|
string $clientTag,
|
||||||
string $folderTag,
|
string $folderTag,
|
||||||
string $infoUrl,
|
string $infoUrl,
|
||||||
string $targetPlatform,
|
string $targetPlatform,
|
||||||
string $phpTag,
|
string $phpTag,
|
||||||
string $shaTag
|
string $shaTag
|
||||||
): string {
|
): string {
|
||||||
$lines = [];
|
$lines = [];
|
||||||
$lines[] = ' <update>';
|
$lines[] = ' <update>';
|
||||||
$lines[] = " <name>{$extName}</name>";
|
$lines[] = " <name>{$extName}</name>";
|
||||||
$lines[] = " <description>{$extName} update</description>";
|
$lines[] = " <description>{$extName} update</description>";
|
||||||
// Element in updates.xml must match what Joomla stores in #__extensions
|
// Element in updates.xml must match what Joomla stores in #__extensions
|
||||||
// For packages: pkg_elementname. For plugins: elementname (folder handles grouping).
|
// For packages: pkg_elementname. For plugins: elementname (folder handles grouping).
|
||||||
$dbElement = ($extType === 'package') ? "pkg_{$extElement}" : $extElement;
|
$dbElement = ($extType === 'package') ? "pkg_{$extElement}" : $extElement;
|
||||||
$lines[] = " <element>{$dbElement}</element>";
|
$lines[] = " <element>{$dbElement}</element>";
|
||||||
$lines[] = " <type>{$extType}</type>";
|
$lines[] = " <type>{$extType}</type>";
|
||||||
$lines[] = " <version>{$entryVersion}</version>";
|
$lines[] = " <version>{$entryVersion}</version>";
|
||||||
if (!empty($clientTag)) $lines[] = $clientTag;
|
if (!empty($clientTag)) {
|
||||||
if (!empty($folderTag)) $lines[] = $folderTag;
|
$lines[] = $clientTag;
|
||||||
$lines[] = " <tags><tag>{$tagName}</tag></tags>";
|
}
|
||||||
$lines[] = " <infourl title=\"{$extName}\">{$infoUrl}</infourl>";
|
if (!empty($folderTag)) {
|
||||||
$lines[] = ' <downloads>';
|
$lines[] = $folderTag;
|
||||||
$lines[] = " <downloadurl type=\"full\" format=\"zip\">{$entryDownloadUrl}</downloadurl>";
|
}
|
||||||
$lines[] = ' </downloads>';
|
$lines[] = " <tags><tag>{$tagName}</tag></tags>";
|
||||||
if (!empty($shaTag)) $lines[] = $shaTag;
|
$lines[] = " <infourl title=\"{$extName}\">{$infoUrl}</infourl>";
|
||||||
$lines[] = " {$targetPlatform}";
|
$lines[] = ' <downloads>';
|
||||||
if (!empty($phpTag)) $lines[] = $phpTag;
|
$lines[] = " <downloadurl type=\"full\" format=\"zip\">{$entryDownloadUrl}</downloadurl>";
|
||||||
$lines[] = ' <maintainer>Moko Consulting</maintainer>';
|
$lines[] = ' </downloads>';
|
||||||
$lines[] = ' <maintainerurl>https://mokoconsulting.tech</maintainerurl>';
|
if (!empty($shaTag)) {
|
||||||
$lines[] = ' </update>';
|
$lines[] = $shaTag;
|
||||||
return implode("\n", $lines);
|
}
|
||||||
|
$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 ----------------------------------------
|
// -- Determine which channels to write ----------------------------------------
|
||||||
@@ -295,7 +385,9 @@ function buildEntry(
|
|||||||
// When dev releases, only dev is updated; everything else is preserved.
|
// When dev releases, only dev is updated; everything else is preserved.
|
||||||
$allChannels = ['development', 'alpha', 'beta', 'rc', 'stable'];
|
$allChannels = ['development', 'alpha', 'beta', 'rc', 'stable'];
|
||||||
$stabilityIndex = array_search($stability === 'development' ? 'development' : $stability, $allChannels);
|
$stabilityIndex = array_search($stability === 'development' ? 'development' : $stability, $allChannels);
|
||||||
if ($stabilityIndex === false) $stabilityIndex = 4; // default to stable
|
if ($stabilityIndex === false) {
|
||||||
|
$stabilityIndex = 4; // default to stable
|
||||||
|
}
|
||||||
|
|
||||||
// Write entries for the current channel AND all lower channels (cascade down)
|
// Write entries for the current channel AND all lower channels (cascade down)
|
||||||
// All cascaded entries point to the CURRENT release (the highest stability being built)
|
// All cascaded entries point to the CURRENT release (the highest stability being built)
|
||||||
@@ -306,25 +398,25 @@ $channelDownloadUrl = "{$giteaUrl}/{$org}/{$repo}/releases/download/{$giteaTag}/
|
|||||||
$channelInfoUrl = "{$giteaUrl}/{$org}/{$repo}/releases/tag/{$giteaTag}";
|
$channelInfoUrl = "{$giteaUrl}/{$org}/{$repo}/releases/tag/{$giteaTag}";
|
||||||
|
|
||||||
for ($i = 0; $i <= $stabilityIndex; $i++) {
|
for ($i = 0; $i <= $stabilityIndex; $i++) {
|
||||||
$channelName = $allChannels[$i];
|
$channelName = $allChannels[$i];
|
||||||
$joomlaTag = $stabilityTagMap[$channelName] ?? $channelName;
|
$joomlaTag = $stabilityTagMap[$channelName] ?? $channelName;
|
||||||
// Only attach SHA to the primary channel entry
|
// Only attach SHA to the primary channel entry
|
||||||
$entrySha = ($i === $stabilityIndex) ? $shaTag : '';
|
$entrySha = ($i === $stabilityIndex) ? $shaTag : '';
|
||||||
|
|
||||||
$entries[] = buildEntry(
|
$entries[] = buildEntry(
|
||||||
$joomlaTag,
|
$joomlaTag,
|
||||||
$channelVersion,
|
$channelVersion,
|
||||||
$channelDownloadUrl,
|
$channelDownloadUrl,
|
||||||
$extName,
|
$extName,
|
||||||
$extElement,
|
$extElement,
|
||||||
$extType,
|
$extType,
|
||||||
$clientTag,
|
$clientTag,
|
||||||
$folderTag,
|
$folderTag,
|
||||||
$channelInfoUrl,
|
$channelInfoUrl,
|
||||||
$targetPlatform,
|
$targetPlatform,
|
||||||
$phpTag,
|
$phpTag,
|
||||||
$entrySha
|
$entrySha
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Preserve existing entries for channels not being updated -----------------
|
// -- Preserve existing entries for channels not being updated -----------------
|
||||||
@@ -332,27 +424,27 @@ $dest = $outputFile ?? "{$root}/updates.xml";
|
|||||||
$preservedEntries = [];
|
$preservedEntries = [];
|
||||||
|
|
||||||
if (file_exists($dest)) {
|
if (file_exists($dest)) {
|
||||||
$existingXml = @simplexml_load_file($dest);
|
$existingXml = @simplexml_load_file($dest);
|
||||||
if ($existingXml) {
|
if ($existingXml) {
|
||||||
// Joomla tags we're writing — don't preserve these
|
// Joomla tags we're writing — don't preserve these
|
||||||
$writtenChannels = [];
|
$writtenChannels = [];
|
||||||
for ($i = 0; $i <= $stabilityIndex; $i++) {
|
for ($i = 0; $i <= $stabilityIndex; $i++) {
|
||||||
$writtenChannels[] = $stabilityTagMap[$allChannels[$i]] ?? $allChannels[$i];
|
$writtenChannels[] = $stabilityTagMap[$allChannels[$i]] ?? $allChannels[$i];
|
||||||
}
|
}
|
||||||
// Also match legacy/alternate tag names (e.g. 'development' = 'dev')
|
// Also match legacy/alternate tag names (e.g. 'development' = 'dev')
|
||||||
$writtenChannels[] = 'development'; // alias for 'dev'
|
$writtenChannels[] = 'development'; // alias for 'dev'
|
||||||
|
|
||||||
foreach ($existingXml->update as $existingUpdate) {
|
foreach ($existingXml->update as $existingUpdate) {
|
||||||
$existingTag = '';
|
$existingTag = '';
|
||||||
if (isset($existingUpdate->tags->tag)) {
|
if (isset($existingUpdate->tags->tag)) {
|
||||||
$existingTag = (string) $existingUpdate->tags->tag;
|
$existingTag = (string) $existingUpdate->tags->tag;
|
||||||
}
|
}
|
||||||
// Keep entries for channels we're NOT overwriting
|
// Keep entries for channels we're NOT overwriting
|
||||||
if (!empty($existingTag) && !in_array($existingTag, $writtenChannels, true)) {
|
if (!empty($existingTag) && !in_array($existingTag, $writtenChannels, true)) {
|
||||||
$preservedEntries[] = ' ' . trim($existingUpdate->asXML());
|
$preservedEntries[] = ' ' . trim($existingUpdate->asXML());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Write updates.xml --------------------------------------------------------
|
// -- Write updates.xml --------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
#!/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.Enterprise
|
||||||
|
* INGROUP: moko-platform
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
* PATH: /lib/Enterprise/ManifestReader.php
|
||||||
|
* BRIEF: Read and parse .mokogitea/manifest.xml — shared across all CLI tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace MokoEnterprise;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manifest Reader
|
||||||
|
*
|
||||||
|
* Parses .mokogitea/manifest.xml and provides typed access to all fields.
|
||||||
|
* Used by CLI tools and the Enterprise library to determine platform,
|
||||||
|
* build configuration, and deployment settings from the repository manifest.
|
||||||
|
*
|
||||||
|
* @since 09.01.00
|
||||||
|
*/
|
||||||
|
class ManifestReader
|
||||||
|
{
|
||||||
|
/** @var array<string, string> Parsed manifest fields */
|
||||||
|
private array $fields = [];
|
||||||
|
|
||||||
|
/** @var bool Whether a manifest was found and parsed */
|
||||||
|
private bool $loaded = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load manifest from a repository root directory.
|
||||||
|
*
|
||||||
|
* @param string $root Repository root path
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function fromPath(string $root): self
|
||||||
|
{
|
||||||
|
$reader = new self();
|
||||||
|
$reader->load($root);
|
||||||
|
return $reader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load and parse the manifest file.
|
||||||
|
*
|
||||||
|
* @param string $root Repository root path
|
||||||
|
*/
|
||||||
|
public function load(string $root): void
|
||||||
|
{
|
||||||
|
$candidates = [
|
||||||
|
"{$root}/.mokogitea/manifest.xml",
|
||||||
|
"{$root}/.mokogitea/.manifest.xml",
|
||||||
|
"{$root}/.mokogitea/.moko-platform",
|
||||||
|
];
|
||||||
|
|
||||||
|
$manifestFile = null;
|
||||||
|
foreach ($candidates as $candidate) {
|
||||||
|
if (file_exists($candidate)) {
|
||||||
|
$manifestFile = $candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($manifestFile === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$xml = @simplexml_load_file($manifestFile);
|
||||||
|
if ($xml === false) {
|
||||||
|
// Fallback: YAML legacy format
|
||||||
|
$content = file_get_contents($manifestFile);
|
||||||
|
if (preg_match('/^platform:\s*(.+)/m', $content, $m)) {
|
||||||
|
$this->fields['platform'] = trim($m[1], " \t\n\r\"'");
|
||||||
|
}
|
||||||
|
$this->loaded = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->fields = [
|
||||||
|
'name' => (string)($xml->identity->name ?? ''),
|
||||||
|
'org' => (string)($xml->identity->org ?? ''),
|
||||||
|
'description' => (string)($xml->identity->description ?? ''),
|
||||||
|
'license' => (string)($xml->identity->license ?? ''),
|
||||||
|
'license-spdx' => (string)($xml->identity->license['spdx'] ?? ''),
|
||||||
|
'version' => (string)($xml->identity->version ?? ''),
|
||||||
|
'platform' => (string)($xml->governance->platform ?? ''),
|
||||||
|
'standards-version' => (string)($xml->governance->{"standards-version"} ?? ''),
|
||||||
|
'language' => (string)($xml->build->language ?? ''),
|
||||||
|
'package-type' => (string)($xml->build->{"package-type"} ?? ''),
|
||||||
|
'entry-point' => (string)($xml->build->{"entry-point"} ?? ''),
|
||||||
|
'source-dir' => (string)($xml->deploy->{"source-dir"} ?? ''),
|
||||||
|
'remote-subdir' => (string)($xml->deploy->{"remote-subdir"} ?? ''),
|
||||||
|
'dev-host' => (string)($xml->deploy->{"dev-host"} ?? ''),
|
||||||
|
'demo-host' => (string)($xml->deploy->{"demo-host"} ?? ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Strip empty values
|
||||||
|
$this->fields = array_filter($this->fields, fn($v) => $v !== '');
|
||||||
|
$this->loaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a manifest was found and loaded.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isLoaded(): bool
|
||||||
|
{
|
||||||
|
return $this->loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single field value.
|
||||||
|
*
|
||||||
|
* @param string $key Field name (e.g. 'platform', 'package-type')
|
||||||
|
* @param string $default Default value if field is missing
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function get(string $key, string $default = ''): string
|
||||||
|
{
|
||||||
|
return $this->fields[$key] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the platform slug, normalized to canonical values.
|
||||||
|
*
|
||||||
|
* @return string One of: joomla, dolibarr, generic, mcp, nodejs
|
||||||
|
*/
|
||||||
|
public function getPlatform(): string
|
||||||
|
{
|
||||||
|
$raw = $this->get('platform', 'generic');
|
||||||
|
return match ($raw) {
|
||||||
|
'waas-component' => 'joomla',
|
||||||
|
'crm-module' => 'dolibarr',
|
||||||
|
default => $raw,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the source/entry-point directory.
|
||||||
|
*
|
||||||
|
* @param string $root Repository root for existence checking
|
||||||
|
* @return string Resolved source directory path (e.g. 'src', 'htdocs')
|
||||||
|
*/
|
||||||
|
public function getSourceDir(string $root = ''): string
|
||||||
|
{
|
||||||
|
$entryPoint = $this->get('entry-point', '');
|
||||||
|
if ($entryPoint !== '') {
|
||||||
|
// Strip trailing filename (e.g. src/index.ts → src)
|
||||||
|
$dir = rtrim(dirname($entryPoint) === '.' ? $entryPoint : dirname($entryPoint), '/');
|
||||||
|
if ($root === '' || is_dir("{$root}/{$dir}")) {
|
||||||
|
return $dir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: check common directories
|
||||||
|
if ($root !== '') {
|
||||||
|
if (is_dir("{$root}/src")) {
|
||||||
|
return 'src';
|
||||||
|
}
|
||||||
|
if (is_dir("{$root}/htdocs")) {
|
||||||
|
return 'htdocs';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'src';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the package type for build decisions.
|
||||||
|
*
|
||||||
|
* @return string e.g. 'package', 'dolibarr', 'generic', 'mcp-server'
|
||||||
|
*/
|
||||||
|
public function getPackageType(): string
|
||||||
|
{
|
||||||
|
return $this->get('package-type', 'generic');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all parsed fields.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function getAll(): array
|
||||||
|
{
|
||||||
|
return $this->fields;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user