1d87be7d5e
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
- Update REPO: from MokoStandards-API to moko-platform in 125 files - Fix wrong org path (mokoconsulting-tech → MokoConsulting) in 10 files - Fix SPDX-LICENSE-IDENTIFIER case in 2 template files - Add missing REPO: field to 3 files Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
573 lines
22 KiB
PHP
573 lines
22 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: MokoStandards.Release
|
||
* INGROUP: MokoStandards.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).
|
||
*
|
||
* The script deliberately leaves all other fields (name, description, tags,
|
||
* targetplatform, php_minimum, etc.) untouched so they can be maintained by
|
||
* hand or by a separate process.
|
||
*
|
||
* 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 MokoStandards):
|
||
* 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\{ApiClient, AuditLogger, CLIApp, Config};
|
||
use MokoStandards\Plugins\Joomla\UpdateXmlGenerator;
|
||
|
||
class GenerateJoomlaUpdateXml extends CLIApp
|
||
{
|
||
public const VERSION = '04.06.00';
|
||
|
||
private ?ApiClient $api = null;
|
||
private AuditLogger $logger;
|
||
|
||
protected function setupArguments(): array
|
||
{
|
||
return [
|
||
'repo:' => 'GitHub repo (org/repo) for remote mode.',
|
||
'tag:' => 'Git tag for this release, e.g. v1.2.0 (required)',
|
||
'zip-url:' => 'Full URL to the release ZIP. Auto-derived from the tag + Makefile EXTENSION_NAME if omitted.',
|
||
'inject-updateserver' => 'Inject / refresh <updateservers> in all Joomla XML manifests.',
|
||
'output:' => 'Local output path for updates.xml (default: ./updates.xml)',
|
||
'makefile:' => 'Local path to Makefile for ZIP URL derivation (default: ./Makefile)',
|
||
];
|
||
}
|
||
|
||
protected function run(): int
|
||
{
|
||
$this->log('📦 Joomla Update XML — release updater v' . self::VERSION, 'INFO');
|
||
$this->logger = new AuditLogger('joomla_update_xml');
|
||
|
||
$tag = $this->getOption('tag', '');
|
||
if (empty($tag)) {
|
||
$this->log('❌ --tag is required (e.g. --tag=v1.2.0)', 'ERROR');
|
||
return 1;
|
||
}
|
||
|
||
$repoArg = $this->getOption('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('❌ --repo must be in org/repo format', 'ERROR');
|
||
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("ℹ️ updates.xml not found in {$org}/{$repo} — bootstrapping first entry", 'INFO');
|
||
$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("(dry-run) updates.xml:\n{$xml}", 'INFO');
|
||
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("✅ updates.xml committed to {$defaultBranch}", 'INFO');
|
||
} catch (\Exception $e) {
|
||
$this->log('❌ Failed to commit: ' . $e->getMessage(), 'ERROR');
|
||
return 1;
|
||
}
|
||
|
||
if ($this->hasOption('inject-updateserver')) {
|
||
$this->injectUpdateServerRemote($org, $repo, $defaultBranch);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
// -------------------------------------------------------------------------
|
||
// Local mode
|
||
// -------------------------------------------------------------------------
|
||
|
||
private function runLocal(string $tag): int
|
||
{
|
||
$outputPath = $this->getOption('output', './updates.xml');
|
||
$ghRepo = getenv('GITHUB_REPOSITORY') ?: '';
|
||
[$org, $repo] = str_contains($ghRepo, '/') ? explode('/', $ghRepo, 2) : ['', ''];
|
||
|
||
$zipUrl = $this->resolveZipUrl($org, $repo, $tag);
|
||
|
||
if (!file_exists($outputPath)) {
|
||
$this->log("ℹ️ {$outputPath} not found — bootstrapping first entry", 'INFO');
|
||
$existingXml = $this->bootstrapXml($org, $repo, $tag, $zipUrl);
|
||
} else {
|
||
$existingXml = file_get_contents($outputPath) ?: '';
|
||
}
|
||
|
||
$xml = $this->updateXml($existingXml, $tag, $zipUrl);
|
||
|
||
if ($this->dryRun) {
|
||
$this->log("(dry-run) updates.xml:\n{$xml}", 'INFO');
|
||
return 0;
|
||
}
|
||
|
||
if (file_put_contents($outputPath, $xml) === false) {
|
||
$this->log("❌ Failed to write: {$outputPath}", 'ERROR');
|
||
return 1;
|
||
}
|
||
$this->log("✅ updates.xml updated ({$outputPath})", 'INFO');
|
||
|
||
// Validate structure
|
||
$result = UpdateXmlGenerator::validate($xml);
|
||
if (!$result['valid']) {
|
||
foreach ($result['errors'] as $err) {
|
||
$this->log("⚠️ {$err}", 'WARN');
|
||
}
|
||
} else {
|
||
$this->log("✅ Structure validated", 'INFO');
|
||
}
|
||
|
||
if ($this->hasOption('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.
|
||
* Called only when no updates.xml exists. The resulting XML is immediately
|
||
* handed to updateXml() so the canonical write path stays unchanged.
|
||
*/
|
||
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("ℹ️ Bootstrapped updates.xml for {$element} v{$version}", 'INFO');
|
||
return $xml;
|
||
}
|
||
|
||
/**
|
||
* Parse Makefile variables (KEY = value / KEY := value / KEY ?= value).
|
||
* @return array<string,string>
|
||
*/
|
||
private function parseMakefileVars(): array
|
||
{
|
||
$path = $this->getOption('makefile', './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
|
||
*
|
||
* All other XML content is preserved byte-for-byte.
|
||
*/
|
||
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('❌ Cannot parse existing updates.xml', 'ERROR');
|
||
return $xml;
|
||
}
|
||
|
||
$updates = $dom->getElementsByTagName('updates')->item(0);
|
||
if (!$updates) {
|
||
$this->log('❌ No <updates> root in updates.xml', 'ERROR');
|
||
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("ℹ️ Updated <downloadurl> for existing v{$version} entry", 'INFO');
|
||
}
|
||
} else {
|
||
// Clone the first (most recent) entry and set new version + url
|
||
$firstEntry = $updates->getElementsByTagName('update')->item(0);
|
||
if ($firstEntry === null) {
|
||
$this->log('❌ No existing <update> entries to clone', 'ERROR');
|
||
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("ℹ️ Prepended new v{$version} entry (cloned from latest)", 'INFO');
|
||
}
|
||
|
||
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->getOption('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("ℹ️ ZIP URL from release asset: {$asset['browser_download_url']}", 'INFO');
|
||
return $asset['browser_download_url'];
|
||
}
|
||
}
|
||
} catch (\Exception $e) {
|
||
$this->log('⚠️ Could not query release assets: ' . $e->getMessage(), 'WARN');
|
||
}
|
||
}
|
||
|
||
// 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("ℹ️ Derived ZIP URL: {$derived}", 'INFO');
|
||
return $derived;
|
||
}
|
||
|
||
/**
|
||
* Read EXTENSION_NAME and EXTENSION_TYPE from the local Makefile to
|
||
* construct the element prefix (mod_, plg_, com_, etc.).
|
||
*/
|
||
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(" ✅ updateservers → {$node['path']}", 'INFO');
|
||
}
|
||
} catch (\Exception $e) {
|
||
$this->log('⚠️ updateservers injection failed: ' . $e->getMessage(), 'WARN');
|
||
}
|
||
}
|
||
|
||
/** @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(" ✅ updateservers → {$path}", 'INFO');
|
||
}
|
||
}
|
||
|
||
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 {
|
||
$this->adapter = \MokoEnterprise\PlatformAdapterFactory::create($config);
|
||
$this->api = $this->adapter->getApiClient();
|
||
return true;
|
||
} catch (\Exception $e) {
|
||
$this->log('❌ API init failed: ' . $e->getMessage(), 'ERROR');
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Execute if run directly
|
||
if (php_sapi_name() === 'cli' && isset($argv[0]) && realpath($argv[0]) === __FILE__) {
|
||
$app = new GenerateJoomlaUpdateXml(
|
||
'generate-joomla-update-xml',
|
||
'Update the download URL in updates.xml on Joomla extension release',
|
||
GenerateJoomlaUpdateXml::VERSION
|
||
);
|
||
exit($app->execute());
|
||
}
|