From c90a5671bd5d007e4f1cbb76017a906e70c0ced0 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sat, 6 Jun 2026 07:19:19 -0500 Subject: [PATCH] feat(cli): add manifest_licensing.php for update server and dlid management New CLI tool that reads from manifest.xml and ensures Joomla extension manifests have correct updateservers, dlid, and blockChildUninstall tags. Supports dry-run and --fix modes. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- cli/manifest_licensing.php | 280 +++++++++++++++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 cli/manifest_licensing.php diff --git a/cli/manifest_licensing.php b/cli/manifest_licensing.php new file mode 100644 index 0000000..7156a51 --- /dev/null +++ b/cli/manifest_licensing.php @@ -0,0 +1,280 @@ +#!/usr/bin/env php + + * + * 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/manifest_licensing.php + * VERSION: 01.00.00 + * BRIEF: Ensure licensing tags (updateservers, dlid) in Joomla extension manifests + */ + +declare(strict_types=1); + +require_once __DIR__ . '/../lib/Enterprise/CliFramework.php'; + +use MokoEnterprise\CliFramework; + +/** + * Reads the block from .mokogitea/manifest.xml and ensures that the + * Joomla extension manifest contains the correct and tags. + * + * manifest.xml licensing block example: + * + * + * true + * true + * https://git.mokoconsulting.tech/{org}/{repo}/updates.xml + * MyExtension Updates + * + * + * Supports {org} and {repo} placeholders in update-server URL, resolved from + * the manifest's block or git remote. + */ +class ManifestLicensingCli extends CliFramework +{ + protected function configure(): void + { + $this->setDescription('Ensure licensing tags (updateservers, dlid) in Joomla extension manifests'); + $this->addArgument('--path', 'Repository root path', '.'); + $this->addArgument('--fix', 'Apply fixes (default: dry-run check only)', false); + $this->addArgument('--github-output', 'Write results to $GITHUB_OUTPUT', false); + } + + protected function run(): int + { + $root = realpath($this->getArgument('--path')) ?: $this->getArgument('--path'); + $fix = (bool) $this->getArgument('--fix'); + $ghOutput = (bool) $this->getArgument('--github-output'); + + // ── 1. Read manifest.xml ────────────────────────────────────────── + $manifestFile = "{$root}/.mokogitea/manifest.xml"; + + if (!file_exists($manifestFile)) { + $this->log('WARN', "No manifest.xml found at {$manifestFile}"); + $this->outputResult($ghOutput, 'skipped', 'No manifest.xml'); + return 0; + } + + $xml = @simplexml_load_file($manifestFile); + + if ($xml === false) { + $this->log('ERROR', "Failed to parse {$manifestFile}"); + return 1; + } + + // ── 2. Check if licensing is enabled ────────────────────────────── + if (!isset($xml->licensing) || (string) ($xml->licensing->enabled ?? '') !== 'true') { + $this->log('INFO', 'Licensing not enabled in manifest.xml — skipping'); + $this->outputResult($ghOutput, 'skipped', 'Licensing not enabled'); + return 0; + } + + $licensingNode = $xml->licensing; + $dlidEnabled = ((string) ($licensingNode->dlid ?? 'true')) === 'true'; + $updateServerUrl = (string) ($licensingNode->{'update-server'} ?? ''); + $updateServerName = (string) ($licensingNode->{'update-server-name'} ?? ''); + + // ── 3. Resolve placeholders ─────────────────────────────────────── + $org = (string) ($xml->identity->org ?? ''); + $repo = (string) ($xml->identity->name ?? ''); + + // Fallback to git remote if manifest doesn't have org/name + if (empty($org) || empty($repo)) { + $remote = trim((string) @shell_exec("cd " . escapeshellarg($root) . " && git remote get-url origin 2>/dev/null")); + + if (preg_match('#[/:]([^/]+)/([^/.]+?)(?:\.git)?$#', $remote, $m)) { + if (empty($org)) { + $org = $m[1]; + } + if (empty($repo)) { + $repo = $m[2]; + } + } + } + + // Default update server URL if not specified + if (empty($updateServerUrl) && !empty($org) && !empty($repo)) { + $updateServerUrl = "https://git.mokoconsulting.tech/{$org}/{$repo}/updates.xml"; + } + + // Resolve {org} and {repo} placeholders + $updateServerUrl = str_replace(['{org}', '{repo}'], [$org, $repo], $updateServerUrl); + + // Default server name from display-name or repo name + if (empty($updateServerName)) { + $displayName = (string) ($xml->identity->{'display-name'} ?? $repo); + $updateServerName = $displayName . ' Updates'; + } + + if (empty($updateServerUrl)) { + $this->log('ERROR', 'Cannot determine update server URL — set in manifest.xml or ensure org/repo are available'); + return 1; + } + + $this->log('INFO', "Licensing enabled — org={$org}, repo={$repo}"); + $this->log('INFO', "Update server: {$updateServerUrl}"); + $this->log('INFO', "DLID required: " . ($dlidEnabled ? 'yes' : 'no')); + + // ── 4. Find Joomla extension manifests ──────────────────────────── + $xmlFiles = array_merge( + glob("{$root}/src/*.xml") ?: [], + glob("{$root}/src/packages/*/*.xml") ?: [], + glob("{$root}/*.xml") ?: [] + ); + + $packageManifest = null; + + foreach ($xmlFiles as $file) { + $content = file_get_contents($file); + + if (!str_contains($content, 'log('WARN', 'No Joomla extension manifest found'); + $this->outputResult($ghOutput, 'skipped', 'No extension manifest'); + return 0; + } + + $relPath = str_replace($root . '/', '', str_replace('\\', '/', $packageManifest)); + $this->log('INFO', "Package manifest: {$relPath}"); + + // ── 5. Check and fix the manifest ───────────────────────────────── + $content = file_get_contents($packageManifest); + $original = $content; + $changes = []; + + // --- 5a. Ensure block with correct URL --- + if (preg_match('#\s*#s', $content)) { + // Empty updateservers block — inject the server + $replacement = "\n" + . " {$updateServerUrl}\n" + . " "; + $content = preg_replace('#\s*#s', $replacement, $content); + $changes[] = 'Added update server URL to empty '; + } elseif (!str_contains($content, '')) { + // No updateservers at all — add before + $serverBlock = "\n \n" + . " {$updateServerUrl}\n" + . " \n"; + $content = str_replace('', $serverBlock . '', $content); + $changes[] = 'Added block'; + } else { + // updateservers exists — verify URL is correct + if (preg_match('#]*>([^<]+)#', $content, $m)) { + if ($m[1] !== $updateServerUrl) { + $content = preg_replace( + '#(]*>)[^<]+()#', + "\${1}{$updateServerUrl}\${2}", + $content + ); + $changes[] = "Updated server URL: {$m[1]} → {$updateServerUrl}"; + } + } + } + + // --- 5b. Ensure tag if required --- + if ($dlidEnabled) { + if (!str_contains($content, ' if present, otherwise before + $dlidTag = ' ' . "\n"; + + if (str_contains($content, '')) { + $content = str_replace('', $dlidTag . "\n ", $content); + } else { + $content = str_replace('', $dlidTag . '', $content); + } + + $changes[] = 'Added tag'; + } + } + + // --- 5c. Ensure for packages --- + if (str_contains($content, 'type="package"') && !str_contains($content, '')) { + $blockTag = ' true' . "\n"; + + if (str_contains($content, ' + $content = preg_replace( + '#(\s*\n)#', + "\${1}{$blockTag}", + $content + ); + } elseif (str_contains($content, '')) { + $content = str_replace('', $blockTag . "\n ", $content); + } else { + $content = str_replace('', $blockTag . '', $content); + } + + $changes[] = 'Added true'; + } + + // ── 6. Report and apply ─────────────────────────────────────────── + if (empty($changes)) { + $this->log('INFO', 'All licensing tags are correct — no changes needed'); + $this->outputResult($ghOutput, 'ok', 'No changes needed'); + return 0; + } + + foreach ($changes as $change) { + $this->log($fix ? 'INFO' : 'WARN', ($fix ? 'Fixed: ' : 'Needs fix: ') . $change); + } + + if ($fix) { + file_put_contents($packageManifest, $content); + $this->log('INFO', "Wrote {$relPath} with " . count($changes) . " change(s)"); + $this->outputResult($ghOutput, 'fixed', implode('; ', $changes)); + } else { + $this->log('WARN', 'Run with --fix to apply changes'); + $this->outputResult($ghOutput, 'needs-fix', implode('; ', $changes)); + return 1; + } + + return 0; + } + + /** + * Write result to $GITHUB_OUTPUT if requested. + */ + private function outputResult(bool $ghOutput, string $status, string $detail): void + { + if (!$ghOutput) { + return; + } + + $outputFile = getenv('GITHUB_OUTPUT'); + + if ($outputFile === false || $outputFile === '') { + echo "licensing_status={$status}\n"; + echo "licensing_detail={$detail}\n"; + return; + } + + $fh = fopen($outputFile, 'a'); + fwrite($fh, "licensing_status={$status}\n"); + fwrite($fh, "licensing_detail={$detail}\n"); + fclose($fh); + } +} + +$app = new ManifestLicensingCli(); +exit($app->execute());