From 2242a611973d188cd6ef1d8325b6bb99513a06e2 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 2 Jun 2026 19:40:38 +0000 Subject: [PATCH 01/21] chore: sync updates.xml 02.18.00 from main [skip ci] --- updates.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/updates.xml b/updates.xml index 6192e76..e3fa419 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ @@ -31,13 +31,13 @@ mokoonyx template site - 02.17.00 + 02.18.00 2026-06-02 https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/stable - https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/stable/tpl_mokoonyx-02.17.00.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/stable/tpl_mokoonyx-02.18.00.zip - 648aa83390654b244bd5f5f1dfd91c34e61637c7b54b5c52858bb0337ceea62e + fcaab94bc82d9e9e11b609dce5e5a9fc21e4f394088e05effb3e004f9719dfd0 stable https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/CHANGELOG.md Moko Consulting -- 2.52.0 From 1859dd728cb25faad5679332a67ab604eabd3cdd Mon Sep 17 00:00:00 2001 From: Moko Consulting Date: Tue, 2 Jun 2026 20:36:50 +0000 Subject: [PATCH 02/21] chore(ci): add CI issue reporter for auto-filing gate failures --- .mokogitea/workflows/repo-health.yml | 1586 +++++++++++++------------- 1 file changed, 817 insertions(+), 769 deletions(-) diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index be52e37..b23d971 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -1,769 +1,817 @@ -# ============================================================================ -# 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: 04.06.00 -# BRIEF: Enforces repository guardrails by validating release configuration, 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, release, scripts, or repo' - required: true - default: all - type: choice - options: - - all - - release - - scripts - - repo - pull_request: - push: - -permissions: - contents: read - -env: - # Release policy - Repository Variables Only - RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX - RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX - - # 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 - - release_config: - name: Release configuration - 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: Guardrails release vars - env: - PROFILE_RAW: ${{ github.event.inputs.profile }} - RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }} - DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} - run: | - set -euo pipefail - - profile="${PROFILE_RAW:-all}" - case "${profile}" in - all|release|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then - { - printf '%s\n' '### Release configuration (Repository Variables)' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' 'Status: SKIPPED' - printf '%s\n' 'Reason: profile excludes release validation' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}" - IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}" - - missing=() - missing_optional=() - - for k in "${required[@]}"; do - v="${!k:-}" - [ -z "${v}" ] && missing+=("${k}") - done - - for k in "${optional[@]}"; do - v="${!k:-}" - [ -z "${v}" ] && missing_optional+=("${k}") - done - - { - printf '%s\n' '### Release configuration (Repository Variables)' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' '| Variable | Status |' - printf '%s\n' '|---|---|' - printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |" - printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |" - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - if [ "${#missing_optional[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing optional repository variables' - for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - if [ "${#missing[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing required repository variables' - for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done - printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.' - } >> "${GITHUB_STEP_SUMMARY}" - exit 1 - fi - - { - printf '%s\n' '### Repository variables validation result' - printf '%s\n' 'Status: OK' - printf '%s\n' 'All required repository variables present.' - printf '%s\n' '' - printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - 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|release|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'release' ] || [ "${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|release|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'release' ] || [ "${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 variables | OK | Repository variables validation |' - 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 - +# ============================================================================ +# 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 release configuration, 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, release, scripts, or repo' + required: true + default: all + type: choice + options: + - all + - release + - scripts + - repo + pull_request: + push: + +permissions: + contents: read + +env: + # Release policy - Repository Variables Only + RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX + RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX + + # 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 + + release_config: + name: Release configuration + 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: Guardrails release vars + env: + PROFILE_RAW: ${{ github.event.inputs.profile }} + RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }} + DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} + run: | + set -euo pipefail + + profile="${PROFILE_RAW:-all}" + case "${profile}" in + all|release|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then + { + printf '%s\n' '### Release configuration (Repository Variables)' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes release validation' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}" + IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}" + + missing=() + missing_optional=() + + for k in "${required[@]}"; do + v="${!k:-}" + [ -z "${v}" ] && missing+=("${k}") + done + + for k in "${optional[@]}"; do + v="${!k:-}" + [ -z "${v}" ] && missing_optional+=("${k}") + done + + { + printf '%s\n' '### Release configuration (Repository Variables)' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Variable | Status |' + printf '%s\n' '|---|---|' + printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |" + printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |" + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${#missing_optional[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing optional repository variables' + for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + if [ "${#missing[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing required repository variables' + for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done + printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.' + } >> "${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + { + printf '%s\n' '### Repository variables validation result' + printf '%s\n' 'Status: OK' + printf '%s\n' 'All required repository variables present.' + printf '%s\n' '' + printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + 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|release|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'release' ] || [ "${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|release|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'release' ] || [ "${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 variables | OK | Repository variables validation |' + 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, release_config, scripts_governance, repo_health] + if: >- + always() && + (needs.release_config.result == 'failure' || + 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 "Release Configuration" \ + "${{ needs.release_config.result }}" \ + "Required repository variables are missing (RS_FTP_PATH_SUFFIX). Check repository settings." + + 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." + -- 2.52.0 From eac32646cb840f0c997a9fce123e324aee5a13d0 Mon Sep 17 00:00:00 2001 From: Moko Consulting Date: Tue, 2 Jun 2026 20:36:50 +0000 Subject: [PATCH 03/21] chore(ci): add CI issue reporter for auto-filing gate failures --- .mokogitea/workflows/pr-check.yml | 500 ++++++++++++++++-------------- 1 file changed, 264 insertions(+), 236 deletions(-) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index ce64a27..e2c82ef 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -1,236 +1,264 @@ -# 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: 05.00.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: 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: 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 - 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: 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 +# 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: 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: 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 + 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: 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." -- 2.52.0 From 04ca05ccc3b7fb5d02e37d46f0d788c8dec2e0e0 Mon Sep 17 00:00:00 2001 From: Moko Consulting Date: Tue, 2 Jun 2026 20:36:51 +0000 Subject: [PATCH 04/21] chore(ci): add CI issue reporter for auto-filing gate failures --- automation/ci-issue-reporter.sh | 237 ++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 automation/ci-issue-reporter.sh diff --git a/automation/ci-issue-reporter.sh b/automation/ci-issue-reporter.sh new file mode 100644 index 0000000..65c47ba --- /dev/null +++ b/automation/ci-issue-reporter.sh @@ -0,0 +1,237 @@ +#!/usr/bin/env bash +# ============================================================================ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Automation.CI +# INGROUP: moko-platform.Automation +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /automation/ci-issue-reporter.sh +# VERSION: 09.23.00 +# BRIEF: Creates or updates a Gitea issue when a CI gate fails. +# Deduplicates by searching open issues with the "ci-auto" label +# whose title matches the gate. If a matching issue exists, a comment +# is appended instead of opening a duplicate. +# ============================================================================ + +set -euo pipefail + +# ── Defaults ──────────────────────────────────────────────────────────────── +GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}" +GITEA_TOKEN="${GITEA_TOKEN:-}" +REPO="${GITHUB_REPOSITORY:-}" +RUN_URL="${GITHUB_SERVER_URL:-${GITEA_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}" +LABEL_NAME="ci-auto" +LABEL_COLOR="#e11d48" + +GATE="" +DETAILS="" +SEVERITY="error" +WORKFLOW="" + +# ── Parse arguments ───────────────────────────────────────────────────────── +usage() { + cat </dev/null || echo "000") + + if [[ "$exists" == "200" ]]; then + # Check if label already exists + local found + found=$(curl -sf \ + -H "Authorization: token ${GITEA_TOKEN}" \ + "${API}/labels" 2>/dev/null \ + | grep -o "\"name\":\"${LABEL_NAME}\"" || true) + + if [[ -z "$found" ]]; then + curl -sf -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/labels" \ + -d "{\"name\":\"${LABEL_NAME}\",\"color\":\"${LABEL_COLOR}\",\"description\":\"Auto-created by CI issue reporter\"}" \ + > /dev/null 2>&1 || true + fi + fi +} + +# ── Search for existing open issue ────────────────────────────────────────── +find_existing_issue() { + # URL-encode the gate name for the query + local query + query=$(printf '%s' "[CI] ${GATE}" | sed 's/ /%20/g; s/\[/%5B/g; s/\]/%5D/g') + + local response + response=$(curl -sf \ + -H "Authorization: token ${GITEA_TOKEN}" \ + "${API}/issues?type=issues&state=open&labels=${LABEL_NAME}&q=${query}&limit=5" \ + 2>/dev/null || echo "[]") + + # Extract the first matching issue number + echo "$response" \ + | grep -oP '"number":\s*\K[0-9]+' \ + | head -1 +} + +# ── Build issue body ──────────────────────────────────────────────────────── +build_body() { + local severity_badge + if [[ "$SEVERITY" == "error" ]]; then + severity_badge="**Severity:** Error" + else + severity_badge="**Severity:** Warning" + fi + + cat </dev/null) + + HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/issues/${EXISTING}/comments" \ + -d "${COMMENT_JSON}" 2>/dev/null || echo "000") + + if [[ "$HTTP" == "201" ]]; then + echo "Commented on existing issue #${EXISTING}" + else + echo "WARNING: Failed to comment on issue #${EXISTING} (HTTP ${HTTP})" + fi +else + # Create new issue + ISSUE_BODY=$(build_body) + ISSUE_JSON=$(python3 -c " +import sys, json +body = sys.stdin.read() +print(json.dumps({ + 'title': sys.argv[1], + 'body': body, + 'labels': [] +}))" "$TITLE" <<< "$ISSUE_BODY" 2>/dev/null) + + # Create the issue + RESPONSE=$(curl -sf -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/issues" \ + -d "${ISSUE_JSON}" 2>/dev/null || echo "{}") + + ISSUE_NUM=$(echo "$RESPONSE" | grep -oP '"number":\s*\K[0-9]+' | head -1) + + if [[ -n "$ISSUE_NUM" ]]; then + # Apply label (separate call — more reliable across Gitea versions) + LABEL_ID=$(curl -sf \ + -H "Authorization: token ${GITEA_TOKEN}" \ + "${API}/labels" 2>/dev/null \ + | grep -oP "\"id\":\s*\K[0-9]+(?=[^}]*\"name\":\s*\"${LABEL_NAME}\")" \ + | head -1 || true) + + if [[ -n "$LABEL_ID" ]]; then + curl -sf -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/issues/${ISSUE_NUM}/labels" \ + -d "{\"labels\":[${LABEL_ID}]}" \ + > /dev/null 2>&1 || true + fi + + echo "Created issue #${ISSUE_NUM}: ${TITLE}" + else + echo "WARNING: Failed to create issue" + echo "Response: ${RESPONSE}" + fi +fi -- 2.52.0 From b1fb0d229441f271b0467aff1b59deca484f11bf Mon Sep 17 00:00:00 2001 From: Moko Consulting Date: Tue, 2 Jun 2026 21:32:48 +0000 Subject: [PATCH 05/21] chore(ci): sync CI issue reporter from Template-Joomla -- 2.52.0 From 2aeb4d5eccf8bd3dd0247410afa4ba72344351b4 Mon Sep 17 00:00:00 2001 From: Moko Consulting Date: Tue, 2 Jun 2026 21:32:49 +0000 Subject: [PATCH 06/21] chore(ci): sync CI issue reporter from Template-Joomla -- 2.52.0 From 6d1aaa952b47cf39acbeb0e6d3670e3baa5f21eb Mon Sep 17 00:00:00 2001 From: Moko Consulting Date: Tue, 2 Jun 2026 21:32:51 +0000 Subject: [PATCH 07/21] chore(ci): sync CI issue reporter from Template-Joomla -- 2.52.0 From 61adcb0bc98b374df6354759cb1f00e1650cad1d Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 2 Jun 2026 15:54:18 -0500 Subject: [PATCH 08/21] fix(ci): add conflict-marker guard to release and PR workflows Prevents releases from shipping with unresolved merge conflict markers. Adds a validation step to both auto-release.yml (blocks release) and pr-check.yml (catches at PR time). Also cleans existing conflict markers from templateDetails.xml. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .mokogitea/workflows/auto-release.yml | 13 +++++++++++++ .mokogitea/workflows/pr-check.yml | 13 +++++++++++++ src/templateDetails.xml | 4 ---- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 1227ff8..2325032 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -131,6 +131,19 @@ jobs: git config --local user.name "gitea-actions[bot]" git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + - name: Check for merge conflict markers + run: | + CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) + if [ -n "$CONFLICTS" ]; then + echo "::error::Merge conflict markers found — aborting release" + echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "No conflict markers found" + - name: Setup moko-platform tools env: MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index e2c82ef..0ac0ef1 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -105,6 +105,19 @@ jobs: - 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: | diff --git a/src/templateDetails.xml b/src/templateDetails.xml index 92dc97e..d722327 100644 --- a/src/templateDetails.xml +++ b/src/templateDetails.xml @@ -36,11 +36,7 @@ mokoonyx -<<<<<<< HEAD 02.16.01-dev -======= - 02.16.01-dev ->>>>>>> origin/main script.php 2026-05-16 Jonathan Miller || Moko Consulting -- 2.52.0 From 97466819c5408f1432f39639efd2daef9ea6ba86 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 2 Jun 2026 22:11:08 +0000 Subject: [PATCH 09/21] chore(version): auto-bump 02.16.02-dev [skip ci] --- .mokogitea/manifest.xml | 2 +- .mokogitea/workflows/issue-branch.yml | 8 ++++---- CHANGELOG.md | 16 ++++++++-------- SECURITY.md | 8 ++++---- src/html/com_joomgallery/category/default.php | 8 ++++---- .../com_joomgallery/category/default_cat.php | 8 ++++---- src/html/com_joomgallery/gallery/default.php | 8 ++++---- src/html/com_joomgallery/image/default.php | 8 ++++---- src/html/layouts/joomla/module/card.php | 8 ++++---- src/html/layouts/mokoonyx/article-metadata.php | 8 ++++---- src/media/css/a11y-high-contrast.css | 8 ++++---- src/templateDetails.xml | 2 +- updates.xml | 2 +- 13 files changed, 47 insertions(+), 47 deletions(-) diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index b908b0a..eb9c7e1 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -9,7 +9,7 @@ Template - MokoOnyx MokoConsulting MokoOnyx - Joomla site template (successor to MokoCassiopeia) - 02.16.01 + 02.16.02 GNU General Public License v3 diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 5cafb81..51c1698 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -7,13 +7,13 @@ # INGROUP: moko-platform.Automation <<<<<<< HEAD <<<<<<< HEAD -# VERSION: 02.16.01 +# VERSION: 02.16.02 ======= -# VERSION: 02.16.01 +# VERSION: 02.16.02 ======= -# VERSION: 02.16.01 +# VERSION: 02.16.02 ======= -# VERSION: 02.16.01 +# VERSION: 02.16.02 >>>>>>> origin/main >>>>>>> origin/main # BRIEF: Auto-create feature branch when an issue is opened diff --git a/CHANGELOG.md b/CHANGELOG.md index 5aec8ab..65b3fc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,35 +10,35 @@ PATH: ./CHANGELOG.md <<<<<<< HEAD <<<<<<< HEAD - VERSION: 02.16.01 + VERSION: 02.16.02 BRIEF: Changelog file documenting version history of MokoOnyx --> -# Changelog — MokoOnyx (VERSION: 02.16.01) +# Changelog — MokoOnyx (VERSION: 02.16.02) ======= - VERSION: 02.16.01 + VERSION: 02.16.02 BRIEF: Changelog file documenting version history of MokoOnyx --> -# Changelog — MokoOnyx (VERSION: 02.16.01) +# Changelog — MokoOnyx (VERSION: 02.16.02) >>>>>>> origin/main ======= - VERSION: 02.16.01 + VERSION: 02.16.02 BRIEF: Changelog file documenting version history of MokoOnyx --> -# Changelog — MokoOnyx (VERSION: 02.16.01) +# Changelog — MokoOnyx (VERSION: 02.16.02) >>>>>>> origin/main ## [Unreleased] ## [02.15.00] --- 2026-05-30 ======= - VERSION: 02.16.01 + VERSION: 02.16.02 BRIEF: Changelog file documenting version history of MokoOnyx --> -# Changelog — MokoOnyx (VERSION: 02.16.01) +# Changelog — MokoOnyx (VERSION: 02.16.02) >>>>>>> origin/main ## [02.14.00] --- 2026-05-30 diff --git a/SECURITY.md b/SECURITY.md index ffd8851..0597bb2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,13 +12,13 @@ FILE: SECURITY.md <<<<<<< HEAD <<<<<<< HEAD - VERSION: 02.16.01 + VERSION: 02.16.02 ======= - VERSION: 02.16.01 + VERSION: 02.16.02 ======= - VERSION: 02.16.01 + VERSION: 02.16.02 ======= - VERSION: 02.16.01 + VERSION: 02.16.02 >>>>>>> origin/main >>>>>>> origin/main BRIEF: Security policy and vulnerability reporting process for MokoOnyx. diff --git a/src/html/com_joomgallery/category/default.php b/src/html/com_joomgallery/category/default.php index a2e6b44..6eedfcd 100644 --- a/src/html/com_joomgallery/category/default.php +++ b/src/html/com_joomgallery/category/default.php @@ -13,13 +13,13 @@ * PATH: /html/com_joomgallery/category/default.php <<<<<<< HEAD <<<<<<< HEAD - * VERSION: 02.16.01 + * VERSION: 02.16.02 ======= - * VERSION: 02.16.01 + * VERSION: 02.16.02 ======= - * VERSION: 02.16.01 + * VERSION: 02.16.02 ======= - * VERSION: 02.16.01 + * VERSION: 02.16.02 >>>>>>> origin/main >>>>>>> origin/main * BRIEF: Category view override — password gate then loads default_cat sub-layout diff --git a/src/html/com_joomgallery/category/default_cat.php b/src/html/com_joomgallery/category/default_cat.php index 7c29a0c..3acc6cd 100644 --- a/src/html/com_joomgallery/category/default_cat.php +++ b/src/html/com_joomgallery/category/default_cat.php @@ -13,13 +13,13 @@ * PATH: /html/com_joomgallery/category/default_cat.php <<<<<<< HEAD <<<<<<< HEAD - * VERSION: 02.16.01 + * VERSION: 02.16.02 ======= - * VERSION: 02.16.01 + * VERSION: 02.16.02 ======= - * VERSION: 02.16.01 + * VERSION: 02.16.02 ======= - * VERSION: 02.16.01 + * VERSION: 02.16.02 >>>>>>> origin/main >>>>>>> origin/main * BRIEF: Category sub-layout — subcategories grid + images grid with pagination diff --git a/src/html/com_joomgallery/gallery/default.php b/src/html/com_joomgallery/gallery/default.php index c64f8b4..e9e3cbe 100644 --- a/src/html/com_joomgallery/gallery/default.php +++ b/src/html/com_joomgallery/gallery/default.php @@ -13,13 +13,13 @@ * PATH: /html/com_joomgallery/gallery/default.php <<<<<<< HEAD <<<<<<< HEAD - * VERSION: 02.16.01 + * VERSION: 02.16.02 ======= - * VERSION: 02.16.01 + * VERSION: 02.16.02 ======= - * VERSION: 02.16.01 + * VERSION: 02.16.02 ======= - * VERSION: 02.16.01 + * VERSION: 02.16.02 >>>>>>> origin/main >>>>>>> origin/main * BRIEF: Gallery view override — main image grid with masonry/justified layout diff --git a/src/html/com_joomgallery/image/default.php b/src/html/com_joomgallery/image/default.php index 52e9b99..ae36d47 100644 --- a/src/html/com_joomgallery/image/default.php +++ b/src/html/com_joomgallery/image/default.php @@ -13,13 +13,13 @@ * PATH: /html/com_joomgallery/image/default.php <<<<<<< HEAD <<<<<<< HEAD - * VERSION: 02.16.01 + * VERSION: 02.16.02 ======= - * VERSION: 02.16.01 + * VERSION: 02.16.02 ======= - * VERSION: 02.16.01 + * VERSION: 02.16.02 ======= - * VERSION: 02.16.01 + * VERSION: 02.16.02 >>>>>>> origin/main >>>>>>> origin/main * BRIEF: Image detail view override — single image with metadata, tags, custom fields diff --git a/src/html/layouts/joomla/module/card.php b/src/html/layouts/joomla/module/card.php index 22cbbb4..98e1557 100644 --- a/src/html/layouts/joomla/module/card.php +++ b/src/html/layouts/joomla/module/card.php @@ -12,13 +12,13 @@ * PATH: /html/layouts/joomla/module/card.php <<<<<<< HEAD <<<<<<< HEAD - * VERSION: 02.16.01 + * VERSION: 02.16.02 ======= - * VERSION: 02.16.01 + * VERSION: 02.16.02 ======= - * VERSION: 02.16.01 + * VERSION: 02.16.02 ======= - * VERSION: 02.16.01 + * VERSION: 02.16.02 >>>>>>> origin/main >>>>>>> origin/main * BRIEF: Custom card module chrome — renders module titles for all modules diff --git a/src/html/layouts/mokoonyx/article-metadata.php b/src/html/layouts/mokoonyx/article-metadata.php index 9a75835..b02c1d5 100644 --- a/src/html/layouts/mokoonyx/article-metadata.php +++ b/src/html/layouts/mokoonyx/article-metadata.php @@ -13,13 +13,13 @@ * PATH: /src/html/layouts/mokoonyx/article-metadata.php <<<<<<< HEAD <<<<<<< HEAD - * VERSION: 02.16.01 + * VERSION: 02.16.02 ======= - * VERSION: 02.16.01 + * VERSION: 02.16.02 ======= - * VERSION: 02.16.01 + * VERSION: 02.16.02 ======= - * VERSION: 02.16.01 + * VERSION: 02.16.02 >>>>>>> origin/main >>>>>>> origin/main * BRIEF: Article metadata footer layout -- renders jcfields grouped by field group diff --git a/src/media/css/a11y-high-contrast.css b/src/media/css/a11y-high-contrast.css index 3d46475..692c412 100644 --- a/src/media/css/a11y-high-contrast.css +++ b/src/media/css/a11y-high-contrast.css @@ -12,13 +12,13 @@ * PATH: ./media/css/a11y-high-contrast.css <<<<<<< HEAD <<<<<<< HEAD - * VERSION: 02.16.01 + * VERSION: 02.16.02 ======= - * VERSION: 02.16.01 + * VERSION: 02.16.02 ======= - * VERSION: 02.16.01 + * VERSION: 02.16.02 ======= - * VERSION: 02.16.01 + * VERSION: 02.16.02 >>>>>>> origin/main >>>>>>> origin/main * BRIEF: High-contrast stylesheet for accessibility toolbar diff --git a/src/templateDetails.xml b/src/templateDetails.xml index d722327..2dc5b20 100644 --- a/src/templateDetails.xml +++ b/src/templateDetails.xml @@ -36,7 +36,7 @@ mokoonyx - 02.16.01-dev + 02.16.02-dev script.php 2026-05-16 Jonathan Miller || Moko Consulting diff --git a/updates.xml b/updates.xml index e3fa419..5b909a6 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ -- 2.52.0 From 6c61c13db639b82bfab59625518af5569d479271 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Tue, 2 Jun 2026 22:11:10 +0000 Subject: [PATCH 10/21] chore: update development channel 02.16.02-dev [skip ci] --- updates.xml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/updates.xml b/updates.xml index 5b909a6..e0cd63a 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ @@ -11,18 +11,18 @@ mokoonyx template site - 02.16.01-dev - 2026-05-31 - https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/development + 02.16.02-dev + 2026-06-02 + https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/development/tpl_mokoonyx-02.16.01-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/development/tpl_mokoonyx-02.16.02-dev.zip - 32afd0081daa16bb8e89fbd29f2780f95ea910c34a3c12c9a5b3535a83c3b97c + e63d517d90735c821b91ecabb4218af71a2b27733860d18f5f87142cae5b5d96 dev https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/CHANGELOG.md Moko Consulting https://mokoconsulting.tech - + 8.1.0 @@ -33,16 +33,16 @@ site 02.18.00 2026-06-02 - https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/stable + https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/stable - https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/stable/tpl_mokoonyx-02.18.00.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/stable/tpl_mokoonyx-02.18.00.zip fcaab94bc82d9e9e11b609dce5e5a9fc21e4f394088e05effb3e004f9719dfd0 stable https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/CHANGELOG.md Moko Consulting https://mokoconsulting.tech - + 8.1.0 -- 2.52.0 From 4ceb9efbf093170b5fc1d04fcb36c27fb104d40a Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Wed, 3 Jun 2026 03:10:39 +0000 Subject: [PATCH 11/21] chore: sync .mokogitea/workflows/repo-health.yml from moko-platform [skip ci] --- .mokogitea/workflows/repo-health.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index b23d971..d7743f0 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -41,7 +41,8 @@ permissions: env: # Release policy - Repository Variables Only - RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX + # RS_FTP_PATH_SUFFIX removed — MokoGitea handles all releases now + RELEASE_REQUIRED_REPO_VARS: RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX # Scripts governance policy -- 2.52.0 From 36c15a3d86300fe11a83519d60b5aa85c7b90458 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Wed, 3 Jun 2026 09:36:56 +0000 Subject: [PATCH 12/21] chore: sync .mokogitea/workflows/repo-health.yml from moko-platform [skip ci] --- .mokogitea/workflows/repo-health.yml | 125 ++------------------------- 1 file changed, 9 insertions(+), 116 deletions(-) diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index d7743f0..8d57aaf 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -11,7 +11,7 @@ # 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 release configuration, scripts governance, tooling availability, and core repository health artifacts. +# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts. # ============================================================================ name: "Generic: Repo Health" @@ -24,13 +24,12 @@ on: workflow_dispatch: inputs: profile: - description: 'Validation profile: all, release, scripts, or repo' + description: 'Validation profile: all, scripts, or repo' required: true default: all type: choice options: - all - - release - scripts - repo pull_request: @@ -40,11 +39,6 @@ permissions: contents: read env: - # Release policy - Repository Variables Only - # RS_FTP_PATH_SUFFIX removed — MokoGitea handles all releases now - RELEASE_REQUIRED_REPO_VARS: - RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX - # Scripts governance policy SCRIPTS_REQUIRED_DIRS: SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate @@ -139,101 +133,6 @@ jobs: printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}" exit 1 - release_config: - name: Release configuration - 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: Guardrails release vars - env: - PROFILE_RAW: ${{ github.event.inputs.profile }} - RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }} - DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} - run: | - set -euo pipefail - - profile="${PROFILE_RAW:-all}" - case "${profile}" in - all|release|scripts|repo) ;; - *) - printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" - exit 1 - ;; - esac - - if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then - { - printf '%s\n' '### Release configuration (Repository Variables)' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' 'Status: SKIPPED' - printf '%s\n' 'Reason: profile excludes release validation' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - exit 0 - fi - - IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}" - IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}" - - missing=() - missing_optional=() - - for k in "${required[@]}"; do - v="${!k:-}" - [ -z "${v}" ] && missing+=("${k}") - done - - for k in "${optional[@]}"; do - v="${!k:-}" - [ -z "${v}" ] && missing_optional+=("${k}") - done - - { - printf '%s\n' '### Release configuration (Repository Variables)' - printf '%s\n' "Profile: ${profile}" - printf '%s\n' '| Variable | Status |' - printf '%s\n' '|---|---|' - printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |" - printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |" - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - - if [ "${#missing_optional[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing optional repository variables' - for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - fi - - if [ "${#missing[@]}" -gt 0 ]; then - { - printf '%s\n' '### Missing required repository variables' - for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done - printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.' - } >> "${GITHUB_STEP_SUMMARY}" - exit 1 - fi - - { - printf '%s\n' '### Repository variables validation result' - printf '%s\n' 'Status: OK' - printf '%s\n' 'All required repository variables present.' - printf '%s\n' '' - printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.' - printf '\n' - } >> "${GITHUB_STEP_SUMMARY}" - scripts_governance: name: Scripts governance needs: access_check @@ -257,14 +156,14 @@ jobs: profile="${PROFILE_RAW:-all}" case "${profile}" in - all|release|scripts|repo) ;; + all|scripts|repo) ;; *) printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" exit 1 ;; esac - if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then + if [ "${profile}" = 'repo' ]; then { printf '%s\n' '### Scripts governance' printf '%s\n' "Profile: ${profile}" @@ -371,14 +270,14 @@ jobs: profile="${PROFILE_RAW:-all}" case "${profile}" in - all|release|scripts|repo) ;; + all|scripts|repo) ;; *) printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" exit 1 ;; esac - if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then + if [ "${profile}" = 'scripts' ]; then { printf '%s\n' '### Repository health' printf '%s\n' "Profile: ${profile}" @@ -705,7 +604,7 @@ jobs: printf '%s\n' '| Domain | Status | Notes |' printf '%s\n' '|---|---|---|' printf '%s\n' '| Access control | OK | Admin-only execution gate |' - printf '%s\n' '| Release variables | OK | Repository variables validation |' + 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 |' @@ -774,11 +673,10 @@ jobs: report-issues: name: "Report Issues" runs-on: ubuntu-latest - needs: [access_check, release_config, scripts_governance, repo_health] + needs: [access_check, scripts_governance, repo_health] if: >- always() && - (needs.release_config.result == 'failure' || - needs.scripts_governance.result == 'failure' || + (needs.scripts_governance.result == 'failure' || needs.repo_health.result == 'failure') steps: @@ -804,10 +702,6 @@ jobs: fi } - report_gate "Release Configuration" \ - "${{ needs.release_config.result }}" \ - "Required repository variables are missing (RS_FTP_PATH_SUFFIX). Check repository settings." - report_gate "Scripts Governance" \ "${{ needs.scripts_governance.result }}" \ "Scripts directory policy violations detected. Review required and allowed directories." @@ -815,4 +709,3 @@ jobs: 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." - -- 2.52.0 From 2886ebdf549bb1fe618a3c15e3f044f88d7ff705 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Wed, 3 Jun 2026 18:40:39 -0500 Subject: [PATCH 13/21] fix(minify): add word boundary assertions to JS keyword restoration regex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The keyword restoration step in minifyJs() was matching 'in' and other short keywords as substrings inside identifiers (init → in it, window → win dow, contains → contain s, getBoundingClientRect → getBoundin gClientRect), corrupting the minified JS output and breaking all template JavaScript on every MokoOnyx site. Adding \b word boundaries ensures only standalone keywords are matched. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- src/helper/minify.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helper/minify.php b/src/helper/minify.php index 9ffbe2f..09c4417 100644 --- a/src/helper/minify.php +++ b/src/helper/minify.php @@ -161,7 +161,7 @@ class MokoMinifyHelper $js = preg_replace('/\s*([{}();,=+\-*\/<>!&|?:])\s*/', '$1', $js); // Restore necessary spaces (after keywords) - $js = preg_replace('/(var|let|const|return|typeof|instanceof|new|delete|throw|case|in|of)([^\s;})><=!&|?:,])/', '$1 $2', $js); + $js = preg_replace('/\b(var|let|const|return|typeof|instanceof|new|delete|throw|case|in|of)\b([^\s;})><=!&|?:,])/', '$1 $2', $js); return trim($js); } -- 2.52.0 From 98fddcbedba9fecad3e266f8d567ba50f28283a0 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Wed, 3 Jun 2026 23:41:29 +0000 Subject: [PATCH 14/21] chore(version): auto-bump 02.16.03-dev [skip ci] --- .mokogitea/manifest.xml | 2 +- .mokogitea/workflows/issue-branch.yml | 8 ++++---- CHANGELOG.md | 16 ++++++++-------- SECURITY.md | 8 ++++---- src/html/com_joomgallery/category/default.php | 8 ++++---- .../com_joomgallery/category/default_cat.php | 8 ++++---- src/html/com_joomgallery/gallery/default.php | 8 ++++---- src/html/com_joomgallery/image/default.php | 8 ++++---- src/html/layouts/joomla/module/card.php | 8 ++++---- src/html/layouts/mokoonyx/article-metadata.php | 8 ++++---- src/media/css/a11y-high-contrast.css | 8 ++++---- src/templateDetails.xml | 2 +- updates.xml | 2 +- 13 files changed, 47 insertions(+), 47 deletions(-) diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index eb9c7e1..f9e920a 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -9,7 +9,7 @@ Template - MokoOnyx MokoConsulting MokoOnyx - Joomla site template (successor to MokoCassiopeia) - 02.16.02 + 02.16.03 GNU General Public License v3 diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 51c1698..178937a 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -7,13 +7,13 @@ # INGROUP: moko-platform.Automation <<<<<<< HEAD <<<<<<< HEAD -# VERSION: 02.16.02 +# VERSION: 02.16.03 ======= -# VERSION: 02.16.02 +# VERSION: 02.16.03 ======= -# VERSION: 02.16.02 +# VERSION: 02.16.03 ======= -# VERSION: 02.16.02 +# VERSION: 02.16.03 >>>>>>> origin/main >>>>>>> origin/main # BRIEF: Auto-create feature branch when an issue is opened diff --git a/CHANGELOG.md b/CHANGELOG.md index 65b3fc9..6d684ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,35 +10,35 @@ PATH: ./CHANGELOG.md <<<<<<< HEAD <<<<<<< HEAD - VERSION: 02.16.02 + VERSION: 02.16.03 BRIEF: Changelog file documenting version history of MokoOnyx --> -# Changelog — MokoOnyx (VERSION: 02.16.02) +# Changelog — MokoOnyx (VERSION: 02.16.03) ======= - VERSION: 02.16.02 + VERSION: 02.16.03 BRIEF: Changelog file documenting version history of MokoOnyx --> -# Changelog — MokoOnyx (VERSION: 02.16.02) +# Changelog — MokoOnyx (VERSION: 02.16.03) >>>>>>> origin/main ======= - VERSION: 02.16.02 + VERSION: 02.16.03 BRIEF: Changelog file documenting version history of MokoOnyx --> -# Changelog — MokoOnyx (VERSION: 02.16.02) +# Changelog — MokoOnyx (VERSION: 02.16.03) >>>>>>> origin/main ## [Unreleased] ## [02.15.00] --- 2026-05-30 ======= - VERSION: 02.16.02 + VERSION: 02.16.03 BRIEF: Changelog file documenting version history of MokoOnyx --> -# Changelog — MokoOnyx (VERSION: 02.16.02) +# Changelog — MokoOnyx (VERSION: 02.16.03) >>>>>>> origin/main ## [02.14.00] --- 2026-05-30 diff --git a/SECURITY.md b/SECURITY.md index 0597bb2..82bfcd2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,13 +12,13 @@ FILE: SECURITY.md <<<<<<< HEAD <<<<<<< HEAD - VERSION: 02.16.02 + VERSION: 02.16.03 ======= - VERSION: 02.16.02 + VERSION: 02.16.03 ======= - VERSION: 02.16.02 + VERSION: 02.16.03 ======= - VERSION: 02.16.02 + VERSION: 02.16.03 >>>>>>> origin/main >>>>>>> origin/main BRIEF: Security policy and vulnerability reporting process for MokoOnyx. diff --git a/src/html/com_joomgallery/category/default.php b/src/html/com_joomgallery/category/default.php index 6eedfcd..eb9c507 100644 --- a/src/html/com_joomgallery/category/default.php +++ b/src/html/com_joomgallery/category/default.php @@ -13,13 +13,13 @@ * PATH: /html/com_joomgallery/category/default.php <<<<<<< HEAD <<<<<<< HEAD - * VERSION: 02.16.02 + * VERSION: 02.16.03 ======= - * VERSION: 02.16.02 + * VERSION: 02.16.03 ======= - * VERSION: 02.16.02 + * VERSION: 02.16.03 ======= - * VERSION: 02.16.02 + * VERSION: 02.16.03 >>>>>>> origin/main >>>>>>> origin/main * BRIEF: Category view override — password gate then loads default_cat sub-layout diff --git a/src/html/com_joomgallery/category/default_cat.php b/src/html/com_joomgallery/category/default_cat.php index 3acc6cd..a783fb8 100644 --- a/src/html/com_joomgallery/category/default_cat.php +++ b/src/html/com_joomgallery/category/default_cat.php @@ -13,13 +13,13 @@ * PATH: /html/com_joomgallery/category/default_cat.php <<<<<<< HEAD <<<<<<< HEAD - * VERSION: 02.16.02 + * VERSION: 02.16.03 ======= - * VERSION: 02.16.02 + * VERSION: 02.16.03 ======= - * VERSION: 02.16.02 + * VERSION: 02.16.03 ======= - * VERSION: 02.16.02 + * VERSION: 02.16.03 >>>>>>> origin/main >>>>>>> origin/main * BRIEF: Category sub-layout — subcategories grid + images grid with pagination diff --git a/src/html/com_joomgallery/gallery/default.php b/src/html/com_joomgallery/gallery/default.php index e9e3cbe..8f7f76b 100644 --- a/src/html/com_joomgallery/gallery/default.php +++ b/src/html/com_joomgallery/gallery/default.php @@ -13,13 +13,13 @@ * PATH: /html/com_joomgallery/gallery/default.php <<<<<<< HEAD <<<<<<< HEAD - * VERSION: 02.16.02 + * VERSION: 02.16.03 ======= - * VERSION: 02.16.02 + * VERSION: 02.16.03 ======= - * VERSION: 02.16.02 + * VERSION: 02.16.03 ======= - * VERSION: 02.16.02 + * VERSION: 02.16.03 >>>>>>> origin/main >>>>>>> origin/main * BRIEF: Gallery view override — main image grid with masonry/justified layout diff --git a/src/html/com_joomgallery/image/default.php b/src/html/com_joomgallery/image/default.php index ae36d47..c7d6aa9 100644 --- a/src/html/com_joomgallery/image/default.php +++ b/src/html/com_joomgallery/image/default.php @@ -13,13 +13,13 @@ * PATH: /html/com_joomgallery/image/default.php <<<<<<< HEAD <<<<<<< HEAD - * VERSION: 02.16.02 + * VERSION: 02.16.03 ======= - * VERSION: 02.16.02 + * VERSION: 02.16.03 ======= - * VERSION: 02.16.02 + * VERSION: 02.16.03 ======= - * VERSION: 02.16.02 + * VERSION: 02.16.03 >>>>>>> origin/main >>>>>>> origin/main * BRIEF: Image detail view override — single image with metadata, tags, custom fields diff --git a/src/html/layouts/joomla/module/card.php b/src/html/layouts/joomla/module/card.php index 98e1557..098ed52 100644 --- a/src/html/layouts/joomla/module/card.php +++ b/src/html/layouts/joomla/module/card.php @@ -12,13 +12,13 @@ * PATH: /html/layouts/joomla/module/card.php <<<<<<< HEAD <<<<<<< HEAD - * VERSION: 02.16.02 + * VERSION: 02.16.03 ======= - * VERSION: 02.16.02 + * VERSION: 02.16.03 ======= - * VERSION: 02.16.02 + * VERSION: 02.16.03 ======= - * VERSION: 02.16.02 + * VERSION: 02.16.03 >>>>>>> origin/main >>>>>>> origin/main * BRIEF: Custom card module chrome — renders module titles for all modules diff --git a/src/html/layouts/mokoonyx/article-metadata.php b/src/html/layouts/mokoonyx/article-metadata.php index b02c1d5..f08a0ce 100644 --- a/src/html/layouts/mokoonyx/article-metadata.php +++ b/src/html/layouts/mokoonyx/article-metadata.php @@ -13,13 +13,13 @@ * PATH: /src/html/layouts/mokoonyx/article-metadata.php <<<<<<< HEAD <<<<<<< HEAD - * VERSION: 02.16.02 + * VERSION: 02.16.03 ======= - * VERSION: 02.16.02 + * VERSION: 02.16.03 ======= - * VERSION: 02.16.02 + * VERSION: 02.16.03 ======= - * VERSION: 02.16.02 + * VERSION: 02.16.03 >>>>>>> origin/main >>>>>>> origin/main * BRIEF: Article metadata footer layout -- renders jcfields grouped by field group diff --git a/src/media/css/a11y-high-contrast.css b/src/media/css/a11y-high-contrast.css index 692c412..ff1770b 100644 --- a/src/media/css/a11y-high-contrast.css +++ b/src/media/css/a11y-high-contrast.css @@ -12,13 +12,13 @@ * PATH: ./media/css/a11y-high-contrast.css <<<<<<< HEAD <<<<<<< HEAD - * VERSION: 02.16.02 + * VERSION: 02.16.03 ======= - * VERSION: 02.16.02 + * VERSION: 02.16.03 ======= - * VERSION: 02.16.02 + * VERSION: 02.16.03 ======= - * VERSION: 02.16.02 + * VERSION: 02.16.03 >>>>>>> origin/main >>>>>>> origin/main * BRIEF: High-contrast stylesheet for accessibility toolbar diff --git a/src/templateDetails.xml b/src/templateDetails.xml index 2dc5b20..8c63667 100644 --- a/src/templateDetails.xml +++ b/src/templateDetails.xml @@ -36,7 +36,7 @@ mokoonyx - 02.16.02-dev + 02.16.03-dev script.php 2026-05-16 Jonathan Miller || Moko Consulting diff --git a/updates.xml b/updates.xml index e0cd63a..8360587 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ -- 2.52.0 From e265edb4bada0b941db58271cff2423aba7b78f5 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Wed, 3 Jun 2026 23:41:31 +0000 Subject: [PATCH 15/21] chore: update development channel 02.16.03-dev [skip ci] --- updates.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/updates.xml b/updates.xml index 8360587..19fa583 100644 --- a/updates.xml +++ b/updates.xml @@ -11,13 +11,13 @@ mokoonyx template site - 02.16.02-dev - 2026-06-02 + 02.16.03-dev + 2026-06-03 https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/development/tpl_mokoonyx-02.16.02-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/development/tpl_mokoonyx-02.16.03-dev.zip - e63d517d90735c821b91ecabb4218af71a2b27733860d18f5f87142cae5b5d96 + 24d67eb360fb9dbc40dfdb5b37167a9fabc8ca9b518987a8fc978a93b53731dc dev https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/CHANGELOG.md Moko Consulting -- 2.52.0 From 0dffd277e0b98e0b0d5c28b08e156f65cd8dde4e Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 00:05:05 -0500 Subject: [PATCH 16/21] refactor: remove JoomGallery template overrides JoomGallery overrides are no longer maintained in the core template. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- src/html/com_joomgallery/category/default.php | 61 ----- .../com_joomgallery/category/default_cat.php | 229 ---------------- src/html/com_joomgallery/category/index.html | 1 - src/html/com_joomgallery/gallery/default.php | 148 ---------- src/html/com_joomgallery/gallery/index.html | 1 - src/html/com_joomgallery/image/default.php | 259 ------------------ src/html/com_joomgallery/image/index.html | 1 - 7 files changed, 700 deletions(-) delete mode 100644 src/html/com_joomgallery/category/default.php delete mode 100644 src/html/com_joomgallery/category/default_cat.php delete mode 100644 src/html/com_joomgallery/category/index.html delete mode 100644 src/html/com_joomgallery/gallery/default.php delete mode 100644 src/html/com_joomgallery/gallery/index.html delete mode 100644 src/html/com_joomgallery/image/default.php delete mode 100644 src/html/com_joomgallery/image/index.html diff --git a/src/html/com_joomgallery/category/default.php b/src/html/com_joomgallery/category/default.php deleted file mode 100644 index 6eedfcd..0000000 --- a/src/html/com_joomgallery/category/default.php +++ /dev/null @@ -1,61 +0,0 @@ - - * - * This file is part of a Moko Consulting project. - * - * SPDX-License-Identifier: GPL-3.0-or-later - * - * FILE INFORMATION - * DEFGROUP: MokoOnyx.Override - * INGROUP: MokoOnyx - * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx - * PATH: /html/com_joomgallery/category/default.php -<<<<<<< HEAD -<<<<<<< HEAD - * VERSION: 02.16.02 -======= - * VERSION: 02.16.02 -======= - * VERSION: 02.16.02 -======= - * VERSION: 02.16.02 ->>>>>>> origin/main ->>>>>>> origin/main - * BRIEF: Category view override — password gate then loads default_cat sub-layout - */ - -// No direct access -defined('_JEXEC') or die; - -use Joomla\CMS\Router\Route; -use Joomla\CMS\Language\Text; -use Joomla\CMS\HTML\HTMLHelper; - -// Import CSS & JS -$wa = $this->document->getWebAssetManager(); -$wa->useStyle('com_joomgallery.site'); -$wa->useStyle('com_joomgallery.jg-icon-font'); -?> - -item->pw_protected) : ?> -
-
-
-

