b3d9ee8255
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 36s
Wrap all CLI tools in cli/, automation/, maintenance/, deploy/, and release/ in classes extending CliFramework. Replaces manual $argv parsing with configure()/addArgument(), moves logic into run(): int, and converts fwrite(STDERR,...) to $this->log(). Two CLIApp subclasses (generate_dolibarr_version_txt, generate_joomla_update_xml) converted to extend CliFramework directly. Every script now gets free --help, --verbose, --quiet, --dry-run, --json, --no-color, banners, coloured logging, and progress bars. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
315 lines
13 KiB
PHP
315 lines
13 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/joomla_build.php
|
|
* VERSION: 09.21.07
|
|
* BRIEF: Build a Joomla extension ZIP from manifest — all types supported
|
|
* NOTE: Called by pre-release and auto-release workflows.
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
|
|
|
use MokoEnterprise\CliFramework;
|
|
|
|
class JoomlaBuildCli extends CliFramework
|
|
{
|
|
protected function configure(): void
|
|
{
|
|
$this->setDescription('Build a Joomla extension ZIP from manifest');
|
|
$this->addArgument('--path', 'Repository root path', '.');
|
|
$this->addArgument('--version', 'Version string (required)', '');
|
|
$this->addArgument('--suffix', 'Version suffix (e.g. -dev)', '');
|
|
$this->addArgument('--output', 'Output directory', 'build');
|
|
$this->addArgument('--github-output', 'Write outputs to GITHUB_OUTPUT file', false);
|
|
}
|
|
|
|
protected function run(): int
|
|
{
|
|
$path = $this->getArgument('--path');
|
|
$version = $this->getArgument('--version');
|
|
$suffix = $this->getArgument('--suffix');
|
|
$outputDir = $this->getArgument('--output');
|
|
$ghOutput = (bool) $this->getArgument('--github-output');
|
|
|
|
if ($version === '') {
|
|
$this->log('ERROR', '::error::--version is required');
|
|
return 1;
|
|
}
|
|
|
|
$path = realpath($path) ?: $path;
|
|
|
|
// ── Find source directory ──────────────────────────────────────────────
|
|
$srcDir = null;
|
|
foreach (['src', 'htdocs'] as $d) {
|
|
if (is_dir("{$path}/{$d}")) { $srcDir = "{$path}/{$d}"; break; }
|
|
}
|
|
if ($srcDir === null) {
|
|
$this->log('ERROR', "::error::No src/ or htdocs/ directory in {$path}");
|
|
return 1;
|
|
}
|
|
|
|
// ── Find manifest ──────────────────────────────────────────────────────
|
|
$manifest = $this->findManifest($srcDir);
|
|
if ($manifest === null) {
|
|
$this->log('ERROR', "::error::No Joomla manifest found in {$srcDir}");
|
|
return 1;
|
|
}
|
|
|
|
$this->log('INFO', "Manifest: {$manifest}");
|
|
|
|
// ── Parse manifest ─────────────────────────────────────────────────────
|
|
$meta = $this->parseManifest($manifest);
|
|
|
|
// Resolve language-key names (e.g. PLG_SYSTEM_MOKOWAAS -> "System - Moko WaaS")
|
|
if (preg_match('/^[A-Z_]+$/', $meta['name'])) {
|
|
$resolved = $this->resolveLanguageKey($srcDir, $meta['name']);
|
|
if ($resolved !== null) { $meta['name'] = $resolved; }
|
|
}
|
|
|
|
$prefix = $this->typePrefix($meta);
|
|
$zipName = "{$prefix}{$meta['element']}-{$version}{$suffix}.zip";
|
|
$zipPath = "{$outputDir}/{$zipName}";
|
|
|
|
$this->log('INFO', "=== Joomla Build: {$meta['type']} — {$meta['element']} {$version}{$suffix} ===");
|
|
$this->log('INFO', " Type: {$meta['type']}");
|
|
$this->log('INFO', " Element: {$meta['element']}");
|
|
$this->log('INFO', " Group: " . ($meta['group'] ?: 'n/a'));
|
|
$this->log('INFO', " Name: {$meta['name']}");
|
|
$this->log('INFO', " Output: {$zipName}");
|
|
|
|
// ── Build ──────────────────────────────────────────────────────────────
|
|
if (!is_dir($outputDir)) { mkdir($outputDir, 0755, true); }
|
|
|
|
if ($meta['type'] === 'package') {
|
|
$this->buildPackageZip($srcDir, $zipPath);
|
|
} else {
|
|
$this->buildZip($srcDir, $zipPath);
|
|
}
|
|
|
|
$sha256 = hash_file('sha256', $zipPath);
|
|
$size = filesize($zipPath);
|
|
|
|
$this->log('INFO', "Package: {$zipPath} ({$size} bytes, SHA: " . substr($sha256, 0, 16) . "...)");
|
|
|
|
// ── Output variables ───────────────────────────────────────────────────
|
|
$vars = [
|
|
'zip_name' => $zipName,
|
|
'zip_path' => $zipPath,
|
|
'sha256' => $sha256,
|
|
'ext_type' => $meta['type'],
|
|
'ext_element' => $meta['element'],
|
|
'ext_name' => $meta['name'],
|
|
'ext_group' => $meta['group'],
|
|
'type_prefix' => $prefix,
|
|
];
|
|
|
|
if ($ghOutput && ($ghFile = getenv('GITHUB_OUTPUT')) !== false && $ghFile !== '') {
|
|
$fh = fopen($ghFile, 'a');
|
|
foreach ($vars as $k => $v) { fwrite($fh, "{$k}={$v}\n"); }
|
|
fclose($fh);
|
|
$this->log('INFO', "Wrote " . count($vars) . " outputs to GITHUB_OUTPUT");
|
|
} else {
|
|
foreach ($vars as $k => $v) { echo "{$k}={$v}\n"; }
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// Private methods
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
private function findManifest(string $dir): ?string
|
|
{
|
|
// Priority: pkg_*.xml (packages), then any *.xml with <extension>
|
|
foreach (glob("{$dir}/pkg_*.xml") ?: [] as $f) { return $f; }
|
|
foreach (glob("{$dir}/*.xml") ?: [] as $f) {
|
|
if (str_contains((string) file_get_contents($f), '<extension')) { return $f; }
|
|
}
|
|
// Broader nested search
|
|
$iter = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
|
|
RecursiveIteratorIterator::SELF_FIRST
|
|
);
|
|
foreach ($iter as $item) {
|
|
if ($item->isFile() && $item->getExtension() === 'xml') {
|
|
if (str_contains((string) file_get_contents($item->getPathname()), '<extension')) {
|
|
return $item->getPathname();
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private function parseManifest(string $file): array
|
|
{
|
|
$xml = simplexml_load_file($file);
|
|
$name = (string) ($xml->name ?? '');
|
|
$type = (string) ($xml->attributes()->type ?? 'component');
|
|
$element = (string) ($xml->element ?? '');
|
|
$group = (string) ($xml->attributes()->group ?? '');
|
|
|
|
// For packages, prefer <packagename> as the clean element (avoids pkg_pkg_ duplication)
|
|
if ($type === 'package' && $element === '') {
|
|
$packageName = (string) ($xml->packagename ?? '');
|
|
if ($packageName !== '') {
|
|
$element = $packageName;
|
|
}
|
|
}
|
|
|
|
// Fallback element detection
|
|
if ($element === '') { $element = (string) ($xml->attributes()->plugin ?? ''); }
|
|
if ($element === '') { $element = (string) ($xml->attributes()->module ?? ''); }
|
|
if ($element === '') {
|
|
$element = strtolower(basename($file, '.xml'));
|
|
if (in_array($element, ['templatedetails', 'manifest'], true)) {
|
|
$element = strtolower(basename(dirname($file)));
|
|
}
|
|
}
|
|
|
|
// Strip existing type prefix to prevent duplication (e.g. pkg_mokowaas -> mokowaas)
|
|
$element = preg_replace('/^(pkg_|com_|mod_|plg_\w+_|tpl_|lib_)/', '', $element);
|
|
|
|
if ($name === '') { $name = $element; }
|
|
|
|
return compact('name', 'type', 'element', 'group');
|
|
}
|
|
|
|
private function typePrefix(array $meta): string
|
|
{
|
|
return match ($meta['type']) {
|
|
'plugin' => "plg_{$meta['group']}_",
|
|
'module' => 'mod_',
|
|
'component' => 'com_',
|
|
'template' => 'tpl_',
|
|
'package' => 'pkg_',
|
|
'library' => 'lib_',
|
|
default => '',
|
|
};
|
|
}
|
|
|
|
private function resolveLanguageKey(string $srcDir, string $key): ?string
|
|
{
|
|
$iter = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($srcDir, FilesystemIterator::SKIP_DOTS)
|
|
);
|
|
foreach ($iter as $item) {
|
|
if ($item->isFile() && str_ends_with($item->getFilename(), '.sys.ini')) {
|
|
foreach (file($item->getPathname()) as $line) {
|
|
if (preg_match('/^' . preg_quote($key, '/') . '="(.+)"/', trim($line), $m)) {
|
|
return $m[1];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private function isExcluded(string $name): bool
|
|
{
|
|
if ($name === '.ftpignore') return true;
|
|
if (str_starts_with($name, 'sftp-config')) return true;
|
|
if (str_starts_with($name, '.env')) return true;
|
|
if (str_starts_with($name, '.build-trigger')) return true;
|
|
$ext = pathinfo($name, PATHINFO_EXTENSION);
|
|
return in_array($ext, ['ppk', 'pem', 'key', 'local'], true);
|
|
}
|
|
|
|
private function buildZip(string $srcDir, string $outPath): void
|
|
{
|
|
$zip = new ZipArchive();
|
|
if ($zip->open($outPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
|
$this->log('ERROR', "::error::Cannot create ZIP: {$outPath}");
|
|
return;
|
|
}
|
|
$iter = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($srcDir, FilesystemIterator::SKIP_DOTS),
|
|
RecursiveIteratorIterator::SELF_FIRST
|
|
);
|
|
foreach ($iter as $file) {
|
|
$local = str_replace('\\', '/', substr($file->getPathname(), strlen($srcDir) + 1));
|
|
if ($this->isExcluded(basename($local))) continue;
|
|
$file->isDir() ? $zip->addEmptyDir($local) : $zip->addFile($file->getPathname(), $local);
|
|
}
|
|
$zip->close();
|
|
}
|
|
|
|
private function buildPackageZip(string $srcDir, string $outPath): void
|
|
{
|
|
$this->log('INFO', "Building Joomla package (multi-extension)...");
|
|
$staging = sys_get_temp_dir() . '/moko_pkg_' . uniqid();
|
|
mkdir($staging, 0755, true);
|
|
|
|
// 1. Zip each sub-extension in packages/
|
|
$packagesDir = "{$srcDir}/packages";
|
|
if (is_dir($packagesDir)) {
|
|
foreach (glob("{$packagesDir}/*", GLOB_ONLYDIR) as $extDir) {
|
|
$subManifest = $this->findManifest($extDir);
|
|
if ($subManifest) {
|
|
$sub = $this->parseManifest($subManifest);
|
|
$subPrefix = $this->typePrefix($sub);
|
|
$subZipName = "{$subPrefix}{$sub['element']}.zip";
|
|
} else {
|
|
$subZipName = basename($extDir) . '.zip';
|
|
}
|
|
|
|
$this->log('INFO', " Sub-extension: {$subZipName}");
|
|
$this->buildZip($extDir, "{$staging}/{$subZipName}");
|
|
}
|
|
}
|
|
|
|
// 2. Copy package-level files (manifest, script, language)
|
|
foreach (glob("{$srcDir}/*.xml") ?: [] as $f) copy($f, "{$staging}/" . basename($f));
|
|
foreach (glob("{$srcDir}/*.php") ?: [] as $f) copy($f, "{$staging}/" . basename($f));
|
|
foreach (['language', 'administrator'] as $d) {
|
|
if (is_dir("{$srcDir}/{$d}")) {
|
|
$this->copyTree("{$srcDir}/{$d}", "{$staging}/{$d}");
|
|
}
|
|
}
|
|
|
|
// 3. Create outer zip
|
|
$this->buildZip($staging, $outPath);
|
|
|
|
// Cleanup
|
|
$this->rmTree($staging);
|
|
}
|
|
|
|
private function copyTree(string $src, string $dst): void
|
|
{
|
|
if (!is_dir($dst)) mkdir($dst, 0755, true);
|
|
$iter = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($src, FilesystemIterator::SKIP_DOTS),
|
|
RecursiveIteratorIterator::SELF_FIRST
|
|
);
|
|
foreach ($iter as $item) {
|
|
$target = "{$dst}/" . $iter->getSubPathname();
|
|
$item->isDir() ? (is_dir($target) || mkdir($target, 0755, true)) : copy($item->getPathname(), $target);
|
|
}
|
|
}
|
|
|
|
private function rmTree(string $dir): void
|
|
{
|
|
if (!is_dir($dir)) return;
|
|
$iter = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
|
|
RecursiveIteratorIterator::CHILD_FIRST
|
|
);
|
|
foreach ($iter as $item) {
|
|
$item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname());
|
|
}
|
|
rmdir($dir);
|
|
}
|
|
}
|
|
|
|
$app = new JoomlaBuildCli();
|
|
exit($app->execute());
|