diff --git a/.gitignore b/.gitignore
index 726a684..26927d2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -152,7 +152,7 @@ package-lock.json
# PHP / Composer tooling
# ============================================================
vendor/
-!src/media/vendor/
+!source/media/vendor/
composer.lock
*.phar
codeception.phar
diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml
index 97027dd..43d13ca 100644
--- a/.mokogitea/manifest.xml
+++ b/.mokogitea/manifest.xml
@@ -21,6 +21,6 @@
PHP
joomla-extension
- src/
+ source/
diff --git a/.mokogitea/workflows/ci-joomla.yml b/.mokogitea/workflows/ci-joomla.yml
index f679e86..e67987b 100644
--- a/.mokogitea/workflows/ci-joomla.yml
+++ b/.mokogitea/workflows/ci-joomla.yml
@@ -67,7 +67,7 @@ jobs:
- name: PHP syntax check
run: |
ERRORS=0
- for DIR in src/ htdocs/; do
+ for DIR in source/ src/ htdocs/; do
if [ -d "$DIR" ]; then
FOUND=1
while IFS= read -r -d '' FILE; do
@@ -207,7 +207,7 @@ jobs:
echo "### Language Directory Check" >> $GITHUB_STEP_SUMMARY
ERRORS=0
- for DIR in src/ htdocs/; do
+ for DIR in source/ src/ htdocs/; do
[ -d "$DIR" ] || continue
# Find all language directories
while IFS= read -r -d '' LANG_DIR; do
@@ -239,7 +239,7 @@ jobs:
MISSING=0
CHECKED=0
- for DIR in src/ htdocs/; do
+ for DIR in source/ src/ htdocs/; do
if [ -d "$DIR" ]; then
while IFS= read -r -d '' SUBDIR; do
CHECKED=$((CHECKED + 1))
@@ -252,7 +252,7 @@ jobs:
done
if [ "${CHECKED}" -eq 0 ]; then
- echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY
+ echo "No source/, 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
@@ -450,7 +450,7 @@ jobs:
# Determine source directory
SRC_DIR=""
- for DIR in src/ htdocs/ lib/; do
+ for DIR in source/ src/ htdocs/ lib/; do
if [ -d "$DIR" ]; then
SRC_DIR="$DIR"
break
@@ -458,7 +458,7 @@ jobs:
done
if [ -z "$SRC_DIR" ]; then
- echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY
+ echo "No source directory found (source/, src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY
exit 0
fi
diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml
index 4d78d7a..7134fb7 100644
--- a/.mokogitea/workflows/pr-check.yml
+++ b/.mokogitea/workflows/pr-check.yml
@@ -1,508 +1,509 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.CI
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
-# 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
-
- # ── Code Validation ────────────────────────────────────────────────────
- validate:
- name: Validate PR
- runs-on: ubuntu-latest
-
- 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: |
- # Read platform from XML manifest ( tag) or plain text fallback
- PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
- [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
- [ -z "$PLATFORM" ] && PLATFORM="generic"
- echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
-
- - 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 MokoGitea
- RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true)
- if [ -n "$RAW_URLS" ]; then
- echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea 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]
-
- steps:
- - name: Trigger RC pre-release
- env:
- GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
- REPO: ${{ github.repository }}
- BRANCH: ${{ github.head_ref }}
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- run: |
- curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_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
- runs-on: ubuntu-latest
- needs: [branch-policy, validate]
- if: >-
- always() &&
- needs.validate.result == 'failure'
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- sparse-checkout: automation/ci-issue-reporter.sh
- sparse-checkout-cone-mode: false
-
- - name: "File issue for PR validation failure"
- env:
- GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- run: |
- chmod +x automation/ci-issue-reporter.sh
- ./automation/ci-issue-reporter.sh \
- --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."
+# Copyright (C) 2026 Moko Consulting
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# FILE INFORMATION
+# DEFGROUP: Gitea.Workflow
+# INGROUP: moko-platform.CI
+# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
+# 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
+
+ # ── Code Validation ────────────────────────────────────────────────────
+ validate:
+ name: Validate PR
+ runs-on: ubuntu-latest
+
+ 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: |
+ # Read platform from XML manifest ( tag) or plain text fallback
+ PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
+ [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
+ [ -z "$PLATFORM" ] && PLATFORM="generic"
+ echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
+
+ - 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 "*/source/*" -o -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 source/ 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 source/ 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 MokoGitea
+ RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true)
+ if [ -n "$RAW_URLS" ]; then
+ echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea 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="source"
+ [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="src"
+ [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
+ if [ ! -d "$SOURCE_DIR" ]; then
+ echo "::warning::No source/, 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]
+
+ steps:
+ - name: Trigger RC pre-release
+ env:
+ GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
+ REPO: ${{ github.repository }}
+ BRANCH: ${{ github.head_ref }}
+ GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
+ run: |
+ curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_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
+ runs-on: ubuntu-latest
+ needs: [branch-policy, validate]
+ if: >-
+ always() &&
+ needs.validate.result == 'failure'
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ sparse-checkout: automation/ci-issue-reporter.sh
+ sparse-checkout-cone-mode: false
+
+ - name: "File issue for PR validation failure"
+ env:
+ GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
+ GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
+ run: |
+ chmod +x automation/ci-issue-reporter.sh
+ ./automation/ci-issue-reporter.sh \
+ --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."
diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml
index 8d57aaf..0c5d5bf 100644
--- a/.mokogitea/workflows/repo-health.yml
+++ b/.mokogitea/workflows/repo-health.yml
@@ -1,711 +1,713 @@
-# ============================================================================
-# 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: Gitea.Workflow
-# INGROUP: moko-platform.Validation
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
-# 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:
- push:
-
-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,.mokogitea/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: .mokogitea/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.MOKOGITEA_TOKEN || secrets.MOKOGITEA_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|gitea-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}}"
- 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 MokoGitea |'
- 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-issues:
- name: "Report Issues"
- runs-on: ubuntu-latest
- needs: [access_check, scripts_governance, repo_health]
- if: >-
- always() &&
- (needs.scripts_governance.result == 'failure' ||
- needs.repo_health.result == 'failure')
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- sparse-checkout: automation/ci-issue-reporter.sh
- sparse-checkout-cone-mode: false
-
- - name: "File issues for failed gates"
- env:
- GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- run: |
- chmod +x automation/ci-issue-reporter.sh
- REPORTER="./automation/ci-issue-reporter.sh"
- WF="Repo Health"
-
- report_gate() {
- local gate="$1" result="$2" details="$3"
- if [ "$result" = "failure" ]; then
- "$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error
- fi
- }
-
- report_gate "Scripts Governance" \
- "${{ needs.scripts_governance.result }}" \
- "Scripts directory policy violations detected. Review required and allowed directories."
-
- report_gate "Repository Health" \
- "${{ needs.repo_health.result }}" \
- "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
+# ============================================================================
+# 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: Gitea.Workflow
+# INGROUP: moko-platform.Validation
+# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
+# 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:
+ push:
+
+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,.mokogitea/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: .mokogitea/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.MOKOGITEA_TOKEN || secrets.MOKOGITEA_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|gitea-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}}"
+ 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: source/, src/, or htdocs/ (any is valid for extension repos)
+ SOURCE_DIR=""
+ if [ -d "source" ]; then
+ SOURCE_DIR="source"
+ elif [ -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 source/
+ SOURCE_DIR=""
+ else
+ missing_required+=("source/, 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 MokoGitea |'
+ 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-issues:
+ name: "Report Issues"
+ runs-on: ubuntu-latest
+ needs: [access_check, scripts_governance, repo_health]
+ if: >-
+ always() &&
+ (needs.scripts_governance.result == 'failure' ||
+ needs.repo_health.result == 'failure')
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ sparse-checkout: automation/ci-issue-reporter.sh
+ sparse-checkout-cone-mode: false
+
+ - name: "File issues for failed gates"
+ env:
+ GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
+ GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
+ run: |
+ chmod +x automation/ci-issue-reporter.sh
+ REPORTER="./automation/ci-issue-reporter.sh"
+ WF="Repo Health"
+
+ report_gate() {
+ local gate="$1" result="$2" details="$3"
+ if [ "$result" = "failure" ]; then
+ "$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error
+ fi
+ }
+
+ report_gate "Scripts Governance" \
+ "${{ needs.scripts_governance.result }}" \
+ "Scripts directory policy violations detected. Review required and allowed directories."
+
+ report_gate "Repository Health" \
+ "${{ needs.repo_health.result }}" \
+ "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
diff --git a/src/index.html b/source/index.html
similarity index 100%
rename from src/index.html
rename to source/index.html
diff --git a/src/language/en-GB/index.html b/source/language/en-GB/index.html
similarity index 100%
rename from src/language/en-GB/index.html
rename to source/language/en-GB/index.html
diff --git a/src/language/en-GB/pkg_mokoog.sys.ini b/source/language/en-GB/pkg_mokoog.sys.ini
similarity index 100%
rename from src/language/en-GB/pkg_mokoog.sys.ini
rename to source/language/en-GB/pkg_mokoog.sys.ini
diff --git a/src/language/en-US/index.html b/source/language/en-US/index.html
similarity index 100%
rename from src/language/en-US/index.html
rename to source/language/en-US/index.html
diff --git a/src/language/en-US/pkg_mokoog.sys.ini b/source/language/en-US/pkg_mokoog.sys.ini
similarity index 100%
rename from src/language/en-US/pkg_mokoog.sys.ini
rename to source/language/en-US/pkg_mokoog.sys.ini
diff --git a/src/language/index.html b/source/language/index.html
similarity index 100%
rename from src/language/index.html
rename to source/language/index.html
diff --git a/src/packages/com_mokoog/api/index.html b/source/packages/com_mokoog/api/index.html
similarity index 100%
rename from src/packages/com_mokoog/api/index.html
rename to source/packages/com_mokoog/api/index.html
diff --git a/src/packages/com_mokoog/api/src/Controller/TagsController.php b/source/packages/com_mokoog/api/src/Controller/TagsController.php
similarity index 100%
rename from src/packages/com_mokoog/api/src/Controller/TagsController.php
rename to source/packages/com_mokoog/api/src/Controller/TagsController.php
diff --git a/src/packages/com_mokoog/api/src/Controller/index.html b/source/packages/com_mokoog/api/src/Controller/index.html
similarity index 100%
rename from src/packages/com_mokoog/api/src/Controller/index.html
rename to source/packages/com_mokoog/api/src/Controller/index.html
diff --git a/src/packages/com_mokoog/api/src/View/Tags/JsonapiView.php b/source/packages/com_mokoog/api/src/View/Tags/JsonapiView.php
similarity index 100%
rename from src/packages/com_mokoog/api/src/View/Tags/JsonapiView.php
rename to source/packages/com_mokoog/api/src/View/Tags/JsonapiView.php
diff --git a/src/packages/com_mokoog/api/src/View/Tags/index.html b/source/packages/com_mokoog/api/src/View/Tags/index.html
similarity index 100%
rename from src/packages/com_mokoog/api/src/View/Tags/index.html
rename to source/packages/com_mokoog/api/src/View/Tags/index.html
diff --git a/src/packages/com_mokoog/api/src/View/index.html b/source/packages/com_mokoog/api/src/View/index.html
similarity index 100%
rename from src/packages/com_mokoog/api/src/View/index.html
rename to source/packages/com_mokoog/api/src/View/index.html
diff --git a/src/packages/com_mokoog/api/src/index.html b/source/packages/com_mokoog/api/src/index.html
similarity index 100%
rename from src/packages/com_mokoog/api/src/index.html
rename to source/packages/com_mokoog/api/src/index.html
diff --git a/src/packages/com_mokoog/forms/filter_tags.xml b/source/packages/com_mokoog/forms/filter_tags.xml
similarity index 100%
rename from src/packages/com_mokoog/forms/filter_tags.xml
rename to source/packages/com_mokoog/forms/filter_tags.xml
diff --git a/src/packages/com_mokoog/forms/index.html b/source/packages/com_mokoog/forms/index.html
similarity index 100%
rename from src/packages/com_mokoog/forms/index.html
rename to source/packages/com_mokoog/forms/index.html
diff --git a/src/packages/com_mokoog/forms/tag.xml b/source/packages/com_mokoog/forms/tag.xml
similarity index 100%
rename from src/packages/com_mokoog/forms/tag.xml
rename to source/packages/com_mokoog/forms/tag.xml
diff --git a/src/packages/com_mokoog/index.html b/source/packages/com_mokoog/index.html
similarity index 100%
rename from src/packages/com_mokoog/index.html
rename to source/packages/com_mokoog/index.html
diff --git a/src/packages/com_mokoog/language/en-GB/com_mokoog.ini b/source/packages/com_mokoog/language/en-GB/com_mokoog.ini
similarity index 100%
rename from src/packages/com_mokoog/language/en-GB/com_mokoog.ini
rename to source/packages/com_mokoog/language/en-GB/com_mokoog.ini
diff --git a/src/packages/com_mokoog/language/en-GB/com_mokoog.sys.ini b/source/packages/com_mokoog/language/en-GB/com_mokoog.sys.ini
similarity index 100%
rename from src/packages/com_mokoog/language/en-GB/com_mokoog.sys.ini
rename to source/packages/com_mokoog/language/en-GB/com_mokoog.sys.ini
diff --git a/src/packages/com_mokoog/language/en-GB/index.html b/source/packages/com_mokoog/language/en-GB/index.html
similarity index 100%
rename from src/packages/com_mokoog/language/en-GB/index.html
rename to source/packages/com_mokoog/language/en-GB/index.html
diff --git a/src/packages/com_mokoog/language/en-US/com_mokoog.ini b/source/packages/com_mokoog/language/en-US/com_mokoog.ini
similarity index 100%
rename from src/packages/com_mokoog/language/en-US/com_mokoog.ini
rename to source/packages/com_mokoog/language/en-US/com_mokoog.ini
diff --git a/src/packages/com_mokoog/language/en-US/com_mokoog.sys.ini b/source/packages/com_mokoog/language/en-US/com_mokoog.sys.ini
similarity index 100%
rename from src/packages/com_mokoog/language/en-US/com_mokoog.sys.ini
rename to source/packages/com_mokoog/language/en-US/com_mokoog.sys.ini
diff --git a/src/packages/com_mokoog/language/en-US/index.html b/source/packages/com_mokoog/language/en-US/index.html
similarity index 100%
rename from src/packages/com_mokoog/language/en-US/index.html
rename to source/packages/com_mokoog/language/en-US/index.html
diff --git a/src/packages/com_mokoog/language/index.html b/source/packages/com_mokoog/language/index.html
similarity index 100%
rename from src/packages/com_mokoog/language/index.html
rename to source/packages/com_mokoog/language/index.html
diff --git a/src/packages/com_mokoog/mokoog.xml b/source/packages/com_mokoog/mokoog.xml
similarity index 100%
rename from src/packages/com_mokoog/mokoog.xml
rename to source/packages/com_mokoog/mokoog.xml
diff --git a/src/packages/com_mokoog/script.php b/source/packages/com_mokoog/script.php
similarity index 100%
rename from src/packages/com_mokoog/script.php
rename to source/packages/com_mokoog/script.php
diff --git a/src/packages/com_mokoog/services/index.html b/source/packages/com_mokoog/services/index.html
similarity index 100%
rename from src/packages/com_mokoog/services/index.html
rename to source/packages/com_mokoog/services/index.html
diff --git a/src/packages/com_mokoog/services/provider.php b/source/packages/com_mokoog/services/provider.php
similarity index 100%
rename from src/packages/com_mokoog/services/provider.php
rename to source/packages/com_mokoog/services/provider.php
diff --git a/src/packages/com_mokoog/sql/index.html b/source/packages/com_mokoog/sql/index.html
similarity index 100%
rename from src/packages/com_mokoog/sql/index.html
rename to source/packages/com_mokoog/sql/index.html
diff --git a/src/packages/com_mokoog/sql/install.mysql.sql b/source/packages/com_mokoog/sql/install.mysql.sql
similarity index 100%
rename from src/packages/com_mokoog/sql/install.mysql.sql
rename to source/packages/com_mokoog/sql/install.mysql.sql
diff --git a/src/packages/com_mokoog/sql/uninstall.mysql.sql b/source/packages/com_mokoog/sql/uninstall.mysql.sql
similarity index 100%
rename from src/packages/com_mokoog/sql/uninstall.mysql.sql
rename to source/packages/com_mokoog/sql/uninstall.mysql.sql
diff --git a/src/packages/com_mokoog/sql/updates/index.html b/source/packages/com_mokoog/sql/updates/index.html
similarity index 100%
rename from src/packages/com_mokoog/sql/updates/index.html
rename to source/packages/com_mokoog/sql/updates/index.html
diff --git a/src/packages/com_mokoog/sql/updates/mysql/01.00.00.sql b/source/packages/com_mokoog/sql/updates/mysql/01.00.00.sql
similarity index 100%
rename from src/packages/com_mokoog/sql/updates/mysql/01.00.00.sql
rename to source/packages/com_mokoog/sql/updates/mysql/01.00.00.sql
diff --git a/src/packages/com_mokoog/sql/updates/mysql/01.01.00.sql b/source/packages/com_mokoog/sql/updates/mysql/01.01.00.sql
similarity index 100%
rename from src/packages/com_mokoog/sql/updates/mysql/01.01.00.sql
rename to source/packages/com_mokoog/sql/updates/mysql/01.01.00.sql
diff --git a/src/packages/com_mokoog/sql/updates/mysql/01.02.00.sql b/source/packages/com_mokoog/sql/updates/mysql/01.02.00.sql
similarity index 100%
rename from src/packages/com_mokoog/sql/updates/mysql/01.02.00.sql
rename to source/packages/com_mokoog/sql/updates/mysql/01.02.00.sql
diff --git a/src/packages/com_mokoog/sql/updates/mysql/index.html b/source/packages/com_mokoog/sql/updates/mysql/index.html
similarity index 100%
rename from src/packages/com_mokoog/sql/updates/mysql/index.html
rename to source/packages/com_mokoog/sql/updates/mysql/index.html
diff --git a/src/packages/com_mokoog/src/ContentType/ContentTypeInterface.php b/source/packages/com_mokoog/src/ContentType/ContentTypeInterface.php
similarity index 100%
rename from src/packages/com_mokoog/src/ContentType/ContentTypeInterface.php
rename to source/packages/com_mokoog/src/ContentType/ContentTypeInterface.php
diff --git a/src/packages/com_mokoog/src/ContentType/HikaShopAdapter.php b/source/packages/com_mokoog/src/ContentType/HikaShopAdapter.php
similarity index 100%
rename from src/packages/com_mokoog/src/ContentType/HikaShopAdapter.php
rename to source/packages/com_mokoog/src/ContentType/HikaShopAdapter.php
diff --git a/src/packages/com_mokoog/src/ContentType/K2Adapter.php b/source/packages/com_mokoog/src/ContentType/K2Adapter.php
similarity index 100%
rename from src/packages/com_mokoog/src/ContentType/K2Adapter.php
rename to source/packages/com_mokoog/src/ContentType/K2Adapter.php
diff --git a/src/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php b/source/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php
similarity index 100%
rename from src/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php
rename to source/packages/com_mokoog/src/ContentType/VirtueMartAdapter.php
diff --git a/src/packages/com_mokoog/src/ContentType/index.html b/source/packages/com_mokoog/src/ContentType/index.html
similarity index 100%
rename from src/packages/com_mokoog/src/ContentType/index.html
rename to source/packages/com_mokoog/src/ContentType/index.html
diff --git a/src/packages/com_mokoog/src/Controller/BatchController.php b/source/packages/com_mokoog/src/Controller/BatchController.php
similarity index 100%
rename from src/packages/com_mokoog/src/Controller/BatchController.php
rename to source/packages/com_mokoog/src/Controller/BatchController.php
diff --git a/src/packages/com_mokoog/src/Controller/DisplayController.php b/source/packages/com_mokoog/src/Controller/DisplayController.php
similarity index 100%
rename from src/packages/com_mokoog/src/Controller/DisplayController.php
rename to source/packages/com_mokoog/src/Controller/DisplayController.php
diff --git a/src/packages/com_mokoog/src/Controller/ImportExportController.php b/source/packages/com_mokoog/src/Controller/ImportExportController.php
similarity index 100%
rename from src/packages/com_mokoog/src/Controller/ImportExportController.php
rename to source/packages/com_mokoog/src/Controller/ImportExportController.php
diff --git a/src/packages/com_mokoog/src/Controller/index.html b/source/packages/com_mokoog/src/Controller/index.html
similarity index 100%
rename from src/packages/com_mokoog/src/Controller/index.html
rename to source/packages/com_mokoog/src/Controller/index.html
diff --git a/src/packages/com_mokoog/src/Extension/MokoOGComponent.php b/source/packages/com_mokoog/src/Extension/MokoOGComponent.php
similarity index 100%
rename from src/packages/com_mokoog/src/Extension/MokoOGComponent.php
rename to source/packages/com_mokoog/src/Extension/MokoOGComponent.php
diff --git a/src/packages/com_mokoog/src/Extension/index.html b/source/packages/com_mokoog/src/Extension/index.html
similarity index 100%
rename from src/packages/com_mokoog/src/Extension/index.html
rename to source/packages/com_mokoog/src/Extension/index.html
diff --git a/src/packages/com_mokoog/src/Field/index.html b/source/packages/com_mokoog/src/Field/index.html
similarity index 100%
rename from src/packages/com_mokoog/src/Field/index.html
rename to source/packages/com_mokoog/src/Field/index.html
diff --git a/src/packages/com_mokoog/src/Model/TagModel.php b/source/packages/com_mokoog/src/Model/TagModel.php
similarity index 100%
rename from src/packages/com_mokoog/src/Model/TagModel.php
rename to source/packages/com_mokoog/src/Model/TagModel.php
diff --git a/src/packages/com_mokoog/src/Model/TagsModel.php b/source/packages/com_mokoog/src/Model/TagsModel.php
similarity index 100%
rename from src/packages/com_mokoog/src/Model/TagsModel.php
rename to source/packages/com_mokoog/src/Model/TagsModel.php
diff --git a/src/packages/com_mokoog/src/Model/index.html b/source/packages/com_mokoog/src/Model/index.html
similarity index 100%
rename from src/packages/com_mokoog/src/Model/index.html
rename to source/packages/com_mokoog/src/Model/index.html
diff --git a/src/packages/com_mokoog/src/Service/index.html b/source/packages/com_mokoog/src/Service/index.html
similarity index 100%
rename from src/packages/com_mokoog/src/Service/index.html
rename to source/packages/com_mokoog/src/Service/index.html
diff --git a/src/packages/com_mokoog/src/Table/TagTable.php b/source/packages/com_mokoog/src/Table/TagTable.php
similarity index 100%
rename from src/packages/com_mokoog/src/Table/TagTable.php
rename to source/packages/com_mokoog/src/Table/TagTable.php
diff --git a/src/packages/com_mokoog/src/Table/index.html b/source/packages/com_mokoog/src/Table/index.html
similarity index 100%
rename from src/packages/com_mokoog/src/Table/index.html
rename to source/packages/com_mokoog/src/Table/index.html
diff --git a/src/packages/com_mokoog/src/View/Tags/HtmlView.php b/source/packages/com_mokoog/src/View/Tags/HtmlView.php
similarity index 100%
rename from src/packages/com_mokoog/src/View/Tags/HtmlView.php
rename to source/packages/com_mokoog/src/View/Tags/HtmlView.php
diff --git a/src/packages/com_mokoog/src/View/Tags/index.html b/source/packages/com_mokoog/src/View/Tags/index.html
similarity index 100%
rename from src/packages/com_mokoog/src/View/Tags/index.html
rename to source/packages/com_mokoog/src/View/Tags/index.html
diff --git a/src/packages/com_mokoog/src/View/Tags/tmpl/index.html b/source/packages/com_mokoog/src/View/Tags/tmpl/index.html
similarity index 100%
rename from src/packages/com_mokoog/src/View/Tags/tmpl/index.html
rename to source/packages/com_mokoog/src/View/Tags/tmpl/index.html
diff --git a/src/packages/com_mokoog/src/View/index.html b/source/packages/com_mokoog/src/View/index.html
similarity index 100%
rename from src/packages/com_mokoog/src/View/index.html
rename to source/packages/com_mokoog/src/View/index.html
diff --git a/src/packages/com_mokoog/src/index.html b/source/packages/com_mokoog/src/index.html
similarity index 100%
rename from src/packages/com_mokoog/src/index.html
rename to source/packages/com_mokoog/src/index.html
diff --git a/src/packages/com_mokoog/tmpl/index.html b/source/packages/com_mokoog/tmpl/index.html
similarity index 100%
rename from src/packages/com_mokoog/tmpl/index.html
rename to source/packages/com_mokoog/tmpl/index.html
diff --git a/src/packages/com_mokoog/tmpl/tags/default.php b/source/packages/com_mokoog/tmpl/tags/default.php
similarity index 100%
rename from src/packages/com_mokoog/tmpl/tags/default.php
rename to source/packages/com_mokoog/tmpl/tags/default.php
diff --git a/src/packages/com_mokoog/tmpl/tags/index.html b/source/packages/com_mokoog/tmpl/tags/index.html
similarity index 100%
rename from src/packages/com_mokoog/tmpl/tags/index.html
rename to source/packages/com_mokoog/tmpl/tags/index.html
diff --git a/src/packages/index.html b/source/packages/index.html
similarity index 100%
rename from src/packages/index.html
rename to source/packages/index.html
diff --git a/src/packages/plg_content_mokoog/forms/index.html b/source/packages/plg_content_mokoog/forms/index.html
similarity index 100%
rename from src/packages/plg_content_mokoog/forms/index.html
rename to source/packages/plg_content_mokoog/forms/index.html
diff --git a/src/packages/plg_content_mokoog/forms/mokoog.xml b/source/packages/plg_content_mokoog/forms/mokoog.xml
similarity index 100%
rename from src/packages/plg_content_mokoog/forms/mokoog.xml
rename to source/packages/plg_content_mokoog/forms/mokoog.xml
diff --git a/src/packages/plg_content_mokoog/index.html b/source/packages/plg_content_mokoog/index.html
similarity index 100%
rename from src/packages/plg_content_mokoog/index.html
rename to source/packages/plg_content_mokoog/index.html
diff --git a/src/packages/plg_content_mokoog/language/en-GB/index.html b/source/packages/plg_content_mokoog/language/en-GB/index.html
similarity index 100%
rename from src/packages/plg_content_mokoog/language/en-GB/index.html
rename to source/packages/plg_content_mokoog/language/en-GB/index.html
diff --git a/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini b/source/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini
similarity index 100%
rename from src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini
rename to source/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.ini
diff --git a/src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.sys.ini b/source/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.sys.ini
similarity index 100%
rename from src/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.sys.ini
rename to source/packages/plg_content_mokoog/language/en-GB/plg_content_mokoog.sys.ini
diff --git a/src/packages/plg_content_mokoog/language/en-US/index.html b/source/packages/plg_content_mokoog/language/en-US/index.html
similarity index 100%
rename from src/packages/plg_content_mokoog/language/en-US/index.html
rename to source/packages/plg_content_mokoog/language/en-US/index.html
diff --git a/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini b/source/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini
similarity index 100%
rename from src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini
rename to source/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.ini
diff --git a/src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.sys.ini b/source/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.sys.ini
similarity index 100%
rename from src/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.sys.ini
rename to source/packages/plg_content_mokoog/language/en-US/plg_content_mokoog.sys.ini
diff --git a/src/packages/plg_content_mokoog/language/index.html b/source/packages/plg_content_mokoog/language/index.html
similarity index 100%
rename from src/packages/plg_content_mokoog/language/index.html
rename to source/packages/plg_content_mokoog/language/index.html
diff --git a/src/packages/plg_content_mokoog/media/css/index.html b/source/packages/plg_content_mokoog/media/css/index.html
similarity index 100%
rename from src/packages/plg_content_mokoog/media/css/index.html
rename to source/packages/plg_content_mokoog/media/css/index.html
diff --git a/src/packages/plg_content_mokoog/media/css/preview.css b/source/packages/plg_content_mokoog/media/css/preview.css
similarity index 100%
rename from src/packages/plg_content_mokoog/media/css/preview.css
rename to source/packages/plg_content_mokoog/media/css/preview.css
diff --git a/src/packages/plg_content_mokoog/media/index.html b/source/packages/plg_content_mokoog/media/index.html
similarity index 100%
rename from src/packages/plg_content_mokoog/media/index.html
rename to source/packages/plg_content_mokoog/media/index.html
diff --git a/src/packages/plg_content_mokoog/media/joomla.asset.json b/source/packages/plg_content_mokoog/media/joomla.asset.json
similarity index 100%
rename from src/packages/plg_content_mokoog/media/joomla.asset.json
rename to source/packages/plg_content_mokoog/media/joomla.asset.json
diff --git a/src/packages/plg_content_mokoog/media/js/index.html b/source/packages/plg_content_mokoog/media/js/index.html
similarity index 100%
rename from src/packages/plg_content_mokoog/media/js/index.html
rename to source/packages/plg_content_mokoog/media/js/index.html
diff --git a/src/packages/plg_content_mokoog/media/js/preview.js b/source/packages/plg_content_mokoog/media/js/preview.js
similarity index 100%
rename from src/packages/plg_content_mokoog/media/js/preview.js
rename to source/packages/plg_content_mokoog/media/js/preview.js
diff --git a/src/packages/plg_content_mokoog/mokoog.php b/source/packages/plg_content_mokoog/mokoog.php
similarity index 100%
rename from src/packages/plg_content_mokoog/mokoog.php
rename to source/packages/plg_content_mokoog/mokoog.php
diff --git a/src/packages/plg_content_mokoog/mokoog.xml b/source/packages/plg_content_mokoog/mokoog.xml
similarity index 100%
rename from src/packages/plg_content_mokoog/mokoog.xml
rename to source/packages/plg_content_mokoog/mokoog.xml
diff --git a/src/packages/plg_content_mokoog/services/index.html b/source/packages/plg_content_mokoog/services/index.html
similarity index 100%
rename from src/packages/plg_content_mokoog/services/index.html
rename to source/packages/plg_content_mokoog/services/index.html
diff --git a/src/packages/plg_content_mokoog/services/provider.php b/source/packages/plg_content_mokoog/services/provider.php
similarity index 100%
rename from src/packages/plg_content_mokoog/services/provider.php
rename to source/packages/plg_content_mokoog/services/provider.php
diff --git a/src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php b/source/packages/plg_content_mokoog/src/Extension/MokoOGContent.php
similarity index 100%
rename from src/packages/plg_content_mokoog/src/Extension/MokoOGContent.php
rename to source/packages/plg_content_mokoog/src/Extension/MokoOGContent.php
diff --git a/src/packages/plg_content_mokoog/src/Extension/index.html b/source/packages/plg_content_mokoog/src/Extension/index.html
similarity index 100%
rename from src/packages/plg_content_mokoog/src/Extension/index.html
rename to source/packages/plg_content_mokoog/src/Extension/index.html
diff --git a/src/packages/plg_content_mokoog/src/Field/index.html b/source/packages/plg_content_mokoog/src/Field/index.html
similarity index 100%
rename from src/packages/plg_content_mokoog/src/Field/index.html
rename to source/packages/plg_content_mokoog/src/Field/index.html
diff --git a/src/packages/plg_content_mokoog/src/index.html b/source/packages/plg_content_mokoog/src/index.html
similarity index 100%
rename from src/packages/plg_content_mokoog/src/index.html
rename to source/packages/plg_content_mokoog/src/index.html
diff --git a/src/packages/plg_system_mokoog/index.html b/source/packages/plg_system_mokoog/index.html
similarity index 100%
rename from src/packages/plg_system_mokoog/index.html
rename to source/packages/plg_system_mokoog/index.html
diff --git a/src/packages/plg_system_mokoog/language/en-GB/index.html b/source/packages/plg_system_mokoog/language/en-GB/index.html
similarity index 100%
rename from src/packages/plg_system_mokoog/language/en-GB/index.html
rename to source/packages/plg_system_mokoog/language/en-GB/index.html
diff --git a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini b/source/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini
similarity index 100%
rename from src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini
rename to source/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.ini
diff --git a/src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.sys.ini b/source/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.sys.ini
similarity index 100%
rename from src/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.sys.ini
rename to source/packages/plg_system_mokoog/language/en-GB/plg_system_mokoog.sys.ini
diff --git a/src/packages/plg_system_mokoog/language/en-US/index.html b/source/packages/plg_system_mokoog/language/en-US/index.html
similarity index 100%
rename from src/packages/plg_system_mokoog/language/en-US/index.html
rename to source/packages/plg_system_mokoog/language/en-US/index.html
diff --git a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini b/source/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini
similarity index 100%
rename from src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini
rename to source/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.ini
diff --git a/src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.sys.ini b/source/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.sys.ini
similarity index 100%
rename from src/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.sys.ini
rename to source/packages/plg_system_mokoog/language/en-US/plg_system_mokoog.sys.ini
diff --git a/src/packages/plg_system_mokoog/language/index.html b/source/packages/plg_system_mokoog/language/index.html
similarity index 100%
rename from src/packages/plg_system_mokoog/language/index.html
rename to source/packages/plg_system_mokoog/language/index.html
diff --git a/src/packages/plg_system_mokoog/mokoog.php b/source/packages/plg_system_mokoog/mokoog.php
similarity index 100%
rename from src/packages/plg_system_mokoog/mokoog.php
rename to source/packages/plg_system_mokoog/mokoog.php
diff --git a/src/packages/plg_system_mokoog/mokoog.xml b/source/packages/plg_system_mokoog/mokoog.xml
similarity index 100%
rename from src/packages/plg_system_mokoog/mokoog.xml
rename to source/packages/plg_system_mokoog/mokoog.xml
diff --git a/src/packages/plg_system_mokoog/services/index.html b/source/packages/plg_system_mokoog/services/index.html
similarity index 100%
rename from src/packages/plg_system_mokoog/services/index.html
rename to source/packages/plg_system_mokoog/services/index.html
diff --git a/src/packages/plg_system_mokoog/services/provider.php b/source/packages/plg_system_mokoog/services/provider.php
similarity index 100%
rename from src/packages/plg_system_mokoog/services/provider.php
rename to source/packages/plg_system_mokoog/services/provider.php
diff --git a/src/packages/plg_system_mokoog/src/Extension/MokoOG.php b/source/packages/plg_system_mokoog/src/Extension/MokoOG.php
similarity index 100%
rename from src/packages/plg_system_mokoog/src/Extension/MokoOG.php
rename to source/packages/plg_system_mokoog/src/Extension/MokoOG.php
diff --git a/src/packages/plg_system_mokoog/src/Extension/index.html b/source/packages/plg_system_mokoog/src/Extension/index.html
similarity index 100%
rename from src/packages/plg_system_mokoog/src/Extension/index.html
rename to source/packages/plg_system_mokoog/src/Extension/index.html
diff --git a/src/packages/plg_system_mokoog/src/Helper/ImageGenerator.php b/source/packages/plg_system_mokoog/src/Helper/ImageGenerator.php
similarity index 100%
rename from src/packages/plg_system_mokoog/src/Helper/ImageGenerator.php
rename to source/packages/plg_system_mokoog/src/Helper/ImageGenerator.php
diff --git a/src/packages/plg_system_mokoog/src/Helper/ImageHelper.php b/source/packages/plg_system_mokoog/src/Helper/ImageHelper.php
similarity index 100%
rename from src/packages/plg_system_mokoog/src/Helper/ImageHelper.php
rename to source/packages/plg_system_mokoog/src/Helper/ImageHelper.php
diff --git a/src/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php b/source/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php
similarity index 100%
rename from src/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php
rename to source/packages/plg_system_mokoog/src/Helper/JsonLdBuilder.php
diff --git a/src/packages/plg_system_mokoog/src/Helper/index.html b/source/packages/plg_system_mokoog/src/Helper/index.html
similarity index 100%
rename from src/packages/plg_system_mokoog/src/Helper/index.html
rename to source/packages/plg_system_mokoog/src/Helper/index.html
diff --git a/src/packages/plg_system_mokoog/src/index.html b/source/packages/plg_system_mokoog/src/index.html
similarity index 100%
rename from src/packages/plg_system_mokoog/src/index.html
rename to source/packages/plg_system_mokoog/src/index.html
diff --git a/src/packages/plg_webservices_mokoog/index.html b/source/packages/plg_webservices_mokoog/index.html
similarity index 100%
rename from src/packages/plg_webservices_mokoog/index.html
rename to source/packages/plg_webservices_mokoog/index.html
diff --git a/src/packages/plg_webservices_mokoog/language/en-GB/index.html b/source/packages/plg_webservices_mokoog/language/en-GB/index.html
similarity index 100%
rename from src/packages/plg_webservices_mokoog/language/en-GB/index.html
rename to source/packages/plg_webservices_mokoog/language/en-GB/index.html
diff --git a/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.ini b/source/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.ini
similarity index 100%
rename from src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.ini
rename to source/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.ini
diff --git a/src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.sys.ini b/source/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.sys.ini
similarity index 100%
rename from src/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.sys.ini
rename to source/packages/plg_webservices_mokoog/language/en-GB/plg_webservices_mokoog.sys.ini
diff --git a/src/packages/plg_webservices_mokoog/language/en-US/index.html b/source/packages/plg_webservices_mokoog/language/en-US/index.html
similarity index 100%
rename from src/packages/plg_webservices_mokoog/language/en-US/index.html
rename to source/packages/plg_webservices_mokoog/language/en-US/index.html
diff --git a/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.ini b/source/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.ini
similarity index 100%
rename from src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.ini
rename to source/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.ini
diff --git a/src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.sys.ini b/source/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.sys.ini
similarity index 100%
rename from src/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.sys.ini
rename to source/packages/plg_webservices_mokoog/language/en-US/plg_webservices_mokoog.sys.ini
diff --git a/src/packages/plg_webservices_mokoog/language/index.html b/source/packages/plg_webservices_mokoog/language/index.html
similarity index 100%
rename from src/packages/plg_webservices_mokoog/language/index.html
rename to source/packages/plg_webservices_mokoog/language/index.html
diff --git a/src/packages/plg_webservices_mokoog/mokoog.php b/source/packages/plg_webservices_mokoog/mokoog.php
similarity index 100%
rename from src/packages/plg_webservices_mokoog/mokoog.php
rename to source/packages/plg_webservices_mokoog/mokoog.php
diff --git a/src/packages/plg_webservices_mokoog/mokoog.xml b/source/packages/plg_webservices_mokoog/mokoog.xml
similarity index 100%
rename from src/packages/plg_webservices_mokoog/mokoog.xml
rename to source/packages/plg_webservices_mokoog/mokoog.xml
diff --git a/src/packages/plg_webservices_mokoog/services/index.html b/source/packages/plg_webservices_mokoog/services/index.html
similarity index 100%
rename from src/packages/plg_webservices_mokoog/services/index.html
rename to source/packages/plg_webservices_mokoog/services/index.html
diff --git a/src/packages/plg_webservices_mokoog/services/provider.php b/source/packages/plg_webservices_mokoog/services/provider.php
similarity index 100%
rename from src/packages/plg_webservices_mokoog/services/provider.php
rename to source/packages/plg_webservices_mokoog/services/provider.php
diff --git a/src/packages/plg_webservices_mokoog/src/Extension/MokoOGWebServices.php b/source/packages/plg_webservices_mokoog/src/Extension/MokoOGWebServices.php
similarity index 100%
rename from src/packages/plg_webservices_mokoog/src/Extension/MokoOGWebServices.php
rename to source/packages/plg_webservices_mokoog/src/Extension/MokoOGWebServices.php
diff --git a/src/packages/plg_webservices_mokoog/src/Extension/index.html b/source/packages/plg_webservices_mokoog/src/Extension/index.html
similarity index 100%
rename from src/packages/plg_webservices_mokoog/src/Extension/index.html
rename to source/packages/plg_webservices_mokoog/src/Extension/index.html
diff --git a/src/packages/plg_webservices_mokoog/src/index.html b/source/packages/plg_webservices_mokoog/src/index.html
similarity index 100%
rename from src/packages/plg_webservices_mokoog/src/index.html
rename to source/packages/plg_webservices_mokoog/src/index.html
diff --git a/src/pkg_mokoog.xml b/source/pkg_mokoog.xml
similarity index 100%
rename from src/pkg_mokoog.xml
rename to source/pkg_mokoog.xml
diff --git a/src/script.php b/source/script.php
similarity index 100%
rename from src/script.php
rename to source/script.php