-
-
- - -
-
- -
- -
-
- - loadTemplate('cat'); ?> - diff --git a/src/html/com_joomgallery/category/default_cat.php b/src/html/com_joomgallery/category/default_cat.php deleted file mode 100644 index 3acc6cd..0000000 --- a/src/html/com_joomgallery/category/default_cat.php +++ /dev/null @@ -1,229 +0,0 @@ - - * - * This file is part of a Moko Consulting project. - * - * SPDX-License-Identifier: GPL-3.0-or-later - * - * FILE INFORMATION - * DEFGROUP: MokoOnyx.Override - * INGROUP: MokoOnyx - * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx - * PATH: /html/com_joomgallery/category/default_cat.php -<<<<<<< HEAD -<<<<<<< HEAD - * VERSION: 02.16.02 -======= - * VERSION: 02.16.02 -======= - * VERSION: 02.16.02 -======= - * VERSION: 02.16.02 ->>>>>>> origin/main ->>>>>>> origin/main - * BRIEF: Category sub-layout — subcategories grid + images grid with pagination - */ - -// No direct access -defined('_JEXEC') or die; - -use Joomla\CMS\Factory; -use Joomla\CMS\Router\Route; -use Joomla\CMS\Language\Text; -use Joomla\CMS\Session\Session; -use Joomla\CMS\HTML\HTMLHelper; -use Joomla\CMS\Layout\LayoutHelper; -use Joomgallery\Component\Joomgallery\Administrator\Helper\JoomHelper; - -// Image params -$image_type = $this->params['configs']->get('jg_category_view_type_image', 'thumbnail', 'STRING'); -$gallery_class = $this->params['configs']->get('jg_category_view_class', 'masonry', 'STRING'); -$num_columns = $this->params['configs']->get('jg_category_view_num_columns', 3, 'INT'); -$image_class = $this->params['configs']->get('jg_category_view_image_class', 0, 'INT'); -$justified_height = $this->params['configs']->get('jg_category_view_justified_height', 200, 'INT'); -$justified_gap = $this->params['configs']->get('jg_category_view_justified_gap', 5, 'INT'); -$image_link = $this->params['configs']->get('jg_category_view_image_link', 'defaultview', 'STRING'); -$lightbox_image = $this->params['configs']->get('jg_category_view_lightbox_image', 'detail', 'STRING'); -$pagination_type = $this->params['configs']->get('jg_category_view_pagination', 0, 'INT'); -$show_subcategories = $this->params['configs']->get('jg_category_view_subcategories', 1, 'INT'); -$subcategory_type_image = $this->params['configs']->get('jg_category_view_type_subcategory_image', 'thumbnail', 'STRING'); -$num_columns_subcats = $this->params['configs']->get('jg_category_view_subcategories_num_columns', 3, 'INT'); - -// Import CSS & JS -$wa = $this->document->getWebAssetManager(); - -if ($gallery_class == 'masonry') { - $wa->useScript('com_joomgallery.masonry'); -} - -if ($gallery_class == 'justified') { - $wa->useScript('com_joomgallery.justified'); - $wa->addInlineStyle('.jg-images[class*=" justified-"] .jg-image-caption-hover { right: ' . $justified_gap . 'px; }'); -} - -$lightbox = false; -if ($image_link == 'lightgallery') { - $lightbox = true; - $wa->useScript('com_joomgallery.lightgallery'); - $wa->useScript('com_joomgallery.lg-thumbnail'); - $wa->useStyle('com_joomgallery.lightgallery-bundle'); -} - -// Initialise the grid script -$iniJS = 'window.joomGrid = {'; -$iniJS .= ' itemid: ' . $this->item->id . ','; -$iniJS .= ' pagination: ' . $pagination_type . ','; -$iniJS .= ' layout: "' . $gallery_class . '",'; -$iniJS .= ' num_columns: ' . $num_columns . ','; -$iniJS .= ' lightbox: ' . ($lightbox ? 'true' : 'false') . ','; -$iniJS .= ' justified: {height: ' . $justified_height . ', gap: ' . $justified_gap . '}'; -$iniJS .= '};'; - -$wa->addInlineScript($iniJS, ['position' => 'before'], [], ['com_joomgallery.joomgrid']); -$wa->useScript('com_joomgallery.joomgrid'); - -// Access check -$canEdit = $this->getAcl()->checkACL('edit', 'com_joomgallery.category', $this->item->id, $this->item->parent_id, true); -$canAdd = $this->getAcl()->checkACL('add', 'com_joomgallery.image', 0, $this->item->id, true); -$canDelete = $this->getAcl()->checkACL('delete', 'com_joomgallery.category', $this->item->id, $this->item->parent_id, true); -$canCheckin = $this->getAcl()->checkACL('editstate', 'com_joomgallery.category', $this->item->id, $this->item->parent_id, true) || $this->item->checked_out == Factory::getUser()->id; -$returnURL = base64_encode(JoomHelper::getViewRoute('category', $this->item->id, $this->item->parent_id, $this->item->language, $this->getLayout())); - -$hasSubcats = $show_subcategories && !empty($this->item->children->items) && count($this->item->children->items) > 0; -$hasImages = !empty($this->item->images->items) && count($this->item->images->items) > 0; -?> - -
- - params['menu']->get('show_page_heading')) : ?> - - - - -

