Files
mokocli/cli/release_package.php
T
Jonathan Miller b491241a58
Universal: Sync Feature Branch Versions / Sync feature branches with dev (push) Has been skipped
Merge branch 'main' into dev
# Conflicts:
#	.mokogitea/CLAUDE.md
#	.mokogitea/ISSUE_TEMPLATE/config.yml
#	.mokogitea/ISSUE_TEMPLATE/documentation.md
#	.mokogitea/ISSUE_TEMPLATE/feature_request.md
#	.mokogitea/ISSUE_TEMPLATE/security.md
#	.mokogitea/branch-protection.yml
#	.mokogitea/bulk-repo-sync.yml
#	.mokogitea/pr-branch-check.yml
#	.mokogitea/renovate.yml
#	.mokogitea/sync-wikis.yml
#	.mokogitea/workflows/auto-bump.yml
#	.mokogitea/workflows/auto-release.yml
#	.mokogitea/workflows/ci-platform.yml
#	.mokogitea/workflows/cleanup.yml
#	.mokogitea/workflows/gitleaks.yml
#	.mokogitea/workflows/issue-branch.yml
#	.mokogitea/workflows/notify.yml
#	.mokogitea/workflows/pre-release.yml
#	.mokogitea/workflows/repo-health.yml
#	.mokogitea/workflows/security-audit.yml
#	.script-registry.json
#	CHANGELOG.md
#	PLUGIN_SCRIPTS.md
#	README.md
#	analysis/index.md
#	automation/bulk_joomla_template.php
#	automation/bulk_sync.php
#	automation/enrich_manifest_xml.php
#	automation/enrich_mokostandards_xml.php
#	automation/index.md
#	automation/migrate_to_gitea.php
#	automation/push_files.php
#	automation/push_manifest_xml.php
#	automation/push_mokostandards_xml.php
#	automation/repo_cleanup.php
#	bin/moko
#	cli/archive_repo.php
#	cli/audit_query.php
#	cli/badge_update.php
#	cli/branch_rename.php
#	cli/bulk_workflow_push.php
#	cli/bulk_workflow_trigger.php
#	cli/changelog_promote.php
#	cli/changelog_prune.php
#	cli/client_dashboard.php
#	cli/client_health_check.php
#	cli/client_inventory.php
#	cli/client_provision.php
#	cli/completion.php
#	cli/create_project.php
#	cli/create_repo.php
#	cli/deploy_joomla.php
#	cli/dev_branch_reset.php
#	cli/grafana_dashboard.php
#	cli/joomla_build.php
#	cli/joomla_compat_check.php
#	cli/joomla_metadata_validate.php
#	cli/joomla_release.php
#	cli/license_manage.php
#	cli/manifest_element.php
#	cli/manifest_licensing.php
#	cli/manifest_read.php
#	cli/package_build.php
#	cli/platform_detect.php
#	cli/release.php
#	cli/release_body_update.php
#	cli/release_cascade.php
#	cli/release_create.php
#	cli/release_manage.php
#	cli/release_mirror.php
#	cli/release_notes.php
#	cli/release_package.php
#	cli/release_promote.php
#	cli/release_publish.php
#	cli/release_validate.php
#	cli/release_verify.php
#	cli/scaffold_client.php
#	cli/sync_rulesets.php
#	cli/theme_lint.php
#	cli/updates_xml_build.php
#	cli/updates_xml_sync.php
#	cli/version_auto_bump.php
#	cli/version_bump.php
#	cli/version_bump_remote.php
#	cli/version_check.php
#	cli/version_read.php
#	cli/version_reset_dev.php
#	cli/version_set_platform.php
#	cli/wiki_sync.php
#	cli/workflow_sync.php
#	composer.json
#	deploy/backup-before-deploy.php
#	deploy/deploy-dolibarr.php
#	deploy/deploy-joomla.php
#	deploy/deploy-sftp.php
#	deploy/health-check.php
#	deploy/rollback-joomla.php
#	deploy/sync-joomla.php
#	fix/fix_line_endings.php
#	fix/fix_permissions.php
#	fix/fix_tabs.php
#	fix/fix_trailing_spaces.php
#	fix/index.md
#	index.md
#	lib/CliBase.php
#	lib/Common.php
#	lib/Enterprise/AbstractProjectPlugin.php
#	lib/Enterprise/ApiClient.php
#	lib/Enterprise/AuditLogger.php
#	lib/Enterprise/CheckpointManager.php
#	lib/Enterprise/CliFramework.php
#	lib/Enterprise/Config.php
#	lib/Enterprise/ConfigValidator.php
#	lib/Enterprise/EnterpriseReadinessValidator.php
#	lib/Enterprise/ErrorRecovery.php
#	lib/Enterprise/FileFixUtility.php
#	lib/Enterprise/GitHubAdapter.php
#	lib/Enterprise/GitPlatformAdapter.php
#	lib/Enterprise/InputValidator.php
#	lib/Enterprise/ManifestParser.php
#	lib/Enterprise/ManifestReader.php
#	lib/Enterprise/MetricsCollector.php
#	lib/Enterprise/MokoGiteaAdapter.php
#	lib/Enterprise/PackageBuilder.php
#	lib/Enterprise/PlatformAdapterFactory.php
#	lib/Enterprise/PluginFactory.php
#	lib/Enterprise/PluginRegistry.php
#	lib/Enterprise/Plugins/ApiPlugin.php
#	lib/Enterprise/Plugins/DocumentationPlugin.php
#	lib/Enterprise/Plugins/DolibarrPlugin.php
#	lib/Enterprise/Plugins/GenericPlugin.php
#	lib/Enterprise/Plugins/JoomlaPlugin.php
#	lib/Enterprise/Plugins/McpServerPlugin.php
#	lib/Enterprise/Plugins/MobilePlugin.php
#	lib/Enterprise/Plugins/NodeJsPlugin.php
#	lib/Enterprise/Plugins/PythonPlugin.php
#	lib/Enterprise/Plugins/TerraformPlugin.php
#	lib/Enterprise/Plugins/WordPressPlugin.php
#	lib/Enterprise/ProjectConfigValidator.php
#	lib/Enterprise/ProjectMetricsCollector.php
#	lib/Enterprise/ProjectPluginInterface.php
#	lib/Enterprise/ProjectTypeDetector.php
#	lib/Enterprise/RecoveryError.php
#	lib/Enterprise/RecoveryManager.php
#	lib/Enterprise/RepositoryHealthChecker.php
#	lib/Enterprise/RepositorySynchronizer.php
#	lib/Enterprise/RetryHelper.php
#	lib/Enterprise/SecurityValidator.php
#	lib/Enterprise/SourceResolver.php
#	lib/Enterprise/SynchronizationException.php
#	lib/Enterprise/TransactionManager.php
#	lib/Enterprise/UnifiedValidation.php
#	lib/index.md
#	lib/plugins/Joomla/UpdateXmlGenerator.php
#	maintenance/index.md
#	maintenance/pin_action_shas.php
#	maintenance/repo_inventory.php
#	maintenance/rotate_secrets.php
#	maintenance/setup_labels.php
#	maintenance/sync_dolibarr_readmes.php
#	maintenance/update_repo_inventory.php
#	maintenance/update_sha_hashes.php
#	maintenance/update_version_from_readme.php
#	mcp/config.example.json
#	mcp/package.json
#	mcp/src/config.ts
#	mcp/src/index.ts
#	mcp/src/runner.ts
#	mcp/src/types.ts
#	phpcs.xml
#	plugin_health_check.php
#	plugin_list.php
#	plugin_metrics.php
#	plugin_readiness.php
#	plugin_validate.php
#	release/generate_dolibarr_version_txt.php
#	release/generate_joomla_update_xml.php
#	src/functions.php
#	templates/configs/README.md
#	templates/configs/index.md
#	templates/configs/manifest.xml.template
#	templates/configs/manifest.yml.template
#	templates/configs/mokostandards.xml.template
#	templates/configs/mokostandards.yml.template
#	templates/configs/phpcs.xml
#	templates/docs/README.md
#	templates/docs/extra/README.md
#	templates/docs/extra/index.md
#	templates/docs/index.md
#	templates/docs/required/GOVERNANCE.md
#	templates/docs/required/README.md
#	templates/docs/required/index.md
#	templates/docs/required/template-CONTRIBUTING.md
#	templates/docs/required/template-README.md
#	templates/docs/required/template-SECURITY.md
#	templates/index.md
#	templates/licenses/README.md
#	templates/licenses/index.md
#	templates/makefiles/README.md
#	templates/mokogitea/CLAUDE.dolibarr.md.template
#	templates/mokogitea/CLAUDE.joomla.md.template
#	templates/mokogitea/CLAUDE.md.template
#	templates/mokogitea/ISSUE_TEMPLATE/config.yml
#	templates/mokogitea/ISSUE_TEMPLATE/documentation.md
#	templates/mokogitea/ISSUE_TEMPLATE/dolibarr_module_id_request.md
#	templates/mokogitea/ISSUE_TEMPLATE/feature_request.md
#	templates/mokogitea/ISSUE_TEMPLATE/security.md
#	templates/mokogitea/README.md
#	templates/mokogitea/copilot-instructions.dolibarr.md.template
#	templates/mokogitea/copilot-instructions.joomla.md.template
#	templates/mokogitea/copilot-instructions.md.template
#	templates/mokogitea/dependabot.yml.template
#	templates/mokogitea/override.tf.template
#	templates/required/README.md
#	templates/schemas/README.md
#	templates/schemas/manifest-schema.xsd
#	templates/schemas/moko-platform-schema.xsd
#	templates/schemas/mokostandards-schema.xsd
#	templates/schemas/schemas/README.md
#	templates/schemas/template-repository-structure.xml
#	templates/scripts/README.md
#	templates/scripts/common/CliBase.template.php
#	templates/scripts/fix/index.md
#	templates/scripts/index.md
#	templates/scripts/release/index.md
#	templates/scripts/release/package_dolibarr.php
#	templates/scripts/release/package_joomla.php
#	templates/scripts/sftp-config/README.md
#	templates/scripts/validate/dolibarr_module.php
#	templates/scripts/validate/index.md
#	templates/scripts/validate/validate_manifest.php
#	templates/scripts/validate/validate_structure.php
#	templates/security/README.md
#	templates/security/index.php
#	templates/stubs/dolibarr.php
#	templates/stubs/joomla.php
#	templates/web/index.php
#	tests/Enterprise/GitPlatformAdapterTest.php
#	tests/Unit/VersionBumpTest.php
#	tests/Unit/VersionReadTest.php
#	tests/index.md
#	tests/test_circuit_breaker_handling.php
#	tests/test_enterprise_libraries.php
#	validate/SECURITY_SCANNING.md
#	validate/auto_detect_platform.php
#	validate/check_changelog.php
#	validate/check_client_theme.php
#	validate/check_composer_deps.php
#	validate/check_dolibarr_module.php
#	validate/check_enterprise_readiness.php
#	validate/check_file_integrity.php
#	validate/check_joomla_manifest.php
#	validate/check_language_structure.php
#	validate/check_license_headers.php
#	validate/check_no_secrets.php
#	validate/check_paths.php
#	validate/check_php_syntax.php
#	validate/check_repo_health.php
#	validate/check_structure.php
#	validate/check_tabs.php
#	validate/check_version_consistency.php
#	validate/check_wiki_health.php
#	validate/check_xml_wellformed.php
#	validate/index.md
#	validate/scan_drift.php
#	wrappers/auto_detect_platform.php
#	wrappers/bulk_sync.php
#	wrappers/check_changelog.php
#	wrappers/check_dolibarr_module.php
#	wrappers/check_enterprise_readiness.php
#	wrappers/check_joomla_manifest.php
#	wrappers/check_language_structure.php
#	wrappers/check_license_headers.php
#	wrappers/check_no_secrets.php
#	wrappers/check_paths.php
#	wrappers/check_php_syntax.php
#	wrappers/check_repo_health.php
#	wrappers/check_structure.php
#	wrappers/check_tabs.php
#	wrappers/check_version_consistency.php
#	wrappers/check_xml_wellformed.php
#	wrappers/deploy_sftp.php
#	wrappers/fix_line_endings.php
#	wrappers/fix_permissions.php
#	wrappers/fix_tabs.php
#	wrappers/fix_trailing_spaces.php
#	wrappers/gen_wrappers.php
#	wrappers/index.md
#	wrappers/pin_action_shas.php
#	wrappers/plugin_health_check.php
#	wrappers/plugin_list.php
#	wrappers/plugin_metrics.php
#	wrappers/plugin_readiness.php
#	wrappers/plugin_validate.php
#	wrappers/scan_drift.php
#	wrappers/setup_labels.php
#	wrappers/sync_dolibarr_readmes.php
#	wrappers/update_sha_hashes.php
#	wrappers/update_version_from_readme.php
2026-06-20 21:43:38 -05:00

