4cc3f5bee4
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 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Successful in 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Generic: Repo Health / Release configuration (push) Successful in 5s
Generic: Repo Health / Scripts governance (push) Successful in 5s
Generic: Repo Health / Release configuration (pull_request) Successful in 6s
Generic: Repo Health / Scripts governance (pull_request) Successful in 6s
Generic: Repo Health / Repository health (push) Successful in 14s
Generic: Repo Health / Repository health (pull_request) Successful in 12s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 44s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 49s
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been skipped
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been skipped
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
- Convert tabs to spaces (3,413 violations) - Fix line endings, trailing whitespace, brace placement - Break lines exceeding 150-char absolute limit - Replace heredoc tab closers with spaces - Fix empty elseif, forbidden function calls - Update phpcs.xml: exclude rules inappropriate for CLI scripts (SideEffects, MissingNamespace, MultipleClasses, HeaderOrder, empty catch blocks) Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
293 lines
9.8 KiB
PHP
293 lines
9.8 KiB
PHP
<?php
|
|
|
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
*
|
|
* This file is part of a Moko Consulting project.
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* FILE INFORMATION
|
|
* DEFGROUP: MokoStandards.Enterprise
|
|
* INGROUP: MokoStandards.Lib
|
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
* PATH: /lib/Enterprise/PackageBuilder.php
|
|
* BRIEF: Builds release packages for generic, Dolibarr module, and Joomla component projects
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace MokoEnterprise;
|
|
|
|
use RecursiveDirectoryIterator;
|
|
use RecursiveIteratorIterator;
|
|
use SplFileInfo;
|
|
use ZipArchive;
|
|
|
|
/**
|
|
* Static factory that creates distributable ZIP release packages.
|
|
*
|
|
* Supports three project types: generic (src/admin/site layout), Dolibarr module
|
|
* (src/ layout), and Joomla component (site/admin/media/language layout).
|
|
* All methods return the path to the created archive (or would-create path in dry-run).
|
|
*/
|
|
class PackageBuilder
|
|
{
|
|
// ── Public API ────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Build a generic release package.
|
|
*
|
|
* Copies src/, admin/, site/, top-level *.xml files, LICENSE* files, and
|
|
* CHANGELOG.md into a build staging directory, then archives them as
|
|
* dist/<packageName>-<version>.zip.
|
|
*
|
|
* @param string $repoRoot Absolute path to the repository root.
|
|
* @param string $packageName Base name for the archive.
|
|
* @param string $version Version string (e.g. "1.2.0").
|
|
* @param bool $dryRun When true, preview without writing.
|
|
* @return string Path to the created archive (or would-create path in dry-run).
|
|
* @throws \RuntimeException When the zip archive cannot be opened.
|
|
*/
|
|
public static function buildGeneric(
|
|
string $repoRoot,
|
|
string $packageName,
|
|
string $version,
|
|
bool $dryRun = false
|
|
): string {
|
|
$buildDir = $repoRoot . '/build';
|
|
$packageDir = $buildDir . '/' . $packageName;
|
|
$distDir = $repoRoot . '/dist';
|
|
$archivePath = $distDir . '/' . $packageName . '-' . $version . '.zip';
|
|
|
|
if ($dryRun) {
|
|
return $archivePath;
|
|
}
|
|
|
|
self::cleanDir($buildDir);
|
|
self::cleanDir($distDir);
|
|
mkdir($packageDir, 0755, true);
|
|
mkdir($distDir, 0755, true);
|
|
|
|
foreach (['src', 'admin', 'site'] as $dir) {
|
|
if (is_dir($repoRoot . '/' . $dir)) {
|
|
self::copyDirectory($repoRoot . '/' . $dir, $packageDir . '/' . $dir);
|
|
}
|
|
}
|
|
|
|
foreach (glob($repoRoot . '/*.xml') ?: [] as $xml) {
|
|
copy($xml, $packageDir . '/' . basename($xml));
|
|
}
|
|
|
|
foreach (glob($repoRoot . '/LICENSE*') ?: [] as $lic) {
|
|
copy($lic, $packageDir . '/' . basename($lic));
|
|
}
|
|
|
|
if (is_file($repoRoot . '/CHANGELOG.md')) {
|
|
copy($repoRoot . '/CHANGELOG.md', $packageDir . '/CHANGELOG.md');
|
|
}
|
|
|
|
self::zip($packageDir, $archivePath, $packageName);
|
|
|
|
return $archivePath;
|
|
}
|
|
|
|
/**
|
|
* Build a Dolibarr module release package.
|
|
*
|
|
* Copies everything under src/ into a build staging directory and archives
|
|
* it as dist/<MODULE_NAME>_<VERSION>.zip.
|
|
*
|
|
* @param string $repoRoot Absolute path to the repository root.
|
|
* @param string $moduleName Module name (used in archive filename).
|
|
* @param string $version Version string.
|
|
* @param bool $dryRun When true, preview without writing.
|
|
* @return string Path to the created archive (or would-create path in dry-run).
|
|
* @throws \RuntimeException When src/ is absent or archive creation fails.
|
|
*/
|
|
public static function buildDolibarr(
|
|
string $repoRoot,
|
|
string $moduleName,
|
|
string $version,
|
|
bool $dryRun = false
|
|
): string {
|
|
$srcDir = $repoRoot . '/src';
|
|
$buildDir = $repoRoot . '/build';
|
|
$distDir = $repoRoot . '/dist';
|
|
$archivePath = $distDir . '/' . $moduleName . '_' . $version . '.zip';
|
|
|
|
if (!is_dir($srcDir)) {
|
|
throw new \RuntimeException("src/ directory not found at {$srcDir}");
|
|
}
|
|
|
|
if ($dryRun) {
|
|
return $archivePath;
|
|
}
|
|
|
|
self::cleanDir($buildDir);
|
|
self::cleanDir($distDir);
|
|
mkdir($buildDir, 0755, true);
|
|
mkdir($distDir, 0755, true);
|
|
|
|
self::copyDirectory($srcDir, $buildDir);
|
|
self::zip($buildDir, $archivePath, '');
|
|
|
|
return $archivePath;
|
|
}
|
|
|
|
/**
|
|
* Build a Joomla component release package.
|
|
*
|
|
* Copies site/, admin/, optional media/ and language/ directories, and the
|
|
* component XML manifest into a build staging directory, then archives as
|
|
* dist/<componentName>_<version>.zip.
|
|
*
|
|
* @param string $repoRoot Absolute path to the repository root.
|
|
* @param string $componentName Component name, e.g. "com_example".
|
|
* @param string $version Version string.
|
|
* @param bool $dryRun When true, preview without writing.
|
|
* @return string Path to the created archive (or would-create path in dry-run).
|
|
* @throws \RuntimeException When required directories are absent or archiving fails.
|
|
*/
|
|
public static function buildJoomla(
|
|
string $repoRoot,
|
|
string $componentName,
|
|
string $version,
|
|
bool $dryRun = false
|
|
): string {
|
|
$buildDir = $repoRoot . '/build';
|
|
$distDir = $repoRoot . '/dist';
|
|
$archivePath = $distDir . '/' . $componentName . '_' . $version . '.zip';
|
|
|
|
if ($dryRun) {
|
|
return $archivePath;
|
|
}
|
|
|
|
self::cleanDir($buildDir);
|
|
self::cleanDir($distDir);
|
|
mkdir($buildDir, 0755, true);
|
|
mkdir($distDir, 0755, true);
|
|
|
|
foreach (['site', 'admin'] as $required) {
|
|
$src = $repoRoot . '/' . $required;
|
|
if (!is_dir($src)) {
|
|
throw new \RuntimeException("Required directory '{$required}/' not found at {$src}");
|
|
}
|
|
self::copyDirectory($src, $buildDir . '/' . $required);
|
|
}
|
|
|
|
foreach (['media', 'language'] as $optional) {
|
|
$src = $repoRoot . '/' . $optional;
|
|
if (is_dir($src)) {
|
|
self::copyDirectory($src, $buildDir . '/' . $optional);
|
|
}
|
|
}
|
|
|
|
$manifest = $repoRoot . '/' . $componentName . '.xml';
|
|
if (is_file($manifest)) {
|
|
copy($manifest, $buildDir . '/' . $componentName . '.xml');
|
|
}
|
|
|
|
self::zip($buildDir, $archivePath, '');
|
|
|
|
return $archivePath;
|
|
}
|
|
|
|
// ── Private helpers ───────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Remove a directory if it exists, then recreate it.
|
|
*
|
|
* @param string $dir Directory path to clean.
|
|
*/
|
|
private static function cleanDir(string $dir): void
|
|
{
|
|
if (is_dir($dir)) {
|
|
self::deleteDirectory($dir);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recursively copy a source directory to a destination.
|
|
*
|
|
* @param string $src Source directory path.
|
|
* @param string $dst Destination directory path.
|
|
*/
|
|
private static function copyDirectory(string $src, string $dst): void
|
|
{
|
|
if (!is_dir($dst)) {
|
|
mkdir($dst, 0755, true);
|
|
}
|
|
|
|
$iter = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($src, RecursiveDirectoryIterator::SKIP_DOTS),
|
|
RecursiveIteratorIterator::SELF_FIRST
|
|
);
|
|
|
|
foreach ($iter as $item) {
|
|
/** @var SplFileInfo $item */
|
|
$target = $dst . '/' . $iter->getSubPathname();
|
|
if ($item->isDir()) {
|
|
if (!is_dir($target)) {
|
|
mkdir($target, 0755, true);
|
|
}
|
|
} else {
|
|
copy($item->getPathname(), $target);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a ZIP archive from a source directory tree.
|
|
*
|
|
* @param string $sourceDir Directory to archive.
|
|
* @param string $archivePath Destination archive path.
|
|
* @param string $prefix Path prefix inside the archive (empty string for no prefix).
|
|
* @throws \RuntimeException When the archive cannot be opened for writing.
|
|
*/
|
|
private static function zip(string $sourceDir, string $archivePath, string $prefix): void
|
|
{
|
|
$zip = new ZipArchive();
|
|
if ($zip->open($archivePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
|
throw new \RuntimeException("Cannot create archive: {$archivePath}");
|
|
}
|
|
|
|
$iter = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($sourceDir, RecursiveDirectoryIterator::SKIP_DOTS),
|
|
RecursiveIteratorIterator::SELF_FIRST
|
|
);
|
|
|
|
foreach ($iter as $item) {
|
|
/** @var SplFileInfo $item */
|
|
$rel = $iter->getSubPathname();
|
|
$name = $prefix !== '' ? $prefix . '/' . $rel : $rel;
|
|
if ($item->isFile()) {
|
|
$zip->addFile($item->getPathname(), $name);
|
|
} elseif ($item->isDir()) {
|
|
$zip->addEmptyDir($name);
|
|
}
|
|
}
|
|
|
|
$zip->close();
|
|
}
|
|
|
|
/**
|
|
* Recursively delete a directory and all its contents.
|
|
*
|
|
* @param string $dir Directory path.
|
|
*/
|
|
private static function deleteDirectory(string $dir): void
|
|
{
|
|
if (!is_dir($dir)) {
|
|
return;
|
|
}
|
|
|
|
$items = array_diff((array) scandir($dir), ['.', '..']);
|
|
foreach ($items as $item) {
|
|
$path = $dir . '/' . $item;
|
|
is_dir($path) ? self::deleteDirectory($path) : unlink($path);
|
|
}
|
|
|
|
rmdir($dir);
|
|
}
|
|
}
|