From 7857cb5781be8a9587a0d4c4378254ae988b5d68 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Mon, 13 Jul 2026 09:50:56 +0000 Subject: [PATCH 01/63] chore(release): build 02.29.00 [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- CHANGELOG.md | 16 ++++------------ SECURITY.md | 2 +- source/html/layouts/joomla/module/card.php | 2 +- .../html/layouts/mokoonyx/article-metadata.php | 2 +- source/media/css/a11y-high-contrast.css | 2 +- source/templateDetails.xml | 2 +- 7 files changed, 10 insertions(+), 18 deletions(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 51c0a29..4b08c70 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 02.28.07 +# VERSION: 02.29.00 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/CHANGELOG.md b/CHANGELOG.md index c2e2ae9..8fd20d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,15 @@ DEFGROUP: Joomla.Template.Site INGROUP: MokoOnyx.Documentation PATH: ./CHANGELOG.md - VERSION: 02.28.07 + VERSION: 02.29.00 BRIEF: Changelog file documenting version history of MokoOnyx --> -# Changelog — MokoOnyx (VERSION: 02.28.07) +# Changelog — MokoOnyx (VERSION: 02.29.00) ## [Unreleased] +## [02.29.00] --- 2026-07-13 + ## [02.28.00] --- 2026-07-13 ## [02.27.06] --- 2026-06-29 @@ -22,13 +24,3 @@ ## [02.27.06] --- 2026-06-29 ## [02.27.00] --- 2026-06-21 - -## [02.27.00] --- 2026-06-21 - -### Added -- Collapsible floating social bar with toggle button and localStorage persistence -- Bootstrap tooltips on all social icon links showing platform name on hover -- URL validation on social platform fields (browser, Joomla server-side, and PHP layout logging) - -### Fixed -- Moved footer social icons below footer-menu and footer module positions for correct display order diff --git a/SECURITY.md b/SECURITY.md index dbfccdc..f21ebc1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,7 +10,7 @@ INGROUP: MokoOnyx.Governance REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx FILE: SECURITY.md - VERSION: 02.28.07 + VERSION: 02.29.00 BRIEF: Security policy and vulnerability reporting process for MokoOnyx. PATH: /SECURITY.md NOTE: This policy is process oriented and does not replace secure engineering practices. diff --git a/source/html/layouts/joomla/module/card.php b/source/html/layouts/joomla/module/card.php index 0dc05cc..188f272 100644 --- a/source/html/layouts/joomla/module/card.php +++ b/source/html/layouts/joomla/module/card.php @@ -10,7 +10,7 @@ * INGROUP: MokoOnyx * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx * PATH: /html/layouts/joomla/module/card.php - * VERSION: 02.28.07 + * VERSION: 02.29.00 * BRIEF: Custom card module chrome — renders module titles for all modules */ diff --git a/source/html/layouts/mokoonyx/article-metadata.php b/source/html/layouts/mokoonyx/article-metadata.php index 8a91039..784d7d2 100644 --- a/source/html/layouts/mokoonyx/article-metadata.php +++ b/source/html/layouts/mokoonyx/article-metadata.php @@ -11,7 +11,7 @@ * INGROUP: MokoOnyx.Layouts * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx * PATH: /src/html/layouts/mokoonyx/article-metadata.php - * VERSION: 02.28.07 + * VERSION: 02.29.00 * BRIEF: Article metadata footer layout -- renders jcfields grouped by field group */ diff --git a/source/media/css/a11y-high-contrast.css b/source/media/css/a11y-high-contrast.css index 5885e7b..8a0d312 100644 --- a/source/media/css/a11y-high-contrast.css +++ b/source/media/css/a11y-high-contrast.css @@ -10,7 +10,7 @@ * INGROUP: MokoOnyx.Accessibility * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx * PATH: ./media/css/a11y-high-contrast.css - * VERSION: 02.28.07 + * VERSION: 02.29.00 * BRIEF: High-contrast stylesheet for accessibility toolbar */ diff --git a/source/templateDetails.xml b/source/templateDetails.xml index 49090d1..ce2bf24 100644 --- a/source/templateDetails.xml +++ b/source/templateDetails.xml @@ -35,7 +35,7 @@ mokoonyx - 02.28.07 + 02.29.00 script.php 2026-05-16 Jonathan Miller || Moko Consulting -- 2.52.0 From a21fa8e8afebdc9bee0e57cafd36096f3e3e939f Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 10:11:37 +0000 Subject: [PATCH 02/63] chore: sync issue-branch.yml from Template-Generic [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 4b08c70..eb67f8f 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow # INGROUP: mokocli.Automation -# VERSION: 02.29.00 +# VERSION: 01.00.00 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" -- 2.52.0 From 9a44e5ef20ead356afbf372330ffb4034f677e64 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 15:28:32 +0000 Subject: [PATCH 03/63] chore: sync standards-compliance.yml from Template-Generic [skip ci] --- .mokogitea/workflows/standards-compliance.yml | 2614 +++++++++++++++++ 1 file changed, 2614 insertions(+) create mode 100644 .mokogitea/workflows/standards-compliance.yml diff --git a/.mokogitea/workflows/standards-compliance.yml b/.mokogitea/workflows/standards-compliance.yml new file mode 100644 index 0000000..7048eab --- /dev/null +++ b/.mokogitea/workflows/standards-compliance.yml @@ -0,0 +1,2614 @@ +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Compliance +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /.mokogitea/workflows/standards-compliance.yml +# VERSION: 04.06.00 +# BRIEF: MokoStandards compliance validation workflow +# NOTE: Validates repository structure, documentation, and coding standards + +name: "Generic: Standards Compliance" + +# ╔════════════════════════════════════════════════════════════════════════╗ +# ║ MOKOSTANDARDS COMPLIANCE WORKFLOW ║ +# ╠════════════════════════════════════════════════════════════════════════╣ +# ║ ║ +# ║ 28 checks across 4 priority tiers: ║ +# ║ ║ +# ║ TIER 1 — CRITICAL (must pass) ║ +# ║ secret-scanning, license-compliance, repository-structure, ║ +# ║ coding-standards, version-consistency ║ +# ║ ║ +# ║ TIER 2 — IMPORTANT (should pass) ║ +# ║ workflow-validation, documentation-quality, readme-completeness, ║ +# ║ git-hygiene, script-integrity ║ +# ║ ║ +# ║ TIER 3 — QUALITY (code metrics) ║ +# ║ line-length, file-naming, insecure-patterns, complexity, ║ +# ║ duplication, dead-code ║ +# ║ ║ +# ║ TIER 4 — SUPPLEMENTARY (informational) ║ +# ║ file-size, binary, todo, deps, links, api-docs, accessibility, ║ +# ║ performance, enterprise, health, terraform ║ +# ║ ║ +# ║ File size: warning >15MB, critical >20MB ║ +# ║ Exempt: .mmdb, .woff2, .woff, .ttf, .otf ║ +# ║ ║ +# ╚════════════════════════════════════════════════════════════════════════╝ + +env: + WORKFLOW_VERSION: "04.04.01" + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +# MokoStandards Policy Compliance: +# - File formatting: Enforces organizational coding standards +# - Reference: docs/policy/file-formatting.md + +# ┌─────────────────────────────────────────────────────────────────────────┐ +# │ WORKFLOW FLOW DIAGRAM │ +# └─────────────────────────────────────────────────────────────────────────┘ +# +# TRIGGER: Push/PR to main/dev/rc branches +# │ +# ▼ +# ┌──────────────────────────────────────────────────────────────┐ +# │ PARALLEL VALIDATION CHECKS │ +# └──────────────────────────────────────────────────────────────┘ +# │ +# ├─────────────┬──────────────┬──────────────┬────────────┐ +# ▼ ▼ ▼ ▼ ▼ +# ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ ┌──────────┐ +# │Repository │File Header │Code Style│ │ Docs │ │ License │ +# │Structure│ │ Validation│ │ Check │ │ Check │ │ Check │ +# └─────────┘ └──────────┘ └──────────┘ └─────────┘ └──────────┘ +# │ │ │ │ │ +# ▼ ▼ ▼ ▼ ▼ +# ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ ┌──────────┐ +# │ Check │ │ Verify │ │ Run │ │ Check │ │ Verify │ +# │Required │ │Copyright │ │ Linters │ │README │ │SPDX-ID │ +# │ Dirs │ │ Header │ │(Python, │ │ Exists │ │ Present │ +# │ │ │ Format │ │PHP,YAML) │ │ │ │ │ +# └─────────┘ └──────────┘ └──────────┘ └─────────┘ └──────────┘ +# │ │ │ │ │ +# └─────────────┴──────────────┴──────────────┴────────────┘ +# │ +# ▼ +# ┌──────────────────┐ +# │ All Checks Pass?│ +# └──────────────────┘ +# │ │ +# YES │ │ NO +# ▼ ▼ +# ┌──────────┐ ┌──────────────┐ +# │ SUCCESS │ │ CREATE ISSUE │ +# │ Summary │ │ with Failure │ +# └──────────┘ │ Details │ +# └──────────────┘ + +on: + push: + branches: [main, dev/**, rc/**, version/**] + pull_request: + branches: [main, dev/**, rc/**] + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + # ════════════════════════════════════════════════════════════════════════ + # TIER 1 — CRITICAL (must pass, blocks merge) + # ════════════════════════════════════════════════════════════════════════ + secret-scanning: + name: Secret Scanning + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Scan for Secrets + run: | + set -x + echo "## 🔒 Secret Scanning" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Scanning for hardcoded secrets and credentials." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Define secret patterns + VIOLATIONS=0 + + # Check for common secret patterns + echo "### Secret Patterns" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Helper: scan with a pattern, show results with file:line, return count + scan_pattern() { + local label="$1" icon="$2" tmpfile="$3" + local count=0 + if [ -f "$tmpfile" ]; then + count=$(wc -l < "$tmpfile") + fi + if [ "$count" -gt 0 ]; then + echo "${icon} **${label}**: ${count} finding(s)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View locations" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| File | Line | Match |" >> $GITHUB_STEP_SUMMARY + echo "|------|------|-------|" >> $GITHUB_STEP_SUMMARY + head -20 "$tmpfile" | while IFS= read -r line; do + FILE=$(echo "$line" | cut -d: -f1 | sed 's|^\./||') + LINENO=$(echo "$line" | cut -d: -f2) + MATCH=$(echo "$line" | cut -d: -f3- | head -c 80 | sed 's/|/\\|/g') + echo "| \`${FILE}\` | ${LINENO} | \`${MATCH}\` |" >> $GITHUB_STEP_SUMMARY + done + if [ "$count" -gt 20 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "*... and $((count - 20)) more*" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + VIOLATIONS=$((VIOLATIONS + count)) + fi + } + + # Pattern 1: password/secret assignments + grep -r -n -E "(password|passwd|pwd|secret|api[_-]?key|token).*=.*['\"]" . \ + --include="*.php" --include="*.py" --include="*.js" --include="*.ts" \ + --exclude-dir=".git" --exclude-dir="vendor" --exclude-dir="node_modules" 2>/dev/null | \ + grep -v -E '(test|example|sample|getenv|getString|getArgument|config\[|/\.\*/|^\s*//|^\s*\*|CREDENTIAL_PATTERNS|SecurityValidator|SECRET_PATTERN|===|!==|ApiClient|str_contains|gen_wrappers)' | \ + grep -v "= ''" | grep -v '= ""' | grep -v '\$this->config' | \ + grep -v 'type="password"' | grep -v 'type="text"' | grep -v 'name="password"' | grep -v 'name="secretkey"' | \ + grep -v '/dev/null > /tmp/secrets2.txt || true + scan_pattern "Private keys" "❌" /tmp/secrets2.txt + + # Pattern 3: AWS keys + grep -r -n -E "AKIA[0-9A-Z]{16}" . \ + --include="*.php" --include="*.py" --include="*.js" --include="*.txt" --include="*.env" \ + --exclude-dir=".git" --exclude-dir="vendor" --exclude-dir="node_modules" 2>/dev/null > /tmp/secrets3.txt || true + scan_pattern "AWS access keys" "❌" /tmp/secrets3.txt + + # Pattern 4: GitHub tokens + grep -r -n -E "gh[ps]_[a-zA-Z0-9]{36}" . \ + --include="*.php" --include="*.py" --include="*.js" --include="*.txt" --include="*.env" \ + --exclude-dir=".git" --exclude-dir="vendor" --exclude-dir="node_modules" 2>/dev/null > /tmp/secrets4.txt || true + scan_pattern "GitHub tokens" "❌" /tmp/secrets4.txt + + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$VIOLATIONS" -gt 0 ]; then + echo "**Total Violations**: $VIOLATIONS" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View detected secrets (file paths only)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/secrets*.txt 2>/dev/null | cut -d: -f1 | sort -u >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Action Required**: Remove hardcoded secrets immediately!" >> $GITHUB_STEP_SUMMARY + echo "Use environment variables or secrets management instead." >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "✅ No hardcoded secrets detected" >> $GITHUB_STEP_SUMMARY + fi + + license-compliance: + name: License Header Validation + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check SPDX Headers + run: | + set -x + echo "### SPDX License Header Check" >> $GITHUB_STEP_SUMMARY + + # Count source files with and without SPDX headers + TOTAL_PHP=0 + WITH_SPDX_PHP=0 + + if find . -name "*.php" -type f ! -path "./vendor/*" | head -1 | grep -q .; then + TOTAL_PHP=$(find . -name "*.php" -type f ! -path "./vendor/*" | wc -l) + WITH_SPDX_PHP=$(find . -name "*.php" -type f ! -path "./vendor/*" -exec grep -l "SPDX-License-Identifier" {} \; | wc -l) + fi + + if [ "$TOTAL_PHP" -gt 0 ]; then + PERCENT=$((WITH_SPDX_PHP * 100 / TOTAL_PHP)) + echo "- PHP files: $WITH_SPDX_PHP/$TOTAL_PHP ($PERCENT%) with SPDX headers" >> $GITHUB_STEP_SUMMARY + + if [ "$PERCENT" -lt 80 ]; then + echo "⚠️ Less than 80% of PHP files have SPDX headers" >> $GITHUB_STEP_SUMMARY + else + echo "✅ Good SPDX header coverage" >> $GITHUB_STEP_SUMMARY + fi + fi + + - name: Validate License File + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### License File Validation" >> $GITHUB_STEP_SUMMARY + + if [ ! -f "LICENSE" ]; then + echo "❌ LICENSE file not found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ❌ Validation Failed: LICENSE File Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** LICENSE file is required for all MokoStandards-compliant repositories" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Add LICENSE file with appropriate open-source license (GPL-3.0-or-later recommended)" >> $GITHUB_STEP_SUMMARY + echo "" + echo "❌ ERROR: LICENSE file not found - This is a critical requirement" + exit 1 + fi + + # Check license type + if grep -qi "GNU GENERAL PUBLIC LICENSE" LICENSE; then + VERSION=$(grep -i "Version 3" LICENSE || echo "") + if [ -n "$VERSION" ]; then + echo "✅ GPL-3.0-or-later license detected" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ GPL license detected but version unclear" >> $GITHUB_STEP_SUMMARY + fi + elif grep -qi "MIT License" LICENSE; then + echo "✅ MIT license detected" >> $GITHUB_STEP_SUMMARY + elif grep -qi "Apache License" LICENSE; then + echo "✅ Apache license detected" >> $GITHUB_STEP_SUMMARY + else + echo "ℹ️ License type could not be automatically detected" >> $GITHUB_STEP_SUMMARY + fi + + repository-structure: + name: Repository Structure Validation + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Required Directories + run: | + set -x + echo "## 📁 Repository Structure Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + MISSING=0 + PRESENT=0 + TOTAL=2 + + echo "### Required Directories" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Directory | Status | Files | Size | Notes |" >> $GITHUB_STEP_SUMMARY + echo "|-----------|--------|-------|------|-------|" >> $GITHUB_STEP_SUMMARY + + # Check required directories + for dir in docs .github; do + if [ -d "$dir" ]; then + FILE_COUNT=$(find "$dir" -type f 2>/dev/null | wc -l) + DIR_SIZE=$(du -sh "$dir" 2>/dev/null | cut -f1) + echo "| $dir/ | ✅ Pass | $FILE_COUNT files | $DIR_SIZE | Complete |" >> $GITHUB_STEP_SUMMARY + PRESENT=$((PRESENT + 1)) + else + echo "| $dir/ | ❌ **Missing** | - | - | **Action Required** |" >> $GITHUB_STEP_SUMMARY + MISSING=$((MISSING + 1)) + fi + done + + echo "" >> $GITHUB_STEP_SUMMARY + PERCENT=$((PRESENT * 100 / TOTAL)) + echo "**Compliance Score:** $PERCENT% ($PRESENT/$TOTAL directories present)" >> $GITHUB_STEP_SUMMARY + + if [ "$MISSING" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 🔴 Critical Issues: $MISSING" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Remediation Steps:**" >> $GITHUB_STEP_SUMMARY + [ ! -d "docs" ] && echo "- Create docs directory: \`mkdir docs && echo '# Documentation' > docs/README.md\`" >> $GITHUB_STEP_SUMMARY + [ ! -d ".github" ] && echo "- Create .github directory: \`mkdir -p .github/workflows\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "📚 Reference: [MokoStandards Repository Structure](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/core-structure.md)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ❌ Validation Failed: Required Directories Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Status:** Repository structure does not meet MokoStandards requirements" >> $GITHUB_STEP_SUMMARY + echo "**Missing:** $MISSING required director(y|ies)" >> $GITHUB_STEP_SUMMARY + echo "**Compliance:** $PERCENT% ($PRESENT/$TOTAL directories present)" >> $GITHUB_STEP_SUMMARY + echo "" + echo "❌ ERROR: Required directories missing - See job summary for remediation steps" + exit 1 + fi + + - name: Check Required Files + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Required Files" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + MISSING=0 + PRESENT=0 + TOTAL=5 + + echo "| File | Status | Size | Last Modified | Notes |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|------|---------------|-------|" >> $GITHUB_STEP_SUMMARY + + # Check required files (CHANGELOG handled separately via find -iname to support src/ChangeLog.md) + for file in README.md LICENSE CONTRIBUTING.md SECURITY.md .editorconfig; do + if [ -f "$file" ]; then + FILE_SIZE=$(wc -c < "$file" 2>/dev/null | awk '{printf "%.1f KB", $1/1024}') + LAST_MOD=$(stat -c %y "$file" 2>/dev/null | cut -d' ' -f1 || echo "Unknown") + CONTENT_CHECK="" + + # Basic content validation + case "$file" in + "README.md") + LINES=$(wc -l < "$file") + [ "$LINES" -lt 10 ] && CONTENT_CHECK="⚠️ Too short" + ;; + "LICENSE") + [ $(wc -c < "$file") -lt 100 ] && CONTENT_CHECK="⚠️ Incomplete?" + ;; + esac + + echo "| $file | ✅ Pass | $FILE_SIZE | $LAST_MOD | Complete $CONTENT_CHECK |" >> $GITHUB_STEP_SUMMARY + PRESENT=$((PRESENT + 1)) + else + echo "| $file | ❌ **Missing** | - | - | **Required** |" >> $GITHUB_STEP_SUMMARY + MISSING=$((MISSING + 1)) + fi + done + + echo "" >> $GITHUB_STEP_SUMMARY + PERCENT=$((PRESENT * 100 / TOTAL)) + echo "**Compliance Score:** $PERCENT% ($PRESENT/$TOTAL files present)" >> $GITHUB_STEP_SUMMARY + + if [ "$MISSING" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 🔴 Critical Issues: $MISSING" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Remediation Steps:**" >> $GITHUB_STEP_SUMMARY + [ ! -f "README.md" ] && echo "- Create README.md: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/docs/required/README.md)" >> $GITHUB_STEP_SUMMARY + [ ! -f "LICENSE" ] && echo "- Add LICENSE file: Choose from [OSI-approved licenses](https://opensource.org/licenses)" >> $GITHUB_STEP_SUMMARY + [ ! -f "CONTRIBUTING.md" ] && echo "- Create CONTRIBUTING.md: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/docs/required/CONTRIBUTING.md)" >> $GITHUB_STEP_SUMMARY + [ ! -f "SECURITY.md" ] && echo "- Create SECURITY.md: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/docs/required/SECURITY.md)" >> $GITHUB_STEP_SUMMARY + [ ! -f ".editorconfig" ] && echo "- Add .editorconfig: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/.editorconfig)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "📚 Reference: [MokoStandards File Requirements](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/file-header-standards.md)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ❌ Validation Failed: Required Files Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Status:** Repository files do not meet MokoStandards requirements" >> $GITHUB_STEP_SUMMARY + echo "**Missing:** $MISSING required file(s)" >> $GITHUB_STEP_SUMMARY + echo "**Compliance:** $PERCENT% ($PRESENT/$TOTAL files present)" >> $GITHUB_STEP_SUMMARY + echo "" + echo "❌ ERROR: Required files missing - See job summary for remediation steps" + exit 1 + fi + + coding-standards: + name: Coding Standards Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check for Tab Characters + run: | + set -x + echo "### Tab Character Detection" >> $GITHUB_STEP_SUMMARY + + # Policy: Tabs are DEFAULT. Only check for tabs in files that REQUIRE spaces. + # Languages requiring spaces: YAML, Python, Haskell, F#, CoffeeScript, Nim, JSON, RST + TABS_IN_SPACES_FILES=$(find . -type f \ + \( -name "*.yml" -o -name "*.yaml" \ + -o -name "*.py" \ + -o -name "*.hs" -o -name "*.lhs" \ + -o -name "*.fs" -o -name "*.fsx" -o -name "*.fsi" \ + -o -name "*.coffee" -o -name "*.litcoffee" \ + -o -name "*.nim" -o -name "*.nims" -o -name "*.nimble" \ + -o -name "*.json" \ + -o -name "*.rst" \) \ + ! -path "./vendor/*" \ + ! -path "./node_modules/*" \ + ! -path "./.git/*" \ + -exec grep -l $'\t' {} \; 2>/dev/null | head -10) + + if [ -n "$TABS_IN_SPACES_FILES" ]; then + echo "⚠️ Tab characters found in files that require spaces:" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$TABS_IN_SPACES_FILES" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "These languages require spaces (tabs will break): YAML, Python, Haskell, F#, CoffeeScript, Nim, JSON, RST" >> $GITHUB_STEP_SUMMARY + echo "All other files (including .md, .ps1, LICENSE, etc.) may use tabs per MokoStandards policy" >> $GITHUB_STEP_SUMMARY + else + echo "✅ No tabs found in files requiring spaces" >> $GITHUB_STEP_SUMMARY + echo "Note: Tabs are allowed in most files (policy default). Only checked files requiring spaces." >> $GITHUB_STEP_SUMMARY + fi + + - name: Check File Encoding + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### File Encoding Check" >> $GITHUB_STEP_SUMMARY + + # Check for UTF-8 encoding (ASCII is a subset of UTF-8 and is acceptable) + NON_UTF8=$(find . -type f \( -name "*.php" -o -name "*.js" -o -name "*.md" \) \ + ! -path "./vendor/*" \ + ! -path "./node_modules/*" \ + ! -path "./.git/*" \ + -exec file {} \; | grep -v "UTF-8" | grep -v "ASCII" | head -5 || true) + + if [ -n "$NON_UTF8" ]; then + echo "⚠️ Non-UTF-8 files detected:" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$NON_UTF8" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + else + echo "✅ All source files appear to be UTF-8 encoded" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check Line Endings + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Line Ending Check" >> $GITHUB_STEP_SUMMARY + + # Check for CRLF line endings + CRLF_FILES=$(find . -type f \( -name "*.php" -o -name "*.js" -o -name "*.md" \) \ + ! -path "./vendor/*" \ + ! -path "./node_modules/*" \ + ! -path "./.git/*" \ + -exec file {} \; | grep "CRLF" | head -5 || true) + + if [ -n "$CRLF_FILES" ]; then + echo "⚠️ Files with CRLF line endings found:" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$CRLF_FILES" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "MokoStandards requires LF line endings" >> $GITHUB_STEP_SUMMARY + else + echo "✅ Line endings are consistent (LF)" >> $GITHUB_STEP_SUMMARY + fi + + version-consistency: + name: Version Consistency Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + extensions: json + tools: composer + coverage: none + + - name: Setup MokoStandards tools + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards 2>/dev/null || true + if [ -d "/tmp/mokostandards" ] && [ -f "/tmp/mokostandards/composer.json" ]; then + cd /tmp/mokostandards + composer install --no-dev --no-interaction --quiet 2>/dev/null || true + fi + + - name: Run Version Consistency Check + id: version_check + run: | + set -x + echo "## 🔢 Version Consistency Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Use MokoStandards tools (no Composer needed on the governed repo) + if [ -f "/tmp/mokostandards/api/validate/check_version_consistency.php" ]; then + php /tmp/mokostandards/api/validate/check_version_consistency.php --path . --verbose 2>&1 | tee /tmp/version-check.log + EXIT_CODE=${PIPESTATUS[0]} + elif [ -f "api/validate/check_version_consistency.php" ]; then + php api/validate/check_version_consistency.php --path . --verbose 2>&1 | tee /tmp/version-check.log + EXIT_CODE=${PIPESTATUS[0]} + else + echo "⏭️ MokoStandards tools not available — skipping version check" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + echo '```' >> $GITHUB_STEP_SUMMARY + cat /tmp/version-check.log >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + if [ "$EXIT_CODE" -eq 0 ]; then + echo "✅ All version numbers are consistent" >> $GITHUB_STEP_SUMMARY + else + echo "❌ Version drift detected" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + + # ════════════════════════════════════════════════════════════════════════ + # TIER 2 — IMPORTANT (should pass) + # ════════════════════════════════════════════════════════════════════════ + workflow-validation: + name: Workflow Configuration Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Required Workflows + run: | + set -x + echo "### GitHub Actions Workflows" >> $GITHUB_STEP_SUMMARY + + WORKFLOWS_DIR=".github/workflows" + + if [ ! -d "$WORKFLOWS_DIR" ]; then + echo "❌ No workflows directory found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ❌ Validation Failed: Workflows Directory Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** .github/workflows directory is required for CI/CD automation" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Create .github/workflows directory and add GitHub Actions workflows" >> $GITHUB_STEP_SUMMARY + echo "" + echo "❌ ERROR: .github/workflows directory not found" + exit 1 + fi + + # Check for recommended workflows + CI_FOUND=false + for wf in ci.yml build.yml ci-dolibarr.yml ci-joomla.yml; do + if [ -f "$WORKFLOWS_DIR/$wf" ]; then + echo "✅ CI workflow present ($wf)" >> $GITHUB_STEP_SUMMARY + CI_FOUND=true + break + fi + done + if [ "$CI_FOUND" = "false" ]; then + echo "⚠️ No CI workflow found (ci.yml, build.yml, ci-dolibarr.yml, or ci-joomla.yml)" >> $GITHUB_STEP_SUMMARY + fi + + if [ -f "$WORKFLOWS_DIR/codeql-analysis.yml" ]; then + echo "✅ CodeQL security scanning present" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ CodeQL workflow not found" >> $GITHUB_STEP_SUMMARY + fi + + # Check for MokoStandards-synced workflows + for wf in deploy-dev.yml deploy-demo.yml deploy-rs.yml sync-version-on-merge.yml auto-release.yml standards-compliance.yml enterprise-firewall-setup.yml; do + if [ -f "$WORKFLOWS_DIR/$wf" ]; then + echo "✅ ${wf}" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ ${wf} not found (synced from MokoStandards)" >> $GITHUB_STEP_SUMMARY + fi + done + + - name: Validate Workflow Syntax + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Workflow YAML Syntax" >> $GITHUB_STEP_SUMMARY + + INVALID=0 + for workflow in $(find .github/workflows -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null); do + if [ -f "$workflow" ]; then + if python3 -c "import yaml, sys; yaml.safe_load(open(sys.argv[1]))" "$workflow" 2>/dev/null; then + echo "✅ $(basename $workflow)" >> $GITHUB_STEP_SUMMARY + else + echo "❌ $(basename $workflow) - invalid YAML" >> $GITHUB_STEP_SUMMARY + INVALID=$((INVALID + 1)) + fi + fi + done + + if [ "$INVALID" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ❌ Validation Failed: Invalid Workflow YAML Syntax" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** $INVALID workflow file(s) have invalid YAML syntax" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Fix YAML syntax errors in the marked workflow files" >> $GITHUB_STEP_SUMMARY + echo "**Tool:** Run \`python3 -c \"import yaml; yaml.safe_load(open('.github/workflows/FILE.yml'))\"\` locally" >> $GITHUB_STEP_SUMMARY + echo "" + echo "❌ ERROR: $INVALID workflow file(s) with invalid YAML syntax" + exit 1 + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ✅ All Workflow Files Have Valid YAML Syntax" >> $GITHUB_STEP_SUMMARY + echo "" + echo "✅ SUCCESS: All workflow files passed YAML validation" + + - name: Validate CodeQL Configuration + if: hashFiles('.github/workflows/codeql-analysis.yml') != '' + run: | + set -e + echo "" >> $GITHUB_STEP_SUMMARY + echo "### CodeQL Language Configuration" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Inline validation (rewritten from Python to bash for PHP-only architecture) + CODEQL_FILE=".github/workflows/codeql-analysis.yml" + + if [ ! -f "$CODEQL_FILE" ]; then + echo "⚠️ CodeQL workflow file not found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ⚠️ CodeQL Workflow Not Found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Status:** CodeQL workflow file not present - skipping language validation" >> $GITHUB_STEP_SUMMARY + echo "" + echo "⚠️ INFO: CodeQL workflow not found - Skipping validation" + exit 0 + fi + + echo "**CodeQL Configuration Analysis**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Extract configured languages from workflow + LANGUAGES=$(grep -A5 "language:" "$CODEQL_FILE" | grep -oP "(?<=')[^']+(?=')" | tr '\n' ' ' || echo "") + + # Check if this is a configuration-only scan (no languages specified) + if grep -q "category.*language:config" "$CODEQL_FILE"; then + echo "**Scan Type:** Configuration-only (no language matrix)" >> $GITHUB_STEP_SUMMARY + echo "**Status:** ✅ Valid configuration for PHP-only repository" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "This CodeQL workflow scans YAML, JSON, shell scripts for security issues." >> $GITHUB_STEP_SUMMARY + echo "PHP security is handled by SecurityValidator enterprise library." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ SUCCESS: CodeQL configuration-only scan properly configured" + exit 0 + fi + + if [ -z "$LANGUAGES" ]; then + echo "❌ No languages configured in CodeQL workflow" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ❌ Validation Failed: CodeQL Languages Not Configured" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** CodeQL workflow exists but has no languages configured" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Configure appropriate languages in codeql-analysis.yml" >> $GITHUB_STEP_SUMMARY + echo "" + echo "❌ ERROR: No languages configured in CodeQL workflow" + exit 1 + fi + + echo "**Configured Languages:** $LANGUAGES" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Validate language presence in repository + INVALID_LANGS="" + VALID_LANGS="" + + for LANG in $LANGUAGES; do + case "$LANG" in + python) + # Check for Python files (should be none in v04.00.04) + if find . -name "*.py" -type f ! -path "./.git/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS python" + echo "✅ Python: Found Python files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS python" + echo "❌ Python: No Python files found (PHP-only repository)" >> $GITHUB_STEP_SUMMARY + fi + ;; + javascript|typescript) + # Check for JS/TS files + if find . \( -name "*.js" -o -name "*.ts" -o -name "*.json" \) -type f ! -path "./.git/*" ! -path "./node_modules/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS $LANG" + echo "✅ $LANG: Found JavaScript/TypeScript/JSON files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS $LANG" + echo "⚠️ $LANG: No JavaScript/TypeScript files found" >> $GITHUB_STEP_SUMMARY + fi + ;; + java) + if find . -name "*.java" -type f ! -path "./.git/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS java" + echo "✅ Java: Found Java files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS java" + echo "⚠️ Java: No Java files found" >> $GITHUB_STEP_SUMMARY + fi + ;; + go) + if find . -name "*.go" -type f ! -path "./.git/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS go" + echo "✅ Go: Found Go files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS go" + echo "⚠️ Go: No Go files found" >> $GITHUB_STEP_SUMMARY + fi + ;; + cpp|c) + if find . \( -name "*.cpp" -o -name "*.c" -o -name "*.h" \) -type f ! -path "./.git/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS $LANG" + echo "✅ $LANG: Found C/C++ files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS $LANG" + echo "⚠️ $LANG: No C/C++ files found" >> $GITHUB_STEP_SUMMARY + fi + ;; + ruby) + if find . -name "*.rb" -type f ! -path "./.git/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS ruby" + echo "✅ Ruby: Found Ruby files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS ruby" + echo "⚠️ Ruby: No Ruby files found" >> $GITHUB_STEP_SUMMARY + fi + ;; + *) + echo "⚠️ $LANG: Unknown language, skipping validation" >> $GITHUB_STEP_SUMMARY + ;; + esac + done + + echo "" >> $GITHUB_STEP_SUMMARY + + # Report results + if [ -n "$INVALID_LANGS" ]; then + echo "**⚠️ Warning:** Some configured languages may not have corresponding files:" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "Invalid languages: $INVALID_LANGS" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note:** This is informational. CodeQL will skip languages without source files." >> $GITHUB_STEP_SUMMARY + echo "For PHP repository (v04.00.04), JavaScript language covers JSON/YAML/shell scripts." >> $GITHUB_STEP_SUMMARY + else + echo "✅ **All configured CodeQL languages have corresponding source files**" >> $GITHUB_STEP_SUMMARY + fi + + # Always succeed - this is informational only + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ✅ CodeQL Configuration Validation Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Status:** CodeQL language configuration reviewed successfully" >> $GITHUB_STEP_SUMMARY + echo "" + echo "✅ SUCCESS: CodeQL validation complete" + exit 0 + + documentation-quality: + name: Documentation Quality Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Validate README.md + run: | + set -x + echo "## 📚 Documentation Quality Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### README.md Analysis" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ ! -f "README.md" ]; then + echo "❌ **Critical:** README.md not found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ❌ Validation Failed: README.md Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** README.md is required for all MokoStandards-compliant repositories" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Create README.md with project description, setup instructions, and usage examples" >> $GITHUB_STEP_SUMMARY + echo "" + echo "❌ ERROR: README.md not found - This is a critical requirement" + exit 1 + fi + + # Detailed content analysis + SIZE=$(wc -c < README.md) + LINES=$(wc -l < README.md) + WORDS=$(wc -w < README.md) + HEADINGS=$(grep -c "^#" README.md || echo 0) + LINKS=$(grep -c "\[.*\](.*)" README.md || echo 0) + CODE_BLOCKS=$(grep -c '```' README.md || echo 0) + + echo "| Metric | Value | Status | Recommendation |" >> $GITHUB_STEP_SUMMARY + echo "|--------|-------|--------|----------------|" >> $GITHUB_STEP_SUMMARY + + # Size check + SIZE_STATUS="✅ Good" + SIZE_REC="Adequate length" + if [ "$SIZE" -lt 500 ]; then + SIZE_STATUS="⚠️ Warning" + SIZE_REC="Add more content (min 500 bytes)" + elif [ "$SIZE" -gt 50000 ]; then + SIZE_STATUS="⚠️ Warning" + SIZE_REC="Consider splitting into multiple docs" + fi + echo "| Size | $SIZE bytes | $SIZE_STATUS | $SIZE_REC |" >> $GITHUB_STEP_SUMMARY + + # Line count + LINES_STATUS="✅ Good" + LINES_REC="Good size" + if [ "$LINES" -lt 20 ]; then + LINES_STATUS="⚠️ Warning" + LINES_REC="Add more sections (min 20 lines)" + fi + echo "| Lines | $LINES | $LINES_STATUS | $LINES_REC |" >> $GITHUB_STEP_SUMMARY + + # Word count + WORDS_STATUS="✅ Good" + WORDS_REC="Good detail" + if [ "$WORDS" -lt 100 ]; then + WORDS_STATUS="⚠️ Warning" + WORDS_REC="Add more description (min 100 words)" + fi + echo "| Words | $WORDS | $WORDS_STATUS | $WORDS_REC |" >> $GITHUB_STEP_SUMMARY + + # Headings + HEADINGS_STATUS="✅ Good" + HEADINGS_REC="Well structured" + if [ "$HEADINGS" -lt 3 ]; then + HEADINGS_STATUS="⚠️ Warning" + HEADINGS_REC="Add more sections (min 3 headings)" + fi + echo "| Headings | $HEADINGS | $HEADINGS_STATUS | $HEADINGS_REC |" >> $GITHUB_STEP_SUMMARY + + # Links + LINKS_STATUS="✅ Good" + LINKS_REC="Includes references" + if [ "$LINKS" -lt 1 ]; then + LINKS_STATUS="ℹ️ Info" + LINKS_REC="Consider adding useful links" + fi + echo "| Links | $LINKS | $LINKS_STATUS | $LINKS_REC |" >> $GITHUB_STEP_SUMMARY + + # Code blocks + CODE_STATUS="✅ Good" + CODE_REC="Includes examples" + if [ "$CODE_BLOCKS" -eq 0 ]; then + CODE_STATUS="ℹ️ Info" + CODE_REC="Consider adding code examples" + fi + echo "| Code blocks | $CODE_BLOCKS | $CODE_STATUS | $CODE_REC |" >> $GITHUB_STEP_SUMMARY + + echo "" >> $GITHUB_STEP_SUMMARY + + # Check for key sections + echo "**Section Coverage:**" >> $GITHUB_STEP_SUMMARY + MISSING_COUNT=0 + grep -qi "install\|setup\|getting started" README.md && echo "- ✅ Installation/Setup instructions" >> $GITHUB_STEP_SUMMARY || { echo "- ⚠️ Missing: Installation/Setup" >> $GITHUB_STEP_SUMMARY; MISSING_COUNT=$((MISSING_COUNT + 1)); } + grep -qi "usage\|example\|how to" README.md && echo "- ✅ Usage examples" >> $GITHUB_STEP_SUMMARY || { echo "- ⚠️ Missing: Usage examples" >> $GITHUB_STEP_SUMMARY; MISSING_COUNT=$((MISSING_COUNT + 1)); } + grep -qi "license" README.md && echo "- ✅ License information" >> $GITHUB_STEP_SUMMARY || { echo "- ⚠️ Missing: License information" >> $GITHUB_STEP_SUMMARY; MISSING_COUNT=$((MISSING_COUNT + 1)); } + grep -qi "contribut" README.md && echo "- ✅ Contributing guidelines" >> $GITHUB_STEP_SUMMARY || echo "- ℹ️ Optional: Contributing section" >> $GITHUB_STEP_SUMMARY + + if [ "$MISSING_COUNT" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**⚠️ $MISSING_COUNT important sections missing**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Validate CHANGELOG.md + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### CHANGELOG.md Analysis" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Locate changelog case-insensitively; accepted at root, src/, or docs/ + CHANGELOG_PATH=$(find . -maxdepth 3 \( -path ./.git -o -path ./node_modules \) -prune \ + -o -iname "changelog.md" -print | head -1 | sed 's|^\./||') + + if [ -z "$CHANGELOG_PATH" ]; then + echo "❌ **Critical:** CHANGELOG.md not found (checked root, src/, docs/)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ❌ Validation Failed: CHANGELOG.md Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** CHANGELOG.md is required for all MokoStandards-compliant repositories" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Create CHANGELOG.md following [Keep a Changelog](https://keepachangelog.com/) format" >> $GITHUB_STEP_SUMMARY + echo "" + echo "❌ ERROR: CHANGELOG.md not found - This is a critical requirement" + exit 1 + fi + + echo "📄 Found: $CHANGELOG_PATH" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Analyze changelog structure + VERSIONS=$(grep -c "## \[" "$CHANGELOG_PATH" || echo 0) + UNRELEASED=$(grep -c "## \[Unreleased\]" "$CHANGELOG_PATH" || echo 0) + DATES=$(grep -c "[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}" "$CHANGELOG_PATH" || echo 0) + SIZE=$(wc -c < "$CHANGELOG_PATH") + + echo "| Metric | Value | Status | Notes |" >> $GITHUB_STEP_SUMMARY + echo "|--------|-------|--------|-------|" >> $GITHUB_STEP_SUMMARY + + # Check format + if grep -qi "## \[.*\]" "$CHANGELOG_PATH"; then + echo "| Format | Keep a Changelog | ✅ Pass | Standard format |" >> $GITHUB_STEP_SUMMARY + else + echo "| Format | Custom | ⚠️ Warning | Consider [Keep a Changelog](https://keepachangelog.com/) |" >> $GITHUB_STEP_SUMMARY + fi + + # Version count + VERSIONS_STATUS="✅ Good" + VERSIONS_NOTE="Well maintained" + if [ "$VERSIONS" -lt 1 ]; then + VERSIONS_STATUS="⚠️ Warning" + VERSIONS_NOTE="Add version entries" + fi + echo "| Versions | $VERSIONS | $VERSIONS_STATUS | $VERSIONS_NOTE |" >> $GITHUB_STEP_SUMMARY + + # Unreleased section + if [ "$UNRELEASED" -gt 0 ]; then + echo "| Unreleased | Yes | ✅ Good | Active development tracked |" >> $GITHUB_STEP_SUMMARY + else + echo "| Unreleased | No | ℹ️ Info | Consider adding [Unreleased] section |" >> $GITHUB_STEP_SUMMARY + fi + + # Dates + DATES_STATUS="✅ Good" + if [ "$DATES" -lt 1 ]; then + DATES_STATUS="⚠️ Warning" + DATES_NOTE="Add release dates" + else + DATES_NOTE="Dates present" + fi + echo "| Release dates | $DATES | $DATES_STATUS | $DATES_NOTE |" >> $GITHUB_STEP_SUMMARY + + # Check for standard sections + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Changelog Sections:**" >> $GITHUB_STEP_SUMMARY + grep -qi "### Added" "$CHANGELOG_PATH" && echo "- ✅ Added section" >> $GITHUB_STEP_SUMMARY || echo "- ℹ️ Added section (optional)" >> $GITHUB_STEP_SUMMARY + grep -qi "### Changed" "$CHANGELOG_PATH" && echo "- ✅ Changed section" >> $GITHUB_STEP_SUMMARY || echo "- ℹ️ Changed section (optional)" >> $GITHUB_STEP_SUMMARY + grep -qi "### Fixed" "$CHANGELOG_PATH" && echo "- ✅ Fixed section" >> $GITHUB_STEP_SUMMARY || echo "- ℹ️ Fixed section (optional)" >> $GITHUB_STEP_SUMMARY + + echo "" >> $GITHUB_STEP_SUMMARY + echo "📚 Reference: [Keep a Changelog](https://keepachangelog.com/)" >> $GITHUB_STEP_SUMMARY + + - name: Check Documentation Index + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Documentation Index" >> $GITHUB_STEP_SUMMARY + + if [ -f "docs/index.md" ] || [ -f "docs/README.md" ]; then + echo "✅ Documentation index found" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ No documentation index (docs/index.md or docs/README.md)" >> $GITHUB_STEP_SUMMARY + fi + + readme-completeness: + name: README Completeness Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check README Sections + run: | + set -x + echo "## 📄 README Completeness Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ ! -f "README.md" ]; then + echo "❌ README.md not found" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # Required sections + REQUIRED_SECTIONS=("Installation" "Usage" "Contributing" "License") + MISSING=0 + PRESENT=0 + + echo "### Required Sections" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + for section in "${REQUIRED_SECTIONS[@]}"; do + if grep -qi "##.*$section" README.md; then + echo "✅ $section" >> $GITHUB_STEP_SUMMARY + PRESENT=$((PRESENT + 1)) + else + echo "❌ $section" >> $GITHUB_STEP_SUMMARY + MISSING=$((MISSING + 1)) + fi + done + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Completeness**: $PRESENT/${#REQUIRED_SECTIONS[@]} required sections present" >> $GITHUB_STEP_SUMMARY + + if [ "$MISSING" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Action Required**: Add missing sections to README.md" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # ============================================================================ + # PHASE 3: Future Enhancements + # ============================================================================ + + git-hygiene: + name: Git Repository Hygiene + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Check .gitignore + run: | + set -x + echo "### .gitignore Validation" >> $GITHUB_STEP_SUMMARY + + if [ ! -f ".gitignore" ]; then + echo "⚠️ .gitignore file not found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ⚠️ Warning: .gitignore Not Found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Status:** .gitignore file is recommended but not required" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation:** Add .gitignore to exclude build artifacts, dependencies, and temporary files" >> $GITHUB_STEP_SUMMARY + echo "" + echo "⚠️ WARNING: .gitignore file not found - Continuing validation" + exit 0 + fi + + # Check for common exclusions + MISSING="" + grep -q "vendor/" .gitignore || MISSING="${MISSING}vendor/ " + grep -q "node_modules/" .gitignore || MISSING="${MISSING}node_modules/ " + + if [ -n "$MISSING" ]; then + echo "⚠️ .gitignore may be missing common exclusions: $MISSING" >> $GITHUB_STEP_SUMMARY + else + echo "✅ .gitignore appears complete" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check for Large Files + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Large File Detection" >> $GITHUB_STEP_SUMMARY + + # Find files larger than 1MB + LARGE_FILES=$(find . -type f -size +1M ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" | head -5) + + if [ -n "$LARGE_FILES" ]; then + echo "⚠️ Large files detected (>1MB):" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$LARGE_FILES" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "Consider using Git LFS for large binary files" >> $GITHUB_STEP_SUMMARY + else + echo "✅ No unusually large files detected" >> $GITHUB_STEP_SUMMARY + fi + + script-integrity: + name: Script Integrity Validation + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.x' + + - name: Validate Script Integrity + id: script_check + run: | + set -x + echo "## 🔐 Script Integrity Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ -f "api/.script-registry.json" ]; then + echo "### Critical Scripts" >> $GITHUB_STEP_SUMMARY + php api/maintenance/update_sha_hashes.php \ + --dry-run --verbose | tee /tmp/script-validation.log + + EXIT_CODE=$? + + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/script-validation.log >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + + if [ "$EXIT_CODE" -eq 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ All critical scripts validated successfully!" >> $GITHUB_STEP_SUMMARY + exit 0 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "❌ Script integrity violations detected" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Review validation report and update registry" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + else + echo "ℹ️ Script registry not found - skipping integrity check" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + + # ════════════════════════════════════════════════════════════════════════ + # TIER 3 — QUALITY (code quality metrics) + # ════════════════════════════════════════════════════════════════════════ + line-length-validation: + name: Line Length Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Line Lengths + run: | + set -x + echo "## 📏 Line Length Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Line length standards: + # - General source code: 120 characters (hard limit) + # - YAML workflows: 180 characters (exception for GitHub Actions) + # - Markdown files: No limit (content-focused) + + echo "### Line Length Standards" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| File Type | Soft Limit | Hard Limit |" >> $GITHUB_STEP_SUMMARY + echo "|-----------|------------|------------|" >> $GITHUB_STEP_SUMMARY + echo "| General source code | 80 chars | 120 chars |" >> $GITHUB_STEP_SUMMARY + echo "| YAML workflows | 80 chars | 180 chars |" >> $GITHUB_STEP_SUMMARY + echo "| Markdown files | N/A | No limit |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Check YAML files (using yamllint which is already configured) + echo "### YAML Files (180 char limit)" >> $GITHUB_STEP_SUMMARY + + YAML_VIOLATIONS=0 + if command -v yamllint >/dev/null 2>&1; then + # Install yamllint if not present + : + else + pip install yamllint >/dev/null 2>&1 || true + fi + + # Run yamllint and count line-length warnings + YAML_OUTPUT=$(yamllint .github/workflows/*.yml 2>&1 | grep "line too long" || true) + if [ -n "$YAML_OUTPUT" ]; then + YAML_VIOLATIONS=$(echo "$YAML_OUTPUT" | wc -l) + echo "⚠️ Found $YAML_VIOLATIONS lines exceeding 180 characters in YAML files" >> $GITHUB_STEP_SUMMARY + echo "
View warnings (informational only)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$YAML_OUTPUT" | head -20 >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "✅ All YAML files comply with 180 character limit" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # Check source code files (PHP, Python, JavaScript, etc.) for 120 char limit + echo "### Source Code Files (120 char limit)" >> $GITHUB_STEP_SUMMARY + + LONG_LINES=$(find . -type f \ + \( -name "*.php" -o -name "*.py" -o -name "*.js" -o -name "*.ts" \ + -o -name "*.go" -o -name "*.rs" -o -name "*.java" -o -name "*.c" \ + -o -name "*.cpp" -o -name "*.h" -o -name "*.sh" \) \ + ! -path "./vendor/*" \ + ! -path "./node_modules/*" \ + ! -path "./.git/*" \ + ! -path "./build/*" \ + ! -path "./dist/*" \ + -exec awk 'length > 120 { print FILENAME ":" NR ": " length " chars" }' {} \; 2>/dev/null | head -20 || true) + + if [ -n "$LONG_LINES" ]; then + LINE_COUNT=$(echo "$LONG_LINES" | wc -l) + echo "⚠️ Found $LINE_COUNT source code lines exceeding 120 characters" >> $GITHUB_STEP_SUMMARY + echo "
View violations (informational)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$LONG_LINES" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "✅ All source code files comply with 120 character limit" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # Confirm Markdown files are not checked + echo "### Markdown Files" >> $GITHUB_STEP_SUMMARY + echo "✅ Markdown files have no line length limit per coding standards" >> $GITHUB_STEP_SUMMARY + echo "Rationale: Content-focused format, URLs, tables, and natural prose flow" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Summary + echo "### Summary" >> $GITHUB_STEP_SUMMARY + echo "This check is **informational only** and does not block merges." >> $GITHUB_STEP_SUMMARY + echo "Line length standards help maintain code readability." >> $GITHUB_STEP_SUMMARY + echo "Exceptions documented in: \`docs/policy/coding-style-guide.md\`" >> $GITHUB_STEP_SUMMARY + + file-naming-standards: + name: File Naming Standards + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check File Naming + run: | + set -x + echo "## 📝 File Naming Standards" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + VIOLATIONS=0 + + # Check PHP files (should be PascalCase for classes) + INVALID_PHP=$(find . -name "*.php" ! -path "./vendor/*" ! -path "./.git/*" ! -regex ".*/[A-Z][a-zA-Z0-9]*\.php" ! -name "index.php" ! -name "functions.php" | wc -l || echo 0) + + # Check config files (should be kebab-case) + INVALID_CONFIG=$(find . -name "*.yml" -o -name "*.yaml" -o -name "*.json" ! -path "./vendor/*" ! -path "./.git/*" ! -path "./node_modules/*" | grep -E "[A-Z_]" | wc -l || echo 0) + + echo "### Naming Violations" >> $GITHUB_STEP_SUMMARY + echo "- **PHP files not PascalCase**: $INVALID_PHP" >> $GITHUB_STEP_SUMMARY + echo "- **Config files not kebab-case**: $INVALID_CONFIG" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + VIOLATIONS=$((INVALID_PHP + INVALID_CONFIG)) + + if [ "$VIOLATIONS" -gt 0 ]; then + echo "⚠️ Found $VIOLATIONS naming convention violation(s)" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Follow naming conventions for consistency" >> $GITHUB_STEP_SUMMARY + else + echo "✅ File naming conventions followed" >> $GITHUB_STEP_SUMMARY + fi + + insecure-patterns: + name: Insecure Code Pattern Detection + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Scan for Insecure Patterns + run: | + set -x + echo "## 🔒 Insecure Code Pattern Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + VIOLATIONS=0 + + # PHP: SQL injection patterns + if grep -r -n "\\$_\(GET\|POST\|REQUEST\).*mysql_query\|mysqli_query" . --include="*.php" ! -path "./vendor/*" 2>/dev/null > /tmp/sql_inject.txt; then + COUNT=$(wc -l < /tmp/sql_inject.txt) + echo "⚠️ Found $COUNT potential SQL injection pattern(s)" >> $GITHUB_STEP_SUMMARY + VIOLATIONS=$((VIOLATIONS + COUNT)) + fi + + # PHP: eval/exec usage + if grep -r -n "eval\|exec\|system\|passthru\|shell_exec" . --include="*.php" ! -path "./vendor/*" 2>/dev/null > /tmp/exec.txt; then + COUNT=$(wc -l < /tmp/exec.txt) + echo "⚠️ Found $COUNT dangerous function call(s)" >> $GITHUB_STEP_SUMMARY + VIOLATIONS=$((VIOLATIONS + COUNT)) + fi + + # Python: eval usage + if grep -r -n "eval(" . --include="*.py" 2>/dev/null > /tmp/py_eval.txt; then + COUNT=$(wc -l < /tmp/py_eval.txt) + echo "⚠️ Found $COUNT Python eval() usage(s)" >> $GITHUB_STEP_SUMMARY + VIOLATIONS=$((VIOLATIONS + COUNT)) + fi + + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$VIOLATIONS" -gt 0 ]; then + echo "**Total Violations**: $VIOLATIONS" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Review and secure flagged patterns" >> $GITHUB_STEP_SUMMARY + else + echo "✅ No insecure patterns detected" >> $GITHUB_STEP_SUMMARY + fi + + code-complexity: + name: Code Complexity Analysis + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + + - name: Analyze Complexity + run: | + set -x + echo "## 📊 Code Complexity Analysis" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + PHP_COUNT=$(find . -name "*.php" ! -path "./vendor/*" ! -path "./.git/*" | wc -l) + + if [ "$PHP_COUNT" -gt 0 ]; then + # Install phploc + wget https://phar.phpunit.de/phploc.phar 2>/dev/null + chmod +x phploc.phar + + echo "### PHP Code Metrics" >> $GITHUB_STEP_SUMMARY + if ./phploc.phar --exclude vendor --exclude .git . 2>&1 | tee /tmp/phploc.txt; then + COMPLEXITY=$(grep "Cyclomatic Complexity" /tmp/phploc.txt | grep "Average" | awk '{print $NF}' || echo "N/A") + echo "**Average Cyclomatic Complexity**: $COMPLEXITY" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$COMPLEXITY" != "N/A" ] && [ $(echo "$COMPLEXITY > 10" | bc -l) -eq 1 ]; then + echo "⚠️ Average complexity exceeds recommended threshold (10)" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Refactor complex functions" >> $GITHUB_STEP_SUMMARY + else + echo "✅ Code complexity within acceptable limits" >> $GITHUB_STEP_SUMMARY + fi + fi + else + echo "ℹ️ No PHP files found for complexity analysis" >> $GITHUB_STEP_SUMMARY + fi + + code-duplication: + name: Code Duplication Detection + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + + - name: Detect Duplicates + run: | + set -x + echo "## 🔁 Code Duplication Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Check if PHP files exist + PHP_COUNT=$(find . -name "*.php" ! -path "./vendor/*" ! -path "./.git/*" | wc -l) + + if [ "$PHP_COUNT" -gt 0 ]; then + echo "### PHP Code Duplication" >> $GITHUB_STEP_SUMMARY + + # Install phpcpd + wget https://phar.phpunit.de/phpcpd.phar 2>/dev/null + chmod +x phpcpd.phar + + # Run duplication detection + if ./phpcpd.phar --exclude vendor --exclude .git . 2>&1 | tee /tmp/phpcpd.txt; then + DUPLICATION=$(grep "Found" /tmp/phpcpd.txt | grep -oE "[0-9]+\.[0-9]+%" | head -1 || echo "0.00%") + echo "📊 **Duplication Rate**: $DUPLICATION" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + DUPLICATION_NUM=$(echo "$DUPLICATION" | sed 's/%//') + if [ $(echo "$DUPLICATION_NUM > 5.0" | bc -l) -eq 1 ]; then + echo "⚠️ Code duplication exceeds 5% threshold" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View duplication details" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/phpcpd.txt >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "✅ Code duplication within acceptable limits (<5%)" >> $GITHUB_STEP_SUMMARY + fi + else + echo "✅ No significant code duplication detected" >> $GITHUB_STEP_SUMMARY + fi + else + echo "ℹ️ No PHP files found for duplication analysis" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note**: This is an informational check to encourage DRY principles." >> $GITHUB_STEP_SUMMARY + + dead-code-detection: + name: Dead Code Detection + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.x' + + - name: Detect Dead Code + run: | + set -x + echo "## 🗑️ Dead Code Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + PY_COUNT=$(find . -name "*.py" ! -path "./vendor/*" ! -path "./.git/*" ! -path "./venv/*" | wc -l) + + if [ "$PY_COUNT" -gt 0 ]; then + pip install vulture 2>/dev/null + echo "### Python Dead Code" >> $GITHUB_STEP_SUMMARY + + if vulture . --exclude vendor,venv,.git 2>&1 | tee /tmp/vulture.txt; then + DEAD_COUNT=$(wc -l < /tmp/vulture.txt || echo 0) + if [ "$DEAD_COUNT" -gt 0 ]; then + echo "⚠️ Found $DEAD_COUNT potential dead code item(s)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View dead code" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + head -50 /tmp/vulture.txt >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "✅ No dead code detected" >> $GITHUB_STEP_SUMMARY + fi + fi + else + echo "ℹ️ No Python files found for dead code analysis" >> $GITHUB_STEP_SUMMARY + fi + + + # ════════════════════════════════════════════════════════════════════════ + # TIER 4 — SUPPLEMENTARY (informational) + # ════════════════════════════════════════════════════════════════════════ + file-size-limits: + name: File Size Limits + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check File Sizes + run: | + set -x + echo "## 📦 File Size Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Exempt file types (allowed to be large) + EXEMPT="! -name *.mmdb ! -name *.woff2 ! -name *.woff ! -name *.ttf ! -name *.otf" + + # Find large files (>15MB warning, >20MB critical) + LARGE_FILES=$(find . -type f -size +15M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" 2>/dev/null | wc -l) + HUGE_FILES=$(find . -type f -size +20M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" 2>/dev/null | wc -l) + + echo "### Size Thresholds" >> $GITHUB_STEP_SUMMARY + echo "- **Warning**: Files >15MB" >> $GITHUB_STEP_SUMMARY + echo "- **Critical**: Files >20MB" >> $GITHUB_STEP_SUMMARY + echo "- **Exempt**: .mmdb, .woff2, .woff, .ttf, .otf" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$HUGE_FILES" -gt 0 ]; then + echo "❌ **Critical**: Found $HUGE_FILES file(s) exceeding 20MB" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View files >20MB" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + find . -type f -size +20M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec ls -lh {} + 2>/dev/null | awk '{print $5, $9}' >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Action Required**: Remove or optimize files >20MB" >> $GITHUB_STEP_SUMMARY + exit 1 + elif [ "$LARGE_FILES" -gt 0 ]; then + echo "⚠️ **Warning**: Found $LARGE_FILES file(s) between 15MB and 20MB" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View files >15MB" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + find . -type f -size +15M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec ls -lh {} + 2>/dev/null | awk '{print $5, $9}' >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Consider optimizing large files" >> $GITHUB_STEP_SUMMARY + else + echo "✅ All files within acceptable size limits" >> $GITHUB_STEP_SUMMARY + fi + + binary-file-detection: + name: Binary File Detection + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Detect Binary Files + run: | + set -x + echo "## 🔍 Binary File Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Find binary files excluding allowed types + BINARIES=$(find . -type f ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" \ + ! -name "*.png" ! -name "*.jpg" ! -name "*.jpeg" ! -name "*.gif" ! -name "*.svg" ! -name "*.ico" \ + ! -name "*.woff" ! -name "*.woff2" ! -name "*.ttf" ! -name "*.eot" \ + -exec file {} \; | grep -v "text" | grep -v "empty" | wc -l || echo 0) + + if [ "$BINARIES" -gt 0 ]; then + echo "⚠️ Found $BINARIES non-image binary file(s)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View binary files" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + find . -type f ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" \ + ! -name "*.png" ! -name "*.jpg" ! -name "*.jpeg" ! -name "*.gif" ! -name "*.svg" ! -name "*.ico" \ + ! -name "*.woff" ! -name "*.woff2" ! -name "*.ttf" ! -name "*.eot" \ + -exec file {} \; | grep -v "text" | grep -v "empty" | cut -d: -f1 >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Source control should primarily contain text files" >> $GITHUB_STEP_SUMMARY + else + echo "✅ No unexpected binary files detected" >> $GITHUB_STEP_SUMMARY + fi + + # ============================================================================ + # PHASE 4: Nice to Have Checks + # ============================================================================ + + todo-fixme-tracking: + name: TODO/FIXME Tracking + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Track Technical Debt + run: | + set -x + echo "## 📝 TODO/FIXME Tracking" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Tracking technical debt markers in source code." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Search for technical debt markers + PATTERNS="TODO|FIXME|HACK|XXX" + EXTENSIONS="*.php *.py *.js *.ts *.go *.rs *.java *.c *.cpp *.h *.hpp *.sh" + + echo "### Technical Debt Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + TOTAL_COUNT=0 + for ext in $EXTENSIONS; do + COUNT=$(find . -type f -name "$ext" ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec grep -n -E "($PATTERNS)" {} + 2>/dev/null | wc -l || echo 0) + TOTAL_COUNT=$((TOTAL_COUNT + COUNT)) + done + + if [ "$TOTAL_COUNT" -gt 0 ]; then + echo "⚠️ Found **$TOTAL_COUNT** technical debt item(s)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View technical debt items" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + for ext in $EXTENSIONS; do + find . -type f -name "$ext" ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec grep -n -H -E "($PATTERNS)" {} + 2>/dev/null | head -100 || true + done >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "✅ No technical debt markers found" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note**: This is an informational check. Technical debt items don't block compliance." >> $GITHUB_STEP_SUMMARY + + dependency-vulnerabilities: + name: Dependency Vulnerability Scanning + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + + - name: Setup Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.x' + + - name: Scan Dependencies + run: | + set -x + echo "## 🛡️ Dependency Vulnerability Scanning" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + VULNERABILITIES=0 + + # PHP Dependencies + if [ -f "composer.json" ]; then + echo "### PHP Dependencies (composer)" >> $GITHUB_STEP_SUMMARY + if composer audit --no-dev 2>&1 | tee /tmp/php_audit.txt; then + echo "✅ No PHP vulnerabilities detected" >> $GITHUB_STEP_SUMMARY + else + VULN_COUNT=$(grep -c "vulnerability" /tmp/php_audit.txt || echo 0) + echo "⚠️ Found $VULN_COUNT PHP vulnerability/vulnerabilities" >> $GITHUB_STEP_SUMMARY + VULNERABILITIES=$((VULNERABILITIES + VULN_COUNT)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + fi + + # Python Dependencies + if [ -f "requirements.txt" ]; then + echo "### Python Dependencies" >> $GITHUB_STEP_SUMMARY + pip install pip-audit 2>&1 > /dev/null + if pip-audit -r requirements.txt 2>&1 | tee /tmp/py_audit.txt; then + echo "✅ No Python vulnerabilities detected" >> $GITHUB_STEP_SUMMARY + else + VULN_COUNT=$(grep -c "vulnerability" /tmp/py_audit.txt || echo 0) + echo "⚠️ Found $VULN_COUNT Python vulnerability/vulnerabilities" >> $GITHUB_STEP_SUMMARY + VULNERABILITIES=$((VULNERABILITIES + VULN_COUNT)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + fi + + # NPM Dependencies + if [ -f "package.json" ]; then + echo "### NPM Dependencies" >> $GITHUB_STEP_SUMMARY + if npm audit --production 2>&1 | tee /tmp/npm_audit.txt; then + echo "✅ No NPM vulnerabilities detected" >> $GITHUB_STEP_SUMMARY + else + VULN_COUNT=$(grep -c "vulnerability" /tmp/npm_audit.txt || echo 0) + echo "⚠️ Found $VULN_COUNT NPM vulnerability/vulnerabilities" >> $GITHUB_STEP_SUMMARY + VULNERABILITIES=$((VULNERABILITIES + VULN_COUNT)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + fi + + if [ "$VULNERABILITIES" -gt 0 ]; then + echo "**Total Vulnerabilities**: $VULNERABILITIES" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Action Required**: Update vulnerable dependencies" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "✅ No dependency vulnerabilities detected" >> $GITHUB_STEP_SUMMARY + fi + + unused-dependencies: + name: Unused Dependencies Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + + - name: Check Unused Dependencies + run: | + set -x + echo "## 📦 Unused Dependencies Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ -f "composer.json" ]; then + echo "### PHP Dependencies" >> $GITHUB_STEP_SUMMARY + + # Install composer-unused + composer global require icanhazstring/composer-unused 2>/dev/null || true + + if composer global exec composer-unused 2>&1 | tee /tmp/unused.txt; then + UNUSED_COUNT=$(grep "unused" /tmp/unused.txt | wc -l || echo 0) + if [ "$UNUSED_COUNT" -gt 0 ]; then + echo "⚠️ Found $UNUSED_COUNT unused dependency/dependencies" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View unused dependencies" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/unused.txt >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "✅ No unused dependencies detected" >> $GITHUB_STEP_SUMMARY + fi + else + echo "✅ All dependencies appear to be in use" >> $GITHUB_STEP_SUMMARY + fi + else + echo "ℹ️ No composer.json found" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Remove unused dependencies to reduce attack surface" >> $GITHUB_STEP_SUMMARY + + broken-link-detection: + name: Broken Link Detection + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Internal Links + run: | + set -x + echo "## 🔗 Broken Link Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Checking internal links in markdown files." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + BROKEN_LINKS=0 + CHECKED_LINKS=0 + + # Find all markdown files + MD_FILES=$(find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*") + + for file in $MD_FILES; do + # Extract markdown links [text](path) + while IFS= read -r line; do + # Extract path from [text](path) + link=$(echo "$line" | sed -n 's/.*\](\([^)]*\)).*/\1/p') + + # Skip external links (http/https) + if echo "$link" | grep -qE "^https?://"; then + continue + fi + + # Skip anchors only + if echo "$link" | grep -qE "^#"; then + continue + fi + + CHECKED_LINKS=$((CHECKED_LINKS + 1)) + + # Get directory of the markdown file + basedir=$(dirname "$file") + + # Resolve relative path + if [ -n "$link" ]; then + # Remove anchor if present + clean_link=$(echo "$link" | sed 's/#.*//') + + # Check if file exists + if [ ! -e "$basedir/$clean_link" ] && [ ! -e "$clean_link" ]; then + echo "Broken link in $file: $link" >> /tmp/broken_links.txt + BROKEN_LINKS=$((BROKEN_LINKS + 1)) + fi + fi + done < <(grep -o '\[.*\](.*)' "$file" 2>/dev/null || true) + done + + echo "### Link Validation Results" >> $GITHUB_STEP_SUMMARY + echo "- **Links Checked**: $CHECKED_LINKS" >> $GITHUB_STEP_SUMMARY + echo "- **Broken Links**: $BROKEN_LINKS" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$BROKEN_LINKS" -gt 0 ]; then + echo "⚠️ Found $BROKEN_LINKS broken internal link(s)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View broken links" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/broken_links.txt 2>/dev/null >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Fix or remove broken links to maintain documentation quality" >> $GITHUB_STEP_SUMMARY + else + if [ "$CHECKED_LINKS" -gt 0 ]; then + echo "✅ All internal links are valid" >> $GITHUB_STEP_SUMMARY + else + echo "ℹ️ No internal links found to check" >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note**: This check validates internal file references only. External URLs are not validated." >> $GITHUB_STEP_SUMMARY + + # ============================================================================ + # PHASE 2: Medium Priority Checks + # ============================================================================ + + api-documentation: + name: API Documentation Coverage + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Documentation + run: | + set -x + echo "## 📚 API Documentation Coverage" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Count public functions/classes + PUBLIC_METHODS=$(grep -r "public function" . --include="*.php" ! -path "./vendor/*" | wc -l || echo 0) + DOCUMENTED=$(grep -B5 -r "public function" . --include="*.php" ! -path "./vendor/*" | grep -c "/\*\*" || echo 0) + + if [ "$PUBLIC_METHODS" -gt 0 ]; then + COVERAGE=$((DOCUMENTED * 100 / PUBLIC_METHODS)) + echo "**Documentation Coverage**: $COVERAGE% ($DOCUMENTED/$PUBLIC_METHODS)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$COVERAGE" -lt 80 ]; then + echo "⚠️ Documentation coverage below 80% threshold" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Add PHPDoc blocks to public methods" >> $GITHUB_STEP_SUMMARY + else + echo "✅ Good documentation coverage" >> $GITHUB_STEP_SUMMARY + fi + else + echo "ℹ️ No public methods found for documentation check" >> $GITHUB_STEP_SUMMARY + fi + + accessibility-check: + name: Accessibility Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Accessibility + run: | + set -x + echo "## ♿ Accessibility Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + HTML_COUNT=$(find . -name "*.html" ! -path "./vendor/*" ! -path "./.git/*" ! -path "./node_modules/*" | wc -l || echo 0) + MD_IMG_COUNT=$(find . -name "*.md" ! -path "./vendor/*" ! -path "./.git/*" -exec grep -l "!\[" {} + 2>/dev/null | wc -l || echo 0) + + if [ "$HTML_COUNT" -gt 0 ] || [ "$MD_IMG_COUNT" -gt 0 ]; then + # Check for images without alt text + MISSING_ALT=0 + + if [ "$HTML_COUNT" -gt 0 ]; then + MISSING_ALT=$(grep -r "> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$MISSING_ALT" -gt 0 ]; then + echo "⚠️ Found images without alt text" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Add descriptive alt text for accessibility" >> $GITHUB_STEP_SUMMARY + else + echo "✅ All images have alt text" >> $GITHUB_STEP_SUMMARY + fi + else + echo "ℹ️ No HTML files found for accessibility check" >> $GITHUB_STEP_SUMMARY + fi + + performance-metrics: + name: Performance Metrics + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Performance Metrics + run: | + set -x + echo "## ⚡ Performance Metrics" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Check if JavaScript bundles exist + if [ -f "package.json" ]; then + echo "### Bundle Analysis" >> $GITHUB_STEP_SUMMARY + + # Check for common bundle files + BUNDLE_SIZE=0 + if [ -d "dist" ]; then + BUNDLE_SIZE=$(du -sb dist/ 2>/dev/null | cut -f1 || echo 0) + elif [ -d "build" ]; then + BUNDLE_SIZE=$(du -sb build/ 2>/dev/null | cut -f1 || echo 0) + fi + + if [ "$BUNDLE_SIZE" -gt 0 ]; then + BUNDLE_MB=$((BUNDLE_SIZE / 1024 / 1024)) + echo "**Bundle Size**: ${BUNDLE_MB}MB" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$BUNDLE_MB" -gt 5 ]; then + echo "⚠️ Bundle size exceeds 5MB threshold" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Optimize bundle size" >> $GITHUB_STEP_SUMMARY + else + echo "✅ Bundle size within acceptable limits" >> $GITHUB_STEP_SUMMARY + fi + else + echo "ℹ️ No build artifacts found" >> $GITHUB_STEP_SUMMARY + fi + else + echo "ℹ️ Not a JavaScript project" >> $GITHUB_STEP_SUMMARY + fi + + enterprise-readiness: + name: Enterprise Readiness Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + extensions: json, mbstring + tools: composer + coverage: none + + - name: Install API Package + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader + else + echo "No composer.json — pulling MokoStandards tools" + if [ ! -d "/tmp/mokostandards" ]; then + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards 2>/dev/null || true + if [ -f "/tmp/mokostandards/composer.json" ]; then + cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true + cd - + fi + fi + fi + + - name: Check Enterprise Readiness + id: enterprise_check + run: | + echo "" >> $GITHUB_STEP_SUMMARY + + SCRIPT="" + if [ -f "api/validate/check_enterprise_readiness.php" ]; then + SCRIPT="api/validate/check_enterprise_readiness.php" + elif [ -f "/tmp/mokostandards/api/validate/check_enterprise_readiness.php" ]; then + SCRIPT="/tmp/mokostandards/api/validate/check_enterprise_readiness.php" + fi + + if [ -n "$SCRIPT" ]; then + php "$SCRIPT" --verbose | tee /tmp/enterprise-check.log + EXIT_CODE=$? + + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/enterprise-check.log >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + + if [ "$EXIT_CODE" -eq 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ Repository meets enterprise readiness criteria!" >> $GITHUB_STEP_SUMMARY + exit 0 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "⚠️ Enterprise readiness issues detected" >> $GITHUB_STEP_SUMMARY + echo "**Note:** This is informational - review recommendations to improve" >> $GITHUB_STEP_SUMMARY + exit 0 # Non-blocking + fi + else + echo "ℹ️ Enterprise readiness check script not found - skipping" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + repository-health: + name: Repository Health Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + extensions: json, mbstring + tools: composer + coverage: none + + - name: Install API Package + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader + else + echo "No composer.json — pulling MokoStandards tools" + if [ ! -d "/tmp/mokostandards" ]; then + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards 2>/dev/null || true + if [ -f "/tmp/mokostandards/composer.json" ]; then + cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true + cd - + fi + fi + fi + + - name: Check Repository Health + id: health_check + run: | + echo "" >> $GITHUB_STEP_SUMMARY + + SCRIPT="" + if [ -f "api/validate/check_repo_health.php" ]; then + SCRIPT="api/validate/check_repo_health.php" + elif [ -f "/tmp/mokostandards/api/validate/check_repo_health.php" ]; then + SCRIPT="/tmp/mokostandards/api/validate/check_repo_health.php" + fi + + if [ -n "$SCRIPT" ]; then + php "$SCRIPT" --verbose | tee /tmp/health-check.log + EXIT_CODE=$? + + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/health-check.log >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + + if [ "$EXIT_CODE" -eq 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ Repository health check passed!" >> $GITHUB_STEP_SUMMARY + exit 0 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "⚠️ Repository health issues detected" >> $GITHUB_STEP_SUMMARY + echo "**Note:** This is informational - review recommendations to improve" >> $GITHUB_STEP_SUMMARY + exit 0 # Non-blocking + fi + else + echo "ℹ️ Repository health check script not found - skipping" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + terraform-validation: + name: Terraform Configuration Validation + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0 + with: + terraform_version: "1.0" + + - name: Validate Terraform Files + run: | + set -x + echo "## 🏗️ Terraform Configuration Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Check if terraform files exist + TF_COUNT=$(find . -name "*.tf" -type f | wc -l || echo 0) + + if [ "$TF_COUNT" -eq 0 ]; then + echo "ℹ️ No Terraform files found in repository" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + echo "**Terraform Files Found**: $TF_COUNT" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Validation Results + VALIDATION_PASSED=true + WARNINGS=0 + ERRORS=0 + + # 1. Check .github/config.tf location (not root override files) + echo "### Override Configuration Check" >> $GITHUB_STEP_SUMMARY + LEGACY_OVERRIDES=$(find . -maxdepth 1 -name "*override*.tf" -o -name "MokoStandards.override.tf" 2>/dev/null | wc -l || echo 0) + if [ "$LEGACY_OVERRIDES" -gt 0 ]; then + echo "⚠️ Found legacy override files in root directory" >> $GITHUB_STEP_SUMMARY + echo "**Expected Location**: .github/config.tf" >> $GITHUB_STEP_SUMMARY + echo "**Legacy files found**: $LEGACY_OVERRIDES" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + if [ -f ".github/config.tf" ]; then + echo "✅ Override configuration in correct location (.github/config.tf)" >> $GITHUB_STEP_SUMMARY + else + echo "ℹ️ No override configuration found" >> $GITHUB_STEP_SUMMARY + fi + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # 2. Terraform Syntax Validation + echo "### Terraform Syntax Validation" >> $GITHUB_STEP_SUMMARY + SYNTAX_ERRORS=0 + + # Find all directories with terraform files + for dir in $(find . -name "*.tf" -type f -exec dirname {} \; | sort -u); do + cd "$dir" || continue + echo "Validating: $dir" >> $GITHUB_STEP_SUMMARY + + # Initialize without backend + terraform init -backend=false > /dev/null 2>&1 || true + + # Validate + if terraform validate -no-color > /tmp/tf_validate.txt 2>&1; then + echo " ✅ Syntax valid" >> $GITHUB_STEP_SUMMARY + else + echo " ❌ Syntax errors found" >> $GITHUB_STEP_SUMMARY + cat /tmp/tf_validate.txt >> $GITHUB_STEP_SUMMARY + SYNTAX_ERRORS=$((SYNTAX_ERRORS + 1)) + VALIDATION_PASSED=false + fi + cd - > /dev/null + done + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$SYNTAX_ERRORS" -eq 0 ]; then + echo "✅ All Terraform files have valid syntax" >> $GITHUB_STEP_SUMMARY + else + echo "❌ Found $SYNTAX_ERRORS directories with syntax errors" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + SYNTAX_ERRORS)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # 3. Terraform Formatting Check + echo "### Terraform Formatting Check" >> $GITHUB_STEP_SUMMARY + FORMAT_ISSUES=0 + + for tf_file in $(find . -name "*.tf" -type f); do + if ! terraform fmt -check=true -no-color "$tf_file" > /dev/null 2>&1; then + FORMAT_ISSUES=$((FORMAT_ISSUES + 1)) + fi + done + + if [ "$FORMAT_ISSUES" -eq 0 ]; then + echo "✅ All Terraform files properly formatted" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Found $FORMAT_ISSUES files with formatting issues" >> $GITHUB_STEP_SUMMARY + echo "**Fix**: Run \`terraform fmt -recursive\`" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # 4. Check for file_metadata blocks + echo "### File Metadata Validation" >> $GITHUB_STEP_SUMMARY + MISSING_METADATA=0 + + for tf_file in $(find . -name "*.tf" -type f); do + if ! grep -q "file_metadata" "$tf_file"; then + MISSING_METADATA=$((MISSING_METADATA + 1)) + fi + done + + if [ "$MISSING_METADATA" -eq 0 ]; then + echo "✅ All Terraform files contain file_metadata block" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Found $MISSING_METADATA files missing file_metadata block" >> $GITHUB_STEP_SUMMARY + echo "**Reference**: docs/policy/terraform-file-standards.md" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # 5. Version Consistency Check + echo "### Version Consistency Check" >> $GITHUB_STEP_SUMMARY + VERSION_MISMATCHES=0 + EXPECTED_VERSION="04.00.04" + + for tf_file in $(find . -name "*.tf" -type f); do + if grep -q "version.*=" "$tf_file"; then + if ! grep -q "version.*=.*\"$EXPECTED_VERSION\"" "$tf_file"; then + VERSION_MISMATCHES=$((VERSION_MISMATCHES + 1)) + fi + fi + done + + if [ "$VERSION_MISMATCHES" -eq 0 ]; then + echo "✅ All Terraform file versions match $EXPECTED_VERSION" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Found $VERSION_MISMATCHES files with version mismatches" >> $GITHUB_STEP_SUMMARY + echo "**Expected Version**: $EXPECTED_VERSION" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # 6. Copyright Header Check + echo "### Copyright Header Check" >> $GITHUB_STEP_SUMMARY + MISSING_COPYRIGHT=0 + + for tf_file in $(find . -name "*.tf" -type f); do + if ! grep -q "Copyright (C)" "$tf_file"; then + MISSING_COPYRIGHT=$((MISSING_COPYRIGHT + 1)) + fi + done + + if [ "$MISSING_COPYRIGHT" -eq 0 ]; then + echo "✅ All Terraform files have copyright headers" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Found $MISSING_COPYRIGHT files missing copyright headers" >> $GITHUB_STEP_SUMMARY + echo "**Reference**: docs/policy/terraform-file-standards.md" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # Summary + echo "---" >> $GITHUB_STEP_SUMMARY + echo "### Validation Summary" >> $GITHUB_STEP_SUMMARY + echo "**Total Files**: $TF_COUNT" >> $GITHUB_STEP_SUMMARY + echo "**Errors**: $ERRORS" >> $GITHUB_STEP_SUMMARY + echo "**Warnings**: $WARNINGS" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$VALIDATION_PASSED" = true ] && [ "$ERRORS" -eq 0 ]; then + echo "✅ **Terraform Validation: PASSED**" >> $GITHUB_STEP_SUMMARY + exit 0 + elif [ "$ERRORS" -gt 0 ]; then + echo "❌ **Terraform Validation: FAILED**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note**: This is an informational check and does not block merges" >> $GITHUB_STEP_SUMMARY + exit 0 # Informational only + else + echo "⚠️ **Terraform Validation: PASSED WITH WARNINGS**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note**: This is an informational check and does not block merges" >> $GITHUB_STEP_SUMMARY + exit 0 # Informational only + fi + + summary: + name: Compliance Summary + runs-on: ubuntu-latest + needs: [ + repository-structure, documentation-quality, coding-standards, line-length-validation, license-compliance, git-hygiene, workflow-validation, version-consistency, script-integrity, enterprise-readiness, repository-health, + todo-fixme-tracking, file-size-limits, secret-scanning, broken-link-detection, + dependency-vulnerabilities, code-duplication, unused-dependencies, readme-completeness, + code-complexity, api-documentation, insecure-patterns, binary-file-detection, + dead-code-detection, file-naming-standards, accessibility-check, performance-metrics, terraform-validation + ] + if: always() + + steps: + - name: Generate Compliance Report + run: | + set -x + echo "# 📊 MokoStandards Compliance Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Calculate overall status + REPO_STATUS="${{ needs.repository-structure.result }}" + DOCS_STATUS="${{ needs.documentation-quality.result }}" + CODE_STATUS="${{ needs.coding-standards.result }}" + LINE_LENGTH_STATUS="${{ needs.line-length-validation.result }}" + LICENSE_STATUS="${{ needs.license-compliance.result }}" + GIT_STATUS="${{ needs.git-hygiene.result }}" + WORKFLOW_STATUS="${{ needs.workflow-validation.result }}" + VERSION_STATUS="${{ needs.version-consistency.result }}" + SCRIPT_STATUS="${{ needs.script-integrity.result }}" + ENTERPRISE_STATUS="${{ needs.enterprise-readiness.result }}" + HEALTH_STATUS="${{ needs.repository-health.result }}" + TERRAFORM_STATUS="${{ needs.terraform-validation.result }}" + + PASSED=0 + FAILED=0 + WARNINGS=0 + TOTAL=28 + + # Critical checks (must pass) + [ "$REPO_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$DOCS_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$CODE_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$LICENSE_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$GIT_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$WORKFLOW_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$VERSION_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$SCRIPT_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + + # Informational checks (don't fail build) + if [ "$ENTERPRISE_STATUS" = "success" ]; then + PASSED=$((PASSED + 1)) + else + WARNINGS=$((WARNINGS + 1)) + fi + + if [ "$HEALTH_STATUS" = "success" ]; then + PASSED=$((PASSED + 1)) + else + WARNINGS=$((WARNINGS + 1)) + fi + + if [ "$TERRAFORM_STATUS" = "success" ]; then + PASSED=$((PASSED + 1)) + else + WARNINGS=$((WARNINGS + 1)) + fi + + # Adjust total to only count critical checks for compliance percentage + CRITICAL_TOTAL=8 + CRITICAL_PASSED=$((PASSED - WARNINGS)) + COMPLIANCE_PERCENT=$((CRITICAL_PASSED * 100 / CRITICAL_TOTAL)) + + # Overall status badge + if [ "$COMPLIANCE_PERCENT" -eq 100 ]; then + echo "## ✅ Overall Status: **COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY + elif [ "$COMPLIANCE_PERCENT" -ge 80 ]; then + echo "## ⚠️ Overall Status: **MOSTLY COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY + elif [ "$COMPLIANCE_PERCENT" -ge 50 ]; then + echo "## ⚠️ Overall Status: **PARTIALLY COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY + else + echo "## ❌ Overall Status: **NON-COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Critical Checks:** $CRITICAL_PASSED/$CRITICAL_TOTAL passed" >> $GITHUB_STEP_SUMMARY + echo "**Total Checks:** $PASSED/$TOTAL passed" >> $GITHUB_STEP_SUMMARY + if [ "$WARNINGS" -gt 0 ]; then + echo "**Informational:** $WARNINGS warning(s)" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # Progress bar + FILLED=$((COMPLIANCE_PERCENT / 5)) + EMPTY=$((20 - FILLED)) + BAR="" + for i in $(seq 1 $FILLED); do BAR="${BAR}█"; done + for i in $(seq 1 $EMPTY); do BAR="${BAR}░"; done + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$BAR $COMPLIANCE_PERCENT%" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Detailed breakdown + echo "## Validation Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Area | Status | Result | Priority |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|--------|----------|" >> $GITHUB_STEP_SUMMARY + + # Repository Structure + if [ "$REPO_STATUS" = "success" ]; then + echo "| 📁 Repository Structure | ✅ Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| 📁 Repository Structure | ❌ Fail | **Action Required** | 🔴 Critical |" >> $GITHUB_STEP_SUMMARY + fi + + # Documentation Quality + if [ "$DOCS_STATUS" = "success" ]; then + echo "| 📚 Documentation Quality | ✅ Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| 📚 Documentation Quality | ❌ Fail | **Action Required** | 🔴 Critical |" >> $GITHUB_STEP_SUMMARY + fi + + # Coding Standards + if [ "$CODE_STATUS" = "success" ]; then + echo "| 💻 Coding Standards | ✅ Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| 💻 Coding Standards | ⚠️ Warning | Review Recommended | 🟡 Medium |" >> $GITHUB_STEP_SUMMARY + fi + + # License Compliance + if [ "$LICENSE_STATUS" = "success" ]; then + echo "| ⚖️ License Compliance | ✅ Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| ⚖️ License Compliance | ❌ Fail | **Action Required** | 🔴 Critical |" >> $GITHUB_STEP_SUMMARY + fi + + # Git Hygiene + if [ "$GIT_STATUS" = "success" ]; then + echo "| 🧹 Git Repository Hygiene | ✅ Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| 🧹 Git Repository Hygiene | ⚠️ Warning | Review Recommended | 🟡 Medium |" >> $GITHUB_STEP_SUMMARY + fi + + # Workflow Configuration + if [ "$WORKFLOW_STATUS" = "success" ]; then + echo "| ⚙️ Workflow Configuration | ✅ Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| ⚙️ Workflow Configuration | ⚠️ Warning | Review Recommended | 🟡 Medium |" >> $GITHUB_STEP_SUMMARY + fi + + # Version Consistency + if [ "$VERSION_STATUS" = "success" ]; then + echo "| 🔢 Version Consistency | ✅ Pass | All versions match | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| 🔢 Version Consistency | ❌ Fail | **Action Required** | 🔴 Critical |" >> $GITHUB_STEP_SUMMARY + fi + + # Script Integrity + if [ "$SCRIPT_STATUS" = "success" ]; then + echo "| 🔐 Script Integrity | ✅ Pass | SHA hashes validated | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| 🔐 Script Integrity | ❌ Fail | **Action Required** | 🔴 Critical |" >> $GITHUB_STEP_SUMMARY + fi + + # Enterprise Readiness (Informational) + if [ "$ENTERPRISE_STATUS" = "success" ]; then + echo "| 🏢 Enterprise Readiness | ✅ Pass | Ready for enterprise | ℹ️ Info |" >> $GITHUB_STEP_SUMMARY + else + echo "| 🏢 Enterprise Readiness | ℹ️ Info | Review suggestions | ℹ️ Info |" >> $GITHUB_STEP_SUMMARY + fi + + # Repository Health (Informational) + if [ "$HEALTH_STATUS" = "success" ]; then + echo "| 🏥 Repository Health | ✅ Pass | Health check passed | ℹ️ Info |" >> $GITHUB_STEP_SUMMARY + else + echo "| 🏥 Repository Health | ℹ️ Info | Review recommendations | ℹ️ Info |" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + + # Action items summary + if [ "$FAILED" -gt 0 ]; then + echo "## ⚡ Action Items" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**$FAILED validation area(s) require attention:**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + [ "$REPO_STATUS" != "success" ] && echo "- 🔴 **Critical:** Fix repository structure issues" >> $GITHUB_STEP_SUMMARY + [ "$DOCS_STATUS" != "success" ] && echo "- 🔴 **Critical:** Improve documentation quality" >> $GITHUB_STEP_SUMMARY + [ "$LICENSE_STATUS" != "success" ] && echo "- 🔴 **Critical:** Resolve license compliance issues" >> $GITHUB_STEP_SUMMARY + [ "$CODE_STATUS" != "success" ] && echo "- 🟡 **Medium:** Review coding standards violations" >> $GITHUB_STEP_SUMMARY + [ "$GIT_STATUS" != "success" ] && echo "- 🟡 **Medium:** Address git repository hygiene items" >> $GITHUB_STEP_SUMMARY + [ "$WORKFLOW_STATUS" != "success" ] && echo "- 🟡 **Medium:** Review workflow configuration" >> $GITHUB_STEP_SUMMARY + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Next Steps:**" >> $GITHUB_STEP_SUMMARY + echo "1. Review detailed results in individual job outputs above" >> $GITHUB_STEP_SUMMARY + echo "2. Follow remediation steps provided for each failure" >> $GITHUB_STEP_SUMMARY + echo "3. Re-run this workflow after making corrections" >> $GITHUB_STEP_SUMMARY + echo "4. Reach 100% compliance before merging" >> $GITHUB_STEP_SUMMARY + else + echo "## 🎉 Excellent!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Your repository is **fully compliant** with MokoStandards!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Achievements:**" >> $GITHUB_STEP_SUMMARY + echo "- ✅ All required directories and files present" >> $GITHUB_STEP_SUMMARY + echo "- ✅ Documentation meets quality standards" >> $GITHUB_STEP_SUMMARY + echo "- ✅ Coding standards followed" >> $GITHUB_STEP_SUMMARY + echo "- ✅ License compliance verified" >> $GITHUB_STEP_SUMMARY + echo "- ✅ Git repository well-maintained" >> $GITHUB_STEP_SUMMARY + echo "- ✅ Workflows properly configured" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "---" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "📚 **Resources:**" >> $GITHUB_STEP_SUMMARY + echo "- [MokoStandards Documentation](https://github.com/mokoconsulting-tech/MokoStandards)" >> $GITHUB_STEP_SUMMARY + echo "- [Repository Structure Guide](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/core-structure.md)" >> $GITHUB_STEP_SUMMARY + echo "- [Documentation Standards](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/document-formatting.md)" >> $GITHUB_STEP_SUMMARY + echo "- [Coding Standards](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/coding-style-guide.md)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "_Generated by MokoStandards Compliance Workflow v${WORKFLOW_VERSION}_" >> $GITHUB_STEP_SUMMARY + + # Create tracking issue for non-compliance if on push + if [ "$COMPLIANCE_PERCENT" -lt 100 ] && [ "${{ github.event_name }}" = "push" ]; then + echo "Creating tracking issue for standards violations..." + fi + + # Exit with error if not fully compliant + if [ "$COMPLIANCE_PERCENT" -lt 100 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ❌ Standards Compliance Failed" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Overall Compliance:** $COMPLIANCE_PERCENT%" >> $GITHUB_STEP_SUMMARY + echo "**Status:** Repository does not meet 100% compliance requirement" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Review and fix all validation failures above" >> $GITHUB_STEP_SUMMARY + echo "" + echo "❌ ERROR: Standards compliance at $COMPLIANCE_PERCENT% - 100% required" + exit 1 + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ✅ Full Standards Compliance Achieved" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Overall Compliance:** 100%" >> $GITHUB_STEP_SUMMARY + echo "**Status:** Repository meets all MokoStandards requirements" >> $GITHUB_STEP_SUMMARY + echo "" + echo "✅ SUCCESS: Repository is fully MokoStandards compliant" + + - name: Create or reopen tracking issue for standards violations + if: failure() + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + REPO="${{ github.repository }}" + RUN_URL="${{ github.server_url }}/${REPO}/actions/runs/${{ github.run_id }}" + DATE=$(date -u '+%Y-%m-%d') + SHA="${{ github.sha }}" + ACTOR="${{ github.actor }}" + BRANCH="${{ github.ref_name }}" + + # Collect failed checks + FAILED="" + [ "${{ needs.repository-structure.result }}" != "success" ] && FAILED="${FAILED}\n- Repository Structure" + [ "${{ needs.documentation-quality.result }}" != "success" ] && FAILED="${FAILED}\n- Documentation Quality" + [ "${{ needs.coding-standards.result }}" != "success" ] && FAILED="${FAILED}\n- Coding Standards" + [ "${{ needs.license-compliance.result }}" != "success" ] && FAILED="${FAILED}\n- License Compliance" + [ "${{ needs.git-hygiene.result }}" != "success" ] && FAILED="${FAILED}\n- Git Hygiene" + [ "${{ needs.workflow-validation.result }}" != "success" ] && FAILED="${FAILED}\n- Workflow Validation" + [ "${{ needs.version-consistency.result }}" != "success" ] && FAILED="${FAILED}\n- Version Consistency" + [ "${{ needs.script-integrity.result }}" != "success" ] && FAILED="${FAILED}\n- Script Integrity" + [ "${{ needs.secret-scanning.result }}" != "success" ] && FAILED="${FAILED}\n- Secret Scanning" + [ "${{ needs.line-length-validation.result }}" != "success" ] && FAILED="${FAILED}\n- Line Length" + [ "${{ needs.file-size-limits.result }}" != "success" ] && FAILED="${FAILED}\n- File Size Limits" + [ "${{ needs.readme-completeness.result }}" != "success" ] && FAILED="${FAILED}\n- README Completeness" + + if [ -z "$FAILED" ]; then + echo "No failed checks to report" + exit 0 + fi + + TITLE="[Standards] Compliance violations — ${DATE}" + BODY="## Standards Compliance Violations + + | Field | Value | + |-------|-------| + | **Branch** | \`${BRANCH}\` | + | **Commit** | \`${SHA:0:7}\` | + | **Actor** | @${ACTOR} | + | **Run** | [View workflow](${RUN_URL}) | + + ### Failed Checks + $(printf '%b' "$FAILED") + + ### Required Actions + 1. Review the [workflow run](${RUN_URL}) for details + 2. Fix each failed check + 3. Push to trigger a new scan + + --- + *Auto-created by standards-compliance workflow*" + + BODY=$(echo "$BODY" | sed 's/^ //') + LABEL="standards-violation" + + gh label create "$LABEL" --repo "$REPO" --color "D73A4A" --description "Standards compliance failure" --force 2>/dev/null || true + + EXISTING=$(gh api "repos/${REPO}/issues?labels=${LABEL}&state=all&per_page=1&sort=created&direction=desc" \ + --jq '.[0].number' 2>/dev/null) + + if [ -n "$EXISTING" ] && [ "$EXISTING" != "null" ]; then + gh api "repos/${REPO}/issues/${EXISTING}" -X PATCH \ + -f title="$TITLE" -f body="$BODY" -f state="open" --silent + echo "Updated issue #${EXISTING}" + else + gh issue create --repo "$REPO" --title "$TITLE" --body "$BODY" \ + --label "$LABEL" --assignee "jmiller" + fi + +# CUSTOMIZATION: +# +# 1. Adjust severity of checks (convert warnings to errors or vice versa) +# 2. Add project-specific validation rules +# 3. Integrate with custom linting tools +# 4. Add notification steps for compliance failures +# 5. Customize required files/directories for your project type + -- 2.52.0 From 0e92599e27dca42fed4dc5bbc4f98bbf9bcd0ee7 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 15:45:02 +0000 Subject: [PATCH 04/63] chore: sync auto-bump.yml from Template-Generic [skip ci] --- .mokogitea/workflows/auto-bump.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml index 91e16f7..b58ea7d 100644 --- a/.mokogitea/workflows/auto-bump.yml +++ b/.mokogitea/workflows/auto-bump.yml @@ -4,7 +4,7 @@ # # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow -# INGROUP: mokocli.Release +# INGROUP: MokoCLI.Release # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # PATH: /.mokogitea/workflows/auto-bump.yml # VERSION: 09.02.00 @@ -44,7 +44,7 @@ jobs: token: ${{ secrets.MOKOGITEA_TOKEN }} fetch-depth: 1 - - name: Setup mokocli tools + - name: Setup MokoCLI tools run: | if ! command -v composer &> /dev/null; then sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 @@ -53,7 +53,7 @@ jobs: echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV" else git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/MokoCLI.git" \ + "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \ /tmp/mokocli cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV" -- 2.52.0 From 8504f7b8a28488f9108c20c6c8173a0903403b5d Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 15:45:03 +0000 Subject: [PATCH 05/63] chore: sync auto-release.yml from Template-Generic [skip ci] --- .mokogitea/workflows/auto-release.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index a7563e0..1b273e4 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -4,17 +4,18 @@ # # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow -# INGROUP: mokocli.Release +# INGROUP: MokoCLI.Release # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # PATH: /.mokogitea/workflows/auto-release.yml # VERSION: 05.01.00 -# BRIEF: Universal build & release � detects platform from manifest.xml +# BRIEF: Universal build & release � detects platform from the MokoGitea Metadata field (API) # # +=======================================================================+ # | UNIVERSAL BUILD & RELEASE PIPELINE | # +=======================================================================+ # | | -# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | +# | Reads platform (joomla|dolibarr|generic) from the MokoGitea | +# | Metadata field (API) to branch logic. | # | | # | Platform-specific: | # | joomla: XML manifest, type-prefixed packages | @@ -81,7 +82,7 @@ jobs: fetch-depth: 1 submodules: recursive - - name: Setup mokocli tools + - name: Setup MokoCLI tools env: MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting @@ -95,7 +96,7 @@ jobs: sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 fi rm -rf /tmp/mokocli - CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoCLI.git + CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli cd /tmp/mokocli composer install --no-dev --no-interaction --quiet @@ -231,7 +232,7 @@ jobs: fi echo "No conflict markers found" - - name: Setup mokocli tools + - name: Setup MokoCLI tools env: MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting @@ -246,7 +247,7 @@ jobs: sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 fi rm -rf /tmp/mokocli - CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoCLI.git + CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli cd /tmp/mokocli composer install --no-dev --no-interaction --quiet -- 2.52.0 From af4a3d055c61774a092676e89694f2184c9efdbb Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 15:45:05 +0000 Subject: [PATCH 06/63] chore: sync branch-cleanup.yml from Template-Generic [skip ci] --- .mokogitea/workflows/branch-cleanup.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mokogitea/workflows/branch-cleanup.yml b/.mokogitea/workflows/branch-cleanup.yml index 25ebae1..909e706 100644 --- a/.mokogitea/workflows/branch-cleanup.yml +++ b/.mokogitea/workflows/branch-cleanup.yml @@ -4,7 +4,7 @@ # # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow -# INGROUP: MokoStandards.Universal +# INGROUP: MokoCLI.Universal # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # PATH: /.mokogitea/workflows/branch-cleanup.yml # VERSION: 01.00.00 -- 2.52.0 From 85ad452af0c71440663d5251b7c8ea08625492d2 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 15:45:06 +0000 Subject: [PATCH 07/63] chore: sync cascade-dev.yml from Template-Generic [skip ci] --- .mokogitea/workflows/cascade-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mokogitea/workflows/cascade-dev.yml b/.mokogitea/workflows/cascade-dev.yml index b2eabe1..31c35ad 100644 --- a/.mokogitea/workflows/cascade-dev.yml +++ b/.mokogitea/workflows/cascade-dev.yml @@ -4,7 +4,7 @@ # # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow -# INGROUP: MokoStandards.Cascade +# INGROUP: MokoCLI.Cascade # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # PATH: /.mokogitea/workflows/cascade-dev.yml # VERSION: 02.00.00 -- 2.52.0 From 4eba8c5f9816a546293b51b3ae71dd3188e3b83b Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 15:45:07 +0000 Subject: [PATCH 08/63] chore: sync ci-generic.yml from Template-Generic [skip ci] --- .mokogitea/workflows/ci-generic.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/ci-generic.yml b/.mokogitea/workflows/ci-generic.yml index 14f81ba..869087e 100644 --- a/.mokogitea/workflows/ci-generic.yml +++ b/.mokogitea/workflows/ci-generic.yml @@ -4,7 +4,7 @@ # # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow -# INGROUP: MokoStandards.CI +# INGROUP: MokoCLI.CI # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # PATH: /.mokogitea/workflows/ci-generic.yml # VERSION: 01.00.00 @@ -131,7 +131,7 @@ jobs: test: name: Tests runs-on: ubuntu-latest - # Independent job (no `needs: lint`): the Gitea Actions scheduler does not + # Independent job (no `needs: lint`): the MokoGitea Actions scheduler does not # offer the dependent 2nd job of a needs-chain to runners, so it stalls in # "waiting" and is reaped by ABANDONED_JOB_TIMEOUT. Guard template repos # directly (same condition lint uses) instead of gating on lint's result. -- 2.52.0 From a89322a3afcdbde601082a75d7051dcea0631647 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 15:45:08 +0000 Subject: [PATCH 09/63] chore: sync ci-issue-reporter.yml from Template-Generic [skip ci] --- .mokogitea/workflows/ci-issue-reporter.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/ci-issue-reporter.yml b/.mokogitea/workflows/ci-issue-reporter.yml index 0520237..600d8c1 100644 --- a/.mokogitea/workflows/ci-issue-reporter.yml +++ b/.mokogitea/workflows/ci-issue-reporter.yml @@ -4,7 +4,7 @@ # # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow -# INGROUP: mokocli.Universal +# INGROUP: MokoCLI.Universal # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # PATH: /.mokogitea/workflows/ci-issue-reporter.yml # VERSION: 01.00.00 @@ -52,7 +52,7 @@ jobs: MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} run: | MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}" - git clone --depth 1 --filter=blob:none --sparse "${MOKOGITEA_URL}/MokoConsulting/MokoCLI.git" /tmp/mokocli + git clone --depth 1 --filter=blob:none --sparse "${MOKOGITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli cd /tmp/mokocli && git sparse-checkout set cli/ci_issue_reporter.sh - name: Report CI failure -- 2.52.0 From bbed991f7fdd823b73cd857d1283079cf5a88202 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 15:45:09 +0000 Subject: [PATCH 10/63] chore: sync cleanup.yml from Template-Generic [skip ci] --- .mokogitea/workflows/cleanup.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/cleanup.yml b/.mokogitea/workflows/cleanup.yml index b4db32b..d08e29e 100644 --- a/.mokogitea/workflows/cleanup.yml +++ b/.mokogitea/workflows/cleanup.yml @@ -4,8 +4,8 @@ # # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow -# INGROUP: MokoStandards.Maintenance -# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# INGROUP: MokoCLI.Maintenance +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # PATH: /.mokogitea/workflows/cleanup.yml # VERSION: 01.00.00 # BRIEF: Scheduled cleanup — delete merged branches and old workflow runs -- 2.52.0 From 5adfdd530b5ec912044cc21a504a9266b44d773a Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 15:45:11 +0000 Subject: [PATCH 11/63] chore: sync gitleaks.yml from Template-Generic [skip ci] --- .mokogitea/workflows/gitleaks.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/gitleaks.yml b/.mokogitea/workflows/gitleaks.yml index 7312838..579f0cb 100644 --- a/.mokogitea/workflows/gitleaks.yml +++ b/.mokogitea/workflows/gitleaks.yml @@ -4,8 +4,8 @@ # # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow -# INGROUP: MokoStandards.Security -# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API +# INGROUP: MokoCLI.Security +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # PATH: /.mokogitea/workflows/gitleaks.yml # VERSION: 01.00.00 # BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens -- 2.52.0 From ab29ffc65f5fe48ce804a91281372a8d7c854b46 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 15:45:12 +0000 Subject: [PATCH 12/63] chore: sync issue-branch.yml from Template-Generic [skip ci] --- .mokogitea/workflows/issue-branch.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index eb67f8f..12a4b11 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -4,7 +4,7 @@ # # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow -# INGROUP: mokocli.Automation +# INGROUP: MokoCLI.Automation # VERSION: 01.00.00 # BRIEF: Auto-create feature branch when an issue is opened -- 2.52.0 From 446a31a680e21f201e17f1680a9f1e9fd1df7b2d Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 15:45:13 +0000 Subject: [PATCH 13/63] chore: sync notify.yml from Template-Generic [skip ci] --- .mokogitea/workflows/notify.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/notify.yml b/.mokogitea/workflows/notify.yml index 656996f..7a03898 100644 --- a/.mokogitea/workflows/notify.yml +++ b/.mokogitea/workflows/notify.yml @@ -4,8 +4,8 @@ # # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow -# INGROUP: MokoStandards.Notifications -# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# INGROUP: MokoCLI.Notifications +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # PATH: /.mokogitea/workflows/notify.yml # VERSION: 01.00.00 # BRIEF: Push notifications via ntfy on release success or workflow failure -- 2.52.0 From ee6790bf49853b83e5787afd2edc9cda9a1d646d Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 15:45:14 +0000 Subject: [PATCH 14/63] chore: sync pr-check.yml from Template-Generic [skip ci] --- .mokogitea/workflows/pr-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index 2874d60..8fb052a 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -4,7 +4,7 @@ # # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow -# INGROUP: mokocli.CI +# INGROUP: MokoCLI.CI # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # PATH: /.mokogitea/workflows/pr-check.yml # VERSION: 09.23.00 -- 2.52.0 From 3ce37abf2fc0c60e23d5d4c57a271716269e4333 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 15:45:15 +0000 Subject: [PATCH 15/63] chore: sync pre-release.yml from Template-Generic [skip ci] --- .mokogitea/workflows/pre-release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index b212772..4ea0367 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -4,7 +4,7 @@ # # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow -# INGROUP: mokocli.Release +# INGROUP: MokoCLI.Release # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # PATH: /.mokogitea/workflows/pre-release.yml # VERSION: 05.02.01 @@ -69,7 +69,7 @@ jobs: run: | git submodule foreach --quiet 'git checkout main && git pull --quiet origin main' 2>/dev/null || true - - name: Setup mokocli tools + - name: Setup MokoCLI tools env: MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting @@ -84,7 +84,7 @@ jobs: sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 fi rm -rf /tmp/mokocli - CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoCLI.git + CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV -- 2.52.0 From e9fec193584f5425736454e87b550606b428c786 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 15:45:16 +0000 Subject: [PATCH 16/63] chore: sync rc-revert.yml from Template-Generic [skip ci] --- .mokogitea/workflows/rc-revert.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mokogitea/workflows/rc-revert.yml b/.mokogitea/workflows/rc-revert.yml index 57934ea..f58d1fa 100644 --- a/.mokogitea/workflows/rc-revert.yml +++ b/.mokogitea/workflows/rc-revert.yml @@ -4,7 +4,7 @@ # # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow -# INGROUP: mokocli.Universal +# INGROUP: MokoCLI.Universal # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # PATH: /.mokogitea/workflows/rc-revert.yml # VERSION: 09.23.00 -- 2.52.0 From 8c188c19f0088ea7b379be9c8de9d2c022d06fea Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 15:45:17 +0000 Subject: [PATCH 17/63] chore: sync repo-health.yml from Template-Generic [skip ci] --- .mokogitea/workflows/repo-health.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index 54d7cf9..3011fb7 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -7,7 +7,7 @@ # # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow -# INGROUP: mokocli.Validation +# INGROUP: MokoCLI.Validation # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # PATH: /.mokogitea/workflows/repo-health.yml # VERSION: 09.23.00 -- 2.52.0 From 28cbadf1dc9cfce4f08d944e85b572a86e24b66b Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 15:45:18 +0000 Subject: [PATCH 18/63] chore: sync sync-on-merge.yml from Template-Generic [skip ci] --- .mokogitea/workflows/sync-on-merge.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/sync-on-merge.yml b/.mokogitea/workflows/sync-on-merge.yml index e362224..3bf0d3b 100644 --- a/.mokogitea/workflows/sync-on-merge.yml +++ b/.mokogitea/workflows/sync-on-merge.yml @@ -12,10 +12,10 @@ jobs: runs-on: ubuntu-latest if: ${{ startsWith(github.event.repository.name, 'Template-') }} steps: - - name: Checkout mokocli + - name: Checkout MokoCLI uses: actions/checkout@v4 with: - repository: MokoConsulting/MokoCLI + repository: MokoConsulting/mokocli token: ${{ secrets.MOKOGITEA_TOKEN }} - name: Setup PHP -- 2.52.0 From 65ccb7afb6095d776ee3ace6fa8b7b7628944aa1 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 15:45:19 +0000 Subject: [PATCH 19/63] chore: sync version-set.yml from Template-Generic [skip ci] --- .mokogitea/workflows/version-set.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mokogitea/workflows/version-set.yml b/.mokogitea/workflows/version-set.yml index eed4192..d7339bd 100644 --- a/.mokogitea/workflows/version-set.yml +++ b/.mokogitea/workflows/version-set.yml @@ -4,7 +4,7 @@ # # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow.Template -# INGROUP: MokoStandards.CI +# INGROUP: MokoCLI.CI # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla # PATH: /.mokogitea/workflows/version-set.yml # VERSION: 01.00.00 -- 2.52.0 From 35230c0f55f040c962639b6274dd610848c563ec Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 15:45:20 +0000 Subject: [PATCH 20/63] chore: sync workflow-sync-trigger.yml from Template-Generic [skip ci] --- .mokogitea/workflows/workflow-sync-trigger.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.mokogitea/workflows/workflow-sync-trigger.yml b/.mokogitea/workflows/workflow-sync-trigger.yml index 69b00c4..ccd9d58 100644 --- a/.mokogitea/workflows/workflow-sync-trigger.yml +++ b/.mokogitea/workflows/workflow-sync-trigger.yml @@ -4,7 +4,7 @@ # # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow -# INGROUP: mokocli.Universal +# INGROUP: MokoCLI.Universal # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # PATH: /.mokogitea/workflows/workflow-sync-trigger.yml # VERSION: 01.01.00 @@ -48,12 +48,12 @@ jobs: echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" echo "Platform: ${PLATFORM:-all}" - - name: Clone mokocli + - name: Clone MokoCLI env: MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} run: | MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}" - git clone --depth 1 "${MOKOGITEA_URL}/MokoConsulting/MokoCLI.git" /tmp/mokocli + git clone --depth 1 "${MOKOGITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli - name: Install PHP run: | -- 2.52.0 From 36bc2319ea6fef9dc2c7a019605fd8697a3f790c Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 15:57:56 +0000 Subject: [PATCH 21/63] chore: sync standards-compliance.yml from Template-Generic [skip ci] --- .mokogitea/workflows/standards-compliance.yml | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/.mokogitea/workflows/standards-compliance.yml b/.mokogitea/workflows/standards-compliance.yml index 7048eab..c1339eb 100644 --- a/.mokogitea/workflows/standards-compliance.yml +++ b/.mokogitea/workflows/standards-compliance.yml @@ -2,11 +2,11 @@ # SPDX-License-Identifier: GPL-3.0-or-later # FILE INFORMATION # DEFGROUP: GitHub.Workflow -# INGROUP: MokoStandards.Compliance -# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# INGROUP: MokoCLI.Compliance +# REPO: https://github.com/mokoconsulting-tech/mokocli # PATH: /.mokogitea/workflows/standards-compliance.yml # VERSION: 04.06.00 -# BRIEF: MokoStandards compliance validation workflow +# BRIEF: MokoCLI compliance validation workflow # NOTE: Validates repository structure, documentation, and coding standards name: "Generic: Standards Compliance" @@ -42,7 +42,7 @@ env: WORKFLOW_VERSION: "04.04.01" FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true -# MokoStandards Policy Compliance: +# MokoCLI Policy Compliance: # - File formatting: Enforces organizational coding standards # - Reference: docs/policy/file-formatting.md @@ -252,7 +252,7 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "### ❌ Validation Failed: LICENSE File Missing" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "**Error:** LICENSE file is required for all MokoStandards-compliant repositories" >> $GITHUB_STEP_SUMMARY + echo "**Error:** LICENSE file is required for all MokoCLI-compliant repositories" >> $GITHUB_STEP_SUMMARY echo "**Action Required:** Add LICENSE file with appropriate open-source license (GPL-3.0-or-later recommended)" >> $GITHUB_STEP_SUMMARY echo "" echo "❌ ERROR: LICENSE file not found - This is a critical requirement" @@ -323,11 +323,11 @@ jobs: [ ! -d "docs" ] && echo "- Create docs directory: \`mkdir docs && echo '# Documentation' > docs/README.md\`" >> $GITHUB_STEP_SUMMARY [ ! -d ".github" ] && echo "- Create .github directory: \`mkdir -p .github/workflows\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "📚 Reference: [MokoStandards Repository Structure](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/core-structure.md)" >> $GITHUB_STEP_SUMMARY + echo "📚 Reference: [MokoCLI Repository Structure](https://github.com/mokoconsulting-tech/mokocli/tree/main/docs/policy/core-structure.md)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### ❌ Validation Failed: Required Directories Missing" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "**Status:** Repository structure does not meet MokoStandards requirements" >> $GITHUB_STEP_SUMMARY + echo "**Status:** Repository structure does not meet MokoCLI requirements" >> $GITHUB_STEP_SUMMARY echo "**Missing:** $MISSING required director(y|ies)" >> $GITHUB_STEP_SUMMARY echo "**Compliance:** $PERCENT% ($PRESENT/$TOTAL directories present)" >> $GITHUB_STEP_SUMMARY echo "" @@ -384,17 +384,17 @@ jobs: echo "### 🔴 Critical Issues: $MISSING" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Remediation Steps:**" >> $GITHUB_STEP_SUMMARY - [ ! -f "README.md" ] && echo "- Create README.md: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/docs/required/README.md)" >> $GITHUB_STEP_SUMMARY + [ ! -f "README.md" ] && echo "- Create README.md: Use [template](https://github.com/mokoconsulting-tech/mokocli/tree/main/templates/docs/required/README.md)" >> $GITHUB_STEP_SUMMARY [ ! -f "LICENSE" ] && echo "- Add LICENSE file: Choose from [OSI-approved licenses](https://opensource.org/licenses)" >> $GITHUB_STEP_SUMMARY - [ ! -f "CONTRIBUTING.md" ] && echo "- Create CONTRIBUTING.md: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/docs/required/CONTRIBUTING.md)" >> $GITHUB_STEP_SUMMARY - [ ! -f "SECURITY.md" ] && echo "- Create SECURITY.md: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/docs/required/SECURITY.md)" >> $GITHUB_STEP_SUMMARY - [ ! -f ".editorconfig" ] && echo "- Add .editorconfig: Use [template](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/templates/.editorconfig)" >> $GITHUB_STEP_SUMMARY + [ ! -f "CONTRIBUTING.md" ] && echo "- Create CONTRIBUTING.md: Use [template](https://github.com/mokoconsulting-tech/mokocli/tree/main/templates/docs/required/CONTRIBUTING.md)" >> $GITHUB_STEP_SUMMARY + [ ! -f "SECURITY.md" ] && echo "- Create SECURITY.md: Use [template](https://github.com/mokoconsulting-tech/mokocli/tree/main/templates/docs/required/SECURITY.md)" >> $GITHUB_STEP_SUMMARY + [ ! -f ".editorconfig" ] && echo "- Add .editorconfig: Use [template](https://github.com/mokoconsulting-tech/mokocli/tree/main/templates/.editorconfig)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "📚 Reference: [MokoStandards File Requirements](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/file-header-standards.md)" >> $GITHUB_STEP_SUMMARY + echo "📚 Reference: [MokoCLI File Requirements](https://github.com/mokoconsulting-tech/mokocli/tree/main/docs/policy/file-header-standards.md)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### ❌ Validation Failed: Required Files Missing" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "**Status:** Repository files do not meet MokoStandards requirements" >> $GITHUB_STEP_SUMMARY + echo "**Status:** Repository files do not meet MokoCLI requirements" >> $GITHUB_STEP_SUMMARY echo "**Missing:** $MISSING required file(s)" >> $GITHUB_STEP_SUMMARY echo "**Compliance:** $PERCENT% ($PRESENT/$TOTAL files present)" >> $GITHUB_STEP_SUMMARY echo "" @@ -437,7 +437,7 @@ jobs: echo "$TABS_IN_SPACES_FILES" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "These languages require spaces (tabs will break): YAML, Python, Haskell, F#, CoffeeScript, Nim, JSON, RST" >> $GITHUB_STEP_SUMMARY - echo "All other files (including .md, .ps1, LICENSE, etc.) may use tabs per MokoStandards policy" >> $GITHUB_STEP_SUMMARY + echo "All other files (including .md, .ps1, LICENSE, etc.) may use tabs per MokoCLI policy" >> $GITHUB_STEP_SUMMARY else echo "✅ No tabs found in files requiring spaces" >> $GITHUB_STEP_SUMMARY echo "Note: Tabs are allowed in most files (policy default). Only checked files requiring spaces." >> $GITHUB_STEP_SUMMARY @@ -483,7 +483,7 @@ jobs: echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "$CRLF_FILES" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "MokoStandards requires LF line endings" >> $GITHUB_STEP_SUMMARY + echo "MokoCLI requires LF line endings" >> $GITHUB_STEP_SUMMARY else echo "✅ Line endings are consistent (LF)" >> $GITHUB_STEP_SUMMARY fi @@ -504,13 +504,13 @@ jobs: tools: composer coverage: none - - name: Setup MokoStandards tools + - name: Setup MokoCLI tools env: GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' run: | git clone --depth 1 --branch version/04 --quiet \ - "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/mokocli.git" \ /tmp/mokostandards 2>/dev/null || true if [ -d "/tmp/mokostandards" ] && [ -f "/tmp/mokostandards/composer.json" ]; then cd /tmp/mokostandards @@ -524,7 +524,7 @@ jobs: echo "## 🔢 Version Consistency Validation" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - # Use MokoStandards tools (no Composer needed on the governed repo) + # Use MokoCLI tools (no Composer needed on the governed repo) if [ -f "/tmp/mokostandards/api/validate/check_version_consistency.php" ]; then php /tmp/mokostandards/api/validate/check_version_consistency.php --path . --verbose 2>&1 | tee /tmp/version-check.log EXIT_CODE=${PIPESTATUS[0]} @@ -532,7 +532,7 @@ jobs: php api/validate/check_version_consistency.php --path . --verbose 2>&1 | tee /tmp/version-check.log EXIT_CODE=${PIPESTATUS[0]} else - echo "⏭️ MokoStandards tools not available — skipping version check" >> $GITHUB_STEP_SUMMARY + echo "⏭️ MokoCLI tools not available — skipping version check" >> $GITHUB_STEP_SUMMARY exit 0 fi @@ -597,12 +597,12 @@ jobs: echo "⚠️ CodeQL workflow not found" >> $GITHUB_STEP_SUMMARY fi - # Check for MokoStandards-synced workflows + # Check for MokoCLI-synced workflows for wf in deploy-dev.yml deploy-demo.yml deploy-rs.yml sync-version-on-merge.yml auto-release.yml standards-compliance.yml enterprise-firewall-setup.yml; do if [ -f "$WORKFLOWS_DIR/$wf" ]; then echo "✅ ${wf}" >> $GITHUB_STEP_SUMMARY else - echo "⚠️ ${wf} not found (synced from MokoStandards)" >> $GITHUB_STEP_SUMMARY + echo "⚠️ ${wf} not found (synced from MokoCLI)" >> $GITHUB_STEP_SUMMARY fi done @@ -809,7 +809,7 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "### ❌ Validation Failed: README.md Missing" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "**Error:** README.md is required for all MokoStandards-compliant repositories" >> $GITHUB_STEP_SUMMARY + echo "**Error:** README.md is required for all MokoCLI-compliant repositories" >> $GITHUB_STEP_SUMMARY echo "**Action Required:** Create README.md with project description, setup instructions, and usage examples" >> $GITHUB_STEP_SUMMARY echo "" echo "❌ ERROR: README.md not found - This is a critical requirement" @@ -915,7 +915,7 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "### ❌ Validation Failed: CHANGELOG.md Missing" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "**Error:** CHANGELOG.md is required for all MokoStandards-compliant repositories" >> $GITHUB_STEP_SUMMARY + echo "**Error:** CHANGELOG.md is required for all MokoCLI-compliant repositories" >> $GITHUB_STEP_SUMMARY echo "**Action Required:** Create CHANGELOG.md following [Keep a Changelog](https://keepachangelog.com/) format" >> $GITHUB_STEP_SUMMARY echo "" echo "❌ ERROR: CHANGELOG.md not found - This is a critical requirement" @@ -1976,10 +1976,10 @@ jobs: if [ -f "composer.json" ]; then composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader else - echo "No composer.json — pulling MokoStandards tools" + echo "No composer.json — pulling MokoCLI tools" if [ ! -d "/tmp/mokostandards" ]; then git clone --depth 1 --branch version/04 --quiet \ - "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/mokocli.git" \ /tmp/mokostandards 2>/dev/null || true if [ -f "/tmp/mokostandards/composer.json" ]; then cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true @@ -2048,10 +2048,10 @@ jobs: if [ -f "composer.json" ]; then composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader else - echo "No composer.json — pulling MokoStandards tools" + echo "No composer.json — pulling MokoCLI tools" if [ ! -d "/tmp/mokostandards" ]; then git clone --depth 1 --branch version/04 --quiet \ - "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/mokocli.git" \ /tmp/mokostandards 2>/dev/null || true if [ -f "/tmp/mokostandards/composer.json" ]; then cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true @@ -2133,7 +2133,7 @@ jobs: # 1. Check .github/config.tf location (not root override files) echo "### Override Configuration Check" >> $GITHUB_STEP_SUMMARY - LEGACY_OVERRIDES=$(find . -maxdepth 1 -name "*override*.tf" -o -name "MokoStandards.override.tf" 2>/dev/null | wc -l || echo 0) + LEGACY_OVERRIDES=$(find . -maxdepth 1 -name "*override*.tf" -o -name "MokoCLI.override.tf" 2>/dev/null | wc -l || echo 0) if [ "$LEGACY_OVERRIDES" -gt 0 ]; then echo "⚠️ Found legacy override files in root directory" >> $GITHUB_STEP_SUMMARY echo "**Expected Location**: .github/config.tf" >> $GITHUB_STEP_SUMMARY @@ -2299,7 +2299,7 @@ jobs: - name: Generate Compliance Report run: | set -x - echo "# 📊 MokoStandards Compliance Report" >> $GITHUB_STEP_SUMMARY + echo "# 📊 MokoCLI Compliance Report" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY # Calculate overall status @@ -2486,7 +2486,7 @@ jobs: else echo "## 🎉 Excellent!" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "Your repository is **fully compliant** with MokoStandards!" >> $GITHUB_STEP_SUMMARY + echo "Your repository is **fully compliant** with MokoCLI!" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Achievements:**" >> $GITHUB_STEP_SUMMARY echo "- ✅ All required directories and files present" >> $GITHUB_STEP_SUMMARY @@ -2501,12 +2501,12 @@ jobs: echo "---" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "📚 **Resources:**" >> $GITHUB_STEP_SUMMARY - echo "- [MokoStandards Documentation](https://github.com/mokoconsulting-tech/MokoStandards)" >> $GITHUB_STEP_SUMMARY - echo "- [Repository Structure Guide](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/core-structure.md)" >> $GITHUB_STEP_SUMMARY - echo "- [Documentation Standards](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/document-formatting.md)" >> $GITHUB_STEP_SUMMARY - echo "- [Coding Standards](https://github.com/mokoconsulting-tech/MokoStandards/tree/main/docs/policy/coding-style-guide.md)" >> $GITHUB_STEP_SUMMARY + echo "- [MokoCLI Documentation](https://github.com/mokoconsulting-tech/mokocli)" >> $GITHUB_STEP_SUMMARY + echo "- [Repository Structure Guide](https://github.com/mokoconsulting-tech/mokocli/tree/main/docs/policy/core-structure.md)" >> $GITHUB_STEP_SUMMARY + echo "- [Documentation Standards](https://github.com/mokoconsulting-tech/mokocli/tree/main/docs/policy/document-formatting.md)" >> $GITHUB_STEP_SUMMARY + echo "- [Coding Standards](https://github.com/mokoconsulting-tech/mokocli/tree/main/docs/policy/coding-style-guide.md)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "_Generated by MokoStandards Compliance Workflow v${WORKFLOW_VERSION}_" >> $GITHUB_STEP_SUMMARY + echo "_Generated by MokoCLI Compliance Workflow v${WORKFLOW_VERSION}_" >> $GITHUB_STEP_SUMMARY # Create tracking issue for non-compliance if on push if [ "$COMPLIANCE_PERCENT" -lt 100 ] && [ "${{ github.event_name }}" = "push" ]; then @@ -2530,9 +2530,9 @@ jobs: echo "### ✅ Full Standards Compliance Achieved" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Overall Compliance:** 100%" >> $GITHUB_STEP_SUMMARY - echo "**Status:** Repository meets all MokoStandards requirements" >> $GITHUB_STEP_SUMMARY + echo "**Status:** Repository meets all MokoCLI requirements" >> $GITHUB_STEP_SUMMARY echo "" - echo "✅ SUCCESS: Repository is fully MokoStandards compliant" + echo "✅ SUCCESS: Repository is fully MokoCLI compliant" - name: Create or reopen tracking issue for standards violations if: failure() -- 2.52.0 From d457929a54eb97b73db733923b8feb7d28725012 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 16:21:18 +0000 Subject: [PATCH 22/63] chore: sync auto-release.yml from Template-Generic [skip ci] --- .mokogitea/workflows/auto-release.yml | 76 +++++++++++++++++++-------- 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 1b273e4..ee87c56 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -8,14 +8,13 @@ # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # PATH: /.mokogitea/workflows/auto-release.yml # VERSION: 05.01.00 -# BRIEF: Universal build & release � detects platform from the MokoGitea Metadata field (API) +# BRIEF: Universal build & release � detects platform from metadata API # # +=======================================================================+ # | UNIVERSAL BUILD & RELEASE PIPELINE | # +=======================================================================+ # | | -# | Reads platform (joomla|dolibarr|generic) from the MokoGitea | -# | Metadata field (API) to branch logic. | +# | Reads metadata API (joomla|dolibarr|generic) to branch logic. | # | | # | Platform-specific: | # | joomla: XML manifest, type-prefixed packages | @@ -116,20 +115,17 @@ jobs: SRC_SHA=$(printf '%s' "$SRC_JSON" | python3 -c "import sys, json; print(json.load(sys.stdin)['commit']['id'])" 2>/dev/null || true) [ -n "$SRC_SHA" ] || { echo "::error::Could not resolve HEAD of ${FROM}"; exit 1; } - # Point rc at the source commit. If rc already exists (a protected branch that - # cannot be deleted), force-update its ref in place instead of delete+recreate: - # deleting a protected branch fails, which then makes the recreate return HTTP 409. - if curl -sf -o /dev/null -H "$AUTH" "${API_BASE}/branches/rc"; then - echo "rc exists - force-updating to ${FROM} (${SRC_SHA})" - curl -sf -X PATCH -H "$AUTH" -H "Content-Type: application/json" \ - "${API_BASE}/git/refs/heads/rc" -d "{\"sha\":\"${SRC_SHA}\",\"force\":true}" \ - || { echo "::error::Failed to force-update rc (CI token needs force-push on the protected rc branch)"; exit 1; } - else - echo "Creating rc from ${FROM}" - curl -sf -X POST -H "$AUTH" -H "Content-Type: application/json" \ - "${API_BASE}/branches" -d "{\"new_branch_name\":\"rc\",\"old_branch_name\":\"${FROM}\"}" \ - || { echo "::error::Failed to create rc from ${FROM}"; exit 1; } - fi + # Point rc at the source commit via git push. Gitea's git/refs PATCH API + # returns HTTP 405 on ANY protected branch (force or not, even for a user in + # the force-push allowlist), so it cannot move a protected rc. git push honors + # the push + force-push allowlists and creates rc if it is absent. + PUSH_URL="https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@${MOKOGITEA_URL#https://}/${GITEA_ORG}/${GITEA_REPO}.git" + git config --global user.name "mokogitea-actions[bot]" + git config --global user.email "actions@mokoconsulting.tech" + git fetch --no-tags "$PUSH_URL" "${FROM}" + git push --force "$PUSH_URL" "FETCH_HEAD:refs/heads/rc" \ + || { echo "::error::Failed to point rc at ${FROM} (${SRC_SHA}) via git push"; exit 1; } + echo "rc set to ${FROM} (${SRC_SHA})" # Repoint the PR at rc, then delete the old source branch (non-fatal). if [ -n "$PR" ]; then @@ -378,13 +374,47 @@ jobs: if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then DATE=$(date +%Y-%m-%d) python3 -c " - import sys + import sys, re version, date = sys.argv[1], sys.argv[2] - content = open('CHANGELOG.md').read() - old = '## [Unreleased]' - new = f'## [Unreleased]\n\n## [{version}] --- {date}' - content = content if ('## [' + version + ']') in content else content.replace(old, new, 1) - open('CHANGELOG.md', 'w').write(content) + lines = open('CHANGELOG.md').read().split('\n') + h2 = re.compile(r'^##\s+\[([^\]]+)\]') + header, sections, cur = [], [], None + for ln in lines: + m = h2.match(ln) + if m: + if cur: sections.append(cur) + cur = {'label': m.group(1).strip(), 'head': ln, 'body': []} + elif cur is None: + header.append(ln) + else: + cur['body'].append(ln) + if cur: sections.append(cur) + def nonempty(b): return any(x.strip() for x in b) + def trim(b): + b = b[:] + while b and not b[0].strip(): b.pop(0) + while b and not b[-1].strip(): b.pop() + return b + unreleased, order, bykey = [], [], {} + for s in sections: + key = s['label'].lower() + if key == 'unreleased': + if nonempty(s['body']): unreleased += s['body'] + continue + if not nonempty(s['body']): continue # drop blank release sections + if key in bykey: bykey[key]['body'] += [''] + s['body'] # merge duplicate heading (never drop content) + else: bykey[key] = s; order.append(key) + rest, seen = [bykey[k] for k in order], set(order) + out = [] + htxt = '\n'.join(header).rstrip() + if htxt: out += [htxt, ''] + out += ['## [Unreleased]', ''] + promote = bool(unreleased) and version.lower() not in seen + if unreleased and not promote: out += trim(unreleased) + [''] + if promote: out += ['## [%s] --- %s' % (version, date), ''] + trim(unreleased) + [''] + for s in rest: out += [s['head'], ''] + trim(s['body']) + [''] + res = re.sub(r'\n{3,}', '\n\n', '\n'.join(out)).rstrip('\n') + '\n' + open('CHANGELOG.md', 'w').write(res) " "$VERSION" "$DATE" git add CHANGELOG.md git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true -- 2.52.0 From ae4fb4d80c43a25420239dcafe133e7d4d56a3b2 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 16:21:20 +0000 Subject: [PATCH 23/63] chore: sync cascade-dev.yml from Template-Generic [skip ci] --- .mokogitea/workflows/cascade-dev.yml | 128 ++++++++++++++++++++++----- 1 file changed, 106 insertions(+), 22 deletions(-) diff --git a/.mokogitea/workflows/cascade-dev.yml b/.mokogitea/workflows/cascade-dev.yml index 31c35ad..8ae17c4 100644 --- a/.mokogitea/workflows/cascade-dev.yml +++ b/.mokogitea/workflows/cascade-dev.yml @@ -7,8 +7,8 @@ # INGROUP: MokoCLI.Cascade # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # PATH: /.mokogitea/workflows/cascade-dev.yml -# VERSION: 02.00.00 -# BRIEF: Cascade main -> dev via PR; auto-merge only if conflict-free, else notify +# VERSION: 02.01.00 +# BRIEF: Cascade main -> dev; auto-merge clean, auto-resolve VERSION-stamp-only conflicts, else notify name: "Cascade Main -> Dev" @@ -16,6 +16,10 @@ on: push: branches: - main + # Daily safety net: catches drift even when main only received [skip ci] pushes + # (which never fire the push trigger above). Off-round minute to avoid a fleet-wide spike. + schedule: + - cron: '23 7 * * *' workflow_dispatch: permissions: @@ -33,7 +37,13 @@ jobs: name: Cascade main -> dev runs-on: ubuntu-latest steps: - - name: Open main -> dev PR (auto-merge if clean, else notify) + - name: Checkout (full history for merge/resolve) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.MOKOGITEA_TOKEN }} + + - name: Cascade main -> dev (auto-resolve version stamps, else notify) env: TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} REPO: ${{ github.repository }} @@ -41,7 +51,7 @@ jobs: set -uo pipefail API="${MOKOGITEA_URL}/api/v1/repos/${REPO}" AUTH="Authorization: token ${TOKEN}" - jqnum() { python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; } + jqget() { python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; } # 0. dev must exist if ! curl -sf -H "$AUTH" "${API}/branches/dev" >/dev/null 2>&1; then @@ -61,8 +71,8 @@ jobs: | python3 -c "import sys,json; d=json.load(sys.stdin); print(next((str(p['number']) for p in d if p.get('head',{}).get('ref')=='main'), ''))" 2>/dev/null || echo "") if [ -z "$PR" ]; then RESP=$(curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/pulls" \ - -d '{"head":"main","base":"dev","title":"chore(sync): cascade main -> dev","body":"Automated cascade of main into dev. Auto-merges only if conflict-free; otherwise left open for manual resolution."}') - PR=$(printf '%s' "$RESP" | jqnum number) + -d '{"head":"main","base":"dev","title":"chore(sync): cascade main -> dev","body":"Automated cascade of main into dev. Auto-merges when conflict-free, auto-resolves VERSION-stamp-only conflicts, otherwise left open for manual resolution."}') + PR=$(printf '%s' "$RESP" | jqget number) if [ -z "$PR" ]; then echo "::warning::Could not open cascade PR: $RESP"; exit 0 fi @@ -71,15 +81,6 @@ jobs: echo "Reusing open cascade PR #${PR}" fi - # 3. wait for MokoGitea to compute mergeability (conflict detection) - MERGEABLE="" - for _ in 1 2 3 4 5 6; do - MERGEABLE=$(curl -sf -H "$AUTH" "${API}/pulls/${PR}" | jqnum mergeable) - case "$MERGEABLE" in True|False) break ;; esac - sleep 3 - done - echo "mergeable=${MERGEABLE}" - notify() { curl -sS \ -H "Title: ${REPO}: dev cascade needs manual merge" \ @@ -90,17 +91,100 @@ jobs: "${NTFY_URL}/${NTFY_TOPIC}" || true } - # 4. auto-merge only if conflict-free; otherwise notify + # 3. wait for MokoGitea to compute mergeability (conflict detection) + MERGEABLE="" + for _ in 1 2 3 4 5 6; do + MERGEABLE=$(curl -sf -H "$AUTH" "${API}/pulls/${PR}" | jqget mergeable) + case "$MERGEABLE" in True|False) break ;; esac + sleep 3 + done + echo "mergeable=${MERGEABLE}" + + # 4a. conflict-free -> merge via API (existing behaviour) if [ "$MERGEABLE" = "True" ]; then CODE=$(curl -s -o /tmp/merge.json -w "%{http_code}" -H "$AUTH" -H "Content-Type: application/json" \ -X POST "${API}/pulls/${PR}/merge" -d '{"Do":"merge","merge_when_checks_succeed":true}') if [ "$CODE" -ge 200 ] && [ "$CODE" -lt 300 ]; then echo "Cascade PR #${PR} merged (or scheduled to merge when checks pass)." - else - echo "::warning::Auto-merge returned HTTP ${CODE}: $(cat /tmp/merge.json)" - notify "could not be auto-merged (HTTP ${CODE})." + exit 0 fi - else - echo "::warning::Cascade PR #${PR} has conflicts (mergeable=${MERGEABLE}); sending notification." - notify "has conflicts and cannot be merged automatically." + echo "::warning::Auto-merge returned HTTP ${CODE}: $(cat /tmp/merge.json)" + notify "could not be auto-merged (HTTP ${CODE})." + exit 0 fi + + # 4b. conflicts -> try to auto-resolve if they are ONLY VERSION-stamp lines. + echo "PR not cleanly mergeable; checking whether conflicts are VERSION-stamp-only..." + git config user.name "MokoGitea Cascade" + git config user.email "actions@mokoconsulting.tech" + git fetch --quiet origin main dev + git checkout -B dev origin/dev + + if git merge --no-ff --no-commit origin/main >/dev/null 2>&1; then + # Became clean at git level (e.g. mergeability was still computing) -> commit + push. + git commit -m "chore(sync): cascade main -> dev [skip ci]" >/dev/null + git push origin dev + echo "Cascade merged cleanly at git level and pushed to dev." + exit 0 + fi + + CONFLICTS=$(git diff --name-only --diff-filter=U) + echo "Conflicted files:"; echo "${CONFLICTS}" + + # A conflict is "stamp-only" when every line inside every conflict block matches + # a version-stamp pattern (VERSION: header, element, or CHANGELOG title). + is_stamp_only() { + awk ' + /^<<<<<<< / { inc=1; next } + inc && /^=======$/ { next } + /^>>>>>>> / { inc=0; next } + inc { if ($0 !~ /(VERSION:||# Changelog)/) { bad=1 } } + END { exit(bad ? 1 : 0) } + ' "$1" + } + # Resolve a stamp-only file by keeping dev (ours) for the conflicting lines, + # preserving all auto-merged content around them. + keep_ours() { + awk ' + /^<<<<<<< / { inc=1; side="ours"; next } + inc && /^=======$/ { side="theirs"; next } + /^>>>>>>> / { inc=0; next } + { if (!inc) { print; next } if (side=="ours") print } + ' "$1" > "$1.resolved" && mv "$1.resolved" "$1" + } + + ALL_STAMP=1 + for f in ${CONFLICTS}; do + if ! is_stamp_only "$f"; then + echo "::notice::$f has non-stamp conflicts -> manual resolution required." + ALL_STAMP=0; break + fi + done + + if [ "$ALL_STAMP" != "1" ]; then + git merge --abort || true + notify "has non-version-stamp conflicts and cannot be auto-resolved." + exit 0 + fi + + echo "All conflicts are VERSION-stamp-only; resolving in favour of dev." + for f in ${CONFLICTS}; do + keep_ours "$f" + git add "$f" + done + + # Best-effort: normalise stamps to dev's version if mokocli is available. + if [ -f /opt/mokocli/cli/version_check.php ]; then + php /opt/mokocli/cli/version_check.php --fix || true + git add -A + fi + + git commit -m "chore(sync): cascade main -> dev (auto-resolved version stamps) [skip ci]" >/dev/null + git push origin dev + echo "Cascade auto-resolved and pushed to dev." + + # Close the now-redundant PR (its changes are in dev) with an explanatory comment. + curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/issues/${PR}/comments" \ + -d '{"body":"Auto-resolved VERSION-stamp-only conflicts and pushed the merge to dev. Closing."}' >/dev/null || true + curl -s -H "$AUTH" -H "Content-Type: application/json" -X PATCH "${API}/pulls/${PR}" \ + -d '{"state":"closed"}' >/dev/null || true -- 2.52.0 From ec8a8ec26fad8acda969e3c3e1d999bc0bc04854 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 16:21:22 +0000 Subject: [PATCH 24/63] chore: sync pr-check.yml from Template-Generic [skip ci] --- .mokogitea/workflows/pr-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index 8fb052a..6da762a 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -224,7 +224,7 @@ jobs: - name: Detect platform id: platform run: | - # Platform comes from the MokoGitea metadata API (public GET); manifest.xml is no longer used. + # Platform comes from the MokoGitea metadata API (public GET). API="${GITHUB_SERVER_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${GITHUB_REPOSITORY}/metadata" PLATFORM="$(curl -sf "$API" 2>/dev/null | python3 -c "import sys, json; print(json.load(sys.stdin).get('platform') or '')" 2>/dev/null || true)" [ -z "$PLATFORM" ] && PLATFORM="generic" -- 2.52.0 From ddba676a24ea683772a57eba7b08085cd5c83c2c Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 19:51:09 +0000 Subject: [PATCH 25/63] chore: sync standards-compliance.yml from Template-Generic [skip ci] --- .mokogitea/workflows/standards-compliance.yml | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/.mokogitea/workflows/standards-compliance.yml b/.mokogitea/workflows/standards-compliance.yml index c1339eb..2a7abc3 100644 --- a/.mokogitea/workflows/standards-compliance.yml +++ b/.mokogitea/workflows/standards-compliance.yml @@ -291,7 +291,7 @@ jobs: MISSING=0 PRESENT=0 - TOTAL=2 + TOTAL=1 echo "### Required Directories" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY @@ -299,7 +299,7 @@ jobs: echo "|-----------|--------|-------|------|-------|" >> $GITHUB_STEP_SUMMARY # Check required directories - for dir in docs .github; do + for dir in .mokogitea; do if [ -d "$dir" ]; then FILE_COUNT=$(find "$dir" -type f 2>/dev/null | wc -l) DIR_SIZE=$(du -sh "$dir" 2>/dev/null | cut -f1) @@ -320,8 +320,7 @@ jobs: echo "### 🔴 Critical Issues: $MISSING" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Remediation Steps:**" >> $GITHUB_STEP_SUMMARY - [ ! -d "docs" ] && echo "- Create docs directory: \`mkdir docs && echo '# Documentation' > docs/README.md\`" >> $GITHUB_STEP_SUMMARY - [ ! -d ".github" ] && echo "- Create .github directory: \`mkdir -p .github/workflows\`" >> $GITHUB_STEP_SUMMARY + [ ! -d ".mokogitea" ] && echo "- Create .mokogitea directory: \`mkdir -p .mokogitea/workflows\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "📚 Reference: [MokoCLI Repository Structure](https://github.com/mokoconsulting-tech/mokocli/tree/main/docs/policy/core-structure.md)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY @@ -564,17 +563,17 @@ jobs: set -x echo "### GitHub Actions Workflows" >> $GITHUB_STEP_SUMMARY - WORKFLOWS_DIR=".github/workflows" + WORKFLOWS_DIR=".mokogitea/workflows" if [ ! -d "$WORKFLOWS_DIR" ]; then echo "❌ No workflows directory found" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### ❌ Validation Failed: Workflows Directory Missing" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "**Error:** .github/workflows directory is required for CI/CD automation" >> $GITHUB_STEP_SUMMARY - echo "**Action Required:** Create .github/workflows directory and add GitHub Actions workflows" >> $GITHUB_STEP_SUMMARY + echo "**Error:** .mokogitea/workflows directory is required for CI/CD automation" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Create .mokogitea/workflows directory and add GitHub Actions workflows" >> $GITHUB_STEP_SUMMARY echo "" - echo "❌ ERROR: .github/workflows directory not found" + echo "❌ ERROR: .mokogitea/workflows directory not found" exit 1 fi @@ -613,7 +612,7 @@ jobs: echo "### Workflow YAML Syntax" >> $GITHUB_STEP_SUMMARY INVALID=0 - for workflow in $(find .github/workflows -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null); do + for workflow in $(find .mokogitea/workflows -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null); do if [ -f "$workflow" ]; then if python3 -c "import yaml, sys; yaml.safe_load(open(sys.argv[1]))" "$workflow" 2>/dev/null; then echo "✅ $(basename $workflow)" >> $GITHUB_STEP_SUMMARY @@ -630,7 +629,7 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "**Error:** $INVALID workflow file(s) have invalid YAML syntax" >> $GITHUB_STEP_SUMMARY echo "**Action Required:** Fix YAML syntax errors in the marked workflow files" >> $GITHUB_STEP_SUMMARY - echo "**Tool:** Run \`python3 -c \"import yaml; yaml.safe_load(open('.github/workflows/FILE.yml'))\"\` locally" >> $GITHUB_STEP_SUMMARY + echo "**Tool:** Run \`python3 -c \"import yaml; yaml.safe_load(open('.mokogitea/workflows/FILE.yml'))\"\` locally" >> $GITHUB_STEP_SUMMARY echo "" echo "❌ ERROR: $INVALID workflow file(s) with invalid YAML syntax" exit 1 @@ -642,7 +641,7 @@ jobs: echo "✅ SUCCESS: All workflow files passed YAML validation" - name: Validate CodeQL Configuration - if: hashFiles('.github/workflows/codeql-analysis.yml') != '' + if: hashFiles('.mokogitea/workflows/codeql-analysis.yml') != '' run: | set -e echo "" >> $GITHUB_STEP_SUMMARY @@ -650,7 +649,7 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY # Inline validation (rewritten from Python to bash for PHP-only architecture) - CODEQL_FILE=".github/workflows/codeql-analysis.yml" + CODEQL_FILE=".mokogitea/workflows/codeql-analysis.yml" if [ ! -f "$CODEQL_FILE" ]; then echo "⚠️ CodeQL workflow file not found" >> $GITHUB_STEP_SUMMARY @@ -1187,7 +1186,7 @@ jobs: fi # Run yamllint and count line-length warnings - YAML_OUTPUT=$(yamllint .github/workflows/*.yml 2>&1 | grep "line too long" || true) + YAML_OUTPUT=$(yamllint .mokogitea/workflows/*.yml 2>&1 | grep "line too long" || true) if [ -n "$YAML_OUTPUT" ]; then YAML_VIOLATIONS=$(echo "$YAML_OUTPUT" | wc -l) echo "⚠️ Found $YAML_VIOLATIONS lines exceeding 180 characters in YAML files" >> $GITHUB_STEP_SUMMARY @@ -2131,17 +2130,17 @@ jobs: WARNINGS=0 ERRORS=0 - # 1. Check .github/config.tf location (not root override files) + # 1. Check .mokogitea/config.tf location (not root override files) echo "### Override Configuration Check" >> $GITHUB_STEP_SUMMARY LEGACY_OVERRIDES=$(find . -maxdepth 1 -name "*override*.tf" -o -name "MokoCLI.override.tf" 2>/dev/null | wc -l || echo 0) if [ "$LEGACY_OVERRIDES" -gt 0 ]; then echo "⚠️ Found legacy override files in root directory" >> $GITHUB_STEP_SUMMARY - echo "**Expected Location**: .github/config.tf" >> $GITHUB_STEP_SUMMARY + echo "**Expected Location**: .mokogitea/config.tf" >> $GITHUB_STEP_SUMMARY echo "**Legacy files found**: $LEGACY_OVERRIDES" >> $GITHUB_STEP_SUMMARY WARNINGS=$((WARNINGS + 1)) else - if [ -f ".github/config.tf" ]; then - echo "✅ Override configuration in correct location (.github/config.tf)" >> $GITHUB_STEP_SUMMARY + if [ -f ".mokogitea/config.tf" ]; then + echo "✅ Override configuration in correct location (.mokogitea/config.tf)" >> $GITHUB_STEP_SUMMARY else echo "ℹ️ No override configuration found" >> $GITHUB_STEP_SUMMARY fi -- 2.52.0 From 9d82799277b937d978a950fa2e0d2516a5e21256 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 20:44:15 +0000 Subject: [PATCH 26/63] chore(ci): remove template-only sync workflows from root sync-on-merge.yml and workflow-sync-trigger.yml are template-only (gated if: Template-*), dead no-ops in this child repo, inherited at scaffold time. sync-on-merge.yml is also broken. The real sync lives in the templates' workflows/custom/. --- .mokogitea/workflows/sync-on-merge.yml | 32 -------- .../workflows/workflow-sync-trigger.yml | 82 ------------------- 2 files changed, 114 deletions(-) delete mode 100644 .mokogitea/workflows/sync-on-merge.yml delete mode 100644 .mokogitea/workflows/workflow-sync-trigger.yml diff --git a/.mokogitea/workflows/sync-on-merge.yml b/.mokogitea/workflows/sync-on-merge.yml deleted file mode 100644 index 3bf0d3b..0000000 --- a/.mokogitea/workflows/sync-on-merge.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Sync Workflows to Repos - -on: - push: - branches: - - main - paths: - - '.mokogitea/workflows/**' - -jobs: - sync: - runs-on: ubuntu-latest - if: ${{ startsWith(github.event.repository.name, 'Template-') }} - steps: - - name: Checkout MokoCLI - uses: actions/checkout@v4 - with: - repository: MokoConsulting/mokocli - token: ${{ secrets.MOKOGITEA_TOKEN }} - - - name: Setup PHP - uses: https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/raw/branch/main/actions/setup-php@v1 - with: - php-version: '8.1' - - - name: Install dependencies - run: composer install --no-dev --no-interaction - - - name: Sync workflows to generic repos - run: php automation/bulk_sync.php --platform generic --org MokoConsulting --workflows-only --auto-merge --token "${{ secrets.MOKOGITEA_TOKEN }}" - env: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} diff --git a/.mokogitea/workflows/workflow-sync-trigger.yml b/.mokogitea/workflows/workflow-sync-trigger.yml deleted file mode 100644 index ccd9d58..0000000 --- a/.mokogitea/workflows/workflow-sync-trigger.yml +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: MokoGitea.Workflow -# INGROUP: MokoCLI.Universal -# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic -# PATH: /.mokogitea/workflows/workflow-sync-trigger.yml -# VERSION: 01.01.00 -# BRIEF: Trigger workflow sync to live repos when a PR is merged to main - -name: "Universal: Workflow Sync Trigger" - -on: - workflow_dispatch: - pull_request: - types: [closed] - branches: - - main - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - sync: - name: Sync workflows to live repos - runs-on: ubuntu-latest - if: >- - startsWith(github.event.repository.name, 'Template-') && - (github.event_name == 'workflow_dispatch' || - (github.event.pull_request.merged == true && - !contains(github.event.pull_request.title, '[skip sync]'))) - - steps: - - name: Determine platform from repo name - id: platform - run: | - REPO="${{ github.event.repository.name }}" - case "$REPO" in - Template-Joomla) PLATFORM="joomla" ;; - Template-Dolibarr) PLATFORM="dolibarr" ;; - Template-Go) PLATFORM="go" ;; - Template-MCP) PLATFORM="mcp" ;; - Template-Generic) PLATFORM="" ;; - *) PLATFORM="" ;; - esac - echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" - echo "Platform: ${PLATFORM:-all}" - - - name: Clone MokoCLI - env: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - run: | - MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}" - git clone --depth 1 "${MOKOGITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli - - - name: Install PHP - run: | - if ! command -v php &> /dev/null; then - apt-get update -qq && apt-get install -y -qq php-cli php-json php-curl > /dev/null 2>&1 - fi - - - name: Install dependencies - run: | - cd /tmp/mokocli - composer install --no-dev --no-interaction --quiet 2>/dev/null || true - - - name: Run workflow sync - env: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - run: | - ARGS="--token ${MOKOGITEA_TOKEN}" - ARGS="${ARGS} --org ${{ vars.GITEA_ORG || github.repository_owner }}" - ARGS="${ARGS} --phase repos" - - PLATFORM="${{ steps.platform.outputs.platform }}" - if [ -n "$PLATFORM" ]; then - ARGS="${ARGS} --platform-filter ${PLATFORM}" - fi - - php /tmp/mokocli/cli/workflow_sync.php ${ARGS} -- 2.52.0 From 273fcfdb7d537ef9f160e87dc978d6645f5c77b6 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 04:39:45 +0000 Subject: [PATCH 27/63] chore: sync auto-bump.yml from Template-Joomla [skip ci] --- .mokogitea/workflows/auto-bump.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml index b58ea7d..a47661a 100644 --- a/.mokogitea/workflows/auto-bump.yml +++ b/.mokogitea/workflows/auto-bump.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow # INGROUP: MokoCLI.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # PATH: /.mokogitea/workflows/auto-bump.yml # VERSION: 09.02.00 # BRIEF: Auto patch-bump version on every push to dev (skips merge commits) @@ -34,8 +34,7 @@ jobs: if: >- !contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[skip bump]') && - !startsWith(github.event.head_commit.message, 'Merge pull request') && - !startsWith(github.event.repository.name, 'Template-') + !startsWith(github.event.head_commit.message, 'Merge pull request') steps: - name: Checkout -- 2.52.0 From 9fb0b150a643a6cf3d0f3611efe6fbf41ddc6426 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 04:39:47 +0000 Subject: [PATCH 28/63] chore: sync auto-release.yml from Template-Joomla [skip ci] --- .mokogitea/workflows/auto-release.yml | 89 +++++---------------------- 1 file changed, 15 insertions(+), 74 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index ee87c56..ce7bc6c 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -5,16 +5,16 @@ # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow # INGROUP: MokoCLI.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic -# PATH: /.mokogitea/workflows/auto-release.yml +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# PATH: /templates/workflows/universal/auto-release.yml.template # VERSION: 05.01.00 -# BRIEF: Universal build & release � detects platform from metadata API +# BRIEF: Universal build & release � detects platform from MokoGitea repo metadata (API) # # +=======================================================================+ # | UNIVERSAL BUILD & RELEASE PIPELINE | # +=======================================================================+ # | | -# | Reads metadata API (joomla|dolibarr|generic) to branch logic. | +# | Reads MokoGitea repo metadata (joomla|dolibarr|generic) to branch logic. | # | | # | Platform-specific: | # | joomla: XML manifest, type-prefixed packages | @@ -104,36 +104,11 @@ jobs: - name: Rename branch to rc run: | - API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - AUTH="Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" - FROM="${{ github.event.pull_request.head.ref || 'dev' }}" - PR="${{ github.event.pull_request.number }}" - - # Resolve the source branch HEAD commit. - SRC_JSON=$(curl -sf -H "$AUTH" "${API_BASE}/branches/${FROM}") \ - || { echo "::error::Source branch ${FROM} not found"; exit 1; } - SRC_SHA=$(printf '%s' "$SRC_JSON" | python3 -c "import sys, json; print(json.load(sys.stdin)['commit']['id'])" 2>/dev/null || true) - [ -n "$SRC_SHA" ] || { echo "::error::Could not resolve HEAD of ${FROM}"; exit 1; } - - # Point rc at the source commit via git push. Gitea's git/refs PATCH API - # returns HTTP 405 on ANY protected branch (force or not, even for a user in - # the force-push allowlist), so it cannot move a protected rc. git push honors - # the push + force-push allowlists and creates rc if it is absent. - PUSH_URL="https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@${MOKOGITEA_URL#https://}/${GITEA_ORG}/${GITEA_REPO}.git" - git config --global user.name "mokogitea-actions[bot]" - git config --global user.email "actions@mokoconsulting.tech" - git fetch --no-tags "$PUSH_URL" "${FROM}" - git push --force "$PUSH_URL" "FETCH_HEAD:refs/heads/rc" \ - || { echo "::error::Failed to point rc at ${FROM} (${SRC_SHA}) via git push"; exit 1; } - echo "rc set to ${FROM} (${SRC_SHA})" - - # Repoint the PR at rc, then delete the old source branch (non-fatal). - if [ -n "$PR" ]; then - curl -s -X PATCH -H "$AUTH" -H "Content-Type: application/json" \ - "${API_BASE}/pulls/${PR}" -d '{"head":"rc"}' >/dev/null || true - fi - curl -s -X DELETE -H "$AUTH" "${API_BASE}/branches/${FROM}" >/dev/null || true - echo "Renamed ${FROM} -> rc" + php ${MOKO_CLI}/branch_rename.php \ + --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --api-base "${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ + --pr "${{ github.event.pull_request.number }}" - name: Checkout rc and configure git run: | @@ -374,47 +349,13 @@ jobs: if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then DATE=$(date +%Y-%m-%d) python3 -c " - import sys, re + import sys version, date = sys.argv[1], sys.argv[2] - lines = open('CHANGELOG.md').read().split('\n') - h2 = re.compile(r'^##\s+\[([^\]]+)\]') - header, sections, cur = [], [], None - for ln in lines: - m = h2.match(ln) - if m: - if cur: sections.append(cur) - cur = {'label': m.group(1).strip(), 'head': ln, 'body': []} - elif cur is None: - header.append(ln) - else: - cur['body'].append(ln) - if cur: sections.append(cur) - def nonempty(b): return any(x.strip() for x in b) - def trim(b): - b = b[:] - while b and not b[0].strip(): b.pop(0) - while b and not b[-1].strip(): b.pop() - return b - unreleased, order, bykey = [], [], {} - for s in sections: - key = s['label'].lower() - if key == 'unreleased': - if nonempty(s['body']): unreleased += s['body'] - continue - if not nonempty(s['body']): continue # drop blank release sections - if key in bykey: bykey[key]['body'] += [''] + s['body'] # merge duplicate heading (never drop content) - else: bykey[key] = s; order.append(key) - rest, seen = [bykey[k] for k in order], set(order) - out = [] - htxt = '\n'.join(header).rstrip() - if htxt: out += [htxt, ''] - out += ['## [Unreleased]', ''] - promote = bool(unreleased) and version.lower() not in seen - if unreleased and not promote: out += trim(unreleased) + [''] - if promote: out += ['## [%s] --- %s' % (version, date), ''] + trim(unreleased) + [''] - for s in rest: out += [s['head'], ''] + trim(s['body']) + [''] - res = re.sub(r'\n{3,}', '\n\n', '\n'.join(out)).rstrip('\n') + '\n' - open('CHANGELOG.md', 'w').write(res) + content = open('CHANGELOG.md').read() + old = '## [Unreleased]' + new = f'## [Unreleased]\n\n## [{version}] --- {date}' + content = content.replace(old, new, 1) + open('CHANGELOG.md', 'w').write(content) " "$VERSION" "$DATE" git add CHANGELOG.md git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true -- 2.52.0 From e43a322ec0260f40bdfc28b0fc5561ed9015f61a Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 04:39:48 +0000 Subject: [PATCH 29/63] chore: sync branch-cleanup.yml from Template-Joomla [skip ci] --- .mokogitea/workflows/branch-cleanup.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mokogitea/workflows/branch-cleanup.yml b/.mokogitea/workflows/branch-cleanup.yml index 909e706..aca4a9d 100644 --- a/.mokogitea/workflows/branch-cleanup.yml +++ b/.mokogitea/workflows/branch-cleanup.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow # INGROUP: MokoCLI.Universal -# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # PATH: /.mokogitea/workflows/branch-cleanup.yml # VERSION: 01.00.00 # BRIEF: Delete feature branches after PR merge -- 2.52.0 From a6232e9800b14d7d53de2e88e62606bc138aa243 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 04:39:49 +0000 Subject: [PATCH 30/63] chore: sync cascade-dev.yml from Template-Joomla [skip ci] --- .mokogitea/workflows/cascade-dev.yml | 207 ++++++--------------------- 1 file changed, 41 insertions(+), 166 deletions(-) diff --git a/.mokogitea/workflows/cascade-dev.yml b/.mokogitea/workflows/cascade-dev.yml index 8ae17c4..1b9f8a6 100644 --- a/.mokogitea/workflows/cascade-dev.yml +++ b/.mokogitea/workflows/cascade-dev.yml @@ -1,190 +1,65 @@ # Copyright (C) 2026 Moko Consulting -# # SPDX-License-Identifier: GPL-3.0-or-later -# # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow -# INGROUP: MokoCLI.Cascade -# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic -# PATH: /.mokogitea/workflows/cascade-dev.yml -# VERSION: 02.01.00 -# BRIEF: Cascade main -> dev; auto-merge clean, auto-resolve VERSION-stamp-only conflicts, else notify +# INGROUP: MokoCLI.Release +# BRIEF: Reset dev to main after each release (cascade). Moved out of auto-release Step 11. +# +# +========================================================================+ +# | CASCADE MAIN -> DEV | +# +========================================================================+ +# | dev mirrors main and is reset on every release. | +# | delete+recreate cannot run against a protected branch, so this | +# | force-pushes main -> dev. The automation identity is force-push | +# | allowlisted on dev via branch-protection.yml. | +# | Runs AFTER the release workflow completes, so dev picks up the | +# | fully version-bumped + changelog-promoted main (not the pre-bump | +# | state). Force-push (not merge) => no version-file conflicts. | +# +========================================================================+ name: "Cascade Main -> Dev" on: - push: - branches: - - main - # Daily safety net: catches drift even when main only received [skip ci] pushes - # (which never fire the push trigger above). Off-round minute to avoid a fleet-wide spike. - schedule: - - cron: '23 7 * * *' + workflow_run: + workflows: ["Universal: Build & Release"] + types: [completed] workflow_dispatch: +concurrency: + group: cascade-dev-${{ github.repository }} + cancel-in-progress: true + permissions: contents: write - pull-requests: write - -env: - MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - # ntfy destination is configured via repo or org variables (org vars are inherited). - NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} - NTFY_TOPIC: ${{ vars.CASCADE_NTFY_TOPIC || vars.NTFY_TOPIC || 'gitea-releases' }} jobs: cascade: - name: Cascade main -> dev + name: Reset dev to main + if: >- + !startsWith(github.event.repository.name, 'Template-') && + (github.event_name == 'workflow_dispatch' || + github.event.workflow_run.conclusion == 'success') runs-on: ubuntu-latest steps: - - name: Checkout (full history for merge/resolve) - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.MOKOGITEA_TOKEN }} - - - name: Cascade main -> dev (auto-resolve version stamps, else notify) + - name: Force dev to main env: TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + SERVER: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} REPO: ${{ github.repository }} run: | - set -uo pipefail - API="${MOKOGITEA_URL}/api/v1/repos/${REPO}" - AUTH="Authorization: token ${TOKEN}" - jqget() { python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; } + set -euo pipefail + git init -q sync && cd sync + git config user.email "mokogitea-actions[bot]@mokoconsulting.tech" + git config user.name "mokogitea-actions[bot]" + git remote add origin "https://x-access-token:${TOKEN}@${SERVER#https://}/${REPO}.git" + git fetch -q --depth=1 origin main - # 0. dev must exist - if ! curl -sf -H "$AUTH" "${API}/branches/dev" >/dev/null 2>&1; then - echo "No dev branch - nothing to cascade."; exit 0 - fi - - # 1. is main ahead of dev? - AHEAD=$(curl -sf -H "$AUTH" "${API}/compare/dev...main" \ - | python3 -c "import sys,json; print(json.load(sys.stdin).get('total_commits',0))" 2>/dev/null || echo 0) - if [ "${AHEAD:-0}" -eq 0 ]; then - echo "dev already up to date with main."; exit 0 - fi - echo "main is ${AHEAD} commit(s) ahead of dev." - - # 2. reuse an open main->dev PR, else create one - PR=$(curl -sf -H "$AUTH" "${API}/pulls?state=open&base=dev" \ - | python3 -c "import sys,json; d=json.load(sys.stdin); print(next((str(p['number']) for p in d if p.get('head',{}).get('ref')=='main'), ''))" 2>/dev/null || echo "") - if [ -z "$PR" ]; then - RESP=$(curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/pulls" \ - -d '{"head":"main","base":"dev","title":"chore(sync): cascade main -> dev","body":"Automated cascade of main into dev. Auto-merges when conflict-free, auto-resolves VERSION-stamp-only conflicts, otherwise left open for manual resolution."}') - PR=$(printf '%s' "$RESP" | jqget number) - if [ -z "$PR" ]; then - echo "::warning::Could not open cascade PR: $RESP"; exit 0 - fi - echo "Opened cascade PR #${PR}" + if git ls-remote --exit-code --heads origin dev >/dev/null 2>&1; then + # dev exists -> force it to match main (dev is a mirror of main) + git push --force origin FETCH_HEAD:refs/heads/dev + echo "dev reset to main" >> "$GITHUB_STEP_SUMMARY" else - echo "Reusing open cascade PR #${PR}" + # dev missing (e.g. renamed to rc mid-cycle) -> recreate from main + git push origin FETCH_HEAD:refs/heads/dev + echo "dev created from main" >> "$GITHUB_STEP_SUMMARY" fi - - notify() { - curl -sS \ - -H "Title: ${REPO}: dev cascade needs manual merge" \ - -H "Tags: warning,twisted_rightwards_arrows" \ - -H "Priority: high" \ - -H "Click: ${MOKOGITEA_URL}/${REPO}/pulls/${PR}" \ - -d "main -> dev cascade PR #${PR} $1 It was NOT auto-merged; resolve it manually." \ - "${NTFY_URL}/${NTFY_TOPIC}" || true - } - - # 3. wait for MokoGitea to compute mergeability (conflict detection) - MERGEABLE="" - for _ in 1 2 3 4 5 6; do - MERGEABLE=$(curl -sf -H "$AUTH" "${API}/pulls/${PR}" | jqget mergeable) - case "$MERGEABLE" in True|False) break ;; esac - sleep 3 - done - echo "mergeable=${MERGEABLE}" - - # 4a. conflict-free -> merge via API (existing behaviour) - if [ "$MERGEABLE" = "True" ]; then - CODE=$(curl -s -o /tmp/merge.json -w "%{http_code}" -H "$AUTH" -H "Content-Type: application/json" \ - -X POST "${API}/pulls/${PR}/merge" -d '{"Do":"merge","merge_when_checks_succeed":true}') - if [ "$CODE" -ge 200 ] && [ "$CODE" -lt 300 ]; then - echo "Cascade PR #${PR} merged (or scheduled to merge when checks pass)." - exit 0 - fi - echo "::warning::Auto-merge returned HTTP ${CODE}: $(cat /tmp/merge.json)" - notify "could not be auto-merged (HTTP ${CODE})." - exit 0 - fi - - # 4b. conflicts -> try to auto-resolve if they are ONLY VERSION-stamp lines. - echo "PR not cleanly mergeable; checking whether conflicts are VERSION-stamp-only..." - git config user.name "MokoGitea Cascade" - git config user.email "actions@mokoconsulting.tech" - git fetch --quiet origin main dev - git checkout -B dev origin/dev - - if git merge --no-ff --no-commit origin/main >/dev/null 2>&1; then - # Became clean at git level (e.g. mergeability was still computing) -> commit + push. - git commit -m "chore(sync): cascade main -> dev [skip ci]" >/dev/null - git push origin dev - echo "Cascade merged cleanly at git level and pushed to dev." - exit 0 - fi - - CONFLICTS=$(git diff --name-only --diff-filter=U) - echo "Conflicted files:"; echo "${CONFLICTS}" - - # A conflict is "stamp-only" when every line inside every conflict block matches - # a version-stamp pattern (VERSION: header, element, or CHANGELOG title). - is_stamp_only() { - awk ' - /^<<<<<<< / { inc=1; next } - inc && /^=======$/ { next } - /^>>>>>>> / { inc=0; next } - inc { if ($0 !~ /(VERSION:||# Changelog)/) { bad=1 } } - END { exit(bad ? 1 : 0) } - ' "$1" - } - # Resolve a stamp-only file by keeping dev (ours) for the conflicting lines, - # preserving all auto-merged content around them. - keep_ours() { - awk ' - /^<<<<<<< / { inc=1; side="ours"; next } - inc && /^=======$/ { side="theirs"; next } - /^>>>>>>> / { inc=0; next } - { if (!inc) { print; next } if (side=="ours") print } - ' "$1" > "$1.resolved" && mv "$1.resolved" "$1" - } - - ALL_STAMP=1 - for f in ${CONFLICTS}; do - if ! is_stamp_only "$f"; then - echo "::notice::$f has non-stamp conflicts -> manual resolution required." - ALL_STAMP=0; break - fi - done - - if [ "$ALL_STAMP" != "1" ]; then - git merge --abort || true - notify "has non-version-stamp conflicts and cannot be auto-resolved." - exit 0 - fi - - echo "All conflicts are VERSION-stamp-only; resolving in favour of dev." - for f in ${CONFLICTS}; do - keep_ours "$f" - git add "$f" - done - - # Best-effort: normalise stamps to dev's version if mokocli is available. - if [ -f /opt/mokocli/cli/version_check.php ]; then - php /opt/mokocli/cli/version_check.php --fix || true - git add -A - fi - - git commit -m "chore(sync): cascade main -> dev (auto-resolved version stamps) [skip ci]" >/dev/null - git push origin dev - echo "Cascade auto-resolved and pushed to dev." - - # Close the now-redundant PR (its changes are in dev) with an explanatory comment. - curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/issues/${PR}/comments" \ - -d '{"body":"Auto-resolved VERSION-stamp-only conflicts and pushed the merge to dev. Closing."}' >/dev/null || true - curl -s -H "$AUTH" -H "Content-Type: application/json" -X PATCH "${API}/pulls/${PR}" \ - -d '{"state":"closed"}' >/dev/null || true -- 2.52.0 From 11f1a7a2932b7e7d4aa2a168ce2e04837e9ba322 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 04:39:50 +0000 Subject: [PATCH 31/63] chore: sync ci-generic.yml from Template-Joomla [skip ci] --- .mokogitea/workflows/ci-generic.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.mokogitea/workflows/ci-generic.yml b/.mokogitea/workflows/ci-generic.yml index 869087e..330e6f9 100644 --- a/.mokogitea/workflows/ci-generic.yml +++ b/.mokogitea/workflows/ci-generic.yml @@ -131,11 +131,10 @@ jobs: test: name: Tests runs-on: ubuntu-latest - # Independent job (no `needs: lint`): the MokoGitea Actions scheduler does not - # offer the dependent 2nd job of a needs-chain to runners, so it stalls in - # "waiting" and is reaped by ABANDONED_JOB_TIMEOUT. Guard template repos - # directly (same condition lint uses) instead of gating on lint's result. - if: ${{ !startsWith(github.event.repository.name, 'Template-') }} + needs: lint + # Run only when lint succeeded; always() forces evaluation so a skipped + # lint (e.g. template repos) skips this job cleanly instead of hanging. + if: ${{ always() && needs.lint.result == 'success' }} steps: - name: Checkout -- 2.52.0 From 5f101b1a00671dc06996693a804f344383839359 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 04:39:51 +0000 Subject: [PATCH 32/63] chore: sync ci-issue-reporter.yml from Template-Joomla [skip ci] --- .mokogitea/workflows/ci-issue-reporter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mokogitea/workflows/ci-issue-reporter.yml b/.mokogitea/workflows/ci-issue-reporter.yml index 600d8c1..335e92f 100644 --- a/.mokogitea/workflows/ci-issue-reporter.yml +++ b/.mokogitea/workflows/ci-issue-reporter.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow # INGROUP: MokoCLI.Universal -# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # PATH: /.mokogitea/workflows/ci-issue-reporter.yml # VERSION: 01.00.00 # BRIEF: Reusable workflow — creates/updates a MokoGitea issue when a CI gate fails. -- 2.52.0 From 5a5406748734190d53bbddeb031be52ee1c2dc1d Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 04:39:52 +0000 Subject: [PATCH 33/63] chore: sync ci-joomla.yml from Template-Joomla [skip ci] --- .mokogitea/workflows/ci-joomla.yml | 1361 +++++++++++++++++++++++++++- 1 file changed, 1328 insertions(+), 33 deletions(-) diff --git a/.mokogitea/workflows/ci-joomla.yml b/.mokogitea/workflows/ci-joomla.yml index 727f661..e61ed5b 100644 --- a/.mokogitea/workflows/ci-joomla.yml +++ b/.mokogitea/workflows/ci-joomla.yml @@ -5,9 +5,9 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: Gitea.Workflow.Template -# INGROUP: MokoStandards.CI -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# DEFGROUP: MokoGitea.Workflow.Template +# INGROUP: MokoCLI.CI +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # PATH: /templates/workflows/joomla/ci-joomla.yml.template # VERSION: 04.06.00 # BRIEF: CI workflow for Joomla extensions — lint, validate, test @@ -18,6 +18,7 @@ on: pull_request: branches: - main + - dev - 'dev/**' workflow_dispatch: @@ -32,6 +33,7 @@ jobs: lint-and-validate: name: Lint & Validate runs-on: ubuntu-latest + if: ${{ !startsWith(github.event.repository.name, 'Template-') }} steps: - name: Checkout repository @@ -45,22 +47,22 @@ jobs: fi php -v && composer --version - - name: Setup mokocli tools + - name: Setup MokoCLI tools env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.GA_TOKEN || github.token }} + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }} MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} run: | if [ -d "/opt/mokocli" ] || [ -d "/tmp/mokocli" ]; then - echo "mokocli already available on runner — skipping clone" + echo "MokoCLI already available on runner — skipping clone" else git clone --depth 1 --branch main --quiet \ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git" \ - /tmp/mokocli 2>/dev/null || echo "mokocli clone skipped — continuing without it" + /tmp/mokocli 2>/dev/null || echo "MokoCLI clone skipped — continuing without it" fi - name: Install dependencies env: - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}' + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}"}}' run: | if [ -f "composer.json" ]; then composer install \ @@ -164,6 +166,75 @@ jobs: echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY fi + - name: Update server & packaging checks + continue-on-error: true + run: | + echo "### Update Server & Packaging" >> $GITHUB_STEP_SUMMARY + WARNINGS=0 + + # Find the extension manifest + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY + else + EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) + + # 1. Check exists and uses MokoGitea update server + if ! grep -q '' "$MANIFEST" 2>/dev/null; then + echo "::warning file=${MANIFEST}::Missing \`\` tag — extension will not receive OTA updates" + echo "- **Missing** \`\` — extension will not receive OTA updates" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + SERVER_URL=$(grep -oP ']*>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1) + if [ -z "$SERVER_URL" ]; then + echo "::warning file=${MANIFEST}::\`\` is empty — no server URL defined" + echo "- **Empty** \`\` — no server URL defined" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + elif ! echo "$SERVER_URL" | grep -q 'git\.mokoconsulting\.tech'; then + echo "::warning file=${MANIFEST}::Update server does not use MokoGitea engine: ${SERVER_URL}" + echo "- **Non-MokoGitea update server:** \`${SERVER_URL}\`" >> $GITHUB_STEP_SUMMARY + echo " Expected: \`https://git.mokoconsulting.tech/{org}/{repo}/updates.xml\`" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + echo "- \`\`: MokoGitea engine ✓" >> $GITHUB_STEP_SUMMARY + fi + fi + + # 2. Check tag exists + if ! grep -q '/dev/null; then + echo "::warning file=${MANIFEST}::Missing \`\` tag — download ID authentication is not configured" + echo "- **Missing** \`\` — download ID authentication not configured" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + echo "- \`\`: present ✓" >> $GITHUB_STEP_SUMMARY + fi + + # 3. For packages: check tag + if [ "$EXT_TYPE" = "package" ]; then + if ! grep -q '' "$MANIFEST" 2>/dev/null; then + echo "::warning file=${MANIFEST}::Package is missing \`\` — child extensions will not be removed on uninstall" + echo "- **Missing** \`\` — child extensions will remain when package is uninstalled" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + echo "- \`\`: present ✓" >> $GITHUB_STEP_SUMMARY + fi + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$WARNINGS" -gt 0 ]; then + echo "**${WARNINGS} packaging warning(s).** These won't block CI but should be addressed." >> $GITHUB_STEP_SUMMARY + else + echo "**Update server & packaging checks passed.**" >> $GITHUB_STEP_SUMMARY + fi + - name: Check language files referenced in manifest run: | echo "### Language File Check" >> $GITHUB_STEP_SUMMARY @@ -251,7 +322,8 @@ jobs: ERRORS=0 # Find all component manifests (XML with type="component") - COMP_MANIFESTS=$(find . -maxdepth 4 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -exec grep -l ']*type="component"' {} ; 2>/dev/null || true) + # Uses maxdepth 10 to reach into nested package repos (packages/*/source/packages/com_*/...) + COMP_MANIFESTS=$(find . -maxdepth 10 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./.claude/*" -exec grep -l ']*type="component"' {} \; 2>/dev/null || true) if [ -z "$COMP_MANIFESTS" ]; then echo "No component extensions found — skipping." >> $GITHUB_STEP_SUMMARY @@ -322,39 +394,162 @@ jobs: echo "Found $(echo "$SQL_FILES" | wc -l) SQL file(s)" >> $GITHUB_STEP_SUMMARY for FILE in $SQL_FILES; do - # Basic syntax check: balanced parentheses, no empty files + # 1. Empty / whitespace-only file check SIZE=$(wc -c < "$FILE" | tr -d ' ') if [ "$SIZE" -eq 0 ]; then - echo "- Empty SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY + echo "::error file=${FILE}::Empty SQL file" + echo "- **Empty** SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY ERRORS=$((ERRORS + 1)) continue fi - - # Check for common SQL errors - if grep -qP '^\s*$' "$FILE" && [ "$SIZE" -lt 5 ]; then - echo "- Whitespace-only SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY + CONTENT_SIZE=$(grep -cP '\S' "$FILE" 2>/dev/null || echo "0") + if [ "$CONTENT_SIZE" -eq 0 ]; then + echo "::error file=${FILE}::Whitespace-only SQL file" + echo "- **Whitespace-only** SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY ERRORS=$((ERRORS + 1)) continue fi echo "- \`${FILE}\`: ${SIZE} bytes" >> $GITHUB_STEP_SUMMARY + + # 2. Joomla table prefix — must use #__ not hardcoded prefixes + # Match table names in CREATE/ALTER/DROP/INSERT/UPDATE/DELETE that don't use #__ + HARDCODED=$(grep -nPi '(CREATE\s+TABLE|ALTER\s+TABLE|DROP\s+TABLE|INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+(`?)(?!#__)[a-z][a-z0-9_]+\2' "$FILE" 2>/dev/null || true) + if [ -n "$HARDCODED" ]; then + COUNT=$(echo "$HARDCODED" | wc -l) + echo "::error file=${FILE}::${COUNT} table reference(s) missing #__ prefix" + echo " - **${COUNT} hardcoded table name(s)** — must use \`#__\` prefix" >> $GITHUB_STEP_SUMMARY + while IFS= read -r LINE; do + LINE_NUM=$(echo "$LINE" | cut -d: -f1) + LINE_TEXT=$(echo "$LINE" | cut -d: -f2- | sed 's/^ *//') + echo " - Line ${LINE_NUM}: \`${LINE_TEXT}\`" >> $GITHUB_STEP_SUMMARY + done <<< "$HARDCODED" + ERRORS=$((ERRORS + COUNT)) + fi + + # 3. CREATE TABLE must use IF NOT EXISTS + BAD_CREATE=$(grep -nPi 'CREATE\s+TABLE\s+(?!IF\s+NOT\s+EXISTS)' "$FILE" 2>/dev/null || true) + if [ -n "$BAD_CREATE" ]; then + COUNT=$(echo "$BAD_CREATE" | wc -l) + echo "::error file=${FILE}::${COUNT} CREATE TABLE without IF NOT EXISTS" + echo " - **${COUNT} CREATE TABLE** missing \`IF NOT EXISTS\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + COUNT)) + fi + + # 4. DROP TABLE must use IF EXISTS + BAD_DROP=$(grep -nPi 'DROP\s+TABLE\s+(?!IF\s+EXISTS)' "$FILE" 2>/dev/null || true) + if [ -n "$BAD_DROP" ]; then + COUNT=$(echo "$BAD_DROP" | wc -l) + echo "::error file=${FILE}::${COUNT} DROP TABLE without IF EXISTS" + echo " - **${COUNT} DROP TABLE** missing \`IF EXISTS\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + COUNT)) + fi + + # 5. Balanced parentheses in CREATE TABLE statements + OPEN=$(grep -oP '\(' "$FILE" 2>/dev/null | wc -l) + CLOSE=$(grep -oP '\)' "$FILE" 2>/dev/null | wc -l) + if [ "$OPEN" -ne "$CLOSE" ]; then + echo "::error file=${FILE}::Unbalanced parentheses (${OPEN} open vs ${CLOSE} close)" + echo " - **Unbalanced parentheses**: ${OPEN} \`(\` vs ${CLOSE} \`)\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + # 6. ENGINE=InnoDB must be specified on CREATE TABLE + if grep -qPi 'CREATE\s+TABLE' "$FILE" 2>/dev/null; then + if ! grep -qPi 'ENGINE\s*=\s*InnoDB' "$FILE" 2>/dev/null; then + echo "::error file=${FILE}::CREATE TABLE missing ENGINE=InnoDB" + echo " - **Missing \`ENGINE=InnoDB\`** — required for Joomla 4+/5" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + + # 7. DEFAULT CHARSET=utf8mb4 must be specified on CREATE TABLE + if grep -qPi 'CREATE\s+TABLE' "$FILE" 2>/dev/null; then + if ! grep -qPi 'CHARSET\s*=\s*utf8mb4|CHARACTER\s+SET\s+utf8mb4' "$FILE" 2>/dev/null; then + echo "::error file=${FILE}::CREATE TABLE missing DEFAULT CHARSET=utf8mb4" + echo " - **Missing \`DEFAULT CHARSET=utf8mb4\`** — required for Joomla 4+/5" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi done - # Check update SQL files follow version numbering pattern - UPDATE_DIR=$(find . -path "*/sql/updates/mysql" -type d -not -path "./.git/*" 2>/dev/null | head -1) - if [ -n "$UPDATE_DIR" ]; then - BAD_NAMES=0 - for UFILE in "$UPDATE_DIR"/*.sql; do - [ ! -f "$UFILE" ] && continue - BASENAME=$(basename "$UFILE" .sql) - if ! echo "$BASENAME" | grep -qP '^\d+\.\d+\.\d+'; then - echo "- Update file \`${UFILE}\` does not follow version naming (expected X.Y.Z.sql)" >> $GITHUB_STEP_SUMMARY - BAD_NAMES=$((BAD_NAMES + 1)) + # 8. Update SQL files must follow version numbering pattern + UPDATE_DIRS=$(find . -path "*/sql/updates/mysql" -type d -not -path "./.git/*" 2>/dev/null) + if [ -n "$UPDATE_DIRS" ]; then + while IFS= read -r UPDATE_DIR; do + for UFILE in "$UPDATE_DIR"/*.sql; do + [ ! -f "$UFILE" ] && continue + BASENAME=$(basename "$UFILE" .sql) + if ! echo "$BASENAME" | grep -qP '^\d+\.\d+\.\d+'; then + echo "::error file=${UFILE}::Update file does not follow version naming (expected X.Y.Z.sql)" + echo "- Update file \`${UFILE}\` does not follow version naming (expected X.Y.Z.sql)" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + done <<< "$UPDATE_DIRS" + fi + + # 9. Install/uninstall pairing — if install exists, uninstall must too + INSTALL_FILES=$(find . -name "install.mysql.sql" -path "*/sql/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) + if [ -n "$INSTALL_FILES" ]; then + while IFS= read -r INSTALL_FILE; do + SQL_DIR=$(dirname "$INSTALL_FILE") + if [ ! -f "${SQL_DIR}/uninstall.mysql.sql" ]; then + echo "::error file=${INSTALL_FILE}::install.mysql.sql found but uninstall.mysql.sql is missing" + echo "- **Missing \`uninstall.mysql.sql\`** in \`${SQL_DIR}/\` — every install must have a matching uninstall" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) fi - done - if [ "$BAD_NAMES" -gt 0 ]; then - ERRORS=$((ERRORS + BAD_NAMES)) - fi + done <<< "$INSTALL_FILES" + fi + + # 10 & 11. Per-component schema/version consistency. Each component + # versions independently, so resolve every sql/updates/mysql dir + # against ITS OWN manifest (not one package-level version): + # (10) the component's manifest version must have a matching update file + # (11) no update file may exceed the component's manifest version + # (this is the guard that catches migration files drifting onto + # a different numbering lane and getting ahead of the release) + UPDATE_DIRS=$(find . -path "*/sql/updates/mysql" -type d -not -path "./.git/*" 2>/dev/null) + if [ -n "$UPDATE_DIRS" ]; then + while IFS= read -r UPDATE_DIR; do + # Component root is three levels up from /sql/updates/mysql + COMP_ROOT=$(dirname "$(dirname "$(dirname "$UPDATE_DIR")")") + # Resolve this component's OWN manifest (XML with at its root) + COMP_MANIFEST="" + for XML_FILE in "$COMP_ROOT"/*.xml; do + [ -f "$XML_FILE" ] || continue + if grep -q "/dev/null; then + COMP_MANIFEST="$XML_FILE" + break + fi + done + if [ -z "$COMP_MANIFEST" ]; then + echo "- No component manifest found for \`${UPDATE_DIR}\` — skipping version match" >> $GITHUB_STEP_SUMMARY + continue + fi + COMP_VERSION=$(grep -oP '\K[^<]+' "$COMP_MANIFEST" 2>/dev/null | head -1) + [ -z "$COMP_VERSION" ] && continue + # Normalise: strip any pre-release suffix (-dev, -rc.N, ...) for comparison + CV_BASE=$(echo "$COMP_VERSION" | sed -E 's/-.*$//') + # (10) a matching update file must exist for the component version + if [ ! -f "${UPDATE_DIR}/${CV_BASE}.sql" ]; then + echo "::error file=${COMP_MANIFEST}::No update SQL file for version ${CV_BASE}" + echo "- **Missing \`${UPDATE_DIR}/${CV_BASE}.sql\`** — update file must exist for manifest version \`${CV_BASE}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + # (11) no update file may exceed the component version + for UFILE in "$UPDATE_DIR"/*.sql; do + [ -f "$UFILE" ] || continue + UVER=$(basename "$UFILE" .sql) + echo "$UVER" | grep -qP '^\d+\.\d+\.\d+$' || continue + TOP=$(printf '%s\n%s\n' "$CV_BASE" "$UVER" | sort -V | tail -1) + if [ "$UVER" != "$CV_BASE" ] && [ "$TOP" = "$UVER" ]; then + echo "::error file=${UFILE}::Update file ${UVER} exceeds manifest version ${CV_BASE}" + echo "- **Update file \`${UVER}.sql\` exceeds manifest version \`${CV_BASE}\`** — renumber onto the release lane (must be <= manifest)" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + done <<< "$UPDATE_DIRS" fi fi @@ -647,6 +842,1104 @@ jobs: echo "**Service provider check passed.**" >> $GITHUB_STEP_SUMMARY fi + - name: Script file reference check + run: | + echo "### Script File Reference" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY + else + MANIFEST_DIR=$(dirname "$MANIFEST") + SCRIPT_FILE=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1) + if [ -z "$SCRIPT_FILE" ]; then + echo "No \`\` referenced — skipping." >> $GITHUB_STEP_SUMMARY + elif [ ! -f "${MANIFEST_DIR}/${SCRIPT_FILE}" ]; then + echo "::error file=${MANIFEST}::Manifest references \`${SCRIPT_FILE}\` but file does not exist" + echo "- **Missing** \`${SCRIPT_FILE}\` — referenced in \`\` but not found" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- \`${SCRIPT_FILE}\`: present ✓" >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} script file issue(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Script file reference check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Media folder validation + run: | + echo "### Media Folder Validation" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY + else + MANIFEST_DIR=$(dirname "$MANIFEST") + + # Check tag and its folder/filename children + MEDIA_DEST=$(grep -oP ']*\bdestination="\K[^"]+' "$MANIFEST" 2>/dev/null | head -1) + MEDIA_FOLDER=$(grep -oP ']*\bfolder="\K[^"]+' "$MANIFEST" 2>/dev/null | head -1) + + if [ -z "$MEDIA_DEST" ] && [ -z "$MEDIA_FOLDER" ]; then + echo "No \`\` tag found — skipping." >> $GITHUB_STEP_SUMMARY + else + if [ -n "$MEDIA_FOLDER" ] && [ ! -d "${MANIFEST_DIR}/${MEDIA_FOLDER}" ]; then + echo "::error file=${MANIFEST}::\`\` references missing directory" + echo "- **Missing** media folder \`${MEDIA_FOLDER}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- Media folder \`${MEDIA_FOLDER:-(inline)}\`: present ✓" >> $GITHUB_STEP_SUMMARY + + # Check child references inside block + if [ -n "$MEDIA_FOLDER" ]; then + MEDIA_FOLDERS=$(sed -n '//p' "$MANIFEST" | grep -oP '\K[^<]+' 2>/dev/null || true) + for F in $MEDIA_FOLDERS; do + if [ ! -d "${MANIFEST_DIR}/${MEDIA_FOLDER}/${F}" ]; then + echo "- **Missing** media subfolder \`${MEDIA_FOLDER}/${F}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + + MEDIA_FILES=$(sed -n '//p' "$MANIFEST" | grep -oP '\K[^<]+' 2>/dev/null || true) + for F in $MEDIA_FILES; do + if [ ! -f "${MANIFEST_DIR}/${MEDIA_FOLDER}/${F}" ]; then + echo "- **Missing** media file \`${MEDIA_FOLDER}/${F}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + fi + fi + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} media reference issue(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Media folder validation passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Target platform check + continue-on-error: true + run: | + echo "### Target Platform Check" >> $GITHUB_STEP_SUMMARY + WARNINGS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY + else + # Check updates.xml for targetplatform if it exists + if [ -f "updates.xml" ]; then + if ! grep -q '/dev/null; then + echo "::warning file=updates.xml::No \`\` found — Joomla updater cannot filter by compatible version" + echo "- **Missing** \`\` in updates.xml" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + echo "- \`\` in updates.xml: present ✓" >> $GITHUB_STEP_SUMMARY + fi + fi + + # Check manifest for minimum PHP/Joomla version hints + if ! grep -qP '|targetplatform|joomla.*version' "$MANIFEST" 2>/dev/null; then + echo "::warning file=${MANIFEST}::No minimum Joomla or PHP version constraint found in manifest" + echo "- **Missing** version constraints (\`\` or \`\`)" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + echo "- Version constraints in manifest: present ✓" >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$WARNINGS" -gt 0 ]; then + echo "**${WARNINGS} target platform warning(s).**" >> $GITHUB_STEP_SUMMARY + else + echo "**Target platform check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Changelog URL check + continue-on-error: true + run: | + echo "### Changelog URL Check" >> $GITHUB_STEP_SUMMARY + WARNINGS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY + else + if ! grep -q '' "$MANIFEST" 2>/dev/null; then + echo "::warning file=${MANIFEST}::Missing \`\` — Joomla updater will not display changelogs" + echo "- **Missing** \`\` — Joomla 4+ shows changelogs in the update manager when this is set" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + CHANGELOG_URL=$(grep -oP '\K[^<]+' "$MANIFEST" | head -1) + echo "- \`\`: \`${CHANGELOG_URL}\` ✓" >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$WARNINGS" -gt 0 ]; then + echo "**${WARNINGS} changelog URL warning(s).**" >> $GITHUB_STEP_SUMMARY + else + echo "**Changelog URL check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Duplicate file references check + continue-on-error: true + run: | + echo "### Duplicate File References" >> $GITHUB_STEP_SUMMARY + WARNINGS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY + else + # Extract all and references + ALL_REFS=$(grep -oP '<(filename|folder)[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null | sort || true) + if [ -z "$ALL_REFS" ]; then + echo "No file/folder references found — skipping." >> $GITHUB_STEP_SUMMARY + else + DUPES=$(echo "$ALL_REFS" | uniq -d) + if [ -n "$DUPES" ]; then + while IFS= read -r DUP; do + COUNT=$(echo "$ALL_REFS" | grep -cx "$DUP") + echo "::warning file=${MANIFEST}::Duplicate reference: \`${DUP}\` appears ${COUNT} times (may be valid if in different sections)" + echo "- **Duplicate:** \`${DUP}\` (${COUNT}x) — check if cross-section" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + done <<< "$DUPES" + else + TOTAL=$(echo "$ALL_REFS" | wc -l) + echo "All ${TOTAL} file/folder references are unique." >> $GITHUB_STEP_SUMMARY + fi + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$WARNINGS" -gt 0 ]; then + echo "**${WARNINGS} duplicate reference(s) found.** Review for cross-section validity." >> $GITHUB_STEP_SUMMARY + else + echo "**Duplicate file references check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Empty language keys check + continue-on-error: true + run: | + echo "### Empty Language Keys" >> $GITHUB_STEP_SUMMARY + WARNINGS=0 + + LANG_FILES=$(find . -name "*.ini" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) + if [ -z "$LANG_FILES" ]; then + echo "No .ini language files found — skipping." >> $GITHUB_STEP_SUMMARY + else + TOTAL_FILES=0 + for FILE in $LANG_FILES; do + TOTAL_FILES=$((TOTAL_FILES + 1)) + # Find lines with KEY= but no value (empty or whitespace-only after =) + EMPTY_KEYS=$(grep -nP '^[A-Z_]+=\s*$' "$FILE" 2>/dev/null || true) + if [ -n "$EMPTY_KEYS" ]; then + COUNT=$(echo "$EMPTY_KEYS" | wc -l) + echo "::warning file=${FILE}::${COUNT} empty language key(s)" + echo "- \`${FILE}\`: ${COUNT} empty key(s)" >> $GITHUB_STEP_SUMMARY + while IFS= read -r LINE; do + LINE_NUM=$(echo "$LINE" | cut -d: -f1) + KEY=$(echo "$LINE" | cut -d: -f2 | cut -d= -f1) + echo " - Line ${LINE_NUM}: \`${KEY}\`" >> $GITHUB_STEP_SUMMARY + done <<< "$EMPTY_KEYS" + WARNINGS=$((WARNINGS + COUNT)) + fi + done + + if [ "$WARNINGS" -eq 0 ]; then + echo "All ${TOTAL_FILES} language file(s) have populated keys." >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$WARNINGS" -gt 0 ]; then + echo "**${WARNINGS} empty language key(s) across ${TOTAL_FILES} file(s).**" >> $GITHUB_STEP_SUMMARY + else + echo "**Empty language keys check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Language key usage consistency + run: | + echo "### Language Key Usage Consistency" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Collect all defined language keys from .ini files + LANG_FILES=$(find . -name "*.ini" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) + if [ -z "$LANG_FILES" ]; then + echo "No .ini language files found — skipping." >> $GITHUB_STEP_SUMMARY + else + # Build list of defined keys + DEFINED_KEYS=$(cat $LANG_FILES 2>/dev/null | grep -oP '^[A-Z][A-Z0-9_]+(?==)' | sort -u) + if [ -z "$DEFINED_KEYS" ]; then + echo "No language keys defined — skipping." >> $GITHUB_STEP_SUMMARY + else + DEFINED_COUNT=$(echo "$DEFINED_KEYS" | wc -l) + echo "Found ${DEFINED_COUNT} defined key(s) across .ini files" >> $GITHUB_STEP_SUMMARY + + # Find keys used in PHP files (Text::_('KEY'), Text::sprintf('KEY'), etc.) + SRC_DIR="" + for DIR in source/ src/ htdocs/; do + [ -d "$DIR" ] && SRC_DIR="$DIR" && break + done + + if [ -n "$SRC_DIR" ]; then + USED_IN_PHP=$(grep -rohP "Text::(?:_|sprintf|plural|alt)\(\s*['\"]([A-Z][A-Z0-9_]+)['\"]" "$SRC_DIR" --include="*.php" 2>/dev/null | grep -oP "[A-Z][A-Z0-9_]+" | sort -u || true) + fi + + # Find keys used in XML files (label=, description=, etc.) + USED_IN_XML=$(find . -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -exec grep -ohP '(?:label|description|title|message|note)="([A-Z][A-Z0-9_]+)"' {} \; 2>/dev/null | grep -oP '[A-Z][A-Z0-9_]+' | sort -u || true) + + ALL_USED=$(printf '%s\n%s' "$USED_IN_PHP" "$USED_IN_XML" | sort -u | grep -v '^$' || true) + + if [ -n "$ALL_USED" ]; then + # Keys used but not defined + MISSING=$(comm -23 <(echo "$ALL_USED") <(echo "$DEFINED_KEYS") 2>/dev/null || true) + if [ -n "$MISSING" ]; then + COUNT=$(echo "$MISSING" | wc -l) + echo "::error::${COUNT} language key(s) used in code but not defined in .ini files" + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Used but not defined:**" >> $GITHUB_STEP_SUMMARY + while IFS= read -r KEY; do + echo "- \`${KEY}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + done <<< "$MISSING" + fi + else + echo "No language key usage found in source files." >> $GITHUB_STEP_SUMMARY + fi + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} undefined language key(s) — these will appear as raw key strings in the UI.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Language key usage consistency passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Orphan language keys + run: | + echo "### Orphan Language Keys" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + LANG_FILES=$(find . -name "*.ini" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) + if [ -z "$LANG_FILES" ]; then + echo "No .ini language files found — skipping." >> $GITHUB_STEP_SUMMARY + else + DEFINED_KEYS=$(cat $LANG_FILES 2>/dev/null | grep -oP '^[A-Z][A-Z0-9_]+(?==)' | sort -u) + if [ -z "$DEFINED_KEYS" ]; then + echo "No language keys defined — skipping." >> $GITHUB_STEP_SUMMARY + else + # Search all PHP, XML, and INI files for key references + ALL_SOURCE=$(find . \( -name "*.php" -o -name "*.xml" -o -name "*.ini" -o -name "*.js" \) -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" 2>/dev/null) + ORPHANS="" + while IFS= read -r KEY; do + # Skip sys.ini _DESC/_TITLE keys — these are Joomla convention keys + # Search for the key in all source files + if ! grep -rql "$KEY" --include="*.php" --include="*.xml" --include="*.js" . 2>/dev/null | grep -qv '\.ini' 2>/dev/null; then + ORPHANS="${ORPHANS}${KEY}\n" + fi + done <<< "$DEFINED_KEYS" + + ORPHANS=$(printf '%b' "$ORPHANS" | grep -v '^$' || true) + if [ -n "$ORPHANS" ]; then + COUNT=$(echo "$ORPHANS" | wc -l) + echo "::error::${COUNT} language key(s) defined in .ini but never referenced in code" + echo "**Orphan keys (defined but unreferenced):**" >> $GITHUB_STEP_SUMMARY + while IFS= read -r KEY; do + echo "- \`${KEY}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + done <<< "$ORPHANS" + fi + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} orphan language key(s) — remove unused keys to reduce translation burden.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Orphan language keys check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: PHP CodeSniffer (Joomla standard) + run: | + echo "### PHP CodeSniffer" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + SRC_DIR="" + for DIR in source/ src/ htdocs/; do + [ -d "$DIR" ] && SRC_DIR="$DIR" && break + done + + if [ -z "$SRC_DIR" ]; then + echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY + else + # Install PHPCS + Joomla coding standard if not already available + PHPCS="" + if [ -f "vendor/bin/phpcs" ]; then + PHPCS="vendor/bin/phpcs" + else + composer global require --no-interaction --quiet \ + squizlabs/php_codesniffer \ + joomla/coding-standards 2>/dev/null || true + GLOBAL_BIN=$(composer global config bin-dir --absolute 2>/dev/null) + if [ -f "${GLOBAL_BIN}/phpcs" ]; then + PHPCS="${GLOBAL_BIN}/phpcs" + # Register Joomla standard + GLOBAL_VENDOR=$(composer global config vendor-dir --absolute 2>/dev/null) + if [ -d "${GLOBAL_VENDOR}/joomla/coding-standards" ]; then + $PHPCS --config-set installed_paths "${GLOBAL_VENDOR}/joomla/coding-standards" 2>/dev/null || true + fi + fi + fi + + if [ -z "$PHPCS" ]; then + echo "Could not install PHP CodeSniffer — skipping." >> $GITHUB_STEP_SUMMARY + else + # Use repo phpcs.xml if present, otherwise use Joomla standard + ARGS="--report=summary --no-colors" + if [ -f "phpcs.xml" ] || [ -f "phpcs.xml.dist" ] || [ -f ".phpcs.xml" ]; then + echo "Using project PHPCS config." >> $GITHUB_STEP_SUMMARY + else + # Check if Joomla standard is available + if $PHPCS -i 2>/dev/null | grep -qi "Joomla"; then + ARGS="$ARGS --standard=Joomla" + echo "Using Joomla coding standard." >> $GITHUB_STEP_SUMMARY + else + ARGS="$ARGS --standard=PSR12" + echo "Joomla standard not available — falling back to PSR-12." >> $GITHUB_STEP_SUMMARY + fi + ARGS="$ARGS ${SRC_DIR}" + fi + + $PHPCS $ARGS 2>&1 | tee /tmp/phpcs-output.txt + EXIT=${PIPESTATUS[0]} + + if [ $EXIT -eq 0 ]; then + echo "**No coding standard violations found.**" >> $GITHUB_STEP_SUMMARY + else + echo '```' >> $GITHUB_STEP_SUMMARY + cat /tmp/phpcs-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "**Coding standard violations found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + fi + fi + + - name: Security patterns check + run: | + echo "### Security Patterns" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + SRC_DIR="" + for DIR in source/ src/ htdocs/; do + [ -d "$DIR" ] && SRC_DIR="$DIR" && break + done + + if [ -z "$SRC_DIR" ]; then + echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY + else + # 1. Direct superglobal access ($_GET, $_POST, $_REQUEST, $_COOKIE) + SUPERGLOBAL_HITS=$(grep -rnP '\$_(GET|POST|REQUEST|COOKIE|FILES)\b' "$SRC_DIR" --include="*.php" 2>/dev/null || true) + if [ -n "$SUPERGLOBAL_HITS" ]; then + COUNT=$(echo "$SUPERGLOBAL_HITS" | wc -l) + echo "::error::${COUNT} direct superglobal access(es) — use Joomla Input API instead" + echo "**Direct superglobal access (use \`\$app->getInput()\` or \`InputFilter\`):**" >> $GITHUB_STEP_SUMMARY + while IFS= read -r HIT; do + FILE=$(echo "$HIT" | cut -d: -f1) + LINE=$(echo "$HIT" | cut -d: -f2) + echo "- \`${FILE}:${LINE}\`" >> $GITHUB_STEP_SUMMARY + done <<< "$SUPERGLOBAL_HITS" + ERRORS=$((ERRORS + COUNT)) + fi + + # 2. Raw SQL query construction (string concatenation in query) + RAW_SQL=$(grep -rnP '\$db->setQuery\(\s*["\x27]' "$SRC_DIR" --include="*.php" 2>/dev/null || true) + if [ -n "$RAW_SQL" ]; then + COUNT=$(echo "$RAW_SQL" | wc -l) + echo "::error::${COUNT} raw SQL string(s) in setQuery() — use query builder or prepared statements" + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Raw SQL strings in \`setQuery()\` (use query builder):**" >> $GITHUB_STEP_SUMMARY + while IFS= read -r HIT; do + FILE=$(echo "$HIT" | cut -d: -f1) + LINE=$(echo "$HIT" | cut -d: -f2) + echo "- \`${FILE}:${LINE}\`" >> $GITHUB_STEP_SUMMARY + done <<< "$RAW_SQL" + ERRORS=$((ERRORS + COUNT)) + fi + + # 3. eval() usage + EVAL_HITS=$(grep -rnP '\beval\s*\(' "$SRC_DIR" --include="*.php" 2>/dev/null || true) + if [ -n "$EVAL_HITS" ]; then + COUNT=$(echo "$EVAL_HITS" | wc -l) + echo "::error::${COUNT} eval() usage(s) — never use eval in extensions" + echo "" >> $GITHUB_STEP_SUMMARY + echo "**\`eval()\` usage (critical security risk):**" >> $GITHUB_STEP_SUMMARY + while IFS= read -r HIT; do + FILE=$(echo "$HIT" | cut -d: -f1) + LINE=$(echo "$HIT" | cut -d: -f2) + echo "- \`${FILE}:${LINE}\`" >> $GITHUB_STEP_SUMMARY + done <<< "$EVAL_HITS" + ERRORS=$((ERRORS + COUNT)) + fi + + # 4. Missing defined('_JEXEC') or JDEFINED guard + while IFS= read -r -d '' PHP_FILE; do + FIRST_PHP=$(grep -nP '^\s*<\?php' "$PHP_FILE" 2>/dev/null | head -1) + if [ -n "$FIRST_PHP" ]; then + # Check first 5 non-empty lines after /dev/null; then + # Skip service provider files (they use a different entry point) + if ! echo "$PHP_FILE" | grep -q "provider.php"; then + echo "::error file=${PHP_FILE}::Missing _JEXEC direct access guard" + echo "- \`${PHP_FILE}\`: missing \`defined('_JEXEC') or die;\` guard" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + fi + done < <(find "$SRC_DIR" -name "*.php" -not -path "./vendor/*" -print0 2>/dev/null) + + if [ "$ERRORS" -eq 0 ]; then + echo "No security issues found." >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} security issue(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Security patterns check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: updates.xml structure validation + run: | + echo "### updates.xml Validation" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + UPDATE_XML=$(find . -maxdepth 2 -name "updates.xml" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null | head -1) + if [ -z "$UPDATE_XML" ]; then + echo "No updates.xml found — skipping." >> $GITHUB_STEP_SUMMARY + else + echo "Found: \`${UPDATE_XML}\`" >> $GITHUB_STEP_SUMMARY + + # 1. Well-formed XML + if command -v php &> /dev/null; then + if ! php -r "@simplexml_load_file('$UPDATE_XML') ?: exit(1);" 2>/dev/null; then + echo "::error file=${UPDATE_XML}::updates.xml is not well-formed XML" + echo "- **Malformed XML** — cannot be parsed" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- Well-formed XML ✓" >> $GITHUB_STEP_SUMMARY + + # 2. Must have root element + if ! grep -q '' "$UPDATE_XML" 2>/dev/null; then + echo "::error file=${UPDATE_XML}::Missing root element" + echo "- **Missing** \`\` root element" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- \`\` root element ✓" >> $GITHUB_STEP_SUMMARY + fi + + # 3. Each must have required children + UPDATE_COUNT=$(grep -c '' "$UPDATE_XML" 2>/dev/null || echo "0") + if [ "$UPDATE_COUNT" -eq 0 ]; then + echo "::error file=${UPDATE_XML}::No entries found" + echo "- **No \`\` entries** — file is empty" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- ${UPDATE_COUNT} \`\` entr(ies) found" >> $GITHUB_STEP_SUMMARY + + for TAG in name version type downloadurl; do + if ! grep -q "<${TAG}>" "$UPDATE_XML" 2>/dev/null; then + echo "::error file=${UPDATE_XML}::Missing required <${TAG}> in update entry" + echo "- **Missing** \`<${TAG}>\` in update entry" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- \`<${TAG}>\` present ✓" >> $GITHUB_STEP_SUMMARY + fi + done + + # 4. Must have + if ! grep -q '/dev/null; then + echo "::error file=${UPDATE_XML}::Missing — Joomla cannot filter by compatible version" + echo "- **Missing** \`\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- \`\` present ✓" >> $GITHUB_STEP_SUMMARY + fi + fi + fi + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} updates.xml issue(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**updates.xml validation passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: CSS/JS asset existence check + run: | + echo "### CSS/JS Asset Existence" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + SRC_DIR="" + for DIR in source/ src/ htdocs/; do + [ -d "$DIR" ] && SRC_DIR="$DIR" && break + done + + if [ -z "$SRC_DIR" ]; then + echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY + else + # Find WebAssetManager registrations in PHP + # registerAndUseScript/Style, useScript/Style with asset name + ASSET_REFS=$(grep -rohP "(?:registerAndUse|register|use)(?:Script|Style)\(\s*['\"]([^'\"]+)['\"]" "$SRC_DIR" --include="*.php" 2>/dev/null | grep -oP "['\"][^'\"]+['\"]" | tr -d "'\"" | sort -u || true) + + # Find joomla.asset.json files + ASSET_JSONS=$(find . -name "joomla.asset.json" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) + + # Find media directories + MEDIA_DIRS=$(find . -type d -name "media" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" 2>/dev/null) + + if [ -z "$ASSET_REFS" ] && [ -z "$ASSET_JSONS" ]; then + echo "No asset registrations found — skipping." >> $GITHUB_STEP_SUMMARY + else + # Validate joomla.asset.json is well-formed + if [ -n "$ASSET_JSONS" ]; then + for ASSET_JSON in $ASSET_JSONS; do + if command -v php &> /dev/null; then + VALID=$(php -r "echo json_decode(file_get_contents('$ASSET_JSON')) ? 'VALID' : 'INVALID';" 2>/dev/null) + if [ "$VALID" != "VALID" ]; then + echo "::error file=${ASSET_JSON}::joomla.asset.json is not valid JSON" + echo "- **Invalid JSON**: \`${ASSET_JSON}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- \`${ASSET_JSON}\`: valid JSON ✓" >> $GITHUB_STEP_SUMMARY + + # Check that referenced URIs in asset JSON point to existing files + URIS=$(php -r " + \$j = json_decode(file_get_contents('$ASSET_JSON'), true); + \$assets = array_merge(\$j['assets'] ?? [], []); + foreach (\$assets as \$a) { + if (isset(\$a['uri'])) echo \$a['uri'] . PHP_EOL; + if (isset(\$a['css'])) foreach ((array)\$a['css'] as \$c) echo (is_array(\$c) ? \$c['uri'] ?? '' : \$c) . PHP_EOL; + if (isset(\$a['js'])) foreach ((array)\$a['js'] as \$c) echo (is_array(\$c) ? \$c['uri'] ?? '' : \$c) . PHP_EOL; + } + " 2>/dev/null | grep -v '^$' | grep -v '^http' || true) + + ASSET_DIR=$(dirname "$ASSET_JSON") + for URI in $URIS; do + # Resolve relative path from asset JSON location + if [ ! -f "${ASSET_DIR}/${URI}" ]; then + echo "::error file=${ASSET_JSON}::Asset URI '${URI}' references missing file" + echo " - **Missing**: \`${URI}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + fi + fi + done + fi + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} asset issue(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**CSS/JS asset existence check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: MVC naming conventions check + run: | + echo "### MVC Naming Conventions" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + SRC_DIR="" + for DIR in source/ src/ htdocs/; do + [ -d "$DIR" ] && SRC_DIR="$DIR" && break + done + + if [ -z "$SRC_DIR" ]; then + echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY + else + # Check View classes — file must match class name pattern + VIEW_FILES=$(find "$SRC_DIR" -name "*.php" -path "*/View/*" -not -path "./vendor/*" 2>/dev/null) + if [ -n "$VIEW_FILES" ]; then + for FILE in $VIEW_FILES; do + BASENAME=$(basename "$FILE" .php) + # View class should contain class declaration matching filename + CLASS_NAME=$(grep -oP '^\s*class\s+\K[A-Za-z0-9_]+' "$FILE" 2>/dev/null | head -1) + if [ -n "$CLASS_NAME" ]; then + # Class name should end with View (HtmlView, JsonView, etc.) + if ! echo "$CLASS_NAME" | grep -qP 'View$'; then + echo "::error file=${FILE}::View class '${CLASS_NAME}' does not end with 'View'" + echo "- \`${FILE}\`: class \`${CLASS_NAME}\` should end with \`View\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + done + fi + + # Check Controller classes + CTRL_FILES=$(find "$SRC_DIR" -name "*.php" -path "*/Controller/*" -not -path "./vendor/*" 2>/dev/null) + if [ -n "$CTRL_FILES" ]; then + for FILE in $CTRL_FILES; do + CLASS_NAME=$(grep -oP '^\s*class\s+\K[A-Za-z0-9_]+' "$FILE" 2>/dev/null | head -1) + if [ -n "$CLASS_NAME" ]; then + if ! echo "$CLASS_NAME" | grep -qP 'Controller$'; then + echo "::error file=${FILE}::Controller class '${CLASS_NAME}' does not end with 'Controller'" + echo "- \`${FILE}\`: class \`${CLASS_NAME}\` should end with \`Controller\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + done + fi + + # Check Model classes + MODEL_FILES=$(find "$SRC_DIR" -name "*.php" -path "*/Model/*" -not -path "./vendor/*" 2>/dev/null) + if [ -n "$MODEL_FILES" ]; then + for FILE in $MODEL_FILES; do + CLASS_NAME=$(grep -oP '^\s*class\s+\K[A-Za-z0-9_]+' "$FILE" 2>/dev/null | head -1) + if [ -n "$CLASS_NAME" ]; then + if ! echo "$CLASS_NAME" | grep -qP 'Model$'; then + echo "::error file=${FILE}::Model class '${CLASS_NAME}' does not end with 'Model'" + echo "- \`${FILE}\`: class \`${CLASS_NAME}\` should end with \`Model\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + done + fi + + # Check Table classes + TABLE_FILES=$(find "$SRC_DIR" -name "*.php" -path "*/Table/*" -not -path "./vendor/*" 2>/dev/null) + if [ -n "$TABLE_FILES" ]; then + for FILE in $TABLE_FILES; do + CLASS_NAME=$(grep -oP '^\s*class\s+\K[A-Za-z0-9_]+' "$FILE" 2>/dev/null | head -1) + if [ -n "$CLASS_NAME" ]; then + if ! echo "$CLASS_NAME" | grep -qP 'Table$'; then + echo "::error file=${FILE}::Table class '${CLASS_NAME}' does not end with 'Table'" + echo "- \`${FILE}\`: class \`${CLASS_NAME}\` should end with \`Table\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + done + fi + + if [ -z "$VIEW_FILES" ] && [ -z "$CTRL_FILES" ] && [ -z "$MODEL_FILES" ] && [ -z "$TABLE_FILES" ]; then + echo "No MVC classes found — skipping." >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} MVC naming issue(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**MVC naming conventions check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Router validation + run: | + echo "### Router Validation" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Only applies to components + COMP_MANIFESTS=$(find . -maxdepth 10 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./.claude/*" -exec grep -l ']*type="component"' {} \; 2>/dev/null || true) + + if [ -z "$COMP_MANIFESTS" ]; then + echo "No component extensions found — skipping." >> $GITHUB_STEP_SUMMARY + else + for MANIFEST in $COMP_MANIFESTS; do + COMP_DIR=$(dirname "$MANIFEST") + COMP_NAME=$(basename "$COMP_DIR") + echo "Component: \`${COMP_NAME}\`" >> $GITHUB_STEP_SUMMARY + + # Check for router class in src/Service/ + ROUTER_FILE=$(find "$COMP_DIR" -name "Router.php" -path "*/Service/*" -not -path "./vendor/*" 2>/dev/null | head -1) + if [ -z "$ROUTER_FILE" ]; then + # Also check for legacy router.php + ROUTER_FILE=$(find "$COMP_DIR" -name "router.php" -not -path "./vendor/*" 2>/dev/null | head -1) + fi + + if [ -z "$ROUTER_FILE" ]; then + echo "::error file=${MANIFEST}::Component '${COMP_NAME}' has no Router class" + echo "- **Missing** Router — SEF URLs will not work" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- Router found: \`${ROUTER_FILE}\`" >> $GITHUB_STEP_SUMMARY + + # Check router implements RouterServiceInterface or RouterInterface + if ! grep -qP 'RouterServiceInterface|RouterInterface|RouterBase' "$ROUTER_FILE" 2>/dev/null; then + echo "::error file=${ROUTER_FILE}::Router does not implement RouterServiceInterface or RouterInterface" + echo " - **Does not implement** required router interface" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo " - Implements router interface ✓" >> $GITHUB_STEP_SUMMARY + fi + fi + done + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} router issue(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Router validation passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: ACL language keys check + run: | + echo "### ACL Language Keys" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + ACCESS_FILES=$(find . -name "access.xml" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) + if [ -z "$ACCESS_FILES" ]; then + echo "No access.xml files found — skipping." >> $GITHUB_STEP_SUMMARY + else + # Collect all defined language keys + LANG_FILES=$(find . -name "*.ini" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) + DEFINED_KEYS="" + if [ -n "$LANG_FILES" ]; then + DEFINED_KEYS=$(cat $LANG_FILES 2>/dev/null | grep -oP '^[A-Z][A-Z0-9_]+(?==)' | sort -u) + fi + + for ACCESS_FILE in $ACCESS_FILES; do + echo "Checking: \`${ACCESS_FILE}\`" >> $GITHUB_STEP_SUMMARY + + # Extract action names and titles/descriptions from access.xml + ACTIONS=$(grep -oP ']*name="\K[^"]+' "$ACCESS_FILE" 2>/dev/null || true) + TITLES=$(grep -oP ']*title="\K[^"]+' "$ACCESS_FILE" 2>/dev/null || true) + DESCS=$(grep -oP ']*description="\K[^"]+' "$ACCESS_FILE" 2>/dev/null || true) + + # Check title keys are defined + for KEY in $TITLES $DESCS; do + # Only check keys that look like language constants (ALL_CAPS_WITH_UNDERSCORES) + if echo "$KEY" | grep -qP '^[A-Z][A-Z0-9_]+$'; then + if [ -n "$DEFINED_KEYS" ]; then + if ! echo "$DEFINED_KEYS" | grep -qx "$KEY"; then + echo "::error file=${ACCESS_FILE}::ACL key '${KEY}' not defined in language files" + echo "- **Undefined** ACL key: \`${KEY}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + fi + done + done + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} ACL language key issue(s) — these will show raw key strings in permissions UI.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**ACL language keys check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Webservices plugin API manifest check + run: | + echo "### Webservices API Manifest" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Find webservices plugin manifests + WS_MANIFESTS=$(find . -maxdepth 10 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./.claude/*" -exec grep -l ']*type="plugin"' {} \; 2>/dev/null | while read -r F; do + if grep -q 'group="webservices"' "$F" 2>/dev/null; then + echo "$F" + fi + done) + + if [ -z "$WS_MANIFESTS" ]; then + echo "No webservices plugins found — skipping." >> $GITHUB_STEP_SUMMARY + else + for MANIFEST in $WS_MANIFESTS; do + WS_DIR=$(dirname "$MANIFEST") + WS_NAME=$(basename "$WS_DIR") + echo "Webservices plugin: \`${WS_NAME}\`" >> $GITHUB_STEP_SUMMARY + + # Find the plugin PHP file + WS_PHP=$(find "$WS_DIR" -name "*.php" -path "*/src/*" -not -path "./vendor/*" 2>/dev/null | head -1) + if [ -z "$WS_PHP" ]; then + WS_PHP=$(find "$WS_DIR" -name "*.php" -not -name "provider.php" -not -path "*/services/*" -not -path "./vendor/*" 2>/dev/null | head -1) + fi + + if [ -n "$WS_PHP" ]; then + # Check it implements WebserviceInterface or registers routes + if ! grep -qP 'WebserviceInterface|onBeforeApiRoute|ApiRouter' "$WS_PHP" 2>/dev/null; then + echo "::error file=${WS_PHP}::Webservices plugin does not implement WebserviceInterface or register API routes" + echo "- \`${WS_PHP}\`: missing \`WebserviceInterface\` or route registration" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- \`${WS_PHP}\`: API routing ✓" >> $GITHUB_STEP_SUMMARY + fi + else + echo "- No plugin PHP class found in \`${WS_DIR}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} webservices API issue(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Webservices API manifest check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: ZIP dry-run build + run: | + echo "### ZIP Dry-Run Build" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY + else + MANIFEST_DIR=$(dirname "$MANIFEST") + EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) + + # Collect all files that should be in the ZIP + MISSING_FILES="" + TOTAL_FILES=0 + + # Files from tags + for F in $(grep -oP ']*>\K[^<]+' "$MANIFEST" 2>/dev/null || true); do + TOTAL_FILES=$((TOTAL_FILES + 1)) + if [ ! -f "${MANIFEST_DIR}/${F}" ]; then + MISSING_FILES="${MISSING_FILES}${F}\n" + fi + done + + # Folders from tags + for F in $(grep -oP ']*>\K[^<]+' "$MANIFEST" 2>/dev/null || true); do + TOTAL_FILES=$((TOTAL_FILES + 1)) + if [ ! -d "${MANIFEST_DIR}/${F}" ]; then + MISSING_FILES="${MISSING_FILES}${F}/\n" + fi + done + + # Script file + SCRIPT=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1) + if [ -n "$SCRIPT" ]; then + TOTAL_FILES=$((TOTAL_FILES + 1)) + if [ ! -f "${MANIFEST_DIR}/${SCRIPT}" ]; then + MISSING_FILES="${MISSING_FILES}${SCRIPT}\n" + fi + fi + + # Media folder + MEDIA_FOLDER=$(grep -oP ']*\bfolder="\K[^"]+' "$MANIFEST" 2>/dev/null | head -1) + if [ -n "$MEDIA_FOLDER" ]; then + TOTAL_FILES=$((TOTAL_FILES + 1)) + if [ ! -d "${MANIFEST_DIR}/${MEDIA_FOLDER}" ]; then + MISSING_FILES="${MISSING_FILES}${MEDIA_FOLDER}/\n" + fi + fi + + # SQL folder + SQL_FOLDER=$(grep -oP '\s*\s*]*>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1) + if [ -n "$SQL_FOLDER" ]; then + SQL_DIR=$(dirname "$SQL_FOLDER") + TOTAL_FILES=$((TOTAL_FILES + 1)) + if [ ! -f "${MANIFEST_DIR}/${SQL_FOLDER}" ]; then + MISSING_FILES="${MISSING_FILES}${SQL_FOLDER}\n" + fi + fi + + MISSING_FILES=$(printf '%b' "$MISSING_FILES" | grep -v '^$' || true) + if [ -n "$MISSING_FILES" ]; then + COUNT=$(echo "$MISSING_FILES" | wc -l) + echo "::error file=${MANIFEST}::ZIP build would fail — ${COUNT} referenced file(s) missing" + echo "**Missing files that would cause ZIP build to fail:**" >> $GITHUB_STEP_SUMMARY + while IFS= read -r F; do + echo "- \`${F}\`" >> $GITHUB_STEP_SUMMARY + done <<< "$MISSING_FILES" + ERRORS=$((ERRORS + COUNT)) + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "Checked ${TOTAL_FILES} manifest reference(s) for \`${EXT_TYPE}\` extension." >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} ZIP build issue(s) — package would be incomplete.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**ZIP dry-run build passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: JavaScript linting + run: | + echo "### JavaScript Linting" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + JS_FILES=$(find . -name "*.js" -not -name "*.min.js" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" 2>/dev/null) + if [ -z "$JS_FILES" ]; then + echo "No JavaScript files found — skipping." >> $GITHUB_STEP_SUMMARY + else + JS_COUNT=$(echo "$JS_FILES" | wc -l) + echo "Found ${JS_COUNT} JavaScript file(s)" >> $GITHUB_STEP_SUMMARY + + # Check if Node.js is available + if command -v node &> /dev/null && command -v npx &> /dev/null; then + # Use project ESLint config if present, otherwise install and use defaults + if [ -f ".eslintrc.js" ] || [ -f ".eslintrc.json" ] || [ -f ".eslintrc.yml" ] || [ -f "eslint.config.js" ] || [ -f "eslint.config.mjs" ]; then + if [ -f "package.json" ]; then + npm install --silent 2>/dev/null || true + fi + npx eslint $JS_FILES 2>&1 | tee /tmp/eslint-output.txt + EXIT=${PIPESTATUS[0]} + else + # Basic syntax check without ESLint config + for FILE in $JS_FILES; do + if ! node --check "$FILE" 2>/tmp/js-syntax-err.txt; then + echo "::error file=${FILE}::JavaScript syntax error" + echo "- \`${FILE}\`: syntax error" >> $GITHUB_STEP_SUMMARY + cat /tmp/js-syntax-err.txt >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + EXIT=$ERRORS + fi + + if [ $EXIT -ne 0 ] && [ -f /tmp/eslint-output.txt ]; then + echo '```' >> $GITHUB_STEP_SUMMARY + cat /tmp/eslint-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + else + echo "Node.js not available — skipping JavaScript lint." >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} JavaScript issue(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**JavaScript linting passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: CSS linting + run: | + echo "### CSS Linting" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + CSS_FILES=$(find . -name "*.css" -not -name "*.min.css" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" 2>/dev/null) + if [ -z "$CSS_FILES" ]; then + echo "No CSS files found — skipping." >> $GITHUB_STEP_SUMMARY + else + CSS_COUNT=$(echo "$CSS_FILES" | wc -l) + echo "Found ${CSS_COUNT} CSS file(s)" >> $GITHUB_STEP_SUMMARY + + for FILE in $CSS_FILES; do + # Basic CSS validation: check for unbalanced braces + OPEN=$(grep -oP '\{' "$FILE" 2>/dev/null | wc -l) + CLOSE=$(grep -oP '\}' "$FILE" 2>/dev/null | wc -l) + if [ "$OPEN" -ne "$CLOSE" ]; then + echo "::error file=${FILE}::Unbalanced braces (${OPEN} open vs ${CLOSE} close)" + echo "- \`${FILE}\`: **unbalanced braces** (${OPEN} \`{\` vs ${CLOSE} \`}\`)" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + # Check for empty file + SIZE=$(wc -c < "$FILE" | tr -d ' ') + if [ "$SIZE" -eq 0 ]; then + echo "::error file=${FILE}::Empty CSS file" + echo "- \`${FILE}\`: **empty file**" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + + # Use Stylelint if available and configured + if command -v npx &> /dev/null; then + if [ -f ".stylelintrc" ] || [ -f ".stylelintrc.json" ] || [ -f "stylelint.config.js" ] || [ -f "stylelint.config.mjs" ]; then + if [ -f "package.json" ]; then + npm install --silent 2>/dev/null || true + fi + npx stylelint "**/*.css" --ignore-pattern "*.min.css" --ignore-pattern "node_modules" --ignore-pattern "vendor" 2>&1 | tee /tmp/stylelint-output.txt + EXIT=${PIPESTATUS[0]} + if [ $EXIT -ne 0 ]; then + echo '```' >> $GITHUB_STEP_SUMMARY + cat /tmp/stylelint-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} CSS issue(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**CSS linting passed.**" >> $GITHUB_STEP_SUMMARY + fi + release-readiness: name: Release Readiness Check runs-on: ubuntu-latest @@ -752,6 +2045,7 @@ jobs: test: name: Tests (PHP ${{ matrix.php }}) runs-on: ubuntu-latest + if: ${{ !startsWith(github.event.repository.name, 'Template-') }} needs: lint-and-validate strategy: @@ -773,7 +2067,7 @@ jobs: - name: Install dependencies env: - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}' + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}"}}' run: | if [ -f "composer.json" ]; then composer install \ @@ -806,6 +2100,7 @@ jobs: static-analysis: name: PHPStan Analysis runs-on: ubuntu-latest + if: ${{ !startsWith(github.event.repository.name, 'Template-') }} needs: lint-and-validate continue-on-error: true @@ -823,7 +2118,7 @@ jobs: - name: Install dependencies env: - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}' + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}"}}' run: | if [ -f "composer.json" ]; then composer install --no-interaction --prefer-dist --optimize-autoloader @@ -890,13 +2185,13 @@ jobs: steps: - name: Trigger pre-release build env: - GA_TOKEN: ${{ secrets.GA_TOKEN }} + MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} REPO: ${{ github.repository }} BRANCH: ${{ github.head_ref }} run: | curl -s -X POST \ "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" \ - -H "Authorization: token ${GA_TOKEN}" \ + -H "Authorization: token ${MOKOGITEA_TOKEN}" \ -H "Content-Type: application/json" \ -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY -- 2.52.0 From e1b588c8516ebcd51b569caf23052d31974dcfdc Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 04:39:53 +0000 Subject: [PATCH 34/63] chore: sync gitleaks.yml from Template-Joomla [skip ci] --- .mokogitea/workflows/gitleaks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mokogitea/workflows/gitleaks.yml b/.mokogitea/workflows/gitleaks.yml index 579f0cb..23b843e 100644 --- a/.mokogitea/workflows/gitleaks.yml +++ b/.mokogitea/workflows/gitleaks.yml @@ -6,7 +6,7 @@ # DEFGROUP: MokoGitea.Workflow # INGROUP: MokoCLI.Security # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli -# PATH: /.mokogitea/workflows/gitleaks.yml +# PATH: /templates/workflows/gitleaks.yml.template # VERSION: 01.00.00 # BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens # -- 2.52.0 From 24268549b500f022b7fcb8503dc32c94e189a435 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 04:39:54 +0000 Subject: [PATCH 35/63] chore: sync notify.yml from Template-Joomla [skip ci] --- .mokogitea/workflows/notify.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.mokogitea/workflows/notify.yml b/.mokogitea/workflows/notify.yml index 7a03898..6d029c2 100644 --- a/.mokogitea/workflows/notify.yml +++ b/.mokogitea/workflows/notify.yml @@ -46,13 +46,13 @@ jobs: WORKFLOW="${{ github.event.workflow_run.name }}" URL="${{ github.event.workflow_run.html_url }}" - curl -sS --retry 3 --retry-connrefused --retry-delay 2 --max-time 20 \ + curl -sS \ -H "Title: ${REPO} released" \ -H "Tags: white_check_mark,package" \ -H "Priority: default" \ -H "Click: ${URL}" \ -d "${WORKFLOW} completed successfully." \ - "${NTFY_URL}/${NTFY_TOPIC}" || echo "::warning::ntfy notification could not be delivered (non-fatal)" + "${NTFY_URL}/${NTFY_TOPIC}" - name: Notify on failure if: github.event.workflow_run.conclusion == 'failure' @@ -61,10 +61,10 @@ jobs: WORKFLOW="${{ github.event.workflow_run.name }}" URL="${{ github.event.workflow_run.html_url }}" - curl -sS --retry 3 --retry-connrefused --retry-delay 2 --max-time 20 \ + curl -sS \ -H "Title: ${REPO} workflow failed" \ -H "Tags: x,warning" \ -H "Priority: high" \ -H "Click: ${URL}" \ -d "${WORKFLOW} failed. Check the run for details." \ - "${NTFY_URL}/${NTFY_TOPIC}" || echo "::warning::ntfy notification could not be delivered (non-fatal)" + "${NTFY_URL}/${NTFY_TOPIC}" -- 2.52.0 From 49d7ddf79d4ff9212ce03c32093ee3e6a8ddcc4b Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 04:39:55 +0000 Subject: [PATCH 36/63] chore: sync pr-check.yml from Template-Joomla [skip ci] --- .mokogitea/workflows/pr-check.yml | 100 ++++-------------------------- 1 file changed, 12 insertions(+), 88 deletions(-) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index 6da762a..2cd896c 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -5,8 +5,8 @@ # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow # INGROUP: MokoCLI.CI -# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic -# PATH: /.mokogitea/workflows/pr-check.yml +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# PATH: /templates/workflows/universal/pr-check.yml.template # VERSION: 09.23.00 # BRIEF: PR gate — branch policy + code validation before merge @@ -47,15 +47,15 @@ jobs: fi ;; fix/*|bugfix/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then + if [ "$BASE" != "dev" ]; then ALLOWED=false - REASON="Fix branches must target 'dev' or 'main', not '${BASE}'" + REASON="Fix branches must target 'dev', not '${BASE}'" fi ;; patch/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ] && [ "$BASE" != "main" ]; then + if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then ALLOWED=false - REASON="Patch branches must target 'dev', 'rc', or 'main', not '${BASE}'" + REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" fi ;; hotfix/*) @@ -86,8 +86,7 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`fix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY - echo "- \`patch/*\` → \`dev\`, \`rc\`, or \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY @@ -97,80 +96,6 @@ jobs: echo "Branch policy: OK (${HEAD} → ${BASE})" echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY - # ── Docs Update Gate (main PRs) ───────────────────────────────────────── - require-docs: - name: Require Docs Update - runs-on: ubuntu-latest - # Enforce only on PRs merging into main: README.md + CHANGELOG.md must both be updated. - if: ${{ github.base_ref == 'main' }} - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Require README.md and CHANGELOG.md in the PR diff - run: | - BASE="${{ github.event.pull_request.base.sha }}" - HEAD="${{ github.event.pull_request.head.sha }}" - CHANGED="$(git diff --name-only "$BASE" "$HEAD" 2>/dev/null || true)" - if [ -z "$CHANGED" ]; then - git fetch -q origin "${{ github.base_ref }}" 2>/dev/null || true - CHANGED="$(git diff --name-only "origin/${{ github.base_ref }}...HEAD" 2>/dev/null || true)" - fi - echo "Changed files in PR:" - echo "$CHANGED" - MISSING="" - echo "$CHANGED" | grep -qxE 'README\.md' || MISSING="README.md" - echo "$CHANGED" | grep -qxE 'CHANGELOG\.md' || MISSING="${MISSING:+$MISSING, }CHANGELOG.md" - if [ -n "$MISSING" ]; then - echo "::error::PRs into main must update: ${MISSING}" - { - echo "## Docs Update Required" - echo "" - echo "PRs merging into \`main\` must update both **README.md** and **CHANGELOG.md**." - echo "" - echo "Not updated in this PR: **${MISSING}**" - } >> "$GITHUB_STEP_SUMMARY" - exit 1 - fi - echo "Docs update present (README.md + CHANGELOG.md)" - echo "## Docs Update: Passed" >> "$GITHUB_STEP_SUMMARY" - - # ── Wiki Update Reminder (main PRs, non-blocking) ─────────────────────── - wiki-reminder: - name: Wiki Update Reminder - runs-on: ubuntu-latest - if: ${{ github.base_ref == 'main' }} - steps: - - name: Remind to update the wiki - env: - TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - SERVER: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - REPO: ${{ github.repository }} - PR: ${{ github.event.pull_request.number }} - run: | - set -uo pipefail - { - echo "## Wiki Update Reminder" - echo "" - echo "Docs are **wiki-first** at MokoConsulting. If this change affects behavior, usage, configuration, or standards, update the repo wiki:" - echo "" - echo "- ${SERVER}/${REPO}/wiki" - echo "" - echo "_Non-blocking reminder._" - } >> "$GITHUB_STEP_SUMMARY" - # Post a single PR comment (idempotent via hidden marker); best-effort, never fails. - API="${SERVER}/api/v1/repos/${REPO}/issues/${PR}/comments" - if [ -n "${TOKEN:-}" ] && [ -n "${PR:-}" ]; then - existing="$(curl -sf -H "Authorization: token ${TOKEN}" "$API" 2>/dev/null | grep -c 'wiki-reminder' || true)" - if [ "${existing:-0}" -eq 0 ]; then - curl -sf -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -X POST "$API" \ - -d '{"body":"\n\n**Wiki reminder:** docs are wiki-first -- if this PR changes behavior, usage, config, or standards, please update the repo wiki before/after merge. _(non-blocking)_"}' >/dev/null 2>&1 || true - fi - fi - echo "Wiki reminder emitted (non-blocking)." - # ── Secret Scanning ────────────────────────────────────────────────── gitleaks: name: Secret Scan @@ -210,7 +135,7 @@ jobs: - name: Check for merge conflict markers run: | - CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --exclude-dir='.git' --exclude-dir='.mokogitea' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) + CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) if [ -n "$CONFLICTS" ]; then echo "::error::Merge conflict markers found in source files" echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY @@ -224,7 +149,7 @@ jobs: - name: Detect platform id: platform run: | - # Platform comes from the MokoGitea metadata API (public GET). + # Platform comes from the MokoGitea metadata API (public GET); manifest.xml is no longer used. API="${GITHUB_SERVER_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${GITHUB_REPOSITORY}/metadata" PLATFORM="$(curl -sf "$API" 2>/dev/null | python3 -c "import sys, json; print(json.load(sys.stdin).get('platform') or '')" 2>/dev/null || true)" [ -z "$PLATFORM" ] && PLATFORM="generic" @@ -258,9 +183,8 @@ jobs: while IFS= read -r -d '' file; do # Skip vendor, node_modules, and index.html stub files case "$file" in ./vendor/*|./node_modules/*) continue ;; esac - # Scan the whole file for the JEXEC/JPATH guard: it is placed after - # the SPDX/file-header docblock, which commonly runs past 20 lines. - if ! grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]" "$file"; then + # Check first 10 lines for JEXEC or JPATH guard + if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then echo "::error file=${file}::Missing JEXEC guard: ${file}" ERRORS=$((ERRORS + 1)) fi @@ -351,7 +275,7 @@ jobs: joomla) MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) if [ -z "$MANIFEST" ]; then - echo "::warning::No Joomla manifest found (MokoSuite site)" + echo "::warning::No Joomla manifest found (WaaS site)" exit 0 fi echo "Manifest: ${MANIFEST}" -- 2.52.0 From 8e877f1be2a193acc313dc727661334bc6caf6bb Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 04:39:56 +0000 Subject: [PATCH 37/63] chore: sync pr-metadata-check.yml from Template-Joomla [skip ci] --- .mokogitea/workflows/pr-metadata-check.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.mokogitea/workflows/pr-metadata-check.yml b/.mokogitea/workflows/pr-metadata-check.yml index 68b7589..a706c41 100644 --- a/.mokogitea/workflows/pr-metadata-check.yml +++ b/.mokogitea/workflows/pr-metadata-check.yml @@ -3,8 +3,8 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: mokocli.Validation +# DEFGROUP: MokoGitea.Workflow +# INGROUP: MokoCLI.Validation # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # PATH: /templates/workflows/joomla/pr-metadata-check.yml.template # VERSION: 01.00.00 @@ -20,7 +20,7 @@ permissions: contents: read env: - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} @@ -28,12 +28,14 @@ jobs: validate-metadata: name: "Validate Joomla Metadata" runs-on: ubuntu-latest + # Skip on template repos (Template-*) — they have no real extension manifest to validate. + if: ${{ !startsWith(github.event.repository.name, 'Template-') }} steps: - name: Checkout uses: actions/checkout@v4 - - name: Setup mokocli tools + - name: Setup MokoCLI tools env: MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting @@ -55,14 +57,14 @@ jobs: - name: Validate metadata against Joomla manifest env: - GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} run: | php ${MOKO_CLI}/joomla_metadata_validate.php \ --path . \ - --token "${GITEA_TOKEN}" \ + --token "${MOKOGITEA_TOKEN}" \ --org "${GITEA_ORG}" \ --repo "${GITEA_REPO}" \ - --api-base "${GITEA_URL}/api/v1" \ + --api-base "${MOKOGITEA_URL}/api/v1" \ --ci if [ $? -ne 0 ]; then -- 2.52.0 From 7cb937a7528a2bdc9c5a8bac37ac4838d2637792 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 04:39:57 +0000 Subject: [PATCH 38/63] chore: sync pre-release.yml from Template-Joomla [skip ci] --- .mokogitea/workflows/pre-release.yml | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index 4ea0367..2c4d3c9 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -5,9 +5,9 @@ # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow # INGROUP: MokoCLI.Release -# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic -# PATH: /.mokogitea/workflows/pre-release.yml -# VERSION: 05.02.01 +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# PATH: /templates/workflows/universal/pre-release.yml.template +# VERSION: 05.02.00 # BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches name: "Universal: Pre-Release" @@ -162,13 +162,7 @@ jobs: git add -A git diff --cached --quiet || { git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" - # Push the bump commit, but do NOT fail the release if the target branch - # is protected and the release identity is not on the push allowlist. - # The build proceeds from the in-tree bumped version regardless; if the - # push is rejected, the next run simply re-bumps from the same base. - if ! git push origin HEAD 2>&1; then - echo "::warning::Version-bump commit could not be pushed (protected branch?). Building from in-tree version ${VERSION} anyway." - fi + git push origin HEAD 2>&1 } # Auto-detect element via manifest_element.php @@ -280,4 +274,4 @@ jobs: echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY - echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY \ No newline at end of file + echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY -- 2.52.0 From 77404ff3b05e0580ffa4588e9f8e8e7fb88ff718 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 04:39:58 +0000 Subject: [PATCH 39/63] chore: sync rc-revert.yml from Template-Joomla [skip ci] --- .mokogitea/workflows/rc-revert.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.mokogitea/workflows/rc-revert.yml b/.mokogitea/workflows/rc-revert.yml index f58d1fa..8874441 100644 --- a/.mokogitea/workflows/rc-revert.yml +++ b/.mokogitea/workflows/rc-revert.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow # INGROUP: MokoCLI.Universal -# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # PATH: /.mokogitea/workflows/rc-revert.yml # VERSION: 09.23.00 # BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge @@ -25,8 +25,7 @@ jobs: runs-on: ubuntu-latest if: >- github.event.pull_request.merged == false && - startsWith(github.event.pull_request.head.ref, 'rc/') && - !startsWith(github.event.repository.name, 'Template-') + startsWith(github.event.pull_request.head.ref, 'rc/') steps: - name: Rename branch -- 2.52.0 From 5111b348bfe6b23c41e72c0c9c542aa67369f728 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 04:39:59 +0000 Subject: [PATCH 40/63] chore: sync repo-health.yml from Template-Joomla [skip ci] --- .mokogitea/workflows/repo-health.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index 3011fb7..31736e8 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -8,8 +8,8 @@ # FILE INFORMATION # DEFGROUP: MokoGitea.Workflow # INGROUP: MokoCLI.Validation -# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic -# PATH: /.mokogitea/workflows/repo-health.yml +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# PATH: /templates/workflows/joomla/repo_health.yml.template # VERSION: 09.23.00 # BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts. # ============================================================================ -- 2.52.0 From 631150a8ad9c69e12acff9e8c4ceca3c27756def Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 04:40:01 +0000 Subject: [PATCH 41/63] chore: sync version-set.yml from Template-Joomla [skip ci] --- .mokogitea/workflows/version-set.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.mokogitea/workflows/version-set.yml b/.mokogitea/workflows/version-set.yml index d7339bd..172c085 100644 --- a/.mokogitea/workflows/version-set.yml +++ b/.mokogitea/workflows/version-set.yml @@ -34,7 +34,6 @@ jobs: set-version: name: Set Version to ${{ inputs.version }} runs-on: ubuntu-latest - if: ${{ !startsWith(github.event.repository.name, 'Template-') }} steps: - name: Validate version format -- 2.52.0 From 894972fd5b3aad15239ef63b913c9e9c11bc5c7a Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 21:09:49 +0000 Subject: [PATCH 42/63] chore: sync auto-bump.yml from Template-Joomla [skip ci] --- .mokogit/workflows/auto-bump.yml | 66 ++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .mokogit/workflows/auto-bump.yml diff --git a/.mokogit/workflows/auto-bump.yml b/.mokogit/workflows/auto-bump.yml new file mode 100644 index 0000000..8c40ed9 --- /dev/null +++ b/.mokogit/workflows/auto-bump.yml @@ -0,0 +1,66 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGit.Workflow +# INGROUP: MokoCLI.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# PATH: /.mokogit/workflows/auto-bump.yml +# VERSION: 09.02.00 +# BRIEF: Auto patch-bump version on every push to dev (skips merge commits) + +name: "Universal: Auto Version Bump" + +on: + push: + branches: + - dev + - rc + - 'feature/**' + - 'patch/**' + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + +permissions: + contents: write + +jobs: + bump: + name: Version Bump + runs-on: release + if: >- + !contains(github.event.head_commit.message, '[skip ci]') && + !contains(github.event.head_commit.message, '[skip bump]') && + !startsWith(github.event.head_commit.message, 'Merge pull request') + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGIT_TOKEN }} + fetch-depth: 1 + + - name: Setup MokoCLI tools + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + if [ -d "/opt/mokocli/cli" ]; then + echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV" + else + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \ + /tmp/mokocli + cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet + echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV" + fi + + - name: Bump version + run: | + php ${MOKO_CLI}/version_auto_bump.php \ + --path . --branch "${GITHUB_REF_NAME}" \ + --token "${{ secrets.MOKOGIT_TOKEN }}" \ + --repo-url "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" -- 2.52.0 From ce248d659b5dc207b1b9f09fccb100113de99491 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 21:09:50 +0000 Subject: [PATCH 43/63] chore: sync auto-release.yml from Template-Joomla [skip ci] --- .mokogit/workflows/auto-release.yml | 469 ++++++++++++++++++++++++++++ 1 file changed, 469 insertions(+) create mode 100644 .mokogit/workflows/auto-release.yml diff --git a/.mokogit/workflows/auto-release.yml b/.mokogit/workflows/auto-release.yml new file mode 100644 index 0000000..cf0bbd0 --- /dev/null +++ b/.mokogit/workflows/auto-release.yml @@ -0,0 +1,469 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGit.Workflow +# INGROUP: MokoCLI.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# PATH: /templates/workflows/universal/auto-release.yml.template +# VERSION: 05.01.00 +# BRIEF: Universal build & release � detects platform from MokoGit repo metadata (API) +# +# +=======================================================================+ +# | UNIVERSAL BUILD & RELEASE PIPELINE | +# +=======================================================================+ +# | | +# | Reads MokoGit repo metadata (joomla|dolibarr|generic) to branch logic. | +# | | +# | Platform-specific: | +# | joomla: XML manifest, type-prefixed packages | +# | dolibarr: mod*.class.php, update.txt, dev version reset | +# | generic: README-only, no update stream | +# | | +# +=======================================================================+ + +name: "Universal: Build & Release" + +on: + pull_request: + types: [opened, synchronize, closed] + branches: + - main + paths-ignore: + - '.mokogit/workflows/**' + - '*.md' + - 'wiki/**' + - '.editorconfig' + - '.gitignore' + - '.gitattributes' + - '.gitmessage' + - 'LICENSE' + workflow_dispatch: + inputs: + action: + description: 'Action to perform' + required: false + type: choice + default: release + options: + - release + - promote-rc + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GIT_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GIT_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + # ── PR Opened → Rename branch to RC and build RC release ───────────────────────── + promote-rc: + name: Promote to RC + runs-on: release + # Skip on template repos (Template-*) — they scaffold other repos and do not release. + if: >- + !startsWith(github.event.repository.name, 'Template-') && + ( + (github.event.action == 'opened' && github.event.pull_request.merged != true) || + (github.event.action == 'synchronize' && github.event.pull_request.merged != true) || + (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') + ) + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGIT_TOKEN }} + fetch-depth: 1 + submodules: recursive + + - name: Setup MokoCLI tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then + echo Using pre-installed /opt/mokocli + echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV + else + echo Falling back to fresh clone + if ! command -v composer > /dev/null 2>&1; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 + fi + rm -rf /tmp/mokocli + CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git + git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli + cd /tmp/mokocli + composer install --no-dev --no-interaction --quiet + echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV + fi + + - name: Rename branch to rc + run: | + php ${MOKO_CLI}/branch_rename.php \ + --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ + --token "${{ secrets.MOKOGIT_TOKEN }}" \ + --api-base "${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" \ + --pr "${{ github.event.pull_request.number }}" + + - name: Checkout rc and configure git + run: | + git fetch origin rc + git checkout rc + git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech" + git config --local user.name "mokogit-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + + - name: Publish RC release + run: | + php ${MOKO_CLI}/release_publish.php \ + --path . --stability rc --bump minor --branch rc \ + --token "${{ secrets.MOKOGIT_TOKEN }}" + + - name: Update RC release notes from CHANGELOG.md + run: | + API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" + TOKEN="${{ secrets.MOKOGIT_TOKEN }}" + + # Extract [Unreleased] section from changelog + NOTES="" + if [ -f "CHANGELOG.md" ]; then + NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) + fi + [ -z "$NOTES" ] && NOTES="Release candidate" + + # Find the RC release and update its body + RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/releases/tags/release-candidate" \ + | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [ -n "$RELEASE_ID" ]; then + python3 -c " + import json, urllib.request + body = open('/dev/stdin').read() + payload = json.dumps({'body': body}).encode() + req = urllib.request.Request( + '${API_BASE}/releases/${RELEASE_ID}', + data=payload, method='PATCH', + headers={ + 'Authorization': 'token ${TOKEN}', + 'Content-Type': 'application/json' + }) + urllib.request.urlopen(req) + " <<< "$NOTES" + echo "RC release notes updated from CHANGELOG.md" + fi + + - name: Summary + if: always() + run: | + echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY + echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY + + # ── Merged PR → Build & Release (or promote RC to stable) ───────────────────────── + release: + name: Build & Release Pipeline + runs-on: release + # Skip on template repos (Template-*) — they scaffold other repos and do not release. + if: >- + !startsWith(github.event.repository.name, 'Template-') && + ( + github.event.pull_request.merged == true || + (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') + ) + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGIT_TOKEN }} + fetch-depth: 0 + submodules: recursive + + - name: Configure git for bot pushes + run: | + git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech" + git config --local user.name "mokogit-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + + - name: Check for merge conflict markers + run: | + CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) + if [ -n "$CONFLICTS" ]; then + echo "::error::Merge conflict markers found — aborting release" + echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "No conflict markers found" + + - name: Setup MokoCLI tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' + run: | + if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then + echo Using pre-installed /opt/mokocli + echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV + else + echo Falling back to fresh clone + if ! command -v composer > /dev/null 2>&1; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 + fi + rm -rf /tmp/mokocli + CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git + git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli + cd /tmp/mokocli + composer install --no-dev --no-interaction --quiet + echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV + fi + + - name: "Detect platform" + id: platform + run: | + php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true + php ${MOKO_CLI}/manifest_read.php --path . --github-output 2>/dev/null || true + + - name: "Determine version bump level" + id: bump + run: | + # Fix/patch branches: version was already bumped by pre-release, just strip suffix + # Feature/dev branches: bump minor for the new stable release + HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}" + case "$HEAD_REF" in + fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;; + *) BUMP="minor" ;; + esac + echo "level=${BUMP}" >> "$GITHUB_OUTPUT" + echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})" + + - name: "Publish stable release" + run: | + BUMP_FLAG="" + if [ "${{ steps.bump.outputs.level }}" != "none" ]; then + BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}" + fi + php ${MOKO_CLI}/release_publish.php \ + --path . --stability stable ${BUMP_FLAG} --branch main \ + --token "${{ secrets.MOKOGIT_TOKEN }}" + + - name: "Read published version" + id: version + run: | + VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "") + VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') + [ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + PLATFORM="${{ steps.platform.outputs.platform }}" + if [[ "$PLATFORM" == joomla* ]]; then + echo "tag=stable" >> "$GITHUB_OUTPUT" + echo "release_tag=stable" >> "$GITHUB_OUTPUT" + else + echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT" + echo "release_tag=v${VERSION}" >> "$GITHUB_OUTPUT" + fi + echo "branch=main" >> "$GITHUB_OUTPUT" + echo "Published version: ${VERSION}" + + - name: "Create semver tag for non-Joomla repos" + id: semver + if: | + steps.version.outputs.skip != 'true' && + !startsWith(steps.platform.outputs.platform, 'joomla') + run: | + VERSION="${{ steps.version.outputs.version }}" + API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" + TOKEN="${{ secrets.MOKOGIT_TOKEN }}" + SEMVER_TAG="v${VERSION}" + + echo "Creating semver tag: ${SEMVER_TAG}" + + # Create the git tag via API + HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \ + -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/tags" \ + -d "{\"tag_name\":\"${SEMVER_TAG}\",\"target\":\"main\",\"message\":\"Release ${VERSION}\"}" 2>/dev/null || echo "000") + + if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then + echo "Created semver tag: ${SEMVER_TAG}" + elif [ "$HTTP_CODE" = "409" ]; then + echo "Semver tag ${SEMVER_TAG} already exists (skipped)" + else + echo "::warning::Failed to create semver tag ${SEMVER_TAG} (HTTP ${HTTP_CODE})" + fi + + echo "semver_tag=${SEMVER_TAG}" >> "$GITHUB_OUTPUT" + + - name: Update release notes and promote changelog + run: | + API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" + TOKEN="${{ secrets.MOKOGIT_TOKEN }}" + + # Get the stable release info (version and ID) + RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/releases/tags/stable" 2>/dev/null || echo '{}') + RELEASE_ID=$(python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" <<< "$RELEASE_JSON" 2>/dev/null || true) + # Extract version from release name (e.g. "06.17.00" or "v06.17.00") + VERSION=$(python3 -c " + import json, sys, re + r = json.load(sys.stdin) + name = r.get('name', '') + m = re.search(r'(\d+\.\d+\.\d+)', name) + print(m.group(1) if m else '') + " <<< "$RELEASE_JSON" 2>/dev/null || true) + + # Extract [Unreleased] section from changelog + NOTES="" + if [ -f "CHANGELOG.md" ]; then + NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) + fi + [ -z "$NOTES" ] && NOTES="Stable release" + + # Update release body via API + if [ -n "$RELEASE_ID" ]; then + python3 -c " + import json, urllib.request + body = open('/dev/stdin').read() + payload = json.dumps({'body': body}).encode() + req = urllib.request.Request( + '${API_BASE}/releases/${RELEASE_ID}', + data=payload, method='PATCH', + headers={ + 'Authorization': 'token ${TOKEN}', + 'Content-Type': 'application/json' + }) + urllib.request.urlopen(req) + " <<< "$NOTES" + echo "Release notes updated from CHANGELOG.md" + fi + + # Promote [Unreleased] → [version] in CHANGELOG.md and reset + if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then + DATE=$(date +%Y-%m-%d) + python3 -c " + import sys + version, date = sys.argv[1], sys.argv[2] + content = open('CHANGELOG.md').read() + old = '## [Unreleased]' + new = f'## [Unreleased]\n\n## [{version}] --- {date}' + content = content.replace(old, new, 1) + open('CHANGELOG.md', 'w').write(content) + " "$VERSION" "$DATE" + git add CHANGELOG.md + git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true + git push origin main || true + echo "Changelog promoted: [Unreleased] → [${VERSION}]" + fi + + # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- + - name: "Step 9: Mirror release to GitHub" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" + php ${MOKO_CLI}/release_mirror.php \ + --version "$VERSION" --tag "$RELEASE_TAG" \ + --token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "$API_BASE" \ + --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ + --branch main 2>&1 || true + echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY + + # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- + - name: "Step 10: Push main to GitHub mirror" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) + GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) + git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ + git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" + git fetch origin main --depth=1 + git push github origin/main:refs/heads/main --force 2>/dev/null \ + && echo "main branch pushed to GitHub mirror" \ + || echo "WARNING: GitHub mirror push failed" + + - name: "Step 11: Delete rc branch (dev reset moved to cascade-dev.yml)" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" + TOKEN="${{ secrets.MOKOGIT_TOKEN }}" + + # Delete rc branch (ephemeral — created by promote-rc) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/rc" 2>/dev/null \ + && echo "Deleted rc branch" || echo "rc branch not found" + + # dev is reset from main by the dedicated "Cascade Main -> Dev" workflow + # (cascade-dev.yml), which runs after this release completes. + echo "rc cleaned; dev reset handled by cascade-dev.yml" >> $GITHUB_STEP_SUMMARY + + - name: "Step 12: Create version branch from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" + TOKEN="${{ secrets.MOKOGIT_TOKEN }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + BRANCH_NAME="version/${VERSION}" + MAIN_SHA=$(git rev-parse HEAD) + + # Delete old version branch if it exists (same version re-release) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" + + # Create version/XX.YY.ZZ from main + curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" + + echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY + + + + # -- Dolibarr post-release: Reset dev version ----------------------------- + - name: "Post-release: Reset dev version" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" + php ${MOKO_CLI}/version_reset_dev.php \ + --token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "${API_BASE}" \ + --branch dev --path . 2>&1 || true + + # -- Summary -------------------------------------------------------------- + - name: Pipeline Summary + if: always() + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PLATFORM="${{ steps.platform.outputs.platform }}" + if [ "${{ steps.version.outputs.skip }}" = "true" ]; then + echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY + echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY + elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then + echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | [View](${MOKOGIT_URL}/${GIT_ORG}/${GIT_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY + fi -- 2.52.0 From 5f62e3f8a7eac6d715099913fe520c390b7d9c64 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 21:09:51 +0000 Subject: [PATCH 44/63] chore: sync branch-cleanup.yml from Template-Joomla [skip ci] --- .mokogit/workflows/branch-cleanup.yml | 49 +++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .mokogit/workflows/branch-cleanup.yml diff --git a/.mokogit/workflows/branch-cleanup.yml b/.mokogit/workflows/branch-cleanup.yml new file mode 100644 index 0000000..545e137 --- /dev/null +++ b/.mokogit/workflows/branch-cleanup.yml @@ -0,0 +1,49 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGit.Workflow +# INGROUP: MokoCLI.Universal +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# PATH: /.mokogit/workflows/branch-cleanup.yml +# VERSION: 01.00.00 +# BRIEF: Delete feature branches after PR merge + +name: "Branch Cleanup" + +on: + pull_request: + types: [closed] + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + cleanup: + name: Delete merged branch + runs-on: ubuntu-latest + if: >- + github.event.pull_request.merged == true && + github.event.pull_request.head.ref != 'dev' && + github.event.pull_request.head.ref != 'main' + + steps: + - name: Delete source branch + run: | + BRANCH="${{ github.event.pull_request.head.ref }}" + API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches" + # URL-encode the branch name's slashes (no PHP dependency on the runner) + ENCODED=$(printf '%s' "${BRANCH}" | sed 's|/|%2F|g') + + STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \ + -H "Authorization: token ${{ secrets.MOKOGIT_TOKEN }}" \ + "${API}/${ENCODED}" 2>/dev/null || true) + + if [ "$STATUS" = "204" ]; then + echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY + elif [ "$STATUS" = "404" ]; then + echo "Branch already deleted: ${BRANCH}" >> $GITHUB_STEP_SUMMARY + else + echo "::warning::Failed to delete branch ${BRANCH} (HTTP ${STATUS})" + fi -- 2.52.0 From e9e978856020f6be20b2ec92428ca2b0a99dcc1f Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 21:09:52 +0000 Subject: [PATCH 45/63] chore: sync cascade-dev.yml from Template-Joomla [skip ci] --- .mokogit/workflows/cascade-dev.yml | 65 ++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 .mokogit/workflows/cascade-dev.yml diff --git a/.mokogit/workflows/cascade-dev.yml b/.mokogit/workflows/cascade-dev.yml new file mode 100644 index 0000000..7a179ea --- /dev/null +++ b/.mokogit/workflows/cascade-dev.yml @@ -0,0 +1,65 @@ +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# FILE INFORMATION +# DEFGROUP: MokoGit.Workflow +# INGROUP: MokoCLI.Release +# BRIEF: Reset dev to main after each release (cascade). Moved out of auto-release Step 11. +# +# +========================================================================+ +# | CASCADE MAIN -> DEV | +# +========================================================================+ +# | dev mirrors main and is reset on every release. | +# | delete+recreate cannot run against a protected branch, so this | +# | force-pushes main -> dev. The automation identity is force-push | +# | allowlisted on dev via branch-protection.yml. | +# | Runs AFTER the release workflow completes, so dev picks up the | +# | fully version-bumped + changelog-promoted main (not the pre-bump | +# | state). Force-push (not merge) => no version-file conflicts. | +# +========================================================================+ + +name: "Cascade Main -> Dev" + +on: + workflow_run: + workflows: ["Universal: Build & Release"] + types: [completed] + workflow_dispatch: + +concurrency: + group: cascade-dev-${{ github.repository }} + cancel-in-progress: true + +permissions: + contents: write + +jobs: + cascade: + name: Reset dev to main + if: >- + !startsWith(github.event.repository.name, 'Template-') && + (github.event_name == 'workflow_dispatch' || + github.event.workflow_run.conclusion == 'success') + runs-on: ubuntu-latest + steps: + - name: Force dev to main + env: + TOKEN: ${{ secrets.MOKOGIT_TOKEN }} + SERVER: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + git init -q sync && cd sync + git config user.email "mokogit-actions[bot]@mokoconsulting.tech" + git config user.name "mokogit-actions[bot]" + git remote add origin "https://x-access-token:${TOKEN}@${SERVER#https://}/${REPO}.git" + git fetch -q --depth=1 origin main + + if git ls-remote --exit-code --heads origin dev >/dev/null 2>&1; then + # dev exists -> force it to match main (dev is a mirror of main) + git push --force origin FETCH_HEAD:refs/heads/dev + echo "dev reset to main" >> "$GITHUB_STEP_SUMMARY" + else + # dev missing (e.g. renamed to rc mid-cycle) -> recreate from main + git push origin FETCH_HEAD:refs/heads/dev + echo "dev created from main" >> "$GITHUB_STEP_SUMMARY" + fi -- 2.52.0 From 11cb107433ec185593952ca91a8d9aed8300e680 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 21:09:53 +0000 Subject: [PATCH 46/63] chore: sync ci-generic.yml from Template-Joomla [skip ci] --- .mokogit/workflows/ci-generic.yml | 202 ++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 .mokogit/workflows/ci-generic.yml diff --git a/.mokogit/workflows/ci-generic.yml b/.mokogit/workflows/ci-generic.yml new file mode 100644 index 0000000..442a94f --- /dev/null +++ b/.mokogit/workflows/ci-generic.yml @@ -0,0 +1,202 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGit.Workflow +# INGROUP: MokoCLI.CI +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic +# PATH: /.mokogit/workflows/ci-generic.yml +# VERSION: 01.00.00 +# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js) + +name: "Generic: Project CI" + +on: + pull_request: + branches: + - main + - dev + - dev/** + - rc/** + workflow_dispatch: + +permissions: + contents: read + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + # ── Lint & Validate ─────────────────────────────────────────────────── + lint: + name: Lint & Validate + runs-on: ubuntu-latest + # Skip on template repos (Template-*) — they hold placeholder scaffolding, not buildable source. + if: ${{ !startsWith(github.event.repository.name, 'Template-') }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Detect toolchain + id: detect + run: | + HAS_PHP=false + HAS_NODE=false + [ -f "composer.json" ] && HAS_PHP=true + [ -f "package.json" ] && HAS_NODE=true + echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT" + echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT" + echo "Toolchain: PHP=$HAS_PHP Node=$HAS_NODE" + + - name: Setup PHP + if: steps.detect.outputs.has_php == 'true' + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 + fi + php -v + + - name: Setup Node.js + if: steps.detect.outputs.has_node == 'true' + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install PHP dependencies + if: steps.detect.outputs.has_php == 'true' + run: | + if [ -f "composer.json" ]; then + composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true + fi + + - name: Install Node.js dependencies + if: steps.detect.outputs.has_node == 'true' + run: | + if [ -f "package.json" ]; then + npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true + fi + + - name: PHP syntax check + if: steps.detect.outputs.has_php == 'true' + run: | + ERRORS=0 + while IFS= read -r -d '' file; do + if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then + echo "::error file=${file}::PHP syntax error" + ERRORS=$((ERRORS + 1)) + fi + done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" -print0) + + echo "## PHP Lint" >> $GITHUB_STEP_SUMMARY + if [ "$ERRORS" -eq 0 ]; then + echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY + else + echo "${ERRORS} file(s) with syntax errors." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + - name: TypeScript/JavaScript lint + if: steps.detect.outputs.has_node == 'true' + run: | + if [ -f "node_modules/.bin/eslint" ]; then + npx eslint src/ --quiet 2>&1 || { echo "::error::ESLint errors found"; exit 1; } + echo "## ESLint" >> $GITHUB_STEP_SUMMARY + echo "All files passed ESLint." >> $GITHUB_STEP_SUMMARY + elif [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f "eslint.config.js" ]; then + echo "::warning::ESLint config found but eslint not installed" + else + echo "No ESLint configured — skipping" + fi + + - name: TypeScript compile check + if: steps.detect.outputs.has_node == 'true' + run: | + if [ -f "tsconfig.json" ] && [ -f "node_modules/.bin/tsc" ]; then + npx tsc --noEmit 2>&1 || { echo "::error::TypeScript compilation errors"; exit 1; } + echo "## TypeScript" >> $GITHUB_STEP_SUMMARY + echo "TypeScript compilation passed." >> $GITHUB_STEP_SUMMARY + fi + + - name: PHPStan static analysis + if: steps.detect.outputs.has_php == 'true' + run: | + if [ -f "phpstan.neon" ] && [ -f "vendor/bin/phpstan" ]; then + vendor/bin/phpstan analyse --no-progress 2>&1 || { echo "::warning::PHPStan found issues"; } + fi + + # ── Tests ───────────────────────────────────────────────────────────── + test: + name: Tests + runs-on: ubuntu-latest + needs: lint + # Run only when lint succeeded; always() forces evaluation so a skipped + # lint (e.g. template repos) skips this job cleanly instead of hanging. + if: ${{ always() && needs.lint.result == 'success' }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Detect toolchain + id: detect + run: | + HAS_PHP=false + HAS_NODE=false + [ -f "composer.json" ] && HAS_PHP=true + [ -f "package.json" ] && HAS_NODE=true + echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT" + echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT" + + - name: Setup PHP + if: steps.detect.outputs.has_php == 'true' + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 + fi + + - name: Setup Node.js + if: steps.detect.outputs.has_node == 'true' + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: | + [ -f "composer.json" ] && composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true + [ -f "package.json" ] && { npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true; } + + - name: Run PHP tests + if: steps.detect.outputs.has_php == 'true' + run: | + if [ -f "vendor/bin/phpunit" ]; then + vendor/bin/phpunit --testdox 2>&1 + echo "## PHPUnit" >> $GITHUB_STEP_SUMMARY + echo "Tests passed." >> $GITHUB_STEP_SUMMARY + elif [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then + echo "::warning::PHPUnit config found but phpunit not installed" + else + echo "No PHPUnit configured — skipping" + fi + + - name: Run Node.js tests + if: steps.detect.outputs.has_node == 'true' + run: | + if jq -e '.scripts.test' package.json > /dev/null 2>&1; then + npm test 2>&1 + echo "## Node.js Tests" >> $GITHUB_STEP_SUMMARY + echo "Tests passed." >> $GITHUB_STEP_SUMMARY + else + echo "No test script in package.json — skipping" + fi + + - name: Build check + run: | + if [ -f "Makefile" ]; then + make build 2>&1 || echo "::warning::Build failed or not configured" + elif [ -f "package.json" ] && jq -e '.scripts.build' package.json > /dev/null 2>&1; then + npm run build 2>&1 || echo "::warning::Build failed" + fi -- 2.52.0 From f99db964517a3ffe64ce4b91db1b6435dd58b825 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 21:09:54 +0000 Subject: [PATCH 47/63] chore: sync ci-issue-reporter.yml from Template-Joomla [skip ci] --- .mokogit/workflows/ci-issue-reporter.yml | 68 ++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .mokogit/workflows/ci-issue-reporter.yml diff --git a/.mokogit/workflows/ci-issue-reporter.yml b/.mokogit/workflows/ci-issue-reporter.yml new file mode 100644 index 0000000..cb1417a --- /dev/null +++ b/.mokogit/workflows/ci-issue-reporter.yml @@ -0,0 +1,68 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGit.Workflow +# INGROUP: MokoCLI.Universal +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# PATH: /.mokogit/workflows/ci-issue-reporter.yml +# VERSION: 01.00.00 +# BRIEF: Reusable workflow — creates/updates a MokoGit issue when a CI gate fails. +# Clones MokoCLI and runs cli/ci_issue_reporter.sh. + +name: "Universal: CI Issue Reporter" + +on: + workflow_call: + inputs: + gate: + description: "CI gate name (e.g. PR Validation, Repository Health)" + required: true + type: string + details: + description: "Human-readable failure description" + required: true + type: string + severity: + description: "error or warning" + required: false + type: string + default: "error" + workflow: + description: "Workflow name for the issue title" + required: false + type: string + default: "" + secrets: + MOKOGIT_TOKEN: + required: true + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + report: + name: "Report: ${{ inputs.gate }}" + runs-on: ubuntu-latest + + steps: + - name: Clone MokoCLI + env: + MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} + run: | + MOKOGIT_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}" + git clone --depth 1 --filter=blob:none --sparse "${MOKOGIT_URL}/MokoConsulting/mokocli.git" /tmp/mokocli + cd /tmp/mokocli && git sparse-checkout set cli/ci_issue_reporter.sh + + - name: Report CI failure + env: + MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} + MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + chmod +x /tmp/mokocli/cli/ci_issue_reporter.sh + /tmp/mokocli/cli/ci_issue_reporter.sh \ + --gate "${{ inputs.gate }}" \ + --details "${{ inputs.details }}" \ + --severity "${{ inputs.severity }}" \ + --workflow "${{ inputs.workflow }}" -- 2.52.0 From fdcc3226d9749dbcfe79010a16dac539a86ca64f Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 21:09:55 +0000 Subject: [PATCH 48/63] chore: sync ci-joomla.yml from Template-Joomla [skip ci] --- .mokogit/workflows/ci-joomla.yml | 2198 ++++++++++++++++++++++++++++++ 1 file changed, 2198 insertions(+) create mode 100644 .mokogit/workflows/ci-joomla.yml diff --git a/.mokogit/workflows/ci-joomla.yml b/.mokogit/workflows/ci-joomla.yml new file mode 100644 index 0000000..5e0077a --- /dev/null +++ b/.mokogit/workflows/ci-joomla.yml @@ -0,0 +1,2198 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGit.Workflow.Template +# INGROUP: MokoCLI.CI +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# PATH: /templates/workflows/joomla/ci-joomla.yml.template +# VERSION: 04.06.00 +# BRIEF: CI workflow for Joomla extensions — lint, validate, test + +name: "Joomla: Extension CI" + +on: + pull_request: + branches: + - main + - dev + - 'dev/**' + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + lint-and-validate: + name: Lint & Validate + runs-on: ubuntu-latest + if: ${{ !startsWith(github.event.repository.name, 'Template-') }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup PHP + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + php -v && composer --version + + - name: Setup MokoCLI tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN || github.token }} + MOKO_CLONE_HOST: ${{ secrets.MOKOGIT_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} + run: | + if [ -d "/opt/mokocli" ] || [ -d "/tmp/mokocli" ]; then + echo "MokoCLI already available on runner — skipping clone" + else + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git" \ + /tmp/mokocli 2>/dev/null || echo "MokoCLI clone skipped — continuing without it" + fi + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.MOKOGIT_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install \ + --no-interaction \ + --prefer-dist \ + --optimize-autoloader + else + echo "No composer.json found — skipping dependency install" + fi + + - name: PHP syntax check + run: | + ERRORS=0 + for DIR in src/ htdocs/; do + if [ -d "$DIR" ]; then + FOUND=1 + while IFS= read -r -d '' FILE; do + OUTPUT=$(php -l "$FILE" 2>&1) + if echo "$OUTPUT" | grep -q "Parse error"; then + echo "::error file=${FILE}::${OUTPUT}" + ERRORS=$((ERRORS + 1)) + fi + done < <(find "$DIR" -name "*.php" -print0) + fi + done + echo "### PHP Syntax Check" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} syntax error(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY + fi + + - name: XML manifest validation + run: | + echo "### XML Manifest Validation" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Find the extension manifest (XML with /dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No Joomla extension manifest found (XML file with \`> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest found: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY + + # Validate well-formed XML + php -r " + \$xml = @simplexml_load_file('$MANIFEST'); + if (\$xml === false) { + echo 'INVALID'; + exit(1); + } + echo 'VALID'; + " > /tmp/xml_result 2>&1 + XML_RESULT=$(cat /tmp/xml_result) + if [ "$XML_RESULT" != "VALID" ]; then + echo "Manifest is not well-formed XML." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY + fi + + # Check required tags: name, version, author + for TAG in name version author; do + if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then + echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY + fi + done + + # Namespace is required for components/plugins but not packages + EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) + if [ "$EXT_TYPE" != "package" ]; then + if ! grep -q "/dev/null; then + echo "Missing required tag: \`\` (required for Joomla 5+ ${EXT_TYPE} extensions)" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Found required tag: \`\`" >> $GITHUB_STEP_SUMMARY + fi + else + echo "Package extension — \`\` not required." >> $GITHUB_STEP_SUMMARY + fi + fi + + if [ "${ERRORS}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${ERRORS} manifest issue(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Update server & packaging checks + continue-on-error: true + run: | + echo "### Update Server & Packaging" >> $GITHUB_STEP_SUMMARY + WARNINGS=0 + + # Find the extension manifest + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY + else + EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) + + # 1. Check exists and uses MokoGit update server + if ! grep -q '' "$MANIFEST" 2>/dev/null; then + echo "::warning file=${MANIFEST}::Missing \`\` tag — extension will not receive OTA updates" + echo "- **Missing** \`\` — extension will not receive OTA updates" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + SERVER_URL=$(grep -oP ']*>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1) + if [ -z "$SERVER_URL" ]; then + echo "::warning file=${MANIFEST}::\`\` is empty — no server URL defined" + echo "- **Empty** \`\` — no server URL defined" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + elif ! echo "$SERVER_URL" | grep -q 'git\.mokoconsulting\.tech'; then + echo "::warning file=${MANIFEST}::Update server does not use MokoGit engine: ${SERVER_URL}" + echo "- **Non-MokoGit update server:** \`${SERVER_URL}\`" >> $GITHUB_STEP_SUMMARY + echo " Expected: \`https://git.mokoconsulting.tech/{org}/{repo}/updates.xml\`" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + echo "- \`\`: MokoGit engine ✓" >> $GITHUB_STEP_SUMMARY + fi + fi + + # 2. Check tag exists + if ! grep -q '/dev/null; then + echo "::warning file=${MANIFEST}::Missing \`\` tag — download ID authentication is not configured" + echo "- **Missing** \`\` — download ID authentication not configured" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + echo "- \`\`: present ✓" >> $GITHUB_STEP_SUMMARY + fi + + # 3. For packages: check tag + if [ "$EXT_TYPE" = "package" ]; then + if ! grep -q '' "$MANIFEST" 2>/dev/null; then + echo "::warning file=${MANIFEST}::Package is missing \`\` — child extensions will not be removed on uninstall" + echo "- **Missing** \`\` — child extensions will remain when package is uninstalled" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + echo "- \`\`: present ✓" >> $GITHUB_STEP_SUMMARY + fi + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$WARNINGS" -gt 0 ]; then + echo "**${WARNINGS} packaging warning(s).** These won't block CI but should be addressed." >> $GITHUB_STEP_SUMMARY + else + echo "**Update server & packaging checks passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check language files referenced in manifest + run: | + echo "### Language File Check" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -n "$MANIFEST" ]; then + # Extract language file references from manifest + LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true) + if [ -z "$LANG_FILES" ]; then + echo "No language file references found in manifest — skipping." >> $GITHUB_STEP_SUMMARY + else + while IFS= read -r LANG_FILE; do + LANG_FILE=$(echo "$LANG_FILE" | xargs) + if [ -z "$LANG_FILE" ]; then + continue + fi + # Check in common locations + FOUND=0 + for BASE in "." "src" "htdocs"; do + if [ -f "${BASE}/${LANG_FILE}" ]; then + FOUND=1 + break + fi + done + if [ "$FOUND" -eq 0 ]; then + echo "Missing language file: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Language file present: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY + fi + done <<< "$LANG_FILES" + fi + else + echo "No manifest found — skipping language check." >> $GITHUB_STEP_SUMMARY + fi + + if [ "${ERRORS}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${ERRORS} missing language file(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Language file check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check index.html files in directories + run: | + echo "### Index.html Check" >> $GITHUB_STEP_SUMMARY + MISSING=0 + CHECKED=0 + + for DIR in src/ htdocs/; do + if [ -d "$DIR" ]; then + while IFS= read -r -d '' SUBDIR; do + CHECKED=$((CHECKED + 1)) + if [ ! -f "${SUBDIR}/index.html" ]; then + echo "Missing index.html in: \`${SUBDIR}\`" >> $GITHUB_STEP_SUMMARY + MISSING=$((MISSING + 1)) + fi + done < <(find "$DIR" -type d -print0) + fi + done + + if [ "${CHECKED}" -eq 0 ]; then + echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY + elif [ "${MISSING}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY + fi + + - name: Check config.xml and access.xml for components + run: | + echo "### Component Config & ACL Check" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Find all component manifests (XML with type="component") + # Uses maxdepth 10 to reach into nested package repos (packages/*/source/packages/com_*/...) + COMP_MANIFESTS=$(find . -maxdepth 10 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./.claude/*" -exec grep -l ']*type="component"' {} \; 2>/dev/null || true) + + if [ -z "$COMP_MANIFESTS" ]; then + echo "No component extensions found — skipping." >> $GITHUB_STEP_SUMMARY + else + for MANIFEST in $COMP_MANIFESTS; do + COMP_DIR=$(dirname "$MANIFEST") + COMP_NAME=$(basename "$COMP_DIR") + echo "Component: `${COMP_NAME}` (manifest: `${MANIFEST}`)" >> $GITHUB_STEP_SUMMARY + + # Check access.xml exists + ACCESS_FILE=$(find "$COMP_DIR" -name "access.xml" -not -path "./.git/*" 2>/dev/null | head -1) + if [ -z "$ACCESS_FILE" ]; then + echo "- Missing `access.xml` — ACL permissions will not work." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + if command -v php &> /dev/null; then + if ! php -r "@simplexml_load_file('$ACCESS_FILE') ?: exit(1);" 2>/dev/null; then + echo "- `access.xml` is not well-formed XML." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + for ACTION in core.admin core.manage; do + if ! grep -q "name=\"${ACTION}\"" "$ACCESS_FILE" 2>/dev/null; then + echo "- `access.xml` missing required action: `${ACTION}`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + echo "- `access.xml`: valid" >> $GITHUB_STEP_SUMMARY + fi + fi + fi + + # Check config.xml exists + CONFIG_FILE=$(find "$COMP_DIR" -name "config.xml" -not -path "./.git/*" 2>/dev/null | head -1) + if [ -z "$CONFIG_FILE" ]; then + echo "- Missing `config.xml` — component Options page will be empty." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + if command -v php &> /dev/null; then + if ! php -r "@simplexml_load_file('$CONFIG_FILE') ?: exit(1);" 2>/dev/null; then + echo "- `config.xml` is not well-formed XML." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- `config.xml`: valid" >> $GITHUB_STEP_SUMMARY + fi + fi + fi + done + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} config/ACL issue(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Component config & ACL check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: SQL schema validation + run: | + echo "### SQL Schema Validation" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Find SQL files in source/htdocs + SQL_FILES=$(find . -name "*.sql" -path "*/sql/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) + if [ -z "$SQL_FILES" ]; then + echo "No SQL files found — skipping." >> $GITHUB_STEP_SUMMARY + else + echo "Found $(echo "$SQL_FILES" | wc -l) SQL file(s)" >> $GITHUB_STEP_SUMMARY + + for FILE in $SQL_FILES; do + # 1. Empty / whitespace-only file check + SIZE=$(wc -c < "$FILE" | tr -d ' ') + if [ "$SIZE" -eq 0 ]; then + echo "::error file=${FILE}::Empty SQL file" + echo "- **Empty** SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + continue + fi + CONTENT_SIZE=$(grep -cP '\S' "$FILE" 2>/dev/null || echo "0") + if [ "$CONTENT_SIZE" -eq 0 ]; then + echo "::error file=${FILE}::Whitespace-only SQL file" + echo "- **Whitespace-only** SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + continue + fi + + echo "- \`${FILE}\`: ${SIZE} bytes" >> $GITHUB_STEP_SUMMARY + + # 2. Joomla table prefix — must use #__ not hardcoded prefixes + # Match table names in CREATE/ALTER/DROP/INSERT/UPDATE/DELETE that don't use #__ + HARDCODED=$(grep -nPi '(CREATE\s+TABLE|ALTER\s+TABLE|DROP\s+TABLE|INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+(`?)(?!#__)[a-z][a-z0-9_]+\2' "$FILE" 2>/dev/null || true) + if [ -n "$HARDCODED" ]; then + COUNT=$(echo "$HARDCODED" | wc -l) + echo "::error file=${FILE}::${COUNT} table reference(s) missing #__ prefix" + echo " - **${COUNT} hardcoded table name(s)** — must use \`#__\` prefix" >> $GITHUB_STEP_SUMMARY + while IFS= read -r LINE; do + LINE_NUM=$(echo "$LINE" | cut -d: -f1) + LINE_TEXT=$(echo "$LINE" | cut -d: -f2- | sed 's/^ *//') + echo " - Line ${LINE_NUM}: \`${LINE_TEXT}\`" >> $GITHUB_STEP_SUMMARY + done <<< "$HARDCODED" + ERRORS=$((ERRORS + COUNT)) + fi + + # 3. CREATE TABLE must use IF NOT EXISTS + BAD_CREATE=$(grep -nPi 'CREATE\s+TABLE\s+(?!IF\s+NOT\s+EXISTS)' "$FILE" 2>/dev/null || true) + if [ -n "$BAD_CREATE" ]; then + COUNT=$(echo "$BAD_CREATE" | wc -l) + echo "::error file=${FILE}::${COUNT} CREATE TABLE without IF NOT EXISTS" + echo " - **${COUNT} CREATE TABLE** missing \`IF NOT EXISTS\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + COUNT)) + fi + + # 4. DROP TABLE must use IF EXISTS + BAD_DROP=$(grep -nPi 'DROP\s+TABLE\s+(?!IF\s+EXISTS)' "$FILE" 2>/dev/null || true) + if [ -n "$BAD_DROP" ]; then + COUNT=$(echo "$BAD_DROP" | wc -l) + echo "::error file=${FILE}::${COUNT} DROP TABLE without IF EXISTS" + echo " - **${COUNT} DROP TABLE** missing \`IF EXISTS\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + COUNT)) + fi + + # 5. Balanced parentheses in CREATE TABLE statements + OPEN=$(grep -oP '\(' "$FILE" 2>/dev/null | wc -l) + CLOSE=$(grep -oP '\)' "$FILE" 2>/dev/null | wc -l) + if [ "$OPEN" -ne "$CLOSE" ]; then + echo "::error file=${FILE}::Unbalanced parentheses (${OPEN} open vs ${CLOSE} close)" + echo " - **Unbalanced parentheses**: ${OPEN} \`(\` vs ${CLOSE} \`)\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + # 6. ENGINE=InnoDB must be specified on CREATE TABLE + if grep -qPi 'CREATE\s+TABLE' "$FILE" 2>/dev/null; then + if ! grep -qPi 'ENGINE\s*=\s*InnoDB' "$FILE" 2>/dev/null; then + echo "::error file=${FILE}::CREATE TABLE missing ENGINE=InnoDB" + echo " - **Missing \`ENGINE=InnoDB\`** — required for Joomla 4+/5" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + + # 7. DEFAULT CHARSET=utf8mb4 must be specified on CREATE TABLE + if grep -qPi 'CREATE\s+TABLE' "$FILE" 2>/dev/null; then + if ! grep -qPi 'CHARSET\s*=\s*utf8mb4|CHARACTER\s+SET\s+utf8mb4' "$FILE" 2>/dev/null; then + echo "::error file=${FILE}::CREATE TABLE missing DEFAULT CHARSET=utf8mb4" + echo " - **Missing \`DEFAULT CHARSET=utf8mb4\`** — required for Joomla 4+/5" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + done + + # 8. Update SQL files must follow version numbering pattern + UPDATE_DIRS=$(find . -path "*/sql/updates/mysql" -type d -not -path "./.git/*" 2>/dev/null) + if [ -n "$UPDATE_DIRS" ]; then + while IFS= read -r UPDATE_DIR; do + for UFILE in "$UPDATE_DIR"/*.sql; do + [ ! -f "$UFILE" ] && continue + BASENAME=$(basename "$UFILE" .sql) + if ! echo "$BASENAME" | grep -qP '^\d+\.\d+\.\d+'; then + echo "::error file=${UFILE}::Update file does not follow version naming (expected X.Y.Z.sql)" + echo "- Update file \`${UFILE}\` does not follow version naming (expected X.Y.Z.sql)" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + done <<< "$UPDATE_DIRS" + fi + + # 9. Install/uninstall pairing — if install exists, uninstall must too + INSTALL_FILES=$(find . -name "install.mysql.sql" -path "*/sql/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) + if [ -n "$INSTALL_FILES" ]; then + while IFS= read -r INSTALL_FILE; do + SQL_DIR=$(dirname "$INSTALL_FILE") + if [ ! -f "${SQL_DIR}/uninstall.mysql.sql" ]; then + echo "::error file=${INSTALL_FILE}::install.mysql.sql found but uninstall.mysql.sql is missing" + echo "- **Missing \`uninstall.mysql.sql\`** in \`${SQL_DIR}/\` — every install must have a matching uninstall" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done <<< "$INSTALL_FILES" + fi + + # 10 & 11. Per-component schema/version consistency. Each component + # versions independently, so resolve every sql/updates/mysql dir + # against ITS OWN manifest (not one package-level version): + # (10) the component's manifest version must have a matching update file + # (11) no update file may exceed the component's manifest version + # (this is the guard that catches migration files drifting onto + # a different numbering lane and getting ahead of the release) + UPDATE_DIRS=$(find . -path "*/sql/updates/mysql" -type d -not -path "./.git/*" 2>/dev/null) + if [ -n "$UPDATE_DIRS" ]; then + while IFS= read -r UPDATE_DIR; do + # Component root is three levels up from /sql/updates/mysql + COMP_ROOT=$(dirname "$(dirname "$(dirname "$UPDATE_DIR")")") + # Resolve this component's OWN manifest (XML with at its root) + COMP_MANIFEST="" + for XML_FILE in "$COMP_ROOT"/*.xml; do + [ -f "$XML_FILE" ] || continue + if grep -q "/dev/null; then + COMP_MANIFEST="$XML_FILE" + break + fi + done + if [ -z "$COMP_MANIFEST" ]; then + echo "- No component manifest found for \`${UPDATE_DIR}\` — skipping version match" >> $GITHUB_STEP_SUMMARY + continue + fi + COMP_VERSION=$(grep -oP '\K[^<]+' "$COMP_MANIFEST" 2>/dev/null | head -1) + [ -z "$COMP_VERSION" ] && continue + # Normalise: strip any pre-release suffix (-dev, -rc.N, ...) for comparison + CV_BASE=$(echo "$COMP_VERSION" | sed -E 's/-.*$//') + # (10) a matching update file must exist for the component version + if [ ! -f "${UPDATE_DIR}/${CV_BASE}.sql" ]; then + echo "::error file=${COMP_MANIFEST}::No update SQL file for version ${CV_BASE}" + echo "- **Missing \`${UPDATE_DIR}/${CV_BASE}.sql\`** — update file must exist for manifest version \`${CV_BASE}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + # (11) no update file may exceed the component version + for UFILE in "$UPDATE_DIR"/*.sql; do + [ -f "$UFILE" ] || continue + UVER=$(basename "$UFILE" .sql) + echo "$UVER" | grep -qP '^\d+\.\d+\.\d+$' || continue + TOP=$(printf '%s\n%s\n' "$CV_BASE" "$UVER" | sort -V | tail -1) + if [ "$UVER" != "$CV_BASE" ] && [ "$TOP" = "$UVER" ]; then + echo "::error file=${UFILE}::Update file ${UVER} exceeds manifest version ${CV_BASE}" + echo "- **Update file \`${UVER}.sql\` exceeds manifest version \`${CV_BASE}\`** — renumber onto the release lane (must be <= manifest)" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + done <<< "$UPDATE_DIRS" + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} SQL issue(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**SQL schema validation passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Manifest file references check + run: | + echo "### Manifest File References" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY + else + MANIFEST_DIR=$(dirname "$MANIFEST") + + # Check references + FILENAMES=$(grep -oP ']*>\K[^<]+' "$MANIFEST" 2>/dev/null || true) + for F in $FILENAMES; do + if [ ! -f "${MANIFEST_DIR}/${F}" ] && [ ! -d "${MANIFEST_DIR}/${F}" ]; then + echo "- Missing: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + + # Check references + FOLDERS=$(grep -oP ']*>\K[^<]+' "$MANIFEST" 2>/dev/null || true) + for F in $FOLDERS; do + if [ ! -d "${MANIFEST_DIR}/${F}" ]; then + echo "- Missing folder: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + + # Check references in package manifests (ZIP files won't exist in source) + EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) + if [ "$EXT_TYPE" != "package" ]; then + FILES=$(grep -oP ']*>\K[^<]+' "$MANIFEST" 2>/dev/null || true) + for F in $FILES; do + if [ ! -f "${MANIFEST_DIR}/${F}" ]; then + echo "- Missing file: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} missing file reference(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Manifest file references check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Form XML validation + run: | + echo "### Form XML Validation" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + FORM_FILES=$(find . -name "*.xml" -path "*/forms/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) + if [ -z "$FORM_FILES" ]; then + echo "No form XML files found — skipping." >> $GITHUB_STEP_SUMMARY + else + echo "Found $(echo "$FORM_FILES" | wc -l) form file(s)" >> $GITHUB_STEP_SUMMARY + for FILE in $FORM_FILES; do + if command -v php &> /dev/null; then + if ! php -r "@simplexml_load_file('$FILE') ?: exit(1);" 2>/dev/null; then + echo "- \`${FILE}\`: malformed XML" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + # Check for valid Joomla form structure + if ! grep -qE '/dev/null; then + echo "- \`${FILE}\`: no \`
\`, \`\`, or \`
\` elements found" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- \`${FILE}\`: valid" >> $GITHUB_STEP_SUMMARY + fi + fi + fi + done + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} form XML issue(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Form XML validation passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Deprecated Joomla API check + continue-on-error: true + run: | + echo "### Deprecated Joomla API Check" >> $GITHUB_STEP_SUMMARY + WARNINGS=0 + + SRC_DIR="" + for DIR in source/ src/ htdocs/; do + [ -d "$DIR" ] && SRC_DIR="$DIR" && break + done + + if [ -z "$SRC_DIR" ]; then + echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY + else + # Joomla 3/4 deprecated patterns that break in Joomla 6 + PATTERNS=( + 'JFactory::' + 'JText::' + 'JHtml::' + 'JRoute::' + 'JUri::' + 'JLog::' + 'JTable::' + 'JInput' + 'CMSFactory::\$application' + 'JApplicationCms' + ) + + for PATTERN in "${PATTERNS[@]}"; do + HITS=$(grep -rnl "$PATTERN" "$SRC_DIR" --include="*.php" 2>/dev/null || true) + if [ -n "$HITS" ]; then + COUNT=$(echo "$HITS" | wc -l) + echo "- \`${PATTERN}\` found in ${COUNT} file(s)" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + COUNT)) + fi + done + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$WARNINGS" -gt 0 ]; then + echo "**${WARNINGS} deprecated API usage(s) found.** These will break in Joomla 6." >> $GITHUB_STEP_SUMMARY + else + echo "**No deprecated APIs found.**" >> $GITHUB_STEP_SUMMARY + fi + fi + + - name: Template output escaping check + continue-on-error: true + run: | + echo "### Template Output Escaping" >> $GITHUB_STEP_SUMMARY + WARNINGS=0 + + TMPL_FILES=$(find . -name "*.php" -path "*/tmpl/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) + if [ -z "$TMPL_FILES" ]; then + echo "No template files found — skipping." >> $GITHUB_STEP_SUMMARY + else + echo "Found $(echo "$TMPL_FILES" | wc -l) template file(s)" >> $GITHUB_STEP_SUMMARY + + for FILE in $TMPL_FILES; do + # Check for unescaped output: or echo $var without escape() + UNESCAPED=$(grep -nP '<\?=\s*\$(?!this->escape)' "$FILE" 2>/dev/null || true) + if [ -n "$UNESCAPED" ]; then + HITS=$(echo "$UNESCAPED" | wc -l) + echo "- \`${FILE}\`: ${HITS} unescaped \`\` output(s) — use \`escape(\$var) ?>\`" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + HITS)) + fi + + # Check for echo without escaping in template context + RAW_ECHO=$(grep -nP '^\s*echo\s+\$(?!this->escape)' "$FILE" 2>/dev/null || true) + if [ -n "$RAW_ECHO" ]; then + HITS=$(echo "$RAW_ECHO" | wc -l) + echo "- \`${FILE}\`: ${HITS} raw \`echo \$var\` — consider \`echo \$this->escape(\$var)\`" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + HITS)) + fi + done + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$WARNINGS" -gt 0 ]; then + echo "**${WARNINGS} potential XSS risk(s) in templates.** Review unescaped output." >> $GITHUB_STEP_SUMMARY + else + echo "**All template output appears properly escaped.**" >> $GITHUB_STEP_SUMMARY + fi + fi + + - name: Namespace consistency check + run: | + echo "### Namespace Consistency" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Find component/plugin manifests with tags + MANIFESTS=$(find . -maxdepth 4 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -exec grep -l '/dev/null || true) + + if [ -z "$MANIFESTS" ]; then + echo "No manifests with \`\` found — skipping." >> $GITHUB_STEP_SUMMARY + else + for MANIFEST in $MANIFESTS; do + NS_PATH=$(grep -oP ']*>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1) + [ -z "$NS_PATH" ] && continue + MANIFEST_DIR=$(dirname "$MANIFEST") + + echo "Manifest: \`${MANIFEST}\` → namespace \`${NS_PATH}\`" >> $GITHUB_STEP_SUMMARY + + # Check PHP files have matching namespace + while IFS= read -r -d '' PHP_FILE; do + FILE_NS=$(grep -oP '^\s*namespace\s+\K[^;]+' "$PHP_FILE" 2>/dev/null | head -1) + [ -z "$FILE_NS" ] && continue + + # Namespace should start with the manifest namespace path + if ! echo "$FILE_NS" | grep -qF "${NS_PATH}"; then + echo "- \`${PHP_FILE}\`: namespace \`${FILE_NS}\` doesn't match manifest \`${NS_PATH}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done < <(find "$MANIFEST_DIR" -name "*.php" -path "*/src/*" -not -path "./vendor/*" -print0 2>/dev/null) + done + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} namespace mismatch(es).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Namespace consistency check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: SPDX license header check + continue-on-error: true + run: | + echo "### SPDX License Headers" >> $GITHUB_STEP_SUMMARY + MISSING=0 + + SRC_DIR="" + for DIR in source/ src/ htdocs/; do + [ -d "$DIR" ] && SRC_DIR="$DIR" && break + done + + if [ -z "$SRC_DIR" ]; then + echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY + else + TOTAL=0 + while IFS= read -r -d '' FILE; do + TOTAL=$((TOTAL + 1)) + if ! head -10 "$FILE" | grep -qi "SPDX"; then + echo "- Missing SPDX header: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY + MISSING=$((MISSING + 1)) + fi + done < <(find "$SRC_DIR" -name "*.php" -not -path "./vendor/*" -print0) + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$MISSING" -gt 0 ]; then + echo "**${MISSING}/${TOTAL} PHP file(s) missing SPDX license header.**" >> $GITHUB_STEP_SUMMARY + else + echo "**All ${TOTAL} PHP files have SPDX headers.**" >> $GITHUB_STEP_SUMMARY + fi + fi + + - name: Service provider check + run: | + echo "### Service Provider Check" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + PROVIDERS=$(find . -name "provider.php" -path "*/services/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) + if [ -z "$PROVIDERS" ]; then + echo "No service providers found — skipping." >> $GITHUB_STEP_SUMMARY + else + for FILE in $PROVIDERS; do + # Must return a ServiceProviderInterface + if ! grep -qP 'ServiceProviderInterface|ComponentInterface|MVCFactoryInterface|DispatcherInterface' "$FILE" 2>/dev/null; then + echo "- \`${FILE}\`: does not reference ServiceProviderInterface or component interfaces" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- \`${FILE}\`: valid service provider" >> $GITHUB_STEP_SUMMARY + fi + + # Must have return statement + if ! grep -qP '^\s*return\s+new\s+' "$FILE" 2>/dev/null; then + echo "- \`${FILE}\`: missing \`return new ...\` statement" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} service provider issue(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Service provider check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Script file reference check + run: | + echo "### Script File Reference" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY + else + MANIFEST_DIR=$(dirname "$MANIFEST") + SCRIPT_FILE=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1) + if [ -z "$SCRIPT_FILE" ]; then + echo "No \`\` referenced — skipping." >> $GITHUB_STEP_SUMMARY + elif [ ! -f "${MANIFEST_DIR}/${SCRIPT_FILE}" ]; then + echo "::error file=${MANIFEST}::Manifest references \`${SCRIPT_FILE}\` but file does not exist" + echo "- **Missing** \`${SCRIPT_FILE}\` — referenced in \`\` but not found" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- \`${SCRIPT_FILE}\`: present ✓" >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} script file issue(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Script file reference check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Media folder validation + run: | + echo "### Media Folder Validation" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY + else + MANIFEST_DIR=$(dirname "$MANIFEST") + + # Check tag and its folder/filename children + MEDIA_DEST=$(grep -oP ']*\bdestination="\K[^"]+' "$MANIFEST" 2>/dev/null | head -1) + MEDIA_FOLDER=$(grep -oP ']*\bfolder="\K[^"]+' "$MANIFEST" 2>/dev/null | head -1) + + if [ -z "$MEDIA_DEST" ] && [ -z "$MEDIA_FOLDER" ]; then + echo "No \`\` tag found — skipping." >> $GITHUB_STEP_SUMMARY + else + if [ -n "$MEDIA_FOLDER" ] && [ ! -d "${MANIFEST_DIR}/${MEDIA_FOLDER}" ]; then + echo "::error file=${MANIFEST}::\`\` references missing directory" + echo "- **Missing** media folder \`${MEDIA_FOLDER}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- Media folder \`${MEDIA_FOLDER:-(inline)}\`: present ✓" >> $GITHUB_STEP_SUMMARY + + # Check child references inside block + if [ -n "$MEDIA_FOLDER" ]; then + MEDIA_FOLDERS=$(sed -n '//p' "$MANIFEST" | grep -oP '\K[^<]+' 2>/dev/null || true) + for F in $MEDIA_FOLDERS; do + if [ ! -d "${MANIFEST_DIR}/${MEDIA_FOLDER}/${F}" ]; then + echo "- **Missing** media subfolder \`${MEDIA_FOLDER}/${F}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + + MEDIA_FILES=$(sed -n '//p' "$MANIFEST" | grep -oP '\K[^<]+' 2>/dev/null || true) + for F in $MEDIA_FILES; do + if [ ! -f "${MANIFEST_DIR}/${MEDIA_FOLDER}/${F}" ]; then + echo "- **Missing** media file \`${MEDIA_FOLDER}/${F}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + fi + fi + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} media reference issue(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Media folder validation passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Target platform check + continue-on-error: true + run: | + echo "### Target Platform Check" >> $GITHUB_STEP_SUMMARY + WARNINGS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY + else + # Check updates.xml for targetplatform if it exists + if [ -f "updates.xml" ]; then + if ! grep -q '/dev/null; then + echo "::warning file=updates.xml::No \`\` found — Joomla updater cannot filter by compatible version" + echo "- **Missing** \`\` in updates.xml" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + echo "- \`\` in updates.xml: present ✓" >> $GITHUB_STEP_SUMMARY + fi + fi + + # Check manifest for minimum PHP/Joomla version hints + if ! grep -qP '|targetplatform|joomla.*version' "$MANIFEST" 2>/dev/null; then + echo "::warning file=${MANIFEST}::No minimum Joomla or PHP version constraint found in manifest" + echo "- **Missing** version constraints (\`\` or \`\`)" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + echo "- Version constraints in manifest: present ✓" >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$WARNINGS" -gt 0 ]; then + echo "**${WARNINGS} target platform warning(s).**" >> $GITHUB_STEP_SUMMARY + else + echo "**Target platform check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Changelog URL check + continue-on-error: true + run: | + echo "### Changelog URL Check" >> $GITHUB_STEP_SUMMARY + WARNINGS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY + else + if ! grep -q '' "$MANIFEST" 2>/dev/null; then + echo "::warning file=${MANIFEST}::Missing \`\` — Joomla updater will not display changelogs" + echo "- **Missing** \`\` — Joomla 4+ shows changelogs in the update manager when this is set" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + CHANGELOG_URL=$(grep -oP '\K[^<]+' "$MANIFEST" | head -1) + echo "- \`\`: \`${CHANGELOG_URL}\` ✓" >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$WARNINGS" -gt 0 ]; then + echo "**${WARNINGS} changelog URL warning(s).**" >> $GITHUB_STEP_SUMMARY + else + echo "**Changelog URL check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Duplicate file references check + continue-on-error: true + run: | + echo "### Duplicate File References" >> $GITHUB_STEP_SUMMARY + WARNINGS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY + else + # Extract all and references + ALL_REFS=$(grep -oP '<(filename|folder)[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null | sort || true) + if [ -z "$ALL_REFS" ]; then + echo "No file/folder references found — skipping." >> $GITHUB_STEP_SUMMARY + else + DUPES=$(echo "$ALL_REFS" | uniq -d) + if [ -n "$DUPES" ]; then + while IFS= read -r DUP; do + COUNT=$(echo "$ALL_REFS" | grep -cx "$DUP") + echo "::warning file=${MANIFEST}::Duplicate reference: \`${DUP}\` appears ${COUNT} times (may be valid if in different sections)" + echo "- **Duplicate:** \`${DUP}\` (${COUNT}x) — check if cross-section" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + done <<< "$DUPES" + else + TOTAL=$(echo "$ALL_REFS" | wc -l) + echo "All ${TOTAL} file/folder references are unique." >> $GITHUB_STEP_SUMMARY + fi + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$WARNINGS" -gt 0 ]; then + echo "**${WARNINGS} duplicate reference(s) found.** Review for cross-section validity." >> $GITHUB_STEP_SUMMARY + else + echo "**Duplicate file references check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Empty language keys check + continue-on-error: true + run: | + echo "### Empty Language Keys" >> $GITHUB_STEP_SUMMARY + WARNINGS=0 + + LANG_FILES=$(find . -name "*.ini" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) + if [ -z "$LANG_FILES" ]; then + echo "No .ini language files found — skipping." >> $GITHUB_STEP_SUMMARY + else + TOTAL_FILES=0 + for FILE in $LANG_FILES; do + TOTAL_FILES=$((TOTAL_FILES + 1)) + # Find lines with KEY= but no value (empty or whitespace-only after =) + EMPTY_KEYS=$(grep -nP '^[A-Z_]+=\s*$' "$FILE" 2>/dev/null || true) + if [ -n "$EMPTY_KEYS" ]; then + COUNT=$(echo "$EMPTY_KEYS" | wc -l) + echo "::warning file=${FILE}::${COUNT} empty language key(s)" + echo "- \`${FILE}\`: ${COUNT} empty key(s)" >> $GITHUB_STEP_SUMMARY + while IFS= read -r LINE; do + LINE_NUM=$(echo "$LINE" | cut -d: -f1) + KEY=$(echo "$LINE" | cut -d: -f2 | cut -d= -f1) + echo " - Line ${LINE_NUM}: \`${KEY}\`" >> $GITHUB_STEP_SUMMARY + done <<< "$EMPTY_KEYS" + WARNINGS=$((WARNINGS + COUNT)) + fi + done + + if [ "$WARNINGS" -eq 0 ]; then + echo "All ${TOTAL_FILES} language file(s) have populated keys." >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$WARNINGS" -gt 0 ]; then + echo "**${WARNINGS} empty language key(s) across ${TOTAL_FILES} file(s).**" >> $GITHUB_STEP_SUMMARY + else + echo "**Empty language keys check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Language key usage consistency + run: | + echo "### Language Key Usage Consistency" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Collect all defined language keys from .ini files + LANG_FILES=$(find . -name "*.ini" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) + if [ -z "$LANG_FILES" ]; then + echo "No .ini language files found — skipping." >> $GITHUB_STEP_SUMMARY + else + # Build list of defined keys + DEFINED_KEYS=$(cat $LANG_FILES 2>/dev/null | grep -oP '^[A-Z][A-Z0-9_]+(?==)' | sort -u) + if [ -z "$DEFINED_KEYS" ]; then + echo "No language keys defined — skipping." >> $GITHUB_STEP_SUMMARY + else + DEFINED_COUNT=$(echo "$DEFINED_KEYS" | wc -l) + echo "Found ${DEFINED_COUNT} defined key(s) across .ini files" >> $GITHUB_STEP_SUMMARY + + # Find keys used in PHP files (Text::_('KEY'), Text::sprintf('KEY'), etc.) + SRC_DIR="" + for DIR in source/ src/ htdocs/; do + [ -d "$DIR" ] && SRC_DIR="$DIR" && break + done + + if [ -n "$SRC_DIR" ]; then + USED_IN_PHP=$(grep -rohP "Text::(?:_|sprintf|plural|alt)\(\s*['\"]([A-Z][A-Z0-9_]+)['\"]" "$SRC_DIR" --include="*.php" 2>/dev/null | grep -oP "[A-Z][A-Z0-9_]+" | sort -u || true) + fi + + # Find keys used in XML files (label=, description=, etc.) + USED_IN_XML=$(find . -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -exec grep -ohP '(?:label|description|title|message|note)="([A-Z][A-Z0-9_]+)"' {} \; 2>/dev/null | grep -oP '[A-Z][A-Z0-9_]+' | sort -u || true) + + ALL_USED=$(printf '%s\n%s' "$USED_IN_PHP" "$USED_IN_XML" | sort -u | grep -v '^$' || true) + + if [ -n "$ALL_USED" ]; then + # Keys used but not defined + MISSING=$(comm -23 <(echo "$ALL_USED") <(echo "$DEFINED_KEYS") 2>/dev/null || true) + if [ -n "$MISSING" ]; then + COUNT=$(echo "$MISSING" | wc -l) + echo "::error::${COUNT} language key(s) used in code but not defined in .ini files" + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Used but not defined:**" >> $GITHUB_STEP_SUMMARY + while IFS= read -r KEY; do + echo "- \`${KEY}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + done <<< "$MISSING" + fi + else + echo "No language key usage found in source files." >> $GITHUB_STEP_SUMMARY + fi + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} undefined language key(s) — these will appear as raw key strings in the UI.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Language key usage consistency passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Orphan language keys + run: | + echo "### Orphan Language Keys" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + LANG_FILES=$(find . -name "*.ini" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) + if [ -z "$LANG_FILES" ]; then + echo "No .ini language files found — skipping." >> $GITHUB_STEP_SUMMARY + else + DEFINED_KEYS=$(cat $LANG_FILES 2>/dev/null | grep -oP '^[A-Z][A-Z0-9_]+(?==)' | sort -u) + if [ -z "$DEFINED_KEYS" ]; then + echo "No language keys defined — skipping." >> $GITHUB_STEP_SUMMARY + else + # Search all PHP, XML, and INI files for key references + ALL_SOURCE=$(find . \( -name "*.php" -o -name "*.xml" -o -name "*.ini" -o -name "*.js" \) -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" 2>/dev/null) + ORPHANS="" + while IFS= read -r KEY; do + # Skip sys.ini _DESC/_TITLE keys — these are Joomla convention keys + # Search for the key in all source files + if ! grep -rql "$KEY" --include="*.php" --include="*.xml" --include="*.js" . 2>/dev/null | grep -qv '\.ini' 2>/dev/null; then + ORPHANS="${ORPHANS}${KEY}\n" + fi + done <<< "$DEFINED_KEYS" + + ORPHANS=$(printf '%b' "$ORPHANS" | grep -v '^$' || true) + if [ -n "$ORPHANS" ]; then + COUNT=$(echo "$ORPHANS" | wc -l) + echo "::error::${COUNT} language key(s) defined in .ini but never referenced in code" + echo "**Orphan keys (defined but unreferenced):**" >> $GITHUB_STEP_SUMMARY + while IFS= read -r KEY; do + echo "- \`${KEY}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + done <<< "$ORPHANS" + fi + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} orphan language key(s) — remove unused keys to reduce translation burden.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Orphan language keys check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: PHP CodeSniffer (Joomla standard) + run: | + echo "### PHP CodeSniffer" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + SRC_DIR="" + for DIR in source/ src/ htdocs/; do + [ -d "$DIR" ] && SRC_DIR="$DIR" && break + done + + if [ -z "$SRC_DIR" ]; then + echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY + else + # Install PHPCS + Joomla coding standard if not already available + PHPCS="" + if [ -f "vendor/bin/phpcs" ]; then + PHPCS="vendor/bin/phpcs" + else + composer global require --no-interaction --quiet \ + squizlabs/php_codesniffer \ + joomla/coding-standards 2>/dev/null || true + GLOBAL_BIN=$(composer global config bin-dir --absolute 2>/dev/null) + if [ -f "${GLOBAL_BIN}/phpcs" ]; then + PHPCS="${GLOBAL_BIN}/phpcs" + # Register Joomla standard + GLOBAL_VENDOR=$(composer global config vendor-dir --absolute 2>/dev/null) + if [ -d "${GLOBAL_VENDOR}/joomla/coding-standards" ]; then + $PHPCS --config-set installed_paths "${GLOBAL_VENDOR}/joomla/coding-standards" 2>/dev/null || true + fi + fi + fi + + if [ -z "$PHPCS" ]; then + echo "Could not install PHP CodeSniffer — skipping." >> $GITHUB_STEP_SUMMARY + else + # Use repo phpcs.xml if present, otherwise use Joomla standard + ARGS="--report=summary --no-colors" + if [ -f "phpcs.xml" ] || [ -f "phpcs.xml.dist" ] || [ -f ".phpcs.xml" ]; then + echo "Using project PHPCS config." >> $GITHUB_STEP_SUMMARY + else + # Check if Joomla standard is available + if $PHPCS -i 2>/dev/null | grep -qi "Joomla"; then + ARGS="$ARGS --standard=Joomla" + echo "Using Joomla coding standard." >> $GITHUB_STEP_SUMMARY + else + ARGS="$ARGS --standard=PSR12" + echo "Joomla standard not available — falling back to PSR-12." >> $GITHUB_STEP_SUMMARY + fi + ARGS="$ARGS ${SRC_DIR}" + fi + + $PHPCS $ARGS 2>&1 | tee /tmp/phpcs-output.txt + EXIT=${PIPESTATUS[0]} + + if [ $EXIT -eq 0 ]; then + echo "**No coding standard violations found.**" >> $GITHUB_STEP_SUMMARY + else + echo '```' >> $GITHUB_STEP_SUMMARY + cat /tmp/phpcs-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "**Coding standard violations found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + fi + fi + + - name: Security patterns check + run: | + echo "### Security Patterns" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + SRC_DIR="" + for DIR in source/ src/ htdocs/; do + [ -d "$DIR" ] && SRC_DIR="$DIR" && break + done + + if [ -z "$SRC_DIR" ]; then + echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY + else + # 1. Direct superglobal access ($_GET, $_POST, $_REQUEST, $_COOKIE) + SUPERGLOBAL_HITS=$(grep -rnP '\$_(GET|POST|REQUEST|COOKIE|FILES)\b' "$SRC_DIR" --include="*.php" 2>/dev/null || true) + if [ -n "$SUPERGLOBAL_HITS" ]; then + COUNT=$(echo "$SUPERGLOBAL_HITS" | wc -l) + echo "::error::${COUNT} direct superglobal access(es) — use Joomla Input API instead" + echo "**Direct superglobal access (use \`\$app->getInput()\` or \`InputFilter\`):**" >> $GITHUB_STEP_SUMMARY + while IFS= read -r HIT; do + FILE=$(echo "$HIT" | cut -d: -f1) + LINE=$(echo "$HIT" | cut -d: -f2) + echo "- \`${FILE}:${LINE}\`" >> $GITHUB_STEP_SUMMARY + done <<< "$SUPERGLOBAL_HITS" + ERRORS=$((ERRORS + COUNT)) + fi + + # 2. Raw SQL query construction (string concatenation in query) + RAW_SQL=$(grep -rnP '\$db->setQuery\(\s*["\x27]' "$SRC_DIR" --include="*.php" 2>/dev/null || true) + if [ -n "$RAW_SQL" ]; then + COUNT=$(echo "$RAW_SQL" | wc -l) + echo "::error::${COUNT} raw SQL string(s) in setQuery() — use query builder or prepared statements" + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Raw SQL strings in \`setQuery()\` (use query builder):**" >> $GITHUB_STEP_SUMMARY + while IFS= read -r HIT; do + FILE=$(echo "$HIT" | cut -d: -f1) + LINE=$(echo "$HIT" | cut -d: -f2) + echo "- \`${FILE}:${LINE}\`" >> $GITHUB_STEP_SUMMARY + done <<< "$RAW_SQL" + ERRORS=$((ERRORS + COUNT)) + fi + + # 3. eval() usage + EVAL_HITS=$(grep -rnP '\beval\s*\(' "$SRC_DIR" --include="*.php" 2>/dev/null || true) + if [ -n "$EVAL_HITS" ]; then + COUNT=$(echo "$EVAL_HITS" | wc -l) + echo "::error::${COUNT} eval() usage(s) — never use eval in extensions" + echo "" >> $GITHUB_STEP_SUMMARY + echo "**\`eval()\` usage (critical security risk):**" >> $GITHUB_STEP_SUMMARY + while IFS= read -r HIT; do + FILE=$(echo "$HIT" | cut -d: -f1) + LINE=$(echo "$HIT" | cut -d: -f2) + echo "- \`${FILE}:${LINE}\`" >> $GITHUB_STEP_SUMMARY + done <<< "$EVAL_HITS" + ERRORS=$((ERRORS + COUNT)) + fi + + # 4. Missing defined('_JEXEC') or JDEFINED guard + while IFS= read -r -d '' PHP_FILE; do + FIRST_PHP=$(grep -nP '^\s*<\?php' "$PHP_FILE" 2>/dev/null | head -1) + if [ -n "$FIRST_PHP" ]; then + # Check first 5 non-empty lines after /dev/null; then + # Skip service provider files (they use a different entry point) + if ! echo "$PHP_FILE" | grep -q "provider.php"; then + echo "::error file=${PHP_FILE}::Missing _JEXEC direct access guard" + echo "- \`${PHP_FILE}\`: missing \`defined('_JEXEC') or die;\` guard" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + fi + done < <(find "$SRC_DIR" -name "*.php" -not -path "./vendor/*" -print0 2>/dev/null) + + if [ "$ERRORS" -eq 0 ]; then + echo "No security issues found." >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} security issue(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Security patterns check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: updates.xml structure validation + run: | + echo "### updates.xml Validation" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + UPDATE_XML=$(find . -maxdepth 2 -name "updates.xml" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null | head -1) + if [ -z "$UPDATE_XML" ]; then + echo "No updates.xml found — skipping." >> $GITHUB_STEP_SUMMARY + else + echo "Found: \`${UPDATE_XML}\`" >> $GITHUB_STEP_SUMMARY + + # 1. Well-formed XML + if command -v php &> /dev/null; then + if ! php -r "@simplexml_load_file('$UPDATE_XML') ?: exit(1);" 2>/dev/null; then + echo "::error file=${UPDATE_XML}::updates.xml is not well-formed XML" + echo "- **Malformed XML** — cannot be parsed" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- Well-formed XML ✓" >> $GITHUB_STEP_SUMMARY + + # 2. Must have root element + if ! grep -q '' "$UPDATE_XML" 2>/dev/null; then + echo "::error file=${UPDATE_XML}::Missing root element" + echo "- **Missing** \`\` root element" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- \`\` root element ✓" >> $GITHUB_STEP_SUMMARY + fi + + # 3. Each must have required children + UPDATE_COUNT=$(grep -c '' "$UPDATE_XML" 2>/dev/null || echo "0") + if [ "$UPDATE_COUNT" -eq 0 ]; then + echo "::error file=${UPDATE_XML}::No entries found" + echo "- **No \`\` entries** — file is empty" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- ${UPDATE_COUNT} \`\` entr(ies) found" >> $GITHUB_STEP_SUMMARY + + for TAG in name version type downloadurl; do + if ! grep -q "<${TAG}>" "$UPDATE_XML" 2>/dev/null; then + echo "::error file=${UPDATE_XML}::Missing required <${TAG}> in update entry" + echo "- **Missing** \`<${TAG}>\` in update entry" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- \`<${TAG}>\` present ✓" >> $GITHUB_STEP_SUMMARY + fi + done + + # 4. Must have + if ! grep -q '/dev/null; then + echo "::error file=${UPDATE_XML}::Missing — Joomla cannot filter by compatible version" + echo "- **Missing** \`\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- \`\` present ✓" >> $GITHUB_STEP_SUMMARY + fi + fi + fi + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} updates.xml issue(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**updates.xml validation passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: CSS/JS asset existence check + run: | + echo "### CSS/JS Asset Existence" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + SRC_DIR="" + for DIR in source/ src/ htdocs/; do + [ -d "$DIR" ] && SRC_DIR="$DIR" && break + done + + if [ -z "$SRC_DIR" ]; then + echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY + else + # Find WebAssetManager registrations in PHP + # registerAndUseScript/Style, useScript/Style with asset name + ASSET_REFS=$(grep -rohP "(?:registerAndUse|register|use)(?:Script|Style)\(\s*['\"]([^'\"]+)['\"]" "$SRC_DIR" --include="*.php" 2>/dev/null | grep -oP "['\"][^'\"]+['\"]" | tr -d "'\"" | sort -u || true) + + # Find joomla.asset.json files + ASSET_JSONS=$(find . -name "joomla.asset.json" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) + + # Find media directories + MEDIA_DIRS=$(find . -type d -name "media" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" 2>/dev/null) + + if [ -z "$ASSET_REFS" ] && [ -z "$ASSET_JSONS" ]; then + echo "No asset registrations found — skipping." >> $GITHUB_STEP_SUMMARY + else + # Validate joomla.asset.json is well-formed + if [ -n "$ASSET_JSONS" ]; then + for ASSET_JSON in $ASSET_JSONS; do + if command -v php &> /dev/null; then + VALID=$(php -r "echo json_decode(file_get_contents('$ASSET_JSON')) ? 'VALID' : 'INVALID';" 2>/dev/null) + if [ "$VALID" != "VALID" ]; then + echo "::error file=${ASSET_JSON}::joomla.asset.json is not valid JSON" + echo "- **Invalid JSON**: \`${ASSET_JSON}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- \`${ASSET_JSON}\`: valid JSON ✓" >> $GITHUB_STEP_SUMMARY + + # Check that referenced URIs in asset JSON point to existing files + URIS=$(php -r " + \$j = json_decode(file_get_contents('$ASSET_JSON'), true); + \$assets = array_merge(\$j['assets'] ?? [], []); + foreach (\$assets as \$a) { + if (isset(\$a['uri'])) echo \$a['uri'] . PHP_EOL; + if (isset(\$a['css'])) foreach ((array)\$a['css'] as \$c) echo (is_array(\$c) ? \$c['uri'] ?? '' : \$c) . PHP_EOL; + if (isset(\$a['js'])) foreach ((array)\$a['js'] as \$c) echo (is_array(\$c) ? \$c['uri'] ?? '' : \$c) . PHP_EOL; + } + " 2>/dev/null | grep -v '^$' | grep -v '^http' || true) + + ASSET_DIR=$(dirname "$ASSET_JSON") + for URI in $URIS; do + # Resolve relative path from asset JSON location + if [ ! -f "${ASSET_DIR}/${URI}" ]; then + echo "::error file=${ASSET_JSON}::Asset URI '${URI}' references missing file" + echo " - **Missing**: \`${URI}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + fi + fi + done + fi + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} asset issue(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**CSS/JS asset existence check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: MVC naming conventions check + run: | + echo "### MVC Naming Conventions" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + SRC_DIR="" + for DIR in source/ src/ htdocs/; do + [ -d "$DIR" ] && SRC_DIR="$DIR" && break + done + + if [ -z "$SRC_DIR" ]; then + echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY + else + # Check View classes — file must match class name pattern + VIEW_FILES=$(find "$SRC_DIR" -name "*.php" -path "*/View/*" -not -path "./vendor/*" 2>/dev/null) + if [ -n "$VIEW_FILES" ]; then + for FILE in $VIEW_FILES; do + BASENAME=$(basename "$FILE" .php) + # View class should contain class declaration matching filename + CLASS_NAME=$(grep -oP '^\s*class\s+\K[A-Za-z0-9_]+' "$FILE" 2>/dev/null | head -1) + if [ -n "$CLASS_NAME" ]; then + # Class name should end with View (HtmlView, JsonView, etc.) + if ! echo "$CLASS_NAME" | grep -qP 'View$'; then + echo "::error file=${FILE}::View class '${CLASS_NAME}' does not end with 'View'" + echo "- \`${FILE}\`: class \`${CLASS_NAME}\` should end with \`View\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + done + fi + + # Check Controller classes + CTRL_FILES=$(find "$SRC_DIR" -name "*.php" -path "*/Controller/*" -not -path "./vendor/*" 2>/dev/null) + if [ -n "$CTRL_FILES" ]; then + for FILE in $CTRL_FILES; do + CLASS_NAME=$(grep -oP '^\s*class\s+\K[A-Za-z0-9_]+' "$FILE" 2>/dev/null | head -1) + if [ -n "$CLASS_NAME" ]; then + if ! echo "$CLASS_NAME" | grep -qP 'Controller$'; then + echo "::error file=${FILE}::Controller class '${CLASS_NAME}' does not end with 'Controller'" + echo "- \`${FILE}\`: class \`${CLASS_NAME}\` should end with \`Controller\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + done + fi + + # Check Model classes + MODEL_FILES=$(find "$SRC_DIR" -name "*.php" -path "*/Model/*" -not -path "./vendor/*" 2>/dev/null) + if [ -n "$MODEL_FILES" ]; then + for FILE in $MODEL_FILES; do + CLASS_NAME=$(grep -oP '^\s*class\s+\K[A-Za-z0-9_]+' "$FILE" 2>/dev/null | head -1) + if [ -n "$CLASS_NAME" ]; then + if ! echo "$CLASS_NAME" | grep -qP 'Model$'; then + echo "::error file=${FILE}::Model class '${CLASS_NAME}' does not end with 'Model'" + echo "- \`${FILE}\`: class \`${CLASS_NAME}\` should end with \`Model\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + done + fi + + # Check Table classes + TABLE_FILES=$(find "$SRC_DIR" -name "*.php" -path "*/Table/*" -not -path "./vendor/*" 2>/dev/null) + if [ -n "$TABLE_FILES" ]; then + for FILE in $TABLE_FILES; do + CLASS_NAME=$(grep -oP '^\s*class\s+\K[A-Za-z0-9_]+' "$FILE" 2>/dev/null | head -1) + if [ -n "$CLASS_NAME" ]; then + if ! echo "$CLASS_NAME" | grep -qP 'Table$'; then + echo "::error file=${FILE}::Table class '${CLASS_NAME}' does not end with 'Table'" + echo "- \`${FILE}\`: class \`${CLASS_NAME}\` should end with \`Table\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + done + fi + + if [ -z "$VIEW_FILES" ] && [ -z "$CTRL_FILES" ] && [ -z "$MODEL_FILES" ] && [ -z "$TABLE_FILES" ]; then + echo "No MVC classes found — skipping." >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} MVC naming issue(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**MVC naming conventions check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Router validation + run: | + echo "### Router Validation" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Only applies to components + COMP_MANIFESTS=$(find . -maxdepth 10 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./.claude/*" -exec grep -l ']*type="component"' {} \; 2>/dev/null || true) + + if [ -z "$COMP_MANIFESTS" ]; then + echo "No component extensions found — skipping." >> $GITHUB_STEP_SUMMARY + else + for MANIFEST in $COMP_MANIFESTS; do + COMP_DIR=$(dirname "$MANIFEST") + COMP_NAME=$(basename "$COMP_DIR") + echo "Component: \`${COMP_NAME}\`" >> $GITHUB_STEP_SUMMARY + + # Check for router class in src/Service/ + ROUTER_FILE=$(find "$COMP_DIR" -name "Router.php" -path "*/Service/*" -not -path "./vendor/*" 2>/dev/null | head -1) + if [ -z "$ROUTER_FILE" ]; then + # Also check for legacy router.php + ROUTER_FILE=$(find "$COMP_DIR" -name "router.php" -not -path "./vendor/*" 2>/dev/null | head -1) + fi + + if [ -z "$ROUTER_FILE" ]; then + echo "::error file=${MANIFEST}::Component '${COMP_NAME}' has no Router class" + echo "- **Missing** Router — SEF URLs will not work" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- Router found: \`${ROUTER_FILE}\`" >> $GITHUB_STEP_SUMMARY + + # Check router implements RouterServiceInterface or RouterInterface + if ! grep -qP 'RouterServiceInterface|RouterInterface|RouterBase' "$ROUTER_FILE" 2>/dev/null; then + echo "::error file=${ROUTER_FILE}::Router does not implement RouterServiceInterface or RouterInterface" + echo " - **Does not implement** required router interface" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo " - Implements router interface ✓" >> $GITHUB_STEP_SUMMARY + fi + fi + done + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} router issue(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Router validation passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: ACL language keys check + run: | + echo "### ACL Language Keys" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + ACCESS_FILES=$(find . -name "access.xml" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) + if [ -z "$ACCESS_FILES" ]; then + echo "No access.xml files found — skipping." >> $GITHUB_STEP_SUMMARY + else + # Collect all defined language keys + LANG_FILES=$(find . -name "*.ini" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null) + DEFINED_KEYS="" + if [ -n "$LANG_FILES" ]; then + DEFINED_KEYS=$(cat $LANG_FILES 2>/dev/null | grep -oP '^[A-Z][A-Z0-9_]+(?==)' | sort -u) + fi + + for ACCESS_FILE in $ACCESS_FILES; do + echo "Checking: \`${ACCESS_FILE}\`" >> $GITHUB_STEP_SUMMARY + + # Extract action names and titles/descriptions from access.xml + ACTIONS=$(grep -oP ']*name="\K[^"]+' "$ACCESS_FILE" 2>/dev/null || true) + TITLES=$(grep -oP ']*title="\K[^"]+' "$ACCESS_FILE" 2>/dev/null || true) + DESCS=$(grep -oP ']*description="\K[^"]+' "$ACCESS_FILE" 2>/dev/null || true) + + # Check title keys are defined + for KEY in $TITLES $DESCS; do + # Only check keys that look like language constants (ALL_CAPS_WITH_UNDERSCORES) + if echo "$KEY" | grep -qP '^[A-Z][A-Z0-9_]+$'; then + if [ -n "$DEFINED_KEYS" ]; then + if ! echo "$DEFINED_KEYS" | grep -qx "$KEY"; then + echo "::error file=${ACCESS_FILE}::ACL key '${KEY}' not defined in language files" + echo "- **Undefined** ACL key: \`${KEY}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + fi + done + done + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} ACL language key issue(s) — these will show raw key strings in permissions UI.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**ACL language keys check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Webservices plugin API manifest check + run: | + echo "### Webservices API Manifest" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Find webservices plugin manifests + WS_MANIFESTS=$(find . -maxdepth 10 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./.claude/*" -exec grep -l ']*type="plugin"' {} \; 2>/dev/null | while read -r F; do + if grep -q 'group="webservices"' "$F" 2>/dev/null; then + echo "$F" + fi + done) + + if [ -z "$WS_MANIFESTS" ]; then + echo "No webservices plugins found — skipping." >> $GITHUB_STEP_SUMMARY + else + for MANIFEST in $WS_MANIFESTS; do + WS_DIR=$(dirname "$MANIFEST") + WS_NAME=$(basename "$WS_DIR") + echo "Webservices plugin: \`${WS_NAME}\`" >> $GITHUB_STEP_SUMMARY + + # Find the plugin PHP file + WS_PHP=$(find "$WS_DIR" -name "*.php" -path "*/src/*" -not -path "./vendor/*" 2>/dev/null | head -1) + if [ -z "$WS_PHP" ]; then + WS_PHP=$(find "$WS_DIR" -name "*.php" -not -name "provider.php" -not -path "*/services/*" -not -path "./vendor/*" 2>/dev/null | head -1) + fi + + if [ -n "$WS_PHP" ]; then + # Check it implements WebserviceInterface or registers routes + if ! grep -qP 'WebserviceInterface|onBeforeApiRoute|ApiRouter' "$WS_PHP" 2>/dev/null; then + echo "::error file=${WS_PHP}::Webservices plugin does not implement WebserviceInterface or register API routes" + echo "- \`${WS_PHP}\`: missing \`WebserviceInterface\` or route registration" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "- \`${WS_PHP}\`: API routing ✓" >> $GITHUB_STEP_SUMMARY + fi + else + echo "- No plugin PHP class found in \`${WS_DIR}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} webservices API issue(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Webservices API manifest check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: ZIP dry-run build + run: | + echo "### ZIP Dry-Run Build" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY + else + MANIFEST_DIR=$(dirname "$MANIFEST") + EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) + + # Collect all files that should be in the ZIP + MISSING_FILES="" + TOTAL_FILES=0 + + # Files from tags + for F in $(grep -oP ']*>\K[^<]+' "$MANIFEST" 2>/dev/null || true); do + TOTAL_FILES=$((TOTAL_FILES + 1)) + if [ ! -f "${MANIFEST_DIR}/${F}" ]; then + MISSING_FILES="${MISSING_FILES}${F}\n" + fi + done + + # Folders from tags + for F in $(grep -oP ']*>\K[^<]+' "$MANIFEST" 2>/dev/null || true); do + TOTAL_FILES=$((TOTAL_FILES + 1)) + if [ ! -d "${MANIFEST_DIR}/${F}" ]; then + MISSING_FILES="${MISSING_FILES}${F}/\n" + fi + done + + # Script file + SCRIPT=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1) + if [ -n "$SCRIPT" ]; then + TOTAL_FILES=$((TOTAL_FILES + 1)) + if [ ! -f "${MANIFEST_DIR}/${SCRIPT}" ]; then + MISSING_FILES="${MISSING_FILES}${SCRIPT}\n" + fi + fi + + # Media folder + MEDIA_FOLDER=$(grep -oP ']*\bfolder="\K[^"]+' "$MANIFEST" 2>/dev/null | head -1) + if [ -n "$MEDIA_FOLDER" ]; then + TOTAL_FILES=$((TOTAL_FILES + 1)) + if [ ! -d "${MANIFEST_DIR}/${MEDIA_FOLDER}" ]; then + MISSING_FILES="${MISSING_FILES}${MEDIA_FOLDER}/\n" + fi + fi + + # SQL folder + SQL_FOLDER=$(grep -oP '\s*\s*]*>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1) + if [ -n "$SQL_FOLDER" ]; then + SQL_DIR=$(dirname "$SQL_FOLDER") + TOTAL_FILES=$((TOTAL_FILES + 1)) + if [ ! -f "${MANIFEST_DIR}/${SQL_FOLDER}" ]; then + MISSING_FILES="${MISSING_FILES}${SQL_FOLDER}\n" + fi + fi + + MISSING_FILES=$(printf '%b' "$MISSING_FILES" | grep -v '^$' || true) + if [ -n "$MISSING_FILES" ]; then + COUNT=$(echo "$MISSING_FILES" | wc -l) + echo "::error file=${MANIFEST}::ZIP build would fail — ${COUNT} referenced file(s) missing" + echo "**Missing files that would cause ZIP build to fail:**" >> $GITHUB_STEP_SUMMARY + while IFS= read -r F; do + echo "- \`${F}\`" >> $GITHUB_STEP_SUMMARY + done <<< "$MISSING_FILES" + ERRORS=$((ERRORS + COUNT)) + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "Checked ${TOTAL_FILES} manifest reference(s) for \`${EXT_TYPE}\` extension." >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} ZIP build issue(s) — package would be incomplete.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**ZIP dry-run build passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: JavaScript linting + run: | + echo "### JavaScript Linting" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + JS_FILES=$(find . -name "*.js" -not -name "*.min.js" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" 2>/dev/null) + if [ -z "$JS_FILES" ]; then + echo "No JavaScript files found — skipping." >> $GITHUB_STEP_SUMMARY + else + JS_COUNT=$(echo "$JS_FILES" | wc -l) + echo "Found ${JS_COUNT} JavaScript file(s)" >> $GITHUB_STEP_SUMMARY + + # Check if Node.js is available + if command -v node &> /dev/null && command -v npx &> /dev/null; then + # Use project ESLint config if present, otherwise install and use defaults + if [ -f ".eslintrc.js" ] || [ -f ".eslintrc.json" ] || [ -f ".eslintrc.yml" ] || [ -f "eslint.config.js" ] || [ -f "eslint.config.mjs" ]; then + if [ -f "package.json" ]; then + npm install --silent 2>/dev/null || true + fi + npx eslint $JS_FILES 2>&1 | tee /tmp/eslint-output.txt + EXIT=${PIPESTATUS[0]} + else + # Basic syntax check without ESLint config + for FILE in $JS_FILES; do + if ! node --check "$FILE" 2>/tmp/js-syntax-err.txt; then + echo "::error file=${FILE}::JavaScript syntax error" + echo "- \`${FILE}\`: syntax error" >> $GITHUB_STEP_SUMMARY + cat /tmp/js-syntax-err.txt >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + EXIT=$ERRORS + fi + + if [ $EXIT -ne 0 ] && [ -f /tmp/eslint-output.txt ]; then + echo '```' >> $GITHUB_STEP_SUMMARY + cat /tmp/eslint-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + else + echo "Node.js not available — skipping JavaScript lint." >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} JavaScript issue(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**JavaScript linting passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: CSS linting + run: | + echo "### CSS Linting" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + CSS_FILES=$(find . -name "*.css" -not -name "*.min.css" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" 2>/dev/null) + if [ -z "$CSS_FILES" ]; then + echo "No CSS files found — skipping." >> $GITHUB_STEP_SUMMARY + else + CSS_COUNT=$(echo "$CSS_FILES" | wc -l) + echo "Found ${CSS_COUNT} CSS file(s)" >> $GITHUB_STEP_SUMMARY + + for FILE in $CSS_FILES; do + # Basic CSS validation: check for unbalanced braces + OPEN=$(grep -oP '\{' "$FILE" 2>/dev/null | wc -l) + CLOSE=$(grep -oP '\}' "$FILE" 2>/dev/null | wc -l) + if [ "$OPEN" -ne "$CLOSE" ]; then + echo "::error file=${FILE}::Unbalanced braces (${OPEN} open vs ${CLOSE} close)" + echo "- \`${FILE}\`: **unbalanced braces** (${OPEN} \`{\` vs ${CLOSE} \`}\`)" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + # Check for empty file + SIZE=$(wc -c < "$FILE" | tr -d ' ') + if [ "$SIZE" -eq 0 ]; then + echo "::error file=${FILE}::Empty CSS file" + echo "- \`${FILE}\`: **empty file**" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done + + # Use Stylelint if available and configured + if command -v npx &> /dev/null; then + if [ -f ".stylelintrc" ] || [ -f ".stylelintrc.json" ] || [ -f "stylelint.config.js" ] || [ -f "stylelint.config.mjs" ]; then + if [ -f "package.json" ]; then + npm install --silent 2>/dev/null || true + fi + npx stylelint "**/*.css" --ignore-pattern "*.min.css" --ignore-pattern "node_modules" --ignore-pattern "vendor" 2>&1 | tee /tmp/stylelint-output.txt + EXIT=${PIPESTATUS[0]} + if [ $EXIT -ne 0 ]; then + echo '```' >> $GITHUB_STEP_SUMMARY + cat /tmp/stylelint-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} CSS issue(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**CSS linting passed.**" >> $GITHUB_STEP_SUMMARY + fi + + release-readiness: + name: Release Readiness Check + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' && github.base_ref == 'main' + continue-on-error: true + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Validate release readiness + run: | + echo "## Release Readiness" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Extract version from README.md + README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1) + if [ -z "$README_VERSION" ]; then + echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "README version: \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Find the extension manifest + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No Joomla extension manifest found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY + + # Check matches README VERSION + MANIFEST_VERSION=$(grep -oP '\K[^<]+' "$MANIFEST" | head -1) + if [ -z "$MANIFEST_VERSION" ]; then + echo "No \`\` tag in manifest." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + elif [ -n "$README_VERSION" ] && [ "$MANIFEST_VERSION" != "$README_VERSION" ]; then + echo "Manifest version \`${MANIFEST_VERSION}\` does not match README \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest version: \`${MANIFEST_VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Check extension type, element, client attributes + EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) + if [ -z "$EXT_TYPE" ]; then + echo "Missing \`type\` attribute on \`\` tag." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Extension type: \`${EXT_TYPE}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Element check (component/module/plugin name) + HAS_ELEMENT=$(grep -cP '<(element|name)>' "$MANIFEST" 2>/dev/null || echo "0") + if [ "$HAS_ELEMENT" -eq 0 ]; then + echo "Missing \`\` or \`\` in manifest." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + # Client attribute for site/admin modules and plugins + if echo "$EXT_TYPE" | grep -qP "^(module|plugin)$"; then + HAS_CLIENT=$(grep -cP ']*\bclient=' "$MANIFEST" 2>/dev/null || echo "0") + if [ "$HAS_CLIENT" -eq 0 ]; then + echo "Missing \`client\` attribute for ${EXT_TYPE} extension." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + fi + + # Check updates.xml exists + if [ -f "updates.xml" ] || [ -f "updates.xml" ]; then + echo "Update XML present." >> $GITHUB_STEP_SUMMARY + else + echo "No updates.xml found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + # Check CHANGELOG.md exists + if [ -f "CHANGELOG.md" ]; then + echo "CHANGELOG.md present." >> $GITHUB_STEP_SUMMARY + else + echo "No CHANGELOG.md found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ $ERRORS -gt 0 ]; then + echo "**${ERRORS} issue(s) must be resolved before release.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Extension is ready for release.**" >> $GITHUB_STEP_SUMMARY + fi + + test: + name: Tests (PHP ${{ matrix.php }}) + runs-on: ubuntu-latest + if: ${{ !startsWith(github.event.repository.name, 'Template-') }} + needs: lint-and-validate + + strategy: + fail-fast: false + matrix: + php: ['8.2', '8.3'] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup PHP ${{ matrix.php }} + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + php -v && composer --version + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.MOKOGIT_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install \ + --no-interaction \ + --prefer-dist \ + --optimize-autoloader + else + echo "No composer.json found — skipping dependency install" + fi + + - name: Run tests + run: | + echo "### Test Results (PHP ${{ matrix.php }})" >> $GITHUB_STEP_SUMMARY + if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then + vendor/bin/phpunit --testdox 2>&1 | tee /tmp/test-output.log + EXIT=${PIPESTATUS[0]} + if [ $EXIT -eq 0 ]; then + echo "All tests passed." >> $GITHUB_STEP_SUMMARY + else + echo "Test failures detected — see log." >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi + exit $EXIT + else + echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY + fi + + static-analysis: + name: PHPStan Analysis + runs-on: ubuntu-latest + if: ${{ !startsWith(github.event.repository.name, 'Template-') }} + needs: lint-and-validate + continue-on-error: true + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup PHP + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + php -v && composer --version + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.MOKOGIT_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install --no-interaction --prefer-dist --optimize-autoloader + fi + + - name: Install PHPStan + run: | + if ! command -v vendor/bin/phpstan &> /dev/null; then + composer require --dev phpstan/phpstan --no-interaction 2>/dev/null || \ + composer global require phpstan/phpstan --no-interaction + fi + + - name: Run PHPStan + run: | + echo "### PHPStan Static Analysis" >> $GITHUB_STEP_SUMMARY + PHPSTAN="vendor/bin/phpstan" + if [ ! -f "$PHPSTAN" ]; then + PHPSTAN=$(composer global config bin-dir --absolute 2>/dev/null)/phpstan + fi + + # Determine source directory + SRC_DIR="" + for DIR in src/ htdocs/ lib/; do + if [ -d "$DIR" ]; then + SRC_DIR="$DIR" + break + fi + done + + if [ -z "$SRC_DIR" ]; then + echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + # Use repo phpstan.neon if present, otherwise use baseline config + ARGS="analyse ${SRC_DIR} --memory-limit=512M --no-progress --error-format=table" + if [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ]; then + echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY + else + ARGS="$ARGS --level=3" + echo "No phpstan.neon found — using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY + fi + + $PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt + EXIT=${PIPESTATUS[0]} + + if [ $EXIT -eq 0 ]; then + echo "**No errors found.**" >> $GITHUB_STEP_SUMMARY + else + ERRORS=$(grep -c "ERROR" /tmp/phpstan-output.txt 2>/dev/null || echo "some") + echo "**${ERRORS} error(s) found.** Review output above." >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + tail -30 /tmp/phpstan-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi + exit $EXIT + + pre-release: + name: Build RC Pre-Release + runs-on: ubuntu-latest + needs: [lint-and-validate, test] + if: github.event_name == 'pull_request' + + steps: + - name: Trigger pre-release build + env: + MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} + REPO: ${{ github.repository }} + BRANCH: ${{ github.head_ref }} + run: | + curl -s -X POST \ + "${GIT_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" \ + -H "Authorization: token ${MOKOGIT_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" + echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY + echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY -- 2.52.0 From b523c91d00098ae1c81bf571b248fc3c6ad7bd5b Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 21:09:56 +0000 Subject: [PATCH 49/63] chore: sync cleanup.yml from Template-Joomla [skip ci] --- .mokogit/workflows/cleanup.yml | 87 ++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 .mokogit/workflows/cleanup.yml diff --git a/.mokogit/workflows/cleanup.yml b/.mokogit/workflows/cleanup.yml new file mode 100644 index 0000000..ae9cc01 --- /dev/null +++ b/.mokogit/workflows/cleanup.yml @@ -0,0 +1,87 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGit.Workflow +# INGROUP: MokoCLI.Maintenance +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# PATH: /.mokogit/workflows/cleanup.yml +# VERSION: 01.00.00 +# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs + +name: "Universal: Repository Cleanup" + +on: + schedule: + - cron: '0 3 * * 0' # Weekly on Sunday at 03:00 UTC + workflow_dispatch: + +permissions: + contents: write + +env: + MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + +jobs: + cleanup: + name: Clean Merged Branches + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.MOKOGIT_TOKEN }} + + - name: Delete merged branches + env: + MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} + run: | + echo "=== Merged Branch Cleanup ===" + API="${MOKOGIT_URL}/api/v1/repos/${{ github.repository }}" + + # List branches via API + BRANCHES=$(curl -sS -H "Authorization: token ${MOKOGIT_TOKEN}" \ + "${API}/branches?limit=50" | jq -r '.[].name') + + DELETED=0 + for BRANCH in $BRANCHES; do + # Skip protected branches + case "$BRANCH" in + main|master|dev|develop|rc|beta|alpha|release|release/*|production|stable|staging|hotfix/*|version/*) continue ;; + esac + + # Check if branch is merged into main + if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then + echo " Deleting merged branch: ${BRANCH}" + curl -sS -X DELETE -H "Authorization: token ${MOKOGIT_TOKEN}" \ + "${API}/branches/${BRANCH}" 2>/dev/null || true + DELETED=$((DELETED + 1)) + fi + done + + echo "Deleted ${DELETED} merged branch(es)" + + - name: Clean old workflow runs + env: + MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} + run: | + echo "=== Workflow Run Cleanup ===" + API="${MOKOGIT_URL}/api/v1/repos/${{ github.repository }}" + CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ) + + # Get old completed runs + RUNS=$(curl -sS -H "Authorization: token ${MOKOGIT_TOKEN}" \ + "${API}/actions/runs?status=completed&limit=50" | \ + jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null) + + DELETED=0 + for RUN_ID in $RUNS; do + curl -sS -X DELETE -H "Authorization: token ${MOKOGIT_TOKEN}" \ + "${API}/actions/runs/${RUN_ID}" 2>/dev/null || true + DELETED=$((DELETED + 1)) + done + + echo "Deleted ${DELETED} old workflow run(s)" -- 2.52.0 From 71b2317b43b083651d2bde994b0435eb2beb8150 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 21:09:57 +0000 Subject: [PATCH 50/63] chore: sync gitleaks.yml from Template-Joomla [skip ci] --- .mokogit/workflows/gitleaks.yml | 92 +++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 .mokogit/workflows/gitleaks.yml diff --git a/.mokogit/workflows/gitleaks.yml b/.mokogit/workflows/gitleaks.yml new file mode 100644 index 0000000..0d272ae --- /dev/null +++ b/.mokogit/workflows/gitleaks.yml @@ -0,0 +1,92 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGit.Workflow +# INGROUP: MokoCLI.Security +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# PATH: /templates/workflows/gitleaks.yml.template +# VERSION: 01.00.00 +# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens +# +# +========================================================================+ +# | SECRET SCANNING | +# +========================================================================+ +# | | +# | Scans commits for leaked secrets using Gitleaks. | +# | | +# | - PR scan: only new commits in the PR | +# | - Scheduled: full repo scan weekly | +# | - Alerts via ntfy on findings | +# | | +# +========================================================================+ + +name: "Universal: Secret Scanning" + +on: + schedule: + - cron: '0 5 * * 1' # Weekly Monday 05:00 UTC + workflow_dispatch: + +permissions: + contents: read + +env: + NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'git-security' }} + +jobs: + gitleaks: + name: Gitleaks Secret Scan + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Gitleaks + run: | + GITLEAKS_VERSION="8.21.2" + curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ + | tar -xz -C /usr/local/bin gitleaks + gitleaks version + + - name: Scan for secrets + id: scan + run: | + echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY + ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json" + + if [ "${{ github.event_name }}" = "pull_request" ]; then + # Scan only PR commits + ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}" + echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY + else + echo "Full repository scan" >> $GITHUB_STEP_SUMMARY + fi + + if gitleaks detect $ARGS 2>&1; then + echo "result=clean" >> "$GITHUB_OUTPUT" + echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY + else + echo "result=found" >> "$GITHUB_OUTPUT" + FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown") + echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + - name: Notify on findings + if: failure() && steps.scan.outputs.result == 'found' + run: | + REPO="${{ github.event.repository.name }}" + curl -sS \ + -H "Title: ${REPO} — secrets detected in code" \ + -H "Tags: rotating_light,key" \ + -H "Priority: urgent" \ + -d "Gitleaks found potential secrets. Review and rotate credentials immediately." \ + "${NTFY_URL}/${NTFY_TOPIC}" || true -- 2.52.0 From 5f3368e77808dbb90804b55e6d50b734dec3566c Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 21:09:58 +0000 Subject: [PATCH 51/63] chore: sync issue-branch.yml from Template-Joomla [skip ci] --- .mokogit/workflows/issue-branch.yml | 73 +++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .mokogit/workflows/issue-branch.yml diff --git a/.mokogit/workflows/issue-branch.yml b/.mokogit/workflows/issue-branch.yml new file mode 100644 index 0000000..c6f52aa --- /dev/null +++ b/.mokogit/workflows/issue-branch.yml @@ -0,0 +1,73 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGit.Workflow +# INGROUP: MokoCLI.Automation +# VERSION: 01.00.00 +# BRIEF: Auto-create feature branch when an issue is opened + +name: "Universal: Issue Branch" + +on: + issues: + types: [opened] + +permissions: + contents: write + issues: write + +env: + MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + +jobs: + create-branch: + name: Create feature branch + runs-on: ubuntu-latest + steps: + - name: Create branch and comment + run: | + TOKEN="${{ secrets.MOKOGIT_TOKEN }}" + API="${MOKOGIT_URL}/api/v1/repos/${{ github.repository }}" + ISSUE_NUM="${{ github.event.issue.number }}" + ISSUE_TITLE="${{ github.event.issue.title }}" + + # Build slug from title: lowercase, replace non-alnum with dash, trim + SLUG=$(echo "${ISSUE_TITLE}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-40) + BRANCH="feature/${ISSUE_NUM}-${SLUG}" + + # Check dev branch exists + DEV_EXISTS=$(curl -sf -o /dev/null -w '%{http_code}' \ + -H "Authorization: token ${TOKEN}" \ + "${API}/branches/dev" 2>/dev/null || echo "000") + + if [ "${DEV_EXISTS}" != "200" ]; then + echo "No dev branch -- skipping" + exit 0 + fi + + # Create branch from dev + HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \ + -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/branches" \ + -d "{\"new_branch_name\":\"${BRANCH}\",\"old_branch_name\":\"dev\"}" 2>/dev/null || echo "000") + + if [ "${HTTP}" = "201" ]; then + echo "Created branch: ${BRANCH}" + + # Comment on issue with branch link + REPO_URL="${MOKOGIT_URL}/${{ github.repository }}" + BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`" + + curl -sf -X POST \ + -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/issues/${ISSUE_NUM}/comments" \ + -d "{\"body\":\"${BODY}\"}" > /dev/null 2>&1 + + echo "Commented on issue #${ISSUE_NUM}" + else + echo "Failed to create branch (HTTP ${HTTP}) -- may already exist" + fi -- 2.52.0 From c36fcef1386fbce61a34c17e0d68f556b3ffb375 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 21:09:59 +0000 Subject: [PATCH 52/63] chore: sync notify.yml from Template-Joomla [skip ci] --- .mokogit/workflows/notify.yml | 70 +++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .mokogit/workflows/notify.yml diff --git a/.mokogit/workflows/notify.yml b/.mokogit/workflows/notify.yml new file mode 100644 index 0000000..f2ad215 --- /dev/null +++ b/.mokogit/workflows/notify.yml @@ -0,0 +1,70 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGit.Workflow +# INGROUP: MokoCLI.Notifications +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# PATH: /.mokogit/workflows/notify.yml +# VERSION: 01.00.00 +# BRIEF: Push notifications via ntfy on release success or workflow failure + +name: "Universal: Notifications" + +on: + workflow_run: + workflows: + - "Universal: Build & Release" + - "Joomla: Extension CI" + - "Generic: Project CI" + types: + - completed + +permissions: + contents: read + +env: + NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'git-releases' }} + +jobs: + notify: + name: Send Notification + runs-on: ubuntu-latest + if: >- + github.event.workflow_run.conclusion == 'success' || + github.event.workflow_run.conclusion == 'failure' + + steps: + - name: Notify on success (releases only) + if: >- + github.event.workflow_run.conclusion == 'success' && + contains(github.event.workflow_run.name, 'Release') + run: | + REPO="${{ github.event.repository.name }}" + WORKFLOW="${{ github.event.workflow_run.name }}" + URL="${{ github.event.workflow_run.html_url }}" + + curl -sS \ + -H "Title: ${REPO} released" \ + -H "Tags: white_check_mark,package" \ + -H "Priority: default" \ + -H "Click: ${URL}" \ + -d "${WORKFLOW} completed successfully." \ + "${NTFY_URL}/${NTFY_TOPIC}" + + - name: Notify on failure + if: github.event.workflow_run.conclusion == 'failure' + run: | + REPO="${{ github.event.repository.name }}" + WORKFLOW="${{ github.event.workflow_run.name }}" + URL="${{ github.event.workflow_run.html_url }}" + + curl -sS \ + -H "Title: ${REPO} workflow failed" \ + -H "Tags: x,warning" \ + -H "Priority: high" \ + -H "Click: ${URL}" \ + -d "${WORKFLOW} failed. Check the run for details." \ + "${NTFY_URL}/${NTFY_TOPIC}" -- 2.52.0 From 4abe87fba0217854fa8e8f474845217573c349b7 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 21:10:01 +0000 Subject: [PATCH 53/63] chore: sync pr-check.yml from Template-Joomla [skip ci] --- .mokogit/workflows/pr-check.yml | 527 ++++++++++++++++++++++++++++++++ 1 file changed, 527 insertions(+) create mode 100644 .mokogit/workflows/pr-check.yml diff --git a/.mokogit/workflows/pr-check.yml b/.mokogit/workflows/pr-check.yml new file mode 100644 index 0000000..cb9e52c --- /dev/null +++ b/.mokogit/workflows/pr-check.yml @@ -0,0 +1,527 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGit.Workflow +# INGROUP: MokoCLI.CI +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# PATH: /templates/workflows/universal/pr-check.yml.template +# VERSION: 09.23.00 +# BRIEF: PR gate — branch policy + code validation before merge + +name: "Universal: PR Check" + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +permissions: + contents: read + pull-requests: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + # ── Branch Policy ────────────────────────────────────────────────────── + branch-policy: + name: Branch Policy + runs-on: ubuntu-latest + steps: + - name: Check branch merge target + run: | + HEAD="${{ github.head_ref }}" + BASE="${{ github.base_ref }}" + + echo "PR: ${HEAD} → ${BASE}" + + ALLOWED=true + REASON="" + + case "$HEAD" in + feature/*|feat/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Feature branches must target 'dev', not '${BASE}'" + fi + ;; + fix/*|bugfix/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Fix branches must target 'dev', not '${BASE}'" + fi + ;; + patch/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then + ALLOWED=false + REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" + fi + ;; + hotfix/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" + fi + ;; + rc) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="RC branch can only merge into 'main', not '${BASE}'" + fi + ;; + dev) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Dev branch can only merge into 'main', not '${BASE}'" + fi + ;; + esac + + if [ "$ALLOWED" = false ]; then + echo "::error::${REASON}" + echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "${REASON}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY + echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "Branch policy: OK (${HEAD} → ${BASE})" + echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY + + # ── Secret Scanning ────────────────────────────────────────────────── + gitleaks: + name: Secret Scan + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Gitleaks + run: | + GITLEAKS_VERSION="8.21.2" + curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ + | tar -xz -C /usr/local/bin gitleaks + + - name: Scan PR commits for secrets + run: | + if gitleaks detect --source . --verbose \ + --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} 2>&1; then + echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY + else + echo "::error::Potential secrets detected in PR commits" + exit 1 + fi + + # ── Code Validation ──────────────────────────────────────────────────── + validate: + name: Validate PR + runs-on: ubuntu-latest + # Skip on template repos (Template-*) — no real manifest/source/changelog to validate. + if: ${{ !startsWith(github.event.repository.name, 'Template-') }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check for merge conflict markers + run: | + CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) + if [ -n "$CONFLICTS" ]; then + echo "::error::Merge conflict markers found in source files" + echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "No conflict markers found" + + - name: Detect platform + id: platform + run: | + # Platform comes from the MokoGit metadata API (public GET); manifest.xml is no longer used. + API="${GITHUB_SERVER_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${GITHUB_REPOSITORY}/metadata" + PLATFORM="$(curl -sf "$API" 2>/dev/null | python3 -c "import sys, json; print(json.load(sys.stdin).get('platform') or '')" 2>/dev/null || true)" + [ -z "$PLATFORM" ] && PLATFORM="generic" + echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" + echo "Detected platform: $PLATFORM" + + - name: Setup PHP + if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 + fi + + - name: PHP syntax check + if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' + run: | + ERRORS=0 + while IFS= read -r -d '' file; do + if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then + ERRORS=$((ERRORS + 1)) + fi + done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0) + echo "PHP lint: ${ERRORS} error(s)" + [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } + + - name: Joomla JEXEC guard check + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + while IFS= read -r -d '' file; do + # Skip vendor, node_modules, and index.html stub files + case "$file" in ./vendor/*|./node_modules/*) continue ;; esac + # Check first 10 lines for JEXEC or JPATH guard + if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then + echo "::error file=${file}::Missing JEXEC guard: ${file}" + ERRORS=$((ERRORS + 1)) + fi + done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0) + if [ "$ERRORS" -gt 0 ]; then + echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard" + echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY + echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "JEXEC guard: OK" + + - name: Joomla directory listing protection + if: steps.platform.outputs.platform == 'joomla' + run: | + MISSING=0 + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && exit 0 + while IFS= read -r dir; do + if [ ! -f "${dir}/index.html" ]; then + echo "::warning::Missing index.html in ${dir} (directory listing protection)" + MISSING=$((MISSING + 1)) + fi + done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*") + if [ "$MISSING" -gt 0 ]; then + echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY + echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY + fi + echo "Directory protection: ${MISSING} missing (advisory)" + + - name: Joomla script file and asset checks + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + [ -z "$MANIFEST" ] && exit 0 + MANIFEST_DIR=$(dirname "$MANIFEST") + + # Check scriptfile exists if declared + SCRIPTFILE=$(sed -n 's/.*\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null) + if [ -n "$SCRIPTFILE" ]; then + if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then + echo "::error::Manifest declares ${SCRIPTFILE} but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}" + ERRORS=$((ERRORS + 1)) + else + echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)" + fi + fi + + # Require joomla.asset.json and validate it + ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1) + if [ -z "$ASSET_JSON" ]; then + echo "::error::joomla.asset.json not found — Joomla asset system is required" + ERRORS=$((ERRORS + 1)) + else + if command -v php &> /dev/null; then + php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || { + echo "::error::joomla.asset.json is not valid JSON" + ERRORS=$((ERRORS + 1)) + } + fi + echo "joomla.asset.json: valid" + fi + + # Validate all XML files in src/ are well-formed + XML_ERRORS=0 + if command -v php &> /dev/null; then + while IFS= read -r -d '' xmlfile; do + if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then + XML_ERRORS=$((XML_ERRORS + 1)) + fi + done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0) + fi + if [ "$XML_ERRORS" -gt 0 ]; then + echo "::error::${XML_ERRORS} XML file(s) are malformed" + ERRORS=$((ERRORS + 1)) + else + echo "XML well-formedness: OK" + fi + + [ "$ERRORS" -gt 0 ] && exit 1 + echo "Joomla asset checks: OK" + + - name: Validate platform manifest + run: | + PLATFORM="${{ steps.platform.outputs.platform }}" + case "$PLATFORM" in + joomla) + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "::warning::No Joomla manifest found (WaaS site)" + exit 0 + fi + echo "Manifest: ${MANIFEST}" + if command -v php &> /dev/null; then + php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; } + fi + for ELEMENT in name version description; do + grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; } + done + # Block legacy raw/branch update server URLs on MokoGit + RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogit\|git\.mokoconsulting\.tech' || true) + if [ -n "$RAW_URLS" ]; then + echo "::error::Manifest contains legacy raw/branch update server URL on MokoGit. Use the MokoGit Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)" + echo "$RAW_URLS" + exit 1 + fi + echo "Joomla manifest valid" + ;; + dolibarr) + MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) + if [ -z "$MOD_FILE" ]; then + echo "::error::No mod*.class.php found" + exit 1 + fi + echo "Dolibarr module: ${MOD_FILE}" + ;; + *) + echo "Generic platform — no manifest validation" + ;; + esac + + - name: Check update stream format + run: | + PLATFORM="${{ steps.platform.outputs.platform }}" + case "$PLATFORM" in + joomla) + if [ -f "updates.xml" ]; then + if command -v php &> /dev/null; then + php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; } + fi + echo "updates.xml valid" + fi + ;; + dolibarr) + [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt" + ;; + esac + + - name: Validate Joomla language files + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + WARNINGS=0 + + # Require both en-GB and en-US language directories + LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1) + if [ -z "$LANG_ROOT" ]; then + echo "No language/ directory found — skipping" + exit 0 + fi + + if [ ! -d "$LANG_ROOT/en-GB" ]; then + echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)" + ERRORS=$((ERRORS + 1)) + fi + if [ ! -d "$LANG_ROOT/en-US" ]; then + echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)" + ERRORS=$((ERRORS + 1)) + fi + + # Check that en-GB and en-US have matching .ini files + if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then + for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do + [ ! -f "$GB_INI" ] && continue + US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")" + if [ ! -f "$US_INI" ]; then + echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US" + ERRORS=$((ERRORS + 1)) + fi + done + for US_INI in "$LANG_ROOT/en-US"/*.ini; do + [ ! -f "$US_INI" ] && continue + GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")" + if [ ! -f "$GB_INI" ]; then + echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB" + ERRORS=$((ERRORS + 1)) + fi + done + fi + + # Find all .ini language files + INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null) + if [ -z "$INI_FILES" ]; then + echo "No .ini language files found" + [ "$ERRORS" -gt 0 ] && exit 1 + exit 0 + fi + + echo "Found $(echo "$INI_FILES" | wc -l) language file(s)" + + for FILE in $INI_FILES; do + FNAME=$(basename "$FILE") + LINENUM=0 + SEEN_KEYS="" + + while IFS= read -r line || [ -n "$line" ]; do + LINENUM=$((LINENUM + 1)) + + # Skip empty lines and comments + [ -z "$line" ] && continue + echo "$line" | grep -qE '^\s*;' && continue + echo "$line" | grep -qE '^\s*$' && continue + + # Must match KEY="VALUE" format + if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then + echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}" + ERRORS=$((ERRORS + 1)) + continue + fi + + # Extract key and check for duplicates + KEY=$(echo "$line" | sed 's/=.*//') + if echo "$SEEN_KEYS" | grep -qx "$KEY"; then + echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}" + ERRORS=$((ERRORS + 1)) + fi + SEEN_KEYS="${SEEN_KEYS} + ${KEY}" + done < "$FILE" + + echo " ${FILE}: checked ${LINENUM} lines" + done + + # Cross-check en-GB vs en-US key consistency + GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1) + US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1) + + if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then + for GB_FILE in "$GB_DIR"/*.ini; do + [ ! -f "$GB_FILE" ] && continue + FNAME=$(basename "$GB_FILE") + US_FILE="$US_DIR/$FNAME" + [ ! -f "$US_FILE" ] && continue + + GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort) + US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort) + + # Keys in en-GB but not en-US + MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS")) + if [ -n "$MISSING_US" ]; then + echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:" + echo "$MISSING_US" | while read -r k; do echo " - $k"; done + WARNINGS=$((WARNINGS + 1)) + fi + + # Keys in en-US but not en-GB + MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS")) + if [ -n "$MISSING_GB" ]; then + echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:" + echo "$MISSING_GB" | while read -r k; do echo " - $k"; done + WARNINGS=$((WARNINGS + 1)) + fi + done + fi + + { + echo "### Language File Validation" + echo "| Metric | Count |" + echo "|---|---|" + echo "| Files checked | $(echo "$INI_FILES" | wc -l) |" + echo "| Errors | ${ERRORS} |" + echo "| Warnings | ${WARNINGS} |" + } >> $GITHUB_STEP_SUMMARY + + if [ "$ERRORS" -gt 0 ]; then + echo "::error::Language validation failed with ${ERRORS} error(s)" + exit 1 + fi + echo "Language files: OK (${WARNINGS} warning(s))" + + - name: Check changelog has unreleased entry + run: | + if [ ! -f "CHANGELOG.md" ]; then + echo "::warning::No CHANGELOG.md found" + exit 0 + fi + # Check for content under [Unreleased] section + if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then + echo "::error::CHANGELOG.md missing [Unreleased] section" + exit 1 + fi + # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased + UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true) + if [ "$UNRELEASED_CONTENT" -eq 0 ]; then + echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes." + echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY + echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY + echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]" + + - name: Verify package source + run: | + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ ! -d "$SOURCE_DIR" ]; then + echo "::warning::No src/ or htdocs/ directory" + exit 0 + fi + FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) + echo "Source: ${FILE_COUNT} files" + [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } + + # ── Pre-Release RC Build ───────────────────────────────────────────────── + pre-release: + name: Build RC Package + runs-on: ubuntu-latest + needs: [branch-policy, validate] + # Run only when both gates succeeded; always() forces evaluation so a skipped + # validate (e.g. template repos) skips this job cleanly instead of hanging. + if: ${{ always() && needs.branch-policy.result == 'success' && needs.validate.result == 'success' }} + + steps: + - name: Trigger RC pre-release + env: + MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} + REPO: ${{ github.repository }} + BRANCH: ${{ github.head_ref }} + MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + curl -s -X POST "${MOKOGIT_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${MOKOGIT_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" + echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY + echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY + + # ── Issue Reporter ────────────────────────────────────────────────────── + report-issues: + name: Report Issues + needs: [branch-policy, validate] + if: >- + always() && + needs.validate.result == 'failure' + uses: ./.mokogit/workflows/ci-issue-reporter.yml + with: + gate: "PR Validation" + workflow: "PR Check" + severity: error + details: "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed." + secrets: inherit -- 2.52.0 From 0a784a32716716cc44734120c1a04bb79dfd16ed Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 21:10:02 +0000 Subject: [PATCH 54/63] chore: sync pr-metadata-check.yml from Template-Joomla [skip ci] --- .mokogit/workflows/pr-metadata-check.yml | 73 ++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .mokogit/workflows/pr-metadata-check.yml diff --git a/.mokogit/workflows/pr-metadata-check.yml b/.mokogit/workflows/pr-metadata-check.yml new file mode 100644 index 0000000..ba552d2 --- /dev/null +++ b/.mokogit/workflows/pr-metadata-check.yml @@ -0,0 +1,73 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGit.Workflow +# INGROUP: MokoCLI.Validation +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# PATH: /templates/workflows/joomla/pr-metadata-check.yml.template +# VERSION: 01.00.00 +# BRIEF: Validate MokoGit metadata matches Joomla extension manifest on PRs + +name: "Joomla: Metadata Validation" + +on: + pull_request: + types: [opened, synchronize, reopened, converted_to_draft, ready_for_review] + +permissions: + contents: read + +env: + MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GIT_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GIT_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +jobs: + validate-metadata: + name: "Validate Joomla Metadata" + runs-on: ubuntu-latest + # Skip on template repos (Template-*) — they have no real extension manifest to validate. + if: ${{ !startsWith(github.event.repository.name, 'Template-') }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup MokoCLI tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + if [ -f /opt/mokocli/cli/joomla_metadata_validate.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then + echo Using pre-installed /opt/mokocli + echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV + else + echo Falling back to fresh clone + if ! command -v composer > /dev/null 2>&1; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 + fi + rm -rf /tmp/mokocli + CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git + git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli + cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet + echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV + fi + + - name: Validate metadata against Joomla manifest + env: + MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} + run: | + php ${MOKO_CLI}/joomla_metadata_validate.php \ + --path . \ + --token "${MOKOGIT_TOKEN}" \ + --org "${GIT_ORG}" \ + --repo "${GIT_REPO}" \ + --api-base "${MOKOGIT_URL}/api/v1" \ + --ci + + if [ $? -ne 0 ]; then + echo "::error::Joomla metadata mismatch — update delivery will fail. Run 'php cli/joomla_metadata_validate.php' locally to see details." + exit 1 + fi -- 2.52.0 From 70545956a73b9a91eb505c5139a159265cac325c Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 21:10:03 +0000 Subject: [PATCH 55/63] chore: sync pre-release.yml from Template-Joomla [skip ci] --- .mokogit/workflows/pre-release.yml | 277 +++++++++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 .mokogit/workflows/pre-release.yml diff --git a/.mokogit/workflows/pre-release.yml b/.mokogit/workflows/pre-release.yml new file mode 100644 index 0000000..7a9031b --- /dev/null +++ b/.mokogit/workflows/pre-release.yml @@ -0,0 +1,277 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGit.Workflow +# INGROUP: MokoCLI.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# PATH: /templates/workflows/universal/pre-release.yml.template +# VERSION: 05.02.00 +# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches + +name: "Universal: Pre-Release" + +on: + push: + branches: + - dev + - 'fix/**' + - 'patch/**' + - 'hotfix/**' + - 'bugfix/**' + - 'chore/**' + - alpha + - beta + - rc + workflow_dispatch: + inputs: + stability: + description: 'Pre-release channel' + required: true + type: choice + options: + - development + - alpha + - beta + - release-candidate + +permissions: + contents: write + +env: + GIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GIT_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GIT_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +jobs: + build: + name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})" + runs-on: release + # Skip on template repos (Template-*) — they scaffold other repos and do not release. + if: >- + !startsWith(github.event.repository.name, 'Template-') && + ( + github.event_name == 'workflow_dispatch' || + github.event_name == 'push' + ) + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.MOKOGIT_TOKEN }} + ref: ${{ github.ref_name }} + submodules: recursive + + - name: Update submodules to main + run: | + git submodule foreach --quiet 'git checkout main && git pull --quiet origin main' 2>/dev/null || true + + - name: Setup MokoCLI tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + # Use pre-installed /opt/mokocli if available (updated by cron every 6h) + if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/cli/manifest_element.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then + echo Using pre-installed /opt/mokocli + echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV + else + echo Falling back to fresh clone + if ! command -v composer > /dev/null 2>&1; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1 + fi + rm -rf /tmp/mokocli + CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git + git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli + cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet + echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV + fi + + - name: Detect platform + id: platform + run: | + # Auto-detect and update platform if not set in manifest + php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true + php ${MOKO_CLI}/manifest_read.php --path . --github-output + + - name: Check platform eligibility (Joomla only) + id: eligibility + run: | + PLATFORM="${{ steps.platform.outputs.platform }}" + if [[ "$PLATFORM" == joomla* ]] || [[ "$PLATFORM" == "joomla" ]]; then + echo "proceed=true" >> "$GITHUB_OUTPUT" + else + echo "proceed=false" >> "$GITHUB_OUTPUT" + echo "::notice::Platform '$PLATFORM' — non-Joomla, skipping pre-release auto-bump" + fi + + - name: Resolve metadata and bump version + id: meta + if: steps.eligibility.outputs.proceed == 'true' + run: | + # Auto-detect stability from branch name on push, or use input on dispatch + if [ "${{ github.event_name }}" = "push" ]; then + case "${{ github.ref_name }}" in + rc) STABILITY="release-candidate" ;; + alpha) STABILITY="alpha" ;; + beta) STABILITY="beta" ;; + *) STABILITY="development" ;; + esac + else + STABILITY="${{ inputs.stability || 'development' }}" + fi + + case "$STABILITY" in + development) SUFFIX="-dev"; TAG="development" ;; + alpha) SUFFIX="-alpha"; TAG="alpha" ;; + beta) SUFFIX="-beta"; TAG="beta" ;; + release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; + esac + + # Bump version via CLI: patch for dev/alpha/beta, minor for RC + case "$STABILITY" in + release-candidate) BUMP="minor" ;; + *) BUMP="patch" ;; + esac + + php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true + + # Set stability suffix and verify consistency + VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01") + VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//') + + php ${MOKO_CLI}/version_set_platform.php \ + --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true + php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + + # Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml + php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true + + # Append suffix for output + if [ -n "$SUFFIX" ]; then + VERSION="${VERSION}${SUFFIX}" + fi + + # Commit version bump + git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech" + git config --local user.name "mokogit-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" + git push origin HEAD 2>&1 + } + + # Auto-detect element via manifest_element.php + php ${MOKO_CLI}/manifest_element.php \ + --path . --version "$VERSION" --stability "$STABILITY" \ + --repo "${GIT_REPO}" --github-output + + # Read back element outputs + EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GIT_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" + echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" + + echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" + + - name: Create release + id: release + if: steps.eligibility.outputs.proceed == 'true' + run: | + TAG="${{ steps.meta.outputs.tag }}" + VERSION="${{ steps.meta.outputs.version }}" + API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" + php ${MOKO_CLI}/release_create.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GIT_REPO}" --branch "${{ github.ref_name }}" --prerelease + + - name: Update release notes from CHANGELOG.md + if: steps.eligibility.outputs.proceed == 'true' + run: | + TAG="${{ steps.meta.outputs.tag }}" + VERSION="${{ steps.meta.outputs.version }}" + API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" + + # Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading) + if [ -f "CHANGELOG.md" ]; then + NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md) + [ -z "$NOTES" ] && NOTES="Release ${VERSION}" + else + NOTES="Release ${VERSION}" + fi + + # Update release body via API + RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGIT_TOKEN }}" \ + "${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [ -n "$RELEASE_ID" ]; then + python3 -c " + import json, urllib.request + body = open('/dev/stdin').read() + payload = json.dumps({'body': body}).encode() + req = urllib.request.Request( + '${API_BASE}/releases/${RELEASE_ID}', + data=payload, method='PATCH', + headers={ + 'Authorization': 'token ${{ secrets.MOKOGIT_TOKEN }}', + 'Content-Type': 'application/json' + }) + urllib.request.urlopen(req) + " <<< "$NOTES" + echo "Release notes updated from CHANGELOG.md" + fi + + - name: Build package and upload + id: package + if: steps.eligibility.outputs.proceed == 'true' + run: | + VERSION="${{ steps.meta.outputs.version }}" + TAG="${{ steps.meta.outputs.tag }}" + API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" + php ${MOKO_CLI}/release_package.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GIT_REPO}" --output /tmp || true + + # updates.xml is generated dynamically by MokoGit license server + # No need to build, commit, or sync updates.xml from workflows + + - name: "Delete lesser pre-release channels (cascade)" + if: steps.eligibility.outputs.proceed == 'true' + continue-on-error: true + run: | + API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" + TOKEN="${{ secrets.MOKOGIT_TOKEN }}" + + php ${MOKO_CLI}/release_cascade.php \ + --stability "${{ steps.meta.outputs.stability }}" \ + --token "${TOKEN}" \ + --api-base "${API_BASE}" + + - name: Summary + if: always() + run: | + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + SHA256="${{ steps.package.outputs.sha256_zip }}" + echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY + echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY + echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY -- 2.52.0 From 59543a4d3601e3822a9d8f95b29b4588cc1ec355 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 21:10:04 +0000 Subject: [PATCH 56/63] chore: sync rc-revert.yml from Template-Joomla [skip ci] --- .mokogit/workflows/rc-revert.yml | 71 ++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 .mokogit/workflows/rc-revert.yml diff --git a/.mokogit/workflows/rc-revert.yml b/.mokogit/workflows/rc-revert.yml new file mode 100644 index 0000000..d91268a --- /dev/null +++ b/.mokogit/workflows/rc-revert.yml @@ -0,0 +1,71 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGit.Workflow +# INGROUP: MokoCLI.Universal +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# PATH: /.mokogit/workflows/rc-revert.yml +# VERSION: 09.23.00 +# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge + +name: "RC Revert" + +on: + pull_request: + types: [closed] + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + revert: + name: Rename rc/ back to dev/ + runs-on: ubuntu-latest + if: >- + github.event.pull_request.merged == false && + startsWith(github.event.pull_request.head.ref, 'rc/') + + steps: + - name: Rename branch + env: + BRANCH: ${{ github.event.pull_request.head.ref }} + REPO: ${{ github.repository }} + GIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + TOKEN: ${{ secrets.MOKOGIT_TOKEN }} + run: | + set -euo pipefail + # BRANCH is attacker-controlled (PR head ref). Strict allowlist before ANY use. + if ! printf '%s' "$BRANCH" | grep -Eq '^rc/[A-Za-z0-9._/-]+$'; then + echo "::error::Refusing unsafe branch name: $BRANCH"; exit 1 + fi + SUFFIX="${BRANCH#rc/}" + DEV_BRANCH="dev/${SUFFIX}" + API="${GIT_URL}/api/v1/repos/${REPO}/branches" + + # Create dev/ branch from rc/ branch + STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \ + -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \ + "${API}" 2>/dev/null || true) + if [ "$STATUS" = "201" ]; then + echo "Created branch: ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY" + else + echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"; exit 1 + fi + + # Read BRANCH from the environment inside PHP (getenv, no string interpolation -> no PHP injection) + ENCODED=$(php -r 'echo rawurlencode(getenv("BRANCH"));') + STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \ + -H "Authorization: token ${TOKEN}" \ + "${API}/${ENCODED}" 2>/dev/null || true) + if [ "$STATUS" = "204" ]; then + echo "Deleted branch: ${BRANCH}" >> "$GITHUB_STEP_SUMMARY" + else + echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})" + fi + + echo "### RC Reverted" >> "$GITHUB_STEP_SUMMARY" + echo "${BRANCH} → ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY" -- 2.52.0 From f035a56f582192ded4332de85d50d08abecb653f Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 21:10:05 +0000 Subject: [PATCH 57/63] chore: sync repo-health.yml from Template-Joomla [skip ci] --- .mokogit/workflows/repo-health.yml | 700 +++++++++++++++++++++++++++++ 1 file changed, 700 insertions(+) create mode 100644 .mokogit/workflows/repo-health.yml diff --git a/.mokogit/workflows/repo-health.yml b/.mokogit/workflows/repo-health.yml new file mode 100644 index 0000000..9422dff --- /dev/null +++ b/.mokogit/workflows/repo-health.yml @@ -0,0 +1,700 @@ +# ============================================================================ +# Copyright (C) 2025 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGit.Workflow +# INGROUP: MokoCLI.Validation +# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# PATH: /templates/workflows/joomla/repo_health.yml.template +# VERSION: 09.23.00 +# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts. +# ============================================================================ + +name: "Generic: Repo Health" + +defaults: + run: + shell: bash + +on: + workflow_dispatch: + inputs: + profile: + description: 'Validation profile: all, scripts, or repo' + required: true + default: all + type: choice + options: + - all + - scripts + - repo + pull_request: + branches: + - main + +permissions: + contents: read + +env: + # Scripts governance policy + SCRIPTS_REQUIRED_DIRS: + SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate + + # Repo health policy + REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogit/workflows/ + REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/ + REPO_DISALLOWED_DIRS: + REPO_DISALLOWED_FILES: TODO.md,todo.md + + # Extended checks toggles + EXTENDED_CHECKS: "true" + + # File / directory variables + DOCS_INDEX: docs/docs-index.md + SCRIPT_DIR: scripts + WORKFLOWS_DIR: .mokogit/workflows + SHELLCHECK_PATTERN: '*.sh' + SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml' + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + access_check: + name: Access control + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + + outputs: + allowed: ${{ steps.perm.outputs.allowed }} + permission: ${{ steps.perm.outputs.permission }} + + steps: + - name: Check actor permission (admin only) + id: perm + env: + TOKEN: ${{ secrets.MOKOGIT_TOKEN || github.token }} + REPO: ${{ github.repository }} + ACTOR: ${{ github.actor }} + run: | + set -euo pipefail + ALLOWED=false + PERMISSION=unknown + METHOD="" + + # Hardcoded authorized users — always allowed + case "$ACTOR" in + jmiller|mokogit-actions[bot]) + ALLOWED=true + PERMISSION=admin + METHOD="hardcoded allowlist" + ;; + *) + # Detect platform and check permissions via API + API_BASE="${GITHUB_API_URL:-${GIT_API_URL:-https://api.github.com}}" + RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}') + PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown") + if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then + ALLOWED=true + fi + METHOD="collaborator API" + ;; + esac + + echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT" + echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT" + + { + echo "## Access Authorization" + echo "" + echo "| Field | Value |" + echo "|-------|-------|" + echo "| **Actor** | \`${ACTOR}\` |" + echo "| **Repository** | \`${REPO}\` |" + echo "| **Permission** | \`${PERMISSION}\` |" + echo "| **Method** | ${METHOD} |" + echo "| **Authorized** | ${ALLOWED} |" + echo "" + if [ "$ALLOWED" = "true" ]; then + echo "${ACTOR} authorized (${METHOD})" + else + echo "${ACTOR} is NOT authorized. Requires admin or maintain role." + fi + } >> "${GITHUB_STEP_SUMMARY}" + + - name: Deny execution when not permitted + if: ${{ steps.perm.outputs.allowed != 'true' }} + run: | + set -euo pipefail + printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}" + exit 1 + + scripts_governance: + name: Scripts governance + needs: access_check + if: ${{ needs.access_check.outputs.allowed == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Scripts folder checks + env: + PROFILE_RAW: ${{ github.event.inputs.profile }} + run: | + set -euo pipefail + + profile="${PROFILE_RAW:-all}" + case "${profile}" in + all|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'repo' ]; then + { + printf '%s\n' '### Scripts governance' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes scripts governance' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + if [ ! -d "${SCRIPT_DIR}" ]; then + { + printf '%s\n' '### Scripts governance' + printf '%s\n' 'Status: OK (advisory)' + printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi + IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}" + + missing_dirs=() + unapproved_dirs=() + + for d in "${required_dirs[@]}"; do + req="${d%/}" + [ ! -d "${req}" ] && missing_dirs+=("${req}/") + done + + while IFS= read -r d; do + allowed=false + for a in "${allowed_dirs[@]}"; do + a_norm="${a%/}" + [ "${d%/}" = "${a_norm}" ] && allowed=true + done + [ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/") + done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##') + + { + printf '%s\n' '### Scripts governance' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Area | Status | Notes |' + printf '%s\n' '|---|---|---|' + + if [ "${#missing_dirs[@]}" -gt 0 ]; then + printf '%s\n' '| Required directories | Warning | Missing required subfolders |' + else + printf '%s\n' '| Required directories | OK | All required subfolders present |' + fi + + if [ "${#unapproved_dirs[@]}" -gt 0 ]; then + printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |' + else + printf '%s\n' '| Directory policy | OK | No unapproved directories |' + fi + + printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |' + printf '\n' + + if [ "${#missing_dirs[@]}" -gt 0 ]; then + printf '%s\n' 'Missing required script directories:' + for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + else + printf '%s\n' 'Missing required script directories: none.' + printf '\n' + fi + + if [ "${#unapproved_dirs[@]}" -gt 0 ]; then + printf '%s\n' 'Unapproved script directories detected:' + for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + else + printf '%s\n' 'Unapproved script directories detected: none.' + printf '\n' + fi + + printf '%s\n' 'Scripts governance completed in advisory mode.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + repo_health: + name: Repository health + needs: access_check + if: ${{ needs.access_check.outputs.allowed == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Repository health checks + env: + PROFILE_RAW: ${{ github.event.inputs.profile }} + run: | + set -euo pipefail + + profile="${PROFILE_RAW:-all}" + case "${profile}" in + all|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'scripts' ]; then + { + printf '%s\n' '### Repository health' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes repository health' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" + IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" + if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi + IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}" + + missing_required=() + missing_optional=() + + # Source directory: src/ or htdocs/ (either is valid for extension repos) + SOURCE_DIR="" + if [ -d "src" ]; then + SOURCE_DIR="src" + elif [ -d "htdocs" ]; then + SOURCE_DIR="htdocs" + elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then + # Platform/tooling repos don't need src/ + SOURCE_DIR="" + else + missing_required+=("src/ or htdocs/ (source directory required)") + fi + + for item in "${required_artifacts[@]}"; do + if printf '%s' "${item}" | grep -q '/$'; then + d="${item%/}" + [ ! -d "${d}" ] && missing_required+=("${item}") + else + [ ! -f "${item}" ] && missing_required+=("${item}") + fi + done + + for f in "${optional_files[@]}"; do + if printf '%s' "${f}" | grep -q '/$'; then + d="${f%/}" + [ ! -d "${d}" ] && missing_optional+=("${f}") + else + [ ! -f "${f}" ] && missing_optional+=("${f}") + fi + done + + for d in "${disallowed_dirs[@]}"; do + d_norm="${d%/}" + [ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)") + done + + for f in "${disallowed_files[@]}"; do + [ -f "${f}" ] && missing_required+=("${f} (disallowed)") + done + + git fetch origin --prune + + dev_paths=() + dev_branches=() + + while IFS= read -r b; do + name="${b#origin/}" + if [ "${name}" = 'dev' ]; then + dev_branches+=("${name}") + else + dev_paths+=("${name}") + fi + done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//') + + if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then + missing_required+=("dev or dev/* branch") + fi + + content_warnings=() + + if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then + content_warnings+=("CHANGELOG.md missing '# Changelog' header") + fi + + if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then + content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)") + fi + + if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then + content_warnings+=("LICENSE does not look like a GPL text") + fi + + if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then + content_warnings+=("README.md missing expected brand keyword") + fi + + export PROFILE_RAW="${profile}" + export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")" + export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")" + export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")" + + report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}") + + { + printf '%s\n' '### Repository health' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Metric | Value |' + printf '%s\n' '|---|---|' + printf '%s\n' "| Missing required | ${#missing_required[@]} |" + printf '%s\n' "| Missing optional | ${#missing_optional[@]} |" + printf '%s\n' "| Content warnings | ${#content_warnings[@]} |" + printf '\n' + + printf '%s\n' '### Guardrails report (JSON)' + printf '%s\n' '```json' + printf '%s\n' "${report_json}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${#missing_required[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing required repo artifacts' + for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done + printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + if [ "${#missing_optional[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing optional repo artifacts' + for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + if [ "${#content_warnings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Repo content warnings' + for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + # -- Joomla-specific checks -- + joomla_findings=() + + MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '/dev/null | head -1 || true)" + if [ -z "${MANIFEST}" ]; then + joomla_findings+=("Joomla XML manifest not found (no *.xml with tag)") + else + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then + joomla_findings+=("XML manifest: type attribute missing or invalid") + fi + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + if ! grep -qP ' missing (required for Joomla 5+)") + fi + fi + + INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)" + if [ "${INI_COUNT}" -eq 0 ]; then + joomla_findings+=("No .ini language files found") + fi + + if [ ! -f 'updates.xml' ]; then + joomla_findings+=("updates.xml missing in root (required for Joomla update server)") + fi + + if [ -n "${SOURCE_DIR}" ]; then + INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site") + for dir in "${INDEX_DIRS[@]}"; do + if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then + joomla_findings+=("${dir}/index.html missing (directory listing protection)") + fi + done + fi + + if [ "${#joomla_findings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Joomla extension checks' + printf '%s\n' '| Check | Status |' + printf '%s\n' '|---|---|' + for f in "${joomla_findings[@]}"; do + printf '%s\n' "| ${f} | Warning |" + done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + else + { + printf '%s\n' '### Joomla extension checks' + printf '%s\n' 'All Joomla-specific checks passed.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + extended_enabled="${EXTENDED_CHECKS:-true}" + extended_findings=() + + if [ "${extended_enabled}" = 'true' ]; then + if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then + : + else + extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)") + fi + + if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then + bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)" + if [ -n "${bad_refs}" ]; then + extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt") + { + printf '%s\n' '### Workflow pinning advisory' + printf '%s\n' 'Found uses: entries pinned to main/master:' + printf '%s\n' '```' + printf '%s\n' "${bad_refs}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + if [ -f "${DOCS_INDEX}" ]; then + missing_links="" + while IFS= read -r docline; do + for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do + case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac + linkpath="${link%%#*}" + linkpath="${linkpath%%\?*}" + [ -z "$linkpath" ] && continue + if [ "${linkpath:0:1}" = "/" ]; then + testpath="${linkpath#/}" + else + testpath="$(dirname "${DOCS_INDEX}")/${linkpath}" + fi + [ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} " + done + done < "${DOCS_INDEX}" + if [ -n "${missing_links}" ]; then + extended_findings+=("docs/docs-index.md contains broken relative links") + { + printf '%s\n' '### Docs index link integrity' + printf '%s\n' 'Broken relative links:' + for bl in ${missing_links}; do + printf '%s\n' "- ${bl}" + done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + if [ -d "${SCRIPT_DIR}" ]; then + if ! command -v shellcheck >/dev/null 2>&1; then + sudo apt-get update -qq + sudo apt-get install -y shellcheck >/dev/null + fi + + sc_out='' + while IFS= read -r shf; do + [ -z "${shf}" ] && continue + out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)" + if [ -n "${out_one}" ]; then + sc_out="${sc_out}${out_one}\n" + fi + done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort) + + if [ -n "${sc_out}" ]; then + extended_findings+=("ShellCheck warnings detected (advisory)") + sc_head="$(printf '%s' "${sc_out}" | head -n 200)" + { + printf '%s\n' '### ShellCheck (advisory)' + printf '%s\n' '```' + printf '%s\n' "${sc_head}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + spdx_missing=() + IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}" + spdx_args=() + for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done + + while IFS= read -r f; do + [ -z "${f}" ] && continue + if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then + spdx_missing+=("${f}") + fi + done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true) + + if [ "${#spdx_missing[@]}" -gt 0 ]; then + extended_findings+=("SPDX header missing in some tracked files (advisory)") + { + printf '%s\n' '### SPDX header advisory' + printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):' + for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + stale_cutoff_days=180 + stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)" + if [ -n "${stale_branches}" ]; then + extended_findings+=("Stale remote branches detected (advisory)") + { + printf '%s\n' '### Git hygiene advisory' + printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):" + while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}" + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + { + printf '%s\n' '### Guardrails coverage matrix' + printf '%s\n' '| Domain | Status | Notes |' + printf '%s\n' '|---|---|---|' + printf '%s\n' '| Access control | OK | Admin-only execution gate |' + printf '%s\n' '| Release policy | N/A | Releases handled by MokoGit |' + printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |' + printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |' + printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |' + if [ "${extended_enabled}" = 'true' ]; then + if [ "${#extended_findings[@]}" -gt 0 ]; then + printf '%s\n' '| Extended checks | Warning | See extended findings below |' + else + printf '%s\n' '| Extended checks | OK | No findings |' + fi + else + printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |' + fi + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Extended findings (advisory)' + for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}" + + + site-health: + name: Site Health + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + + - name: Uptime check + if: env.URLS != '' + run: | + echo "$URLS" > /tmp/urls.txt + php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down" + rm -f /tmp/urls.txt + env: + URLS: ${{ vars.MONITORED_URLS }} + + - name: SSL certificate check + if: env.DOMAINS != '' + run: | + echo "$DOMAINS" > /tmp/domains.txt + php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon" + rm -f /tmp/domains.txt + env: + DOMAINS: ${{ vars.MONITORED_DOMAINS }} + + - name: Summary + if: always() + run: | + echo "### Site Health" >> $GITHUB_STEP_SUMMARY + echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY + + # ═══════════════════════════════════════════════════════════════════════ + # Issue Reporter — file issues for failed gates + # ═══════════════════════════════════════════════════════════════════════ + report-scripts: + name: "Report: Scripts Governance" + needs: [access_check, scripts_governance] + if: >- + always() && + needs.scripts_governance.result == 'failure' + uses: ./.mokogit/workflows/ci-issue-reporter.yml + with: + gate: "Scripts Governance" + workflow: "Repo Health" + severity: error + details: "Scripts directory policy violations detected. Review required and allowed directories." + secrets: inherit + + report-health: + name: "Report: Repository Health" + needs: [access_check, repo_health] + if: >- + always() && + needs.repo_health.result == 'failure' + uses: ./.mokogit/workflows/ci-issue-reporter.yml + with: + gate: "Repository Health" + workflow: "Repo Health" + severity: error + details: "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary." + secrets: inherit -- 2.52.0 From 5494ed409cb92f57f9dbb5be0492e6a535aa4cd5 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 21:10:06 +0000 Subject: [PATCH 58/63] chore: sync standards-compliance.yml from Template-Joomla [skip ci] --- .mokogit/workflows/standards-compliance.yml | 2613 +++++++++++++++++++ 1 file changed, 2613 insertions(+) create mode 100644 .mokogit/workflows/standards-compliance.yml diff --git a/.mokogit/workflows/standards-compliance.yml b/.mokogit/workflows/standards-compliance.yml new file mode 100644 index 0000000..4a8d9d6 --- /dev/null +++ b/.mokogit/workflows/standards-compliance.yml @@ -0,0 +1,2613 @@ +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoCLI.Compliance +# REPO: https://github.com/mokoconsulting-tech/mokocli +# PATH: /.mokogit/workflows/standards-compliance.yml +# VERSION: 04.06.00 +# BRIEF: MokoCLI compliance validation workflow +# NOTE: Validates repository structure, documentation, and coding standards + +name: "Generic: Standards Compliance" + +# ╔════════════════════════════════════════════════════════════════════════╗ +# ║ MOKOSTANDARDS COMPLIANCE WORKFLOW ║ +# ╠════════════════════════════════════════════════════════════════════════╣ +# ║ ║ +# ║ 28 checks across 4 priority tiers: ║ +# ║ ║ +# ║ TIER 1 — CRITICAL (must pass) ║ +# ║ secret-scanning, license-compliance, repository-structure, ║ +# ║ coding-standards, version-consistency ║ +# ║ ║ +# ║ TIER 2 — IMPORTANT (should pass) ║ +# ║ workflow-validation, documentation-quality, readme-completeness, ║ +# ║ git-hygiene, script-integrity ║ +# ║ ║ +# ║ TIER 3 — QUALITY (code metrics) ║ +# ║ line-length, file-naming, insecure-patterns, complexity, ║ +# ║ duplication, dead-code ║ +# ║ ║ +# ║ TIER 4 — SUPPLEMENTARY (informational) ║ +# ║ file-size, binary, todo, deps, links, api-docs, accessibility, ║ +# ║ performance, enterprise, health, terraform ║ +# ║ ║ +# ║ File size: warning >15MB, critical >20MB ║ +# ║ Exempt: .mmdb, .woff2, .woff, .ttf, .otf ║ +# ║ ║ +# ╚════════════════════════════════════════════════════════════════════════╝ + +env: + WORKFLOW_VERSION: "04.04.01" + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +# MokoCLI Policy Compliance: +# - File formatting: Enforces organizational coding standards +# - Reference: docs/policy/file-formatting.md + +# ┌─────────────────────────────────────────────────────────────────────────┐ +# │ WORKFLOW FLOW DIAGRAM │ +# └─────────────────────────────────────────────────────────────────────────┘ +# +# TRIGGER: Push/PR to main/dev/rc branches +# │ +# ▼ +# ┌──────────────────────────────────────────────────────────────┐ +# │ PARALLEL VALIDATION CHECKS │ +# └──────────────────────────────────────────────────────────────┘ +# │ +# ├─────────────┬──────────────┬──────────────┬────────────┐ +# ▼ ▼ ▼ ▼ ▼ +# ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ ┌──────────┐ +# │Repository │File Header │Code Style│ │ Docs │ │ License │ +# │Structure│ │ Validation│ │ Check │ │ Check │ │ Check │ +# └─────────┘ └──────────┘ └──────────┘ └─────────┘ └──────────┘ +# │ │ │ │ │ +# ▼ ▼ ▼ ▼ ▼ +# ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ ┌──────────┐ +# │ Check │ │ Verify │ │ Run │ │ Check │ │ Verify │ +# │Required │ │Copyright │ │ Linters │ │README │ │SPDX-ID │ +# │ Dirs │ │ Header │ │(Python, │ │ Exists │ │ Present │ +# │ │ │ Format │ │PHP,YAML) │ │ │ │ │ +# └─────────┘ └──────────┘ └──────────┘ └─────────┘ └──────────┘ +# │ │ │ │ │ +# └─────────────┴──────────────┴──────────────┴────────────┘ +# │ +# ▼ +# ┌──────────────────┐ +# │ All Checks Pass?│ +# └──────────────────┘ +# │ │ +# YES │ │ NO +# ▼ ▼ +# ┌──────────┐ ┌──────────────┐ +# │ SUCCESS │ │ CREATE ISSUE │ +# │ Summary │ │ with Failure │ +# └──────────┘ │ Details │ +# └──────────────┘ + +on: + push: + branches: [main, dev/**, rc/**, version/**] + pull_request: + branches: [main, dev/**, rc/**] + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + # ════════════════════════════════════════════════════════════════════════ + # TIER 1 — CRITICAL (must pass, blocks merge) + # ════════════════════════════════════════════════════════════════════════ + secret-scanning: + name: Secret Scanning + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Scan for Secrets + run: | + set -x + echo "## 🔒 Secret Scanning" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Scanning for hardcoded secrets and credentials." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Define secret patterns + VIOLATIONS=0 + + # Check for common secret patterns + echo "### Secret Patterns" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Helper: scan with a pattern, show results with file:line, return count + scan_pattern() { + local label="$1" icon="$2" tmpfile="$3" + local count=0 + if [ -f "$tmpfile" ]; then + count=$(wc -l < "$tmpfile") + fi + if [ "$count" -gt 0 ]; then + echo "${icon} **${label}**: ${count} finding(s)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View locations" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| File | Line | Match |" >> $GITHUB_STEP_SUMMARY + echo "|------|------|-------|" >> $GITHUB_STEP_SUMMARY + head -20 "$tmpfile" | while IFS= read -r line; do + FILE=$(echo "$line" | cut -d: -f1 | sed 's|^\./||') + LINENO=$(echo "$line" | cut -d: -f2) + MATCH=$(echo "$line" | cut -d: -f3- | head -c 80 | sed 's/|/\\|/g') + echo "| \`${FILE}\` | ${LINENO} | \`${MATCH}\` |" >> $GITHUB_STEP_SUMMARY + done + if [ "$count" -gt 20 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "*... and $((count - 20)) more*" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + VIOLATIONS=$((VIOLATIONS + count)) + fi + } + + # Pattern 1: password/secret assignments + grep -r -n -E "(password|passwd|pwd|secret|api[_-]?key|token).*=.*['\"]" . \ + --include="*.php" --include="*.py" --include="*.js" --include="*.ts" \ + --exclude-dir=".git" --exclude-dir="vendor" --exclude-dir="node_modules" 2>/dev/null | \ + grep -v -E '(test|example|sample|getenv|getString|getArgument|config\[|/\.\*/|^\s*//|^\s*\*|CREDENTIAL_PATTERNS|SecurityValidator|SECRET_PATTERN|===|!==|ApiClient|str_contains|gen_wrappers)' | \ + grep -v "= ''" | grep -v '= ""' | grep -v '\$this->config' | \ + grep -v 'type="password"' | grep -v 'type="text"' | grep -v 'name="password"' | grep -v 'name="secretkey"' | \ + grep -v '/dev/null > /tmp/secrets2.txt || true + scan_pattern "Private keys" "❌" /tmp/secrets2.txt + + # Pattern 3: AWS keys + grep -r -n -E "AKIA[0-9A-Z]{16}" . \ + --include="*.php" --include="*.py" --include="*.js" --include="*.txt" --include="*.env" \ + --exclude-dir=".git" --exclude-dir="vendor" --exclude-dir="node_modules" 2>/dev/null > /tmp/secrets3.txt || true + scan_pattern "AWS access keys" "❌" /tmp/secrets3.txt + + # Pattern 4: GitHub tokens + grep -r -n -E "gh[ps]_[a-zA-Z0-9]{36}" . \ + --include="*.php" --include="*.py" --include="*.js" --include="*.txt" --include="*.env" \ + --exclude-dir=".git" --exclude-dir="vendor" --exclude-dir="node_modules" 2>/dev/null > /tmp/secrets4.txt || true + scan_pattern "GitHub tokens" "❌" /tmp/secrets4.txt + + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$VIOLATIONS" -gt 0 ]; then + echo "**Total Violations**: $VIOLATIONS" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View detected secrets (file paths only)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/secrets*.txt 2>/dev/null | cut -d: -f1 | sort -u >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Action Required**: Remove hardcoded secrets immediately!" >> $GITHUB_STEP_SUMMARY + echo "Use environment variables or secrets management instead." >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "✅ No hardcoded secrets detected" >> $GITHUB_STEP_SUMMARY + fi + + license-compliance: + name: License Header Validation + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check SPDX Headers + run: | + set -x + echo "### SPDX License Header Check" >> $GITHUB_STEP_SUMMARY + + # Count source files with and without SPDX headers + TOTAL_PHP=0 + WITH_SPDX_PHP=0 + + if find . -name "*.php" -type f ! -path "./vendor/*" | head -1 | grep -q .; then + TOTAL_PHP=$(find . -name "*.php" -type f ! -path "./vendor/*" | wc -l) + WITH_SPDX_PHP=$(find . -name "*.php" -type f ! -path "./vendor/*" -exec grep -l "SPDX-License-Identifier" {} \; | wc -l) + fi + + if [ "$TOTAL_PHP" -gt 0 ]; then + PERCENT=$((WITH_SPDX_PHP * 100 / TOTAL_PHP)) + echo "- PHP files: $WITH_SPDX_PHP/$TOTAL_PHP ($PERCENT%) with SPDX headers" >> $GITHUB_STEP_SUMMARY + + if [ "$PERCENT" -lt 80 ]; then + echo "⚠️ Less than 80% of PHP files have SPDX headers" >> $GITHUB_STEP_SUMMARY + else + echo "✅ Good SPDX header coverage" >> $GITHUB_STEP_SUMMARY + fi + fi + + - name: Validate License File + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### License File Validation" >> $GITHUB_STEP_SUMMARY + + if [ ! -f "LICENSE" ]; then + echo "❌ LICENSE file not found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ❌ Validation Failed: LICENSE File Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** LICENSE file is required for all MokoCLI-compliant repositories" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Add LICENSE file with appropriate open-source license (GPL-3.0-or-later recommended)" >> $GITHUB_STEP_SUMMARY + echo "" + echo "❌ ERROR: LICENSE file not found - This is a critical requirement" + exit 1 + fi + + # Check license type + if grep -qi "GNU GENERAL PUBLIC LICENSE" LICENSE; then + VERSION=$(grep -i "Version 3" LICENSE || echo "") + if [ -n "$VERSION" ]; then + echo "✅ GPL-3.0-or-later license detected" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ GPL license detected but version unclear" >> $GITHUB_STEP_SUMMARY + fi + elif grep -qi "MIT License" LICENSE; then + echo "✅ MIT license detected" >> $GITHUB_STEP_SUMMARY + elif grep -qi "Apache License" LICENSE; then + echo "✅ Apache license detected" >> $GITHUB_STEP_SUMMARY + else + echo "ℹ️ License type could not be automatically detected" >> $GITHUB_STEP_SUMMARY + fi + + repository-structure: + name: Repository Structure Validation + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Required Directories + run: | + set -x + echo "## 📁 Repository Structure Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + MISSING=0 + PRESENT=0 + TOTAL=1 + + echo "### Required Directories" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Directory | Status | Files | Size | Notes |" >> $GITHUB_STEP_SUMMARY + echo "|-----------|--------|-------|------|-------|" >> $GITHUB_STEP_SUMMARY + + # Check required directories + for dir in .mokogit; do + if [ -d "$dir" ]; then + FILE_COUNT=$(find "$dir" -type f 2>/dev/null | wc -l) + DIR_SIZE=$(du -sh "$dir" 2>/dev/null | cut -f1) + echo "| $dir/ | ✅ Pass | $FILE_COUNT files | $DIR_SIZE | Complete |" >> $GITHUB_STEP_SUMMARY + PRESENT=$((PRESENT + 1)) + else + echo "| $dir/ | ❌ **Missing** | - | - | **Action Required** |" >> $GITHUB_STEP_SUMMARY + MISSING=$((MISSING + 1)) + fi + done + + echo "" >> $GITHUB_STEP_SUMMARY + PERCENT=$((PRESENT * 100 / TOTAL)) + echo "**Compliance Score:** $PERCENT% ($PRESENT/$TOTAL directories present)" >> $GITHUB_STEP_SUMMARY + + if [ "$MISSING" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 🔴 Critical Issues: $MISSING" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Remediation Steps:**" >> $GITHUB_STEP_SUMMARY + [ ! -d ".mokogit" ] && echo "- Create .mokogit directory: \`mkdir -p .mokogit/workflows\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "📚 Reference: [MokoCLI Repository Structure](https://github.com/mokoconsulting-tech/mokocli/tree/main/docs/policy/core-structure.md)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ❌ Validation Failed: Required Directories Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Status:** Repository structure does not meet MokoCLI requirements" >> $GITHUB_STEP_SUMMARY + echo "**Missing:** $MISSING required director(y|ies)" >> $GITHUB_STEP_SUMMARY + echo "**Compliance:** $PERCENT% ($PRESENT/$TOTAL directories present)" >> $GITHUB_STEP_SUMMARY + echo "" + echo "❌ ERROR: Required directories missing - See job summary for remediation steps" + exit 1 + fi + + - name: Check Required Files + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Required Files" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + MISSING=0 + PRESENT=0 + TOTAL=5 + + echo "| File | Status | Size | Last Modified | Notes |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|------|---------------|-------|" >> $GITHUB_STEP_SUMMARY + + # Check required files (CHANGELOG handled separately via find -iname to support src/ChangeLog.md) + for file in README.md LICENSE CONTRIBUTING.md SECURITY.md .editorconfig; do + if [ -f "$file" ]; then + FILE_SIZE=$(wc -c < "$file" 2>/dev/null | awk '{printf "%.1f KB", $1/1024}') + LAST_MOD=$(stat -c %y "$file" 2>/dev/null | cut -d' ' -f1 || echo "Unknown") + CONTENT_CHECK="" + + # Basic content validation + case "$file" in + "README.md") + LINES=$(wc -l < "$file") + [ "$LINES" -lt 10 ] && CONTENT_CHECK="⚠️ Too short" + ;; + "LICENSE") + [ $(wc -c < "$file") -lt 100 ] && CONTENT_CHECK="⚠️ Incomplete?" + ;; + esac + + echo "| $file | ✅ Pass | $FILE_SIZE | $LAST_MOD | Complete $CONTENT_CHECK |" >> $GITHUB_STEP_SUMMARY + PRESENT=$((PRESENT + 1)) + else + echo "| $file | ❌ **Missing** | - | - | **Required** |" >> $GITHUB_STEP_SUMMARY + MISSING=$((MISSING + 1)) + fi + done + + echo "" >> $GITHUB_STEP_SUMMARY + PERCENT=$((PRESENT * 100 / TOTAL)) + echo "**Compliance Score:** $PERCENT% ($PRESENT/$TOTAL files present)" >> $GITHUB_STEP_SUMMARY + + if [ "$MISSING" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 🔴 Critical Issues: $MISSING" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Remediation Steps:**" >> $GITHUB_STEP_SUMMARY + [ ! -f "README.md" ] && echo "- Create README.md: Use [template](https://github.com/mokoconsulting-tech/mokocli/tree/main/templates/docs/required/README.md)" >> $GITHUB_STEP_SUMMARY + [ ! -f "LICENSE" ] && echo "- Add LICENSE file: Choose from [OSI-approved licenses](https://opensource.org/licenses)" >> $GITHUB_STEP_SUMMARY + [ ! -f "CONTRIBUTING.md" ] && echo "- Create CONTRIBUTING.md: Use [template](https://github.com/mokoconsulting-tech/mokocli/tree/main/templates/docs/required/CONTRIBUTING.md)" >> $GITHUB_STEP_SUMMARY + [ ! -f "SECURITY.md" ] && echo "- Create SECURITY.md: Use [template](https://github.com/mokoconsulting-tech/mokocli/tree/main/templates/docs/required/SECURITY.md)" >> $GITHUB_STEP_SUMMARY + [ ! -f ".editorconfig" ] && echo "- Add .editorconfig: Use [template](https://github.com/mokoconsulting-tech/mokocli/tree/main/templates/.editorconfig)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "📚 Reference: [MokoCLI File Requirements](https://github.com/mokoconsulting-tech/mokocli/tree/main/docs/policy/file-header-standards.md)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ❌ Validation Failed: Required Files Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Status:** Repository files do not meet MokoCLI requirements" >> $GITHUB_STEP_SUMMARY + echo "**Missing:** $MISSING required file(s)" >> $GITHUB_STEP_SUMMARY + echo "**Compliance:** $PERCENT% ($PRESENT/$TOTAL files present)" >> $GITHUB_STEP_SUMMARY + echo "" + echo "❌ ERROR: Required files missing - See job summary for remediation steps" + exit 1 + fi + + coding-standards: + name: Coding Standards Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check for Tab Characters + run: | + set -x + echo "### Tab Character Detection" >> $GITHUB_STEP_SUMMARY + + # Policy: Tabs are DEFAULT. Only check for tabs in files that REQUIRE spaces. + # Languages requiring spaces: YAML, Python, Haskell, F#, CoffeeScript, Nim, JSON, RST + TABS_IN_SPACES_FILES=$(find . -type f \ + \( -name "*.yml" -o -name "*.yaml" \ + -o -name "*.py" \ + -o -name "*.hs" -o -name "*.lhs" \ + -o -name "*.fs" -o -name "*.fsx" -o -name "*.fsi" \ + -o -name "*.coffee" -o -name "*.litcoffee" \ + -o -name "*.nim" -o -name "*.nims" -o -name "*.nimble" \ + -o -name "*.json" \ + -o -name "*.rst" \) \ + ! -path "./vendor/*" \ + ! -path "./node_modules/*" \ + ! -path "./.git/*" \ + -exec grep -l $'\t' {} \; 2>/dev/null | head -10) + + if [ -n "$TABS_IN_SPACES_FILES" ]; then + echo "⚠️ Tab characters found in files that require spaces:" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$TABS_IN_SPACES_FILES" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "These languages require spaces (tabs will break): YAML, Python, Haskell, F#, CoffeeScript, Nim, JSON, RST" >> $GITHUB_STEP_SUMMARY + echo "All other files (including .md, .ps1, LICENSE, etc.) may use tabs per MokoCLI policy" >> $GITHUB_STEP_SUMMARY + else + echo "✅ No tabs found in files requiring spaces" >> $GITHUB_STEP_SUMMARY + echo "Note: Tabs are allowed in most files (policy default). Only checked files requiring spaces." >> $GITHUB_STEP_SUMMARY + fi + + - name: Check File Encoding + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### File Encoding Check" >> $GITHUB_STEP_SUMMARY + + # Check for UTF-8 encoding (ASCII is a subset of UTF-8 and is acceptable) + NON_UTF8=$(find . -type f \( -name "*.php" -o -name "*.js" -o -name "*.md" \) \ + ! -path "./vendor/*" \ + ! -path "./node_modules/*" \ + ! -path "./.git/*" \ + -exec file {} \; | grep -v "UTF-8" | grep -v "ASCII" | head -5 || true) + + if [ -n "$NON_UTF8" ]; then + echo "⚠️ Non-UTF-8 files detected:" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$NON_UTF8" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + else + echo "✅ All source files appear to be UTF-8 encoded" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check Line Endings + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Line Ending Check" >> $GITHUB_STEP_SUMMARY + + # Check for CRLF line endings + CRLF_FILES=$(find . -type f \( -name "*.php" -o -name "*.js" -o -name "*.md" \) \ + ! -path "./vendor/*" \ + ! -path "./node_modules/*" \ + ! -path "./.git/*" \ + -exec file {} \; | grep "CRLF" | head -5 || true) + + if [ -n "$CRLF_FILES" ]; then + echo "⚠️ Files with CRLF line endings found:" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$CRLF_FILES" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "MokoCLI requires LF line endings" >> $GITHUB_STEP_SUMMARY + else + echo "✅ Line endings are consistent (LF)" >> $GITHUB_STEP_SUMMARY + fi + + version-consistency: + name: Version Consistency Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + extensions: json + tools: composer + coverage: none + + - name: Setup MokoCLI tools + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/mokocli.git" \ + /tmp/mokostandards 2>/dev/null || true + if [ -d "/tmp/mokostandards" ] && [ -f "/tmp/mokostandards/composer.json" ]; then + cd /tmp/mokostandards + composer install --no-dev --no-interaction --quiet 2>/dev/null || true + fi + + - name: Run Version Consistency Check + id: version_check + run: | + set -x + echo "## 🔢 Version Consistency Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Use MokoCLI tools (no Composer needed on the governed repo) + if [ -f "/tmp/mokostandards/api/validate/check_version_consistency.php" ]; then + php /tmp/mokostandards/api/validate/check_version_consistency.php --path . --verbose 2>&1 | tee /tmp/version-check.log + EXIT_CODE=${PIPESTATUS[0]} + elif [ -f "api/validate/check_version_consistency.php" ]; then + php api/validate/check_version_consistency.php --path . --verbose 2>&1 | tee /tmp/version-check.log + EXIT_CODE=${PIPESTATUS[0]} + else + echo "⏭️ MokoCLI tools not available — skipping version check" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + echo '```' >> $GITHUB_STEP_SUMMARY + cat /tmp/version-check.log >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + if [ "$EXIT_CODE" -eq 0 ]; then + echo "✅ All version numbers are consistent" >> $GITHUB_STEP_SUMMARY + else + echo "❌ Version drift detected" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + + # ════════════════════════════════════════════════════════════════════════ + # TIER 2 — IMPORTANT (should pass) + # ════════════════════════════════════════════════════════════════════════ + workflow-validation: + name: Workflow Configuration Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Required Workflows + run: | + set -x + echo "### GitHub Actions Workflows" >> $GITHUB_STEP_SUMMARY + + WORKFLOWS_DIR=".mokogit/workflows" + + if [ ! -d "$WORKFLOWS_DIR" ]; then + echo "❌ No workflows directory found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ❌ Validation Failed: Workflows Directory Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** .mokogit/workflows directory is required for CI/CD automation" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Create .mokogit/workflows directory and add GitHub Actions workflows" >> $GITHUB_STEP_SUMMARY + echo "" + echo "❌ ERROR: .mokogit/workflows directory not found" + exit 1 + fi + + # Check for recommended workflows + CI_FOUND=false + for wf in ci.yml build.yml ci-dolibarr.yml ci-joomla.yml; do + if [ -f "$WORKFLOWS_DIR/$wf" ]; then + echo "✅ CI workflow present ($wf)" >> $GITHUB_STEP_SUMMARY + CI_FOUND=true + break + fi + done + if [ "$CI_FOUND" = "false" ]; then + echo "⚠️ No CI workflow found (ci.yml, build.yml, ci-dolibarr.yml, or ci-joomla.yml)" >> $GITHUB_STEP_SUMMARY + fi + + if [ -f "$WORKFLOWS_DIR/codeql-analysis.yml" ]; then + echo "✅ CodeQL security scanning present" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ CodeQL workflow not found" >> $GITHUB_STEP_SUMMARY + fi + + # Check for MokoCLI-synced workflows + for wf in deploy-dev.yml deploy-demo.yml deploy-rs.yml sync-version-on-merge.yml auto-release.yml standards-compliance.yml enterprise-firewall-setup.yml; do + if [ -f "$WORKFLOWS_DIR/$wf" ]; then + echo "✅ ${wf}" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ ${wf} not found (synced from MokoCLI)" >> $GITHUB_STEP_SUMMARY + fi + done + + - name: Validate Workflow Syntax + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Workflow YAML Syntax" >> $GITHUB_STEP_SUMMARY + + INVALID=0 + for workflow in $(find .mokogit/workflows -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null); do + if [ -f "$workflow" ]; then + if python3 -c "import yaml, sys; yaml.safe_load(open(sys.argv[1]))" "$workflow" 2>/dev/null; then + echo "✅ $(basename $workflow)" >> $GITHUB_STEP_SUMMARY + else + echo "❌ $(basename $workflow) - invalid YAML" >> $GITHUB_STEP_SUMMARY + INVALID=$((INVALID + 1)) + fi + fi + done + + if [ "$INVALID" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ❌ Validation Failed: Invalid Workflow YAML Syntax" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** $INVALID workflow file(s) have invalid YAML syntax" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Fix YAML syntax errors in the marked workflow files" >> $GITHUB_STEP_SUMMARY + echo "**Tool:** Run \`python3 -c \"import yaml; yaml.safe_load(open('.mokogit/workflows/FILE.yml'))\"\` locally" >> $GITHUB_STEP_SUMMARY + echo "" + echo "❌ ERROR: $INVALID workflow file(s) with invalid YAML syntax" + exit 1 + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ✅ All Workflow Files Have Valid YAML Syntax" >> $GITHUB_STEP_SUMMARY + echo "" + echo "✅ SUCCESS: All workflow files passed YAML validation" + + - name: Validate CodeQL Configuration + if: hashFiles('.mokogit/workflows/codeql-analysis.yml') != '' + run: | + set -e + echo "" >> $GITHUB_STEP_SUMMARY + echo "### CodeQL Language Configuration" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Inline validation (rewritten from Python to bash for PHP-only architecture) + CODEQL_FILE=".mokogit/workflows/codeql-analysis.yml" + + if [ ! -f "$CODEQL_FILE" ]; then + echo "⚠️ CodeQL workflow file not found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ⚠️ CodeQL Workflow Not Found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Status:** CodeQL workflow file not present - skipping language validation" >> $GITHUB_STEP_SUMMARY + echo "" + echo "⚠️ INFO: CodeQL workflow not found - Skipping validation" + exit 0 + fi + + echo "**CodeQL Configuration Analysis**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Extract configured languages from workflow + LANGUAGES=$(grep -A5 "language:" "$CODEQL_FILE" | grep -oP "(?<=')[^']+(?=')" | tr '\n' ' ' || echo "") + + # Check if this is a configuration-only scan (no languages specified) + if grep -q "category.*language:config" "$CODEQL_FILE"; then + echo "**Scan Type:** Configuration-only (no language matrix)" >> $GITHUB_STEP_SUMMARY + echo "**Status:** ✅ Valid configuration for PHP-only repository" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "This CodeQL workflow scans YAML, JSON, shell scripts for security issues." >> $GITHUB_STEP_SUMMARY + echo "PHP security is handled by SecurityValidator enterprise library." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ SUCCESS: CodeQL configuration-only scan properly configured" + exit 0 + fi + + if [ -z "$LANGUAGES" ]; then + echo "❌ No languages configured in CodeQL workflow" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ❌ Validation Failed: CodeQL Languages Not Configured" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** CodeQL workflow exists but has no languages configured" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Configure appropriate languages in codeql-analysis.yml" >> $GITHUB_STEP_SUMMARY + echo "" + echo "❌ ERROR: No languages configured in CodeQL workflow" + exit 1 + fi + + echo "**Configured Languages:** $LANGUAGES" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Validate language presence in repository + INVALID_LANGS="" + VALID_LANGS="" + + for LANG in $LANGUAGES; do + case "$LANG" in + python) + # Check for Python files (should be none in v04.00.04) + if find . -name "*.py" -type f ! -path "./.git/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS python" + echo "✅ Python: Found Python files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS python" + echo "❌ Python: No Python files found (PHP-only repository)" >> $GITHUB_STEP_SUMMARY + fi + ;; + javascript|typescript) + # Check for JS/TS files + if find . \( -name "*.js" -o -name "*.ts" -o -name "*.json" \) -type f ! -path "./.git/*" ! -path "./node_modules/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS $LANG" + echo "✅ $LANG: Found JavaScript/TypeScript/JSON files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS $LANG" + echo "⚠️ $LANG: No JavaScript/TypeScript files found" >> $GITHUB_STEP_SUMMARY + fi + ;; + java) + if find . -name "*.java" -type f ! -path "./.git/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS java" + echo "✅ Java: Found Java files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS java" + echo "⚠️ Java: No Java files found" >> $GITHUB_STEP_SUMMARY + fi + ;; + go) + if find . -name "*.go" -type f ! -path "./.git/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS go" + echo "✅ Go: Found Go files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS go" + echo "⚠️ Go: No Go files found" >> $GITHUB_STEP_SUMMARY + fi + ;; + cpp|c) + if find . \( -name "*.cpp" -o -name "*.c" -o -name "*.h" \) -type f ! -path "./.git/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS $LANG" + echo "✅ $LANG: Found C/C++ files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS $LANG" + echo "⚠️ $LANG: No C/C++ files found" >> $GITHUB_STEP_SUMMARY + fi + ;; + ruby) + if find . -name "*.rb" -type f ! -path "./.git/*" | grep -q .; then + VALID_LANGS="$VALID_LANGS ruby" + echo "✅ Ruby: Found Ruby files" >> $GITHUB_STEP_SUMMARY + else + INVALID_LANGS="$INVALID_LANGS ruby" + echo "⚠️ Ruby: No Ruby files found" >> $GITHUB_STEP_SUMMARY + fi + ;; + *) + echo "⚠️ $LANG: Unknown language, skipping validation" >> $GITHUB_STEP_SUMMARY + ;; + esac + done + + echo "" >> $GITHUB_STEP_SUMMARY + + # Report results + if [ -n "$INVALID_LANGS" ]; then + echo "**⚠️ Warning:** Some configured languages may not have corresponding files:" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "Invalid languages: $INVALID_LANGS" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note:** This is informational. CodeQL will skip languages without source files." >> $GITHUB_STEP_SUMMARY + echo "For PHP repository (v04.00.04), JavaScript language covers JSON/YAML/shell scripts." >> $GITHUB_STEP_SUMMARY + else + echo "✅ **All configured CodeQL languages have corresponding source files**" >> $GITHUB_STEP_SUMMARY + fi + + # Always succeed - this is informational only + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ✅ CodeQL Configuration Validation Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Status:** CodeQL language configuration reviewed successfully" >> $GITHUB_STEP_SUMMARY + echo "" + echo "✅ SUCCESS: CodeQL validation complete" + exit 0 + + documentation-quality: + name: Documentation Quality Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Validate README.md + run: | + set -x + echo "## 📚 Documentation Quality Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### README.md Analysis" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ ! -f "README.md" ]; then + echo "❌ **Critical:** README.md not found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ❌ Validation Failed: README.md Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** README.md is required for all MokoCLI-compliant repositories" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Create README.md with project description, setup instructions, and usage examples" >> $GITHUB_STEP_SUMMARY + echo "" + echo "❌ ERROR: README.md not found - This is a critical requirement" + exit 1 + fi + + # Detailed content analysis + SIZE=$(wc -c < README.md) + LINES=$(wc -l < README.md) + WORDS=$(wc -w < README.md) + HEADINGS=$(grep -c "^#" README.md || echo 0) + LINKS=$(grep -c "\[.*\](.*)" README.md || echo 0) + CODE_BLOCKS=$(grep -c '```' README.md || echo 0) + + echo "| Metric | Value | Status | Recommendation |" >> $GITHUB_STEP_SUMMARY + echo "|--------|-------|--------|----------------|" >> $GITHUB_STEP_SUMMARY + + # Size check + SIZE_STATUS="✅ Good" + SIZE_REC="Adequate length" + if [ "$SIZE" -lt 500 ]; then + SIZE_STATUS="⚠️ Warning" + SIZE_REC="Add more content (min 500 bytes)" + elif [ "$SIZE" -gt 50000 ]; then + SIZE_STATUS="⚠️ Warning" + SIZE_REC="Consider splitting into multiple docs" + fi + echo "| Size | $SIZE bytes | $SIZE_STATUS | $SIZE_REC |" >> $GITHUB_STEP_SUMMARY + + # Line count + LINES_STATUS="✅ Good" + LINES_REC="Good size" + if [ "$LINES" -lt 20 ]; then + LINES_STATUS="⚠️ Warning" + LINES_REC="Add more sections (min 20 lines)" + fi + echo "| Lines | $LINES | $LINES_STATUS | $LINES_REC |" >> $GITHUB_STEP_SUMMARY + + # Word count + WORDS_STATUS="✅ Good" + WORDS_REC="Good detail" + if [ "$WORDS" -lt 100 ]; then + WORDS_STATUS="⚠️ Warning" + WORDS_REC="Add more description (min 100 words)" + fi + echo "| Words | $WORDS | $WORDS_STATUS | $WORDS_REC |" >> $GITHUB_STEP_SUMMARY + + # Headings + HEADINGS_STATUS="✅ Good" + HEADINGS_REC="Well structured" + if [ "$HEADINGS" -lt 3 ]; then + HEADINGS_STATUS="⚠️ Warning" + HEADINGS_REC="Add more sections (min 3 headings)" + fi + echo "| Headings | $HEADINGS | $HEADINGS_STATUS | $HEADINGS_REC |" >> $GITHUB_STEP_SUMMARY + + # Links + LINKS_STATUS="✅ Good" + LINKS_REC="Includes references" + if [ "$LINKS" -lt 1 ]; then + LINKS_STATUS="ℹ️ Info" + LINKS_REC="Consider adding useful links" + fi + echo "| Links | $LINKS | $LINKS_STATUS | $LINKS_REC |" >> $GITHUB_STEP_SUMMARY + + # Code blocks + CODE_STATUS="✅ Good" + CODE_REC="Includes examples" + if [ "$CODE_BLOCKS" -eq 0 ]; then + CODE_STATUS="ℹ️ Info" + CODE_REC="Consider adding code examples" + fi + echo "| Code blocks | $CODE_BLOCKS | $CODE_STATUS | $CODE_REC |" >> $GITHUB_STEP_SUMMARY + + echo "" >> $GITHUB_STEP_SUMMARY + + # Check for key sections + echo "**Section Coverage:**" >> $GITHUB_STEP_SUMMARY + MISSING_COUNT=0 + grep -qi "install\|setup\|getting started" README.md && echo "- ✅ Installation/Setup instructions" >> $GITHUB_STEP_SUMMARY || { echo "- ⚠️ Missing: Installation/Setup" >> $GITHUB_STEP_SUMMARY; MISSING_COUNT=$((MISSING_COUNT + 1)); } + grep -qi "usage\|example\|how to" README.md && echo "- ✅ Usage examples" >> $GITHUB_STEP_SUMMARY || { echo "- ⚠️ Missing: Usage examples" >> $GITHUB_STEP_SUMMARY; MISSING_COUNT=$((MISSING_COUNT + 1)); } + grep -qi "license" README.md && echo "- ✅ License information" >> $GITHUB_STEP_SUMMARY || { echo "- ⚠️ Missing: License information" >> $GITHUB_STEP_SUMMARY; MISSING_COUNT=$((MISSING_COUNT + 1)); } + grep -qi "contribut" README.md && echo "- ✅ Contributing guidelines" >> $GITHUB_STEP_SUMMARY || echo "- ℹ️ Optional: Contributing section" >> $GITHUB_STEP_SUMMARY + + if [ "$MISSING_COUNT" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**⚠️ $MISSING_COUNT important sections missing**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Validate CHANGELOG.md + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### CHANGELOG.md Analysis" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Locate changelog case-insensitively; accepted at root, src/, or docs/ + CHANGELOG_PATH=$(find . -maxdepth 3 \( -path ./.git -o -path ./node_modules \) -prune \ + -o -iname "changelog.md" -print | head -1 | sed 's|^\./||') + + if [ -z "$CHANGELOG_PATH" ]; then + echo "❌ **Critical:** CHANGELOG.md not found (checked root, src/, docs/)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ❌ Validation Failed: CHANGELOG.md Missing" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Error:** CHANGELOG.md is required for all MokoCLI-compliant repositories" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Create CHANGELOG.md following [Keep a Changelog](https://keepachangelog.com/) format" >> $GITHUB_STEP_SUMMARY + echo "" + echo "❌ ERROR: CHANGELOG.md not found - This is a critical requirement" + exit 1 + fi + + echo "📄 Found: $CHANGELOG_PATH" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Analyze changelog structure + VERSIONS=$(grep -c "## \[" "$CHANGELOG_PATH" || echo 0) + UNRELEASED=$(grep -c "## \[Unreleased\]" "$CHANGELOG_PATH" || echo 0) + DATES=$(grep -c "[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}" "$CHANGELOG_PATH" || echo 0) + SIZE=$(wc -c < "$CHANGELOG_PATH") + + echo "| Metric | Value | Status | Notes |" >> $GITHUB_STEP_SUMMARY + echo "|--------|-------|--------|-------|" >> $GITHUB_STEP_SUMMARY + + # Check format + if grep -qi "## \[.*\]" "$CHANGELOG_PATH"; then + echo "| Format | Keep a Changelog | ✅ Pass | Standard format |" >> $GITHUB_STEP_SUMMARY + else + echo "| Format | Custom | ⚠️ Warning | Consider [Keep a Changelog](https://keepachangelog.com/) |" >> $GITHUB_STEP_SUMMARY + fi + + # Version count + VERSIONS_STATUS="✅ Good" + VERSIONS_NOTE="Well maintained" + if [ "$VERSIONS" -lt 1 ]; then + VERSIONS_STATUS="⚠️ Warning" + VERSIONS_NOTE="Add version entries" + fi + echo "| Versions | $VERSIONS | $VERSIONS_STATUS | $VERSIONS_NOTE |" >> $GITHUB_STEP_SUMMARY + + # Unreleased section + if [ "$UNRELEASED" -gt 0 ]; then + echo "| Unreleased | Yes | ✅ Good | Active development tracked |" >> $GITHUB_STEP_SUMMARY + else + echo "| Unreleased | No | ℹ️ Info | Consider adding [Unreleased] section |" >> $GITHUB_STEP_SUMMARY + fi + + # Dates + DATES_STATUS="✅ Good" + if [ "$DATES" -lt 1 ]; then + DATES_STATUS="⚠️ Warning" + DATES_NOTE="Add release dates" + else + DATES_NOTE="Dates present" + fi + echo "| Release dates | $DATES | $DATES_STATUS | $DATES_NOTE |" >> $GITHUB_STEP_SUMMARY + + # Check for standard sections + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Changelog Sections:**" >> $GITHUB_STEP_SUMMARY + grep -qi "### Added" "$CHANGELOG_PATH" && echo "- ✅ Added section" >> $GITHUB_STEP_SUMMARY || echo "- ℹ️ Added section (optional)" >> $GITHUB_STEP_SUMMARY + grep -qi "### Changed" "$CHANGELOG_PATH" && echo "- ✅ Changed section" >> $GITHUB_STEP_SUMMARY || echo "- ℹ️ Changed section (optional)" >> $GITHUB_STEP_SUMMARY + grep -qi "### Fixed" "$CHANGELOG_PATH" && echo "- ✅ Fixed section" >> $GITHUB_STEP_SUMMARY || echo "- ℹ️ Fixed section (optional)" >> $GITHUB_STEP_SUMMARY + + echo "" >> $GITHUB_STEP_SUMMARY + echo "📚 Reference: [Keep a Changelog](https://keepachangelog.com/)" >> $GITHUB_STEP_SUMMARY + + - name: Check Documentation Index + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Documentation Index" >> $GITHUB_STEP_SUMMARY + + if [ -f "docs/index.md" ] || [ -f "docs/README.md" ]; then + echo "✅ Documentation index found" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ No documentation index (docs/index.md or docs/README.md)" >> $GITHUB_STEP_SUMMARY + fi + + readme-completeness: + name: README Completeness Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check README Sections + run: | + set -x + echo "## 📄 README Completeness Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ ! -f "README.md" ]; then + echo "❌ README.md not found" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # Required sections + REQUIRED_SECTIONS=("Installation" "Usage" "Contributing" "License") + MISSING=0 + PRESENT=0 + + echo "### Required Sections" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + for section in "${REQUIRED_SECTIONS[@]}"; do + if grep -qi "##.*$section" README.md; then + echo "✅ $section" >> $GITHUB_STEP_SUMMARY + PRESENT=$((PRESENT + 1)) + else + echo "❌ $section" >> $GITHUB_STEP_SUMMARY + MISSING=$((MISSING + 1)) + fi + done + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Completeness**: $PRESENT/${#REQUIRED_SECTIONS[@]} required sections present" >> $GITHUB_STEP_SUMMARY + + if [ "$MISSING" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Action Required**: Add missing sections to README.md" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # ============================================================================ + # PHASE 3: Future Enhancements + # ============================================================================ + + git-hygiene: + name: Git Repository Hygiene + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Check .gitignore + run: | + set -x + echo "### .gitignore Validation" >> $GITHUB_STEP_SUMMARY + + if [ ! -f ".gitignore" ]; then + echo "⚠️ .gitignore file not found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ⚠️ Warning: .gitignore Not Found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Status:** .gitignore file is recommended but not required" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation:** Add .gitignore to exclude build artifacts, dependencies, and temporary files" >> $GITHUB_STEP_SUMMARY + echo "" + echo "⚠️ WARNING: .gitignore file not found - Continuing validation" + exit 0 + fi + + # Check for common exclusions + MISSING="" + grep -q "vendor/" .gitignore || MISSING="${MISSING}vendor/ " + grep -q "node_modules/" .gitignore || MISSING="${MISSING}node_modules/ " + + if [ -n "$MISSING" ]; then + echo "⚠️ .gitignore may be missing common exclusions: $MISSING" >> $GITHUB_STEP_SUMMARY + else + echo "✅ .gitignore appears complete" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check for Large Files + run: | + set -x + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Large File Detection" >> $GITHUB_STEP_SUMMARY + + # Find files larger than 1MB + LARGE_FILES=$(find . -type f -size +1M ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" | head -5) + + if [ -n "$LARGE_FILES" ]; then + echo "⚠️ Large files detected (>1MB):" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$LARGE_FILES" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "Consider using Git LFS for large binary files" >> $GITHUB_STEP_SUMMARY + else + echo "✅ No unusually large files detected" >> $GITHUB_STEP_SUMMARY + fi + + script-integrity: + name: Script Integrity Validation + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.x' + + - name: Validate Script Integrity + id: script_check + run: | + set -x + echo "## 🔐 Script Integrity Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ -f "api/.script-registry.json" ]; then + echo "### Critical Scripts" >> $GITHUB_STEP_SUMMARY + php api/maintenance/update_sha_hashes.php \ + --dry-run --verbose | tee /tmp/script-validation.log + + EXIT_CODE=$? + + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/script-validation.log >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + + if [ "$EXIT_CODE" -eq 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ All critical scripts validated successfully!" >> $GITHUB_STEP_SUMMARY + exit 0 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "❌ Script integrity violations detected" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Review validation report and update registry" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + else + echo "ℹ️ Script registry not found - skipping integrity check" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + + # ════════════════════════════════════════════════════════════════════════ + # TIER 3 — QUALITY (code quality metrics) + # ════════════════════════════════════════════════════════════════════════ + line-length-validation: + name: Line Length Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Line Lengths + run: | + set -x + echo "## 📏 Line Length Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Line length standards: + # - General source code: 120 characters (hard limit) + # - YAML workflows: 180 characters (exception for GitHub Actions) + # - Markdown files: No limit (content-focused) + + echo "### Line Length Standards" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| File Type | Soft Limit | Hard Limit |" >> $GITHUB_STEP_SUMMARY + echo "|-----------|------------|------------|" >> $GITHUB_STEP_SUMMARY + echo "| General source code | 80 chars | 120 chars |" >> $GITHUB_STEP_SUMMARY + echo "| YAML workflows | 80 chars | 180 chars |" >> $GITHUB_STEP_SUMMARY + echo "| Markdown files | N/A | No limit |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Check YAML files (using yamllint which is already configured) + echo "### YAML Files (180 char limit)" >> $GITHUB_STEP_SUMMARY + + YAML_VIOLATIONS=0 + if command -v yamllint >/dev/null 2>&1; then + # Install yamllint if not present + : + else + pip install yamllint >/dev/null 2>&1 || true + fi + + # Run yamllint and count line-length warnings + YAML_OUTPUT=$(yamllint .mokogit/workflows/*.yml 2>&1 | grep "line too long" || true) + if [ -n "$YAML_OUTPUT" ]; then + YAML_VIOLATIONS=$(echo "$YAML_OUTPUT" | wc -l) + echo "⚠️ Found $YAML_VIOLATIONS lines exceeding 180 characters in YAML files" >> $GITHUB_STEP_SUMMARY + echo "
View warnings (informational only)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$YAML_OUTPUT" | head -20 >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "✅ All YAML files comply with 180 character limit" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # Check source code files (PHP, Python, JavaScript, etc.) for 120 char limit + echo "### Source Code Files (120 char limit)" >> $GITHUB_STEP_SUMMARY + + LONG_LINES=$(find . -type f \ + \( -name "*.php" -o -name "*.py" -o -name "*.js" -o -name "*.ts" \ + -o -name "*.go" -o -name "*.rs" -o -name "*.java" -o -name "*.c" \ + -o -name "*.cpp" -o -name "*.h" -o -name "*.sh" \) \ + ! -path "./vendor/*" \ + ! -path "./node_modules/*" \ + ! -path "./.git/*" \ + ! -path "./build/*" \ + ! -path "./dist/*" \ + -exec awk 'length > 120 { print FILENAME ":" NR ": " length " chars" }' {} \; 2>/dev/null | head -20 || true) + + if [ -n "$LONG_LINES" ]; then + LINE_COUNT=$(echo "$LONG_LINES" | wc -l) + echo "⚠️ Found $LINE_COUNT source code lines exceeding 120 characters" >> $GITHUB_STEP_SUMMARY + echo "
View violations (informational)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$LONG_LINES" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "✅ All source code files comply with 120 character limit" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # Confirm Markdown files are not checked + echo "### Markdown Files" >> $GITHUB_STEP_SUMMARY + echo "✅ Markdown files have no line length limit per coding standards" >> $GITHUB_STEP_SUMMARY + echo "Rationale: Content-focused format, URLs, tables, and natural prose flow" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Summary + echo "### Summary" >> $GITHUB_STEP_SUMMARY + echo "This check is **informational only** and does not block merges." >> $GITHUB_STEP_SUMMARY + echo "Line length standards help maintain code readability." >> $GITHUB_STEP_SUMMARY + echo "Exceptions documented in: \`docs/policy/coding-style-guide.md\`" >> $GITHUB_STEP_SUMMARY + + file-naming-standards: + name: File Naming Standards + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check File Naming + run: | + set -x + echo "## 📝 File Naming Standards" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + VIOLATIONS=0 + + # Check PHP files (should be PascalCase for classes) + INVALID_PHP=$(find . -name "*.php" ! -path "./vendor/*" ! -path "./.git/*" ! -regex ".*/[A-Z][a-zA-Z0-9]*\.php" ! -name "index.php" ! -name "functions.php" | wc -l || echo 0) + + # Check config files (should be kebab-case) + INVALID_CONFIG=$(find . -name "*.yml" -o -name "*.yaml" -o -name "*.json" ! -path "./vendor/*" ! -path "./.git/*" ! -path "./node_modules/*" | grep -E "[A-Z_]" | wc -l || echo 0) + + echo "### Naming Violations" >> $GITHUB_STEP_SUMMARY + echo "- **PHP files not PascalCase**: $INVALID_PHP" >> $GITHUB_STEP_SUMMARY + echo "- **Config files not kebab-case**: $INVALID_CONFIG" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + VIOLATIONS=$((INVALID_PHP + INVALID_CONFIG)) + + if [ "$VIOLATIONS" -gt 0 ]; then + echo "⚠️ Found $VIOLATIONS naming convention violation(s)" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Follow naming conventions for consistency" >> $GITHUB_STEP_SUMMARY + else + echo "✅ File naming conventions followed" >> $GITHUB_STEP_SUMMARY + fi + + insecure-patterns: + name: Insecure Code Pattern Detection + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Scan for Insecure Patterns + run: | + set -x + echo "## 🔒 Insecure Code Pattern Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + VIOLATIONS=0 + + # PHP: SQL injection patterns + if grep -r -n "\\$_\(GET\|POST\|REQUEST\).*mysql_query\|mysqli_query" . --include="*.php" ! -path "./vendor/*" 2>/dev/null > /tmp/sql_inject.txt; then + COUNT=$(wc -l < /tmp/sql_inject.txt) + echo "⚠️ Found $COUNT potential SQL injection pattern(s)" >> $GITHUB_STEP_SUMMARY + VIOLATIONS=$((VIOLATIONS + COUNT)) + fi + + # PHP: eval/exec usage + if grep -r -n "eval\|exec\|system\|passthru\|shell_exec" . --include="*.php" ! -path "./vendor/*" 2>/dev/null > /tmp/exec.txt; then + COUNT=$(wc -l < /tmp/exec.txt) + echo "⚠️ Found $COUNT dangerous function call(s)" >> $GITHUB_STEP_SUMMARY + VIOLATIONS=$((VIOLATIONS + COUNT)) + fi + + # Python: eval usage + if grep -r -n "eval(" . --include="*.py" 2>/dev/null > /tmp/py_eval.txt; then + COUNT=$(wc -l < /tmp/py_eval.txt) + echo "⚠️ Found $COUNT Python eval() usage(s)" >> $GITHUB_STEP_SUMMARY + VIOLATIONS=$((VIOLATIONS + COUNT)) + fi + + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$VIOLATIONS" -gt 0 ]; then + echo "**Total Violations**: $VIOLATIONS" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Review and secure flagged patterns" >> $GITHUB_STEP_SUMMARY + else + echo "✅ No insecure patterns detected" >> $GITHUB_STEP_SUMMARY + fi + + code-complexity: + name: Code Complexity Analysis + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + + - name: Analyze Complexity + run: | + set -x + echo "## 📊 Code Complexity Analysis" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + PHP_COUNT=$(find . -name "*.php" ! -path "./vendor/*" ! -path "./.git/*" | wc -l) + + if [ "$PHP_COUNT" -gt 0 ]; then + # Install phploc + wget https://phar.phpunit.de/phploc.phar 2>/dev/null + chmod +x phploc.phar + + echo "### PHP Code Metrics" >> $GITHUB_STEP_SUMMARY + if ./phploc.phar --exclude vendor --exclude .git . 2>&1 | tee /tmp/phploc.txt; then + COMPLEXITY=$(grep "Cyclomatic Complexity" /tmp/phploc.txt | grep "Average" | awk '{print $NF}' || echo "N/A") + echo "**Average Cyclomatic Complexity**: $COMPLEXITY" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$COMPLEXITY" != "N/A" ] && [ $(echo "$COMPLEXITY > 10" | bc -l) -eq 1 ]; then + echo "⚠️ Average complexity exceeds recommended threshold (10)" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Refactor complex functions" >> $GITHUB_STEP_SUMMARY + else + echo "✅ Code complexity within acceptable limits" >> $GITHUB_STEP_SUMMARY + fi + fi + else + echo "ℹ️ No PHP files found for complexity analysis" >> $GITHUB_STEP_SUMMARY + fi + + code-duplication: + name: Code Duplication Detection + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + + - name: Detect Duplicates + run: | + set -x + echo "## 🔁 Code Duplication Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Check if PHP files exist + PHP_COUNT=$(find . -name "*.php" ! -path "./vendor/*" ! -path "./.git/*" | wc -l) + + if [ "$PHP_COUNT" -gt 0 ]; then + echo "### PHP Code Duplication" >> $GITHUB_STEP_SUMMARY + + # Install phpcpd + wget https://phar.phpunit.de/phpcpd.phar 2>/dev/null + chmod +x phpcpd.phar + + # Run duplication detection + if ./phpcpd.phar --exclude vendor --exclude .git . 2>&1 | tee /tmp/phpcpd.txt; then + DUPLICATION=$(grep "Found" /tmp/phpcpd.txt | grep -oE "[0-9]+\.[0-9]+%" | head -1 || echo "0.00%") + echo "📊 **Duplication Rate**: $DUPLICATION" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + DUPLICATION_NUM=$(echo "$DUPLICATION" | sed 's/%//') + if [ $(echo "$DUPLICATION_NUM > 5.0" | bc -l) -eq 1 ]; then + echo "⚠️ Code duplication exceeds 5% threshold" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View duplication details" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/phpcpd.txt >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "✅ Code duplication within acceptable limits (<5%)" >> $GITHUB_STEP_SUMMARY + fi + else + echo "✅ No significant code duplication detected" >> $GITHUB_STEP_SUMMARY + fi + else + echo "ℹ️ No PHP files found for duplication analysis" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note**: This is an informational check to encourage DRY principles." >> $GITHUB_STEP_SUMMARY + + dead-code-detection: + name: Dead Code Detection + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.x' + + - name: Detect Dead Code + run: | + set -x + echo "## 🗑️ Dead Code Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + PY_COUNT=$(find . -name "*.py" ! -path "./vendor/*" ! -path "./.git/*" ! -path "./venv/*" | wc -l) + + if [ "$PY_COUNT" -gt 0 ]; then + pip install vulture 2>/dev/null + echo "### Python Dead Code" >> $GITHUB_STEP_SUMMARY + + if vulture . --exclude vendor,venv,.git 2>&1 | tee /tmp/vulture.txt; then + DEAD_COUNT=$(wc -l < /tmp/vulture.txt || echo 0) + if [ "$DEAD_COUNT" -gt 0 ]; then + echo "⚠️ Found $DEAD_COUNT potential dead code item(s)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View dead code" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + head -50 /tmp/vulture.txt >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "✅ No dead code detected" >> $GITHUB_STEP_SUMMARY + fi + fi + else + echo "ℹ️ No Python files found for dead code analysis" >> $GITHUB_STEP_SUMMARY + fi + + + # ════════════════════════════════════════════════════════════════════════ + # TIER 4 — SUPPLEMENTARY (informational) + # ════════════════════════════════════════════════════════════════════════ + file-size-limits: + name: File Size Limits + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check File Sizes + run: | + set -x + echo "## 📦 File Size Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Exempt file types (allowed to be large) + EXEMPT="! -name *.mmdb ! -name *.woff2 ! -name *.woff ! -name *.ttf ! -name *.otf" + + # Find large files (>15MB warning, >20MB critical) + LARGE_FILES=$(find . -type f -size +15M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" 2>/dev/null | wc -l) + HUGE_FILES=$(find . -type f -size +20M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" 2>/dev/null | wc -l) + + echo "### Size Thresholds" >> $GITHUB_STEP_SUMMARY + echo "- **Warning**: Files >15MB" >> $GITHUB_STEP_SUMMARY + echo "- **Critical**: Files >20MB" >> $GITHUB_STEP_SUMMARY + echo "- **Exempt**: .mmdb, .woff2, .woff, .ttf, .otf" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$HUGE_FILES" -gt 0 ]; then + echo "❌ **Critical**: Found $HUGE_FILES file(s) exceeding 20MB" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View files >20MB" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + find . -type f -size +20M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec ls -lh {} + 2>/dev/null | awk '{print $5, $9}' >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Action Required**: Remove or optimize files >20MB" >> $GITHUB_STEP_SUMMARY + exit 1 + elif [ "$LARGE_FILES" -gt 0 ]; then + echo "⚠️ **Warning**: Found $LARGE_FILES file(s) between 15MB and 20MB" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View files >15MB" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + find . -type f -size +15M $EXEMPT ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec ls -lh {} + 2>/dev/null | awk '{print $5, $9}' >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Consider optimizing large files" >> $GITHUB_STEP_SUMMARY + else + echo "✅ All files within acceptable size limits" >> $GITHUB_STEP_SUMMARY + fi + + binary-file-detection: + name: Binary File Detection + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Detect Binary Files + run: | + set -x + echo "## 🔍 Binary File Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Find binary files excluding allowed types + BINARIES=$(find . -type f ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" \ + ! -name "*.png" ! -name "*.jpg" ! -name "*.jpeg" ! -name "*.gif" ! -name "*.svg" ! -name "*.ico" \ + ! -name "*.woff" ! -name "*.woff2" ! -name "*.ttf" ! -name "*.eot" \ + -exec file {} \; | grep -v "text" | grep -v "empty" | wc -l || echo 0) + + if [ "$BINARIES" -gt 0 ]; then + echo "⚠️ Found $BINARIES non-image binary file(s)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View binary files" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + find . -type f ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" \ + ! -name "*.png" ! -name "*.jpg" ! -name "*.jpeg" ! -name "*.gif" ! -name "*.svg" ! -name "*.ico" \ + ! -name "*.woff" ! -name "*.woff2" ! -name "*.ttf" ! -name "*.eot" \ + -exec file {} \; | grep -v "text" | grep -v "empty" | cut -d: -f1 >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Source control should primarily contain text files" >> $GITHUB_STEP_SUMMARY + else + echo "✅ No unexpected binary files detected" >> $GITHUB_STEP_SUMMARY + fi + + # ============================================================================ + # PHASE 4: Nice to Have Checks + # ============================================================================ + + todo-fixme-tracking: + name: TODO/FIXME Tracking + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Track Technical Debt + run: | + set -x + echo "## 📝 TODO/FIXME Tracking" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Tracking technical debt markers in source code." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Search for technical debt markers + PATTERNS="TODO|FIXME|HACK|XXX" + EXTENSIONS="*.php *.py *.js *.ts *.go *.rs *.java *.c *.cpp *.h *.hpp *.sh" + + echo "### Technical Debt Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + TOTAL_COUNT=0 + for ext in $EXTENSIONS; do + COUNT=$(find . -type f -name "$ext" ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec grep -n -E "($PATTERNS)" {} + 2>/dev/null | wc -l || echo 0) + TOTAL_COUNT=$((TOTAL_COUNT + COUNT)) + done + + if [ "$TOTAL_COUNT" -gt 0 ]; then + echo "⚠️ Found **$TOTAL_COUNT** technical debt item(s)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View technical debt items" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + for ext in $EXTENSIONS; do + find . -type f -name "$ext" ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*" -exec grep -n -H -E "($PATTERNS)" {} + 2>/dev/null | head -100 || true + done >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "✅ No technical debt markers found" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note**: This is an informational check. Technical debt items don't block compliance." >> $GITHUB_STEP_SUMMARY + + dependency-vulnerabilities: + name: Dependency Vulnerability Scanning + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + + - name: Setup Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.x' + + - name: Scan Dependencies + run: | + set -x + echo "## 🛡️ Dependency Vulnerability Scanning" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + VULNERABILITIES=0 + + # PHP Dependencies + if [ -f "composer.json" ]; then + echo "### PHP Dependencies (composer)" >> $GITHUB_STEP_SUMMARY + if composer audit --no-dev 2>&1 | tee /tmp/php_audit.txt; then + echo "✅ No PHP vulnerabilities detected" >> $GITHUB_STEP_SUMMARY + else + VULN_COUNT=$(grep -c "vulnerability" /tmp/php_audit.txt || echo 0) + echo "⚠️ Found $VULN_COUNT PHP vulnerability/vulnerabilities" >> $GITHUB_STEP_SUMMARY + VULNERABILITIES=$((VULNERABILITIES + VULN_COUNT)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + fi + + # Python Dependencies + if [ -f "requirements.txt" ]; then + echo "### Python Dependencies" >> $GITHUB_STEP_SUMMARY + pip install pip-audit 2>&1 > /dev/null + if pip-audit -r requirements.txt 2>&1 | tee /tmp/py_audit.txt; then + echo "✅ No Python vulnerabilities detected" >> $GITHUB_STEP_SUMMARY + else + VULN_COUNT=$(grep -c "vulnerability" /tmp/py_audit.txt || echo 0) + echo "⚠️ Found $VULN_COUNT Python vulnerability/vulnerabilities" >> $GITHUB_STEP_SUMMARY + VULNERABILITIES=$((VULNERABILITIES + VULN_COUNT)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + fi + + # NPM Dependencies + if [ -f "package.json" ]; then + echo "### NPM Dependencies" >> $GITHUB_STEP_SUMMARY + if npm audit --production 2>&1 | tee /tmp/npm_audit.txt; then + echo "✅ No NPM vulnerabilities detected" >> $GITHUB_STEP_SUMMARY + else + VULN_COUNT=$(grep -c "vulnerability" /tmp/npm_audit.txt || echo 0) + echo "⚠️ Found $VULN_COUNT NPM vulnerability/vulnerabilities" >> $GITHUB_STEP_SUMMARY + VULNERABILITIES=$((VULNERABILITIES + VULN_COUNT)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + fi + + if [ "$VULNERABILITIES" -gt 0 ]; then + echo "**Total Vulnerabilities**: $VULNERABILITIES" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Action Required**: Update vulnerable dependencies" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "✅ No dependency vulnerabilities detected" >> $GITHUB_STEP_SUMMARY + fi + + unused-dependencies: + name: Unused Dependencies Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + + - name: Check Unused Dependencies + run: | + set -x + echo "## 📦 Unused Dependencies Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ -f "composer.json" ]; then + echo "### PHP Dependencies" >> $GITHUB_STEP_SUMMARY + + # Install composer-unused + composer global require icanhazstring/composer-unused 2>/dev/null || true + + if composer global exec composer-unused 2>&1 | tee /tmp/unused.txt; then + UNUSED_COUNT=$(grep "unused" /tmp/unused.txt | wc -l || echo 0) + if [ "$UNUSED_COUNT" -gt 0 ]; then + echo "⚠️ Found $UNUSED_COUNT unused dependency/dependencies" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View unused dependencies" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/unused.txt >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + else + echo "✅ No unused dependencies detected" >> $GITHUB_STEP_SUMMARY + fi + else + echo "✅ All dependencies appear to be in use" >> $GITHUB_STEP_SUMMARY + fi + else + echo "ℹ️ No composer.json found" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Remove unused dependencies to reduce attack surface" >> $GITHUB_STEP_SUMMARY + + broken-link-detection: + name: Broken Link Detection + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Internal Links + run: | + set -x + echo "## 🔗 Broken Link Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Checking internal links in markdown files." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + BROKEN_LINKS=0 + CHECKED_LINKS=0 + + # Find all markdown files + MD_FILES=$(find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" ! -path "./node_modules/*") + + for file in $MD_FILES; do + # Extract markdown links [text](path) + while IFS= read -r line; do + # Extract path from [text](path) + link=$(echo "$line" | sed -n 's/.*\](\([^)]*\)).*/\1/p') + + # Skip external links (http/https) + if echo "$link" | grep -qE "^https?://"; then + continue + fi + + # Skip anchors only + if echo "$link" | grep -qE "^#"; then + continue + fi + + CHECKED_LINKS=$((CHECKED_LINKS + 1)) + + # Get directory of the markdown file + basedir=$(dirname "$file") + + # Resolve relative path + if [ -n "$link" ]; then + # Remove anchor if present + clean_link=$(echo "$link" | sed 's/#.*//') + + # Check if file exists + if [ ! -e "$basedir/$clean_link" ] && [ ! -e "$clean_link" ]; then + echo "Broken link in $file: $link" >> /tmp/broken_links.txt + BROKEN_LINKS=$((BROKEN_LINKS + 1)) + fi + fi + done < <(grep -o '\[.*\](.*)' "$file" 2>/dev/null || true) + done + + echo "### Link Validation Results" >> $GITHUB_STEP_SUMMARY + echo "- **Links Checked**: $CHECKED_LINKS" >> $GITHUB_STEP_SUMMARY + echo "- **Broken Links**: $BROKEN_LINKS" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$BROKEN_LINKS" -gt 0 ]; then + echo "⚠️ Found $BROKEN_LINKS broken internal link(s)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "View broken links" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/broken_links.txt 2>/dev/null >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "
" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Fix or remove broken links to maintain documentation quality" >> $GITHUB_STEP_SUMMARY + else + if [ "$CHECKED_LINKS" -gt 0 ]; then + echo "✅ All internal links are valid" >> $GITHUB_STEP_SUMMARY + else + echo "ℹ️ No internal links found to check" >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note**: This check validates internal file references only. External URLs are not validated." >> $GITHUB_STEP_SUMMARY + + # ============================================================================ + # PHASE 2: Medium Priority Checks + # ============================================================================ + + api-documentation: + name: API Documentation Coverage + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Documentation + run: | + set -x + echo "## 📚 API Documentation Coverage" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Count public functions/classes + PUBLIC_METHODS=$(grep -r "public function" . --include="*.php" ! -path "./vendor/*" | wc -l || echo 0) + DOCUMENTED=$(grep -B5 -r "public function" . --include="*.php" ! -path "./vendor/*" | grep -c "/\*\*" || echo 0) + + if [ "$PUBLIC_METHODS" -gt 0 ]; then + COVERAGE=$((DOCUMENTED * 100 / PUBLIC_METHODS)) + echo "**Documentation Coverage**: $COVERAGE% ($DOCUMENTED/$PUBLIC_METHODS)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$COVERAGE" -lt 80 ]; then + echo "⚠️ Documentation coverage below 80% threshold" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Add PHPDoc blocks to public methods" >> $GITHUB_STEP_SUMMARY + else + echo "✅ Good documentation coverage" >> $GITHUB_STEP_SUMMARY + fi + else + echo "ℹ️ No public methods found for documentation check" >> $GITHUB_STEP_SUMMARY + fi + + accessibility-check: + name: Accessibility Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Accessibility + run: | + set -x + echo "## ♿ Accessibility Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + HTML_COUNT=$(find . -name "*.html" ! -path "./vendor/*" ! -path "./.git/*" ! -path "./node_modules/*" | wc -l || echo 0) + MD_IMG_COUNT=$(find . -name "*.md" ! -path "./vendor/*" ! -path "./.git/*" -exec grep -l "!\[" {} + 2>/dev/null | wc -l || echo 0) + + if [ "$HTML_COUNT" -gt 0 ] || [ "$MD_IMG_COUNT" -gt 0 ]; then + # Check for images without alt text + MISSING_ALT=0 + + if [ "$HTML_COUNT" -gt 0 ]; then + MISSING_ALT=$(grep -r "> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$MISSING_ALT" -gt 0 ]; then + echo "⚠️ Found images without alt text" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Add descriptive alt text for accessibility" >> $GITHUB_STEP_SUMMARY + else + echo "✅ All images have alt text" >> $GITHUB_STEP_SUMMARY + fi + else + echo "ℹ️ No HTML files found for accessibility check" >> $GITHUB_STEP_SUMMARY + fi + + performance-metrics: + name: Performance Metrics + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check Performance Metrics + run: | + set -x + echo "## ⚡ Performance Metrics" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Check if JavaScript bundles exist + if [ -f "package.json" ]; then + echo "### Bundle Analysis" >> $GITHUB_STEP_SUMMARY + + # Check for common bundle files + BUNDLE_SIZE=0 + if [ -d "dist" ]; then + BUNDLE_SIZE=$(du -sb dist/ 2>/dev/null | cut -f1 || echo 0) + elif [ -d "build" ]; then + BUNDLE_SIZE=$(du -sb build/ 2>/dev/null | cut -f1 || echo 0) + fi + + if [ "$BUNDLE_SIZE" -gt 0 ]; then + BUNDLE_MB=$((BUNDLE_SIZE / 1024 / 1024)) + echo "**Bundle Size**: ${BUNDLE_MB}MB" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$BUNDLE_MB" -gt 5 ]; then + echo "⚠️ Bundle size exceeds 5MB threshold" >> $GITHUB_STEP_SUMMARY + echo "**Recommendation**: Optimize bundle size" >> $GITHUB_STEP_SUMMARY + else + echo "✅ Bundle size within acceptable limits" >> $GITHUB_STEP_SUMMARY + fi + else + echo "ℹ️ No build artifacts found" >> $GITHUB_STEP_SUMMARY + fi + else + echo "ℹ️ Not a JavaScript project" >> $GITHUB_STEP_SUMMARY + fi + + enterprise-readiness: + name: Enterprise Readiness Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + extensions: json, mbstring + tools: composer + coverage: none + + - name: Install API Package + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader + else + echo "No composer.json — pulling MokoCLI tools" + if [ ! -d "/tmp/mokostandards" ]; then + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/mokocli.git" \ + /tmp/mokostandards 2>/dev/null || true + if [ -f "/tmp/mokostandards/composer.json" ]; then + cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true + cd - + fi + fi + fi + + - name: Check Enterprise Readiness + id: enterprise_check + run: | + echo "" >> $GITHUB_STEP_SUMMARY + + SCRIPT="" + if [ -f "api/validate/check_enterprise_readiness.php" ]; then + SCRIPT="api/validate/check_enterprise_readiness.php" + elif [ -f "/tmp/mokostandards/api/validate/check_enterprise_readiness.php" ]; then + SCRIPT="/tmp/mokostandards/api/validate/check_enterprise_readiness.php" + fi + + if [ -n "$SCRIPT" ]; then + php "$SCRIPT" --verbose | tee /tmp/enterprise-check.log + EXIT_CODE=$? + + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/enterprise-check.log >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + + if [ "$EXIT_CODE" -eq 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ Repository meets enterprise readiness criteria!" >> $GITHUB_STEP_SUMMARY + exit 0 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "⚠️ Enterprise readiness issues detected" >> $GITHUB_STEP_SUMMARY + echo "**Note:** This is informational - review recommendations to improve" >> $GITHUB_STEP_SUMMARY + exit 0 # Non-blocking + fi + else + echo "ℹ️ Enterprise readiness check script not found - skipping" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + repository-health: + name: Repository Health Check + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.1' + extensions: json, mbstring + tools: composer + coverage: none + + - name: Install API Package + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader + else + echo "No composer.json — pulling MokoCLI tools" + if [ ! -d "/tmp/mokostandards" ]; then + git clone --depth 1 --branch version/04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/mokocli.git" \ + /tmp/mokostandards 2>/dev/null || true + if [ -f "/tmp/mokostandards/composer.json" ]; then + cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true + cd - + fi + fi + fi + + - name: Check Repository Health + id: health_check + run: | + echo "" >> $GITHUB_STEP_SUMMARY + + SCRIPT="" + if [ -f "api/validate/check_repo_health.php" ]; then + SCRIPT="api/validate/check_repo_health.php" + elif [ -f "/tmp/mokostandards/api/validate/check_repo_health.php" ]; then + SCRIPT="/tmp/mokostandards/api/validate/check_repo_health.php" + fi + + if [ -n "$SCRIPT" ]; then + php "$SCRIPT" --verbose | tee /tmp/health-check.log + EXIT_CODE=$? + + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/health-check.log >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + + if [ "$EXIT_CODE" -eq 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ Repository health check passed!" >> $GITHUB_STEP_SUMMARY + exit 0 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "⚠️ Repository health issues detected" >> $GITHUB_STEP_SUMMARY + echo "**Note:** This is informational - review recommendations to improve" >> $GITHUB_STEP_SUMMARY + exit 0 # Non-blocking + fi + else + echo "ℹ️ Repository health check script not found - skipping" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + terraform-validation: + name: Terraform Configuration Validation + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0 + with: + terraform_version: "1.0" + + - name: Validate Terraform Files + run: | + set -x + echo "## 🏗️ Terraform Configuration Validation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Check if terraform files exist + TF_COUNT=$(find . -name "*.tf" -type f | wc -l || echo 0) + + if [ "$TF_COUNT" -eq 0 ]; then + echo "ℹ️ No Terraform files found in repository" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + echo "**Terraform Files Found**: $TF_COUNT" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Validation Results + VALIDATION_PASSED=true + WARNINGS=0 + ERRORS=0 + + # 1. Check .mokogit/config.tf location (not root override files) + echo "### Override Configuration Check" >> $GITHUB_STEP_SUMMARY + LEGACY_OVERRIDES=$(find . -maxdepth 1 -name "*override*.tf" -o -name "MokoCLI.override.tf" 2>/dev/null | wc -l || echo 0) + if [ "$LEGACY_OVERRIDES" -gt 0 ]; then + echo "⚠️ Found legacy override files in root directory" >> $GITHUB_STEP_SUMMARY + echo "**Expected Location**: .mokogit/config.tf" >> $GITHUB_STEP_SUMMARY + echo "**Legacy files found**: $LEGACY_OVERRIDES" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + else + if [ -f ".mokogit/config.tf" ]; then + echo "✅ Override configuration in correct location (.mokogit/config.tf)" >> $GITHUB_STEP_SUMMARY + else + echo "ℹ️ No override configuration found" >> $GITHUB_STEP_SUMMARY + fi + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # 2. Terraform Syntax Validation + echo "### Terraform Syntax Validation" >> $GITHUB_STEP_SUMMARY + SYNTAX_ERRORS=0 + + # Find all directories with terraform files + for dir in $(find . -name "*.tf" -type f -exec dirname {} \; | sort -u); do + cd "$dir" || continue + echo "Validating: $dir" >> $GITHUB_STEP_SUMMARY + + # Initialize without backend + terraform init -backend=false > /dev/null 2>&1 || true + + # Validate + if terraform validate -no-color > /tmp/tf_validate.txt 2>&1; then + echo " ✅ Syntax valid" >> $GITHUB_STEP_SUMMARY + else + echo " ❌ Syntax errors found" >> $GITHUB_STEP_SUMMARY + cat /tmp/tf_validate.txt >> $GITHUB_STEP_SUMMARY + SYNTAX_ERRORS=$((SYNTAX_ERRORS + 1)) + VALIDATION_PASSED=false + fi + cd - > /dev/null + done + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$SYNTAX_ERRORS" -eq 0 ]; then + echo "✅ All Terraform files have valid syntax" >> $GITHUB_STEP_SUMMARY + else + echo "❌ Found $SYNTAX_ERRORS directories with syntax errors" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + SYNTAX_ERRORS)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # 3. Terraform Formatting Check + echo "### Terraform Formatting Check" >> $GITHUB_STEP_SUMMARY + FORMAT_ISSUES=0 + + for tf_file in $(find . -name "*.tf" -type f); do + if ! terraform fmt -check=true -no-color "$tf_file" > /dev/null 2>&1; then + FORMAT_ISSUES=$((FORMAT_ISSUES + 1)) + fi + done + + if [ "$FORMAT_ISSUES" -eq 0 ]; then + echo "✅ All Terraform files properly formatted" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Found $FORMAT_ISSUES files with formatting issues" >> $GITHUB_STEP_SUMMARY + echo "**Fix**: Run \`terraform fmt -recursive\`" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # 4. Check for file_metadata blocks + echo "### File Metadata Validation" >> $GITHUB_STEP_SUMMARY + MISSING_METADATA=0 + + for tf_file in $(find . -name "*.tf" -type f); do + if ! grep -q "file_metadata" "$tf_file"; then + MISSING_METADATA=$((MISSING_METADATA + 1)) + fi + done + + if [ "$MISSING_METADATA" -eq 0 ]; then + echo "✅ All Terraform files contain file_metadata block" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Found $MISSING_METADATA files missing file_metadata block" >> $GITHUB_STEP_SUMMARY + echo "**Reference**: docs/policy/terraform-file-standards.md" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # 5. Version Consistency Check + echo "### Version Consistency Check" >> $GITHUB_STEP_SUMMARY + VERSION_MISMATCHES=0 + EXPECTED_VERSION="04.00.04" + + for tf_file in $(find . -name "*.tf" -type f); do + if grep -q "version.*=" "$tf_file"; then + if ! grep -q "version.*=.*\"$EXPECTED_VERSION\"" "$tf_file"; then + VERSION_MISMATCHES=$((VERSION_MISMATCHES + 1)) + fi + fi + done + + if [ "$VERSION_MISMATCHES" -eq 0 ]; then + echo "✅ All Terraform file versions match $EXPECTED_VERSION" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Found $VERSION_MISMATCHES files with version mismatches" >> $GITHUB_STEP_SUMMARY + echo "**Expected Version**: $EXPECTED_VERSION" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # 6. Copyright Header Check + echo "### Copyright Header Check" >> $GITHUB_STEP_SUMMARY + MISSING_COPYRIGHT=0 + + for tf_file in $(find . -name "*.tf" -type f); do + if ! grep -q "Copyright (C)" "$tf_file"; then + MISSING_COPYRIGHT=$((MISSING_COPYRIGHT + 1)) + fi + done + + if [ "$MISSING_COPYRIGHT" -eq 0 ]; then + echo "✅ All Terraform files have copyright headers" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Found $MISSING_COPYRIGHT files missing copyright headers" >> $GITHUB_STEP_SUMMARY + echo "**Reference**: docs/policy/terraform-file-standards.md" >> $GITHUB_STEP_SUMMARY + WARNINGS=$((WARNINGS + 1)) + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # Summary + echo "---" >> $GITHUB_STEP_SUMMARY + echo "### Validation Summary" >> $GITHUB_STEP_SUMMARY + echo "**Total Files**: $TF_COUNT" >> $GITHUB_STEP_SUMMARY + echo "**Errors**: $ERRORS" >> $GITHUB_STEP_SUMMARY + echo "**Warnings**: $WARNINGS" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$VALIDATION_PASSED" = true ] && [ "$ERRORS" -eq 0 ]; then + echo "✅ **Terraform Validation: PASSED**" >> $GITHUB_STEP_SUMMARY + exit 0 + elif [ "$ERRORS" -gt 0 ]; then + echo "❌ **Terraform Validation: FAILED**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note**: This is an informational check and does not block merges" >> $GITHUB_STEP_SUMMARY + exit 0 # Informational only + else + echo "⚠️ **Terraform Validation: PASSED WITH WARNINGS**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note**: This is an informational check and does not block merges" >> $GITHUB_STEP_SUMMARY + exit 0 # Informational only + fi + + summary: + name: Compliance Summary + runs-on: ubuntu-latest + needs: [ + repository-structure, documentation-quality, coding-standards, line-length-validation, license-compliance, git-hygiene, workflow-validation, version-consistency, script-integrity, enterprise-readiness, repository-health, + todo-fixme-tracking, file-size-limits, secret-scanning, broken-link-detection, + dependency-vulnerabilities, code-duplication, unused-dependencies, readme-completeness, + code-complexity, api-documentation, insecure-patterns, binary-file-detection, + dead-code-detection, file-naming-standards, accessibility-check, performance-metrics, terraform-validation + ] + if: always() + + steps: + - name: Generate Compliance Report + run: | + set -x + echo "# 📊 MokoCLI Compliance Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Calculate overall status + REPO_STATUS="${{ needs.repository-structure.result }}" + DOCS_STATUS="${{ needs.documentation-quality.result }}" + CODE_STATUS="${{ needs.coding-standards.result }}" + LINE_LENGTH_STATUS="${{ needs.line-length-validation.result }}" + LICENSE_STATUS="${{ needs.license-compliance.result }}" + GIT_STATUS="${{ needs.git-hygiene.result }}" + WORKFLOW_STATUS="${{ needs.workflow-validation.result }}" + VERSION_STATUS="${{ needs.version-consistency.result }}" + SCRIPT_STATUS="${{ needs.script-integrity.result }}" + ENTERPRISE_STATUS="${{ needs.enterprise-readiness.result }}" + HEALTH_STATUS="${{ needs.repository-health.result }}" + TERRAFORM_STATUS="${{ needs.terraform-validation.result }}" + + PASSED=0 + FAILED=0 + WARNINGS=0 + TOTAL=28 + + # Critical checks (must pass) + [ "$REPO_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$DOCS_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$CODE_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$LICENSE_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$GIT_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$WORKFLOW_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$VERSION_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + [ "$SCRIPT_STATUS" = "success" ] && PASSED=$((PASSED + 1)) || FAILED=$((FAILED + 1)) + + # Informational checks (don't fail build) + if [ "$ENTERPRISE_STATUS" = "success" ]; then + PASSED=$((PASSED + 1)) + else + WARNINGS=$((WARNINGS + 1)) + fi + + if [ "$HEALTH_STATUS" = "success" ]; then + PASSED=$((PASSED + 1)) + else + WARNINGS=$((WARNINGS + 1)) + fi + + if [ "$TERRAFORM_STATUS" = "success" ]; then + PASSED=$((PASSED + 1)) + else + WARNINGS=$((WARNINGS + 1)) + fi + + # Adjust total to only count critical checks for compliance percentage + CRITICAL_TOTAL=8 + CRITICAL_PASSED=$((PASSED - WARNINGS)) + COMPLIANCE_PERCENT=$((CRITICAL_PASSED * 100 / CRITICAL_TOTAL)) + + # Overall status badge + if [ "$COMPLIANCE_PERCENT" -eq 100 ]; then + echo "## ✅ Overall Status: **COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY + elif [ "$COMPLIANCE_PERCENT" -ge 80 ]; then + echo "## ⚠️ Overall Status: **MOSTLY COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY + elif [ "$COMPLIANCE_PERCENT" -ge 50 ]; then + echo "## ⚠️ Overall Status: **PARTIALLY COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY + else + echo "## ❌ Overall Status: **NON-COMPLIANT** ($COMPLIANCE_PERCENT%)" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Critical Checks:** $CRITICAL_PASSED/$CRITICAL_TOTAL passed" >> $GITHUB_STEP_SUMMARY + echo "**Total Checks:** $PASSED/$TOTAL passed" >> $GITHUB_STEP_SUMMARY + if [ "$WARNINGS" -gt 0 ]; then + echo "**Informational:** $WARNINGS warning(s)" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + + # Progress bar + FILLED=$((COMPLIANCE_PERCENT / 5)) + EMPTY=$((20 - FILLED)) + BAR="" + for i in $(seq 1 $FILLED); do BAR="${BAR}█"; done + for i in $(seq 1 $EMPTY); do BAR="${BAR}░"; done + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$BAR $COMPLIANCE_PERCENT%" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Detailed breakdown + echo "## Validation Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Area | Status | Result | Priority |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|--------|----------|" >> $GITHUB_STEP_SUMMARY + + # Repository Structure + if [ "$REPO_STATUS" = "success" ]; then + echo "| 📁 Repository Structure | ✅ Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| 📁 Repository Structure | ❌ Fail | **Action Required** | 🔴 Critical |" >> $GITHUB_STEP_SUMMARY + fi + + # Documentation Quality + if [ "$DOCS_STATUS" = "success" ]; then + echo "| 📚 Documentation Quality | ✅ Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| 📚 Documentation Quality | ❌ Fail | **Action Required** | 🔴 Critical |" >> $GITHUB_STEP_SUMMARY + fi + + # Coding Standards + if [ "$CODE_STATUS" = "success" ]; then + echo "| 💻 Coding Standards | ✅ Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| 💻 Coding Standards | ⚠️ Warning | Review Recommended | 🟡 Medium |" >> $GITHUB_STEP_SUMMARY + fi + + # License Compliance + if [ "$LICENSE_STATUS" = "success" ]; then + echo "| ⚖️ License Compliance | ✅ Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| ⚖️ License Compliance | ❌ Fail | **Action Required** | 🔴 Critical |" >> $GITHUB_STEP_SUMMARY + fi + + # Git Hygiene + if [ "$GIT_STATUS" = "success" ]; then + echo "| 🧹 Git Repository Hygiene | ✅ Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| 🧹 Git Repository Hygiene | ⚠️ Warning | Review Recommended | 🟡 Medium |" >> $GITHUB_STEP_SUMMARY + fi + + # Workflow Configuration + if [ "$WORKFLOW_STATUS" = "success" ]; then + echo "| ⚙️ Workflow Configuration | ✅ Pass | Compliant | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| ⚙️ Workflow Configuration | ⚠️ Warning | Review Recommended | 🟡 Medium |" >> $GITHUB_STEP_SUMMARY + fi + + # Version Consistency + if [ "$VERSION_STATUS" = "success" ]; then + echo "| 🔢 Version Consistency | ✅ Pass | All versions match | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| 🔢 Version Consistency | ❌ Fail | **Action Required** | 🔴 Critical |" >> $GITHUB_STEP_SUMMARY + fi + + # Script Integrity + if [ "$SCRIPT_STATUS" = "success" ]; then + echo "| 🔐 Script Integrity | ✅ Pass | SHA hashes validated | - |" >> $GITHUB_STEP_SUMMARY + else + echo "| 🔐 Script Integrity | ❌ Fail | **Action Required** | 🔴 Critical |" >> $GITHUB_STEP_SUMMARY + fi + + # Enterprise Readiness (Informational) + if [ "$ENTERPRISE_STATUS" = "success" ]; then + echo "| 🏢 Enterprise Readiness | ✅ Pass | Ready for enterprise | ℹ️ Info |" >> $GITHUB_STEP_SUMMARY + else + echo "| 🏢 Enterprise Readiness | ℹ️ Info | Review suggestions | ℹ️ Info |" >> $GITHUB_STEP_SUMMARY + fi + + # Repository Health (Informational) + if [ "$HEALTH_STATUS" = "success" ]; then + echo "| 🏥 Repository Health | ✅ Pass | Health check passed | ℹ️ Info |" >> $GITHUB_STEP_SUMMARY + else + echo "| 🏥 Repository Health | ℹ️ Info | Review recommendations | ℹ️ Info |" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + + # Action items summary + if [ "$FAILED" -gt 0 ]; then + echo "## ⚡ Action Items" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**$FAILED validation area(s) require attention:**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + [ "$REPO_STATUS" != "success" ] && echo "- 🔴 **Critical:** Fix repository structure issues" >> $GITHUB_STEP_SUMMARY + [ "$DOCS_STATUS" != "success" ] && echo "- 🔴 **Critical:** Improve documentation quality" >> $GITHUB_STEP_SUMMARY + [ "$LICENSE_STATUS" != "success" ] && echo "- 🔴 **Critical:** Resolve license compliance issues" >> $GITHUB_STEP_SUMMARY + [ "$CODE_STATUS" != "success" ] && echo "- 🟡 **Medium:** Review coding standards violations" >> $GITHUB_STEP_SUMMARY + [ "$GIT_STATUS" != "success" ] && echo "- 🟡 **Medium:** Address git repository hygiene items" >> $GITHUB_STEP_SUMMARY + [ "$WORKFLOW_STATUS" != "success" ] && echo "- 🟡 **Medium:** Review workflow configuration" >> $GITHUB_STEP_SUMMARY + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Next Steps:**" >> $GITHUB_STEP_SUMMARY + echo "1. Review detailed results in individual job outputs above" >> $GITHUB_STEP_SUMMARY + echo "2. Follow remediation steps provided for each failure" >> $GITHUB_STEP_SUMMARY + echo "3. Re-run this workflow after making corrections" >> $GITHUB_STEP_SUMMARY + echo "4. Reach 100% compliance before merging" >> $GITHUB_STEP_SUMMARY + else + echo "## 🎉 Excellent!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Your repository is **fully compliant** with MokoCLI!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Achievements:**" >> $GITHUB_STEP_SUMMARY + echo "- ✅ All required directories and files present" >> $GITHUB_STEP_SUMMARY + echo "- ✅ Documentation meets quality standards" >> $GITHUB_STEP_SUMMARY + echo "- ✅ Coding standards followed" >> $GITHUB_STEP_SUMMARY + echo "- ✅ License compliance verified" >> $GITHUB_STEP_SUMMARY + echo "- ✅ Git repository well-maintained" >> $GITHUB_STEP_SUMMARY + echo "- ✅ Workflows properly configured" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "---" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "📚 **Resources:**" >> $GITHUB_STEP_SUMMARY + echo "- [MokoCLI Documentation](https://github.com/mokoconsulting-tech/mokocli)" >> $GITHUB_STEP_SUMMARY + echo "- [Repository Structure Guide](https://github.com/mokoconsulting-tech/mokocli/tree/main/docs/policy/core-structure.md)" >> $GITHUB_STEP_SUMMARY + echo "- [Documentation Standards](https://github.com/mokoconsulting-tech/mokocli/tree/main/docs/policy/document-formatting.md)" >> $GITHUB_STEP_SUMMARY + echo "- [Coding Standards](https://github.com/mokoconsulting-tech/mokocli/tree/main/docs/policy/coding-style-guide.md)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "_Generated by MokoCLI Compliance Workflow v${WORKFLOW_VERSION}_" >> $GITHUB_STEP_SUMMARY + + # Create tracking issue for non-compliance if on push + if [ "$COMPLIANCE_PERCENT" -lt 100 ] && [ "${{ github.event_name }}" = "push" ]; then + echo "Creating tracking issue for standards violations..." + fi + + # Exit with error if not fully compliant + if [ "$COMPLIANCE_PERCENT" -lt 100 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ❌ Standards Compliance Failed" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Overall Compliance:** $COMPLIANCE_PERCENT%" >> $GITHUB_STEP_SUMMARY + echo "**Status:** Repository does not meet 100% compliance requirement" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Review and fix all validation failures above" >> $GITHUB_STEP_SUMMARY + echo "" + echo "❌ ERROR: Standards compliance at $COMPLIANCE_PERCENT% - 100% required" + exit 1 + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ✅ Full Standards Compliance Achieved" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Overall Compliance:** 100%" >> $GITHUB_STEP_SUMMARY + echo "**Status:** Repository meets all MokoCLI requirements" >> $GITHUB_STEP_SUMMARY + echo "" + echo "✅ SUCCESS: Repository is fully MokoCLI compliant" + + - name: Create or reopen tracking issue for standards violations + if: failure() + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + REPO="${{ github.repository }}" + RUN_URL="${{ github.server_url }}/${REPO}/actions/runs/${{ github.run_id }}" + DATE=$(date -u '+%Y-%m-%d') + SHA="${{ github.sha }}" + ACTOR="${{ github.actor }}" + BRANCH="${{ github.ref_name }}" + + # Collect failed checks + FAILED="" + [ "${{ needs.repository-structure.result }}" != "success" ] && FAILED="${FAILED}\n- Repository Structure" + [ "${{ needs.documentation-quality.result }}" != "success" ] && FAILED="${FAILED}\n- Documentation Quality" + [ "${{ needs.coding-standards.result }}" != "success" ] && FAILED="${FAILED}\n- Coding Standards" + [ "${{ needs.license-compliance.result }}" != "success" ] && FAILED="${FAILED}\n- License Compliance" + [ "${{ needs.git-hygiene.result }}" != "success" ] && FAILED="${FAILED}\n- Git Hygiene" + [ "${{ needs.workflow-validation.result }}" != "success" ] && FAILED="${FAILED}\n- Workflow Validation" + [ "${{ needs.version-consistency.result }}" != "success" ] && FAILED="${FAILED}\n- Version Consistency" + [ "${{ needs.script-integrity.result }}" != "success" ] && FAILED="${FAILED}\n- Script Integrity" + [ "${{ needs.secret-scanning.result }}" != "success" ] && FAILED="${FAILED}\n- Secret Scanning" + [ "${{ needs.line-length-validation.result }}" != "success" ] && FAILED="${FAILED}\n- Line Length" + [ "${{ needs.file-size-limits.result }}" != "success" ] && FAILED="${FAILED}\n- File Size Limits" + [ "${{ needs.readme-completeness.result }}" != "success" ] && FAILED="${FAILED}\n- README Completeness" + + if [ -z "$FAILED" ]; then + echo "No failed checks to report" + exit 0 + fi + + TITLE="[Standards] Compliance violations — ${DATE}" + BODY="## Standards Compliance Violations + + | Field | Value | + |-------|-------| + | **Branch** | \`${BRANCH}\` | + | **Commit** | \`${SHA:0:7}\` | + | **Actor** | @${ACTOR} | + | **Run** | [View workflow](${RUN_URL}) | + + ### Failed Checks + $(printf '%b' "$FAILED") + + ### Required Actions + 1. Review the [workflow run](${RUN_URL}) for details + 2. Fix each failed check + 3. Push to trigger a new scan + + --- + *Auto-created by standards-compliance workflow*" + + BODY=$(echo "$BODY" | sed 's/^ //') + LABEL="standards-violation" + + gh label create "$LABEL" --repo "$REPO" --color "D73A4A" --description "Standards compliance failure" --force 2>/dev/null || true + + EXISTING=$(gh api "repos/${REPO}/issues?labels=${LABEL}&state=all&per_page=1&sort=created&direction=desc" \ + --jq '.[0].number' 2>/dev/null) + + if [ -n "$EXISTING" ] && [ "$EXISTING" != "null" ]; then + gh api "repos/${REPO}/issues/${EXISTING}" -X PATCH \ + -f title="$TITLE" -f body="$BODY" -f state="open" --silent + echo "Updated issue #${EXISTING}" + else + gh issue create --repo "$REPO" --title "$TITLE" --body "$BODY" \ + --label "$LABEL" --assignee "jmiller" + fi + +# CUSTOMIZATION: +# +# 1. Adjust severity of checks (convert warnings to errors or vice versa) +# 2. Add project-specific validation rules +# 3. Integrate with custom linting tools +# 4. Add notification steps for compliance failures +# 5. Customize required files/directories for your project type + -- 2.52.0 From af6a51553ee3f27ab51abbceb25cb673acdec8bf Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 21:10:07 +0000 Subject: [PATCH 59/63] chore: sync version-set.yml from Template-Joomla [skip ci] --- .mokogit/workflows/version-set.yml | 130 +++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 .mokogit/workflows/version-set.yml diff --git a/.mokogit/workflows/version-set.yml b/.mokogit/workflows/version-set.yml new file mode 100644 index 0000000..73a33f6 --- /dev/null +++ b/.mokogit/workflows/version-set.yml @@ -0,0 +1,130 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGit.Workflow.Template +# INGROUP: MokoCLI.CI +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla +# PATH: /.mokogit/workflows/version-set.yml +# VERSION: 01.00.00 +# BRIEF: Set or reset the extension version across all version-bearing files + +name: "Joomla: Set Version" + +on: + workflow_dispatch: + inputs: + version: + description: "Version number (e.g. 01.00.00)" + required: true + type: string + branch: + description: "Branch to update (default: current)" + required: false + type: string + +permissions: + contents: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + set-version: + name: Set Version to ${{ inputs.version }} + runs-on: ubuntu-latest + + steps: + - name: Validate version format + run: | + VERSION="${{ inputs.version }}" + if ! echo "$VERSION" | grep -qP '^\d{2}\.\d{2}\.\d{2}$'; then + echo "::error::Invalid version format '${VERSION}' — expected XX.YY.ZZ (e.g. 01.00.00)" + exit 1 + fi + echo "VERSION=${VERSION}" >> "$GITHUB_ENV" + + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ secrets.MOKOGIT_TOKEN || github.token }} + ref: ${{ inputs.branch || github.ref }} + fetch-depth: 1 + + - name: Update manifest version + run: | + MANIFEST="" + for XML_FILE in $(find . -maxdepth 3 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "::warning::No Joomla extension manifest found — skipping manifest update" + else + OLD_VER=$(grep -oP '\K[^<]+' "$MANIFEST" | head -1) + sed -i "s|${OLD_VER}|${VERSION}|" "$MANIFEST" + echo "Manifest: ${OLD_VER} → ${VERSION} (${MANIFEST})" + fi + + - name: Update README.md version + run: | + if [ -f "README.md" ]; then + if grep -qP '^\s*VERSION:\s*\d' README.md; then + sed -i -E "s/(VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" README.md + echo "README.md version updated to ${VERSION}" + else + echo "::warning::No VERSION line found in README.md — skipping" + fi + fi + + - name: Update CHANGELOG.md + run: | + if [ -f "CHANGELOG.md" ]; then + DATE=$(date +%Y-%m-%d) + # Check if this version already has an entry + if grep -q "^\#\# \[${VERSION}\]" CHANGELOG.md; then + echo "CHANGELOG.md already has entry for ${VERSION} — skipping" + else + # Insert new version entry after [Unreleased] or at the top after header + if grep -q '^\#\# \[Unreleased\]' CHANGELOG.md; then + sed -i "/^\#\# \[Unreleased\]/a\\\\n## [${VERSION}] --- ${DATE}" CHANGELOG.md + else + sed -i "/^\# Changelog/a\\\\n## [Unreleased]\n\n## [${VERSION}] --- ${DATE}" CHANGELOG.md + fi + echo "CHANGELOG.md: added entry for ${VERSION}" + fi + else + echo "::warning::No CHANGELOG.md found — skipping" + fi + + - name: Update FILE INFORMATION blocks + run: | + # Update VERSION in file header blocks (# VERSION: XX.YY.ZZ) + find . -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.php" -o -name "*.md" \) \ + -not -path "./.git/*" -not -path "./vendor/*" -print0 2>/dev/null | \ + while IFS= read -r -d '' FILE; do + if head -20 "$FILE" | grep -qP '^\s*#?\s*VERSION:\s*\d{2}\.\d{2}\.\d{2}'; then + sed -i -E "s/(#?\s*VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" "$FILE" + echo "Updated FILE INFORMATION VERSION in ${FILE}" + fi + done + + - name: Commit and push + run: | + git config user.name "Moko Consulting [bot]" + git config user.email "hello@mokoconsulting.tech" + git add -A + if git diff --cached --quiet; then + echo "No version changes detected — nothing to commit" + else + git commit -m "chore: set version to ${VERSION} [skip bump] + +Authored-by: Moko Consulting" + git push + echo "### Version Set" >> $GITHUB_STEP_SUMMARY + echo "Version updated to \`${VERSION}\` on branch \`${GITHUB_REF_NAME}\`" >> $GITHUB_STEP_SUMMARY + fi -- 2.52.0 From 0404202d8d97a63fd160e59c82df0fed10581915 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 14 Jul 2026 20:21:01 -0500 Subject: [PATCH 60/63] chore(rebrand): gitea -> git (MokoGIT brand, .mokogitea -> .mokogit) Case-sensitive gitea->git: brand MokoGitea->MokoGIT, MOKOGITEA_TOKEN-> MOKOGIT_TOKEN, GITEA_URL/ORG/REPO->GIT_*, gitea-actions[bot]->git-actions, ntfy gitea-*->git-*, .mokogitea/->.mokogit/. Protected: literal .gitea and git.mokoconsulting.tech. Claude-Session: https://claude.ai/code/session_01DQEMmJPe61ya7HDfA6BHP8 --- {.mokogitea => .mokogit/.mokogitea}/CLAUDE.md | 6 +- .../.mokogitea}/ISSUE_TEMPLATE/adr.md | 0 .../.mokogitea}/ISSUE_TEMPLATE/bug_report.md | 0 .../.mokogitea}/ISSUE_TEMPLATE/config.yml | 2 +- .../ISSUE_TEMPLATE/documentation.md | 0 .../ISSUE_TEMPLATE/feature_request.md | 2 +- .../ISSUE_TEMPLATE/joomla_issue.md | 0 .../.mokogitea}/ISSUE_TEMPLATE/question.md | 0 .../.mokogitea}/ISSUE_TEMPLATE/rfc.md | 0 .../.mokogitea}/ISSUE_TEMPLATE/security.md | 2 +- .../.mokogitea}/ISSUE_TEMPLATE/version.md | 0 .../ISSUE_TEMPLATE/waas_site_issue.md | 0 .../.mokogitea}/branch-protection.yml | 18 ++--- .../.mokogitea}/cascade-dev.yml | 0 .../.mokogitea}/cleanup.yml | 8 +-- .../.mokogitea}/deploy-manual.yml | 2 +- .../.mokogitea}/gitleaks.yml | 4 +- .../.mokogitea}/notify.yml | 4 +- .../.mokogitea}/pr-check.yml | 2 +- .../.mokogitea}/pre-release.yml | 38 +++++----- .../.mokogitea}/security-audit.yml | 4 +- .../.mokogitea}/workflows/auto-bump.yml | 14 ++-- .../.mokogitea}/workflows/auto-release.yml | 72 +++++++++---------- .../.mokogitea}/workflows/branch-cleanup.yml | 6 +- .../.mokogitea}/workflows/cascade-dev.yml | 8 +-- .../.mokogitea}/workflows/ci-generic.yml | 4 +- .../workflows/ci-issue-reporter.yml | 18 ++--- .../.mokogitea}/workflows/ci-joomla.yml | 26 +++---- .../.mokogitea}/workflows/cleanup.yml | 24 +++---- .../.mokogitea}/workflows/gitleaks.yml | 4 +- .../.mokogitea}/workflows/issue-branch.yml | 10 +-- .../.mokogitea}/workflows/notify.yml | 6 +- .../.mokogitea}/workflows/pr-check.yml | 18 ++--- .../workflows/pr-metadata-check.yml | 22 +++--- .../.mokogitea}/workflows/pre-release.yml | 46 ++++++------ .../.mokogitea}/workflows/rc-revert.yml | 10 +-- .../.mokogitea}/workflows/repo-health.yml | 18 ++--- .../workflows/standards-compliance.yml | 32 ++++----- .../.mokogitea}/workflows/version-set.yml | 6 +- CONTRIBUTING.md | 4 +- README.md | 6 +- source/language/en-GB/tpl_mokoonyx.sys.ini | 2 +- source/templateDetails.xml | 2 +- wiki/development.md | 6 +- wiki/guides/installation.md | 2 +- wiki/home.md | 2 +- wiki/installation.md | 2 +- wiki/reference/contributing.md | 2 +- wiki/reference/development.md | 6 +- wiki/unnamed.md | 2 +- 50 files changed, 236 insertions(+), 236 deletions(-) rename {.mokogitea => .mokogit/.mokogitea}/CLAUDE.md (92%) rename {.mokogitea => .mokogit/.mokogitea}/ISSUE_TEMPLATE/adr.md (100%) rename {.mokogitea => .mokogit/.mokogitea}/ISSUE_TEMPLATE/bug_report.md (100%) rename {.mokogitea => .mokogit/.mokogitea}/ISSUE_TEMPLATE/config.yml (99%) rename {.mokogitea => .mokogit/.mokogitea}/ISSUE_TEMPLATE/documentation.md (100%) rename {.mokogitea => .mokogit/.mokogitea}/ISSUE_TEMPLATE/feature_request.md (96%) rename {.mokogitea => .mokogit/.mokogitea}/ISSUE_TEMPLATE/joomla_issue.md (100%) rename {.mokogitea => .mokogit/.mokogitea}/ISSUE_TEMPLATE/question.md (100%) rename {.mokogitea => .mokogit/.mokogitea}/ISSUE_TEMPLATE/rfc.md (100%) rename {.mokogitea => .mokogit/.mokogitea}/ISSUE_TEMPLATE/security.md (95%) rename {.mokogitea => .mokogit/.mokogitea}/ISSUE_TEMPLATE/version.md (100%) rename {.mokogitea => .mokogit/.mokogitea}/ISSUE_TEMPLATE/waas_site_issue.md (100%) rename {.mokogitea => .mokogit/.mokogitea}/branch-protection.yml (94%) rename {.mokogitea => .mokogit/.mokogitea}/cascade-dev.yml (100%) rename {.mokogitea => .mokogit/.mokogitea}/cleanup.yml (91%) rename {.mokogitea => .mokogit/.mokogitea}/deploy-manual.yml (99%) rename {.mokogitea => .mokogit/.mokogitea}/gitleaks.yml (97%) rename {.mokogitea => .mokogit/.mokogitea}/notify.yml (96%) rename {.mokogitea => .mokogit/.mokogitea}/pr-check.yml (99%) rename {.mokogitea => .mokogit/.mokogitea}/pre-release.yml (90%) rename {.mokogitea => .mokogit/.mokogitea}/security-audit.yml (96%) rename {.mokogitea => .mokogit/.mokogitea}/workflows/auto-bump.yml (80%) rename {.mokogitea => .mokogit/.mokogitea}/workflows/auto-release.yml (88%) rename {.mokogitea => .mokogit/.mokogitea}/workflows/branch-cleanup.yml (90%) rename {.mokogitea => .mokogit/.mokogitea}/workflows/cascade-dev.yml (92%) rename {.mokogitea => .mokogit/.mokogitea}/workflows/ci-generic.yml (98%) rename {.mokogitea => .mokogit/.mokogitea}/workflows/ci-issue-reporter.yml (75%) rename {.mokogitea => .mokogit/.mokogitea}/workflows/ci-joomla.yml (98%) rename {.mokogitea => .mokogit/.mokogitea}/workflows/cleanup.yml (80%) rename {.mokogitea => .mokogit/.mokogitea}/workflows/gitleaks.yml (97%) rename {.mokogitea => .mokogit/.mokogitea}/workflows/issue-branch.yml (88%) rename {.mokogitea => .mokogit/.mokogitea}/workflows/notify.yml (94%) rename {.mokogitea => .mokogit/.mokogitea}/workflows/pr-check.yml (96%) rename {.mokogitea => .mokogit/.mokogitea}/workflows/pr-metadata-check.yml (78%) rename {.mokogitea => .mokogit/.mokogitea}/workflows/pre-release.yml (86%) rename {.mokogitea => .mokogit/.mokogitea}/workflows/rc-revert.yml (90%) rename {.mokogitea => .mokogit/.mokogitea}/workflows/repo-health.yml (98%) rename {.mokogitea => .mokogit/.mokogitea}/workflows/standards-compliance.yml (98%) rename {.mokogitea => .mokogit/.mokogitea}/workflows/version-set.yml (96%) diff --git a/.mokogitea/CLAUDE.md b/.mokogit/.mokogitea/CLAUDE.md similarity index 92% rename from .mokogitea/CLAUDE.md rename to .mokogit/.mokogitea/CLAUDE.md index 06e44f5..158e13e 100644 --- a/.mokogitea/CLAUDE.md +++ b/.mokogit/.mokogitea/CLAUDE.md @@ -51,10 +51,10 @@ Client repos (`client-clarksvillefurs`, `client-optainfunding`, etc.) install `t - **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js` - **Attribution**: `Authored-by: Moko Consulting` -- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`) +- **Workflow directory**: `.mokogit/` (not `.gitea/` or `.github/`) - **Minification**: handled at build time (CI) and runtime (MokoMinifyHelper) -- **Wiki**: documentation lives in the MokoGitea wiki, not `docs/` files -- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki) +- **Wiki**: documentation lives in the MokoGIT wiki, not `docs/` files +- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/.mokogit/wiki) ## Coding Standards diff --git a/.mokogitea/ISSUE_TEMPLATE/adr.md b/.mokogit/.mokogitea/ISSUE_TEMPLATE/adr.md similarity index 100% rename from .mokogitea/ISSUE_TEMPLATE/adr.md rename to .mokogit/.mokogitea/ISSUE_TEMPLATE/adr.md diff --git a/.mokogitea/ISSUE_TEMPLATE/bug_report.md b/.mokogit/.mokogitea/ISSUE_TEMPLATE/bug_report.md similarity index 100% rename from .mokogitea/ISSUE_TEMPLATE/bug_report.md rename to .mokogit/.mokogitea/ISSUE_TEMPLATE/bug_report.md diff --git a/.mokogitea/ISSUE_TEMPLATE/config.yml b/.mokogit/.mokogitea/ISSUE_TEMPLATE/config.yml similarity index 99% rename from .mokogitea/ISSUE_TEMPLATE/config.yml rename to .mokogit/.mokogitea/ISSUE_TEMPLATE/config.yml index 633177f..1fa45b8 100644 --- a/.mokogitea/ISSUE_TEMPLATE/config.yml +++ b/.mokogit/.mokogitea/ISSUE_TEMPLATE/config.yml @@ -8,7 +8,7 @@ contact_links: url: https://mokoconsulting.tech/ about: Get help or ask questions through our website - name: MokoStandards Documentation - url: https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki + url: https://git.mokoconsulting.tech/MokoConsulting/.mokogit/wiki about: View our coding standards and best practices - name: Report a Security Vulnerability url: https://git.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new diff --git a/.mokogitea/ISSUE_TEMPLATE/documentation.md b/.mokogit/.mokogitea/ISSUE_TEMPLATE/documentation.md similarity index 100% rename from .mokogitea/ISSUE_TEMPLATE/documentation.md rename to .mokogit/.mokogitea/ISSUE_TEMPLATE/documentation.md diff --git a/.mokogitea/ISSUE_TEMPLATE/feature_request.md b/.mokogit/.mokogitea/ISSUE_TEMPLATE/feature_request.md similarity index 96% rename from .mokogitea/ISSUE_TEMPLATE/feature_request.md rename to .mokogit/.mokogitea/ISSUE_TEMPLATE/feature_request.md index 984fcbe..6c1bbad 100644 --- a/.mokogitea/ISSUE_TEMPLATE/feature_request.md +++ b/.mokogit/.mokogitea/ISSUE_TEMPLATE/feature_request.md @@ -37,7 +37,7 @@ If you have ideas about how this could be implemented, share them here: Add any other context, mockups, or screenshots about the feature request here. ## Relevant Standards -Does this relate to any standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki)? +Does this relate to any standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/.mokogit/wiki)? - [ ] Accessibility (WCAG 2.1 AA) - [ ] Localization (en_US/en_GB) - [ ] Security best practices diff --git a/.mokogitea/ISSUE_TEMPLATE/joomla_issue.md b/.mokogit/.mokogitea/ISSUE_TEMPLATE/joomla_issue.md similarity index 100% rename from .mokogitea/ISSUE_TEMPLATE/joomla_issue.md rename to .mokogit/.mokogitea/ISSUE_TEMPLATE/joomla_issue.md diff --git a/.mokogitea/ISSUE_TEMPLATE/question.md b/.mokogit/.mokogitea/ISSUE_TEMPLATE/question.md similarity index 100% rename from .mokogitea/ISSUE_TEMPLATE/question.md rename to .mokogit/.mokogitea/ISSUE_TEMPLATE/question.md diff --git a/.mokogitea/ISSUE_TEMPLATE/rfc.md b/.mokogit/.mokogitea/ISSUE_TEMPLATE/rfc.md similarity index 100% rename from .mokogitea/ISSUE_TEMPLATE/rfc.md rename to .mokogit/.mokogitea/ISSUE_TEMPLATE/rfc.md diff --git a/.mokogitea/ISSUE_TEMPLATE/security.md b/.mokogit/.mokogitea/ISSUE_TEMPLATE/security.md similarity index 95% rename from .mokogitea/ISSUE_TEMPLATE/security.md rename to .mokogit/.mokogitea/ISSUE_TEMPLATE/security.md index 1aa208d..782c01f 100644 --- a/.mokogitea/ISSUE_TEMPLATE/security.md +++ b/.mokogit/.mokogitea/ISSUE_TEMPLATE/security.md @@ -35,7 +35,7 @@ Use this template only for: ## Standards Reference -Does this relate to security standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki)? +Does this relate to security standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/.mokogit/wiki)? - [ ] SPDX license identifiers - [ ] Secret management - [ ] Dependency security diff --git a/.mokogitea/ISSUE_TEMPLATE/version.md b/.mokogit/.mokogitea/ISSUE_TEMPLATE/version.md similarity index 100% rename from .mokogitea/ISSUE_TEMPLATE/version.md rename to .mokogit/.mokogitea/ISSUE_TEMPLATE/version.md diff --git a/.mokogitea/ISSUE_TEMPLATE/waas_site_issue.md b/.mokogit/.mokogitea/ISSUE_TEMPLATE/waas_site_issue.md similarity index 100% rename from .mokogitea/ISSUE_TEMPLATE/waas_site_issue.md rename to .mokogit/.mokogitea/ISSUE_TEMPLATE/waas_site_issue.md diff --git a/.mokogitea/branch-protection.yml b/.mokogit/.mokogitea/branch-protection.yml similarity index 94% rename from .mokogitea/branch-protection.yml rename to .mokogit/.mokogitea/branch-protection.yml index 2dff8b9..4af3651 100644 --- a/.mokogitea/branch-protection.yml +++ b/.mokogit/.mokogitea/branch-protection.yml @@ -1,7 +1,7 @@ # Copyright (C) 2026 Moko Consulting # SPDX-License-Identifier: GPL-3.0-or-later # FILE INFORMATION -# DEFGROUP: Gitea.Workflow +# DEFGROUP: Git.Workflow # INGROUP: moko-platform.Automation # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform # PATH: /.gitea/workflows/branch-protection.yml @@ -42,8 +42,8 @@ on: default: '' env: - GITEA_URL: https://git.mokoconsulting.tech - GITEA_ORG: MokoConsulting + GIT_URL: https://git.mokoconsulting.tech + GIT_ORG: MokoConsulting permissions: contents: read @@ -59,10 +59,10 @@ jobs: env: GA_TOKEN: ${{ secrets.GA_TOKEN }} run: | - API="${GITEA_URL}/api/v1" + API="${GIT_URL}/api/v1" # Platform/standards/infra repos to exclude - EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private MokoStandards moko-platform MokoTesting" + EXCLUDE="git-org-config org-profile git-private .mokogit-private MokoStandards moko-platform MokoTesting" EXCLUDE="$EXCLUDE MokoStandards-Template-Client MokoStandards-Template-Dolibarr MokoStandards-Template-Generic MokoStandards-Template-Joomla MokoDoliProjTemplate" if [ -n "${{ inputs.repos }}" ]; then @@ -75,7 +75,7 @@ jobs: while true; do BATCH=$(curl -sS \ -H "Authorization: token ${GA_TOKEN}" \ - "${API}/orgs/${GITEA_ORG}/repos?page=${PAGE}&limit=50" \ + "${API}/orgs/${GIT_ORG}/repos?page=${PAGE}&limit=50" \ | jq -r '.[].name // empty') [ -z "$BATCH" ] && break REPOS="$REPOS $BATCH" @@ -108,7 +108,7 @@ jobs: GA_TOKEN: ${{ secrets.GA_TOKEN }} DRY_RUN: ${{ inputs.dry_run || 'false' }} run: | - API="${GITEA_URL}/api/v1" + API="${GIT_URL}/api/v1" REPOS="${{ steps.repos.outputs.repos }}" SUCCESS=0 @@ -215,7 +215,7 @@ jobs: curl -sS -o /dev/null -w "" \ -X DELETE \ -H "Authorization: token ${GA_TOKEN}" \ - "${API}/repos/${GITEA_ORG}/${REPO}/branch_protections/${ENCODED_NAME}" 2>/dev/null || true + "${API}/repos/${GIT_ORG}/${REPO}/branch_protections/${ENCODED_NAME}" 2>/dev/null || true # Create rule RESPONSE=$(curl -sS -w "\n%{http_code}" \ @@ -223,7 +223,7 @@ jobs: -H "Authorization: token ${GA_TOKEN}" \ -H "Content-Type: application/json" \ -d "$RULE" \ - "${API}/repos/${GITEA_ORG}/${REPO}/branch_protections") + "${API}/repos/${GIT_ORG}/${REPO}/branch_protections") HTTP=$(echo "$RESPONSE" | tail -1) BODY=$(echo "$RESPONSE" | sed '$d') diff --git a/.mokogitea/cascade-dev.yml b/.mokogit/.mokogitea/cascade-dev.yml similarity index 100% rename from .mokogitea/cascade-dev.yml rename to .mokogit/.mokogitea/cascade-dev.yml diff --git a/.mokogitea/cleanup.yml b/.mokogit/.mokogitea/cleanup.yml similarity index 91% rename from .mokogitea/cleanup.yml rename to .mokogit/.mokogitea/cleanup.yml index 5e9423f..49c4621 100644 --- a/.mokogitea/cleanup.yml +++ b/.mokogit/.mokogitea/cleanup.yml @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: Gitea.Workflow +# DEFGROUP: Git.Workflow # INGROUP: MokoStandards.Maintenance # REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards # PATH: /.gitea/workflows/cleanup.yml @@ -21,7 +21,7 @@ permissions: contents: write env: - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} jobs: cleanup: @@ -40,7 +40,7 @@ jobs: GA_TOKEN: ${{ secrets.GA_TOKEN }} run: | echo "=== Merged Branch Cleanup ===" - API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + API="${GIT_URL}/api/v1/repos/${{ github.repository }}" # List branches via API BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \ @@ -69,7 +69,7 @@ jobs: GA_TOKEN: ${{ secrets.GA_TOKEN }} run: | echo "=== Workflow Run Cleanup ===" - API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + API="${GIT_URL}/api/v1/repos/${{ github.repository }}" CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ) # Get old completed runs diff --git a/.mokogitea/deploy-manual.yml b/.mokogit/.mokogitea/deploy-manual.yml similarity index 99% rename from .mokogitea/deploy-manual.yml rename to .mokogit/.mokogitea/deploy-manual.yml index df4c5bc..2342612 100644 --- a/.mokogitea/deploy-manual.yml +++ b/.mokogit/.mokogitea/deploy-manual.yml @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: Gitea.Workflow +# DEFGROUP: Git.Workflow # INGROUP: MokoStandards.Deploy # REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API # PATH: /templates/workflows/joomla/deploy-manual.yml.template diff --git a/.mokogitea/gitleaks.yml b/.mokogit/.mokogitea/gitleaks.yml similarity index 97% rename from .mokogitea/gitleaks.yml rename to .mokogit/.mokogitea/gitleaks.yml index 6532d76..4b99528 100644 --- a/.mokogitea/gitleaks.yml +++ b/.mokogit/.mokogitea/gitleaks.yml @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: Gitea.Workflow +# DEFGROUP: Git.Workflow # INGROUP: MokoStandards.Security # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API # PATH: /templates/workflows/gitleaks.yml.template @@ -38,7 +38,7 @@ permissions: env: NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} - NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'git-security' }} jobs: gitleaks: diff --git a/.mokogitea/notify.yml b/.mokogit/.mokogitea/notify.yml similarity index 96% rename from .mokogitea/notify.yml rename to .mokogit/.mokogitea/notify.yml index d59ed49..456799b 100644 --- a/.mokogitea/notify.yml +++ b/.mokogit/.mokogitea/notify.yml @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: Gitea.Workflow +# DEFGROUP: Git.Workflow # INGROUP: MokoStandards.Notifications # REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards # PATH: /.gitea/workflows/notify.yml @@ -27,7 +27,7 @@ permissions: env: NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} - NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'git-releases' }} jobs: notify: diff --git a/.mokogitea/pr-check.yml b/.mokogit/.mokogitea/pr-check.yml similarity index 99% rename from .mokogitea/pr-check.yml rename to .mokogit/.mokogitea/pr-check.yml index a993f30..4985ba5 100644 --- a/.mokogitea/pr-check.yml +++ b/.mokogit/.mokogitea/pr-check.yml @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: Gitea.Workflow +# DEFGROUP: Git.Workflow # INGROUP: MokoStandards.CI # REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards # PATH: /.gitea/workflows/pr-check.yml diff --git a/.mokogitea/pre-release.yml b/.mokogit/.mokogitea/pre-release.yml similarity index 90% rename from .mokogitea/pre-release.yml rename to .mokogit/.mokogitea/pre-release.yml index 35e8abb..13e6c93 100644 --- a/.mokogitea/pre-release.yml +++ b/.mokogit/.mokogitea/pre-release.yml @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: Gitea.Workflow +# DEFGROUP: Git.Workflow # INGROUP: MokoStandards.Release # REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards # PATH: /.gitea/workflows/pre-release.yml @@ -29,9 +29,9 @@ permissions: contents: write env: - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + GIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GIT_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GIT_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} jobs: build: @@ -103,8 +103,8 @@ jobs: fi # Commit version bump - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" + git config --local user.email "git-actions[bot]@mokoconsulting.tech" + git config --local user.name "git-actions[bot]" git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" git add -A git diff --cached --quiet || { @@ -120,11 +120,11 @@ jobs: if [ -z "$EXT_ELEMENT" ]; then EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') case "$EXT_ELEMENT" in - templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; + templatedetails|manifest) EXT_ELEMENT=$(echo "${GIT_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; esac fi else - EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + EXT_ELEMENT=$(echo "${GIT_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') fi ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip" @@ -172,7 +172,7 @@ jobs: echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT" echo "ZIP: ${ZIP_NAME} (SHA: ${SHA256:0:16}...)" - - name: Create or replace Gitea release + - name: Create or replace Git release id: release run: | TAG="${{ steps.meta.outputs.tag }}" @@ -182,7 +182,7 @@ jobs: ZIP_NAME="${{ steps.meta.outputs.zip_name }}" EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}" TOKEN="${{ secrets.GA_TOKEN }}" - API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + API="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" BRANCH=$(git branch --show-current) BODY="## ${VERSION} ($(date +%Y-%m-%d)) @@ -237,7 +237,7 @@ jobs: export PY_STABILITY="$STABILITY" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \ PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \ - PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO" + PY_GIT_ORG="$GIT_ORG" PY_GIT_REPO="$GIT_REPO" python3 << 'PYEOF' import re, os @@ -247,9 +247,9 @@ jobs: zip_name = os.environ["PY_ZIP_NAME"] tag = os.environ["PY_TAG"] date = os.environ["PY_DATE"] - gitea_org = os.environ["PY_GITEA_ORG"] - gitea_repo = os.environ["PY_GITEA_REPO"] - download_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}" + git_org = os.environ["PY_GIT_ORG"] + git_repo = os.environ["PY_GIT_REPO"] + download_url = f"https://git.mokoconsulting.tech/{git_org}/{git_repo}/releases/download/{tag}/{zip_name}" with open("updates.xml", "r") as f: content = f.read() @@ -280,8 +280,8 @@ jobs: # Commit and push to current branch if ! git diff --quiet updates.xml 2>/dev/null; then - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" + git config --local user.email "git-actions[bot]@mokoconsulting.tech" + git config --local user.name "git-actions[bot]" git add updates.xml git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" git push origin HEAD 2>&1 || echo "WARNING: push failed" @@ -290,8 +290,8 @@ jobs: - name: "Sync updates.xml to all branches" run: | CURRENT_BRANCH="${{ github.ref_name }}" - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "gitea-actions[bot]" + git config --local user.email "git-actions[bot]@mokoconsulting.tech" + git config --local user.name "git-actions[bot]" # Sync updates.xml to main and dev (whichever isn't current) for BRANCH in main dev; do @@ -312,7 +312,7 @@ jobs: - name: "Delete lesser pre-release channels (cascade)" continue-on-error: true run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" TOKEN="${{ secrets.GA_TOKEN }}" STABILITY="${{ steps.meta.outputs.stability }}" diff --git a/.mokogitea/security-audit.yml b/.mokogit/.mokogitea/security-audit.yml similarity index 96% rename from .mokogitea/security-audit.yml rename to .mokogit/.mokogitea/security-audit.yml index ff6de4c..f038bfd 100644 --- a/.mokogitea/security-audit.yml +++ b/.mokogit/.mokogitea/security-audit.yml @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: Gitea.Workflow +# DEFGROUP: Git.Workflow # INGROUP: MokoStandards.Security # REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards # PATH: /.gitea/workflows/security-audit.yml @@ -30,7 +30,7 @@ permissions: env: NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} - NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'git-security' }} jobs: audit: diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogit/.mokogitea/workflows/auto-bump.yml similarity index 80% rename from .mokogitea/workflows/auto-bump.yml rename to .mokogit/.mokogitea/workflows/auto-bump.yml index a47661a..fba253e 100644 --- a/.mokogitea/workflows/auto-bump.yml +++ b/.mokogit/.mokogitea/workflows/auto-bump.yml @@ -3,10 +3,10 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: MokoGitea.Workflow +# DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.Release # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli -# PATH: /.mokogitea/workflows/auto-bump.yml +# PATH: /.mokogit/workflows/auto-bump.yml # VERSION: 09.02.00 # BRIEF: Auto patch-bump version on every push to dev (skips merge commits) @@ -22,7 +22,7 @@ on: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} permissions: contents: write @@ -40,7 +40,7 @@ jobs: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: - token: ${{ secrets.MOKOGITEA_TOKEN }} + token: ${{ secrets.MOKOGIT_TOKEN }} fetch-depth: 1 - name: Setup MokoCLI tools @@ -52,7 +52,7 @@ jobs: echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV" else git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \ + "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \ /tmp/mokocli cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV" @@ -62,5 +62,5 @@ jobs: run: | php ${MOKO_CLI}/version_auto_bump.php \ --path . --branch "${GITHUB_REF_NAME}" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + --token "${{ secrets.MOKOGIT_TOKEN }}" \ + --repo-url "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogit/.mokogitea/workflows/auto-release.yml similarity index 88% rename from .mokogitea/workflows/auto-release.yml rename to .mokogit/.mokogitea/workflows/auto-release.yml index ce7bc6c..d6438cb 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogit/.mokogitea/workflows/auto-release.yml @@ -3,18 +3,18 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: MokoGitea.Workflow +# DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.Release # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # PATH: /templates/workflows/universal/auto-release.yml.template # VERSION: 05.01.00 -# BRIEF: Universal build & release � detects platform from MokoGitea repo metadata (API) +# BRIEF: Universal build & release � detects platform from MokoGIT repo metadata (API) # # +=======================================================================+ # | UNIVERSAL BUILD & RELEASE PIPELINE | # +=======================================================================+ # | | -# | Reads MokoGitea repo metadata (joomla|dolibarr|generic) to branch logic. | +# | Reads MokoGIT repo metadata (joomla|dolibarr|generic) to branch logic. | # | | # | Platform-specific: | # | joomla: XML manifest, type-prefixed packages | @@ -31,7 +31,7 @@ on: branches: - main paths-ignore: - - '.mokogitea/workflows/**' + - '.mokogit/workflows/**' - '*.md' - 'wiki/**' - '.editorconfig' @@ -52,9 +52,9 @@ on: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GIT_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GIT_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} permissions: contents: write @@ -77,13 +77,13 @@ jobs: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: - token: ${{ secrets.MOKOGITEA_TOKEN }} + token: ${{ secrets.MOKOGIT_TOKEN }} fetch-depth: 1 submodules: recursive - name: Setup MokoCLI tools env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting run: | if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then @@ -106,28 +106,28 @@ jobs: run: | php ${MOKO_CLI}/branch_rename.php \ --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ + --token "${{ secrets.MOKOGIT_TOKEN }}" \ + --api-base "${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" \ --pr "${{ github.event.pull_request.number }}" - name: Checkout rc and configure git run: | git fetch origin rc git checkout rc - git config --local user.email "mokogitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "mokogitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech" + git config --local user.name "mokogit-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - name: Publish RC release run: | php ${MOKO_CLI}/release_publish.php \ --path . --stability rc --bump minor --branch rc \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" + --token "${{ secrets.MOKOGIT_TOKEN }}" - name: Update RC release notes from CHANGELOG.md run: | - API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" + TOKEN="${{ secrets.MOKOGIT_TOKEN }}" # Extract [Unreleased] section from changelog NOTES="" @@ -180,15 +180,15 @@ jobs: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: - token: ${{ secrets.MOKOGITEA_TOKEN }} + token: ${{ secrets.MOKOGIT_TOKEN }} fetch-depth: 0 submodules: recursive - name: Configure git for bot pushes run: | - git config --local user.email "mokogitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "mokogitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech" + git config --local user.name "mokogit-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" - name: Check for merge conflict markers run: | @@ -205,7 +205,7 @@ jobs: - name: Setup MokoCLI tools env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' run: | @@ -252,7 +252,7 @@ jobs: fi php ${MOKO_CLI}/release_publish.php \ --path . --stability stable ${BUMP_FLAG} --branch main \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" + --token "${{ secrets.MOKOGIT_TOKEN }}" - name: "Read published version" id: version @@ -279,8 +279,8 @@ jobs: !startsWith(steps.platform.outputs.platform, 'joomla') run: | VERSION="${{ steps.version.outputs.version }}" - API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" + TOKEN="${{ secrets.MOKOGIT_TOKEN }}" SEMVER_TAG="v${VERSION}" echo "Creating semver tag: ${SEMVER_TAG}" @@ -304,8 +304,8 @@ jobs: - name: Update release notes and promote changelog run: | - API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" + TOKEN="${{ secrets.MOKOGIT_TOKEN }}" # Get the stable release info (version and ID) RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \ @@ -373,10 +373,10 @@ jobs: VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}" GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" php ${MOKO_CLI}/release_mirror.php \ --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "$API_BASE" \ --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ --branch main 2>&1 || true echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY @@ -402,8 +402,8 @@ jobs: if: steps.version.outputs.skip != 'true' continue-on-error: true run: | - API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" + TOKEN="${{ secrets.MOKOGIT_TOKEN }}" # Delete rc branch (ephemeral — created by promote-rc) curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ @@ -418,8 +418,8 @@ jobs: if: steps.version.outputs.skip != 'true' continue-on-error: true run: | - API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" + TOKEN="${{ secrets.MOKOGIT_TOKEN }}" VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" BRANCH_NAME="version/${VERSION}" MAIN_SHA=$(git rev-parse HEAD) @@ -439,9 +439,9 @@ jobs: if: steps.version.outputs.skip != 'true' continue-on-error: true run: | - API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" php ${MOKO_CLI}/version_reset_dev.php \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ + --token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "${API_BASE}" \ --branch dev --path . 2>&1 || true # -- Summary -------------------------------------------------------------- @@ -465,5 +465,5 @@ jobs: echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Release | [View](${MOKOGITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY + echo "| Release | [View](${MOKOGIT_URL}/${GIT_ORG}/${GIT_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY fi diff --git a/.mokogitea/workflows/branch-cleanup.yml b/.mokogit/.mokogitea/workflows/branch-cleanup.yml similarity index 90% rename from .mokogitea/workflows/branch-cleanup.yml rename to .mokogit/.mokogitea/workflows/branch-cleanup.yml index aca4a9d..bc37ded 100644 --- a/.mokogitea/workflows/branch-cleanup.yml +++ b/.mokogit/.mokogitea/workflows/branch-cleanup.yml @@ -3,10 +3,10 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: MokoGitea.Workflow +# DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.Universal # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli -# PATH: /.mokogitea/workflows/branch-cleanup.yml +# PATH: /.mokogit/workflows/branch-cleanup.yml # VERSION: 01.00.00 # BRIEF: Delete feature branches after PR merge @@ -37,7 +37,7 @@ jobs: ENCODED=$(printf '%s' "${BRANCH}" | sed 's|/|%2F|g') STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \ - -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ + -H "Authorization: token ${{ secrets.MOKOGIT_TOKEN }}" \ "${API}/${ENCODED}" 2>/dev/null || true) if [ "$STATUS" = "204" ]; then diff --git a/.mokogitea/workflows/cascade-dev.yml b/.mokogit/.mokogitea/workflows/cascade-dev.yml similarity index 92% rename from .mokogitea/workflows/cascade-dev.yml rename to .mokogit/.mokogitea/workflows/cascade-dev.yml index 1b9f8a6..6c7e26a 100644 --- a/.mokogitea/workflows/cascade-dev.yml +++ b/.mokogit/.mokogitea/workflows/cascade-dev.yml @@ -1,7 +1,7 @@ # Copyright (C) 2026 Moko Consulting # SPDX-License-Identifier: GPL-3.0-or-later # FILE INFORMATION -# DEFGROUP: MokoGitea.Workflow +# DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.Release # BRIEF: Reset dev to main after each release (cascade). Moved out of auto-release Step 11. # @@ -43,14 +43,14 @@ jobs: steps: - name: Force dev to main env: - TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + TOKEN: ${{ secrets.MOKOGIT_TOKEN }} SERVER: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} REPO: ${{ github.repository }} run: | set -euo pipefail git init -q sync && cd sync - git config user.email "mokogitea-actions[bot]@mokoconsulting.tech" - git config user.name "mokogitea-actions[bot]" + git config user.email "mokogit-actions[bot]@mokoconsulting.tech" + git config user.name "mokogit-actions[bot]" git remote add origin "https://x-access-token:${TOKEN}@${SERVER#https://}/${REPO}.git" git fetch -q --depth=1 origin main diff --git a/.mokogitea/workflows/ci-generic.yml b/.mokogit/.mokogitea/workflows/ci-generic.yml similarity index 98% rename from .mokogitea/workflows/ci-generic.yml rename to .mokogit/.mokogitea/workflows/ci-generic.yml index 330e6f9..053109a 100644 --- a/.mokogitea/workflows/ci-generic.yml +++ b/.mokogit/.mokogitea/workflows/ci-generic.yml @@ -3,10 +3,10 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: MokoGitea.Workflow +# DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.CI # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic -# PATH: /.mokogitea/workflows/ci-generic.yml +# PATH: /.mokogit/workflows/ci-generic.yml # VERSION: 01.00.00 # BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js) diff --git a/.mokogitea/workflows/ci-issue-reporter.yml b/.mokogit/.mokogitea/workflows/ci-issue-reporter.yml similarity index 75% rename from .mokogitea/workflows/ci-issue-reporter.yml rename to .mokogit/.mokogitea/workflows/ci-issue-reporter.yml index 335e92f..ede368d 100644 --- a/.mokogitea/workflows/ci-issue-reporter.yml +++ b/.mokogit/.mokogitea/workflows/ci-issue-reporter.yml @@ -3,12 +3,12 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: MokoGitea.Workflow +# DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.Universal # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli -# PATH: /.mokogitea/workflows/ci-issue-reporter.yml +# PATH: /.mokogit/workflows/ci-issue-reporter.yml # VERSION: 01.00.00 -# BRIEF: Reusable workflow — creates/updates a MokoGitea issue when a CI gate fails. +# BRIEF: Reusable workflow — creates/updates a MokoGIT issue when a CI gate fails. # Clones MokoCLI and runs cli/ci_issue_reporter.sh. name: "Universal: CI Issue Reporter" @@ -35,7 +35,7 @@ on: type: string default: "" secrets: - MOKOGITEA_TOKEN: + MOKOGIT_TOKEN: required: true env: @@ -49,16 +49,16 @@ jobs: steps: - name: Clone MokoCLI env: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} run: | - MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}" - git clone --depth 1 --filter=blob:none --sparse "${MOKOGITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli + MOKOGIT_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}" + git clone --depth 1 --filter=blob:none --sparse "${MOKOGIT_URL}/MokoConsulting/mokocli.git" /tmp/mokocli cd /tmp/mokocli && git sparse-checkout set cli/ci_issue_reporter.sh - name: Report CI failure env: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} + MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} run: | chmod +x /tmp/mokocli/cli/ci_issue_reporter.sh /tmp/mokocli/cli/ci_issue_reporter.sh \ diff --git a/.mokogitea/workflows/ci-joomla.yml b/.mokogit/.mokogitea/workflows/ci-joomla.yml similarity index 98% rename from .mokogitea/workflows/ci-joomla.yml rename to .mokogit/.mokogitea/workflows/ci-joomla.yml index e61ed5b..f9a25d9 100644 --- a/.mokogitea/workflows/ci-joomla.yml +++ b/.mokogit/.mokogitea/workflows/ci-joomla.yml @@ -5,7 +5,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: MokoGitea.Workflow.Template +# DEFGROUP: MokoGIT.Workflow.Template # INGROUP: MokoCLI.CI # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # PATH: /templates/workflows/joomla/ci-joomla.yml.template @@ -49,8 +49,8 @@ jobs: - name: Setup MokoCLI tools env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }} - MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN || github.token }} + MOKO_CLONE_HOST: ${{ secrets.MOKOGIT_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} run: | if [ -d "/opt/mokocli" ] || [ -d "/tmp/mokocli" ]; then echo "MokoCLI already available on runner — skipping clone" @@ -62,7 +62,7 @@ jobs: - name: Install dependencies env: - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}"}}' + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.MOKOGIT_TOKEN || github.token }}"}}' run: | if [ -f "composer.json" ]; then composer install \ @@ -186,7 +186,7 @@ jobs: else EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) - # 1. Check exists and uses MokoGitea update server + # 1. Check exists and uses MokoGIT update server if ! grep -q '' "$MANIFEST" 2>/dev/null; then echo "::warning file=${MANIFEST}::Missing \`\` tag — extension will not receive OTA updates" echo "- **Missing** \`\` — extension will not receive OTA updates" >> $GITHUB_STEP_SUMMARY @@ -198,12 +198,12 @@ jobs: echo "- **Empty** \`\` — no server URL defined" >> $GITHUB_STEP_SUMMARY WARNINGS=$((WARNINGS + 1)) elif ! echo "$SERVER_URL" | grep -q 'git\.mokoconsulting\.tech'; then - echo "::warning file=${MANIFEST}::Update server does not use MokoGitea engine: ${SERVER_URL}" - echo "- **Non-MokoGitea update server:** \`${SERVER_URL}\`" >> $GITHUB_STEP_SUMMARY + echo "::warning file=${MANIFEST}::Update server does not use MokoGIT engine: ${SERVER_URL}" + echo "- **Non-MokoGIT update server:** \`${SERVER_URL}\`" >> $GITHUB_STEP_SUMMARY echo " Expected: \`https://git.mokoconsulting.tech/{org}/{repo}/updates.xml\`" >> $GITHUB_STEP_SUMMARY WARNINGS=$((WARNINGS + 1)) else - echo "- \`\`: MokoGitea engine ✓" >> $GITHUB_STEP_SUMMARY + echo "- \`\`: MokoGIT engine ✓" >> $GITHUB_STEP_SUMMARY fi fi @@ -2067,7 +2067,7 @@ jobs: - name: Install dependencies env: - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}"}}' + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.MOKOGIT_TOKEN || github.token }}"}}' run: | if [ -f "composer.json" ]; then composer install \ @@ -2118,7 +2118,7 @@ jobs: - name: Install dependencies env: - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}"}}' + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.MOKOGIT_TOKEN || github.token }}"}}' run: | if [ -f "composer.json" ]; then composer install --no-interaction --prefer-dist --optimize-autoloader @@ -2185,13 +2185,13 @@ jobs: steps: - name: Trigger pre-release build env: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} REPO: ${{ github.repository }} BRANCH: ${{ github.head_ref }} run: | curl -s -X POST \ - "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" \ - -H "Authorization: token ${MOKOGITEA_TOKEN}" \ + "${GIT_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" \ + -H "Authorization: token ${MOKOGIT_TOKEN}" \ -H "Content-Type: application/json" \ -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY diff --git a/.mokogitea/workflows/cleanup.yml b/.mokogit/.mokogitea/workflows/cleanup.yml similarity index 80% rename from .mokogitea/workflows/cleanup.yml rename to .mokogit/.mokogitea/workflows/cleanup.yml index d08e29e..9c13931 100644 --- a/.mokogitea/workflows/cleanup.yml +++ b/.mokogit/.mokogitea/workflows/cleanup.yml @@ -3,10 +3,10 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: MokoGitea.Workflow +# DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.Maintenance # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli -# PATH: /.mokogitea/workflows/cleanup.yml +# PATH: /.mokogit/workflows/cleanup.yml # VERSION: 01.00.00 # BRIEF: Scheduled cleanup — delete merged branches and old workflow runs @@ -21,7 +21,7 @@ permissions: contents: write env: - MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} jobs: cleanup: @@ -33,17 +33,17 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - token: ${{ secrets.MOKOGITEA_TOKEN }} + token: ${{ secrets.MOKOGIT_TOKEN }} - name: Delete merged branches env: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} run: | echo "=== Merged Branch Cleanup ===" - API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}" + API="${MOKOGIT_URL}/api/v1/repos/${{ github.repository }}" # List branches via API - BRANCHES=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \ + BRANCHES=$(curl -sS -H "Authorization: token ${MOKOGIT_TOKEN}" \ "${API}/branches?limit=50" | jq -r '.[].name') DELETED=0 @@ -56,7 +56,7 @@ jobs: # Check if branch is merged into main if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then echo " Deleting merged branch: ${BRANCH}" - curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \ + curl -sS -X DELETE -H "Authorization: token ${MOKOGIT_TOKEN}" \ "${API}/branches/${BRANCH}" 2>/dev/null || true DELETED=$((DELETED + 1)) fi @@ -66,20 +66,20 @@ jobs: - name: Clean old workflow runs env: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} run: | echo "=== Workflow Run Cleanup ===" - API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}" + API="${MOKOGIT_URL}/api/v1/repos/${{ github.repository }}" CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ) # Get old completed runs - RUNS=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \ + RUNS=$(curl -sS -H "Authorization: token ${MOKOGIT_TOKEN}" \ "${API}/actions/runs?status=completed&limit=50" | \ jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null) DELETED=0 for RUN_ID in $RUNS; do - curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \ + curl -sS -X DELETE -H "Authorization: token ${MOKOGIT_TOKEN}" \ "${API}/actions/runs/${RUN_ID}" 2>/dev/null || true DELETED=$((DELETED + 1)) done diff --git a/.mokogitea/workflows/gitleaks.yml b/.mokogit/.mokogitea/workflows/gitleaks.yml similarity index 97% rename from .mokogitea/workflows/gitleaks.yml rename to .mokogit/.mokogitea/workflows/gitleaks.yml index 23b843e..cea7c9e 100644 --- a/.mokogitea/workflows/gitleaks.yml +++ b/.mokogit/.mokogitea/workflows/gitleaks.yml @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: MokoGitea.Workflow +# DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.Security # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # PATH: /templates/workflows/gitleaks.yml.template @@ -34,7 +34,7 @@ permissions: env: NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} - NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'git-security' }} jobs: gitleaks: diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogit/.mokogitea/workflows/issue-branch.yml similarity index 88% rename from .mokogitea/workflows/issue-branch.yml rename to .mokogit/.mokogitea/workflows/issue-branch.yml index 12a4b11..da40381 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogit/.mokogitea/workflows/issue-branch.yml @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: MokoGitea.Workflow +# DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.Automation # VERSION: 01.00.00 # BRIEF: Auto-create feature branch when an issue is opened @@ -19,7 +19,7 @@ permissions: issues: write env: - MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} jobs: create-branch: @@ -28,8 +28,8 @@ jobs: steps: - name: Create branch and comment run: | - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}" + TOKEN="${{ secrets.MOKOGIT_TOKEN }}" + API="${MOKOGIT_URL}/api/v1/repos/${{ github.repository }}" ISSUE_NUM="${{ github.event.issue.number }}" ISSUE_TITLE="${{ github.event.issue.title }}" @@ -58,7 +58,7 @@ jobs: echo "Created branch: ${BRANCH}" # Comment on issue with branch link - REPO_URL="${MOKOGITEA_URL}/${{ github.repository }}" + REPO_URL="${MOKOGIT_URL}/${{ github.repository }}" BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`" curl -sf -X POST \ diff --git a/.mokogitea/workflows/notify.yml b/.mokogit/.mokogitea/workflows/notify.yml similarity index 94% rename from .mokogitea/workflows/notify.yml rename to .mokogit/.mokogitea/workflows/notify.yml index 6d029c2..f51ad68 100644 --- a/.mokogitea/workflows/notify.yml +++ b/.mokogit/.mokogitea/workflows/notify.yml @@ -3,10 +3,10 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: MokoGitea.Workflow +# DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.Notifications # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli -# PATH: /.mokogitea/workflows/notify.yml +# PATH: /.mokogit/workflows/notify.yml # VERSION: 01.00.00 # BRIEF: Push notifications via ntfy on release success or workflow failure @@ -26,7 +26,7 @@ permissions: env: NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} - NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'git-releases' }} jobs: notify: diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogit/.mokogitea/workflows/pr-check.yml similarity index 96% rename from .mokogitea/workflows/pr-check.yml rename to .mokogit/.mokogitea/workflows/pr-check.yml index 2cd896c..1c75f14 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogit/.mokogitea/workflows/pr-check.yml @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: MokoGitea.Workflow +# DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.CI # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # PATH: /templates/workflows/universal/pr-check.yml.template @@ -149,7 +149,7 @@ jobs: - name: Detect platform id: platform run: | - # Platform comes from the MokoGitea metadata API (public GET); manifest.xml is no longer used. + # Platform comes from the MokoGIT metadata API (public GET); manifest.xml is no longer used. API="${GITHUB_SERVER_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${GITHUB_REPOSITORY}/metadata" PLATFORM="$(curl -sf "$API" 2>/dev/null | python3 -c "import sys, json; print(json.load(sys.stdin).get('platform') or '')" 2>/dev/null || true)" [ -z "$PLATFORM" ] && PLATFORM="generic" @@ -285,10 +285,10 @@ jobs: for ELEMENT in name version description; do grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; } done - # Block legacy raw/branch update server URLs on MokoGitea - RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true) + # Block legacy raw/branch update server URLs on MokoGIT + RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogit\|git\.mokoconsulting\.tech' || true) if [ -n "$RAW_URLS" ]; then - echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the MokoGitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)" + echo "::error::Manifest contains legacy raw/branch update server URL on MokoGIT. Use the MokoGIT Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)" echo "$RAW_URLS" exit 1 fi @@ -502,12 +502,12 @@ jobs: steps: - name: Trigger RC pre-release env: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} REPO: ${{ github.repository }} BRANCH: ${{ github.head_ref }} - MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} run: | - curl -s -X POST "${MOKOGITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${MOKOGITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" + curl -s -X POST "${MOKOGIT_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${MOKOGIT_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY @@ -518,7 +518,7 @@ jobs: if: >- always() && needs.validate.result == 'failure' - uses: ./.mokogitea/workflows/ci-issue-reporter.yml + uses: ./.mokogit/workflows/ci-issue-reporter.yml with: gate: "PR Validation" workflow: "PR Check" diff --git a/.mokogitea/workflows/pr-metadata-check.yml b/.mokogit/.mokogitea/workflows/pr-metadata-check.yml similarity index 78% rename from .mokogitea/workflows/pr-metadata-check.yml rename to .mokogit/.mokogitea/workflows/pr-metadata-check.yml index a706c41..01d4ddc 100644 --- a/.mokogitea/workflows/pr-metadata-check.yml +++ b/.mokogit/.mokogitea/workflows/pr-metadata-check.yml @@ -3,12 +3,12 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: MokoGitea.Workflow +# DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.Validation # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # PATH: /templates/workflows/joomla/pr-metadata-check.yml.template # VERSION: 01.00.00 -# BRIEF: Validate MokoGitea metadata matches Joomla extension manifest on PRs +# BRIEF: Validate MokoGIT metadata matches Joomla extension manifest on PRs name: "Joomla: Metadata Validation" @@ -20,9 +20,9 @@ permissions: contents: read env: - MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GIT_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GIT_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} jobs: validate-metadata: @@ -37,7 +37,7 @@ jobs: - name: Setup MokoCLI tools env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting run: | if [ -f /opt/mokocli/cli/joomla_metadata_validate.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then @@ -57,14 +57,14 @@ jobs: - name: Validate metadata against Joomla manifest env: - MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} run: | php ${MOKO_CLI}/joomla_metadata_validate.php \ --path . \ - --token "${MOKOGITEA_TOKEN}" \ - --org "${GITEA_ORG}" \ - --repo "${GITEA_REPO}" \ - --api-base "${MOKOGITEA_URL}/api/v1" \ + --token "${MOKOGIT_TOKEN}" \ + --org "${GIT_ORG}" \ + --repo "${GIT_REPO}" \ + --api-base "${MOKOGIT_URL}/api/v1" \ --ci if [ $? -ne 0 ]; then diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogit/.mokogitea/workflows/pre-release.yml similarity index 86% rename from .mokogitea/workflows/pre-release.yml rename to .mokogit/.mokogitea/workflows/pre-release.yml index 2c4d3c9..a1e5b62 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogit/.mokogitea/workflows/pre-release.yml @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: MokoGitea.Workflow +# DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.Release # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # PATH: /templates/workflows/universal/pre-release.yml.template @@ -40,9 +40,9 @@ permissions: contents: write env: - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + GIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GIT_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GIT_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} jobs: build: @@ -61,7 +61,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - token: ${{ secrets.MOKOGITEA_TOKEN }} + token: ${{ secrets.MOKOGIT_TOKEN }} ref: ${{ github.ref_name }} submodules: recursive @@ -71,7 +71,7 @@ jobs: - name: Setup MokoCLI tools env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting run: | # Use pre-installed /opt/mokocli if available (updated by cron every 6h) @@ -156,9 +156,9 @@ jobs: fi # Commit version bump - git config --local user.email "mokogitea-actions[bot]@mokoconsulting.tech" - git config --local user.name "mokogitea-actions[bot]" - git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech" + git config --local user.name "mokogit-actions[bot]" + git remote set-url origin "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" git add -A git diff --cached --quiet || { git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" @@ -168,12 +168,12 @@ jobs: # Auto-detect element via manifest_element.php php ${MOKO_CLI}/manifest_element.php \ --path . --version "$VERSION" --stability "$STABILITY" \ - --repo "${GITEA_REPO}" --github-output + --repo "${GIT_REPO}" --github-output # Read back element outputs EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GIT_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" echo "version=${VERSION}" >> "$GITHUB_OUTPUT" @@ -191,18 +191,18 @@ jobs: run: | TAG="${{ steps.meta.outputs.tag }}" VERSION="${{ steps.meta.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" php ${MOKO_CLI}/release_create.php \ --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease + --token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GIT_REPO}" --branch "${{ github.ref_name }}" --prerelease - name: Update release notes from CHANGELOG.md if: steps.eligibility.outputs.proceed == 'true' run: | TAG="${{ steps.meta.outputs.tag }}" VERSION="${{ steps.meta.outputs.version }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" # Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading) if [ -f "CHANGELOG.md" ]; then @@ -213,7 +213,7 @@ jobs: fi # Update release body via API - RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ + RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGIT_TOKEN }}" \ "${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) if [ -n "$RELEASE_ID" ]; then @@ -225,7 +225,7 @@ jobs: '${API_BASE}/releases/${RELEASE_ID}', data=payload, method='PATCH', headers={ - 'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}', + 'Authorization': 'token ${{ secrets.MOKOGIT_TOKEN }}', 'Content-Type': 'application/json' }) urllib.request.urlopen(req) @@ -239,21 +239,21 @@ jobs: run: | VERSION="${{ steps.meta.outputs.version }}" TAG="${{ steps.meta.outputs.tag }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" php ${MOKO_CLI}/release_package.php \ --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --output /tmp || true + --token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GIT_REPO}" --output /tmp || true - # updates.xml is generated dynamically by MokoGitea license server + # updates.xml is generated dynamically by MokoGIT license server # No need to build, commit, or sync updates.xml from workflows - name: "Delete lesser pre-release channels (cascade)" if: steps.eligibility.outputs.proceed == 'true' continue-on-error: true run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" + TOKEN="${{ secrets.MOKOGIT_TOKEN }}" php ${MOKO_CLI}/release_cascade.php \ --stability "${{ steps.meta.outputs.stability }}" \ diff --git a/.mokogitea/workflows/rc-revert.yml b/.mokogit/.mokogitea/workflows/rc-revert.yml similarity index 90% rename from .mokogitea/workflows/rc-revert.yml rename to .mokogit/.mokogitea/workflows/rc-revert.yml index 8874441..e28d4bb 100644 --- a/.mokogitea/workflows/rc-revert.yml +++ b/.mokogit/.mokogitea/workflows/rc-revert.yml @@ -3,10 +3,10 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: MokoGitea.Workflow +# DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.Universal # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli -# PATH: /.mokogitea/workflows/rc-revert.yml +# PATH: /.mokogit/workflows/rc-revert.yml # VERSION: 09.23.00 # BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge @@ -32,8 +32,8 @@ jobs: env: BRANCH: ${{ github.event.pull_request.head.ref }} REPO: ${{ github.repository }} - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + GIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + TOKEN: ${{ secrets.MOKOGIT_TOKEN }} run: | set -euo pipefail # BRANCH is attacker-controlled (PR head ref). Strict allowlist before ANY use. @@ -42,7 +42,7 @@ jobs: fi SUFFIX="${BRANCH#rc/}" DEV_BRANCH="dev/${SUFFIX}" - API="${GITEA_URL}/api/v1/repos/${REPO}/branches" + API="${GIT_URL}/api/v1/repos/${REPO}/branches" # Create dev/ branch from rc/ branch STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \ diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogit/.mokogitea/workflows/repo-health.yml similarity index 98% rename from .mokogitea/workflows/repo-health.yml rename to .mokogit/.mokogitea/workflows/repo-health.yml index 31736e8..dbf433b 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogit/.mokogitea/workflows/repo-health.yml @@ -6,7 +6,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: MokoGitea.Workflow +# DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.Validation # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # PATH: /templates/workflows/joomla/repo_health.yml.template @@ -45,7 +45,7 @@ env: SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate # Repo health policy - REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/ + REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogit/workflows/ REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/ REPO_DISALLOWED_DIRS: REPO_DISALLOWED_FILES: TODO.md,todo.md @@ -56,7 +56,7 @@ env: # File / directory variables DOCS_INDEX: docs/docs-index.md SCRIPT_DIR: scripts - WORKFLOWS_DIR: .mokogitea/workflows + WORKFLOWS_DIR: .mokogit/workflows SHELLCHECK_PATTERN: '*.sh' SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml' FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true @@ -77,7 +77,7 @@ jobs: - name: Check actor permission (admin only) id: perm env: - TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }} + TOKEN: ${{ secrets.MOKOGIT_TOKEN || github.token }} REPO: ${{ github.repository }} ACTOR: ${{ github.actor }} run: | @@ -88,14 +88,14 @@ jobs: # Hardcoded authorized users — always allowed case "$ACTOR" in - jmiller|mokogitea-actions[bot]) + jmiller|mokogit-actions[bot]) ALLOWED=true PERMISSION=admin METHOD="hardcoded allowlist" ;; *) # Detect platform and check permissions via API - API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}" + API_BASE="${GITHUB_API_URL:-${GIT_API_URL:-https://api.github.com}}" RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \ "${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}') PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown") @@ -605,7 +605,7 @@ jobs: printf '%s\n' '| Domain | Status | Notes |' printf '%s\n' '|---|---|---|' printf '%s\n' '| Access control | OK | Admin-only execution gate |' - printf '%s\n' '| Release policy | N/A | Releases handled by MokoGitea |' + printf '%s\n' '| Release policy | N/A | Releases handled by MokoGIT |' printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |' printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |' printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |' @@ -677,7 +677,7 @@ jobs: if: >- always() && needs.scripts_governance.result == 'failure' - uses: ./.mokogitea/workflows/ci-issue-reporter.yml + uses: ./.mokogit/workflows/ci-issue-reporter.yml with: gate: "Scripts Governance" workflow: "Repo Health" @@ -691,7 +691,7 @@ jobs: if: >- always() && needs.repo_health.result == 'failure' - uses: ./.mokogitea/workflows/ci-issue-reporter.yml + uses: ./.mokogit/workflows/ci-issue-reporter.yml with: gate: "Repository Health" workflow: "Repo Health" diff --git a/.mokogitea/workflows/standards-compliance.yml b/.mokogit/.mokogitea/workflows/standards-compliance.yml similarity index 98% rename from .mokogitea/workflows/standards-compliance.yml rename to .mokogit/.mokogitea/workflows/standards-compliance.yml index 2a7abc3..4a8d9d6 100644 --- a/.mokogitea/workflows/standards-compliance.yml +++ b/.mokogit/.mokogitea/workflows/standards-compliance.yml @@ -4,7 +4,7 @@ # DEFGROUP: GitHub.Workflow # INGROUP: MokoCLI.Compliance # REPO: https://github.com/mokoconsulting-tech/mokocli -# PATH: /.mokogitea/workflows/standards-compliance.yml +# PATH: /.mokogit/workflows/standards-compliance.yml # VERSION: 04.06.00 # BRIEF: MokoCLI compliance validation workflow # NOTE: Validates repository structure, documentation, and coding standards @@ -299,7 +299,7 @@ jobs: echo "|-----------|--------|-------|------|-------|" >> $GITHUB_STEP_SUMMARY # Check required directories - for dir in .mokogitea; do + for dir in .mokogit; do if [ -d "$dir" ]; then FILE_COUNT=$(find "$dir" -type f 2>/dev/null | wc -l) DIR_SIZE=$(du -sh "$dir" 2>/dev/null | cut -f1) @@ -320,7 +320,7 @@ jobs: echo "### 🔴 Critical Issues: $MISSING" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Remediation Steps:**" >> $GITHUB_STEP_SUMMARY - [ ! -d ".mokogitea" ] && echo "- Create .mokogitea directory: \`mkdir -p .mokogitea/workflows\`" >> $GITHUB_STEP_SUMMARY + [ ! -d ".mokogit" ] && echo "- Create .mokogit directory: \`mkdir -p .mokogit/workflows\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "📚 Reference: [MokoCLI Repository Structure](https://github.com/mokoconsulting-tech/mokocli/tree/main/docs/policy/core-structure.md)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY @@ -563,17 +563,17 @@ jobs: set -x echo "### GitHub Actions Workflows" >> $GITHUB_STEP_SUMMARY - WORKFLOWS_DIR=".mokogitea/workflows" + WORKFLOWS_DIR=".mokogit/workflows" if [ ! -d "$WORKFLOWS_DIR" ]; then echo "❌ No workflows directory found" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### ❌ Validation Failed: Workflows Directory Missing" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "**Error:** .mokogitea/workflows directory is required for CI/CD automation" >> $GITHUB_STEP_SUMMARY - echo "**Action Required:** Create .mokogitea/workflows directory and add GitHub Actions workflows" >> $GITHUB_STEP_SUMMARY + echo "**Error:** .mokogit/workflows directory is required for CI/CD automation" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Create .mokogit/workflows directory and add GitHub Actions workflows" >> $GITHUB_STEP_SUMMARY echo "" - echo "❌ ERROR: .mokogitea/workflows directory not found" + echo "❌ ERROR: .mokogit/workflows directory not found" exit 1 fi @@ -612,7 +612,7 @@ jobs: echo "### Workflow YAML Syntax" >> $GITHUB_STEP_SUMMARY INVALID=0 - for workflow in $(find .mokogitea/workflows -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null); do + for workflow in $(find .mokogit/workflows -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null); do if [ -f "$workflow" ]; then if python3 -c "import yaml, sys; yaml.safe_load(open(sys.argv[1]))" "$workflow" 2>/dev/null; then echo "✅ $(basename $workflow)" >> $GITHUB_STEP_SUMMARY @@ -629,7 +629,7 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "**Error:** $INVALID workflow file(s) have invalid YAML syntax" >> $GITHUB_STEP_SUMMARY echo "**Action Required:** Fix YAML syntax errors in the marked workflow files" >> $GITHUB_STEP_SUMMARY - echo "**Tool:** Run \`python3 -c \"import yaml; yaml.safe_load(open('.mokogitea/workflows/FILE.yml'))\"\` locally" >> $GITHUB_STEP_SUMMARY + echo "**Tool:** Run \`python3 -c \"import yaml; yaml.safe_load(open('.mokogit/workflows/FILE.yml'))\"\` locally" >> $GITHUB_STEP_SUMMARY echo "" echo "❌ ERROR: $INVALID workflow file(s) with invalid YAML syntax" exit 1 @@ -641,7 +641,7 @@ jobs: echo "✅ SUCCESS: All workflow files passed YAML validation" - name: Validate CodeQL Configuration - if: hashFiles('.mokogitea/workflows/codeql-analysis.yml') != '' + if: hashFiles('.mokogit/workflows/codeql-analysis.yml') != '' run: | set -e echo "" >> $GITHUB_STEP_SUMMARY @@ -649,7 +649,7 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY # Inline validation (rewritten from Python to bash for PHP-only architecture) - CODEQL_FILE=".mokogitea/workflows/codeql-analysis.yml" + CODEQL_FILE=".mokogit/workflows/codeql-analysis.yml" if [ ! -f "$CODEQL_FILE" ]; then echo "⚠️ CodeQL workflow file not found" >> $GITHUB_STEP_SUMMARY @@ -1186,7 +1186,7 @@ jobs: fi # Run yamllint and count line-length warnings - YAML_OUTPUT=$(yamllint .mokogitea/workflows/*.yml 2>&1 | grep "line too long" || true) + YAML_OUTPUT=$(yamllint .mokogit/workflows/*.yml 2>&1 | grep "line too long" || true) if [ -n "$YAML_OUTPUT" ]; then YAML_VIOLATIONS=$(echo "$YAML_OUTPUT" | wc -l) echo "⚠️ Found $YAML_VIOLATIONS lines exceeding 180 characters in YAML files" >> $GITHUB_STEP_SUMMARY @@ -2130,17 +2130,17 @@ jobs: WARNINGS=0 ERRORS=0 - # 1. Check .mokogitea/config.tf location (not root override files) + # 1. Check .mokogit/config.tf location (not root override files) echo "### Override Configuration Check" >> $GITHUB_STEP_SUMMARY LEGACY_OVERRIDES=$(find . -maxdepth 1 -name "*override*.tf" -o -name "MokoCLI.override.tf" 2>/dev/null | wc -l || echo 0) if [ "$LEGACY_OVERRIDES" -gt 0 ]; then echo "⚠️ Found legacy override files in root directory" >> $GITHUB_STEP_SUMMARY - echo "**Expected Location**: .mokogitea/config.tf" >> $GITHUB_STEP_SUMMARY + echo "**Expected Location**: .mokogit/config.tf" >> $GITHUB_STEP_SUMMARY echo "**Legacy files found**: $LEGACY_OVERRIDES" >> $GITHUB_STEP_SUMMARY WARNINGS=$((WARNINGS + 1)) else - if [ -f ".mokogitea/config.tf" ]; then - echo "✅ Override configuration in correct location (.mokogitea/config.tf)" >> $GITHUB_STEP_SUMMARY + if [ -f ".mokogit/config.tf" ]; then + echo "✅ Override configuration in correct location (.mokogit/config.tf)" >> $GITHUB_STEP_SUMMARY else echo "ℹ️ No override configuration found" >> $GITHUB_STEP_SUMMARY fi diff --git a/.mokogitea/workflows/version-set.yml b/.mokogit/.mokogitea/workflows/version-set.yml similarity index 96% rename from .mokogitea/workflows/version-set.yml rename to .mokogit/.mokogitea/workflows/version-set.yml index 172c085..d451b9b 100644 --- a/.mokogitea/workflows/version-set.yml +++ b/.mokogit/.mokogitea/workflows/version-set.yml @@ -3,10 +3,10 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: MokoGitea.Workflow.Template +# DEFGROUP: MokoGIT.Workflow.Template # INGROUP: MokoCLI.CI # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla -# PATH: /.mokogitea/workflows/version-set.yml +# PATH: /.mokogit/workflows/version-set.yml # VERSION: 01.00.00 # BRIEF: Set or reset the extension version across all version-bearing files @@ -48,7 +48,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - token: ${{ secrets.MOKOGITEA_TOKEN || github.token }} + token: ${{ secrets.MOKOGIT_TOKEN || github.token }} ref: ${{ inputs.branch || github.ref }} fetch-depth: 1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 73f36d9..2cb5400 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,7 @@ feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge 7. **Merging to main** triggers the stable release pipeline: - Minor version bump (e.g., `02.09.xx` → `02.10.00`) - Stability suffix stripped (clean version) - - MokoGitea release created with ZIP/tar.gz packages + - MokoGIT release created with ZIP/tar.gz packages - `updates.xml` updated (Joomla extensions) - `dev` branch recreated from `main` @@ -119,7 +119,7 @@ This ensures Joomla sites on ANY stability channel see the update (Joomla only s The version tools update all files containing version stamps: -- `.mokogitea/manifest.xml` (canonical source) +- `.mokogit/manifest.xml` (canonical source) - Joomla XML manifests (`` tag) - `README.md`, `CHANGELOG.md` (`VERSION:` pattern) - `package.json`, `pyproject.toml` diff --git a/README.md b/README.md index ba00d91..301b248 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ A modern, lightweight Joomla site template built on Cassiopeia with Font Awesome | **PHP** | 8.3+ | | **License** | GPL-3.0-or-later | | **Replaces** | MokoCassiopeia (auto-migrates on install) | -| **Repository** | [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) (primary) | +| **Repository** | [MokoGIT](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) (primary) | --- @@ -95,7 +95,7 @@ Key parameters include: --- -> **[MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki)** -- central standards hub for all Moko Consulting projects. +> **[MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/.mokogit/wiki)** -- central standards hub for all Moko Consulting projects. --- @@ -117,4 +117,4 @@ This project is licensed under the GNU General Public License v3.0 or later -- s --- -*[Moko Consulting](https://mokoconsulting.tech) -- [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki)* +*[Moko Consulting](https://mokoconsulting.tech) -- [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/.mokogit/wiki)* diff --git a/source/language/en-GB/tpl_mokoonyx.sys.ini b/source/language/en-GB/tpl_mokoonyx.sys.ini index 3657ef8..4fe1b58 100644 --- a/source/language/en-GB/tpl_mokoonyx.sys.ini +++ b/source/language/en-GB/tpl_mokoonyx.sys.ini @@ -28,4 +28,4 @@ TPL_MOKOONYX_POSITION_TOP_B="Top-b" TPL_MOKOONYX_POSITION_TOPBAR="Top Bar" TPL_MOKOONYX_POSITION_DRAWER_LEFT="Drawer-Left" TPL_MOKOONYX_POSITION_DRAWER_RIGHT="Drawer-Right" -TPL_MOKOONYX_XML_DESCRIPTION="

\"Version\" \"License\" \"Joomla\" \"PHP\"

MokoOnyx Template Description

MokoOnyx continues Joomla’s tradition of space-themed default templates— building on the legacy of Solarflare (Joomla 1.0), Milkyway (Joomla 1.5), and Protostar (Joomla 3.0).

This template is a customized fork of the Cassiopeia template introduced in Joomla 4, preserving its modern, accessible, and mobile-first foundation while introducing new stylistic enhancements and structural refinements specifically tailored for use by Moko Consulting.

Custom Colour Themes

Starter palette files are included with the template. To create a custom colour scheme, copy templates/mokoonyx/templates/light.custom.css to media/templates/site/mokoonyx/css/theme/light.custom.css, or templates/mokoonyx/templates/dark.custom.css to media/templates/site/mokoonyx/css/theme/dark.custom.css. Customise the CSS variables to match your brand, then activate your palette in System → Site Templates → MokoOnyx → Theme tab by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the CSS Variables tab in template options.

Custom CSS & JavaScript

For site-specific styles and scripts that should survive template updates, create the following files:

  • media/templates/site/mokoonyx/css/user.css — loaded on every page for custom CSS overrides.
  • media/templates/site/mokoonyx/js/user.js — loaded on every page for custom JavaScript.

These files are gitignored and will not be overwritten by template updates.

Code Attribution

This template is based on the original Cassiopeia template developed by the Joomla! Project and released under the GNU General Public License.

Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards.

It includes integration with Bootstrap TOC, an open-source table of contents generator by A. Feld, licensed under the MIT License.

All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable.

" +TPL_MOKOONYX_XML_DESCRIPTION="

\"Version\" \"License\" \"Joomla\" \"PHP\"

MokoOnyx Template Description

MokoOnyx continues Joomla’s tradition of space-themed default templates— building on the legacy of Solarflare (Joomla 1.0), Milkyway (Joomla 1.5), and Protostar (Joomla 3.0).

This template is a customized fork of the Cassiopeia template introduced in Joomla 4, preserving its modern, accessible, and mobile-first foundation while introducing new stylistic enhancements and structural refinements specifically tailored for use by Moko Consulting.

Custom Colour Themes

Starter palette files are included with the template. To create a custom colour scheme, copy templates/mokoonyx/templates/light.custom.css to media/templates/site/mokoonyx/css/theme/light.custom.css, or templates/mokoonyx/templates/dark.custom.css to media/templates/site/mokoonyx/css/theme/dark.custom.css. Customise the CSS variables to match your brand, then activate your palette in System → Site Templates → MokoOnyx → Theme tab by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the CSS Variables tab in template options.

Custom CSS & JavaScript

For site-specific styles and scripts that should survive template updates, create the following files:

  • media/templates/site/mokoonyx/css/user.css — loaded on every page for custom CSS overrides.
  • media/templates/site/mokoonyx/js/user.js — loaded on every page for custom JavaScript.

These files are gitignored and will not be overwritten by template updates.

Code Attribution

This template is based on the original Cassiopeia template developed by the Joomla! Project and released under the GNU General Public License.

Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards.

It includes integration with Bootstrap TOC, an open-source table of contents generator by A. Feld, licensed under the MIT License.

All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable.

" diff --git a/source/templateDetails.xml b/source/templateDetails.xml index ce2bf24..b5aeab8 100644 --- a/source/templateDetails.xml +++ b/source/templateDetails.xml @@ -41,7 +41,7 @@ Jonathan Miller || Moko Consulting hello@mokoconsulting.tech (C)GNU General Public License Version 3 - 2026 Moko Consulting - Version License Joomla PHP

MokoOnyx

MokoOnyx is a modern, lightweight enhancement layer built on Joomla's Cassiopeia template. It adds Font Awesome 7, Bootstrap 5 helpers, automatic Table of Contents, advanced Dark Mode theming, and optional Google Tag Manager / GA4 integration — all with minimal core overrides for maximum upgrade compatibility.

Custom Colour Themes

Copy templates/mokoonyx/templates/light.custom.css to media/templates/site/mokoonyx/css/theme/light.custom.css (or dark equivalent), customise the CSS variables, then select "Custom" in the Theme tab.

Custom CSS & JavaScript

  • media/templates/site/mokoonyx/css/user.css — custom CSS overrides
  • media/templates/site/mokoonyx/js/user.js — custom JavaScript

These files survive template updates.

]]>
+ Version License Joomla PHP

MokoOnyx

MokoOnyx is a modern, lightweight enhancement layer built on Joomla's Cassiopeia template. It adds Font Awesome 7, Bootstrap 5 helpers, automatic Table of Contents, advanced Dark Mode theming, and optional Google Tag Manager / GA4 integration — all with minimal core overrides for maximum upgrade compatibility.

Custom Colour Themes

Copy templates/mokoonyx/templates/light.custom.css to media/templates/site/mokoonyx/css/theme/light.custom.css (or dark equivalent), customise the CSS variables, then select "Custom" in the Theme tab.

Custom CSS & JavaScript

  • media/templates/site/mokoonyx/css/user.css — custom CSS overrides
  • media/templates/site/mokoonyx/js/user.js — custom JavaScript

These files survive template updates.

]]>
8.1.0 1 diff --git a/wiki/development.md b/wiki/development.md index b001b02..a293581 100644 --- a/wiki/development.md +++ b/wiki/development.md @@ -144,7 +144,7 @@ After the release package is built: 1. Tag the release: `git tag {version}` 2. Push tags: `git push origin --tags` -3. Create a MokoGitea release and attach the ZIP from `dist/` +3. Create a MokoGIT release and attach the ZIP from `dist/` ### Generate checksums @@ -185,7 +185,7 @@ Creates SHA-256 checksums for all ZIP files in `dist/`. ## Contributing -1. Fork the repository on [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) +1. Fork the repository on [MokoGIT](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) 2. Create a feature branch from `dev` 3. Make your changes in `src/` 4. Run `make validate` to ensure all checks pass @@ -245,7 +245,7 @@ When a PR is merged from `dev` to `main`: 1. The minor version is bumped (e.g., `02.08.xx` -> `02.09.00`) 2. Stability suffixes are stripped (stable release) -3. A MokoGitea release is created with the build package (ZIP + tar.gz + SHA-256) +3. A MokoGIT release is created with the build package (ZIP + tar.gz + SHA-256) 4. `updates.xml` is updated for the Joomla update server 5. The `dev` branch is recreated from `main` 6. A `version/XX.YY.ZZ` archive branch is created diff --git a/wiki/guides/installation.md b/wiki/guides/installation.md index e9702ba..4a0e779 100644 --- a/wiki/guides/installation.md +++ b/wiki/guides/installation.md @@ -50,7 +50,7 @@ MokoOnyx includes an automatic update server. Joomla will notify you when new ve | Priority | Server | |----------|--------| -| 1 | Gitea (git.mokoconsulting.tech) | +| 1 | Git (git.mokoconsulting.tech) | | 2 | GitHub (github.com) | --- diff --git a/wiki/home.md b/wiki/home.md index 5e35f0f..fe01b7e 100644 --- a/wiki/home.md +++ b/wiki/home.md @@ -12,7 +12,7 @@ A modern, lightweight Joomla site template built on Cassiopeia with Font Awesome | **PHP** | 8.1+ | | **License** | GPL-3.0-or-later | | **Replaces** | MokoCassiopeia (auto-migrates on install) | -| **Platform** | [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) (primary) | +| **Platform** | [MokoGIT](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) (primary) | --- diff --git a/wiki/installation.md b/wiki/installation.md index 51f024b..a23bed8 100644 --- a/wiki/installation.md +++ b/wiki/installation.md @@ -50,7 +50,7 @@ MokoOnyx includes an automatic update server. Joomla will notify you when new ve | Priority | Server | |----------|--------| -| 1 | MokoGitea (git.mokoconsulting.tech) | +| 1 | MokoGIT (git.mokoconsulting.tech) | | 2 | GitHub (github.com) | --- diff --git a/wiki/reference/contributing.md b/wiki/reference/contributing.md index 9da3454..df85214 100644 --- a/wiki/reference/contributing.md +++ b/wiki/reference/contributing.md @@ -70,7 +70,7 @@ Commit messages should: ## Reporting Issues -Bug reports and enhancement requests should be filed as Gitea issues and include: +Bug reports and enhancement requests should be filed as Git issues and include: * Clear reproduction steps or use cases * Expected versus actual behavior diff --git a/wiki/reference/development.md b/wiki/reference/development.md index d2b983a..b338446 100644 --- a/wiki/reference/development.md +++ b/wiki/reference/development.md @@ -144,7 +144,7 @@ After the release package is built: 1. Tag the release: `git tag {version}` 2. Push tags: `git push origin --tags` -3. Create a Gitea release and attach the ZIP from `dist/` +3. Create a Git release and attach the ZIP from `dist/` ### Generate checksums @@ -185,7 +185,7 @@ Creates SHA-256 checksums for all ZIP files in `dist/`. ## Contributing -1. Fork the repository on [Gitea](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) +1. Fork the repository on [Git](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) 2. Create a feature branch from `dev` 3. Make your changes in `src/` 4. Run `make validate` to ensure all checks pass @@ -245,7 +245,7 @@ When a PR is merged from `dev` to `main`: 1. The minor version is bumped (e.g., `02.08.xx` -> `02.09.00`) 2. Stability suffixes are stripped (stable release) -3. A Gitea release is created with the build package (ZIP + tar.gz + SHA-256) +3. A Git release is created with the build package (ZIP + tar.gz + SHA-256) 4. `updates.xml` is updated for the Joomla update server 5. The `dev` branch is recreated from `main` 6. A `version/XX.YY.ZZ` archive branch is created diff --git a/wiki/unnamed.md b/wiki/unnamed.md index aed06df..91a01a7 100644 --- a/wiki/unnamed.md +++ b/wiki/unnamed.md @@ -70,7 +70,7 @@ Commit messages should: ## Reporting Issues -Bug reports and enhancement requests should be filed as MokoGitea issues and include: +Bug reports and enhancement requests should be filed as MokoGIT issues and include: * Clear reproduction steps or use cases * Expected versus actual behavior -- 2.52.0 From c5fce57776313cb752ff4caecf86fbcbd7c059e1 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 14 Jul 2026 20:39:49 -0500 Subject: [PATCH 61/63] chore(rebrand): fix residual gitea (space-path files + gitea-named files) Claude-Session: https://claude.ai/code/session_01DQEMmJPe61ya7HDfA6BHP8 --- .mokogit/{.mokogitea => .mokogit}/CLAUDE.md | 0 .mokogit/{.mokogitea => .mokogit}/ISSUE_TEMPLATE/adr.md | 0 .mokogit/{.mokogitea => .mokogit}/ISSUE_TEMPLATE/bug_report.md | 0 .mokogit/{.mokogitea => .mokogit}/ISSUE_TEMPLATE/config.yml | 0 .mokogit/{.mokogitea => .mokogit}/ISSUE_TEMPLATE/documentation.md | 0 .../{.mokogitea => .mokogit}/ISSUE_TEMPLATE/feature_request.md | 0 .mokogit/{.mokogitea => .mokogit}/ISSUE_TEMPLATE/joomla_issue.md | 0 .mokogit/{.mokogitea => .mokogit}/ISSUE_TEMPLATE/question.md | 0 .mokogit/{.mokogitea => .mokogit}/ISSUE_TEMPLATE/rfc.md | 0 .mokogit/{.mokogitea => .mokogit}/ISSUE_TEMPLATE/security.md | 0 .mokogit/{.mokogitea => .mokogit}/ISSUE_TEMPLATE/version.md | 0 .../{.mokogitea => .mokogit}/ISSUE_TEMPLATE/waas_site_issue.md | 0 .mokogit/{.mokogitea => .mokogit}/branch-protection.yml | 0 .mokogit/{.mokogitea => .mokogit}/cascade-dev.yml | 0 .mokogit/{.mokogitea => .mokogit}/cleanup.yml | 0 .mokogit/{.mokogitea => .mokogit}/deploy-manual.yml | 0 .mokogit/{.mokogitea => .mokogit}/gitleaks.yml | 0 .mokogit/{.mokogitea => .mokogit}/notify.yml | 0 .mokogit/{.mokogitea => .mokogit}/pr-check.yml | 0 .mokogit/{.mokogitea => .mokogit}/pre-release.yml | 0 .mokogit/{.mokogitea => .mokogit}/security-audit.yml | 0 .mokogit/{.mokogitea => .mokogit}/workflows/auto-bump.yml | 0 .mokogit/{.mokogitea => .mokogit}/workflows/auto-release.yml | 0 .mokogit/{.mokogitea => .mokogit}/workflows/branch-cleanup.yml | 0 .mokogit/{.mokogitea => .mokogit}/workflows/cascade-dev.yml | 0 .mokogit/{.mokogitea => .mokogit}/workflows/ci-generic.yml | 0 .mokogit/{.mokogitea => .mokogit}/workflows/ci-issue-reporter.yml | 0 .mokogit/{.mokogitea => .mokogit}/workflows/ci-joomla.yml | 0 .mokogit/{.mokogitea => .mokogit}/workflows/cleanup.yml | 0 .mokogit/{.mokogitea => .mokogit}/workflows/gitleaks.yml | 0 .mokogit/{.mokogitea => .mokogit}/workflows/issue-branch.yml | 0 .mokogit/{.mokogitea => .mokogit}/workflows/notify.yml | 0 .mokogit/{.mokogitea => .mokogit}/workflows/pr-check.yml | 0 .mokogit/{.mokogitea => .mokogit}/workflows/pr-metadata-check.yml | 0 .mokogit/{.mokogitea => .mokogit}/workflows/pre-release.yml | 0 .mokogit/{.mokogitea => .mokogit}/workflows/rc-revert.yml | 0 .mokogit/{.mokogitea => .mokogit}/workflows/repo-health.yml | 0 .../{.mokogitea => .mokogit}/workflows/standards-compliance.yml | 0 .mokogit/{.mokogitea => .mokogit}/workflows/version-set.yml | 0 39 files changed, 0 insertions(+), 0 deletions(-) rename .mokogit/{.mokogitea => .mokogit}/CLAUDE.md (100%) rename .mokogit/{.mokogitea => .mokogit}/ISSUE_TEMPLATE/adr.md (100%) rename .mokogit/{.mokogitea => .mokogit}/ISSUE_TEMPLATE/bug_report.md (100%) rename .mokogit/{.mokogitea => .mokogit}/ISSUE_TEMPLATE/config.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/ISSUE_TEMPLATE/documentation.md (100%) rename .mokogit/{.mokogitea => .mokogit}/ISSUE_TEMPLATE/feature_request.md (100%) rename .mokogit/{.mokogitea => .mokogit}/ISSUE_TEMPLATE/joomla_issue.md (100%) rename .mokogit/{.mokogitea => .mokogit}/ISSUE_TEMPLATE/question.md (100%) rename .mokogit/{.mokogitea => .mokogit}/ISSUE_TEMPLATE/rfc.md (100%) rename .mokogit/{.mokogitea => .mokogit}/ISSUE_TEMPLATE/security.md (100%) rename .mokogit/{.mokogitea => .mokogit}/ISSUE_TEMPLATE/version.md (100%) rename .mokogit/{.mokogitea => .mokogit}/ISSUE_TEMPLATE/waas_site_issue.md (100%) rename .mokogit/{.mokogitea => .mokogit}/branch-protection.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/cascade-dev.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/cleanup.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/deploy-manual.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/gitleaks.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/notify.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/pr-check.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/pre-release.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/security-audit.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/workflows/auto-bump.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/workflows/auto-release.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/workflows/branch-cleanup.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/workflows/cascade-dev.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/workflows/ci-generic.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/workflows/ci-issue-reporter.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/workflows/ci-joomla.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/workflows/cleanup.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/workflows/gitleaks.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/workflows/issue-branch.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/workflows/notify.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/workflows/pr-check.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/workflows/pr-metadata-check.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/workflows/pre-release.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/workflows/rc-revert.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/workflows/repo-health.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/workflows/standards-compliance.yml (100%) rename .mokogit/{.mokogitea => .mokogit}/workflows/version-set.yml (100%) diff --git a/.mokogit/.mokogitea/CLAUDE.md b/.mokogit/.mokogit/CLAUDE.md similarity index 100% rename from .mokogit/.mokogitea/CLAUDE.md rename to .mokogit/.mokogit/CLAUDE.md diff --git a/.mokogit/.mokogitea/ISSUE_TEMPLATE/adr.md b/.mokogit/.mokogit/ISSUE_TEMPLATE/adr.md similarity index 100% rename from .mokogit/.mokogitea/ISSUE_TEMPLATE/adr.md rename to .mokogit/.mokogit/ISSUE_TEMPLATE/adr.md diff --git a/.mokogit/.mokogitea/ISSUE_TEMPLATE/bug_report.md b/.mokogit/.mokogit/ISSUE_TEMPLATE/bug_report.md similarity index 100% rename from .mokogit/.mokogitea/ISSUE_TEMPLATE/bug_report.md rename to .mokogit/.mokogit/ISSUE_TEMPLATE/bug_report.md diff --git a/.mokogit/.mokogitea/ISSUE_TEMPLATE/config.yml b/.mokogit/.mokogit/ISSUE_TEMPLATE/config.yml similarity index 100% rename from .mokogit/.mokogitea/ISSUE_TEMPLATE/config.yml rename to .mokogit/.mokogit/ISSUE_TEMPLATE/config.yml diff --git a/.mokogit/.mokogitea/ISSUE_TEMPLATE/documentation.md b/.mokogit/.mokogit/ISSUE_TEMPLATE/documentation.md similarity index 100% rename from .mokogit/.mokogitea/ISSUE_TEMPLATE/documentation.md rename to .mokogit/.mokogit/ISSUE_TEMPLATE/documentation.md diff --git a/.mokogit/.mokogitea/ISSUE_TEMPLATE/feature_request.md b/.mokogit/.mokogit/ISSUE_TEMPLATE/feature_request.md similarity index 100% rename from .mokogit/.mokogitea/ISSUE_TEMPLATE/feature_request.md rename to .mokogit/.mokogit/ISSUE_TEMPLATE/feature_request.md diff --git a/.mokogit/.mokogitea/ISSUE_TEMPLATE/joomla_issue.md b/.mokogit/.mokogit/ISSUE_TEMPLATE/joomla_issue.md similarity index 100% rename from .mokogit/.mokogitea/ISSUE_TEMPLATE/joomla_issue.md rename to .mokogit/.mokogit/ISSUE_TEMPLATE/joomla_issue.md diff --git a/.mokogit/.mokogitea/ISSUE_TEMPLATE/question.md b/.mokogit/.mokogit/ISSUE_TEMPLATE/question.md similarity index 100% rename from .mokogit/.mokogitea/ISSUE_TEMPLATE/question.md rename to .mokogit/.mokogit/ISSUE_TEMPLATE/question.md diff --git a/.mokogit/.mokogitea/ISSUE_TEMPLATE/rfc.md b/.mokogit/.mokogit/ISSUE_TEMPLATE/rfc.md similarity index 100% rename from .mokogit/.mokogitea/ISSUE_TEMPLATE/rfc.md rename to .mokogit/.mokogit/ISSUE_TEMPLATE/rfc.md diff --git a/.mokogit/.mokogitea/ISSUE_TEMPLATE/security.md b/.mokogit/.mokogit/ISSUE_TEMPLATE/security.md similarity index 100% rename from .mokogit/.mokogitea/ISSUE_TEMPLATE/security.md rename to .mokogit/.mokogit/ISSUE_TEMPLATE/security.md diff --git a/.mokogit/.mokogitea/ISSUE_TEMPLATE/version.md b/.mokogit/.mokogit/ISSUE_TEMPLATE/version.md similarity index 100% rename from .mokogit/.mokogitea/ISSUE_TEMPLATE/version.md rename to .mokogit/.mokogit/ISSUE_TEMPLATE/version.md diff --git a/.mokogit/.mokogitea/ISSUE_TEMPLATE/waas_site_issue.md b/.mokogit/.mokogit/ISSUE_TEMPLATE/waas_site_issue.md similarity index 100% rename from .mokogit/.mokogitea/ISSUE_TEMPLATE/waas_site_issue.md rename to .mokogit/.mokogit/ISSUE_TEMPLATE/waas_site_issue.md diff --git a/.mokogit/.mokogitea/branch-protection.yml b/.mokogit/.mokogit/branch-protection.yml similarity index 100% rename from .mokogit/.mokogitea/branch-protection.yml rename to .mokogit/.mokogit/branch-protection.yml diff --git a/.mokogit/.mokogitea/cascade-dev.yml b/.mokogit/.mokogit/cascade-dev.yml similarity index 100% rename from .mokogit/.mokogitea/cascade-dev.yml rename to .mokogit/.mokogit/cascade-dev.yml diff --git a/.mokogit/.mokogitea/cleanup.yml b/.mokogit/.mokogit/cleanup.yml similarity index 100% rename from .mokogit/.mokogitea/cleanup.yml rename to .mokogit/.mokogit/cleanup.yml diff --git a/.mokogit/.mokogitea/deploy-manual.yml b/.mokogit/.mokogit/deploy-manual.yml similarity index 100% rename from .mokogit/.mokogitea/deploy-manual.yml rename to .mokogit/.mokogit/deploy-manual.yml diff --git a/.mokogit/.mokogitea/gitleaks.yml b/.mokogit/.mokogit/gitleaks.yml similarity index 100% rename from .mokogit/.mokogitea/gitleaks.yml rename to .mokogit/.mokogit/gitleaks.yml diff --git a/.mokogit/.mokogitea/notify.yml b/.mokogit/.mokogit/notify.yml similarity index 100% rename from .mokogit/.mokogitea/notify.yml rename to .mokogit/.mokogit/notify.yml diff --git a/.mokogit/.mokogitea/pr-check.yml b/.mokogit/.mokogit/pr-check.yml similarity index 100% rename from .mokogit/.mokogitea/pr-check.yml rename to .mokogit/.mokogit/pr-check.yml diff --git a/.mokogit/.mokogitea/pre-release.yml b/.mokogit/.mokogit/pre-release.yml similarity index 100% rename from .mokogit/.mokogitea/pre-release.yml rename to .mokogit/.mokogit/pre-release.yml diff --git a/.mokogit/.mokogitea/security-audit.yml b/.mokogit/.mokogit/security-audit.yml similarity index 100% rename from .mokogit/.mokogitea/security-audit.yml rename to .mokogit/.mokogit/security-audit.yml diff --git a/.mokogit/.mokogitea/workflows/auto-bump.yml b/.mokogit/.mokogit/workflows/auto-bump.yml similarity index 100% rename from .mokogit/.mokogitea/workflows/auto-bump.yml rename to .mokogit/.mokogit/workflows/auto-bump.yml diff --git a/.mokogit/.mokogitea/workflows/auto-release.yml b/.mokogit/.mokogit/workflows/auto-release.yml similarity index 100% rename from .mokogit/.mokogitea/workflows/auto-release.yml rename to .mokogit/.mokogit/workflows/auto-release.yml diff --git a/.mokogit/.mokogitea/workflows/branch-cleanup.yml b/.mokogit/.mokogit/workflows/branch-cleanup.yml similarity index 100% rename from .mokogit/.mokogitea/workflows/branch-cleanup.yml rename to .mokogit/.mokogit/workflows/branch-cleanup.yml diff --git a/.mokogit/.mokogitea/workflows/cascade-dev.yml b/.mokogit/.mokogit/workflows/cascade-dev.yml similarity index 100% rename from .mokogit/.mokogitea/workflows/cascade-dev.yml rename to .mokogit/.mokogit/workflows/cascade-dev.yml diff --git a/.mokogit/.mokogitea/workflows/ci-generic.yml b/.mokogit/.mokogit/workflows/ci-generic.yml similarity index 100% rename from .mokogit/.mokogitea/workflows/ci-generic.yml rename to .mokogit/.mokogit/workflows/ci-generic.yml diff --git a/.mokogit/.mokogitea/workflows/ci-issue-reporter.yml b/.mokogit/.mokogit/workflows/ci-issue-reporter.yml similarity index 100% rename from .mokogit/.mokogitea/workflows/ci-issue-reporter.yml rename to .mokogit/.mokogit/workflows/ci-issue-reporter.yml diff --git a/.mokogit/.mokogitea/workflows/ci-joomla.yml b/.mokogit/.mokogit/workflows/ci-joomla.yml similarity index 100% rename from .mokogit/.mokogitea/workflows/ci-joomla.yml rename to .mokogit/.mokogit/workflows/ci-joomla.yml diff --git a/.mokogit/.mokogitea/workflows/cleanup.yml b/.mokogit/.mokogit/workflows/cleanup.yml similarity index 100% rename from .mokogit/.mokogitea/workflows/cleanup.yml rename to .mokogit/.mokogit/workflows/cleanup.yml diff --git a/.mokogit/.mokogitea/workflows/gitleaks.yml b/.mokogit/.mokogit/workflows/gitleaks.yml similarity index 100% rename from .mokogit/.mokogitea/workflows/gitleaks.yml rename to .mokogit/.mokogit/workflows/gitleaks.yml diff --git a/.mokogit/.mokogitea/workflows/issue-branch.yml b/.mokogit/.mokogit/workflows/issue-branch.yml similarity index 100% rename from .mokogit/.mokogitea/workflows/issue-branch.yml rename to .mokogit/.mokogit/workflows/issue-branch.yml diff --git a/.mokogit/.mokogitea/workflows/notify.yml b/.mokogit/.mokogit/workflows/notify.yml similarity index 100% rename from .mokogit/.mokogitea/workflows/notify.yml rename to .mokogit/.mokogit/workflows/notify.yml diff --git a/.mokogit/.mokogitea/workflows/pr-check.yml b/.mokogit/.mokogit/workflows/pr-check.yml similarity index 100% rename from .mokogit/.mokogitea/workflows/pr-check.yml rename to .mokogit/.mokogit/workflows/pr-check.yml diff --git a/.mokogit/.mokogitea/workflows/pr-metadata-check.yml b/.mokogit/.mokogit/workflows/pr-metadata-check.yml similarity index 100% rename from .mokogit/.mokogitea/workflows/pr-metadata-check.yml rename to .mokogit/.mokogit/workflows/pr-metadata-check.yml diff --git a/.mokogit/.mokogitea/workflows/pre-release.yml b/.mokogit/.mokogit/workflows/pre-release.yml similarity index 100% rename from .mokogit/.mokogitea/workflows/pre-release.yml rename to .mokogit/.mokogit/workflows/pre-release.yml diff --git a/.mokogit/.mokogitea/workflows/rc-revert.yml b/.mokogit/.mokogit/workflows/rc-revert.yml similarity index 100% rename from .mokogit/.mokogitea/workflows/rc-revert.yml rename to .mokogit/.mokogit/workflows/rc-revert.yml diff --git a/.mokogit/.mokogitea/workflows/repo-health.yml b/.mokogit/.mokogit/workflows/repo-health.yml similarity index 100% rename from .mokogit/.mokogitea/workflows/repo-health.yml rename to .mokogit/.mokogit/workflows/repo-health.yml diff --git a/.mokogit/.mokogitea/workflows/standards-compliance.yml b/.mokogit/.mokogit/workflows/standards-compliance.yml similarity index 100% rename from .mokogit/.mokogitea/workflows/standards-compliance.yml rename to .mokogit/.mokogit/workflows/standards-compliance.yml diff --git a/.mokogit/.mokogitea/workflows/version-set.yml b/.mokogit/.mokogit/workflows/version-set.yml similarity index 100% rename from .mokogit/.mokogitea/workflows/version-set.yml rename to .mokogit/.mokogit/workflows/version-set.yml -- 2.52.0 From ab7fe287a66082f8530df0b01fcc22c3cd34e6b0 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Wed, 15 Jul 2026 03:24:40 +0000 Subject: [PATCH 62/63] fix(ci): harden issue-branch.yml against Actions injection (sync from Template-Generic) --- .mokogit/workflows/issue-branch.yml | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/.mokogit/workflows/issue-branch.yml b/.mokogit/workflows/issue-branch.yml index c6f52aa..e736ca8 100644 --- a/.mokogit/workflows/issue-branch.yml +++ b/.mokogit/workflows/issue-branch.yml @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: MokoGit.Workflow +# DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.Automation # VERSION: 01.00.00 # BRIEF: Auto-create feature branch when an issue is opened @@ -27,14 +27,22 @@ jobs: runs-on: ubuntu-latest steps: - name: Create branch and comment + # SECURITY: never interpolate github.event.* into a run: script — the + # Actions engine splices the raw value into the shell source (command + # injection via a crafted issue title). Pass everything through env so + # the values arrive as ordinary shell variables that are not re-parsed. + env: + MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} + REPO: ${{ github.repository }} + ISSUE_NUM: ${{ github.event.issue.number }} + ISSUE_TITLE: ${{ github.event.issue.title }} run: | - TOKEN="${{ secrets.MOKOGIT_TOKEN }}" - API="${MOKOGIT_URL}/api/v1/repos/${{ github.repository }}" - ISSUE_NUM="${{ github.event.issue.number }}" - ISSUE_TITLE="${{ github.event.issue.title }}" + TOKEN="${MOKOGIT_TOKEN}" + API="${MOKOGIT_URL}/api/v1/repos/${REPO}" - # Build slug from title: lowercase, replace non-alnum with dash, trim - SLUG=$(echo "${ISSUE_TITLE}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-40) + # Build slug from title: lowercase, replace non-alnum with dash, trim. + # printf (not echo) so a title beginning with "-" is not read as flags. + SLUG=$(printf '%s' "${ISSUE_TITLE}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-40) BRANCH="feature/${ISSUE_NUM}-${SLUG}" # Check dev branch exists @@ -58,7 +66,7 @@ jobs: echo "Created branch: ${BRANCH}" # Comment on issue with branch link - REPO_URL="${MOKOGIT_URL}/${{ github.repository }}" + REPO_URL="${MOKOGIT_URL}/${REPO}" BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`" curl -sf -X POST \ -- 2.52.0 From 54196c833b264496ea2a72f08c76d2b96f461463 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Wed, 15 Jul 2026 03:24:42 +0000 Subject: [PATCH 63/63] fix(ci): harden branch-cleanup.yml against Actions injection (sync from Template-Generic) --- .mokogit/workflows/branch-cleanup.yml | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/.mokogit/workflows/branch-cleanup.yml b/.mokogit/workflows/branch-cleanup.yml index 545e137..183b1df 100644 --- a/.mokogit/workflows/branch-cleanup.yml +++ b/.mokogit/workflows/branch-cleanup.yml @@ -3,9 +3,9 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # FILE INFORMATION -# DEFGROUP: MokoGit.Workflow +# DEFGROUP: MokoGIT.Workflow # INGROUP: MokoCLI.Universal -# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # PATH: /.mokogit/workflows/branch-cleanup.yml # VERSION: 01.00.00 # BRIEF: Delete feature branches after PR merge @@ -30,14 +30,25 @@ jobs: steps: - name: Delete source branch + # SECURITY: the PR head ref is attacker-controllable. Pass it (and the + # repo/token) through env so the Actions engine cannot splice it into the + # shell source, and validate it to a safe charset before use. + env: + MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }} + REPO: ${{ github.repository }} + API_BASE: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + BRANCH: ${{ github.event.pull_request.head.ref }} run: | - BRANCH="${{ github.event.pull_request.head.ref }}" - API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches" + set -euo pipefail + if ! printf '%s' "${BRANCH}" | grep -Eq '^[A-Za-z0-9._/-]+$'; then + echo "::error::unsafe branch name; refusing to proceed"; exit 1 + fi + API="${API_BASE}/api/v1/repos/${REPO}/branches" # URL-encode the branch name's slashes (no PHP dependency on the runner) ENCODED=$(printf '%s' "${BRANCH}" | sed 's|/|%2F|g') STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \ - -H "Authorization: token ${{ secrets.MOKOGIT_TOKEN }}" \ + -H "Authorization: token ${MOKOGIT_TOKEN}" \ "${API}/${ENCODED}" 2>/dev/null || true) if [ "$STATUS" = "204" ]; then -- 2.52.0