550 lines
21 KiB
PHP

#!/usr/bin/env php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
<<<<<<< HEAD
* DEFGROUP: MokoCLI.CLI
* INGROUP: MokoCLI
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
=======
* DEFGROUP: mokoplatform.CLI
* INGROUP: mokoplatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
>>>>>>> main
* PATH: /cli/release_package.php
* BRIEF: Build packages (ZIP + tar.gz) with SHA-256 and upload to Gitea release
*/
declare(strict_types=1);
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoCli\{CliFramework, SourceResolver};
class ReleasePackageCli extends CliFramework
{
/** @var array<int, string> */
private array $excludePatterns = [
'sftp-config*',
'.ftpignore',
'*.ppk',
'*.pem',
'*.key',
'.env*',
'*.local',
'.build-trigger',
];
protected function configure(): void
{
$this->setDescription('Build packages (ZIP + tar.gz) with SHA-256 and upload to Gitea release');
$this->addArgument('--path', 'Repository root path', '.');
$this->addArgument('--version', 'Release version', '');
$this->addArgument('--tag', 'Release tag name', '');
$this->addArgument('--token', 'Gitea API token', '');
$this->addArgument('--api-base', 'Gitea API base URL', '');
$this->addArgument('--repo', 'Repo name for element detection fallback', '');
$this->addArgument('--output', 'Output directory for built packages', '');
}
protected function run(): int
{
$path = $this->getArgument('--path');
$version = $this->getArgument('--version');
$tag = $this->getArgument('--tag');
$token = $this->getArgument('--token');
$apiBase = $this->getArgument('--api-base');
$repoName = $this->getArgument('--repo');
$outputDir = $this->getArgument('--output');
if ($outputDir === '' || $outputDir === null) {
$outputDir = sys_get_temp_dir();
}
// Allow token from environment
if ($token === '' || $token === null) {
$token = getenv('MOKOGITEA_TOKEN') ?: (getenv('GITEA_TOKEN') ?: '');
}
if ($version === '' || $tag === '' || $token === '' || $apiBase === '') {
$this->log('ERROR', "Usage: release_package.php --path . --version VER --tag TAG --token TOKEN --api-base URL");
$this->log('ERROR', " --repo REPO Repo name for element detection fallback");
$this->log('ERROR', " --output DIR Output directory for built packages (default: sys_get_temp_dir())");
$this->log('ERROR', " Token can also be set via MOKOGITEA_TOKEN or GITEA_TOKEN env var");
return 1;
}
$root = realpath($path) ?: $path;
// ── Read platform from .mokogitea/manifest.xml ───────────────────────
$detectedPlatform = 'generic';
$detectedEntryPoint = '';
$mokoManifest = "{$root}/.mokogitea/manifest.xml";
if (file_exists($mokoManifest)) {
$mokoXml = @simplexml_load_file($mokoManifest);
if ($mokoXml !== false) {
$rawPlatform = (string)($mokoXml->governance->platform ?? '');
if ($rawPlatform !== '') {
$detectedPlatform = match ($rawPlatform) {
'waas-component' => 'joomla',
'crm-module' => 'dolibarr',
default => $rawPlatform,
};
}
$detectedEntryPoint = (string)($mokoXml->build->{"entry-point"} ?? '');
}
}
// ── Detect element metadata from manifest XML ────────────────────────
$extElement = '';
$extType = '';
$extFolder = '';
$typePrefix = '';
SourceResolver::warnIfLegacy($root);
$manifestFiles = array_merge(
SourceResolver::globSource($root, 'pkg_*.xml'),
SourceResolver::globSource($root, '*.xml'),
glob("{$root}/*.xml") ?: []
);
$extManifest = null;
foreach ($manifestFiles as $file) {
$content = file_get_contents($file);
if ($content !== false && strpos($content, '<extension') !== false) {
$extManifest = $file;
break;
}
}
if ($extManifest !== null) {
$xml = file_get_contents($extManifest);
if ($xml === false) {
$xml = '';
}
// Extension type and folder
if (preg_match('/type="([^"]*)"/', $xml, $tm)) {
$extType = $tm[1];
}
if (preg_match('/group="([^"]*)"/', $xml, $gm)) {
$extFolder = $gm[1];
}
// Element name
if (preg_match('/<element>([^<]+)<\/element>/', $xml, $em)) {
$extElement = $em[1];
}
if ($extElement === '' && preg_match('/module="([^"]*)"/', $xml, $mm)) {
$extElement = $mm[1];
}
if ($extElement === '' && preg_match('/plugin="([^"]*)"/', $xml, $pm)) {
$extElement = $pm[1];
}
if ($extType === 'package' && preg_match('/<packagename>([^<]+)<\/packagename>/', $xml, $pn)) {
$extElement = $pn[1];
}
if ($extElement === '') {
$extElement = strtolower(basename($extManifest, '.xml'));
if (in_array($extElement, ['templatedetails', 'manifest'], true)) {
$extElement = strtolower(str_replace([' ', '-'], '', $repoName !== '' ? $repoName : basename($root)));
}
}
}
// Fallback to repo name
if ($extElement === '') {
$extElement = strtolower(str_replace([' ', '-'], '', $repoName !== '' ? $repoName : basename($root)));
}
// Strip existing type prefix to prevent duplication
$extElement = (string) preg_replace('/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)/', '', $extElement);
// Compute type prefix
switch ($extType) {
case 'plugin':
$typePrefix = "plg_{$extFolder}_";
break;
case 'module':
$typePrefix = 'mod_';
break;
case 'component':
$typePrefix = 'com_';
break;
case 'template':
$typePrefix = 'tpl_';
break;
case 'library':
$typePrefix = 'lib_';
break;
case 'package':
$typePrefix = 'pkg_';
break;
}
echo "Element: {$typePrefix}{$extElement}\n";
echo "Type: {$extType}\n";
// ── Compute filenames ────────────────────────────────────────────────
$baseName = "{$typePrefix}{$extElement}-{$version}";
$zipFile = "{$outputDir}/{$baseName}.zip";
$tarFile = "{$outputDir}/{$baseName}.tar.gz";
echo "ZIP: {$baseName}.zip\n";
echo "TAR: {$baseName}.tar.gz\n";
// ── Find source directory ────────────────────────────────────────────
$sourceDir = null;
if ($detectedEntryPoint !== '') {
$entryDir = rtrim(dirname($detectedEntryPoint) === '.' ? $detectedEntryPoint : dirname($detectedEntryPoint), '/');
if (is_dir("{$root}/{$entryDir}")) {
$sourceDir = "{$root}/{$entryDir}";
}
}
if ($sourceDir === null) {
$sourceDir = SourceResolver::resolveAbsolute($root);
}
if ($sourceDir === null) {
echo "No source/ or src/ directory found — skipping package build\n";
return 0;
}
echo "Source: {$sourceDir}\n";
// ── Build packages ───────────────────────────────────────────────────
$isJoomlaPackage = ($extType === 'package' && is_dir("{$sourceDir}/packages"));
if ($isJoomlaPackage) {
echo "Building Joomla package (sub-extensions)...\n";
$zip = new \ZipArchive();
if ($zip->open($zipFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
$this->log('ERROR', "Failed to create ZIP: {$zipFile}");
return 1;
}
$packageDirs = glob("{$sourceDir}/packages/*", GLOB_ONLYDIR) ?: [];
foreach ($packageDirs as $pkgDir) {
$subName = basename($pkgDir);
$subZipPath = "{$outputDir}/{$subName}.zip";
// 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.
$subSourceDir = $pkgDir;
$subSrcAbs = SourceResolver::resolveAbsolute($pkgDir);
if ($subSrcAbs !== null) {
$srcManifests = array_merge(
glob("{$subSrcAbs}/*.xml") ?: [],
glob("{$subSrcAbs}/pkg_*.xml") ?: []
);
foreach ($srcManifests as $mf) {
if (strpos(file_get_contents($mf) ?: '', '<extension') !== false) {
$subSourceDir = $subSrcAbs;
$subSrcName = SourceResolver::resolve($pkgDir);
echo " Sub-package {$subName}: using {$subSrcName}/ entry-point\n";
break;
}
}
}
$subZip = new \ZipArchive();
if ($subZip->open($subZipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
$this->log('ERROR', "Failed to create sub-package ZIP: {$subZipPath}");
continue;
}
$this->addDirToZip($subZip, $subSourceDir, '', $this->excludePatterns);
$subZip->close();
$zip->addFile($subZipPath, "packages/{$subName}.zip");
echo " Sub-package: {$subName}.zip\n";
}
$pkgManifests = glob("{$sourceDir}/pkg_*.xml") ?: [];
foreach ($pkgManifests as $pkgXml) {
$pkgContent = file_get_contents($pkgXml);
if (strpos($pkgContent, '<files>') !== false && strpos($pkgContent, 'folder="packages"') === false) {
$pkgContent = str_replace('<files>', '<files folder="packages">', $pkgContent);
file_put_contents($pkgXml, $pkgContent);
echo " Fixed: added folder=\"packages\" to " . basename($pkgXml) . "\n";
}
}
$topLevelFiles = array_merge(
glob("{$sourceDir}/*.xml") ?: [],
glob("{$sourceDir}/*.php") ?: []
);
foreach ($topLevelFiles as $tlFile) {
if (!$this->isExcluded(basename($tlFile), $this->excludePatterns)) {
$zip->addFile($tlFile, basename($tlFile));
}
}
$topLevelDirs = glob("{$sourceDir}/*", GLOB_ONLYDIR) ?: [];
foreach ($topLevelDirs as $tlDir) {
$dirName = basename($tlDir);
if ($dirName === 'packages') {
continue;
}
$this->addDirToZip($zip, $tlDir, $dirName, $this->excludePatterns);
echo " Included dir: {$dirName}/\n";
}
$zip->close();
echo "ZIP created: {$zipFile}\n";
} else {
echo "Building standard extension ZIP...\n";
$zip = new \ZipArchive();
if ($zip->open($zipFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
$this->log('ERROR', "Failed to create ZIP: {$zipFile}");
return 1;
}
$this->addDirToZip($zip, $sourceDir, '', $this->excludePatterns);
$zip->close();
echo "ZIP created: {$zipFile}\n";
}
// ── Build tar.gz ─────────────────────────────────────────────────────
$tarExcludeArgs = [];
foreach ($this->excludePatterns as $pattern) {
$tarExcludeArgs[] = '--exclude=' . escapeshellarg($pattern);
}
$tarCommand = sprintf(
'tar -czf %s -C %s %s .',
escapeshellarg($tarFile),
escapeshellarg($sourceDir),
implode(' ', $tarExcludeArgs)
);
$tarReturnCode = 0;
$tarOutputLines = [];
exec($tarCommand . ' 2>&1', $tarOutputLines, $tarReturnCode);
if (!file_exists($tarFile)) {
$this->log('ERROR', "Failed to create tar.gz: {$tarFile}");
if ($tarOutputLines !== []) {
$this->log('ERROR', implode("\n", $tarOutputLines));
}
return 1;
}
echo "TAR created: {$tarFile}\n";
// ── Compute SHA-256 checksums ────────────────────────────────────────
$zipHash = hash_file('sha256', $zipFile);
$tarHash = hash_file('sha256', $tarFile);
if ($zipHash === false || $tarHash === false) {
$this->log('ERROR', "Failed to compute SHA-256 checksums");
return 1;
}
$zipSha = "{$zipFile}.sha256";
$tarSha = "{$tarFile}.sha256";
file_put_contents($zipSha, "{$zipHash} {$baseName}.zip\n");
file_put_contents($tarSha, "{$tarHash} {$baseName}.tar.gz\n");
echo "SHA-256 (ZIP): {$zipHash}\n";
echo "SHA-256 (TAR): {$tarHash}\n";
echo "sha256_zip={$zipHash}\n";
echo "zip_name={$baseName}.zip\n";
// Write to GITHUB_OUTPUT if available
$ghOutput = getenv('GITHUB_OUTPUT');
if ($ghOutput) {
file_put_contents($ghOutput, "sha256_zip={$zipHash}\nzip_name={$baseName}.zip\n", FILE_APPEND);
}
// ── Get release ID from tag ──────────────────────────────────────────
$result = $this->giteaApiRequest("{$apiBase}/releases/tags/{$tag}", $token);
if ($result['data'] === null || !isset($result['data']['id'])) {
$this->log('ERROR', "No release found for tag: {$tag} (HTTP {$result['code']})");
return 1;
}
$releaseId = (int) $result['data']['id'];
echo "Release ID: {$releaseId} (tag: {$tag})\n";
// ── Delete existing assets with same names ───────────────────────────
$assetsResult = $this->giteaApiRequest("{$apiBase}/releases/{$releaseId}/assets", $token);
$existingAssets = $assetsResult['data'] ?? [];
$uploadNames = [
"{$baseName}.zip",
"{$baseName}.tar.gz",
"{$baseName}.zip.sha256",
"{$baseName}.tar.gz.sha256",
];
foreach ($existingAssets as $asset) {
if (!is_array($asset)) {
continue;
}
$assetName = $asset['name'] ?? '';
$assetId = $asset['id'] ?? 0;
if (in_array($assetName, $uploadNames, true) && $assetId > 0) {
$this->giteaApiRequest("{$apiBase}/releases/{$releaseId}/assets/{$assetId}", $token, 'DELETE');
echo "Deleted existing asset: {$assetName}\n";
}
}
// ── Upload assets ────────────────────────────────────────────────────
$filesToUpload = [
"{$baseName}.zip" => $zipFile,
"{$baseName}.tar.gz" => $tarFile,
"{$baseName}.zip.sha256" => $zipSha,
"{$baseName}.tar.gz.sha256" => $tarSha,
];
$uploaded = 0;
foreach ($filesToUpload as $name => $localPath) {
if (!file_exists($localPath)) {
$this->log('ERROR', "File not found, skipping: {$localPath}");
continue;
}
$uploadUrl = "{$apiBase}/releases/{$releaseId}/assets?name=" . urlencode($name);
$httpCode = $this->giteaUploadAsset($uploadUrl, $token, $localPath);
$status = ($httpCode >= 200 && $httpCode < 300) ? 'OK' : "FAILED ({$httpCode})";
echo "Upload: {$name}{$status}\n";
if ($httpCode >= 200 && $httpCode < 300) {
$uploaded++;
}
}
// ── Summary ──────────────────────────────────────────────────────────
echo "\n";
echo "Package build complete\n";
echo " Element: {$typePrefix}{$extElement}\n";
echo " Version: {$version}\n";
echo " Tag: {$tag}\n";
echo " Uploaded: {$uploaded}/" . count($filesToUpload) . " asset(s)\n";
return $uploaded === count($filesToUpload) ? 0 : 1;
}
/**
* Perform a Gitea API request.
*
* @return array{data: array<string, mixed>|null, code: int}
*/
private function giteaApiRequest(string $url, string $token, string $method = 'GET', ?string $body = null): array
{
$ch = curl_init($url);
if ($ch === false) {
return ['data' => null, 'code' => 0];
}
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: token {$token}",
'Content-Type: application/json',
],
CURLOPT_TIMEOUT => 30,
CURLOPT_CUSTOMREQUEST => $method,
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$response = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode < 200 || $httpCode >= 300 || !is_string($response) || $response === '') {
return ['data' => null, 'code' => $httpCode];
}
$decoded = json_decode($response, true);
return ['data' => is_array($decoded) ? $decoded : null, 'code' => $httpCode];
}
/**
* Upload a file as a release asset.
*/
private function giteaUploadAsset(string $url, string $token, string $filePath): int
{
$ch = curl_init($url);
if ($ch === false) {
return 0;
}
$fileContent = file_get_contents($filePath);
if ($fileContent === false) {
return 0;
}
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: token {$token}",
'Content-Type: application/octet-stream',
],
CURLOPT_POSTFIELDS => $fileContent,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
]);
curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $httpCode;
}
/**
* Check if a filename matches any exclusion pattern.
*/
private function isExcluded(string $filename, array $patterns): bool
{
$basename = basename($filename);
foreach ($patterns as $pattern) {
if (fnmatch($pattern, $basename)) {
return true;
}
}
return false;
}
/**
* Recursively add files from a directory to a ZipArchive.
*/
private function addDirToZip(\ZipArchive $zip, string $sourceDir, string $prefix, array $excludes): void
{
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($sourceDir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($iterator as $file) {
if (!$file instanceof \SplFileInfo || !$file->isFile()) {
continue;
}
$realPath = $file->getRealPath();
if ($realPath === false) {
continue;
}
if ($this->isExcluded($file->getFilename(), $excludes)) {
continue;
}
$relativePath = substr($realPath, strlen($sourceDir) + 1);
// Normalise to forward slashes for ZIP compatibility
$relativePath = str_replace('\\', '/', $relativePath);
$archivePath = $prefix !== '' ? "{$prefix}/{$relativePath}" : $relativePath;
$zip->addFile($realPath, $archivePath);
}
}
}
$app = new ReleasePackageCli();
exit($app->execute());