ae2860c3b5
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.1) (push) 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 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) 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
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 6s
Generic: Repo Health / Access control (push) Successful in 9s
Universal: PR Check / Validate PR (pull_request) Failing after 10s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 22s
Universal: Auto Version Bump / Version Bump (push) Failing after 23s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m13s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 1m17s
Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
557 lines
21 KiB
PHP
557 lines
21 KiB
PHP
#!/usr/bin/env 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: MokoPlatform.Release
|
|
* INGROUP: MokoPlatform.Scripts
|
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
* PATH: /release/generate_joomla_update_xml.php
|
|
* BRIEF: Create or update the <downloadurl> in updates.xml on release
|
|
*
|
|
* Minimal Joomla Update Server helper. On each release it:
|
|
* 1. Reads the existing updates.xml, or bootstraps a new one from Makefile
|
|
* metadata if none exists yet.
|
|
* 2. If an <update> entry for this version already exists, updates only
|
|
* its <downloadurl>.
|
|
* 3. If this is a new version, clones the latest entry, bumps <version>
|
|
* and <downloadurl>, and prepends it.
|
|
* 4. Optionally injects / refreshes the <updateservers> block in every
|
|
* Joomla XML install manifest found in the repo (--inject-updateserver).
|
|
*
|
|
* Runs in two modes:
|
|
* Local — reads/writes updates.xml on disk (for use inside a release
|
|
* workflow that has already checked out the repo).
|
|
* Remote — reads/commits updates.xml via GitHub API.
|
|
*
|
|
* Usage (local — GitHub Actions release workflow):
|
|
* php generate_joomla_update_xml.php \
|
|
* --tag=v1.2.0 \
|
|
* [--zip-url=https://github.com/org/repo/releases/download/v1.2.0/mod_foo-1.2.0.zip] \
|
|
* [--inject-updateserver]
|
|
*
|
|
* Usage (remote — from moko-platform):
|
|
* php generate_joomla_update_xml.php \
|
|
* --repo=mokoconsulting-tech/WaasComponent \
|
|
* --tag=v1.2.0 \
|
|
* [--zip-url=https://...] \
|
|
* [--inject-updateserver]
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/../vendor/autoload.php';
|
|
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
|
|
|
|
use MokoEnterprise\CliFramework;
|
|
use MokoEnterprise\{ApiClient, AuditLogger, Config};
|
|
use MokoStandards\Plugins\Joomla\UpdateXmlGenerator;
|
|
|
|
class GenerateJoomlaUpdateXmlCli extends CliFramework
|
|
{
|
|
public const VERSION = '09.22.00';
|
|
|
|
private ?ApiClient $api = null;
|
|
private AuditLogger $logger;
|
|
|
|
protected function configure(): void
|
|
{
|
|
$this->setDescription('Update the download URL in updates.xml on Joomla extension release');
|
|
$this->addArgument('--repo', 'GitHub repo (org/repo) for remote mode', '');
|
|
$this->addArgument('--tag', 'Git tag for this release, e.g. v1.2.0 (required)', '');
|
|
$this->addArgument('--zip-url', 'Full URL to the release ZIP', '');
|
|
$this->addArgument('--inject-updateserver', 'Inject / refresh <updateservers> in all Joomla XML manifests', false);
|
|
$this->addArgument('--output', 'Local output path for updates.xml (default: ./updates.xml)', './updates.xml');
|
|
$this->addArgument('--makefile', 'Local path to Makefile for ZIP URL derivation (default: ./Makefile)', './Makefile');
|
|
}
|
|
|
|
protected function run(): int
|
|
{
|
|
$this->log('INFO', 'Joomla Update XML — release updater v' . self::VERSION);
|
|
$this->logger = new AuditLogger('joomla_update_xml');
|
|
|
|
$tag = $this->getArgument('--tag');
|
|
if (empty($tag)) {
|
|
$this->log('ERROR', '--tag is required (e.g. --tag=v1.2.0)');
|
|
return 1;
|
|
}
|
|
|
|
$repoArg = $this->getArgument('--repo');
|
|
return !empty($repoArg)
|
|
? $this->runRemote($repoArg, $tag)
|
|
: $this->runLocal($tag);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Remote mode
|
|
// -------------------------------------------------------------------------
|
|
|
|
private function runRemote(string $repoArg, string $tag): int
|
|
{
|
|
if (!$this->initApi()) {
|
|
return 1;
|
|
}
|
|
|
|
if (!str_contains($repoArg, '/')) {
|
|
$this->log('ERROR', '--repo must be in org/repo format');
|
|
return 1;
|
|
}
|
|
[$org, $repo] = explode('/', $repoArg, 2);
|
|
|
|
$repoData = $this->api->get("/repos/{$org}/{$repo}");
|
|
$defaultBranch = $repoData['default_branch'] ?? 'main';
|
|
|
|
$existingXml = $this->fetchRemoteFile($org, $repo, 'updates.xml');
|
|
if ($existingXml === null) {
|
|
$this->log('INFO', "updates.xml not found in {$org}/{$repo} — bootstrapping first entry");
|
|
$zipUrl = $this->resolveZipUrl($org, $repo, $tag);
|
|
$existingXml = $this->bootstrapXml($org, $repo, $tag, $zipUrl);
|
|
}
|
|
|
|
$zipUrl = $this->resolveZipUrl($org, $repo, $tag);
|
|
$xml = $this->updateXml($existingXml, $tag, $zipUrl);
|
|
|
|
if ($this->dryRun) {
|
|
$this->log('INFO', "(dry-run) updates.xml:\n{$xml}");
|
|
return 0;
|
|
}
|
|
|
|
$sha = $this->fetchRemoteFileSha($org, $repo, 'updates.xml');
|
|
$payload = [
|
|
'message' => "chore(release): update download URL in updates.xml for {$tag}",
|
|
'content' => base64_encode($xml),
|
|
'branch' => $defaultBranch,
|
|
];
|
|
if ($sha !== null) {
|
|
$payload['sha'] = $sha;
|
|
}
|
|
try {
|
|
$this->api->put("/repos/{$org}/{$repo}/contents/updates.xml", $payload);
|
|
$this->log('INFO', "updates.xml committed to {$defaultBranch}");
|
|
} catch (\Exception $e) {
|
|
$this->log('ERROR', 'Failed to commit: ' . $e->getMessage());
|
|
return 1;
|
|
}
|
|
|
|
if ($this->getArgument('--inject-updateserver')) {
|
|
$this->injectUpdateServerRemote($org, $repo, $defaultBranch);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Local mode
|
|
// -------------------------------------------------------------------------
|
|
|
|
private function runLocal(string $tag): int
|
|
{
|
|
$outputPath = $this->getArgument('--output');
|
|
$ghRepo = getenv('GITHUB_REPOSITORY') ?: '';
|
|
[$org, $repo] = str_contains($ghRepo, '/') ? explode('/', $ghRepo, 2) : ['', ''];
|
|
|
|
$zipUrl = $this->resolveZipUrl($org, $repo, $tag);
|
|
|
|
if (!file_exists($outputPath)) {
|
|
$this->log('INFO', "{$outputPath} not found — bootstrapping first entry");
|
|
$existingXml = $this->bootstrapXml($org, $repo, $tag, $zipUrl);
|
|
} else {
|
|
$existingXml = file_get_contents($outputPath) ?: '';
|
|
}
|
|
|
|
$xml = $this->updateXml($existingXml, $tag, $zipUrl);
|
|
|
|
if ($this->dryRun) {
|
|
$this->log('INFO', "(dry-run) updates.xml:\n{$xml}");
|
|
return 0;
|
|
}
|
|
|
|
if (file_put_contents($outputPath, $xml) === false) {
|
|
$this->log('ERROR', "Failed to write: {$outputPath}");
|
|
return 1;
|
|
}
|
|
$this->log('INFO', "updates.xml updated ({$outputPath})");
|
|
|
|
// Validate structure
|
|
$result = UpdateXmlGenerator::validate($xml);
|
|
if (!$result['valid']) {
|
|
foreach ($result['errors'] as $err) {
|
|
$this->warning($err);
|
|
}
|
|
} else {
|
|
$this->log('INFO', 'Structure validated');
|
|
}
|
|
|
|
if ($this->getArgument('--inject-updateserver') && !empty($org) && !empty($repo)) {
|
|
$url = "https://raw.githubusercontent.com/{$org}/{$repo}/main/updates.xml";
|
|
foreach ($this->findJoomlaManifests('.') as $path) {
|
|
$this->injectUpdateServerIntoFile($path, $url, $repo);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Bootstrap — build a minimal updates.xml when none exists yet
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Create a minimal but valid updates.xml skeleton with one <update> entry.
|
|
*/
|
|
private function bootstrapXml(string $org, string $repo, string $tag, string $zipUrl): string
|
|
{
|
|
$version = ltrim($tag, 'vV');
|
|
$vars = $this->parseMakefileVars();
|
|
|
|
$extensionName = $vars['EXTENSION_NAME'] ?? $repo;
|
|
$extensionType = strtolower($vars['EXTENSION_TYPE'] ?? 'module');
|
|
$displayName = $vars['EXTENSION_DISPLAY_NAME'] ?? $repo;
|
|
$pluginGroup = strtolower($vars['PLUGIN_GROUP'] ?? 'system');
|
|
$moduleType = strtolower($vars['MODULE_TYPE'] ?? 'site');
|
|
|
|
$element = match ($extensionType) {
|
|
'module' => "mod_{$extensionName}",
|
|
'plugin' => "plg_{$pluginGroup}_{$extensionName}",
|
|
'component' => "com_{$extensionName}",
|
|
'package' => "pkg_{$extensionName}",
|
|
'template' => "tpl_{$extensionName}",
|
|
default => $extensionName,
|
|
};
|
|
|
|
// client attribute only applies to modules
|
|
$clientAttr = ($extensionType === 'module')
|
|
? ' client="' . htmlspecialchars($moduleType, ENT_XML1) . '"'
|
|
: '';
|
|
|
|
$updateServerUrl = (!empty($org) && !empty($repo))
|
|
? "https://raw.githubusercontent.com/{$org}/{$repo}/main/updates.xml"
|
|
: '';
|
|
|
|
$updateServersBlock = $updateServerUrl !== ''
|
|
? "\n <updateservers>\n <server type=\"extension\" name=\"{$displayName} Updates\">{$updateServerUrl}</server>\n </updateservers>"
|
|
: '';
|
|
|
|
$xml = <<<XML
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<updates>
|
|
<update>
|
|
<name>{$displayName}</name>
|
|
<description>{$displayName} Joomla extension</description>
|
|
<element>{$element}</element>
|
|
<type>{$extensionType}</type>
|
|
<version>{$version}</version>
|
|
<downloads>
|
|
<downloadurl type="full" format="zip"{$clientAttr}>{$zipUrl}</downloadurl>
|
|
</downloads>
|
|
<tags>
|
|
<tag>stable</tag>
|
|
</tags>
|
|
<targetplatform name="joomla" version="4\.[0-9]+" />
|
|
<php_minimum>8.1</php_minimum>{$updateServersBlock}
|
|
</update>
|
|
</updates>
|
|
XML;
|
|
|
|
$this->log('INFO', "Bootstrapped updates.xml for {$element} v{$version}");
|
|
return $xml;
|
|
}
|
|
|
|
/**
|
|
* Parse Makefile variables (KEY = value / KEY := value / KEY ?= value).
|
|
* @return array<string,string>
|
|
*/
|
|
private function parseMakefileVars(): array
|
|
{
|
|
$path = $this->getArgument('--makefile');
|
|
if (!file_exists($path)) {
|
|
return [];
|
|
}
|
|
$vars = [];
|
|
foreach (explode("\n", file_get_contents($path) ?: '') as $line) {
|
|
if (preg_match('/^([A-Z_]+)\s*[:?]?=\s*(.+)$/', trim($line), $m)) {
|
|
$vars[$m[1]] = trim($m[2]);
|
|
}
|
|
}
|
|
return $vars;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Core XML update — only touches <version> and <downloadurl>
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Update updates.xml:
|
|
* - If an <update> entry for $tag's version exists -> update its <downloadurl>
|
|
* - Otherwise -> clone the first entry, set new <version> + <downloadurl>, prepend
|
|
*/
|
|
private function updateXml(string $xml, string $tag, string $zipUrl): string
|
|
{
|
|
$version = ltrim($tag, 'vV');
|
|
|
|
$dom = new \DOMDocument();
|
|
$dom->formatOutput = true;
|
|
$dom->preserveWhiteSpace = false;
|
|
if (!@$dom->loadXML($xml)) {
|
|
$this->log('ERROR', 'Cannot parse existing updates.xml');
|
|
return $xml;
|
|
}
|
|
|
|
$updates = $dom->getElementsByTagName('updates')->item(0);
|
|
if (!$updates) {
|
|
$this->log('ERROR', 'No <updates> root in updates.xml');
|
|
return $xml;
|
|
}
|
|
|
|
$existingEntry = null;
|
|
foreach ($updates->getElementsByTagName('update') as $node) {
|
|
$vNode = $node->getElementsByTagName('version')->item(0);
|
|
if ($vNode && trim($vNode->textContent) === $version) {
|
|
$existingEntry = $node;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($existingEntry !== null) {
|
|
// Update only the downloadurl in the existing entry
|
|
$dlNodes = $existingEntry->getElementsByTagName('downloadurl');
|
|
if ($dlNodes->length > 0) {
|
|
$dlNodes->item(0)->textContent = $zipUrl;
|
|
$this->log('INFO', "Updated <downloadurl> for existing v{$version} entry");
|
|
}
|
|
} else {
|
|
// Clone the first (most recent) entry and set new version + url
|
|
$firstEntry = $updates->getElementsByTagName('update')->item(0);
|
|
if ($firstEntry === null) {
|
|
$this->log('ERROR', 'No existing <update> entries to clone');
|
|
return $xml;
|
|
}
|
|
|
|
/** @var \DOMElement $newEntry */
|
|
$newEntry = $firstEntry->cloneNode(true);
|
|
|
|
// Set version
|
|
$vNode = $newEntry->getElementsByTagName('version')->item(0);
|
|
if ($vNode) {
|
|
$vNode->textContent = $version;
|
|
}
|
|
|
|
// Set downloadurl
|
|
$dlNode = $newEntry->getElementsByTagName('downloadurl')->item(0);
|
|
if ($dlNode) {
|
|
$dlNode->textContent = $zipUrl;
|
|
}
|
|
|
|
// Prepend before first entry
|
|
$updates->insertBefore($newEntry, $firstEntry);
|
|
$this->log('INFO', "Prepended new v{$version} entry (cloned from latest)");
|
|
}
|
|
|
|
return $dom->saveXML() ?: $xml;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// ZIP URL resolution
|
|
// -------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Resolve the download ZIP URL in priority order:
|
|
* 1. --zip-url CLI option
|
|
* 2. GitHub Releases API (first .zip asset on the tag's release)
|
|
* 3. Derived from EXTENSION_NAME in Makefile + standard GitHub releases path
|
|
*/
|
|
private function resolveZipUrl(string $org, string $repo, string $tag): string
|
|
{
|
|
$explicit = $this->getArgument('--zip-url');
|
|
if (!empty($explicit)) {
|
|
return $explicit;
|
|
}
|
|
|
|
// Try GitHub Releases API
|
|
if ($this->api !== null && !empty($org) && !empty($repo)) {
|
|
try {
|
|
$release = $this->api->get("/repos/{$org}/{$repo}/releases/tags/{$tag}");
|
|
foreach ($release['assets'] ?? [] as $asset) {
|
|
if (str_ends_with($asset['name'] ?? '', '.zip')) {
|
|
$this->log('INFO', "ZIP URL from release asset: {$asset['browser_download_url']}");
|
|
return $asset['browser_download_url'];
|
|
}
|
|
}
|
|
} catch (\Exception $e) {
|
|
$this->warning('Could not query release assets: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
// Derive from Makefile
|
|
$version = ltrim($tag, 'vV');
|
|
$element = $this->elementFromMakefile();
|
|
$zipName = "{$element}-{$version}.zip";
|
|
$derived = "https://github.com/{$org}/{$repo}/releases/download/{$tag}/{$zipName}";
|
|
$this->log('INFO', "Derived ZIP URL: {$derived}");
|
|
return $derived;
|
|
}
|
|
|
|
/**
|
|
* Read EXTENSION_NAME and EXTENSION_TYPE from the local Makefile.
|
|
*/
|
|
private function elementFromMakefile(): string
|
|
{
|
|
$vars = $this->parseMakefileVars();
|
|
$name = $vars['EXTENSION_NAME'] ?? 'extension';
|
|
$type = strtolower($vars['EXTENSION_TYPE'] ?? 'module');
|
|
$group = strtolower($vars['PLUGIN_GROUP'] ?? 'system');
|
|
return match ($type) {
|
|
'module' => "mod_{$name}",
|
|
'plugin' => "plg_{$group}_{$name}",
|
|
'component' => "com_{$name}",
|
|
'package' => "pkg_{$name}",
|
|
'template' => "tpl_{$name}",
|
|
default => $name,
|
|
};
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// <updateservers> injection into Joomla manifests
|
|
// -------------------------------------------------------------------------
|
|
|
|
private function injectUpdateServerRemote(string $org, string $repo, string $branch): void
|
|
{
|
|
$url = "https://raw.githubusercontent.com/{$org}/{$repo}/{$branch}/updates.xml";
|
|
try {
|
|
$tree = $this->api->get("/repos/{$org}/{$repo}/git/trees/{$branch}", ['recursive' => '1']);
|
|
foreach ($tree['tree'] ?? [] as $node) {
|
|
if (!str_ends_with($node['path'] ?? '', '.xml') || $node['type'] !== 'blob') {
|
|
continue;
|
|
}
|
|
$content = $this->fetchRemoteFile($org, $repo, $node['path']);
|
|
if ($content === null || !$this->isJoomlaManifest($content)) {
|
|
continue;
|
|
}
|
|
$updated = $this->injectUpdateServerIntoXml($content, $url, $repo);
|
|
if ($updated === null) {
|
|
continue;
|
|
}
|
|
$sha = $this->fetchRemoteFileSha($org, $repo, $node['path']);
|
|
$payload = [
|
|
'message' => 'chore(release): refresh <updateservers> URL in manifest',
|
|
'content' => base64_encode($updated),
|
|
'branch' => $branch,
|
|
];
|
|
if ($sha !== null) {
|
|
$payload['sha'] = $sha;
|
|
}
|
|
$this->api->put("/repos/{$org}/{$repo}/contents/{$node['path']}", $payload);
|
|
$this->log('INFO', " updateservers -> {$node['path']}");
|
|
}
|
|
} catch (\Exception $e) {
|
|
$this->warning('updateservers injection failed: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/** @return string[] */
|
|
private function findJoomlaManifests(string $dir): array
|
|
{
|
|
$found = [];
|
|
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir));
|
|
foreach ($iterator as $file) {
|
|
if (!$file->isFile() || $file->getExtension() !== 'xml') {
|
|
continue;
|
|
}
|
|
$content = file_get_contents((string) $file) ?: '';
|
|
if ($this->isJoomlaManifest($content)) {
|
|
$found[] = (string) $file;
|
|
}
|
|
}
|
|
return $found;
|
|
}
|
|
|
|
private function isJoomlaManifest(string $xml): bool
|
|
{
|
|
return (bool) preg_match('/<extension\s[^>]*type=/', $xml);
|
|
}
|
|
|
|
private function injectUpdateServerIntoFile(string $path, string $url, string $repoName): void
|
|
{
|
|
$content = file_get_contents($path) ?: '';
|
|
$updated = $this->injectUpdateServerIntoXml($content, $url, $repoName);
|
|
if ($updated !== null) {
|
|
file_put_contents($path, $updated);
|
|
$this->log('INFO', " updateservers -> {$path}");
|
|
}
|
|
}
|
|
|
|
private function injectUpdateServerIntoXml(string $xml, string $url, string $repoName): ?string
|
|
{
|
|
$dom = new \DOMDocument();
|
|
$dom->formatOutput = true;
|
|
$dom->preserveWhiteSpace = false;
|
|
if (!@$dom->loadXML($xml)) {
|
|
return null;
|
|
}
|
|
$root = $dom->documentElement;
|
|
if (!$root || strtolower($root->nodeName) !== 'extension') {
|
|
return null;
|
|
}
|
|
|
|
// Remove existing block so we can replace it cleanly
|
|
foreach (iterator_to_array($dom->getElementsByTagName('updateservers')) as $node) {
|
|
$node->parentNode?->removeChild($node);
|
|
}
|
|
|
|
$servers = $dom->createElement('updateservers');
|
|
$server = $dom->createElement('server');
|
|
$server->setAttribute('type', 'extension');
|
|
$server->setAttribute('name', "{$repoName} Updates");
|
|
$server->textContent = $url;
|
|
$servers->appendChild($server);
|
|
$root->appendChild($servers);
|
|
|
|
$out = $dom->saveXML();
|
|
return ($out !== false && $out !== $xml) ? $out : null;
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// API helpers
|
|
// -------------------------------------------------------------------------
|
|
|
|
private function initApi(): bool
|
|
{
|
|
$config = Config::load();
|
|
try {
|
|
$adapter = \MokoEnterprise\PlatformAdapterFactory::create($config);
|
|
$this->api = $adapter->getApiClient();
|
|
return true;
|
|
} catch (\Exception $e) {
|
|
$this->log('ERROR', 'API init failed: ' . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private function fetchRemoteFile(string $org, string $repo, string $path): ?string
|
|
{
|
|
try {
|
|
$r = $this->api->get("/repos/{$org}/{$repo}/contents/{$path}");
|
|
return base64_decode(str_replace(["\n", "\r"], '', $r['content'] ?? '')) ?: null;
|
|
} catch (\Exception $e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private function fetchRemoteFileSha(string $org, string $repo, string $path): ?string
|
|
{
|
|
try {
|
|
return $this->api->get("/repos/{$org}/{$repo}/contents/{$path}")['sha'] ?? null;
|
|
} catch (\Exception $e) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
$app = new GenerateJoomlaUpdateXmlCli();
|
|
exit($app->execute());
|