From 84259c6636989448f49ba01b0dad0052a3564fa7 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Sun, 28 Jun 2026 14:33:53 -0500 Subject: [PATCH] fix: auto-download pre-built release for empty submodule sub-packages When a Joomla package has a sub-package that is a git submodule with an empty or missing source directory (e.g. failed CI checkout), the packager now falls back to downloading the latest stable release ZIP from the submodule's Gitea remote. Also supports pre-staged ZIPs in the output directory, allowing manual or workflow-based pre-population of sub-package archives. Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG --- cli/release_package.php | 115 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/cli/release_package.php b/cli/release_package.php index c5f83f4..48aadd1 100644 --- a/cli/release_package.php +++ b/cli/release_package.php @@ -250,6 +250,14 @@ class ReleasePackageCli extends CliFramework } $subZipPath = "{$outputDir}/{$subName}.zip"; + // Use pre-built ZIP if staged in the output directory + if (file_exists($subZipPath) && filesize($subZipPath) > 0) { + $zip->addFile($subZipPath, "packages/{$subName}.zip"); + $sizeKb = number_format(filesize($subZipPath) / 1024, 1); + echo " Sub-package: {$subName}.zip (pre-built, {$sizeKb} KB)\n"; + continue; + } + // If sub-package is a full repo checkout (e.g. git submodule), // look for a source/ or src/ subdirectory containing a Joomla manifest XML // and zip that instead of the repo root. @@ -270,6 +278,16 @@ class ReleasePackageCli extends CliFramework } } + // If source dir has no manifest, the submodule may be empty. + // Try to download the pre-built release from Gitea. + $hasManifest = !empty(glob("{$subSourceDir}/*.xml") ?: []); + if (!$hasManifest && $token !== '' && $this->downloadSubmoduleRelease($root, $subName, $subZipPath, $token)) { + $zip->addFile($subZipPath, "packages/{$subName}.zip"); + $sizeKb = number_format(filesize($subZipPath) / 1024, 1); + echo " Sub-package: {$subName}.zip (downloaded release, {$sizeKb} KB)\n"; + continue; + } + $subZip = new \ZipArchive(); if ($subZip->open($subZipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) { $this->log('ERROR', "Failed to create sub-package ZIP: {$subZipPath}"); @@ -527,6 +545,103 @@ class ReleasePackageCli extends CliFramework return false; } + /** + * Download a pre-built release ZIP for a sub-package that is a git submodule + * with an empty or missing source directory. + * + * Reads .gitmodules to find the submodule's remote URL, derives the Gitea + * API path, and downloads the latest stable release asset. + */ + private function downloadSubmoduleRelease(string $root, string $subName, string $destPath, string $token): bool + { + $gitmodulesPath = "{$root}/.gitmodules"; + if (!file_exists($gitmodulesPath)) { + return false; + } + + $gitmodules = file_get_contents($gitmodulesPath); + if ($gitmodules === false) { + return false; + } + + // Find the submodule URL by matching the subName in the path + if (!preg_match('/\[submodule\s[^\]]*\]\s*\n\s*path\s*=\s*[^\n]*' . preg_quote($subName, '/') . '\s*\n\s*url\s*=\s*(\S+)/m', $gitmodules, $matches)) { + return false; + } + + $remoteUrl = preg_replace('/\.git$/', '', $matches[1]); + + // Extract org/repo from the URL + if (!preg_match('#[/:]([^/]+)/([^/]+)$#', $remoteUrl, $parts)) { + return false; + } + + $org = $parts[1]; + $repo = $parts[2]; + + // Derive the Gitea API base from the remote URL + $parsed = parse_url($remoteUrl); + $scheme = $parsed['scheme'] ?? 'https'; + $host = $parsed['host'] ?? ''; + if ($host === '') { + return false; + } + $apiBase = "{$scheme}://{$host}/api/v1/repos/{$org}/{$repo}"; + + echo " Submodule {$subName}: source empty, downloading release from {$org}/{$repo}...\n"; + + // Get the stable release + $result = $this->giteaApiRequest("{$apiBase}/releases/tags/stable", $token); + if ($result['data'] === null || !isset($result['data']['assets'])) { + echo " WARNING: No stable release found for {$org}/{$repo}\n"; + return false; + } + + // Find the ZIP asset (not .sha256) + $downloadUrl = ''; + foreach ($result['data']['assets'] as $asset) { + if (!is_array($asset)) { + continue; + } + $name = $asset['name'] ?? ''; + if (str_ends_with($name, '.zip') && !str_ends_with($name, '.sha256')) { + $downloadUrl = $asset['browser_download_url'] ?? ''; + break; + } + } + + if ($downloadUrl === '') { + echo " WARNING: No ZIP asset in {$org}/{$repo} stable release\n"; + return false; + } + + // Download the ZIP + $ch = curl_init($downloadUrl); + if ($ch === false) { + return false; + } + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_HTTPHEADER => ["Authorization: token {$token}"], + CURLOPT_TIMEOUT => 120, + ]); + $content = curl_exec($ch); + $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode < 200 || $httpCode >= 300 || !is_string($content) || $content === '') { + echo " WARNING: Download failed (HTTP {$httpCode})\n"; + return false; + } + + if (file_put_contents($destPath, $content) === false) { + return false; + } + + return true; + } + /** * Recursively add files from a directory to a ZipArchive. */ -- 2.52.0