diff --git a/.mokogitea/workflows/ci-platform.yml b/.mokogitea/workflows/ci-platform.yml index b17e62b..79b5a4a 100644 --- a/.mokogitea/workflows/ci-platform.yml +++ b/.mokogitea/workflows/ci-platform.yml @@ -412,6 +412,12 @@ jobs: if: always() steps: + - name: Checkout + uses: actions/checkout@v4 + with: + sparse-checkout: automation/ci-issue-reporter.sh + sparse-checkout-cone-mode: false + - name: Check gate results run: | { @@ -437,3 +443,46 @@ jobs: echo "::error::One or more CI gates failed" exit 1 fi + + - name: "File issues for failed gates" + if: >- + always() && + (needs.code-quality.result == 'failure' || + needs.tests.result == 'failure' || + needs.self-health.result == 'failure' || + needs.governance.result == 'failure' || + needs.templates.result == 'failure') + env: + GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + chmod +x automation/ci-issue-reporter.sh + REPORTER="./automation/ci-issue-reporter.sh" + WF="Platform CI" + + report_gate() { + local gate="$1" result="$2" details="$3" + if [ "$result" = "failure" ]; then + "$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error + fi + } + + report_gate "Code Quality" \ + "${{ needs.code-quality.result }}" \ + "PHPCS (PSR-12), PHPStan, or PHP syntax checks failed. Run \`composer check\` locally to reproduce." + + report_gate "Unit Tests" \ + "${{ needs.tests.result }}" \ + "PHPUnit tests failed on one or more PHP versions (8.1, 8.2, 8.3). Run \`vendor/bin/phpunit --testdox\` locally." + + report_gate "Self-Health" \ + "${{ needs.self-health.result }}" \ + "Self-health score fell below the 80% threshold. Run \`php bin/moko health -- --path .\` locally." + + report_gate "Governance" \ + "${{ needs.governance.result }}" \ + "Governance checks failed (license headers, secrets, or version consistency). Check the CI run summary for specifics." + + report_gate "Template Integrity" \ + "${{ needs.templates.result }}" \ + "Workflow or gitignore templates failed YAML validation or are missing required entries." diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 3c44a06..2c46832 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: moko-platform.Automation -# VERSION: 09.23.00 +# VERSION: 09.24.00 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/README.md b/README.md index 0286944..fef7c46 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ DEFGROUP: MokoPlatform.Root INGROUP: MokoPlatform REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform PATH: /README.md -VERSION: 09.23.00 +VERSION: 09.24.00 BRIEF: Project overview and documentation --> diff --git a/automation/ci-issue-reporter.sh b/automation/ci-issue-reporter.sh new file mode 100644 index 0000000..65c47ba --- /dev/null +++ b/automation/ci-issue-reporter.sh @@ -0,0 +1,237 @@ +#!/usr/bin/env bash +# ============================================================================ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Automation.CI +# INGROUP: moko-platform.Automation +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /automation/ci-issue-reporter.sh +# VERSION: 09.23.00 +# BRIEF: Creates or updates a Gitea issue when a CI gate fails. +# Deduplicates by searching open issues with the "ci-auto" label +# whose title matches the gate. If a matching issue exists, a comment +# is appended instead of opening a duplicate. +# ============================================================================ + +set -euo pipefail + +# ── Defaults ──────────────────────────────────────────────────────────────── +GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}" +GITEA_TOKEN="${GITEA_TOKEN:-}" +REPO="${GITHUB_REPOSITORY:-}" +RUN_URL="${GITHUB_SERVER_URL:-${GITEA_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}" +LABEL_NAME="ci-auto" +LABEL_COLOR="#e11d48" + +GATE="" +DETAILS="" +SEVERITY="error" +WORKFLOW="" + +# ── Parse arguments ───────────────────────────────────────────────────────── +usage() { + cat </dev/null || echo "000") + + if [[ "$exists" == "200" ]]; then + # Check if label already exists + local found + found=$(curl -sf \ + -H "Authorization: token ${GITEA_TOKEN}" \ + "${API}/labels" 2>/dev/null \ + | grep -o "\"name\":\"${LABEL_NAME}\"" || true) + + if [[ -z "$found" ]]; then + curl -sf -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/labels" \ + -d "{\"name\":\"${LABEL_NAME}\",\"color\":\"${LABEL_COLOR}\",\"description\":\"Auto-created by CI issue reporter\"}" \ + > /dev/null 2>&1 || true + fi + fi +} + +# ── Search for existing open issue ────────────────────────────────────────── +find_existing_issue() { + # URL-encode the gate name for the query + local query + query=$(printf '%s' "[CI] ${GATE}" | sed 's/ /%20/g; s/\[/%5B/g; s/\]/%5D/g') + + local response + response=$(curl -sf \ + -H "Authorization: token ${GITEA_TOKEN}" \ + "${API}/issues?type=issues&state=open&labels=${LABEL_NAME}&q=${query}&limit=5" \ + 2>/dev/null || echo "[]") + + # Extract the first matching issue number + echo "$response" \ + | grep -oP '"number":\s*\K[0-9]+' \ + | head -1 +} + +# ── Build issue body ──────────────────────────────────────────────────────── +build_body() { + local severity_badge + if [[ "$SEVERITY" == "error" ]]; then + severity_badge="**Severity:** Error" + else + severity_badge="**Severity:** Warning" + fi + + cat </dev/null) + + HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/issues/${EXISTING}/comments" \ + -d "${COMMENT_JSON}" 2>/dev/null || echo "000") + + if [[ "$HTTP" == "201" ]]; then + echo "Commented on existing issue #${EXISTING}" + else + echo "WARNING: Failed to comment on issue #${EXISTING} (HTTP ${HTTP})" + fi +else + # Create new issue + ISSUE_BODY=$(build_body) + ISSUE_JSON=$(python3 -c " +import sys, json +body = sys.stdin.read() +print(json.dumps({ + 'title': sys.argv[1], + 'body': body, + 'labels': [] +}))" "$TITLE" <<< "$ISSUE_BODY" 2>/dev/null) + + # Create the issue + RESPONSE=$(curl -sf -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/issues" \ + -d "${ISSUE_JSON}" 2>/dev/null || echo "{}") + + ISSUE_NUM=$(echo "$RESPONSE" | grep -oP '"number":\s*\K[0-9]+' | head -1) + + if [[ -n "$ISSUE_NUM" ]]; then + # Apply label (separate call — more reliable across Gitea versions) + LABEL_ID=$(curl -sf \ + -H "Authorization: token ${GITEA_TOKEN}" \ + "${API}/labels" 2>/dev/null \ + | grep -oP "\"id\":\s*\K[0-9]+(?=[^}]*\"name\":\s*\"${LABEL_NAME}\")" \ + | head -1 || true) + + if [[ -n "$LABEL_ID" ]]; then + curl -sf -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/issues/${ISSUE_NUM}/labels" \ + -d "{\"labels\":[${LABEL_ID}]}" \ + > /dev/null 2>&1 || true + fi + + echo "Created issue #${ISSUE_NUM}: ${TITLE}" + else + echo "WARNING: Failed to create issue" + echo "Response: ${RESPONSE}" + fi +fi diff --git a/automation/push_files.php b/automation/push_files.php index e101f9e..f6cdc19 100644 --- a/automation/push_files.php +++ b/automation/push_files.php @@ -230,7 +230,8 @@ class PushFiles extends CliFramework { // Read platform from repo's .mokogitea/manifest.xml via API try { - $manifestData = $this->adapter->getFileContent($org, $repo, '.mokogitea/manifest.xml', 'main'); + $fileInfo = $this->adapter->getFileContents($org, $repo, '.mokogitea/manifest.xml', 'main'); + $manifestData = isset($fileInfo['content']) ? base64_decode($fileInfo['content']) : ''; if (!empty($manifestData)) { $xml = @simplexml_load_string($manifestData); if ($xml !== false) { diff --git a/cli/branch_rename.php b/cli/branch_rename.php index 8a22e6c..09beebc 100644 --- a/cli/branch_rename.php +++ b/cli/branch_rename.php @@ -10,7 +10,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/branch_rename.php - * VERSION: 09.23.00 + * VERSION: 09.24.00 * BRIEF: Rename a git branch via Gitea API (create new, update PR, delete old) */ diff --git a/cli/bulk_workflow_push.php b/cli/bulk_workflow_push.php index 15fee97..60485ec 100644 --- a/cli/bulk_workflow_push.php +++ b/cli/bulk_workflow_push.php @@ -12,7 +12,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/bulk_workflow_push.php - * VERSION: 09.23.00 + * VERSION: 09.24.00 * BRIEF: Push a workflow file to all governed repos via the Gitea Contents API */ diff --git a/cli/bulk_workflow_trigger.php b/cli/bulk_workflow_trigger.php index ee33899..6ada026 100644 --- a/cli/bulk_workflow_trigger.php +++ b/cli/bulk_workflow_trigger.php @@ -12,7 +12,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/bulk_workflow_trigger.php - * VERSION: 09.23.00 + * VERSION: 09.24.00 * BRIEF: Trigger a workflow across multiple repos at once */ diff --git a/cli/client_dashboard.php b/cli/client_dashboard.php index cb54228..fd678cb 100644 --- a/cli/client_dashboard.php +++ b/cli/client_dashboard.php @@ -12,7 +12,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/client_dashboard.php - * VERSION: 09.23.00 + * VERSION: 09.24.00 * BRIEF: Generate unified client dashboard HTML */ diff --git a/cli/client_inventory.php b/cli/client_inventory.php index 458e84c..452c226 100644 --- a/cli/client_inventory.php +++ b/cli/client_inventory.php @@ -12,7 +12,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/client_inventory.php - * VERSION: 09.23.00 + * VERSION: 09.24.00 * BRIEF: Discover and list all client-waas repos with their server configuration status */ diff --git a/cli/client_provision.php b/cli/client_provision.php index 4d37687..ae22297 100644 --- a/cli/client_provision.php +++ b/cli/client_provision.php @@ -12,7 +12,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/client_provision.php - * VERSION: 09.23.00 + * VERSION: 09.24.00 * BRIEF: Provision a new client environment end-to-end */ diff --git a/cli/grafana_dashboard.php b/cli/grafana_dashboard.php index e6a89ea..04b0195 100644 --- a/cli/grafana_dashboard.php +++ b/cli/grafana_dashboard.php @@ -12,7 +12,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/grafana_dashboard.php - * VERSION: 09.23.00 + * VERSION: 09.24.00 * BRIEF: Manage Grafana dashboards via API */ diff --git a/cli/joomla_build.php b/cli/joomla_build.php index 1896611..701b18b 100644 --- a/cli/joomla_build.php +++ b/cli/joomla_build.php @@ -10,7 +10,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/joomla_build.php - * VERSION: 09.23.00 + * VERSION: 09.24.00 * BRIEF: Build a Joomla extension ZIP from manifest — all types supported * NOTE: Called by pre-release and auto-release workflows. */ diff --git a/cli/manifest_read.php b/cli/manifest_read.php index eacb9bb..b6f8b6f 100644 --- a/cli/manifest_read.php +++ b/cli/manifest_read.php @@ -10,7 +10,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/manifest_read.php - * VERSION: 09.23.00 + * VERSION: 09.24.00 * BRIEF: Parse .manifest.xml and output requested field(s) for CI consumption */ diff --git a/cli/release_cascade.php b/cli/release_cascade.php index 70d00ef..15e5188 100644 --- a/cli/release_cascade.php +++ b/cli/release_cascade.php @@ -10,7 +10,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/release_cascade.php - * VERSION: 09.23.00 + * VERSION: 09.24.00 * BRIEF: DEPRECATED — cascade behavior removed. Each release stream is independent. */ diff --git a/cli/release_package.php b/cli/release_package.php index be6abf5..6b2d3df 100644 --- a/cli/release_package.php +++ b/cli/release_package.php @@ -230,12 +230,31 @@ class ReleasePackageCli extends CliFramework $subName = basename($pkgDir); $subZipPath = "{$outputDir}/{$subName}.zip"; + // If sub-package is a full repo checkout (e.g. git submodule), + // look for a src/ subdirectory containing a Joomla manifest XML + // and zip that instead of the repo root. + $subSourceDir = $pkgDir; + $srcCandidate = "{$pkgDir}/src"; + if (is_dir($srcCandidate)) { + $srcManifests = array_merge( + glob("{$srcCandidate}/*.xml") ?: [], + glob("{$srcCandidate}/pkg_*.xml") ?: [] + ); + foreach ($srcManifests as $mf) { + if (strpos(file_get_contents($mf) ?: '', 'open($subZipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) { $this->log('ERROR', "Failed to create sub-package ZIP: {$subZipPath}"); continue; } - $this->addDirToZip($subZip, $pkgDir, '', $this->excludePatterns); + $this->addDirToZip($subZip, $subSourceDir, '', $this->excludePatterns); $subZip->close(); $zip->addFile($subZipPath, "packages/{$subName}.zip"); diff --git a/cli/release_publish.php b/cli/release_publish.php index 957de2c..907264b 100644 --- a/cli/release_publish.php +++ b/cli/release_publish.php @@ -10,7 +10,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/release_publish.php - * VERSION: 09.23.00 + * VERSION: 09.24.00 * BRIEF: Publish a release and create copies for all lesser stability streams. */ diff --git a/cli/scaffold_client.php b/cli/scaffold_client.php index 26cd822..02eeb22 100644 --- a/cli/scaffold_client.php +++ b/cli/scaffold_client.php @@ -12,7 +12,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/scaffold_client.php - * VERSION: 09.23.00 + * VERSION: 09.24.00 * BRIEF: Scaffold a new client-waas repo from Template-Client-WaaS with pre-configured settings */ diff --git a/cli/updates_xml_sync.php b/cli/updates_xml_sync.php index 6a7832b..3f38cd7 100644 --- a/cli/updates_xml_sync.php +++ b/cli/updates_xml_sync.php @@ -10,7 +10,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/updates_xml_sync.php - * VERSION: 09.23.00 + * VERSION: 09.24.00 * BRIEF: Sync updates.xml to target branches via Gitea API * NOTE: Called by pre-release and auto-release workflows after updates.xml * is modified on the current branch. Pushes the file to other branches diff --git a/cli/version_auto_bump.php b/cli/version_auto_bump.php index ea1b921..cae0e5d 100644 --- a/cli/version_auto_bump.php +++ b/cli/version_auto_bump.php @@ -10,7 +10,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/version_auto_bump.php - * VERSION: 09.23.00 + * VERSION: 09.24.00 * BRIEF: Auto patch-bump, set stability suffix, and commit — single CLI replacing inline workflow bash */ @@ -109,10 +109,18 @@ class VersionAutoBumpCli extends CliFramework echo "{$line}\n"; } - // Step 2: Read version + // Step 2: Read version (--quiet suppresses banner so only the version is output) $versionOutput = []; - exec("{$php} {$cli}/version_read.php --path " . escapeshellarg($path) . " 2>&1", $versionOutput, $versionRc); - $version = trim($versionOutput[0] ?? ''); + exec("{$php} {$cli}/version_read.php --path " . escapeshellarg($path) . " --quiet 2>&1", $versionOutput, $versionRc); + // Take the last non-empty line — the version is always the final output + $version = ''; + foreach (array_reverse($versionOutput) as $line) { + $line = trim($line); + if (preg_match('/^\d{2}\.\d{2}\.\d{2}/', $line)) { + $version = $line; + break; + } + } if (empty($version)) { echo "No version found — skipping\n"; diff --git a/cli/version_check.php b/cli/version_check.php index e9b8e82..6a9771b 100644 --- a/cli/version_check.php +++ b/cli/version_check.php @@ -10,7 +10,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/version_check.php - * VERSION: 09.23.00 + * VERSION: 09.24.00 * BRIEF: Validate version consistency across README, manifests, and sub-packages */ diff --git a/cli/version_set_platform.php b/cli/version_set_platform.php index 8b380c6..8b3a66f 100644 --- a/cli/version_set_platform.php +++ b/cli/version_set_platform.php @@ -53,6 +53,12 @@ class VersionSetPlatformCli extends CliFramework // Strip any existing suffix(es) before applying the correct one $version = preg_replace('/(-(dev|alpha|beta|rc))+$/', '', $version); + // Validate version format — must be XX.YY.ZZ to prevent XML corruption + if (!preg_match('/^\d{2}\.\d{2}\.\d{2}$/', $version)) { + $this->log('ERROR', "Invalid version format: '{$version}' — expected XX.YY.ZZ"); + return 1; + } + // Append stability suffix for non-stable releases $stabilitySuffixMap = [ 'stable' => '', diff --git a/cli/wiki_sync.php b/cli/wiki_sync.php index fd68aae..706abdc 100644 --- a/cli/wiki_sync.php +++ b/cli/wiki_sync.php @@ -10,7 +10,7 @@ * INGROUP: moko-platform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /cli/wiki_sync.php - * VERSION: 09.23.00 + * VERSION: 09.24.00 * BRIEF: Sync select wiki pages from moko-platform to all template repos */ diff --git a/deploy/backup-before-deploy.php b/deploy/backup-before-deploy.php index 165cbe2..484b7a5 100644 --- a/deploy/backup-before-deploy.php +++ b/deploy/backup-before-deploy.php @@ -12,7 +12,7 @@ * INGROUP: MokoPlatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /deploy/backup-before-deploy.php - * VERSION: 09.23.00 + * VERSION: 09.24.00 * BRIEF: Snapshot Joomla directories before deployment for rollback capability */ diff --git a/deploy/deploy-dolibarr.php b/deploy/deploy-dolibarr.php index b164e36..15047dc 100644 --- a/deploy/deploy-dolibarr.php +++ b/deploy/deploy-dolibarr.php @@ -12,7 +12,7 @@ * INGROUP: MokoPlatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /deploy/deploy-dolibarr.php - * VERSION: 09.23.00 + * VERSION: 09.24.00 * BRIEF: Deploy Dolibarr module files to a remote server via SFTP/rsync */ diff --git a/deploy/health-check.php b/deploy/health-check.php index 339d97b..455c8de 100644 --- a/deploy/health-check.php +++ b/deploy/health-check.php @@ -12,7 +12,7 @@ * INGROUP: MokoPlatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /deploy/health-check.php - * VERSION: 09.23.00 + * VERSION: 09.24.00 * BRIEF: Post-deploy health check — verify a Joomla site is responding correctly */ diff --git a/deploy/rollback-joomla.php b/deploy/rollback-joomla.php index d3f8b1d..2537f1b 100644 --- a/deploy/rollback-joomla.php +++ b/deploy/rollback-joomla.php @@ -12,7 +12,7 @@ * INGROUP: MokoPlatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /deploy/rollback-joomla.php - * VERSION: 09.23.00 + * VERSION: 09.24.00 * BRIEF: Rollback a Joomla deployment by restoring from a pre-deploy snapshot */ diff --git a/deploy/sync-joomla.php b/deploy/sync-joomla.php index 0c638c0..3aec234 100644 --- a/deploy/sync-joomla.php +++ b/deploy/sync-joomla.php @@ -12,7 +12,7 @@ * INGROUP: MokoPlatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /deploy/sync-joomla.php - * VERSION: 09.23.00 + * VERSION: 09.24.00 * BRIEF: Sync Joomla site directories between two servers via rsync over SSH */ diff --git a/lib/Enterprise/CliFramework.php b/lib/Enterprise/CliFramework.php index fe68f33..91a9a0b 100644 --- a/lib/Enterprise/CliFramework.php +++ b/lib/Enterprise/CliFramework.php @@ -141,6 +141,26 @@ abstract class CliFramework /** @var float Script start time for elapsed-time reporting. */ private float $startTime; + // ========================================================================= + // Display output — all decorative output goes to stderr + // ========================================================================= + + /** + * Write decorative/diagnostic output to stderr. + * + * All non-data output (banners, progress bars, section headers, status + * lines, log messages) MUST use this method so that stdout is reserved + * for machine-readable data. This ensures that shell captures like + * VERSION=$(php version_read.php --path .) + * only receive the actual data, not decorative text. + * + * @since 04.00.16 + */ + protected function display(string $text): void + { + fwrite(STDERR, $text); + } + // ========================================================================= // Constructor // ========================================================================= @@ -326,14 +346,14 @@ abstract class CliFramework protected function printHelp(): void { $w = $this->termWidth(); - echo $this->c(self::C_BOLD . self::C_CYAN, $this->scriptName); + $this->display($this->c(self::C_BOLD . self::C_CYAN, $this->scriptName)); if ($this->description !== '') { - echo ' — ' . $this->description; + $this->display(' — ' . $this->description); } - echo "\n"; - echo $this->c(self::C_DIM, str_repeat(self::BOX_H, $w)) . "\n\n"; - echo $this->c(self::C_BOLD, 'Usage:') . " php {$this->scriptName}.php [options]\n\n"; - echo $this->c(self::C_BOLD, 'Options:') . "\n"; + $this->display("\n"); + $this->display($this->c(self::C_DIM, str_repeat(self::BOX_H, $w)) . "\n\n"); + $this->display($this->c(self::C_BOLD, 'Usage:') . " php {$this->scriptName}.php [options]\n\n"); + $this->display($this->c(self::C_BOLD, 'Options:') . "\n"); $builtIn = [ '--help' => ['desc' => 'Show this help message', 'default' => null], @@ -348,16 +368,16 @@ abstract class CliFramework $hint = ($default !== null && $default !== false) ? $this->c(self::C_DIM, " (default: {$default})") : ''; - printf( + $this->display(sprintf( " %s%-22s%s%s%s\n", self::C_CYAN, $name, self::C_RESET, $def['desc'], $hint - ); + )); } - echo "\n"; + $this->display("\n"); } // ========================================================================= @@ -378,23 +398,23 @@ abstract class CliFramework $titleLine = $this->padRight($titleStyled, $inner, strlen($titleRaw)); $descLine = ($desc !== '') ? $this->padRight(" {$desc}", $inner) : null; - echo "\n"; - echo $this->c( + $this->display("\n"); + $this->display($this->c( self::C_CYAN, self::BOX_TL . str_repeat(self::BOX_H, $inner) . self::BOX_TR - ) . "\n"; - echo $this->c(self::C_CYAN, self::BOX_V) + ) . "\n"); + $this->display($this->c(self::C_CYAN, self::BOX_V) . $this->c(self::C_BOLD, $titleLine) - . $this->c(self::C_CYAN, self::BOX_V) . "\n"; + . $this->c(self::C_CYAN, self::BOX_V) . "\n"); if ($descLine !== null) { - echo $this->c(self::C_CYAN, self::BOX_V) + $this->display($this->c(self::C_CYAN, self::BOX_V) . $this->c(self::C_DIM, $descLine) - . $this->c(self::C_CYAN, self::BOX_V) . "\n"; + . $this->c(self::C_CYAN, self::BOX_V) . "\n"); } - echo $this->c( + $this->display($this->c( self::C_CYAN, self::BOX_BL . str_repeat(self::BOX_H, $inner) . self::BOX_BR - ) . "\n\n"; + ) . "\n\n"); } /** Print the dry-run notice box. */ @@ -403,18 +423,18 @@ abstract class CliFramework $w = min($this->termWidth(), 70); $msg = ' ' . self::ICON_DRY . ' DRY-RUN MODE — no changes will be written '; $row = $this->padRight($msg, $w - 2); - echo $this->c( + $this->display($this->c( self::C_YELLOW . self::C_BOLD, self::BOX_TL . str_repeat(self::BOX_H, $w - 2) . self::BOX_TR - ) . "\n"; - echo $this->c( + ) . "\n"); + $this->display($this->c( self::C_YELLOW . self::C_BOLD, self::BOX_V . $row . self::BOX_V - ) . "\n"; - echo $this->c( + ) . "\n"); + $this->display($this->c( self::C_YELLOW . self::C_BOLD, self::BOX_BL . str_repeat(self::BOX_H, $w - 2) . self::BOX_BR - ) . "\n\n"; + ) . "\n\n"); } // ========================================================================= @@ -435,11 +455,11 @@ abstract class CliFramework $w = $this->termWidth(); $text = " {$title} "; $fill = max(0, $w - strlen($text) - 4); - echo "\n"; - echo $this->c( + $this->display("\n"); + $this->display($this->c( self::C_CYAN, str_repeat(self::BOX_H, 2) . $text . str_repeat(self::BOX_H, $fill) - ) . "\n\n"; + ) . "\n\n"); } /** Print a plain horizontal divider. */ @@ -449,7 +469,7 @@ abstract class CliFramework return; } $this->clearProgress(); - echo $this->c(self::C_DIM, str_repeat(self::BOX_H, $this->termWidth())) . "\n"; + $this->display($this->c(self::C_DIM, str_repeat(self::BOX_H, $this->termWidth())) . "\n"); } // ========================================================================= @@ -495,11 +515,7 @@ abstract class CliFramework $line = "{$ts} {$icon} {$badge} {$text}\n"; - if ($level === 'ERROR') { - fwrite(STDERR, $line); - } else { - echo $line; - } + $this->display($line); } /** Log a success message. */ @@ -564,7 +580,7 @@ abstract class CliFramework ? ' ' . $this->c(self::C_DIM, "— {$detail}") : ''; - echo ' ' . $this->c($color . self::C_BOLD, $icon) . ' ' . $label . $suffix . "\n"; + $this->display(' ' . $this->c($color . self::C_BOLD, $icon) . ' ' . $label . $suffix . "\n"); } // ========================================================================= @@ -601,10 +617,10 @@ abstract class CliFramework $line = " [{$bar}] {$percent} {$counter}{$suffix}"; if ($newline) { - echo "\r{$line}\n"; + $this->display("\r{$line}\n"); $this->progressActive = false; } else { - echo "\r{$line}"; + $this->display("\r{$line}"); $this->progressActive = true; } } @@ -613,7 +629,7 @@ abstract class CliFramework protected function clearProgress(): void { if ($this->progressActive) { - echo "\r" . str_repeat(' ', $this->termWidth()) . "\r"; + $this->display("\r" . str_repeat(' ', $this->termWidth()) . "\r"); $this->progressActive = false; } } @@ -644,8 +660,8 @@ abstract class CliFramework $maxKey = max(array_map('strlen', array_keys($rows))); $inner = $maxKey + 20; - echo "\n"; - echo $this->c($color, self::BOX_TL . str_repeat(self::BOX_H, $inner) . self::BOX_TR) . "\n"; + $this->display("\n"); + $this->display($this->c($color, self::BOX_TL . str_repeat(self::BOX_H, $inner) . self::BOX_TR) . "\n"); foreach ($rows as $label => $value) { $valStr = (string) $value; @@ -653,10 +669,10 @@ abstract class CliFramework $padding = $inner - strlen($label) - $valVis - 4; $row = ' ' . $this->c(self::C_BOLD, $label) . str_repeat(' ', max(1, $padding)) . $valStr . ' '; - echo $this->c($color, self::BOX_V) . $row . $this->c($color, self::BOX_V) . "\n"; + $this->display($this->c($color, self::BOX_V) . $row . $this->c($color, self::BOX_V) . "\n"); } - echo $this->c($color, self::BOX_BL . str_repeat(self::BOX_H, $inner) . self::BOX_BR) . "\n\n"; + $this->display($this->c($color, self::BOX_BL . str_repeat(self::BOX_H, $inner) . self::BOX_BR) . "\n\n"); } /** @@ -702,7 +718,7 @@ abstract class CliFramework $this->clearProgress(); $badge = $this->c(self::C_BOLD . self::C_MAGENTA, "Step {$current}/{$total}"); $arrow = $this->c(self::C_DIM, self::ICON_INFO); - echo "\n{$badge} {$arrow} {$title}\n"; + $this->display("\n{$badge} {$arrow} {$title}\n"); } // ========================================================================= @@ -964,13 +980,13 @@ abstract class CliFramework } // Header. - echo $sep . "\n"; + $this->display($sep . "\n"); $headerLine = '|'; foreach ($headers as $i => $h) { $headerLine .= ' ' . $this->c(self::C_BOLD, str_pad($h, $widths[$i])) . ' |'; } - echo $headerLine . "\n"; - echo $sep . "\n"; + $this->display($headerLine . "\n"); + $this->display($sep . "\n"); // Rows. foreach ($rows as $row) { @@ -978,9 +994,9 @@ abstract class CliFramework foreach ($row as $i => $cell) { $line .= ' ' . str_pad((string) $cell, $widths[$i]) . ' |'; } - echo $line . "\n"; + $this->display($line . "\n"); } - echo $sep . "\n"; + $this->display($sep . "\n"); } } diff --git a/templates/docs/required/template-update-server-dolibarr.md b/templates/docs/required/template-update-server-dolibarr.md index c29079d..8c185d0 100644 --- a/templates/docs/required/template-update-server-dolibarr.md +++ b/templates/docs/required/template-update-server-dolibarr.md @@ -1,70 +1,56 @@ - - -# Dolibarr Update Server - -[![moko-platform](https://img.shields.io/badge/moko--platform-{{standards_version}}-blue)](https://git.mokoconsulting.tech/MokoConsulting/moko-platform) - -This document explains how `update.txt` is automatically managed for this Dolibarr module. +MokoGitea provides a built-in Update Server that can serve Dolibarr-compatible JSON update feeds from repository releases. **No static feed file is needed in the repository.** ## How It Works -Dolibarr checks for module updates by fetching a plain-text file from the URL in `$this->url_last_version` in the module descriptor (`src/core/modules/mod*.class.php`). The file must contain **only the version string** — no JSON, no XML, no trailing newline. +1. **Enable Update Server** in the repository's Settings > Advanced Settings +2. **Configure metadata** in Settings > Update Server (set platform to `dolibarr`) +3. **Create releases** with tagged module archives +4. MokoGitea serves the update feed at `/{owner}/{repo}/updates/dolibarr.json` -### Automatic Generation - -| Event | Workflow | `update.txt` Content | `$this->version` | -|-------|----------|---------------------|-------------------| -| Merge to `main` | `auto-release.yml` | `XX.YY.ZZ` (real version) | Real version | -| Push to `dev/**` | `deploy-dev.yml` | `development` | `development` | -| Push to `rc/**` | `deploy-dev.yml` | `XX.YY.ZZ-rc` | RC version | - -### Module Descriptor - -The `url_last_version` in your module descriptor should point to: +## Feed URL ``` -https://raw.githubusercontent.com/mokoconsulting-tech/{{REPO_NAME}}/main/update.txt +https://git.mokoconsulting.tech/{owner}/{repo}/updates/dolibarr.json ``` -This is set automatically by `version_set_platform.php` during the build pipeline. **Never manually edit `$this->version` or `$this->url_last_version`** — the workflows handle it. +## Release Naming Convention -### Branch Lifecycle +Release assets should follow: ``` -dev/XX.YY.ZZ → rc/XX.YY.ZZ → main → version/XX -(development) (release candidate) (stable release) (frozen snapshot) +{module_name}-{version}.zip ``` -1. **Development** (`dev/**`): `update.txt` = `development`, `$this->version` = `development` -2. **Release Candidate** (`rc/**`): `update.txt` = `XX.YY.ZZ-rc`, version set to RC -3. **Stable Release** (merge to `main`): `auto-release.yml` writes real version to `update.txt`, creates GitHub Release + tag, creates `version/XX` branch -4. **Frozen Snapshot** (`version/XX`): immutable, never force-pushed +Examples: +- `mokocrm-18.0.1.zip` +- `mokodolisign-3.2.0.zip` -### Health Checks +## Update Server Settings -The `repo_health.yml` workflow verifies on every commit: +Configure these in Settings > Update Server: -- `update.txt` exists in the repository root -- Module descriptor (`mod*.class.php`) exists in `src/core/modules/` -- `$this->numero` is set and non-zero -- `$this->version` is not hardcoded (should be set by workflow) -- `url_last_version` points to `update.txt` (not `update.json`) -- `url_last_version` references `/main/` branch on the main branch +| Field | Description | Example | +|-------|-------------|---------| +| Platform | Set to `dolibarr` | `dolibarr` | +| Extension Name | Dolibarr module directory name | `mokocrm` | +| Display Name | Human-readable name | `Module - MokoCRM` | +| Extension Type | Usually `module` | `module` | +| Maintainer | Organization name | `Moko Consulting` | +| Support URL | Product support page | `https://mokoconsulting.tech/support/mokocrm` | ---- +## Download Gating -*Managed by [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform). See [docs/workflows/update-server.md](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/blob/main/docs/workflows/update-server.md) for the full specification.* +Same three modes as Joomla: `none`, `prerelease`, `all`. + +## Status + +Dolibarr Update Server support is currently **disabled for all modules except MokoCRM**. Metadata is pre-configured and ready to enable when needed. + +## What NOT to Do + +- **Do NOT commit static feed files to the repository** +- **Do NOT use legacy update check mechanisms** — use the built-in feed diff --git a/templates/docs/required/template-update-server-joomla.md b/templates/docs/required/template-update-server-joomla.md index ae66059..d47dc93 100644 --- a/templates/docs/required/template-update-server-joomla.md +++ b/templates/docs/required/template-update-server-joomla.md @@ -1,122 +1,90 @@ - - -# Joomla Update Server - -[![moko-platform](https://img.shields.io/badge/moko--platform-{{standards_version}}-blue)](https://git.mokoconsulting.tech/MokoConsulting/moko-platform) - -This document explains how `updates.xml` is automatically managed for this Joomla extension following the [Joomla Update Server specification](https://docs.joomla.org/Deploying_an_Update_Server). +MokoGitea provides a built-in Update Server that dynamically generates Joomla-compatible update XML feeds from repository releases. **No static `updates.xml` file is needed in the repository.** ## How It Works -Joomla checks for extension updates by fetching an XML file from the URL defined in the `` tag in the extension's XML manifest. moko-platform generates this file automatically. +1. **Enable Update Server** in the repository's Settings > Advanced Settings +2. **Configure metadata** in Settings > Update Server (extension name, type, target version, etc.) +3. **Create releases** with tagged assets (e.g. `pkg_mokowaas-02.19.00.zip`) +4. MokoGitea automatically serves the update feed at `/{owner}/{repo}/updates.xml` -### Automatic Generation +## Feed URL -| Event | Workflow | `` | `` | -|-------|----------|---------|-------------| -| Merge to `main` | `auto-release.yml` | `stable` | `XX.YY.ZZ` | -| Push to `dev/**` | `deploy-dev.yml` | `development` | `development` | -| Push to `rc/**` | `deploy-dev.yml` | `rc` | `XX.YY.ZZ-rc` | +``` +https://git.mokoconsulting.tech/{owner}/{repo}/updates.xml +``` -### Generated XML Structure +This URL is what goes into your Joomla extension's `update_server` element in the manifest XML. + +## Manifest Configuration + +In your extension's manifest XML (`*.xml`), add: ```xml - - - - Extension Name - Extension Name update - com_extensionname - component - 01.02.03 - site - system - - stable - - - https://git.mokoconsulting.tech/.../releases/download/v01.02.03/com_ext-01.02.03.zip - https://github.com/.../releases/download/v01.02.03/com_ext-01.02.03.zip - - - 8.2 - Moko Consulting - https://mokoconsulting.tech - - + + + https://git.mokoconsulting.tech/MokoConsulting/{RepoName}/updates.xml + + ``` -### Metadata Source +## Release Naming Convention -All metadata is extracted from the extension's XML manifest (`src/*.xml`) at build time: - -| XML Element | Source | Notes | -|-------------|--------|-------| -| `` | `` in manifest | Extension display name | -| `` | `` in manifest | Must match installed extension identifier | -| `` | `type` attribute on `` | `component`, `module`, `plugin`, `library`, `package`, `template` | -| `` | `client` attribute on `` | `site` or `administrator` — **required for plugins and modules** | -| `` | `group` attribute on `` | Plugin group (e.g., `system`, `content`) — **required for plugins** | -| `` | `` in manifest | Falls back to Joomla 5.x / 6.x if not specified | -| `` | `` in manifest | Included only if present | - -### Extension Manifest Setup - -Your XML manifest must include an `` tag pointing to the `updates.xml` on the `main` branch: - -```xml - - My Extension - com_myextension - - - - https://git.mokoconsulting.tech/mokoconsulting-tech/{{REPO_NAME}}/raw/branch/main/updates.xml - - - https://raw.githubusercontent.com/mokoconsulting-tech/{{REPO_NAME}}/main/updates.xml - - - -``` - -### Branch Lifecycle +Release assets must follow this naming pattern for the feed generator to detect them: ``` -dev/XX.YY.ZZ → rc/XX.YY.ZZ → main → version/XX -(development) (rc) (stable) (frozen snapshot) +{extension_name}-{version}.zip +{extension_name}-{version}.tar.gz ``` -1. **Development** (`dev/**`): `updates.xml` with `development`, download points to branch archive -2. **Release Candidate** (`rc/**`): `updates.xml` with `rc`, version set to `XX.YY.ZZ-rc` -3. **Stable Release** (merge to `main`): `updates.xml` with `stable`, download points to Gitea Release asset (primary) + GitHub Release asset (mirror) -4. **Frozen Snapshot** (`version/XX`): immutable, never force-pushed +Examples: +- `pkg_mokowaas-02.19.00.zip` +- `tpl_mokoonyx-02.19.00.zip` +- `mod_mokojoomhero-01.05.00.zip` -### Health Checks +## Update Server Settings -The `repo_health.yml` workflow verifies on every commit: +Configure these in Settings > Update Server: -- `updates.xml` exists in the repository root -- XML manifest exists with `` tag -- ``, ``, ``, `` tags present -- Extension `type` attribute is valid -- Language `.ini` files exist -- `index.html` directory listing protection in `src/`, `src/admin/`, `src/site/` +| Field | Description | Example | +|-------|-------------|---------| +| Extension Name | Joomla element name | `pkg_mokowaas` | +| Display Name | Human-readable name | `Package - MokoWaaS` | +| Extension Type | package, plugin, template, module, component | `package` | +| Target Version | Regex for compatible Joomla versions | `(5|6)\..*` | +| PHP Minimum | Minimum PHP version | `8.1` | +| Maintainer | Organization name | `Moko Consulting` | +| Maintainer URL | Organization website | `https://mokoconsulting.tech` | +| Support URL | Product support page | `https://mokoconsulting.tech/products/{alias}` | +| Info URL | Product information page | `https://mokoconsulting.tech/products/{alias}` | ---- +## Download Gating -*Managed by [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform). See [docs/workflows/update-server.md](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/blob/main/docs/workflows/update-server.md) for the full specification.* +Three modes control who can download release assets: + +| Mode | Behavior | +|------|----------| +| `none` | All downloads are public | +| `prerelease` | Pre-release downloads require a license key; stable releases are public | +| `all` | All downloads require a license key | + +The update feed XML is **always public** — only the actual file downloads are gated. + +## What NOT to Do + +- **Do NOT commit `updates.xml` to the repository** — it is served dynamically +- **Do NOT use static update server workflows** — the old CI-generated approach is deprecated +- **Do NOT hardcode version numbers in feed URLs** — the feed auto-detects from releases + +## Changelog Feed + +A changelog XML is also served automatically at: + +``` +https://git.mokoconsulting.tech/{owner}/{repo}/changelog.xml +``` + +This is generated from release notes (markdown body of each release). diff --git a/templates/joomla/updates.xml.template b/templates/joomla/updates.xml.template deleted file mode 100644 index bee07f8..0000000 --- a/templates/joomla/updates.xml.template +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - - - {{EXTENSION_NAME}} - {{EXTENSION_NAME}} development build — unstable. - {{EXTENSION_ELEMENT}} - {{EXTENSION_TYPE}} - {{EXTENSION_FOLDER}} - {{EXTENSION_CLIENT}} - {{VERSION}} - {{DATE}} - https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/releases/tag/development - - https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/releases/download/development/{{EXTENSION_ELEMENT}}-{{VERSION}}-dev.zip - - - development - Moko Consulting - https://mokoconsulting.tech - - 8.1 - - - - - {{EXTENSION_NAME}} - {{EXTENSION_NAME}} alpha build — early testing. - {{EXTENSION_ELEMENT}} - {{EXTENSION_TYPE}} - {{EXTENSION_FOLDER}} - {{EXTENSION_CLIENT}} - {{VERSION}} - {{DATE}} - https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/releases/tag/alpha - - https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/releases/download/alpha/{{EXTENSION_ELEMENT}}-{{VERSION}}-alpha.zip - - - alpha - Moko Consulting - https://mokoconsulting.tech - - 8.1 - - - - - {{EXTENSION_NAME}} - {{EXTENSION_NAME}} beta build — feature complete, stability testing. - {{EXTENSION_ELEMENT}} - {{EXTENSION_TYPE}} - {{EXTENSION_FOLDER}} - {{EXTENSION_CLIENT}} - {{VERSION}} - {{DATE}} - https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/releases/tag/beta - - https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/releases/download/beta/{{EXTENSION_ELEMENT}}-{{VERSION}}-beta.zip - - - beta - Moko Consulting - https://mokoconsulting.tech - - 8.1 - - - - - {{EXTENSION_NAME}} - {{EXTENSION_NAME}} release candidate — testing only. - {{EXTENSION_ELEMENT}} - {{EXTENSION_TYPE}} - {{EXTENSION_FOLDER}} - {{EXTENSION_CLIENT}} - {{VERSION}} - {{DATE}} - https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/releases/tag/release-candidate - - https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/releases/download/release-candidate/{{EXTENSION_ELEMENT}}-{{VERSION}}-rc.zip - - - rc - Moko Consulting - https://mokoconsulting.tech - - 8.1 - - - - - {{EXTENSION_NAME}} - {{EXTENSION_NAME}} — Moko Consulting Joomla extension. - {{EXTENSION_ELEMENT}} - {{EXTENSION_TYPE}} - {{EXTENSION_FOLDER}} - {{EXTENSION_CLIENT}} - {{VERSION}} - {{DATE}} - https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/releases/tag/stable - - https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/releases/download/stable/{{EXTENSION_ELEMENT}}-{{VERSION}}.zip - - - stable - Moko Consulting - https://mokoconsulting.tech - - 8.1 - - - diff --git a/tests/Unit/VersionBumpTest.php b/tests/Unit/VersionBumpTest.php index c3184bb..67146a8 100644 --- a/tests/Unit/VersionBumpTest.php +++ b/tests/Unit/VersionBumpTest.php @@ -63,7 +63,7 @@ class VersionBumpTest extends TestCase { file_put_contents( "{$this->tmpDir}/README.md", - "\nSome content\n" + "\nSome content\n" ); $this->execute(); diff --git a/tests/Unit/VersionReadTest.php b/tests/Unit/VersionReadTest.php index 984bd5e..4cbba42 100644 --- a/tests/Unit/VersionReadTest.php +++ b/tests/Unit/VersionReadTest.php @@ -34,7 +34,7 @@ class VersionReadTest extends TestCase { file_put_contents( "{$this->tmpDir}/README.md", - "# Test\n\n" + "# Test\n\n" ); $this->assertSame('02.03.04', trim($this->runScript())); @@ -68,7 +68,7 @@ class VersionReadTest extends TestCase { file_put_contents( "{$this->tmpDir}/README.md", - "\n" + "\n" ); mkdir("{$this->tmpDir}/src", 0755, true); file_put_contents( diff --git a/validate/check_file_integrity.php b/validate/check_file_integrity.php index 81b936c..3ca36cd 100644 --- a/validate/check_file_integrity.php +++ b/validate/check_file_integrity.php @@ -12,7 +12,7 @@ * INGROUP: MokoPlatform * REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform * PATH: /validate/check_file_integrity.php - * VERSION: 09.23.00 + * VERSION: 09.24.00 * BRIEF: Compare deployed files on a remote server against the local repository to detect drift */