escape($this->item->title); ?>

- - - item->description)) : ?> -
- item->description; ?> -
- - - - - - - - - -
-

- (int) $this->item->id, - 'items' => $this->item->children->items, - 'num_columns' => (int) $num_columns_subcats, - 'image_type' => $subcategory_type_image, - ]; - echo LayoutHelper::render('joomgallery.grids.categories', $catsData); - ?> -
- - - - -
- -

- - - (int) $this->item->id, - 'layout' => $gallery_class, - 'items' => $this->item->images->items, - 'num_columns' => (int) $num_columns, - 'caption_align' => 'center', - 'image_class' => $image_class, - 'image_type' => $image_type, - 'lightbox_type' => $lightbox_image, - 'image_link' => $image_link, - 'image_title' => false, - 'title_link' => 'defaultview', - 'image_desc' => false, - 'image_date' => false, - 'image_author' => false, - 'image_tags' => false, - ]; - echo LayoutHelper::render('joomgallery.grids.images', $imgsData); - ?> - - - -
- - - - - - item->parent_id > 0 && $this->item->parent_id != 1) : ?> -
- - - -
- - - -
diff --git a/src/html/com_joomgallery/category/index.html b/src/html/com_joomgallery/category/index.html deleted file mode 100644 index 2efb97f..0000000 --- a/src/html/com_joomgallery/category/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/html/com_joomgallery/gallery/default.php b/src/html/com_joomgallery/gallery/default.php deleted file mode 100644 index e9e3cbe..0000000 --- a/src/html/com_joomgallery/gallery/default.php +++ /dev/null @@ -1,148 +0,0 @@ - - * - * This file is part of a Moko Consulting project. - * - * SPDX-License-Identifier: GPL-3.0-or-later - * - * FILE INFORMATION - * DEFGROUP: MokoOnyx.Override - * INGROUP: MokoOnyx - * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx - * PATH: /html/com_joomgallery/gallery/default.php -<<<<<<< HEAD -<<<<<<< HEAD - * VERSION: 02.16.02 -======= - * VERSION: 02.16.02 -======= - * VERSION: 02.16.02 -======= - * VERSION: 02.16.02 ->>>>>>> origin/main ->>>>>>> origin/main - * BRIEF: Gallery view override — main image grid with masonry/justified layout - */ - -// No direct access -defined('_JEXEC') or die; - -use Joomla\CMS\Router\Route; -use Joomla\CMS\Language\Text; -use Joomla\CMS\Layout\LayoutHelper; - -// Image params -$image_type = $this->params['configs']->get('jg_gallery_view_type_image', 'thumbnail', 'STRING'); -$gallery_class = $this->params['configs']->get('jg_gallery_view_class', 'masonry', 'STRING'); -$num_columns = $this->params['configs']->get('jg_gallery_view_num_columns', 3, 'INT'); -$image_class = $this->params['configs']->get('jg_gallery_view_image_class', 0, 'INT'); -$justified_height = $this->params['configs']->get('jg_gallery_view_justified_height', 200, 'INT'); -$justified_gap = $this->params['configs']->get('jg_gallery_view_justified_gap', 5, 'INT'); -$image_link = $this->params['configs']->get('jg_gallery_view_image_link', 'defaultview', 'STRING'); -$lightbox_image = $this->params['configs']->get('jg_category_view_lightbox_image', 'detail', 'STRING'); -$browse_categories_link = $this->params['configs']->get('jg_gallery_view_browse_categories_link', 1, 'INT'); - -// Import CSS & JS -$wa = $this->document->getWebAssetManager(); -$wa->useStyle('com_joomgallery.site'); -$wa->useStyle('com_joomgallery.jg-icon-font'); - -if ($gallery_class == 'masonry') { - $wa->useScript('com_joomgallery.masonry'); -} - -if ($gallery_class == 'justified') { - $wa->useScript('com_joomgallery.justified'); - $wa->addInlineStyle('.jg-images[class*=" justified-"] .jg-image-caption-hover { right: ' . $justified_gap . 'px; }'); -} - -$lightbox = false; -if ($image_link == 'lightgallery') { - $lightbox = true; - $wa->useScript('com_joomgallery.lightgallery'); - $wa->useScript('com_joomgallery.lg-thumbnail'); - $wa->useStyle('com_joomgallery.lightgallery-bundle'); -} - -// Initialise the grid script -$iniJS = 'window.joomGrid = {'; -$iniJS .= ' itemid: ' . $this->item->id . ','; -$iniJS .= ' pagination: 0,'; -$iniJS .= ' layout: "' . $gallery_class . '",'; -$iniJS .= ' num_columns: ' . $num_columns . ','; -$iniJS .= ' lightbox: ' . ($lightbox ? 'true' : 'false') . ','; -$iniJS .= ' justified: {height: ' . $justified_height . ', gap: ' . $justified_gap . '}'; -$iniJS .= '};'; - -$wa->addInlineScript($iniJS, ['position' => 'before'], [], ['com_joomgallery.joomgrid']); -$wa->useScript('com_joomgallery.joomgrid'); -?> - - diff --git a/src/html/com_joomgallery/gallery/index.html b/src/html/com_joomgallery/gallery/index.html deleted file mode 100644 index 2efb97f..0000000 --- a/src/html/com_joomgallery/gallery/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/html/com_joomgallery/image/default.php b/src/html/com_joomgallery/image/default.php deleted file mode 100644 index ae36d47..0000000 --- a/src/html/com_joomgallery/image/default.php +++ /dev/null @@ -1,259 +0,0 @@ - - * - * This file is part of a Moko Consulting project. - * - * SPDX-License-Identifier: GPL-3.0-or-later - * - * FILE INFORMATION - * DEFGROUP: MokoOnyx.Override - * INGROUP: MokoOnyx - * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx - * PATH: /html/com_joomgallery/image/default.php -<<<<<<< HEAD -<<<<<<< HEAD - * VERSION: 02.16.02 -======= - * VERSION: 02.16.02 -======= - * VERSION: 02.16.02 -======= - * VERSION: 02.16.02 ->>>>>>> origin/main ->>>>>>> origin/main - * BRIEF: Image detail view override — single image with metadata, tags, custom fields - */ - -// No direct access -defined('_JEXEC') or die; - -use Joomla\CMS\Factory; -use Joomla\CMS\Router\Route; -use Joomla\CMS\Language\Text; -use Joomla\CMS\Session\Session; -use Joomla\CMS\HTML\HTMLHelper; -use Joomla\CMS\Layout\FileLayout; -use Joomla\CMS\User\UserFactoryInterface; -use Joomla\Component\Fields\Administrator\Helper\FieldsHelper; -use Joomgallery\Component\Joomgallery\Administrator\Helper\JoomHelper; - -// Image params -$image_type = $this->params['configs']->get('jg_detail_view_type_image', 'detail', 'STRING'); -$show_title = $this->params['configs']->get('jg_detail_view_show_title', 0, 'INT'); -$show_category = $this->params['configs']->get('jg_detail_view_show_category', 0, 'INT'); -$show_description = $this->params['configs']->get('jg_detail_view_show_description', 0, 'INT'); -$show_imgdate = $this->params['configs']->get('jg_detail_view_show_imgdate', 0, 'INT'); -$show_imgauthor = $this->params['configs']->get('jg_detail_view_show_imgauthor', 0, 'INT'); -$show_created_by = $this->params['configs']->get('jg_detail_view_show_created_by', 0, 'INT'); -$show_votes = $this->params['configs']->get('jg_detail_view_show_votes', 0, 'INT'); -$show_rating = $this->params['configs']->get('jg_detail_view_show_rating', 0, 'INT'); -$show_hits = $this->params['configs']->get('jg_detail_view_show_hits', 0, 'INT'); -$show_downloads = $this->params['configs']->get('jg_detail_view_show_downloads', 0, 'INT'); -$show_tags = $this->params['configs']->get('jg_detail_view_show_tags', 0, 'INT'); -$show_metadata = $this->params['configs']->get('jg_detail_view_show_metadata', 0, 'INT'); - -// Import CSS & JS -$wa = $this->document->getWebAssetManager(); -$wa->useStyle('com_joomgallery.site'); -$wa->useStyle('com_joomgallery.jg-icon-font'); - -// Access check -$canEdit = $this->getAcl()->checkACL('edit', 'com_joomgallery.image', $this->item->id, $this->item->catid, true); -$canDelete = $this->getAcl()->checkACL('delete', 'com_joomgallery.image', $this->item->id, $this->item->catid, true); -$canCheckin = $this->getAcl()->checkACL('editstate', 'com_joomgallery.image', $this->item->id, $this->item->catid, true) || $this->item->checked_out == Factory::getUser()->id; -$returnURL = base64_encode(JoomHelper::getViewRoute('image', $this->item->id, $this->item->catid, $this->item->language, $this->getLayout())); - -// Tags -$tagLayout = new FileLayout('joomgallery.content.tags'); -$tags = $tagLayout->render($this->item->tags); - -// Metadata -$metadataLayout = new FileLayout('joomgallery.content.metadata'); -$metadata = $metadataLayout->render($this->item->imgmetadata); - -// Custom Fields -$fields = FieldsHelper::getFields('com_joomgallery.image', $this->item); - -// Check if we have any info rows to show -$hasInfo = $show_category || $show_imgdate || $show_imgauthor || $show_created_by - || $show_votes || $show_rating || $show_hits || $show_downloads - || $show_tags || $show_metadata || count($fields) > 0; -?> - -
- -

