Files
Jonathan Miller 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
style: fix all PHPCS PSR-12 violations across 74 files (7539 → 0 errors)
- 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>
2026-05-24 17:07:51 -05:00

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);
}
}