escape($this->item->title); ?>

- - - - - escape($this->item->cattitle); ?> - - - - - - - - -
-
- <?php echo $this->escape($this->item->title); ?> - item->description)) : ?> -
item->description; ?>
- -
- - - -
-

- - - - - - - - - - - - - - - - - - - - - - - - - get(UserFactoryInterface::class)->loadUserById($this->item->created_by); ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0) : ?> - - - - - component->getAccess()->checkViewLevel($field->access) && $field->params->get('display') > 0) : ?> - - params->get('showlabel', true)) : ?> - - - - - - - - - - -
- - escape($this->item->cattitle); ?> - -
- -
escape($user->name); ?>
escape($this->item->votes); ?>
escape($this->item->rating); ?>
item->hits; ?>
item->downloads; ?>
escape($field->title); ?>value; ?>
-
- - - -
diff --git a/src/html/com_joomgallery/image/index.html b/src/html/com_joomgallery/image/index.html deleted file mode 100644 index 2efb97f..0000000 --- a/src/html/com_joomgallery/image/index.html +++ /dev/null @@ -1 +0,0 @@ - -- 2.52.0 From ad5bb9d46a62d6dc2b9a78d955eb8556a2574385 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Thu, 4 Jun 2026 12:03:54 +0000 Subject: [PATCH 17/21] chore(version): auto-bump 02.18.02-dev [skip ci] --- .mokogitea/manifest.xml | 2 +- .mokogitea/workflows/issue-branch.yml | 2 +- CHANGELOG.md | 4 ++-- SECURITY.md | 2 +- src/html/layouts/joomla/module/card.php | 2 +- src/html/layouts/mokoonyx/article-metadata.php | 2 +- src/media/css/a11y-high-contrast.css | 2 +- src/templateDetails.xml | 2 +- updates.xml | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 4022801..a4f35e7 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -9,7 +9,7 @@ Template - MokoOnyx MokoConsulting MokoOnyx - Joomla site template (successor to MokoCassiopeia) - 02.18.00 + 02.18.02 GNU General Public License v3 diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 8d957fe..556666e 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: moko-platform.Automation -# VERSION: 02.18.00 +# VERSION: 02.18.02 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/CHANGELOG.md b/CHANGELOG.md index 145bd48..6379e91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,11 @@ DEFGROUP: Joomla.Template.Site INGROUP: MokoOnyx.Documentation PATH: ./CHANGELOG.md - VERSION: 02.18.00 + VERSION: 02.18.02 BRIEF: Changelog file documenting version history of MokoOnyx --> -# Changelog — MokoOnyx (VERSION: 02.18.00) +# Changelog — MokoOnyx (VERSION: 02.18.02) ## [Unreleased] ## [02.18.00] --- 2026-06-02 diff --git a/SECURITY.md b/SECURITY.md index 1bef69f..313758e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,7 +10,7 @@ INGROUP: MokoOnyx.Governance REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx FILE: SECURITY.md - VERSION: 02.18.00 + VERSION: 02.18.02 BRIEF: Security policy and vulnerability reporting process for MokoOnyx. PATH: /SECURITY.md NOTE: This policy is process oriented and does not replace secure engineering practices. diff --git a/src/html/layouts/joomla/module/card.php b/src/html/layouts/joomla/module/card.php index cd50dfb..5ef143b 100644 --- a/src/html/layouts/joomla/module/card.php +++ b/src/html/layouts/joomla/module/card.php @@ -10,7 +10,7 @@ * INGROUP: MokoOnyx * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx * PATH: /html/layouts/joomla/module/card.php - * VERSION: 02.18.00 + * VERSION: 02.18.02 * BRIEF: Custom card module chrome — renders module titles for all modules */ diff --git a/src/html/layouts/mokoonyx/article-metadata.php b/src/html/layouts/mokoonyx/article-metadata.php index 0bc842e..107ebaf 100644 --- a/src/html/layouts/mokoonyx/article-metadata.php +++ b/src/html/layouts/mokoonyx/article-metadata.php @@ -11,7 +11,7 @@ * INGROUP: MokoOnyx.Layouts * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx * PATH: /src/html/layouts/mokoonyx/article-metadata.php - * VERSION: 02.18.00 + * VERSION: 02.18.02 * BRIEF: Article metadata footer layout -- renders jcfields grouped by field group */ diff --git a/src/media/css/a11y-high-contrast.css b/src/media/css/a11y-high-contrast.css index c3d9466..9e7e63f 100644 --- a/src/media/css/a11y-high-contrast.css +++ b/src/media/css/a11y-high-contrast.css @@ -10,7 +10,7 @@ * INGROUP: MokoOnyx.Accessibility * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx * PATH: ./media/css/a11y-high-contrast.css - * VERSION: 02.18.00 + * VERSION: 02.18.02 * BRIEF: High-contrast stylesheet for accessibility toolbar */ diff --git a/src/templateDetails.xml b/src/templateDetails.xml index 3bfbb5e..a4ddd55 100644 --- a/src/templateDetails.xml +++ b/src/templateDetails.xml @@ -36,7 +36,7 @@ mokoonyx - 02.18.01-dev + 02.18.02-dev script.php 2026-05-16 Jonathan Miller || Moko Consulting diff --git a/updates.xml b/updates.xml index 19fa583..d3e2663 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ -- 2.52.0 From 0b47958b282682a148f55570bbb8a9504528d8cc Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Thu, 4 Jun 2026 12:03:56 +0000 Subject: [PATCH 18/21] chore: update development channel 02.18.02-dev [skip ci] --- updates.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/updates.xml b/updates.xml index d3e2663..afaa5fd 100644 --- a/updates.xml +++ b/updates.xml @@ -11,13 +11,13 @@ mokoonyx template site - 02.16.03-dev - 2026-06-03 + 02.18.02-dev + 2026-06-04 https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/development/tpl_mokoonyx-02.16.03-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/development/tpl_mokoonyx-02.18.02-dev.zip - 24d67eb360fb9dbc40dfdb5b37167a9fabc8ca9b518987a8fc978a93b53731dc + 3fa5f6d867a2eea7f7425cf1e9b196e8739decafd67f1b51b37260cf7c6aa582 dev https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/CHANGELOG.md Moko Consulting -- 2.52.0 From 9f88c265bcfed15e091c5eff943cf78dbb4f66c4 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 07:11:34 -0500 Subject: [PATCH 19/21] feat(script): add removeDeletedFiles() to clean up stale overrides on upgrade Joomla's installer never deletes files on upgrade. This adds a maintenance list of files/dirs removed from the package so they get cleaned up on existing installs. Starts with the JoomGallery template overrides removed in this release. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- src/script.php | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/script.php b/src/script.php index d78ceb6..c48027e 100644 --- a/src/script.php +++ b/src/script.php @@ -93,6 +93,7 @@ class Tpl_MokoonyxInstallerScript implements InstallerScriptInterface $this->replaceCassiopeiaReferences(); $this->clearFaviconStamp(); $this->cleanMediaFolder(); + $this->removeDeletedFiles(); $this->lockExtension(); } @@ -483,6 +484,68 @@ class Tpl_MokoonyxInstallerScript implements InstallerScriptInterface } } + /** + * Remove files and directories that were shipped in previous versions + * but have since been deleted from the package. + * + * Joomla's installer never deletes files on upgrade — it only + * adds/overwrites. This method fills that gap so stale overrides + * and deprecated assets don't linger on disk. + * + * Maintain this list: when you delete a file from the repo, add its + * path here (relative to the template root) so existing installs + * get cleaned up on the next update. + */ + private function removeDeletedFiles(): void + { + $templateRoot = JPATH_ROOT . '/templates/' . self::NEW_NAME; + + // Paths relative to templates/mokoonyx/ + $deletedFiles = [ + // JoomGallery template overrides — removed in 02.19.00 + 'html/com_joomgallery/category/default.php', + 'html/com_joomgallery/category/default_cat.php', + 'html/com_joomgallery/category/index.html', + 'html/com_joomgallery/gallery/default.php', + 'html/com_joomgallery/gallery/index.html', + 'html/com_joomgallery/image/default.php', + 'html/com_joomgallery/image/index.html', + ]; + + // Directories to remove (only if empty after file deletion) + $deletedDirs = [ + 'html/com_joomgallery/image', + 'html/com_joomgallery/gallery', + 'html/com_joomgallery/category', + 'html/com_joomgallery', + ]; + + $removed = 0; + + foreach ($deletedFiles as $relPath) { + $file = $templateRoot . '/' . $relPath; + if (is_file($file)) { + @unlink($file); + $removed++; + } + } + + foreach ($deletedDirs as $relPath) { + $dir = $templateRoot . '/' . $relPath; + if (is_dir($dir)) { + // Only remove if empty + $entries = @scandir($dir); + if ($entries && count($entries) <= 2) { // . and .. only + @rmdir($dir); + } + } + } + + if ($removed > 0) { + $this->logMessage("Removed {$removed} deprecated file(s) from previous versions."); + } + } + private function logMessage(string $message, string $priority = 'info'): void { $priorities = [ -- 2.52.0 From 0e420718cf7226aca992e6f5941b6bd9baa2d1c5 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Thu, 4 Jun 2026 12:12:21 +0000 Subject: [PATCH 20/21] chore(version): auto-bump 02.18.03-dev [skip ci] --- .mokogitea/manifest.xml | 2 +- .mokogitea/workflows/issue-branch.yml | 2 +- CHANGELOG.md | 4 ++-- SECURITY.md | 2 +- src/html/layouts/joomla/module/card.php | 2 +- src/html/layouts/mokoonyx/article-metadata.php | 2 +- src/media/css/a11y-high-contrast.css | 2 +- src/templateDetails.xml | 2 +- updates.xml | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index a4f35e7..003e016 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -9,7 +9,7 @@ Template - MokoOnyx MokoConsulting MokoOnyx - Joomla site template (successor to MokoCassiopeia) - 02.18.02 + 02.18.03 GNU General Public License v3 diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 556666e..1db46f7 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: moko-platform.Automation -# VERSION: 02.18.02 +# VERSION: 02.18.03 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/CHANGELOG.md b/CHANGELOG.md index 6379e91..2f31489 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,11 @@ DEFGROUP: Joomla.Template.Site INGROUP: MokoOnyx.Documentation PATH: ./CHANGELOG.md - VERSION: 02.18.02 + VERSION: 02.18.03 BRIEF: Changelog file documenting version history of MokoOnyx --> -# Changelog — MokoOnyx (VERSION: 02.18.02) +# Changelog — MokoOnyx (VERSION: 02.18.03) ## [Unreleased] ## [02.18.00] --- 2026-06-02 diff --git a/SECURITY.md b/SECURITY.md index 313758e..a60c876 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,7 +10,7 @@ INGROUP: MokoOnyx.Governance REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx FILE: SECURITY.md - VERSION: 02.18.02 + VERSION: 02.18.03 BRIEF: Security policy and vulnerability reporting process for MokoOnyx. PATH: /SECURITY.md NOTE: This policy is process oriented and does not replace secure engineering practices. diff --git a/src/html/layouts/joomla/module/card.php b/src/html/layouts/joomla/module/card.php index 5ef143b..3c6071d 100644 --- a/src/html/layouts/joomla/module/card.php +++ b/src/html/layouts/joomla/module/card.php @@ -10,7 +10,7 @@ * INGROUP: MokoOnyx * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx * PATH: /html/layouts/joomla/module/card.php - * VERSION: 02.18.02 + * VERSION: 02.18.03 * BRIEF: Custom card module chrome — renders module titles for all modules */ diff --git a/src/html/layouts/mokoonyx/article-metadata.php b/src/html/layouts/mokoonyx/article-metadata.php index 107ebaf..45a27be 100644 --- a/src/html/layouts/mokoonyx/article-metadata.php +++ b/src/html/layouts/mokoonyx/article-metadata.php @@ -11,7 +11,7 @@ * INGROUP: MokoOnyx.Layouts * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx * PATH: /src/html/layouts/mokoonyx/article-metadata.php - * VERSION: 02.18.02 + * VERSION: 02.18.03 * BRIEF: Article metadata footer layout -- renders jcfields grouped by field group */ diff --git a/src/media/css/a11y-high-contrast.css b/src/media/css/a11y-high-contrast.css index 9e7e63f..39c8e45 100644 --- a/src/media/css/a11y-high-contrast.css +++ b/src/media/css/a11y-high-contrast.css @@ -10,7 +10,7 @@ * INGROUP: MokoOnyx.Accessibility * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx * PATH: ./media/css/a11y-high-contrast.css - * VERSION: 02.18.02 + * VERSION: 02.18.03 * BRIEF: High-contrast stylesheet for accessibility toolbar */ diff --git a/src/templateDetails.xml b/src/templateDetails.xml index a4ddd55..fadc206 100644 --- a/src/templateDetails.xml +++ b/src/templateDetails.xml @@ -36,7 +36,7 @@ mokoonyx - 02.18.02-dev + 02.18.03-dev script.php 2026-05-16 Jonathan Miller || Moko Consulting diff --git a/updates.xml b/updates.xml index afaa5fd..d11ac30 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ -- 2.52.0 From 50762b3e208a245e7b4184739f80f122924192a6 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Thu, 4 Jun 2026 12:12:23 +0000 Subject: [PATCH 21/21] chore: update development channel 02.18.03-dev [skip ci] --- updates.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/updates.xml b/updates.xml index d11ac30..81165d5 100644 --- a/updates.xml +++ b/updates.xml @@ -11,13 +11,13 @@ mokoonyx template site - 02.18.02-dev + 02.18.03-dev 2026-06-04 https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/development/tpl_mokoonyx-02.18.02-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/development/tpl_mokoonyx-02.18.03-dev.zip - 3fa5f6d867a2eea7f7425cf1e9b196e8739decafd67f1b51b37260cf7c6aa582 + 5ae848d5bf54e9c3ba4f3255e023658635e3371979bf88df38cb1478b98aeee6 dev https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/CHANGELOG.md Moko Consulting -- 2.52.0