From aa36a01a7ff5c0d37bfa4256066b71527ab38e26 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 2 Jun 2026 21:58:43 -0500 Subject: [PATCH 01/42] chore: bump version to 01.01.00-dev for next development cycle Co-Authored-By: Claude Opus 4.6 (1M context) --- .mokogitea/manifest.xml | 2 +- src/packages/com_mokobackup/mokobackup.xml | 2 +- src/packages/plg_quickicon_mokobackup/mokobackup.xml | 2 +- src/packages/plg_system_mokobackup/mokobackup.xml | 2 +- src/packages/plg_task_mokobackup/mokobackup.xml | 2 +- src/packages/plg_webservices_mokobackup/mokobackup.xml | 2 +- src/pkg_mokobackup.xml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 41e81ee..5ef8494 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -5,7 +5,7 @@ Package - MokoJoomBackup MokoConsulting Full-site backup and restore for Joomla — database, files, and configuration - 01.00.00 + 01.01.00-dev GNU General Public License v3 diff --git a/src/packages/com_mokobackup/mokobackup.xml b/src/packages/com_mokobackup/mokobackup.xml index 131212e..4711c60 100644 --- a/src/packages/com_mokobackup/mokobackup.xml +++ b/src/packages/com_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> com_mokobackup - 01.00.00 + 01.01.00-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_quickicon_mokobackup/mokobackup.xml b/src/packages/plg_quickicon_mokobackup/mokobackup.xml index b2ff32a..5a55e79 100644 --- a/src/packages/plg_quickicon_mokobackup/mokobackup.xml +++ b/src/packages/plg_quickicon_mokobackup/mokobackup.xml @@ -1,7 +1,7 @@ plg_quickicon_mokobackup - 01.00.00 + 01.01.00-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_system_mokobackup/mokobackup.xml b/src/packages/plg_system_mokobackup/mokobackup.xml index f13d733..1f1b1eb 100644 --- a/src/packages/plg_system_mokobackup/mokobackup.xml +++ b/src/packages/plg_system_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_system_mokobackup - 01.00.00 + 01.01.00-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_task_mokobackup/mokobackup.xml b/src/packages/plg_task_mokobackup/mokobackup.xml index 92b274e..ab99004 100644 --- a/src/packages/plg_task_mokobackup/mokobackup.xml +++ b/src/packages/plg_task_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_task_mokobackup - 01.00.00 + 01.01.00-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_webservices_mokobackup/mokobackup.xml b/src/packages/plg_webservices_mokobackup/mokobackup.xml index 5d4f103..e54426a 100644 --- a/src/packages/plg_webservices_mokobackup/mokobackup.xml +++ b/src/packages/plg_webservices_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_webservices_mokobackup - 01.00.00 + 01.01.00-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/pkg_mokobackup.xml b/src/pkg_mokobackup.xml index 334846c..2de7a14 100644 --- a/src/pkg_mokobackup.xml +++ b/src/pkg_mokobackup.xml @@ -8,7 +8,7 @@ Package - MokoJoomBackup mokobackup - 01.00.00 + 01.01.00-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech -- 2.52.0 From 507b2c9448501486262ae5b3e10744fff0d0457e Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Wed, 3 Jun 2026 03:11:26 +0000 Subject: [PATCH 02/42] chore: sync .mokogitea/workflows/repo-health.yml from moko-platform [skip ci] --- .mokogitea/workflows/repo-health.yml | 1635 +++++++++++++------------- 1 file changed, 818 insertions(+), 817 deletions(-) diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index e3653eb..d7743f0 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -1,817 +1,818 @@ -# ============================================================================ -# 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." - +# ============================================================================ +# 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 + # 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 + + # 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 610f875ad959f659dbd12dc56b1ad3a634be7add Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Wed, 3 Jun 2026 04:37:27 -0500 Subject: [PATCH 03/42] refactor: rename Kickstart to MokoRestore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace all Akeeba Kickstart branding with MokoRestore: - Rename Kickstart.php to MokoRestore.php - Rename class Kickstart to MokoRestore - Update DB column: include_kickstart → include_mokorestore - Update form field and language string keys - Update all variable names, log messages, and comments - Update CHANGELOG references Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .mokogitea/workflows/repo-health.yml | 1636 ++++++++--------- CHANGELOG.md | 4 +- src/packages/com_mokobackup/forms/profile.xml | 6 +- .../language/en-GB/com_mokobackup.ini | 4 +- .../com_mokobackup/sql/install.mysql.sql | 2 +- .../src/Engine/AkeebaImporter.php | 2 +- .../src/Engine/BackupEngine.php | 18 +- .../Engine/{Kickstart.php => MokoRestore.php} | 10 +- .../src/Engine/SteppedBackupEngine.php | 18 +- .../src/Engine/SteppedSession.php | 2 +- 10 files changed, 851 insertions(+), 851 deletions(-) rename src/packages/com_mokobackup/src/Engine/{Kickstart.php => MokoRestore.php} (98%) diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index d7743f0..03290b0 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -1,818 +1,818 @@ -# ============================================================================ -# 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 - # 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 - - # 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." - +# ============================================================================ +# 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 + # 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 + + # 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." + diff --git a/CHANGELOG.md b/CHANGELOG.md index 4db115d..f228f01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ - Remote upload integrated into BackupEngine as Step 3 after archive creation - Option to delete local copy after successful remote upload (per-profile setting) - Restore engine with file restoration and database import -- Standalone Kickstart restore script (restore.php) — self-contained site restoration without Joomla, like Akeeba Kickstart +- MokoRestore standalone restore script (restore.php) — self-contained site restoration without Joomla, - "Include Restore Script" toggle per profile — wraps backup with restore.php + site-backup.zip - FileRestorer class with protected file handling (preserves configuration.php, .htaccess) - DatabaseImporter with streaming line-by-line SQL execution and error tolerance @@ -28,7 +28,7 @@ - RestoreEngine auto-detects JPA vs ZIP format - AES-256 archive encryption with per-profile password (#17) - Encrypted archive support in RestoreEngine (password parameter) -- Encrypted archive support in Kickstart restore.php (password field in UI) +- Encrypted archive support in MokoRestore restore.php (password field in UI) - SHA-256 checksum computed and stored after archive creation (#15) - "Verify Integrity" toolbar button re-computes hash and compares against stored checksum - S3-compatible remote storage: AWS S3, Wasabi, Backblaze B2, MinIO (#16) diff --git a/src/packages/com_mokobackup/forms/profile.xml b/src/packages/com_mokobackup/forms/profile.xml index f712686..69bef05 100644 --- a/src/packages/com_mokobackup/forms/profile.xml +++ b/src/packages/com_mokobackup/forms/profile.xml @@ -70,10 +70,10 @@ maxlength="512" /> diff --git a/src/packages/com_mokobackup/language/en-GB/com_mokobackup.ini b/src/packages/com_mokobackup/language/en-GB/com_mokobackup.ini index 468be17..dd9c1d0 100644 --- a/src/packages/com_mokobackup/language/en-GB/com_mokobackup.ini +++ b/src/packages/com_mokobackup/language/en-GB/com_mokobackup.ini @@ -75,8 +75,8 @@ COM_MOKOBACKUP_FIELD_SPLIT_SIZE="Split Size (MB)" COM_MOKOBACKUP_FIELD_SPLIT_SIZE_DESC="Split archive into parts of this size in MB. 0 = no splitting." COM_MOKOBACKUP_FIELD_BACKUP_DIR="Backup Directory" COM_MOKOBACKUP_FIELD_BACKUP_DIR_DESC="Relative path from Joomla root where backup archives are stored" -COM_MOKOBACKUP_FIELD_INCLUDE_KICKSTART="Include Restore Script" -COM_MOKOBACKUP_FIELD_INCLUDE_KICKSTART_DESC="Include a standalone restore.php inside the backup archive. This creates a self-contained package that can restore the site on a blank server without Joomla installed — like Akeeba Kickstart." +COM_MOKOBACKUP_FIELD_INCLUDE_MOKORESTORE="Include Restore Script" +COM_MOKOBACKUP_FIELD_INCLUDE_MOKORESTORE_DESC="Include MokoRestore (standalone restore.php) inside the backup archive. Creates a self-contained package that can restore the site on a blank server without Joomla installed." ; Exclusion filter fields COM_MOKOBACKUP_FIELD_EXCLUDE_DIRS="Exclude Directories" diff --git a/src/packages/com_mokobackup/sql/install.mysql.sql b/src/packages/com_mokobackup/sql/install.mysql.sql index fe7c580..b565a01 100644 --- a/src/packages/com_mokobackup/sql/install.mysql.sql +++ b/src/packages/com_mokobackup/sql/install.mysql.sql @@ -30,7 +30,7 @@ CREATE TABLE IF NOT EXISTS `#__mokobackup_profiles` ( `s3_path` VARCHAR(512) NOT NULL DEFAULT '/backups', `remote_keep_local` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Keep local copy after upload', `encryption_password` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'AES-256 archive encryption password (blank = no encryption)', - `include_kickstart` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Include standalone restore.php in archive', + `include_mokorestore` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Include MokoRestore standalone restore script in archive', `notify_email` VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'Comma-separated notification emails', `notify_on_success` TINYINT(1) NOT NULL DEFAULT 0, `notify_on_failure` TINYINT(1) NOT NULL DEFAULT 1, diff --git a/src/packages/com_mokobackup/src/Engine/AkeebaImporter.php b/src/packages/com_mokobackup/src/Engine/AkeebaImporter.php index cafa191..3edbe14 100644 --- a/src/packages/com_mokobackup/src/Engine/AkeebaImporter.php +++ b/src/packages/com_mokobackup/src/Engine/AkeebaImporter.php @@ -246,7 +246,7 @@ class AkeebaImporter 's3_bucket' => $config['engine.postproc.s3.bucket'] ?? '', 's3_path' => $config['engine.postproc.s3.directory'] ?? '/backups', 'remote_keep_local' => 1, - 'include_kickstart' => (int) (($config['akeeba.advanced.embedded_installer'] ?? 'none') !== 'none'), + 'include_mokorestore' => (int) (($config['akeeba.advanced.embedded_installer'] ?? 'none') !== 'none'), 'published' => 1, 'ordering' => (int) $akProfile->id, 'created' => $now, diff --git a/src/packages/com_mokobackup/src/Engine/BackupEngine.php b/src/packages/com_mokobackup/src/Engine/BackupEngine.php index 923c6d4..60e0910 100644 --- a/src/packages/com_mokobackup/src/Engine/BackupEngine.php +++ b/src/packages/com_mokobackup/src/Engine/BackupEngine.php @@ -187,21 +187,21 @@ class BackupEngine $this->log('Archive created: ' . $sizeHuman); $this->log('SHA-256: ' . ($checksum ?: 'N/A')); - // Step 2.5: Wrap with Kickstart restore script (if enabled) - $includeKickstart = (bool) ($profile->include_kickstart ?? false); + // Step 2.5: Wrap with MokoRestore script (if enabled) + $includeMokoRestore = (bool) ($profile->include_mokorestore ?? false); - if ($includeKickstart) { - $this->log('Wrapping with Kickstart restore script...'); - $kickstartName = str_replace('.zip', '-kickstart.zip', $archiveName); - $kickstartPath = $this->backupDir . '/' . $kickstartName; - Kickstart::wrap($archivePath, $kickstartPath); + if ($includeMokoRestore) { + $this->log('Wrapping with MokoRestore script...'); + $mokoRestoreName = str_replace('.zip', '-mokorestore.zip', $archiveName); + $mokoRestorePath = $this->backupDir . '/' . $mokoRestoreName; + MokoRestore::wrap($archivePath, $mokoRestorePath); // Replace the original archive with the wrapped one @unlink($archivePath); - rename($kickstartPath, $archivePath); + rename($mokoRestorePath, $archivePath); $totalSize = filesize($archivePath); $sizeHuman = number_format($totalSize / 1048576, 2) . ' MB'; - $this->log('Kickstart archive created: ' . $sizeHuman); + $this->log('MokoRestore archive created: ' . $sizeHuman); } $remoteFilename = ''; diff --git a/src/packages/com_mokobackup/src/Engine/Kickstart.php b/src/packages/com_mokobackup/src/Engine/MokoRestore.php similarity index 98% rename from src/packages/com_mokobackup/src/Engine/Kickstart.php rename to src/packages/com_mokobackup/src/Engine/MokoRestore.php index e230c10..38c2c0a 100644 --- a/src/packages/com_mokobackup/src/Engine/Kickstart.php +++ b/src/packages/com_mokobackup/src/Engine/MokoRestore.php @@ -9,7 +9,7 @@ * * Standalone restore script generator. * - * When "Include Kickstart" is enabled on a profile, the backup archive + * When "Include MokoRestore" is enabled on a profile, the backup archive * is wrapped: * * outer.zip @@ -17,14 +17,14 @@ * └── site-backup.zip ← The actual site backup * * Upload outer.zip to a blank server, extract, open restore.php in a - * browser, and it handles everything — just like Akeeba Kickstart. + * browser, and it handles everything — self-contained site restoration. */ namespace Joomla\Component\MokoBackup\Administrator\Engine; defined('_JEXEC') or die; -class Kickstart +class MokoRestore { /** * Wrap a backup archive with the standalone restore script. @@ -39,7 +39,7 @@ class Kickstart $zip = new \ZipArchive(); if ($zip->open($outputPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) { - throw new \RuntimeException('Cannot create kickstart archive: ' . $outputPath); + throw new \RuntimeException('Cannot create MokoRestore archive: ' . $outputPath); } // Add the standalone restore script @@ -68,7 +68,7 @@ class Kickstart return <<<'RESTORE_PHP' excludeTables = $this->parseNewlineList($profile->exclude_tables ?? ''); $session->backupDir = $profile->backup_dir ?: 'administrator/components/com_mokobackup/backups'; $session->remoteStorage = $profile->remote_storage ?? 'none'; - $session->includeKickstart = (bool) ($profile->include_kickstart ?? false); + $session->includeMokoRestore = (bool) ($profile->include_mokorestore ?? false); $session->remoteKeepLocal = (bool) ($profile->remote_keep_local ?? true); // Build archive path @@ -288,7 +288,7 @@ class SteppedBackupEngine } /** - * Finalize phase: add database.sql to ZIP, apply kickstart wrapper. + * Finalize phase: add database.sql to ZIP, apply MokoRestore wrapper. */ private function stepFinalize(SteppedSession $session): void { @@ -314,15 +314,15 @@ class SteppedBackupEngine $totalSize = file_exists($session->archivePath) ? filesize($session->archivePath) : 0; - // Kickstart wrapper - if ($session->includeKickstart) { - $session->log('Wrapping with Kickstart restore script...'); - $kickstartPath = $session->archivePath . '.kickstart.zip'; - Kickstart::wrap($session->archivePath, $kickstartPath); + // MokoRestore wrapper + if ($session->includeMokoRestore) { + $session->log('Wrapping with MokoRestore script...'); + $mokoRestorePath = $session->archivePath . '.mokorestore.zip'; + MokoRestore::wrap($session->archivePath, $mokoRestorePath); @unlink($session->archivePath); - rename($kickstartPath, $session->archivePath); + rename($mokoRestorePath, $session->archivePath); $totalSize = filesize($session->archivePath); - $session->log('Kickstart archive created'); + $session->log('MokoRestore archive created'); } // Update record diff --git a/src/packages/com_mokobackup/src/Engine/SteppedSession.php b/src/packages/com_mokobackup/src/Engine/SteppedSession.php index b4f3002..76f000d 100644 --- a/src/packages/com_mokobackup/src/Engine/SteppedSession.php +++ b/src/packages/com_mokobackup/src/Engine/SteppedSession.php @@ -51,7 +51,7 @@ class SteppedSession public array $excludeFiles = []; public array $excludeTables = []; public string $remoteStorage = 'none'; - public bool $includeKickstart = false; + public bool $includeMokoRestore = false; public bool $remoteKeepLocal = true; // Progress -- 2.52.0 From e5ca71f2c58f39e2c953ce9f645a6de4fc02b15e Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 07:40:04 -0500 Subject: [PATCH 04/42] chore: sync workflows from moko-platform + fix SQL install path - Sync all universal workflows from moko-platform v09.23.00 - Add pre-release.yml for dev/alpha/beta/rc builds - Add auto-bump.yml, branch-cleanup.yml, issue-branch.yml, update-server.yml - Fix mokobackup.xml: include install/uninstall SQL files in section Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .mokogitea/workflows/auto-bump.yml | 66 +++++ .mokogitea/workflows/auto-release.yml | 15 +- .mokogitea/workflows/branch-cleanup.yml | 48 ++++ .mokogitea/workflows/cleanup.yml | 2 +- .mokogitea/workflows/gitleaks.yml | 2 +- .mokogitea/workflows/issue-branch.yml | 73 +++++ .mokogitea/workflows/notify.yml | 2 +- .mokogitea/workflows/pre-release.yml | 224 +++++++++++++++ .mokogitea/workflows/repo-health.yml | 3 +- .mokogitea/workflows/security-audit.yml | 2 +- .mokogitea/workflows/update-server.yml | 302 +++++++++++++++++++++ src/packages/com_mokobackup/mokobackup.xml | 3 +- 12 files changed, 721 insertions(+), 21 deletions(-) create mode 100644 .mokogitea/workflows/auto-bump.yml create mode 100644 .mokogitea/workflows/branch-cleanup.yml create mode 100644 .mokogitea/workflows/issue-branch.yml create mode 100644 .mokogitea/workflows/pre-release.yml create mode 100644 .mokogitea/workflows/update-server.yml diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml new file mode 100644 index 0000000..33aff71 --- /dev/null +++ b/.mokogitea/workflows/auto-bump.yml @@ -0,0 +1,66 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /.mokogitea/workflows/auto-bump.yml +# VERSION: 09.23.00 +# BRIEF: Auto patch-bump version on every push to dev (skips merge commits) + +name: "Universal: Auto Version Bump" + +on: + push: + branches: + - dev + - rc + - 'feature/**' + - 'patch/**' + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + +permissions: + contents: write + +jobs: + bump: + name: Version Bump + runs-on: release + if: >- + !contains(github.event.head_commit.message, '[skip ci]') && + !contains(github.event.head_commit.message, '[skip bump]') && + !startsWith(github.event.head_commit.message, 'Merge pull request') + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 1 + + - name: Setup moko-platform tools + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + if [ -d "/opt/moko-platform/cli" ]; then + echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV" + else + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet + echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" + fi + + - name: Bump version + run: | + php ${MOKO_CLI}/version_auto_bump.php \ + --path . --branch "${GITHUB_REF_NAME}" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 78dec4b..6fb2b44 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -7,7 +7,7 @@ # INGROUP: moko-platform.Release # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform # PATH: /templates/workflows/universal/auto-release.yml.template -# VERSION: 05.00.00 +# VERSION: 09.23.00 # BRIEF: Universal build & release � detects platform from manifest.xml # # +========================================================================+ @@ -131,19 +131,6 @@ 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/branch-cleanup.yml b/.mokogitea/workflows/branch-cleanup.yml new file mode 100644 index 0000000..67a735f --- /dev/null +++ b/.mokogitea/workflows/branch-cleanup.yml @@ -0,0 +1,48 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoPlatform.Universal +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /.mokogitea/workflows/branch-cleanup.yml +# VERSION: 09.23.00 +# BRIEF: Delete feature branches after PR merge + +name: "Branch Cleanup" + +on: + pull_request: + types: [closed] + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + cleanup: + name: Delete merged branch + runs-on: ubuntu-latest + if: >- + github.event.pull_request.merged == true && + github.event.pull_request.head.ref != 'dev' && + github.event.pull_request.head.ref != 'main' + + steps: + - name: Delete source branch + run: | + BRANCH="${{ github.event.pull_request.head.ref }}" + API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches" + ENCODED=$(php -r "echo rawurlencode('${BRANCH}');") + + STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \ + -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ + "${API}/${ENCODED}" 2>/dev/null || true) + + if [ "$STATUS" = "204" ]; then + echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY + elif [ "$STATUS" = "404" ]; then + echo "Branch already deleted: ${BRANCH}" >> $GITHUB_STEP_SUMMARY + else + echo "::warning::Failed to delete branch ${BRANCH} (HTTP ${STATUS})" + fi diff --git a/.mokogitea/workflows/cleanup.yml b/.mokogitea/workflows/cleanup.yml index 29ca4d4..70521b3 100644 --- a/.mokogitea/workflows/cleanup.yml +++ b/.mokogitea/workflows/cleanup.yml @@ -7,7 +7,7 @@ # INGROUP: moko-platform.Maintenance # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform # PATH: /.gitea/workflows/cleanup.yml -# VERSION: 01.00.00 +# VERSION: 09.23.00 # BRIEF: Scheduled cleanup — delete merged branches and old workflow runs name: "Universal: Repository Cleanup" diff --git a/.mokogitea/workflows/gitleaks.yml b/.mokogitea/workflows/gitleaks.yml index e0fdd1d..9126c91 100644 --- a/.mokogitea/workflows/gitleaks.yml +++ b/.mokogitea/workflows/gitleaks.yml @@ -7,7 +7,7 @@ # INGROUP: moko-platform.Security # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform # PATH: /templates/workflows/gitleaks.yml.template -# VERSION: 01.00.00 +# VERSION: 09.23.00 # BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens # # +========================================================================+ diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml new file mode 100644 index 0000000..3c44a06 --- /dev/null +++ b/.mokogitea/workflows/issue-branch.yml @@ -0,0 +1,73 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Automation +# VERSION: 09.23.00 +# BRIEF: Auto-create feature branch when an issue is opened + +name: "Universal: Issue Branch" + +on: + issues: + types: [opened] + +permissions: + contents: write + issues: write + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + +jobs: + create-branch: + name: Create feature branch + runs-on: ubuntu-latest + steps: + - name: Create branch and comment + run: | + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + ISSUE_NUM="${{ github.event.issue.number }}" + ISSUE_TITLE="${{ github.event.issue.title }}" + + # Build slug from title: lowercase, replace non-alnum with dash, trim + SLUG=$(echo "${ISSUE_TITLE}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-40) + BRANCH="feature/${ISSUE_NUM}-${SLUG}" + + # Check dev branch exists + DEV_EXISTS=$(curl -sf -o /dev/null -w '%{http_code}' \ + -H "Authorization: token ${TOKEN}" \ + "${API}/branches/dev" 2>/dev/null || echo "000") + + if [ "${DEV_EXISTS}" != "200" ]; then + echo "No dev branch -- skipping" + exit 0 + fi + + # Create branch from dev + HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \ + -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/branches" \ + -d "{\"new_branch_name\":\"${BRANCH}\",\"old_branch_name\":\"dev\"}" 2>/dev/null || echo "000") + + if [ "${HTTP}" = "201" ]; then + echo "Created branch: ${BRANCH}" + + # Comment on issue with branch link + REPO_URL="${GITEA_URL}/${{ github.repository }}" + BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`" + + curl -sf -X POST \ + -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/issues/${ISSUE_NUM}/comments" \ + -d "{\"body\":\"${BODY}\"}" > /dev/null 2>&1 + + echo "Commented on issue #${ISSUE_NUM}" + else + echo "Failed to create branch (HTTP ${HTTP}) -- may already exist" + fi diff --git a/.mokogitea/workflows/notify.yml b/.mokogitea/workflows/notify.yml index cde4541..c18b809 100644 --- a/.mokogitea/workflows/notify.yml +++ b/.mokogitea/workflows/notify.yml @@ -7,7 +7,7 @@ # INGROUP: moko-platform.Notifications # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform # PATH: /.gitea/workflows/notify.yml -# VERSION: 01.00.00 +# VERSION: 09.23.00 # BRIEF: Push notifications via ntfy on release success or workflow failure name: "Universal: Notifications" diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml new file mode 100644 index 0000000..ff818ba --- /dev/null +++ b/.mokogitea/workflows/pre-release.yml @@ -0,0 +1,224 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /templates/workflows/universal/pre-release.yml.template +# VERSION: 09.23.00 +# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch + +name: "Universal: Pre-Release" + +on: + pull_request: + types: [closed] + branches: + - dev + workflow_dispatch: + inputs: + stability: + description: 'Pre-release channel' + required: true + type: choice + options: + - development + - alpha + - beta + - release-candidate + +permissions: + contents: write + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +jobs: + build: + name: "Build Pre-Release (${{ inputs.stability || 'development' }})" + runs-on: release + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.MOKOGITEA_TOKEN }} + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform-api + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet + echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" + + - name: Detect platform + id: platform + run: | + php ${MOKO_CLI}/manifest_read.php --path . --github-output + + - name: Resolve metadata and bump version + id: meta + run: | + STABILITY="${{ inputs.stability || 'development' }}" + + case "$STABILITY" in + development) TAG="development" ;; + alpha) TAG="alpha" ;; + beta) TAG="beta" ;; + release-candidate) TAG="release-candidate" ;; + esac + + # Set stability suffix, bump preserves it, fix consistency + php ${MOKO_CLI}/version_set_platform.php \ + --path . --version "$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo '00.00.01')" \ + --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true + php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + + # Read final version (includes suffix, e.g. 01.02.15-dev) + VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) + [ -z "$VERSION" ] && VERSION="00.00.01" + + # Commit version bump + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + 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" + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" + git push origin HEAD 2>&1 + } + + # Auto-detect element via manifest_element.php + php ${MOKO_CLI}/manifest_element.php \ + --path . --version "$VERSION" --stability "$STABILITY" \ + --repo "${GITEA_REPO}" --github-output + + # Read back element outputs + EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" + echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" + + echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION} ===" + + - name: Create release + id: release + run: | + TAG="${{ steps.meta.outputs.tag }}" + VERSION="${{ steps.meta.outputs.version }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_create.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --branch dev --prerelease + + - name: Build package and upload + id: package + run: | + VERSION="${{ steps.meta.outputs.version }}" + TAG="${{ steps.meta.outputs.tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php ${MOKO_CLI}/release_package.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --output /tmp || true + + - name: Update updates.xml + if: steps.platform.outputs.platform == 'joomla' + run: | + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + SHA256="${{ steps.package.outputs.sha256_zip }}" + + if [ ! -f "updates.xml" ]; then + echo "No updates.xml -- skipping" + exit 0 + fi + + SHA_FLAG="" + [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}" + + php ${MOKO_CLI}/updates_xml_build.php \ + --path . --version "${VERSION}" --stability "${STABILITY}" \ + --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ + ${SHA_FLAG} + + # Commit and push + if ! git diff --quiet updates.xml 2>/dev/null; then + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git add updates.xml + git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" + git push origin HEAD 2>&1 || echo "WARNING: push failed" + fi + + - name: "Sync updates.xml to all branches" + if: steps.platform.outputs.platform == 'joomla' + run: | + CURRENT_BRANCH="${{ github.ref_name }}" + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + + for BRANCH in main dev; do + [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue + echo "Syncing updates.xml -> ${BRANCH}" + git fetch origin "${BRANCH}" 2>/dev/null || continue + git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue + git checkout "${CURRENT_BRANCH}" -- updates.xml + if ! git diff --quiet updates.xml 2>/dev/null; then + git add updates.xml + git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]" + git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed" + fi + git checkout "${CURRENT_BRANCH}" 2>/dev/null + done + + - name: "Delete lesser pre-release channels (cascade)" + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + php ${MOKO_CLI}/release_cascade.php \ + --stability "${{ steps.meta.outputs.stability }}" \ + --token "${TOKEN}" \ + --api-base "${API_BASE}" + + - name: Summary + if: always() + run: | + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + SHA256="${{ steps.package.outputs.sha256_zip }}" + echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY + echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY + echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml index 03290b0..e3653eb 100644 --- a/.mokogitea/workflows/repo-health.yml +++ b/.mokogitea/workflows/repo-health.yml @@ -41,8 +41,7 @@ permissions: env: # Release policy - Repository Variables Only - # RS_FTP_PATH_SUFFIX removed — MokoGitea handles all releases now - RELEASE_REQUIRED_REPO_VARS: + RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX # Scripts governance policy diff --git a/.mokogitea/workflows/security-audit.yml b/.mokogitea/workflows/security-audit.yml index 714d407..1bd9470 100644 --- a/.mokogitea/workflows/security-audit.yml +++ b/.mokogitea/workflows/security-audit.yml @@ -7,7 +7,7 @@ # INGROUP: moko-platform.Security # REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform # PATH: /.gitea/workflows/security-audit.yml -# VERSION: 01.00.00 +# VERSION: 09.23.00 # BRIEF: Dependency vulnerability scanning for composer and npm packages name: "Universal: Security Audit" diff --git a/.mokogitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml new file mode 100644 index 0000000..ac5c9a5 --- /dev/null +++ b/.mokogitea/workflows/update-server.yml @@ -0,0 +1,302 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Universal +# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform +# PATH: /templates/workflows/update-server.yml +# VERSION: 09.23.00 +# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches +# +# Thin wrapper around moko-platform CLI tools. +# Builds packages, updates updates.xml, and optionally deploys via SFTP. +# +# Joomla filters update entries by the user's "Minimum Stability" setting. + +name: "Update Server" + +on: + push: + branches: + - 'dev' + - 'dev/**' + - 'alpha/**' + - 'beta/**' + - 'rc/**' + paths: + - 'src/**' + - 'htdocs/**' + pull_request: + types: [closed] + branches: + - 'dev' + - 'dev/**' + - 'alpha/**' + - 'beta/**' + - 'rc/**' + paths: + - 'src/**' + - 'htdocs/**' + workflow_dispatch: + inputs: + stability: + description: 'Stability tag' + required: true + default: 'development' + type: choice + options: + - development + - alpha + - beta + - rc + - stable + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + update-xml: + name: Update Server + runs-on: release + if: >- + github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 0 + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}' + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform 2>/dev/null || true + if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then + cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true + fi + echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV" + + - name: Detect platform + id: platform + run: php ${MOKO_CLI}/manifest_read.php --path . --github-output + + - name: Resolve stability and bump version + id: meta + run: | + BRANCH="${{ github.ref_name }}" + + # Configure git for bot pushes + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + 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" + + # Determine stability from branch or manual input + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + STABILITY="${{ inputs.stability }}" + elif [[ "$BRANCH" == rc/* ]]; then + STABILITY="rc" + elif [[ "$BRANCH" == beta/* ]]; then + STABILITY="beta" + elif [[ "$BRANCH" == alpha/* ]]; then + STABILITY="alpha" + else + STABILITY="development" + fi + + # Gitea release tag per stability + case "$STABILITY" in + development) TAG="development" ;; + alpha) TAG="alpha" ;; + beta) TAG="beta" ;; + rc) TAG="release-candidate" ;; + *) TAG="stable" ;; + esac + + # Bump patch, set platform suffix, fix consistency — version_bump preserves suffix + php ${MOKO_CLI}/version_set_platform.php \ + --path . --version "$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo '00.00.01')" \ + --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true + php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true + php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + + # Read final version (includes suffix, e.g. 01.02.15-dev) + VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01") + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + + # Commit version bump if changed + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] " + git push + } + + - name: Create release and upload package + id: package + run: | + VERSION="${{ steps.meta.outputs.version }}" + TAG="${{ steps.meta.outputs.tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # Create or update Gitea release + php ${MOKO_CLI}/release_create.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease + + # Build package and upload + php ${MOKO_CLI}/release_package.php \ + --path . --version "$VERSION" --tag "$TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --repo "${GITEA_REPO}" --output /tmp || true + + - name: Update updates.xml + if: steps.platform.outputs.platform == 'joomla' + run: | + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + SHA256="${{ steps.package.outputs.sha256_zip }}" + + if [ ! -f "updates.xml" ]; then + echo "No updates.xml — skipping" + exit 0 + fi + + SHA_FLAG="" + [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}" + + php ${MOKO_CLI}/updates_xml_build.php \ + --path . --version "${VERSION}" --stability "${STABILITY}" \ + --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ + ${SHA_FLAG} + + # Commit and push updates.xml + git add updates.xml + git diff --cached --quiet || { + git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" + git push + } + + - name: Sync updates.xml to main + if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla' + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \ + "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) + + if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then + python3 -c " + import base64, json, urllib.request, sys + with open('updates.xml', 'rb') as f: + content = base64.b64encode(f.read()).decode() + payload = json.dumps({ + 'content': content, + 'sha': '${FILE_SHA}', + 'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]', + 'branch': 'main' + }).encode() + req = urllib.request.Request( + '${API_BASE}/contents/updates.xml', + data=payload, method='PUT', + headers={ + 'Authorization': 'token ${GITEA_TOKEN}', + 'Content-Type': 'application/json' + }) + try: + urllib.request.urlopen(req) + print('updates.xml synced to main') + except Exception as e: + print(f'WARNING: sync to main failed: {e}', file=sys.stderr) + " + fi + + - name: SFTP deploy to dev server + if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev' + env: + DEV_HOST: ${{ vars.DEV_FTP_HOST }} + DEV_PATH: ${{ vars.DEV_FTP_PATH }} + DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} + DEV_USER: ${{ vars.DEV_FTP_USERNAME }} + DEV_PORT: ${{ vars.DEV_FTP_PORT }} + DEV_KEY: ${{ secrets.DEV_FTP_KEY }} + DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }} + run: | + # Permission check: admin or maintain role required + ACTOR="${{ github.actor }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ + "${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read") + case "$PERMISSION" in + admin|maintain|write) ;; + *) + echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write" + exit 0 + ;; + esac + + [ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; } + + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + [ ! -d "$SOURCE_DIR" ] && exit 0 + + PORT="${DEV_PORT:-22}" + REMOTE="${DEV_PATH%/}" + [ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}" + + printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \ + "$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json + if [ -n "$DEV_KEY" ]; then + echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key + printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json + else + printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json + fi + + PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true) + if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then + php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json + elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then + php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json + fi + rm -f /tmp/deploy_key /tmp/sftp-config.json + echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY + + - name: Summary + if: always() + run: | + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + DISPLAY="${VERSION}" + echo "## Update Server" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY diff --git a/src/packages/com_mokobackup/mokobackup.xml b/src/packages/com_mokobackup/mokobackup.xml index 4711c60..a4ab7ec 100644 --- a/src/packages/com_mokobackup/mokobackup.xml +++ b/src/packages/com_mokobackup/mokobackup.xml @@ -64,7 +64,8 @@ profile - mysql + install.mysql.sql + uninstall.mysql.sql updates -- 2.52.0 From 1f225689babf3a269c15503dc067686e82c11cdf Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Thu, 4 Jun 2026 12:40:59 +0000 Subject: [PATCH 05/42] chore(version): auto-bump 01.01.01-dev [skip ci] --- .mokogitea/manifest.xml | 2 +- .mokogitea/workflows/issue-branch.yml | 2 +- README.md | 2 +- src/packages/com_mokobackup/mokobackup.xml | 2 +- src/packages/plg_quickicon_mokobackup/mokobackup.xml | 2 +- src/packages/plg_system_mokobackup/mokobackup.xml | 2 +- src/packages/plg_task_mokobackup/mokobackup.xml | 2 +- src/packages/plg_webservices_mokobackup/mokobackup.xml | 2 +- src/pkg_mokobackup.xml | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 5ef8494..1b87e71 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -5,7 +5,7 @@ Package - MokoJoomBackup MokoConsulting Full-site backup and restore for Joomla — database, files, and configuration - 01.01.00-dev + 01.01.01-dev GNU General Public License v3 diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 3c44a06..c5182e8 100644 --- a/.mokogitea/workflows/issue-branch.yml +++ b/.mokogitea/workflows/issue-branch.yml @@ -5,7 +5,7 @@ # FILE INFORMATION # DEFGROUP: Gitea.Workflow # INGROUP: moko-platform.Automation -# VERSION: 09.23.00 +# VERSION: 01.01.01 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/README.md b/README.md index ec51852..c999d0f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoJoomBackup - + Full-site backup and restore for Joomla — database, files, and configuration. diff --git a/src/packages/com_mokobackup/mokobackup.xml b/src/packages/com_mokobackup/mokobackup.xml index a4ab7ec..3c2484a 100644 --- a/src/packages/com_mokobackup/mokobackup.xml +++ b/src/packages/com_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> com_mokobackup - 01.01.00-dev + 01.01.01-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_quickicon_mokobackup/mokobackup.xml b/src/packages/plg_quickicon_mokobackup/mokobackup.xml index 5a55e79..ae56eeb 100644 --- a/src/packages/plg_quickicon_mokobackup/mokobackup.xml +++ b/src/packages/plg_quickicon_mokobackup/mokobackup.xml @@ -1,7 +1,7 @@ plg_quickicon_mokobackup - 01.01.00-dev + 01.01.01-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_system_mokobackup/mokobackup.xml b/src/packages/plg_system_mokobackup/mokobackup.xml index 1f1b1eb..681ccd3 100644 --- a/src/packages/plg_system_mokobackup/mokobackup.xml +++ b/src/packages/plg_system_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_system_mokobackup - 01.01.00-dev + 01.01.01-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_task_mokobackup/mokobackup.xml b/src/packages/plg_task_mokobackup/mokobackup.xml index ab99004..fc31566 100644 --- a/src/packages/plg_task_mokobackup/mokobackup.xml +++ b/src/packages/plg_task_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_task_mokobackup - 01.01.00-dev + 01.01.01-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_webservices_mokobackup/mokobackup.xml b/src/packages/plg_webservices_mokobackup/mokobackup.xml index e54426a..f04a3a3 100644 --- a/src/packages/plg_webservices_mokobackup/mokobackup.xml +++ b/src/packages/plg_webservices_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_webservices_mokobackup - 01.01.00-dev + 01.01.01-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/pkg_mokobackup.xml b/src/pkg_mokobackup.xml index 2de7a14..9ae5d7a 100644 --- a/src/pkg_mokobackup.xml +++ b/src/pkg_mokobackup.xml @@ -8,7 +8,7 @@ Package - MokoJoomBackup mokobackup - 01.01.00-dev + 01.01.01-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech -- 2.52.0 From 3d88281e72ee38c40740a8100d798f36b5834302 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Thu, 4 Jun 2026 12:41:02 +0000 Subject: [PATCH 06/42] chore: update development channel 01.01.01-dev [skip ci] --- updates.xml | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/updates.xml b/updates.xml index 5ba8cf1..daf6dce 100644 --- a/updates.xml +++ b/updates.xml @@ -1,15 +1,27 @@ - + + + - - Package - MokoJoomBackup - Full-site backup and restore for Joomla - mokobackup - package - 01.00.00 - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/tag/v01.00.00 - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/download/v01.00.00/pkg_mokobackup-01.00.00.zip - - - 8.1.0 - + + Package - MokoJoomBackup + Package - MokoJoomBackup development build. + pkg_mokobackup + package + site + 01.01.01-dev + 2026-06-04 + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/tag/development + + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/download/development/pkg_mokobackup-01.01.01-dev.zip + + 4c2702d04479863bbffef705f55fe10f8ffbe01c598d9fb4b410bb117fac1ff2 + dev + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/raw/branch/main/CHANGELOG.md + Moko Consulting + https://mokoconsulting.tech + + -- 2.52.0 From 6ed0eee4a1849e2fce3b8d4a7ad2d9e3aafbb590 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 08:01:05 -0500 Subject: [PATCH 07/42] feat: add update site notice on dashboard and post-install Link directly to the Joomla Update Sites record for pkg_mokobackup on the Backups dashboard and after install/update, so users can configure their download key without a license warning popup. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../language/en-GB/com_mokobackup.ini | 5 ++ .../language/en-US/com_mokobackup.ini | 3 ++ .../src/View/Backups/HtmlView.php | 43 ++++++++++++++++ src/script.php | 50 +++++++++++++++++++ 4 files changed, 101 insertions(+) diff --git a/src/packages/com_mokobackup/language/en-GB/com_mokobackup.ini b/src/packages/com_mokobackup/language/en-GB/com_mokobackup.ini index dd9c1d0..0921f20 100644 --- a/src/packages/com_mokobackup/language/en-GB/com_mokobackup.ini +++ b/src/packages/com_mokobackup/language/en-GB/com_mokobackup.ini @@ -189,6 +189,11 @@ COM_MOKOBACKUP_FIELD_S3_PATH_DESC="Optional path prefix inside the bucket (e.g. COM_MOKOBACKUP_TOOLBAR_IMPORT_AKEEBA="Import from Akeeba" COM_MOKOBACKUP_AKEEBA_NOT_FOUND="Akeeba Backup tables not found. Is Akeeba Backup Pro installed?" +; Update site notice +COM_MOKOBACKUP_UPDATE_SITE_NOTICE="To receive automatic updates, configure your Update Site with your download key." +COM_MOKOBACKUP_UPDATE_SITE_MISSING="MokoJoomBackup update site not found. Reinstall the package to register the update server." +COM_MOKOBACKUP_POSTINSTALL_UPDATE_SITE="MokoJoomBackup installed successfully. Configure your Update Site to receive automatic updates." + ; Errors COM_MOKOBACKUP_ERROR_FILE_NOT_FOUND="Backup archive file not found or has been deleted." COM_MOKOBACKUP_ERROR_NO_RECORD_SELECTED="No backup record selected for restore." diff --git a/src/packages/com_mokobackup/language/en-US/com_mokobackup.ini b/src/packages/com_mokobackup/language/en-US/com_mokobackup.ini index 2f000f1..309264d 100644 --- a/src/packages/com_mokobackup/language/en-US/com_mokobackup.ini +++ b/src/packages/com_mokobackup/language/en-US/com_mokobackup.ini @@ -13,3 +13,6 @@ COM_MOKOBACKUP_PROFILES_TITLE="Backup Profiles" COM_MOKOBACKUP_TOOLBAR_BACKUP_NOW="Backup Now" COM_MOKOBACKUP_NO_BACKUPS="No backups found. Click 'Backup Now' to create your first backup." COM_MOKOBACKUP_NO_PROFILES="No backup profiles found." +COM_MOKOBACKUP_UPDATE_SITE_NOTICE="To receive automatic updates, configure your Update Site with your download key." +COM_MOKOBACKUP_UPDATE_SITE_MISSING="MokoJoomBackup update site not found. Reinstall the package to register the update server." +COM_MOKOBACKUP_POSTINSTALL_UPDATE_SITE="MokoJoomBackup installed successfully. Configure your Update Site to receive automatic updates." diff --git a/src/packages/com_mokobackup/src/View/Backups/HtmlView.php b/src/packages/com_mokobackup/src/View/Backups/HtmlView.php index 1dde363..b6fdf9f 100644 --- a/src/packages/com_mokobackup/src/View/Backups/HtmlView.php +++ b/src/packages/com_mokobackup/src/View/Backups/HtmlView.php @@ -15,6 +15,7 @@ defined('_JEXEC') or die; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Router\Route; use Joomla\CMS\Toolbar\ToolbarHelper; class HtmlView extends BaseHtmlView @@ -44,11 +45,53 @@ class HtmlView extends BaseHtmlView $db->setQuery($query); $this->profiles = $db->loadObjectList() ?: []; + $this->checkUpdateSite(); $this->addToolbar(); parent::display($tpl); } + /** + * Show an info notice linking to the update site record so the user + * can configure their download key for automatic updates. + */ + protected function checkUpdateSite(): void + { + $db = Factory::getDbo(); + + // Find the update site ID linked to pkg_mokobackup + $query = $db->getQuery(true) + ->select($db->quoteName('us.update_site_id')) + ->from($db->quoteName('#__update_sites', 'us')) + ->join( + 'INNER', + $db->quoteName('#__update_sites_extensions', 'use') + . ' ON ' . $db->quoteName('use.update_site_id') . ' = ' . $db->quoteName('us.update_site_id') + ) + ->join( + 'INNER', + $db->quoteName('#__extensions', 'e') + . ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id') + ) + ->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokobackup')) + ->where($db->quoteName('e.type') . ' = ' . $db->quote('package')) + ->setLimit(1); + + $db->setQuery($query); + $updateSiteId = (int) $db->loadResult(); + + if ($updateSiteId > 0) { + $editUrl = Route::_( + 'index.php?option=com_installer&view=updatesites&task=updatesite.edit&id=' . $updateSiteId + ); + + Factory::getApplication()->enqueueMessage( + Text::sprintf('COM_MOKOBACKUP_UPDATE_SITE_NOTICE', $editUrl), + 'info' + ); + } + } + protected function addToolbar(): void { ToolbarHelper::title(Text::_('COM_MOKOBACKUP_BACKUPS_TITLE'), 'database'); diff --git a/src/script.php b/src/script.php index ed0db68..ea6ed40 100644 --- a/src/script.php +++ b/src/script.php @@ -12,6 +12,7 @@ defined('_JEXEC') or die; use Joomla\CMS\Factory; use Joomla\CMS\Installer\InstallerAdapter; use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; class Pkg_MokoBackupInstallerScript { @@ -118,5 +119,54 @@ class Pkg_MokoBackupInstallerScript file_put_contents($backupDir . '/index.html', ''); } } + + // Show update site link after install or update + $this->showUpdateSiteNotice(); + } + + /** + * Show an info message linking directly to the update site record + * so the user can configure their download key. + * + * @return void + */ + private function showUpdateSiteNotice(): void + { + try { + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select($db->quoteName('us.update_site_id')) + ->from($db->quoteName('#__update_sites', 'us')) + ->join( + 'INNER', + $db->quoteName('#__update_sites_extensions', 'use') + . ' ON ' . $db->quoteName('use.update_site_id') . ' = ' . $db->quoteName('us.update_site_id') + ) + ->join( + 'INNER', + $db->quoteName('#__extensions', 'e') + . ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id') + ) + ->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokobackup')) + ->where($db->quoteName('e.type') . ' = ' . $db->quote('package')) + ->setLimit(1); + + $db->setQuery($query); + $updateSiteId = (int) $db->loadResult(); + + if ($updateSiteId > 0) { + $editUrl = Route::_( + 'index.php?option=com_installer&view=updatesites&task=updatesite.edit&id=' . $updateSiteId + ); + + Factory::getApplication()->enqueueMessage( + Text::sprintf('COM_MOKOBACKUP_POSTINSTALL_UPDATE_SITE', $editUrl), + 'info' + ); + } + } catch (\Throwable $e) { + // Non-critical — silently ignore + } } } -- 2.52.0 From 44e309c57f7690cbc76e43a815fee3b7e51a0d82 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Thu, 4 Jun 2026 13:02:04 +0000 Subject: [PATCH 08/42] chore(version): auto-bump 01.01.02-dev [skip ci] --- .mokogitea/manifest.xml | 2 +- .mokogitea/workflows/issue-branch.yml | 2 +- README.md | 2 +- src/packages/com_mokobackup/mokobackup.xml | 2 +- src/packages/plg_quickicon_mokobackup/mokobackup.xml | 2 +- src/packages/plg_system_mokobackup/mokobackup.xml | 2 +- src/packages/plg_task_mokobackup/mokobackup.xml | 2 +- src/packages/plg_webservices_mokobackup/mokobackup.xml | 2 +- src/pkg_mokobackup.xml | 2 +- updates.xml | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 1b87e71..d603390 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -5,7 +5,7 @@ Package - MokoJoomBackup MokoConsulting Full-site backup and restore for Joomla — database, files, and configuration - 01.01.01-dev + 01.01.02-dev GNU General Public License v3 diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index c5182e8..96d0c2a 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: 01.01.01 +# VERSION: 01.01.02 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/README.md b/README.md index c999d0f..edb0f31 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoJoomBackup - + Full-site backup and restore for Joomla — database, files, and configuration. diff --git a/src/packages/com_mokobackup/mokobackup.xml b/src/packages/com_mokobackup/mokobackup.xml index 3c2484a..12c60e3 100644 --- a/src/packages/com_mokobackup/mokobackup.xml +++ b/src/packages/com_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> com_mokobackup - 01.01.01-dev + 01.01.02-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_quickicon_mokobackup/mokobackup.xml b/src/packages/plg_quickicon_mokobackup/mokobackup.xml index ae56eeb..4f7b287 100644 --- a/src/packages/plg_quickicon_mokobackup/mokobackup.xml +++ b/src/packages/plg_quickicon_mokobackup/mokobackup.xml @@ -1,7 +1,7 @@ plg_quickicon_mokobackup - 01.01.01-dev + 01.01.02-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_system_mokobackup/mokobackup.xml b/src/packages/plg_system_mokobackup/mokobackup.xml index 681ccd3..776b919 100644 --- a/src/packages/plg_system_mokobackup/mokobackup.xml +++ b/src/packages/plg_system_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_system_mokobackup - 01.01.01-dev + 01.01.02-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_task_mokobackup/mokobackup.xml b/src/packages/plg_task_mokobackup/mokobackup.xml index fc31566..85c68af 100644 --- a/src/packages/plg_task_mokobackup/mokobackup.xml +++ b/src/packages/plg_task_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_task_mokobackup - 01.01.01-dev + 01.01.02-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_webservices_mokobackup/mokobackup.xml b/src/packages/plg_webservices_mokobackup/mokobackup.xml index f04a3a3..31cabb6 100644 --- a/src/packages/plg_webservices_mokobackup/mokobackup.xml +++ b/src/packages/plg_webservices_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_webservices_mokobackup - 01.01.01-dev + 01.01.02-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/pkg_mokobackup.xml b/src/pkg_mokobackup.xml index 9ae5d7a..9aec17f 100644 --- a/src/pkg_mokobackup.xml +++ b/src/pkg_mokobackup.xml @@ -8,7 +8,7 @@ Package - MokoJoomBackup mokobackup - 01.01.01-dev + 01.01.02-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/updates.xml b/updates.xml index daf6dce..6fb1ca9 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ -- 2.52.0 From 5ee4f7a578c628eb58fb8ee9e4349062bea0787d Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Thu, 4 Jun 2026 13:02:06 +0000 Subject: [PATCH 09/42] chore: update development channel 01.01.02-dev [skip ci] --- updates.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/updates.xml b/updates.xml index 6fb1ca9..deb391e 100644 --- a/updates.xml +++ b/updates.xml @@ -11,13 +11,13 @@ pkg_mokobackup package site - 01.01.01-dev + 01.01.02-dev 2026-06-04 https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/download/development/pkg_mokobackup-01.01.01-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/download/development/pkg_mokobackup-01.01.02-dev.zip - 4c2702d04479863bbffef705f55fe10f8ffbe01c598d9fb4b410bb117fac1ff2 + 9b4c337c41439dfb8292d6817951b2bb51a8015e208afdbe92444a0cb52214c0 dev https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/raw/branch/main/CHANGELOG.md Moko Consulting -- 2.52.0 From 6bab1ad5fa788f01ccad67189758a9ec7bbc1a31 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 08:15:31 -0500 Subject: [PATCH 10/42] fix: add SQL update migration and error handling for PR review - Add sql/updates/mysql/01.01.01.sql to rename include_kickstart column to include_mokorestore for existing installations - Wrap checkUpdateSite() in try/catch to prevent dashboard crash - Use COM_MOKOBACKUP_UPDATE_SITE_MISSING when no update site found Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../sql/updates/mysql/01.01.01.sql | 1 + .../src/View/Backups/HtmlView.php | 65 +++++++++++-------- 2 files changed, 38 insertions(+), 28 deletions(-) create mode 100644 src/packages/com_mokobackup/sql/updates/mysql/01.01.01.sql diff --git a/src/packages/com_mokobackup/sql/updates/mysql/01.01.01.sql b/src/packages/com_mokobackup/sql/updates/mysql/01.01.01.sql new file mode 100644 index 0000000..ef33f11 --- /dev/null +++ b/src/packages/com_mokobackup/sql/updates/mysql/01.01.01.sql @@ -0,0 +1 @@ +ALTER TABLE `#__mokobackup_profiles` CHANGE `include_kickstart` `include_mokorestore` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Include MokoRestore standalone restore script in archive'; diff --git a/src/packages/com_mokobackup/src/View/Backups/HtmlView.php b/src/packages/com_mokobackup/src/View/Backups/HtmlView.php index b6fdf9f..f1a224c 100644 --- a/src/packages/com_mokobackup/src/View/Backups/HtmlView.php +++ b/src/packages/com_mokobackup/src/View/Backups/HtmlView.php @@ -57,38 +57,47 @@ class HtmlView extends BaseHtmlView */ protected function checkUpdateSite(): void { - $db = Factory::getDbo(); + try { + $db = Factory::getDbo(); - // Find the update site ID linked to pkg_mokobackup - $query = $db->getQuery(true) - ->select($db->quoteName('us.update_site_id')) - ->from($db->quoteName('#__update_sites', 'us')) - ->join( - 'INNER', - $db->quoteName('#__update_sites_extensions', 'use') - . ' ON ' . $db->quoteName('use.update_site_id') . ' = ' . $db->quoteName('us.update_site_id') - ) - ->join( - 'INNER', - $db->quoteName('#__extensions', 'e') - . ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id') - ) - ->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokobackup')) - ->where($db->quoteName('e.type') . ' = ' . $db->quote('package')) - ->setLimit(1); + // Find the update site ID linked to pkg_mokobackup + $query = $db->getQuery(true) + ->select($db->quoteName('us.update_site_id')) + ->from($db->quoteName('#__update_sites', 'us')) + ->join( + 'INNER', + $db->quoteName('#__update_sites_extensions', 'use') + . ' ON ' . $db->quoteName('use.update_site_id') . ' = ' . $db->quoteName('us.update_site_id') + ) + ->join( + 'INNER', + $db->quoteName('#__extensions', 'e') + . ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id') + ) + ->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokobackup')) + ->where($db->quoteName('e.type') . ' = ' . $db->quote('package')) + ->setLimit(1); - $db->setQuery($query); - $updateSiteId = (int) $db->loadResult(); + $db->setQuery($query); + $updateSiteId = (int) $db->loadResult(); - if ($updateSiteId > 0) { - $editUrl = Route::_( - 'index.php?option=com_installer&view=updatesites&task=updatesite.edit&id=' . $updateSiteId - ); + if ($updateSiteId > 0) { + $editUrl = Route::_( + 'index.php?option=com_installer&view=updatesites&task=updatesite.edit&id=' . $updateSiteId + ); - Factory::getApplication()->enqueueMessage( - Text::sprintf('COM_MOKOBACKUP_UPDATE_SITE_NOTICE', $editUrl), - 'info' - ); + Factory::getApplication()->enqueueMessage( + Text::sprintf('COM_MOKOBACKUP_UPDATE_SITE_NOTICE', $editUrl), + 'info' + ); + } else { + Factory::getApplication()->enqueueMessage( + Text::_('COM_MOKOBACKUP_UPDATE_SITE_MISSING'), + 'warning' + ); + } + } catch (\Throwable $e) { + // Non-critical — silently ignore } } -- 2.52.0 From 2aee667d00d84f05d5a55376adedef4175bcaaac Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Thu, 4 Jun 2026 13:16:01 +0000 Subject: [PATCH 11/42] chore(version): auto-bump 01.01.03-dev [skip ci] --- .mokogitea/manifest.xml | 2 +- .mokogitea/workflows/issue-branch.yml | 2 +- README.md | 2 +- src/packages/com_mokobackup/mokobackup.xml | 2 +- src/packages/plg_quickicon_mokobackup/mokobackup.xml | 2 +- src/packages/plg_system_mokobackup/mokobackup.xml | 2 +- src/packages/plg_task_mokobackup/mokobackup.xml | 2 +- src/packages/plg_webservices_mokobackup/mokobackup.xml | 2 +- src/pkg_mokobackup.xml | 2 +- updates.xml | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index d603390..07f0731 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -5,7 +5,7 @@ Package - MokoJoomBackup MokoConsulting Full-site backup and restore for Joomla — database, files, and configuration - 01.01.02-dev + 01.01.03-dev GNU General Public License v3 diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 96d0c2a..cc5f056 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: 01.01.02 +# VERSION: 01.01.03 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/README.md b/README.md index edb0f31..5a5b12d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoJoomBackup - + Full-site backup and restore for Joomla — database, files, and configuration. diff --git a/src/packages/com_mokobackup/mokobackup.xml b/src/packages/com_mokobackup/mokobackup.xml index 12c60e3..43a744f 100644 --- a/src/packages/com_mokobackup/mokobackup.xml +++ b/src/packages/com_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> com_mokobackup - 01.01.02-dev + 01.01.03-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_quickicon_mokobackup/mokobackup.xml b/src/packages/plg_quickicon_mokobackup/mokobackup.xml index 4f7b287..eaa988a 100644 --- a/src/packages/plg_quickicon_mokobackup/mokobackup.xml +++ b/src/packages/plg_quickicon_mokobackup/mokobackup.xml @@ -1,7 +1,7 @@ plg_quickicon_mokobackup - 01.01.02-dev + 01.01.03-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_system_mokobackup/mokobackup.xml b/src/packages/plg_system_mokobackup/mokobackup.xml index 776b919..a8d2c3b 100644 --- a/src/packages/plg_system_mokobackup/mokobackup.xml +++ b/src/packages/plg_system_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_system_mokobackup - 01.01.02-dev + 01.01.03-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_task_mokobackup/mokobackup.xml b/src/packages/plg_task_mokobackup/mokobackup.xml index 85c68af..063445a 100644 --- a/src/packages/plg_task_mokobackup/mokobackup.xml +++ b/src/packages/plg_task_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_task_mokobackup - 01.01.02-dev + 01.01.03-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_webservices_mokobackup/mokobackup.xml b/src/packages/plg_webservices_mokobackup/mokobackup.xml index 31cabb6..376fba1 100644 --- a/src/packages/plg_webservices_mokobackup/mokobackup.xml +++ b/src/packages/plg_webservices_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_webservices_mokobackup - 01.01.02-dev + 01.01.03-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/pkg_mokobackup.xml b/src/pkg_mokobackup.xml index 9aec17f..c206ba4 100644 --- a/src/pkg_mokobackup.xml +++ b/src/pkg_mokobackup.xml @@ -8,7 +8,7 @@ Package - MokoJoomBackup mokobackup - 01.01.02-dev + 01.01.03-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/updates.xml b/updates.xml index deb391e..55c0958 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ -- 2.52.0 From 70df427cfeb49dceb967b6d9af3eb70023f1f9e4 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Thu, 4 Jun 2026 13:16:02 +0000 Subject: [PATCH 12/42] chore: update development channel 01.01.03-dev [skip ci] --- updates.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/updates.xml b/updates.xml index 55c0958..8dd2da5 100644 --- a/updates.xml +++ b/updates.xml @@ -11,13 +11,13 @@ pkg_mokobackup package site - 01.01.02-dev + 01.01.03-dev 2026-06-04 https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/download/development/pkg_mokobackup-01.01.02-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/download/development/pkg_mokobackup-01.01.03-dev.zip - 9b4c337c41439dfb8292d6817951b2bb51a8015e208afdbe92444a0cb52214c0 + 3c28f5726d85769c0aa6ac7b686f1ae2a5c3eacdf72fd4dbb5d6c722d93a47fc dev https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/raw/branch/main/CHANGELOG.md Moko Consulting -- 2.52.0 From 8eab341c1ea5ff03492b5b4bc368e8cf7b2f14db Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Thu, 4 Jun 2026 13:17:49 +0000 Subject: [PATCH 13/42] chore(version): pre-release bump to 01.01.03-dev [skip ci] --- src/packages/com_mokobackup/mokobackup.xml | 2 +- src/packages/plg_quickicon_mokobackup/mokobackup.xml | 2 +- src/packages/plg_system_mokobackup/mokobackup.xml | 2 +- src/packages/plg_task_mokobackup/mokobackup.xml | 2 +- src/packages/plg_webservices_mokobackup/mokobackup.xml | 2 +- src/pkg_mokobackup.xml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/packages/com_mokobackup/mokobackup.xml b/src/packages/com_mokobackup/mokobackup.xml index 43a744f..82d67b6 100644 --- a/src/packages/com_mokobackup/mokobackup.xml +++ b/src/packages/com_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> com_mokobackup - 01.01.03-dev + 01.01.03-rc 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_quickicon_mokobackup/mokobackup.xml b/src/packages/plg_quickicon_mokobackup/mokobackup.xml index eaa988a..2d055ef 100644 --- a/src/packages/plg_quickicon_mokobackup/mokobackup.xml +++ b/src/packages/plg_quickicon_mokobackup/mokobackup.xml @@ -1,7 +1,7 @@ plg_quickicon_mokobackup - 01.01.03-dev + 01.01.03-rc 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_system_mokobackup/mokobackup.xml b/src/packages/plg_system_mokobackup/mokobackup.xml index a8d2c3b..0c51af3 100644 --- a/src/packages/plg_system_mokobackup/mokobackup.xml +++ b/src/packages/plg_system_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_system_mokobackup - 01.01.03-dev + 01.01.03-rc 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_task_mokobackup/mokobackup.xml b/src/packages/plg_task_mokobackup/mokobackup.xml index 063445a..a27fa48 100644 --- a/src/packages/plg_task_mokobackup/mokobackup.xml +++ b/src/packages/plg_task_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_task_mokobackup - 01.01.03-dev + 01.01.03-rc 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_webservices_mokobackup/mokobackup.xml b/src/packages/plg_webservices_mokobackup/mokobackup.xml index 376fba1..d5073ba 100644 --- a/src/packages/plg_webservices_mokobackup/mokobackup.xml +++ b/src/packages/plg_webservices_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_webservices_mokobackup - 01.01.03-dev + 01.01.03-rc 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/pkg_mokobackup.xml b/src/pkg_mokobackup.xml index c206ba4..3c10770 100644 --- a/src/pkg_mokobackup.xml +++ b/src/pkg_mokobackup.xml @@ -8,7 +8,7 @@ Package - MokoJoomBackup mokobackup - 01.01.03-dev + 01.01.03-rc 2026-06-02 Moko Consulting hello@mokoconsulting.tech -- 2.52.0 From 119379bb16304a8dddde965f00aa7e3c081503a8 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Thu, 4 Jun 2026 13:17:50 +0000 Subject: [PATCH 14/42] chore: update release-candidate channel 01.01.03-dev [skip ci] --- updates.xml | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/updates.xml b/updates.xml index 8dd2da5..ccf5f1d 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ @@ -13,15 +13,34 @@ site 01.01.03-dev 2026-06-04 - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/tag/development + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/download/development/pkg_mokobackup-01.01.03-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/download/development/pkg_mokobackup-01.01.03-dev.zip 3c28f5726d85769c0aa6ac7b686f1ae2a5c3eacdf72fd4dbb5d6c722d93a47fc dev https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/raw/branch/main/CHANGELOG.md Moko Consulting https://mokoconsulting.tech + + + + Package - MokoJoomBackup + Package - MokoJoomBackup release-candidate build. + pkg_mokobackup + package + site + 01.01.03 + 2026-06-04 + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/tag/release-candidate + + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/download/release-candidate/pkg_mokobackup-01.01.03.zip + + 507c98657d666a66b112eb86d4a29b2ea993d1452cd7aeba9f58c6f911f34c9f + release-candidate + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/raw/branch/main/CHANGELOG.md + Moko Consulting + https://mokoconsulting.tech -- 2.52.0 From 3ded91608a7101b2ee07e308fdf918219fd96173 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 08:57:26 -0500 Subject: [PATCH 15/42] fix: remove orphaned scriptfile from component manifest The component manifest declared script.php but no script.php exists in the component sub-package. The install script belongs at the package level (pkg_mokobackup.xml) only. This caused Joomla to abort component installation before copying files or running SQL, producing the "SQL File not found" error. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- src/packages/com_mokobackup/mokobackup.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/packages/com_mokobackup/mokobackup.xml b/src/packages/com_mokobackup/mokobackup.xml index 82d67b6..89c2bb9 100644 --- a/src/packages/com_mokobackup/mokobackup.xml +++ b/src/packages/com_mokobackup/mokobackup.xml @@ -19,8 +19,6 @@ Joomla\Component\MokoBackup - script.php - sql/install.mysql.sql -- 2.52.0 From a34b715cffadcb5bc08aca4a361a256778008131 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Thu, 4 Jun 2026 13:57:46 +0000 Subject: [PATCH 16/42] chore(version): auto-bump 01.01.04-dev [skip ci] --- .mokogitea/manifest.xml | 2 +- .mokogitea/workflows/issue-branch.yml | 2 +- README.md | 2 +- src/packages/com_mokobackup/mokobackup.xml | 2 +- src/packages/plg_quickicon_mokobackup/mokobackup.xml | 2 +- src/packages/plg_system_mokobackup/mokobackup.xml | 2 +- src/packages/plg_task_mokobackup/mokobackup.xml | 2 +- src/packages/plg_webservices_mokobackup/mokobackup.xml | 2 +- src/pkg_mokobackup.xml | 2 +- updates.xml | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 07f0731..3e76d1d 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -5,7 +5,7 @@ Package - MokoJoomBackup MokoConsulting Full-site backup and restore for Joomla — database, files, and configuration - 01.01.03-dev + 01.01.04-dev GNU General Public License v3 diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index cc5f056..413ef84 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: 01.01.03 +# VERSION: 01.01.04 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/README.md b/README.md index 5a5b12d..ca6b7b9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoJoomBackup - + Full-site backup and restore for Joomla — database, files, and configuration. diff --git a/src/packages/com_mokobackup/mokobackup.xml b/src/packages/com_mokobackup/mokobackup.xml index 89c2bb9..ae6bc92 100644 --- a/src/packages/com_mokobackup/mokobackup.xml +++ b/src/packages/com_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> com_mokobackup - 01.01.03-rc + 01.01.04-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_quickicon_mokobackup/mokobackup.xml b/src/packages/plg_quickicon_mokobackup/mokobackup.xml index 2d055ef..2856a6b 100644 --- a/src/packages/plg_quickicon_mokobackup/mokobackup.xml +++ b/src/packages/plg_quickicon_mokobackup/mokobackup.xml @@ -1,7 +1,7 @@ plg_quickicon_mokobackup - 01.01.03-rc + 01.01.04-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_system_mokobackup/mokobackup.xml b/src/packages/plg_system_mokobackup/mokobackup.xml index 0c51af3..1746c42 100644 --- a/src/packages/plg_system_mokobackup/mokobackup.xml +++ b/src/packages/plg_system_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_system_mokobackup - 01.01.03-rc + 01.01.04-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_task_mokobackup/mokobackup.xml b/src/packages/plg_task_mokobackup/mokobackup.xml index a27fa48..1b72d8d 100644 --- a/src/packages/plg_task_mokobackup/mokobackup.xml +++ b/src/packages/plg_task_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_task_mokobackup - 01.01.03-rc + 01.01.04-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_webservices_mokobackup/mokobackup.xml b/src/packages/plg_webservices_mokobackup/mokobackup.xml index d5073ba..f7ebd45 100644 --- a/src/packages/plg_webservices_mokobackup/mokobackup.xml +++ b/src/packages/plg_webservices_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_webservices_mokobackup - 01.01.03-rc + 01.01.04-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/pkg_mokobackup.xml b/src/pkg_mokobackup.xml index 3c10770..2b3d7cd 100644 --- a/src/pkg_mokobackup.xml +++ b/src/pkg_mokobackup.xml @@ -8,7 +8,7 @@ Package - MokoJoomBackup mokobackup - 01.01.03-rc + 01.01.04-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/updates.xml b/updates.xml index ccf5f1d..152906b 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ -- 2.52.0 From c3cbad1a00f57fc24a95431dc64611bed1191c70 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Thu, 4 Jun 2026 13:57:48 +0000 Subject: [PATCH 17/42] chore: update development channel 01.01.04-dev [skip ci] --- updates.xml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/updates.xml b/updates.xml index 152906b..dd2a367 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ @@ -11,18 +11,18 @@ pkg_mokobackup package site - 01.01.03-dev + 01.01.04-dev 2026-06-04 - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/tag/development + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/download/development/pkg_mokobackup-01.01.03-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/download/development/pkg_mokobackup-01.01.04-dev.zip - 3c28f5726d85769c0aa6ac7b686f1ae2a5c3eacdf72fd4dbb5d6c722d93a47fc + c3163a6abdd8cfb63982e40ce8750a8743bd8e153d8537c144ccc534ddaa2bf3 dev https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/raw/branch/main/CHANGELOG.md Moko Consulting https://mokoconsulting.tech - + Package - MokoJoomBackup @@ -32,15 +32,15 @@ site 01.01.03 2026-06-04 - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/tag/release-candidate + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/tag/release-candidate - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/download/release-candidate/pkg_mokobackup-01.01.03.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/download/release-candidate/pkg_mokobackup-01.01.03.zip 507c98657d666a66b112eb86d4a29b2ea993d1452cd7aeba9f58c6f911f34c9f release-candidate https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/raw/branch/main/CHANGELOG.md Moko Consulting https://mokoconsulting.tech - + -- 2.52.0 From e495c786fbaaf4cb72d9b0145b92b52d486fdc1d Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Thu, 4 Jun 2026 14:02:49 +0000 Subject: [PATCH 18/42] chore: update development channel 01.01.04-dev [skip ci] --- updates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/updates.xml b/updates.xml index dd2a367..2fb784d 100644 --- a/updates.xml +++ b/updates.xml @@ -17,7 +17,7 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/download/development/pkg_mokobackup-01.01.04-dev.zip - c3163a6abdd8cfb63982e40ce8750a8743bd8e153d8537c144ccc534ddaa2bf3 + 73b6dedf4bf295697a289f6baf1d0f680d0c8ab4f07ffaa2761be04469f97fa4 dev https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/raw/branch/main/CHANGELOG.md Moko Consulting -- 2.52.0 From f884314e282cb169ecbc6a7ac449300494df686b Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 09:20:51 -0500 Subject: [PATCH 19/42] fix: consolidate admin files into single files block Joomla core components use a single block with entries for all subdirectories. Our manifest had multiple separate blocks which may not be fully processed by the component installer. Consolidate to match the Joomla standard pattern using with folder entries. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- src/packages/com_mokobackup/mokobackup.xml | 47 ++++++---------------- 1 file changed, 12 insertions(+), 35 deletions(-) diff --git a/src/packages/com_mokobackup/mokobackup.xml b/src/packages/com_mokobackup/mokobackup.xml index ae6bc92..1dcc977 100644 --- a/src/packages/com_mokobackup/mokobackup.xml +++ b/src/packages/com_mokobackup/mokobackup.xml @@ -38,46 +38,23 @@ - - provider.php - - - Controller - Engine - Extension - Model - Table - View - - - backup.xml - profile.xml - filter_backups.xml - filter_profiles.xml - - - backups - backup - profiles - profile - - - install.mysql.sql - uninstall.mysql.sql - updates - - - mokobackup.php - - - en-GB/com_mokobackup.ini - en-GB/com_mokobackup.sys.ini - COM_MOKOBACKUP COM_MOKOBACKUP_SUBMENU_BACKUPS COM_MOKOBACKUP_SUBMENU_PROFILES + + cli + forms + services + sql + src + tmpl + + + en-GB/com_mokobackup.ini + en-GB/com_mokobackup.sys.ini + -- 2.52.0 From 8e51abee5422c8395492fbe9c1f4f5aed67ddff6 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Thu, 4 Jun 2026 14:21:21 +0000 Subject: [PATCH 20/42] chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] --- .mokogitea/workflows/auto-release.yml | 555 +++++++++++++------------- 1 file changed, 285 insertions(+), 270 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 6fb2b44..44a2d64 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -1,270 +1,285 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# PATH: /templates/workflows/universal/auto-release.yml.template -# VERSION: 09.23.00 -# BRIEF: Universal build & release � detects platform from manifest.xml -# -# +========================================================================+ -# | UNIVERSAL BUILD & RELEASE PIPELINE | -# +========================================================================+ -# | | -# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | -# | | -# | Platform-specific: | -# | joomla: XML manifest, updates.xml, type-prefixed packages | -# | dolibarr: mod*.class.php, update.txt, dev version reset | -# | generic: README-only, no update stream | -# | | -# +========================================================================+ - -name: "Universal: Build & Release" - -on: - pull_request: - types: [opened, closed] - branches: - - main - workflow_dispatch: - inputs: - action: - description: 'Action to perform' - required: false - type: choice - default: release - options: - - release - - promote-rc - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -permissions: - contents: write - -jobs: - # ── PR Opened → Rename branch to RC and build RC release ───────────────────── - promote-rc: - name: Promote to RC - runs-on: release - if: >- - (github.event.action == 'opened' && github.event.pull_request.merged != true) || - (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 1 - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - # Always fetch latest CLI tools — never use stale cache from previous runs - rm -rf /tmp/moko-platform-api - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api - composer install --no-dev --no-interaction --quiet - - - name: Rename branch to rc - run: | - php /tmp/moko-platform-api/cli/branch_rename.php \ - --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ - --pr "${{ github.event.pull_request.number }}" - - - name: Checkout rc and configure git - run: | - git fetch origin rc - git checkout rc - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - 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: Publish RC release - run: | - php /tmp/moko-platform-api/cli/release_publish.php \ - --path . --stability rc --bump minor --branch rc \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" - - - name: Summary - if: always() - run: | - echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY - echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY - - # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── - release: - name: Build & Release Pipeline - runs-on: release - if: >- - github.event.pull_request.merged == true || - (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 0 - - - name: Configure git for bot pushes - run: | - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - 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: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' - run: | - # Ensure PHP + Composer are available - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - # Always fetch latest CLI tools — never use stale cache from previous runs - rm -rf /tmp/moko-platform-api - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api - composer install --no-dev --no-interaction --quiet - - - - name: "Publish stable release" - run: | - php /tmp/moko-platform-api/cli/release_publish.php \ - --path . --stability stable --bump minor --branch main \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" - - # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- - - name: "Step 9: Mirror release to GitHub" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GH_MIRROR_TOKEN != '' - continue-on-error: true - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_mirror.php \ - --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ - --branch main 2>&1 || true - echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY - - # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- - - name: "Step 10: Push main to GitHub mirror" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GH_MIRROR_TOKEN != '' - continue-on-error: true - run: | - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) - GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) - git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ - git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" - git fetch origin main --depth=1 - git push github origin/main:refs/heads/main --force 2>/dev/null \ - && echo "main branch pushed to GitHub mirror" \ - || echo "WARNING: GitHub mirror push failed" - - - name: "Step 11: Delete rc branch and recreate dev from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - - # Delete rc branch (ephemeral — created by promote-rc) - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/rc" 2>/dev/null \ - && echo "Deleted rc branch" || echo "rc branch not found" - - # Delete dev branch - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" - - # Recreate dev from main (now includes version bump + changelog promotion) - curl -sf -X POST -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/branches" \ - -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" - - echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY - - - name: "Step 12: Create version branch from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - BRANCH_NAME="version/${VERSION}" - MAIN_SHA=$(git rev-parse HEAD) - - # Delete old version branch if it exists (same version re-release) - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" - - # Create version/XX.YY.ZZ from main - curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" - - echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY - - - - # -- Dolibarr post-release: Reset dev version ----------------------------- - - name: "Post-release: Reset dev version" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/version_reset_dev.php \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ - --branch dev --path . 2>&1 || true - - # -- Summary -------------------------------------------------------------- - - name: Pipeline Summary - if: always() - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - PLATFORM="${{ steps.platform.outputs.platform }}" - if [ "${{ steps.version.outputs.skip }}" = "true" ]; then - echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY - echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY - elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then - echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY - echo "|------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY - fi +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# PATH: /templates/workflows/universal/auto-release.yml.template +# VERSION: 05.00.00 +# BRIEF: Universal build & release � detects platform from manifest.xml +# +# +========================================================================+ +# | UNIVERSAL BUILD & RELEASE PIPELINE | +# +========================================================================+ +# | | +# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | +# | | +# | Platform-specific: | +# | joomla: XML manifest, updates.xml, type-prefixed packages | +# | dolibarr: mod*.class.php, update.txt, dev version reset | +# | generic: README-only, no update stream | +# | | +# +========================================================================+ + +name: "Universal: Build & Release" + +on: + pull_request: + types: [opened, closed] + branches: + - main + workflow_dispatch: + inputs: + action: + description: 'Action to perform' + required: false + type: choice + default: release + options: + - release + - promote-rc + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + # ── PR Opened → Rename branch to RC and build RC release ───────────────────── + promote-rc: + name: Promote to RC + runs-on: release + if: >- + (github.event.action == 'opened' && github.event.pull_request.merged != true) || + (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 1 + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform-api + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api + composer install --no-dev --no-interaction --quiet + + - name: Rename branch to rc + run: | + php /tmp/moko-platform-api/cli/branch_rename.php \ + --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ + --pr "${{ github.event.pull_request.number }}" + + - name: Checkout rc and configure git + run: | + git fetch origin rc + git checkout rc + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + 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: Publish RC release + run: | + php /tmp/moko-platform-api/cli/release_publish.php \ + --path . --stability rc --bump minor --branch rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --skip-update-stream + + - name: Summary + if: always() + run: | + echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY + echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY + + # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── + release: + name: Build & Release Pipeline + runs-on: release + if: >- + github.event.pull_request.merged == true || + (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 0 + + - name: Configure git for bot pushes + run: | + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + 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 }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' + run: | + # Ensure PHP + Composer are available + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform-api + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api + composer install --no-dev --no-interaction --quiet + + + - name: "Publish stable release" + run: | + php /tmp/moko-platform-api/cli/release_publish.php \ + --path . --stability stable --bump minor --branch main \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --skip-update-stream + + # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- + - name: "Step 9: Mirror release to GitHub" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_mirror.php \ + --version "$VERSION" --tag "$RELEASE_TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ + --branch main 2>&1 || true + echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY + + # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- + - name: "Step 10: Push main to GitHub mirror" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) + GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) + git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ + git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" + git fetch origin main --depth=1 + git push github origin/main:refs/heads/main --force 2>/dev/null \ + && echo "main branch pushed to GitHub mirror" \ + || echo "WARNING: GitHub mirror push failed" + + - name: "Step 11: Delete rc branch and recreate dev from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + # Delete rc branch (ephemeral — created by promote-rc) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/rc" 2>/dev/null \ + && echo "Deleted rc branch" || echo "rc branch not found" + + # Delete dev branch + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" + + # Recreate dev from main (now includes version bump + changelog promotion) + curl -sf -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/branches" \ + -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" + + echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY + + - name: "Step 12: Create version branch from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + BRANCH_NAME="version/${VERSION}" + MAIN_SHA=$(git rev-parse HEAD) + + # Delete old version branch if it exists (same version re-release) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" + + # Create version/XX.YY.ZZ from main + curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" + + echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY + + + + # -- Dolibarr post-release: Reset dev version ----------------------------- + - name: "Post-release: Reset dev version" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/version_reset_dev.php \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ + --branch dev --path . 2>&1 || true + + # -- Summary -------------------------------------------------------------- + - name: Pipeline Summary + if: always() + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PLATFORM="${{ steps.platform.outputs.platform }}" + if [ "${{ steps.version.outputs.skip }}" = "true" ]; then + echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY + echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY + elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then + echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY + fi -- 2.52.0 From 4f14009003af1e6fcdcf8f23b352a8c5bc880f05 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Thu, 4 Jun 2026 14:34:10 +0000 Subject: [PATCH 21/42] feat(update): migrate update server URL to Gitea Pages [skip ci] --- src/pkg_mokobackup.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pkg_mokobackup.xml b/src/pkg_mokobackup.xml index 2b3d7cd..d02c244 100644 --- a/src/pkg_mokobackup.xml +++ b/src/pkg_mokobackup.xml @@ -32,6 +32,6 @@ - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/raw/branch/main/updates.xml + https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/updates.xml -- 2.52.0 From 41c1bb5d68ee561dcd70a86b5d25961c6dbddff3 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Thu, 4 Jun 2026 14:44:22 +0000 Subject: [PATCH 22/42] chore(version): pre-release bump to 01.01.04-dev [skip ci] --- .mokogitea/workflows/auto-release.yml | 570 +++++++++++++------------- 1 file changed, 285 insertions(+), 285 deletions(-) diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml index 44a2d64..141fdcc 100644 --- a/.mokogitea/workflows/auto-release.yml +++ b/.mokogitea/workflows/auto-release.yml @@ -1,285 +1,285 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Release -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# PATH: /templates/workflows/universal/auto-release.yml.template -# VERSION: 05.00.00 -# BRIEF: Universal build & release � detects platform from manifest.xml -# -# +========================================================================+ -# | UNIVERSAL BUILD & RELEASE PIPELINE | -# +========================================================================+ -# | | -# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | -# | | -# | Platform-specific: | -# | joomla: XML manifest, updates.xml, type-prefixed packages | -# | dolibarr: mod*.class.php, update.txt, dev version reset | -# | generic: README-only, no update stream | -# | | -# +========================================================================+ - -name: "Universal: Build & Release" - -on: - pull_request: - types: [opened, closed] - branches: - - main - workflow_dispatch: - inputs: - action: - description: 'Action to perform' - required: false - type: choice - default: release - options: - - release - - promote-rc - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -permissions: - contents: write - -jobs: - # ── PR Opened → Rename branch to RC and build RC release ───────────────────── - promote-rc: - name: Promote to RC - runs-on: release - if: >- - (github.event.action == 'opened' && github.event.pull_request.merged != true) || - (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 1 - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - # Always fetch latest CLI tools — never use stale cache from previous runs - rm -rf /tmp/moko-platform-api - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api - composer install --no-dev --no-interaction --quiet - - - name: Rename branch to rc - run: | - php /tmp/moko-platform-api/cli/branch_rename.php \ - --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ - --pr "${{ github.event.pull_request.number }}" - - - name: Checkout rc and configure git - run: | - git fetch origin rc - git checkout rc - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - 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: Publish RC release - run: | - php /tmp/moko-platform-api/cli/release_publish.php \ - --path . --stability rc --bump minor --branch rc \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --skip-update-stream - - - name: Summary - if: always() - run: | - echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY - echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY - - # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── - release: - name: Build & Release Pipeline - runs-on: release - if: >- - github.event.pull_request.merged == true || - (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 0 - - - name: Configure git for bot pushes - run: | - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - 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 }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' - run: | - # Ensure PHP + Composer are available - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - # Always fetch latest CLI tools — never use stale cache from previous runs - rm -rf /tmp/moko-platform-api - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform-api - cd /tmp/moko-platform-api - composer install --no-dev --no-interaction --quiet - - - - name: "Publish stable release" - run: | - php /tmp/moko-platform-api/cli/release_publish.php \ - --path . --stability stable --bump minor --branch main \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" \ - --skip-update-stream - - # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- - - name: "Step 9: Mirror release to GitHub" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GH_MIRROR_TOKEN != '' - continue-on-error: true - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - RELEASE_TAG="${{ steps.version.outputs.release_tag }}" - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/release_mirror.php \ - --version "$VERSION" --tag "$RELEASE_TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ - --branch main 2>&1 || true - echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY - - # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- - - name: "Step 10: Push main to GitHub mirror" - if: >- - steps.version.outputs.skip != 'true' && - secrets.GH_MIRROR_TOKEN != '' - continue-on-error: true - run: | - GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" - GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) - GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) - git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ - git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" - git fetch origin main --depth=1 - git push github origin/main:refs/heads/main --force 2>/dev/null \ - && echo "main branch pushed to GitHub mirror" \ - || echo "WARNING: GitHub mirror push failed" - - - name: "Step 11: Delete rc branch and recreate dev from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - - # Delete rc branch (ephemeral — created by promote-rc) - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/rc" 2>/dev/null \ - && echo "Deleted rc branch" || echo "rc branch not found" - - # Delete dev branch - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ - "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" - - # Recreate dev from main (now includes version bump + changelog promotion) - curl -sf -X POST -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - "${API_BASE}/branches" \ - -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" - - echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY - - - name: "Step 12: Create version branch from main" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - BRANCH_NAME="version/${VERSION}" - MAIN_SHA=$(git rev-parse HEAD) - - # Delete old version branch if it exists (same version re-release) - curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" - - # Create version/XX.YY.ZZ from main - curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" - - echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY - - - - # -- Dolibarr post-release: Reset dev version ----------------------------- - - name: "Post-release: Reset dev version" - if: steps.version.outputs.skip != 'true' - continue-on-error: true - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - php /tmp/moko-platform-api/cli/version_reset_dev.php \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ - --branch dev --path . 2>&1 || true - - # -- Summary -------------------------------------------------------------- - - name: Pipeline Summary - if: always() - run: | - VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" - PLATFORM="${{ steps.platform.outputs.platform }}" - if [ "${{ steps.version.outputs.skip }}" = "true" ]; then - echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY - echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY - elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then - echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY - else - echo "" >> $GITHUB_STEP_SUMMARY - echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY - echo "|------|--------|" >> $GITHUB_STEP_SUMMARY - echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY - fi +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.Release +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# PATH: /templates/workflows/universal/auto-release.yml.template +# VERSION: 05.00.00 +# BRIEF: Universal build & release � detects platform from manifest.xml +# +# +========================================================================+ +# | UNIVERSAL BUILD & RELEASE PIPELINE | +# +========================================================================+ +# | | +# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | +# | | +# | Platform-specific: | +# | joomla: XML manifest, updates.xml, type-prefixed packages | +# | dolibarr: mod*.class.php, update.txt, dev version reset | +# | generic: README-only, no update stream | +# | | +# +========================================================================+ + +name: "Universal: Build & Release" + +on: + pull_request: + types: [opened, closed] + branches: + - main + workflow_dispatch: + inputs: + action: + description: 'Action to perform' + required: false + type: choice + default: release + options: + - release + - promote-rc + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + # ── PR Opened → Rename branch to RC and build RC release ───────────────────── + promote-rc: + name: Promote to RC + runs-on: release + if: >- + (github.event.action == 'opened' && github.event.pull_request.merged != true) || + (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 1 + + - name: Setup moko-platform tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform-api + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api + composer install --no-dev --no-interaction --quiet + + - name: Rename branch to rc + run: | + php /tmp/moko-platform-api/cli/branch_rename.php \ + --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ + --pr "${{ github.event.pull_request.number }}" + + - name: Checkout rc and configure git + run: | + git fetch origin rc + git checkout rc + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + 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: Publish RC release + run: | + php /tmp/moko-platform-api/cli/release_publish.php \ + --path . --stability rc --bump minor --branch rc \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --skip-update-stream + + - name: Summary + if: always() + run: | + echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY + echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY + + # ── Merged PR → Build & Release (or promote RC to stable) ──────────────────── + release: + name: Build & Release Pipeline + runs-on: release + if: >- + github.event.pull_request.merged == true || + (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc') + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.MOKOGITEA_TOKEN }} + fetch-depth: 0 + + - name: Configure git for bot pushes + run: | + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + 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 }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' + run: | + # Ensure PHP + Composer are available + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + # Always fetch latest CLI tools — never use stale cache from previous runs + rm -rf /tmp/moko-platform-api + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ + /tmp/moko-platform-api + cd /tmp/moko-platform-api + composer install --no-dev --no-interaction --quiet + + + - name: "Publish stable release" + run: | + php /tmp/moko-platform-api/cli/release_publish.php \ + --path . --stability stable --bump minor --branch main \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" \ + --skip-update-stream + + # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- + - name: "Step 9: Mirror release to GitHub" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/release_mirror.php \ + --version "$VERSION" --tag "$RELEASE_TAG" \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ + --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ + --branch main 2>&1 || true + echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY + + # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- + - name: "Step 10: Push main to GitHub mirror" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_MIRROR_TOKEN != '' + continue-on-error: true + run: | + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) + GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) + git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ + git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" + git fetch origin main --depth=1 + git push github origin/main:refs/heads/main --force 2>/dev/null \ + && echo "main branch pushed to GitHub mirror" \ + || echo "WARNING: GitHub mirror push failed" + + - name: "Step 11: Delete rc branch and recreate dev from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + + # Delete rc branch (ephemeral — created by promote-rc) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/rc" 2>/dev/null \ + && echo "Deleted rc branch" || echo "rc branch not found" + + # Delete dev branch + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" + + # Recreate dev from main (now includes version bump + changelog promotion) + curl -sf -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/branches" \ + -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" + + echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY + + - name: "Step 12: Create version branch from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + BRANCH_NAME="version/${VERSION}" + MAIN_SHA=$(git rev-parse HEAD) + + # Delete old version branch if it exists (same version re-release) + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}" + + # Create version/XX.YY.ZZ from main + curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed" + + echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY + + + + # -- Dolibarr post-release: Reset dev version ----------------------------- + - name: "Post-release: Reset dev version" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + php /tmp/moko-platform-api/cli/version_reset_dev.php \ + --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ + --branch dev --path . 2>&1 || true + + # -- Summary -------------------------------------------------------------- + - name: Pipeline Summary + if: always() + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PLATFORM="${{ steps.platform.outputs.platform }}" + if [ "${{ steps.version.outputs.skip }}" = "true" ]; then + echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY + echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY + elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then + echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY + fi -- 2.52.0 From 8d082de47f499d93abe73d5fae767d51ee3dc999 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Thu, 4 Jun 2026 14:44:23 +0000 Subject: [PATCH 23/42] chore: update development channel 01.01.04-dev [skip ci] --- updates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/updates.xml b/updates.xml index 2fb784d..ec21878 100644 --- a/updates.xml +++ b/updates.xml @@ -17,7 +17,7 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/download/development/pkg_mokobackup-01.01.04-dev.zip - 73b6dedf4bf295697a289f6baf1d0f680d0c8ab4f07ffaa2761be04469f97fa4 + 45ff0812d06f9e48738c5a2854c274ad9cd5fd8f1a508d54e0847167c4446b10 dev https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/raw/branch/main/CHANGELOG.md Moko Consulting -- 2.52.0 From 4c15f1242679a3e4da62066a4f97ee4eccc0e405 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Thu, 4 Jun 2026 15:14:33 +0000 Subject: [PATCH 24/42] chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] --- .mokogitea/workflows/pr-check.yml | 636 +++++++++++++++++------------- 1 file changed, 372 insertions(+), 264 deletions(-) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index bf72613..9d0cb35 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -1,264 +1,372 @@ -# 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." +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.CI +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# PATH: /templates/workflows/universal/pr-check.yml.template +# VERSION: 09.23.00 +# BRIEF: PR gate — branch policy + code validation before merge + +name: "Universal: PR Check" + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +permissions: + contents: read + pull-requests: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + # ── Branch Policy ────────────────────────────────────────────────────── + branch-policy: + name: Branch Policy + runs-on: ubuntu-latest + steps: + - name: Check branch merge target + run: | + HEAD="${{ github.head_ref }}" + BASE="${{ github.base_ref }}" + + echo "PR: ${HEAD} → ${BASE}" + + ALLOWED=true + REASON="" + + case "$HEAD" in + feature/*|feat/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Feature branches must target 'dev', not '${BASE}'" + fi + ;; + fix/*|bugfix/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Fix branches must target 'dev', not '${BASE}'" + fi + ;; + patch/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then + ALLOWED=false + REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" + fi + ;; + hotfix/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" + fi + ;; + rc) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="RC branch can only merge into 'main', not '${BASE}'" + fi + ;; + dev) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Dev branch can only merge into 'main', not '${BASE}'" + fi + ;; + esac + + if [ "$ALLOWED" = false ]; then + echo "::error::${REASON}" + echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "${REASON}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY + echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "Branch policy: OK (${HEAD} → ${BASE})" + echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY + + # ── Code Validation ──────────────────────────────────────────────────── + validate: + name: Validate PR + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check for merge conflict markers + run: | + CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) + if [ -n "$CONFLICTS" ]; then + echo "::error::Merge conflict markers found in source files" + echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "No conflict markers found" + + - name: Detect platform + id: platform + run: | + # Read platform from XML manifest ( tag) or plain text fallback + PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1) + [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]') + [ -z "$PLATFORM" ] && PLATFORM="generic" + echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" + + - name: Setup PHP + if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 + fi + + - name: PHP syntax check + if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' + run: | + ERRORS=0 + while IFS= read -r -d '' file; do + if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then + ERRORS=$((ERRORS + 1)) + fi + done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0) + echo "PHP lint: ${ERRORS} error(s)" + [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } + + - name: 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: Validate Joomla language files + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + WARNINGS=0 + + # Find all .ini language files + INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null) + if [ -z "$INI_FILES" ]; then + echo "No .ini language files found — skipping" + exit 0 + fi + + echo "Found $(echo "$INI_FILES" | wc -l) language file(s)" + + for FILE in $INI_FILES; do + FNAME=$(basename "$FILE") + LINENUM=0 + SEEN_KEYS="" + + while IFS= read -r line || [ -n "$line" ]; do + LINENUM=$((LINENUM + 1)) + + # Skip empty lines and comments + [ -z "$line" ] && continue + echo "$line" | grep -qE '^\s*;' && continue + echo "$line" | grep -qE '^\s*$' && continue + + # Must match KEY="VALUE" format + if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then + echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}" + ERRORS=$((ERRORS + 1)) + continue + fi + + # Extract key and check for duplicates + KEY=$(echo "$line" | sed 's/=.*//') + if echo "$SEEN_KEYS" | grep -qx "$KEY"; then + echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}" + ERRORS=$((ERRORS + 1)) + fi + SEEN_KEYS="${SEEN_KEYS} + ${KEY}" + done < "$FILE" + + echo " ${FILE}: checked ${LINENUM} lines" + done + + # Cross-check en-GB vs en-US key consistency + GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1) + US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1) + + if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then + for GB_FILE in "$GB_DIR"/*.ini; do + [ ! -f "$GB_FILE" ] && continue + FNAME=$(basename "$GB_FILE") + US_FILE="$US_DIR/$FNAME" + [ ! -f "$US_FILE" ] && continue + + GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort) + US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort) + + # Keys in en-GB but not en-US + MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS")) + if [ -n "$MISSING_US" ]; then + echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:" + echo "$MISSING_US" | while read -r k; do echo " - $k"; done + WARNINGS=$((WARNINGS + 1)) + fi + + # Keys in en-US but not en-GB + MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS")) + if [ -n "$MISSING_GB" ]; then + echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:" + echo "$MISSING_GB" | while read -r k; do echo " - $k"; done + WARNINGS=$((WARNINGS + 1)) + fi + done + fi + + { + echo "### Language File Validation" + echo "| Metric | Count |" + echo "|---|---|" + echo "| Files checked | $(echo "$INI_FILES" | wc -l) |" + echo "| Errors | ${ERRORS} |" + echo "| Warnings | ${WARNINGS} |" + } >> $GITHUB_STEP_SUMMARY + + if [ "$ERRORS" -gt 0 ]; then + echo "::error::Language validation failed with ${ERRORS} error(s)" + exit 1 + fi + echo "Language files: OK (${WARNINGS} warning(s))" + + - name: Check changelog has unreleased entry + run: | + if [ ! -f "CHANGELOG.md" ]; then + echo "::warning::No CHANGELOG.md found" + exit 0 + fi + # Check for content under [Unreleased] section + if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then + echo "::error::CHANGELOG.md missing [Unreleased] section" + exit 1 + fi + # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased + UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true) + if [ "$UNRELEASED_CONTENT" -eq 0 ]; then + echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes." + echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY + echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY + echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]" + + - name: Verify package source + run: | + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ ! -d "$SOURCE_DIR" ]; then + echo "::warning::No src/ or htdocs/ directory" + exit 0 + fi + FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) + echo "Source: ${FILE_COUNT} files" + [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } + + # ── Pre-Release RC Build ───────────────────────────────────────────────── + pre-release: + name: Build RC Package + runs-on: ubuntu-latest + needs: [branch-policy, validate] + + steps: + - name: Trigger RC pre-release + env: + GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + REPO: ${{ github.repository }} + BRANCH: ${{ github.head_ref }} + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" + echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY + echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY + + # ── Issue Reporter ────────────────────────────────────────────────────── + report-issues: + name: Report Issues + runs-on: ubuntu-latest + needs: [branch-policy, validate] + if: >- + always() && + needs.validate.result == 'failure' + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + sparse-checkout: automation/ci-issue-reporter.sh + sparse-checkout-cone-mode: false + + - name: "File issue for PR validation failure" + env: + GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + chmod +x automation/ci-issue-reporter.sh + ./automation/ci-issue-reporter.sh \ + --gate "PR Validation" \ + --workflow "PR Check" \ + --severity error \ + --details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed." -- 2.52.0 From e9d7889417055596bf2b4f443469edfce3aaff68 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Thu, 4 Jun 2026 15:27:07 +0000 Subject: [PATCH 25/42] chore: remove updates.xml [skip ci] --- updates.xml | 46 ---------------------------------------------- 1 file changed, 46 deletions(-) delete mode 100644 updates.xml diff --git a/updates.xml b/updates.xml deleted file mode 100644 index ec21878..0000000 --- a/updates.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - Package - MokoJoomBackup - Package - MokoJoomBackup development build. - pkg_mokobackup - package - site - 01.01.04-dev - 2026-06-04 - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/tag/development - - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/download/development/pkg_mokobackup-01.01.04-dev.zip - - 45ff0812d06f9e48738c5a2854c274ad9cd5fd8f1a508d54e0847167c4446b10 - dev - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/raw/branch/main/CHANGELOG.md - Moko Consulting - https://mokoconsulting.tech - - - - Package - MokoJoomBackup - Package - MokoJoomBackup release-candidate build. - pkg_mokobackup - package - site - 01.01.03 - 2026-06-04 - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/tag/release-candidate - - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/download/release-candidate/pkg_mokobackup-01.01.03.zip - - 507c98657d666a66b112eb86d4a29b2ea993d1452cd7aeba9f58c6f911f34c9f - release-candidate - https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/raw/branch/main/CHANGELOG.md - Moko Consulting - https://mokoconsulting.tech - - - -- 2.52.0 From 8c5ed1ed768c4e01e84302433dff6634b891a6be Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Thu, 4 Jun 2026 15:30:34 +0000 Subject: [PATCH 26/42] chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] --- .mokogitea/workflows/pr-check.yml | 39 ++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index 9d0cb35..473eeb2 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -202,10 +202,47 @@ jobs: ERRORS=0 WARNINGS=0 + # Require both en-GB and en-US language directories + LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1) + if [ -z "$LANG_ROOT" ]; then + echo "No language/ directory found — skipping" + exit 0 + fi + + if [ ! -d "$LANG_ROOT/en-GB" ]; then + echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)" + ERRORS=$((ERRORS + 1)) + fi + if [ ! -d "$LANG_ROOT/en-US" ]; then + echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)" + ERRORS=$((ERRORS + 1)) + fi + + # Check that en-GB and en-US have matching .ini files + if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then + for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do + [ ! -f "$GB_INI" ] && continue + US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")" + if [ ! -f "$US_INI" ]; then + echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US" + ERRORS=$((ERRORS + 1)) + fi + done + for US_INI in "$LANG_ROOT/en-US"/*.ini; do + [ ! -f "$US_INI" ] && continue + GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")" + if [ ! -f "$GB_INI" ]; then + echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB" + ERRORS=$((ERRORS + 1)) + fi + done + fi + # Find all .ini language files INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null) if [ -z "$INI_FILES" ]; then - echo "No .ini language files found — skipping" + echo "No .ini language files found" + [ "$ERRORS" -gt 0 ] && exit 1 exit 0 fi -- 2.52.0 From 61abf724374c00447447889d45c4fcfdf0eabb3a Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Thu, 4 Jun 2026 15:39:23 +0000 Subject: [PATCH 27/42] chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] --- .mokogitea/workflows/pr-check.yml | 92 +++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index 473eeb2..3dd7540 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -147,6 +147,98 @@ jobs: echo "PHP lint: ${ERRORS} error(s)" [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } + - name: Joomla JEXEC guard check + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + while IFS= read -r -d '' file; do + # Skip vendor, node_modules, and index.html stub files + case "$file" in ./vendor/*|./node_modules/*) continue ;; esac + # Check first 10 lines for JEXEC or JPATH guard + if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then + echo "::error file=${file}::Missing JEXEC guard: ${file}" + ERRORS=$((ERRORS + 1)) + fi + done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0) + if [ "$ERRORS" -gt 0 ]; then + echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard" + echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY + echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "JEXEC guard: OK" + + - name: Joomla directory listing protection + if: steps.platform.outputs.platform == 'joomla' + run: | + MISSING=0 + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && exit 0 + while IFS= read -r dir; do + if [ ! -f "${dir}/index.html" ]; then + echo "::warning::Missing index.html in ${dir} (directory listing protection)" + MISSING=$((MISSING + 1)) + fi + done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*") + if [ "$MISSING" -gt 0 ]; then + echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY + echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY + fi + echo "Directory protection: ${MISSING} missing (advisory)" + + - name: Joomla script file and asset checks + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + [ -z "$MANIFEST" ] && exit 0 + MANIFEST_DIR=$(dirname "$MANIFEST") + + # Check scriptfile exists if declared + SCRIPTFILE=$(sed -n 's/.*\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null) + if [ -n "$SCRIPTFILE" ]; then + if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then + echo "::error::Manifest declares ${SCRIPTFILE} but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}" + ERRORS=$((ERRORS + 1)) + else + echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)" + fi + fi + + # Require joomla.asset.json and validate it + ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1) + if [ -z "$ASSET_JSON" ]; then + echo "::error::joomla.asset.json not found — Joomla asset system is required" + ERRORS=$((ERRORS + 1)) + else + if command -v php &> /dev/null; then + php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || { + echo "::error::joomla.asset.json is not valid JSON" + ERRORS=$((ERRORS + 1)) + } + fi + echo "joomla.asset.json: valid" + fi + + # Validate all XML files in src/ are well-formed + XML_ERRORS=0 + if command -v php &> /dev/null; then + while IFS= read -r -d '' xmlfile; do + if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then + XML_ERRORS=$((XML_ERRORS + 1)) + fi + done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0) + fi + if [ "$XML_ERRORS" -gt 0 ]; then + echo "::error::${XML_ERRORS} XML file(s) are malformed" + ERRORS=$((ERRORS + 1)) + else + echo "XML well-formedness: OK" + fi + + [ "$ERRORS" -gt 0 ] && exit 1 + echo "Joomla asset checks: OK" + - name: Validate platform manifest run: | PLATFORM="${{ steps.platform.outputs.platform }}" -- 2.52.0 From 25e06fd08c431ae2c616a0d3c26aee5f9d3b56b8 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Thu, 4 Jun 2026 15:56:54 +0000 Subject: [PATCH 28/42] chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] --- .mokogitea/workflows/pr-check.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index 3dd7540..4d78d7a 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -256,6 +256,13 @@ jobs: for ELEMENT in name version description; do grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; } done + # Block legacy raw/branch update server URLs on MokoGitea + RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true) + if [ -n "$RAW_URLS" ]; then + echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)" + echo "$RAW_URLS" + exit 1 + fi echo "Joomla manifest valid" ;; dolibarr) -- 2.52.0 From c1521cb2354c32b8cf735fb3867739fcb5cfc3ac Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 11:20:35 -0500 Subject: [PATCH 29/42] feat: add dashboard view and console, content, actionlog plugins (#24, #25, #26, #27) Dashboard view becomes the default landing page with status cards, quick actions (backup now w/ profile selector), and system health checks. Three new plugins round out the package: - plg_console_mokobackup: CLI commands (run, list, profiles, restore, cleanup) - plg_content_mokobackup: auto-backup before extension install/update - plg_actionlog_mokobackup: logs backup and profile actions to User Action Logs BackupEngine now dispatches onMokoBackupAfterRun for plugin listeners. Package manifest and install script updated to include and auto-enable the new plugins. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../language/en-GB/com_mokobackup.ini | 15 + .../language/en-US/com_mokobackup.ini | 13 + src/packages/com_mokobackup/mokobackup.xml | 1 + .../src/Controller/DisplayController.php | 2 +- .../src/Engine/BackupEngine.php | 29 ++ .../src/Model/DashboardModel.php | 163 +++++++++++ .../src/View/Dashboard/HtmlView.php | 48 ++++ .../com_mokobackup/tmpl/dashboard/default.php | 265 ++++++++++++++++++ .../en-GB/plg_actionlog_mokobackup.ini | 9 + .../en-GB/plg_actionlog_mokobackup.sys.ini | 3 + .../en-US/plg_actionlog_mokobackup.ini | 9 + .../en-US/plg_actionlog_mokobackup.sys.ini | 3 + .../plg_actionlog_mokobackup/mokobackup.php | 11 + .../plg_actionlog_mokobackup/mokobackup.xml | 32 +++ .../services/provider.php | 37 +++ .../src/Extension/MokoBackupActionlog.php | 174 ++++++++++++ .../language/en-GB/plg_console_mokobackup.ini | 3 + .../en-GB/plg_console_mokobackup.sys.ini | 3 + .../language/en-US/plg_console_mokobackup.ini | 3 + .../en-US/plg_console_mokobackup.sys.ini | 3 + .../plg_console_mokobackup/mokobackup.php | 11 + .../plg_console_mokobackup/mokobackup.xml | 32 +++ .../services/provider.php | 37 +++ .../src/Command/CleanupCommand.php | 125 +++++++++ .../src/Command/ListCommand.php | 87 ++++++ .../src/Command/ProfilesCommand.php | 68 +++++ .../src/Command/RestoreCommand.php | 101 +++++++ .../src/Command/RunCommand.php | 68 +++++ .../src/Extension/MokoBackupConsole.php | 45 +++ .../language/en-GB/plg_content_mokobackup.ini | 9 + .../en-GB/plg_content_mokobackup.sys.ini | 3 + .../language/en-US/plg_content_mokobackup.ini | 9 + .../en-US/plg_content_mokobackup.sys.ini | 3 + .../plg_content_mokobackup/mokobackup.php | 11 + .../plg_content_mokobackup/mokobackup.xml | 71 +++++ .../services/provider.php | 37 +++ .../src/Extension/MokoBackupContent.php | 95 +++++++ src/pkg_mokobackup.xml | 3 + src/script.php | 33 +++ 39 files changed, 1673 insertions(+), 1 deletion(-) create mode 100644 src/packages/com_mokobackup/src/Model/DashboardModel.php create mode 100644 src/packages/com_mokobackup/src/View/Dashboard/HtmlView.php create mode 100644 src/packages/com_mokobackup/tmpl/dashboard/default.php create mode 100644 src/packages/plg_actionlog_mokobackup/language/en-GB/plg_actionlog_mokobackup.ini create mode 100644 src/packages/plg_actionlog_mokobackup/language/en-GB/plg_actionlog_mokobackup.sys.ini create mode 100644 src/packages/plg_actionlog_mokobackup/language/en-US/plg_actionlog_mokobackup.ini create mode 100644 src/packages/plg_actionlog_mokobackup/language/en-US/plg_actionlog_mokobackup.sys.ini create mode 100644 src/packages/plg_actionlog_mokobackup/mokobackup.php create mode 100644 src/packages/plg_actionlog_mokobackup/mokobackup.xml create mode 100644 src/packages/plg_actionlog_mokobackup/services/provider.php create mode 100644 src/packages/plg_actionlog_mokobackup/src/Extension/MokoBackupActionlog.php create mode 100644 src/packages/plg_console_mokobackup/language/en-GB/plg_console_mokobackup.ini create mode 100644 src/packages/plg_console_mokobackup/language/en-GB/plg_console_mokobackup.sys.ini create mode 100644 src/packages/plg_console_mokobackup/language/en-US/plg_console_mokobackup.ini create mode 100644 src/packages/plg_console_mokobackup/language/en-US/plg_console_mokobackup.sys.ini create mode 100644 src/packages/plg_console_mokobackup/mokobackup.php create mode 100644 src/packages/plg_console_mokobackup/mokobackup.xml create mode 100644 src/packages/plg_console_mokobackup/services/provider.php create mode 100644 src/packages/plg_console_mokobackup/src/Command/CleanupCommand.php create mode 100644 src/packages/plg_console_mokobackup/src/Command/ListCommand.php create mode 100644 src/packages/plg_console_mokobackup/src/Command/ProfilesCommand.php create mode 100644 src/packages/plg_console_mokobackup/src/Command/RestoreCommand.php create mode 100644 src/packages/plg_console_mokobackup/src/Command/RunCommand.php create mode 100644 src/packages/plg_console_mokobackup/src/Extension/MokoBackupConsole.php create mode 100644 src/packages/plg_content_mokobackup/language/en-GB/plg_content_mokobackup.ini create mode 100644 src/packages/plg_content_mokobackup/language/en-GB/plg_content_mokobackup.sys.ini create mode 100644 src/packages/plg_content_mokobackup/language/en-US/plg_content_mokobackup.ini create mode 100644 src/packages/plg_content_mokobackup/language/en-US/plg_content_mokobackup.sys.ini create mode 100644 src/packages/plg_content_mokobackup/mokobackup.php create mode 100644 src/packages/plg_content_mokobackup/mokobackup.xml create mode 100644 src/packages/plg_content_mokobackup/services/provider.php create mode 100644 src/packages/plg_content_mokobackup/src/Extension/MokoBackupContent.php diff --git a/src/packages/com_mokobackup/language/en-GB/com_mokobackup.ini b/src/packages/com_mokobackup/language/en-GB/com_mokobackup.ini index 0921f20..442c06a 100644 --- a/src/packages/com_mokobackup/language/en-GB/com_mokobackup.ini +++ b/src/packages/com_mokobackup/language/en-GB/com_mokobackup.ini @@ -8,9 +8,24 @@ COM_MOKOBACKUP="MokoJoomBackup" COM_MOKOBACKUP_DESCRIPTION="Full-site backup and restore for Joomla" ; Submenu +COM_MOKOBACKUP_SUBMENU_DASHBOARD="Dashboard" COM_MOKOBACKUP_SUBMENU_BACKUPS="Backup Records" COM_MOKOBACKUP_SUBMENU_PROFILES="Backup Profiles" +; Dashboard view +COM_MOKOBACKUP_DASHBOARD_TITLE="MokoJoomBackup Dashboard" +COM_MOKOBACKUP_DASHBOARD_LAST_BACKUP="Last Backup" +COM_MOKOBACKUP_DASHBOARD_NO_BACKUPS="No backups yet" +COM_MOKOBACKUP_DASHBOARD_NEXT_SCHEDULED="Next Scheduled" +COM_MOKOBACKUP_DASHBOARD_NO_SCHEDULED="No tasks scheduled" +COM_MOKOBACKUP_DASHBOARD_TOTAL_BACKUPS="Total Backups" +COM_MOKOBACKUP_DASHBOARD_STORAGE="Storage Used" +COM_MOKOBACKUP_DASHBOARD_FAILURES_7D="%d failures (7 days)" +COM_MOKOBACKUP_DASHBOARD_QUICK_ACTIONS="Quick Actions" +COM_MOKOBACKUP_DASHBOARD_SCHEDULED_TASKS="Scheduled Tasks" +COM_MOKOBACKUP_DASHBOARD_UPDATE_SITE="Update Site" +COM_MOKOBACKUP_DASHBOARD_SYSTEM_HEALTH="System Health" + ; Backups view COM_MOKOBACKUP_BACKUPS_TITLE="Backup Records" COM_MOKOBACKUP_BACKUPS_TABLE_CAPTION="Table of backup records" diff --git a/src/packages/com_mokobackup/language/en-US/com_mokobackup.ini b/src/packages/com_mokobackup/language/en-US/com_mokobackup.ini index 309264d..577580b 100644 --- a/src/packages/com_mokobackup/language/en-US/com_mokobackup.ini +++ b/src/packages/com_mokobackup/language/en-US/com_mokobackup.ini @@ -6,8 +6,21 @@ COM_MOKOBACKUP="MokoJoomBackup" COM_MOKOBACKUP_DESCRIPTION="Full-site backup and restore for Joomla" +COM_MOKOBACKUP_SUBMENU_DASHBOARD="Dashboard" COM_MOKOBACKUP_SUBMENU_BACKUPS="Backup Records" COM_MOKOBACKUP_SUBMENU_PROFILES="Backup Profiles" +COM_MOKOBACKUP_DASHBOARD_TITLE="MokoJoomBackup Dashboard" +COM_MOKOBACKUP_DASHBOARD_LAST_BACKUP="Last Backup" +COM_MOKOBACKUP_DASHBOARD_NO_BACKUPS="No backups yet" +COM_MOKOBACKUP_DASHBOARD_NEXT_SCHEDULED="Next Scheduled" +COM_MOKOBACKUP_DASHBOARD_NO_SCHEDULED="No tasks scheduled" +COM_MOKOBACKUP_DASHBOARD_TOTAL_BACKUPS="Total Backups" +COM_MOKOBACKUP_DASHBOARD_STORAGE="Storage Used" +COM_MOKOBACKUP_DASHBOARD_FAILURES_7D="%d failures (7 days)" +COM_MOKOBACKUP_DASHBOARD_QUICK_ACTIONS="Quick Actions" +COM_MOKOBACKUP_DASHBOARD_SCHEDULED_TASKS="Scheduled Tasks" +COM_MOKOBACKUP_DASHBOARD_UPDATE_SITE="Update Site" +COM_MOKOBACKUP_DASHBOARD_SYSTEM_HEALTH="System Health" COM_MOKOBACKUP_BACKUPS_TITLE="Backup Records" COM_MOKOBACKUP_PROFILES_TITLE="Backup Profiles" COM_MOKOBACKUP_TOOLBAR_BACKUP_NOW="Backup Now" diff --git a/src/packages/com_mokobackup/mokobackup.xml b/src/packages/com_mokobackup/mokobackup.xml index 1dcc977..f715793 100644 --- a/src/packages/com_mokobackup/mokobackup.xml +++ b/src/packages/com_mokobackup/mokobackup.xml @@ -40,6 +40,7 @@ COM_MOKOBACKUP + COM_MOKOBACKUP_SUBMENU_DASHBOARD COM_MOKOBACKUP_SUBMENU_BACKUPS COM_MOKOBACKUP_SUBMENU_PROFILES diff --git a/src/packages/com_mokobackup/src/Controller/DisplayController.php b/src/packages/com_mokobackup/src/Controller/DisplayController.php index 5e4ec11..5425324 100644 --- a/src/packages/com_mokobackup/src/Controller/DisplayController.php +++ b/src/packages/com_mokobackup/src/Controller/DisplayController.php @@ -16,5 +16,5 @@ use Joomla\CMS\MVC\Controller\BaseController; class DisplayController extends BaseController { - protected $default_view = 'backups'; + protected $default_view = 'dashboard'; } diff --git a/src/packages/com_mokobackup/src/Engine/BackupEngine.php b/src/packages/com_mokobackup/src/Engine/BackupEngine.php index 60e0910..cf456cd 100644 --- a/src/packages/com_mokobackup/src/Engine/BackupEngine.php +++ b/src/packages/com_mokobackup/src/Engine/BackupEngine.php @@ -13,6 +13,7 @@ namespace Joomla\Component\MokoBackup\Administrator\Engine; defined('_JEXEC') or die; use Joomla\CMS\Factory; +use Joomla\Event\Event; class BackupEngine { @@ -250,6 +251,9 @@ class BackupEngine // Send success notification NotificationSender::send($profile, $update, true, implode("\n", $this->log)); + // Dispatch event for actionlog and other listeners + $this->dispatchAfterRun(true, $recordId, $description, $profileId, $origin); + return [ 'success' => true, 'message' => 'Backup complete: ' . $archiveName . ' (' . $sizeHuman . ')', @@ -275,6 +279,9 @@ class BackupEngine // Send failure notification NotificationSender::send($profile, $update, false, implode("\n", $this->log)); + // Dispatch event for actionlog and other listeners + $this->dispatchAfterRun(false, $recordId, $description, $profileId, $origin); + return ['success' => false, 'message' => 'Backup failed: ' . $e->getMessage(), 'record_id' => $recordId]; } } @@ -445,6 +452,28 @@ class BackupEngine )); } + /** + * Dispatch the onMokoBackupAfterRun event so plugins (actionlog, etc.) can react. + */ + private function dispatchAfterRun(bool $success, int $recordId, string $description, int $profileId, string $origin): void + { + try { + $app = Factory::getApplication(); + + $event = new Event('onMokoBackupAfterRun', [ + 'success' => $success, + 'record_id' => $recordId, + 'description' => $description, + 'profile_id' => $profileId, + 'origin' => $origin, + ]); + + $app->getDispatcher()->dispatch('onMokoBackupAfterRun', $event); + } catch (\Throwable $e) { + // Never let a listener failure break the backup result + } + } + private function log(string $message): void { $this->log[] = '[' . date('H:i:s') . '] ' . $message; diff --git a/src/packages/com_mokobackup/src/Model/DashboardModel.php b/src/packages/com_mokobackup/src/Model/DashboardModel.php new file mode 100644 index 0000000..d10a2b2 --- /dev/null +++ b/src/packages/com_mokobackup/src/Model/DashboardModel.php @@ -0,0 +1,163 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Component\MokoBackup\Administrator\Model; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\MVC\Model\BaseDatabaseModel; + +class DashboardModel extends BaseDatabaseModel +{ + /** + * Get the most recent completed backup record. + * + * @return object|null + */ + public function getLastBackup(): ?object + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('r.*, p.title AS profile_title') + ->from($db->quoteName('#__mokobackup_records', 'r')) + ->join('LEFT', $db->quoteName('#__mokobackup_profiles', 'p') . ' ON p.id = r.profile_id') + ->where($db->quoteName('r.status') . ' = ' . $db->quote('complete')) + ->order($db->quoteName('r.backupend') . ' DESC'); + $db->setQuery($query, 0, 1); + + return $db->loadObject() ?: null; + } + + /** + * Query com_scheduler for the next scheduled MokoBackup task. + * + * @return object|null Object with next_execution and title, or null + */ + public function getNextScheduled(): ?object + { + $db = $this->getDatabase(); + + try { + $query = $db->getQuery(true) + ->select($db->quoteName(['t.next_execution', 't.title'])) + ->from($db->quoteName('#__scheduler_tasks', 't')) + ->where($db->quoteName('t.type') . ' = ' . $db->quote('mokobackup.run_profile')) + ->where($db->quoteName('t.state') . ' = 1') + ->order($db->quoteName('t.next_execution') . ' ASC'); + $db->setQuery($query, 0, 1); + + return $db->loadObject() ?: null; + } catch (\Throwable $e) { + return null; + } + } + + /** + * Get backup statistics. + * + * @return object Object with total_count, total_size, fail_count_7d + */ + public function getStats(): object + { + $db = $this->getDatabase(); + + // Total completed backups and storage + $query = $db->getQuery(true) + ->select('COUNT(*) AS total_count') + ->select('COALESCE(SUM(' . $db->quoteName('total_size') . '), 0) AS total_size') + ->from($db->quoteName('#__mokobackup_records')) + ->where($db->quoteName('status') . ' = ' . $db->quote('complete')); + $db->setQuery($query); + $stats = $db->loadObject(); + + // Failures in last 7 days + $cutoff = date('Y-m-d H:i:s', strtotime('-7 days')); + $query = $db->getQuery(true) + ->select('COUNT(*) AS fail_count') + ->from($db->quoteName('#__mokobackup_records')) + ->where($db->quoteName('status') . ' = ' . $db->quote('fail')) + ->where($db->quoteName('backupstart') . ' >= ' . $db->quote($cutoff)); + $db->setQuery($query); + $stats->fail_count_7d = (int) $db->loadResult(); + + return $stats; + } + + /** + * Check system health for backup readiness. + * + * @return array Array of check results [{label, status, detail}] + */ + public function getSystemHealth(): array + { + $checks = []; + + // PHP version + $checks[] = (object) [ + 'label' => 'PHP Version', + 'status' => version_compare(PHP_VERSION, '8.1.0', '>='), + 'detail' => PHP_VERSION, + ]; + + // ZipArchive extension + $checks[] = (object) [ + 'label' => 'ZipArchive', + 'status' => extension_loaded('zip'), + 'detail' => extension_loaded('zip') ? 'Loaded' : 'Not loaded', + ]; + + // AES-256 encryption support + $aesSupport = defined('ZipArchive::EM_AES_256'); + $checks[] = (object) [ + 'label' => 'AES-256 Encryption', + 'status' => $aesSupport, + 'detail' => $aesSupport ? 'Available' : 'Requires libzip 1.2.0+', + ]; + + // Backup directory writable + $backupDir = JPATH_ADMINISTRATOR . '/components/com_mokobackup/backups'; + $writable = is_dir($backupDir) && is_writable($backupDir); + $checks[] = (object) [ + 'label' => 'Backup Directory', + 'status' => $writable, + 'detail' => $writable ? 'Writable' : 'Not writable or missing', + ]; + + // Disk space + $freeSpace = @disk_free_space($backupDir ?: JPATH_ROOT); + $freeGB = $freeSpace ? round($freeSpace / 1073741824, 1) : 0; + $checks[] = (object) [ + 'label' => 'Free Disk Space', + 'status' => $freeGB >= 1.0, + 'detail' => $freeGB . ' GB free', + ]; + + return $checks; + } + + /** + * Get published backup profiles for the quick-action selector. + * + * @return array + */ + public function getProfiles(): array + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['id', 'title', 'backup_type'])) + ->from($db->quoteName('#__mokobackup_profiles')) + ->where($db->quoteName('published') . ' = 1') + ->order($db->quoteName('ordering') . ' ASC'); + $db->setQuery($query); + + return $db->loadObjectList() ?: []; + } +} diff --git a/src/packages/com_mokobackup/src/View/Dashboard/HtmlView.php b/src/packages/com_mokobackup/src/View/Dashboard/HtmlView.php new file mode 100644 index 0000000..bbfa660 --- /dev/null +++ b/src/packages/com_mokobackup/src/View/Dashboard/HtmlView.php @@ -0,0 +1,48 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Component\MokoBackup\Administrator\View\Dashboard; + +defined('_JEXEC') or die; + +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Toolbar\ToolbarHelper; + +class HtmlView extends BaseHtmlView +{ + public ?object $lastBackup = null; + public ?object $nextScheduled = null; + public object $stats; + public array $systemHealth = []; + public array $profiles = []; + + public function display($tpl = null): void + { + /** @var \Joomla\Component\MokoBackup\Administrator\Model\DashboardModel $model */ + $model = $this->getModel(); + + $this->lastBackup = $model->getLastBackup(); + $this->nextScheduled = $model->getNextScheduled(); + $this->stats = $model->getStats(); + $this->systemHealth = $model->getSystemHealth(); + $this->profiles = $model->getProfiles(); + + $this->addToolbar(); + + parent::display($tpl); + } + + protected function addToolbar(): void + { + ToolbarHelper::title(Text::_('COM_MOKOBACKUP_DASHBOARD_TITLE'), 'archive'); + ToolbarHelper::preferences('com_mokobackup'); + } +} diff --git a/src/packages/com_mokobackup/tmpl/dashboard/default.php b/src/packages/com_mokobackup/tmpl/dashboard/default.php new file mode 100644 index 0000000..8d7d43c --- /dev/null +++ b/src/packages/com_mokobackup/tmpl/dashboard/default.php @@ -0,0 +1,265 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Session\Session; + +$ajaxToken = Session::getFormToken(); +$ajaxUrl = Route::_('index.php?option=com_mokobackup&format=json', false); +?> +
+ +
+
+
+ +
+ lastBackup) : ?> +

+ lastBackup->backupend, Text::_('DATE_FORMAT_LC4')); ?> +

+ + escape($this->lastBackup->profile_title); ?> + — + lastBackup->total_size); ?> + + +

+ +
+
+
+ +
+
+
+ +
+ nextScheduled) : ?> +

+ nextScheduled->next_execution, Text::_('DATE_FORMAT_LC4')); ?> +

+ escape($this->nextScheduled->title); ?> + +

+ +
+
+
+ +
+
+
+ +
+

stats->total_count; ?>

+
+
+
+ +
+
+
+ +
+

+ stats->total_size); ?> +

+ stats->fail_count_7d > 0) : ?> + + stats->fail_count_7d); ?> + + +
+
+
+
+ + +
+
+
+
+
+
+
+ profiles)) : ?> +
+ + +
+ + + +
+
+
+ + +
+
+
+
+
+
+ + + systemHealth as $check) : ?> + + + + + + + +
+ status) : ?> + + + + + escape($check->label); ?>escape($check->detail); ?>
+
+
+
+
+ + + + + diff --git a/src/packages/plg_actionlog_mokobackup/language/en-GB/plg_actionlog_mokobackup.ini b/src/packages/plg_actionlog_mokobackup/language/en-GB/plg_actionlog_mokobackup.ini new file mode 100644 index 0000000..6997740 --- /dev/null +++ b/src/packages/plg_actionlog_mokobackup/language/en-GB/plg_actionlog_mokobackup.ini @@ -0,0 +1,9 @@ +; MokoJoomBackup — Actionlog Plugin language file (en-GB) +PLG_ACTIONLOG_MOKOBACKUP="Action Log - MokoJoomBackup" +PLG_ACTIONLOG_MOKOBACKUP_DESCRIPTION="Logs MokoJoomBackup actions (backup, restore, profile changes) to User Action Logs." +PLG_ACTIONLOG_MOKOBACKUP_PROFILE_CREATED="User {username} created backup profile "{title}" (ID: {id})" +PLG_ACTIONLOG_MOKOBACKUP_PROFILE_UPDATED="User {username} updated backup profile "{title}" (ID: {id})" +PLG_ACTIONLOG_MOKOBACKUP_PROFILE_DELETED="User {username} deleted backup profile "{title}" (ID: {id})" +PLG_ACTIONLOG_MOKOBACKUP_RECORD_DELETED="User {username} deleted backup record "{title}" (ID: {id})" +PLG_ACTIONLOG_MOKOBACKUP_BACKUP_COMPLETE="Backup completed: "{title}" (ID: {id}, profile: {profile_id}, origin: {origin})" +PLG_ACTIONLOG_MOKOBACKUP_BACKUP_FAILED="Backup FAILED: "{title}" (ID: {id}, profile: {profile_id}, origin: {origin})" diff --git a/src/packages/plg_actionlog_mokobackup/language/en-GB/plg_actionlog_mokobackup.sys.ini b/src/packages/plg_actionlog_mokobackup/language/en-GB/plg_actionlog_mokobackup.sys.ini new file mode 100644 index 0000000..3e1c655 --- /dev/null +++ b/src/packages/plg_actionlog_mokobackup/language/en-GB/plg_actionlog_mokobackup.sys.ini @@ -0,0 +1,3 @@ +; MokoJoomBackup — Actionlog Plugin system language file (en-GB) +PLG_ACTIONLOG_MOKOBACKUP="Action Log - MokoJoomBackup" +PLG_ACTIONLOG_MOKOBACKUP_DESCRIPTION="Logs MokoJoomBackup actions (backup, restore, profile changes) to User Action Logs." diff --git a/src/packages/plg_actionlog_mokobackup/language/en-US/plg_actionlog_mokobackup.ini b/src/packages/plg_actionlog_mokobackup/language/en-US/plg_actionlog_mokobackup.ini new file mode 100644 index 0000000..27cf1d6 --- /dev/null +++ b/src/packages/plg_actionlog_mokobackup/language/en-US/plg_actionlog_mokobackup.ini @@ -0,0 +1,9 @@ +; MokoJoomBackup — Actionlog Plugin language file (en-US) +PLG_ACTIONLOG_MOKOBACKUP="Action Log - MokoJoomBackup" +PLG_ACTIONLOG_MOKOBACKUP_DESCRIPTION="Logs MokoJoomBackup actions (backup, restore, profile changes) to User Action Logs." +PLG_ACTIONLOG_MOKOBACKUP_PROFILE_CREATED="User {username} created backup profile "{title}" (ID: {id})" +PLG_ACTIONLOG_MOKOBACKUP_PROFILE_UPDATED="User {username} updated backup profile "{title}" (ID: {id})" +PLG_ACTIONLOG_MOKOBACKUP_PROFILE_DELETED="User {username} deleted backup profile "{title}" (ID: {id})" +PLG_ACTIONLOG_MOKOBACKUP_RECORD_DELETED="User {username} deleted backup record "{title}" (ID: {id})" +PLG_ACTIONLOG_MOKOBACKUP_BACKUP_COMPLETE="Backup completed: "{title}" (ID: {id}, profile: {profile_id}, origin: {origin})" +PLG_ACTIONLOG_MOKOBACKUP_BACKUP_FAILED="Backup FAILED: "{title}" (ID: {id}, profile: {profile_id}, origin: {origin})" diff --git a/src/packages/plg_actionlog_mokobackup/language/en-US/plg_actionlog_mokobackup.sys.ini b/src/packages/plg_actionlog_mokobackup/language/en-US/plg_actionlog_mokobackup.sys.ini new file mode 100644 index 0000000..1737124 --- /dev/null +++ b/src/packages/plg_actionlog_mokobackup/language/en-US/plg_actionlog_mokobackup.sys.ini @@ -0,0 +1,3 @@ +; MokoJoomBackup — Actionlog Plugin system language file (en-US) +PLG_ACTIONLOG_MOKOBACKUP="Action Log - MokoJoomBackup" +PLG_ACTIONLOG_MOKOBACKUP_DESCRIPTION="Logs MokoJoomBackup actions (backup, restore, profile changes) to User Action Logs." diff --git a/src/packages/plg_actionlog_mokobackup/mokobackup.php b/src/packages/plg_actionlog_mokobackup/mokobackup.php new file mode 100644 index 0000000..2a4226a --- /dev/null +++ b/src/packages/plg_actionlog_mokobackup/mokobackup.php @@ -0,0 +1,11 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +defined('_JEXEC') or die; diff --git a/src/packages/plg_actionlog_mokobackup/mokobackup.xml b/src/packages/plg_actionlog_mokobackup/mokobackup.xml new file mode 100644 index 0000000..b5fe605 --- /dev/null +++ b/src/packages/plg_actionlog_mokobackup/mokobackup.xml @@ -0,0 +1,32 @@ + + + + plg_actionlog_mokobackup + 01.01.04-dev + 2026-06-04 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_ACTIONLOG_MOKOBACKUP_DESCRIPTION + + Joomla\Plugin\Actionlog\MokoBackup + + + mokobackup.php + services + src + + + + language/en-GB/plg_actionlog_mokobackup.ini + language/en-GB/plg_actionlog_mokobackup.sys.ini + + diff --git a/src/packages/plg_actionlog_mokobackup/services/provider.php b/src/packages/plg_actionlog_mokobackup/services/provider.php new file mode 100644 index 0000000..b13a445 --- /dev/null +++ b/src/packages/plg_actionlog_mokobackup/services/provider.php @@ -0,0 +1,37 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Extension\PluginInterface; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Event\DispatcherInterface; +use Joomla\Plugin\Actionlog\MokoBackup\Extension\MokoBackupActionlog; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new MokoBackupActionlog( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('actionlog', 'mokobackup') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/src/packages/plg_actionlog_mokobackup/src/Extension/MokoBackupActionlog.php b/src/packages/plg_actionlog_mokobackup/src/Extension/MokoBackupActionlog.php new file mode 100644 index 0000000..2cb97e7 --- /dev/null +++ b/src/packages/plg_actionlog_mokobackup/src/Extension/MokoBackupActionlog.php @@ -0,0 +1,174 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Plugin\Actionlog\MokoBackup\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Event\Model; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\Actionlogs\Administrator\Helper\ActionlogsHelper; +use Joomla\Event\Event; +use Joomla\Event\SubscriberInterface; + +final class MokoBackupActionlog extends CMSPlugin implements SubscriberInterface +{ + protected $autoloadLanguage = true; + + public static function getSubscribedEvents(): array + { + return [ + 'onContentAfterSave' => 'onContentAfterSave', + 'onContentAfterDelete' => 'onContentAfterDelete', + 'onMokoBackupAfterRun' => 'onMokoBackupAfterRun', + ]; + } + + /** + * Log when a backup profile is saved (created or updated). + */ + public function onContentAfterSave(Event $event): void + { + [$context, $table, $isNew] = array_values($event->getArguments()); + + if ($context !== 'com_mokobackup.profile') { + return; + } + + $messageKey = $isNew + ? 'PLG_ACTIONLOG_MOKOBACKUP_PROFILE_CREATED' + : 'PLG_ACTIONLOG_MOKOBACKUP_PROFILE_UPDATED'; + + $this->addLog( + [ + $messageKey, + 'id' => $table->id, + 'title' => $table->title, + 'userid' => $this->getCurrentUserId(), + 'username' => $this->getCurrentUserName(), + ], + $messageKey, + 'com_mokobackup.profile', + $this->getCurrentUserId() + ); + } + + /** + * Log when a backup profile or record is deleted. + */ + public function onContentAfterDelete(Event $event): void + { + [$context, $table] = array_values($event->getArguments()); + + if ($context === 'com_mokobackup.profile') { + $this->addLog( + [ + 'PLG_ACTIONLOG_MOKOBACKUP_PROFILE_DELETED', + 'id' => $table->id, + 'title' => $table->title ?? '', + 'userid' => $this->getCurrentUserId(), + 'username' => $this->getCurrentUserName(), + ], + 'PLG_ACTIONLOG_MOKOBACKUP_PROFILE_DELETED', + 'com_mokobackup.profile', + $this->getCurrentUserId() + ); + } elseif ($context === 'com_mokobackup.backup') { + $this->addLog( + [ + 'PLG_ACTIONLOG_MOKOBACKUP_RECORD_DELETED', + 'id' => $table->id, + 'title' => $table->description ?? 'Backup #' . $table->id, + 'userid' => $this->getCurrentUserId(), + 'username' => $this->getCurrentUserName(), + ], + 'PLG_ACTIONLOG_MOKOBACKUP_RECORD_DELETED', + 'com_mokobackup.backup', + $this->getCurrentUserId() + ); + } + } + + /** + * Log when a backup completes or fails. + * This event should be dispatched from BackupEngine. + */ + public function onMokoBackupAfterRun(Event $event): void + { + $args = $event->getArguments(); + + $success = $args['success'] ?? false; + $recordId = $args['record_id'] ?? 0; + $description = $args['description'] ?? ''; + $profileId = $args['profile_id'] ?? 0; + $origin = $args['origin'] ?? 'backend'; + + $messageKey = $success + ? 'PLG_ACTIONLOG_MOKOBACKUP_BACKUP_COMPLETE' + : 'PLG_ACTIONLOG_MOKOBACKUP_BACKUP_FAILED'; + + $this->addLog( + [ + $messageKey, + 'id' => $recordId, + 'title' => $description ?: 'Backup #' . $recordId, + 'profile_id' => $profileId, + 'origin' => $origin, + 'userid' => $this->getCurrentUserId(), + 'username' => $this->getCurrentUserName(), + ], + $messageKey, + 'com_mokobackup.backup', + $this->getCurrentUserId() + ); + } + + /** + * Write an action log entry. + */ + private function addLog(array $message, string $messageLanguageKey, string $context, int $userId): void + { + $params = [ + 'message_language_key' => $messageLanguageKey, + 'message' => json_encode($message), + 'date' => date('Y-m-d H:i:s'), + 'extension' => 'com_mokobackup', + 'user_id' => $userId, + 'ip_address' => ActionlogsHelper::getIp(), + 'item_id' => $message['id'] ?? 0, + ]; + + try { + $db = Factory::getDbo(); + $db->insertObject('#__action_logs', (object) $params); + } catch (\Throwable $e) { + // Non-critical — don't break the operation + } + } + + private function getCurrentUserId(): int + { + try { + return (int) Factory::getApplication()->getIdentity()->id; + } catch (\Throwable $e) { + return 0; + } + } + + private function getCurrentUserName(): string + { + try { + return Factory::getApplication()->getIdentity()->username ?: 'system'; + } catch (\Throwable $e) { + return 'system'; + } + } +} diff --git a/src/packages/plg_console_mokobackup/language/en-GB/plg_console_mokobackup.ini b/src/packages/plg_console_mokobackup/language/en-GB/plg_console_mokobackup.ini new file mode 100644 index 0000000..4b87bca --- /dev/null +++ b/src/packages/plg_console_mokobackup/language/en-GB/plg_console_mokobackup.ini @@ -0,0 +1,3 @@ +; MokoJoomBackup — Console Plugin language file (en-GB) +PLG_CONSOLE_MOKOBACKUP="Console - MokoJoomBackup" +PLG_CONSOLE_MOKOBACKUP_DESCRIPTION="CLI commands for MokoJoomBackup: run, list, profiles, restore, cleanup." diff --git a/src/packages/plg_console_mokobackup/language/en-GB/plg_console_mokobackup.sys.ini b/src/packages/plg_console_mokobackup/language/en-GB/plg_console_mokobackup.sys.ini new file mode 100644 index 0000000..02fb8d8 --- /dev/null +++ b/src/packages/plg_console_mokobackup/language/en-GB/plg_console_mokobackup.sys.ini @@ -0,0 +1,3 @@ +; MokoJoomBackup — Console Plugin system language file (en-GB) +PLG_CONSOLE_MOKOBACKUP="Console - MokoJoomBackup" +PLG_CONSOLE_MOKOBACKUP_DESCRIPTION="CLI commands for MokoJoomBackup: run, list, profiles, restore, cleanup." diff --git a/src/packages/plg_console_mokobackup/language/en-US/plg_console_mokobackup.ini b/src/packages/plg_console_mokobackup/language/en-US/plg_console_mokobackup.ini new file mode 100644 index 0000000..9fa5c15 --- /dev/null +++ b/src/packages/plg_console_mokobackup/language/en-US/plg_console_mokobackup.ini @@ -0,0 +1,3 @@ +; MokoJoomBackup — Console Plugin language file (en-US) +PLG_CONSOLE_MOKOBACKUP="Console - MokoJoomBackup" +PLG_CONSOLE_MOKOBACKUP_DESCRIPTION="CLI commands for MokoJoomBackup: run, list, profiles, restore, cleanup." diff --git a/src/packages/plg_console_mokobackup/language/en-US/plg_console_mokobackup.sys.ini b/src/packages/plg_console_mokobackup/language/en-US/plg_console_mokobackup.sys.ini new file mode 100644 index 0000000..d22c08c --- /dev/null +++ b/src/packages/plg_console_mokobackup/language/en-US/plg_console_mokobackup.sys.ini @@ -0,0 +1,3 @@ +; MokoJoomBackup — Console Plugin system language file (en-US) +PLG_CONSOLE_MOKOBACKUP="Console - MokoJoomBackup" +PLG_CONSOLE_MOKOBACKUP_DESCRIPTION="CLI commands for MokoJoomBackup: run, list, profiles, restore, cleanup." diff --git a/src/packages/plg_console_mokobackup/mokobackup.php b/src/packages/plg_console_mokobackup/mokobackup.php new file mode 100644 index 0000000..724a1bb --- /dev/null +++ b/src/packages/plg_console_mokobackup/mokobackup.php @@ -0,0 +1,11 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +defined('_JEXEC') or die; diff --git a/src/packages/plg_console_mokobackup/mokobackup.xml b/src/packages/plg_console_mokobackup/mokobackup.xml new file mode 100644 index 0000000..c641770 --- /dev/null +++ b/src/packages/plg_console_mokobackup/mokobackup.xml @@ -0,0 +1,32 @@ + + + + plg_console_mokobackup + 01.01.04-dev + 2026-06-04 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_CONSOLE_MOKOBACKUP_DESCRIPTION + + Joomla\Plugin\Console\MokoBackup + + + mokobackup.php + services + src + + + + language/en-GB/plg_console_mokobackup.ini + language/en-GB/plg_console_mokobackup.sys.ini + + diff --git a/src/packages/plg_console_mokobackup/services/provider.php b/src/packages/plg_console_mokobackup/services/provider.php new file mode 100644 index 0000000..3bacb2f --- /dev/null +++ b/src/packages/plg_console_mokobackup/services/provider.php @@ -0,0 +1,37 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Extension\PluginInterface; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Event\DispatcherInterface; +use Joomla\Plugin\Console\MokoBackup\Extension\MokoBackupConsole; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new MokoBackupConsole( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('console', 'mokobackup') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/src/packages/plg_console_mokobackup/src/Command/CleanupCommand.php b/src/packages/plg_console_mokobackup/src/Command/CleanupCommand.php new file mode 100644 index 0000000..b26a2a5 --- /dev/null +++ b/src/packages/plg_console_mokobackup/src/Command/CleanupCommand.php @@ -0,0 +1,125 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Plugin\Console\MokoBackup\Command; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\Console\Command\AbstractCommand; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class CleanupCommand extends AbstractCommand +{ + protected static $defaultName = 'mokobackup:cleanup'; + + protected function configure(): void + { + $this->setDescription('Clean up old backup records and archive files'); + $this->addOption('max-age', null, InputOption::VALUE_REQUIRED, 'Max age in days', '30'); + $this->addOption('max-count', null, InputOption::VALUE_REQUIRED, 'Max number of backups to keep', '10'); + $this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show what would be deleted without deleting'); + } + + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $maxAge = (int) $input->getOption('max-age'); + $maxCount = (int) $input->getOption('max-count'); + $dryRun = $input->getOption('dry-run'); + + $io->title('MokoJoomBackup — Cleanup'); + + if ($dryRun) { + $io->note('Dry run — no files will be deleted.'); + } + + $db = Factory::getDbo(); + $deleted = 0; + + // Delete by age + $cutoff = date('Y-m-d H:i:s', strtotime("-{$maxAge} days")); + $query = $db->getQuery(true) + ->select('id, absolute_path, description, backupstart') + ->from($db->quoteName('#__mokobackup_records')) + ->where($db->quoteName('backupstart') . ' < ' . $db->quote($cutoff)) + ->where($db->quoteName('status') . ' = ' . $db->quote('complete')); + $db->setQuery($query); + $expired = $db->loadObjectList(); + + foreach ($expired as $record) { + $io->text('Expired: #' . $record->id . ' — ' . $record->backupstart . ' — ' . ($record->description ?: 'no description')); + + if (!$dryRun) { + if (!empty($record->absolute_path) && is_file($record->absolute_path)) { + @unlink($record->absolute_path); + } + + $db->setQuery( + $db->getQuery(true) + ->delete($db->quoteName('#__mokobackup_records')) + ->where($db->quoteName('id') . ' = ' . (int) $record->id) + ); + $db->execute(); + } + + $deleted++; + } + + // Enforce max count + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokobackup_records')) + ->where($db->quoteName('status') . ' = ' . $db->quote('complete')); + $db->setQuery($query); + $totalCount = (int) $db->loadResult(); + + if ($totalCount > $maxCount) { + $excess = $totalCount - $maxCount; + $query = $db->getQuery(true) + ->select('id, absolute_path, description, backupstart') + ->from($db->quoteName('#__mokobackup_records')) + ->where($db->quoteName('status') . ' = ' . $db->quote('complete')) + ->order($db->quoteName('backupstart') . ' ASC'); + $db->setQuery($query, 0, $excess); + $oldest = $db->loadObjectList(); + + foreach ($oldest as $record) { + $io->text('Over limit: #' . $record->id . ' — ' . $record->backupstart); + + if (!$dryRun) { + if (!empty($record->absolute_path) && is_file($record->absolute_path)) { + @unlink($record->absolute_path); + } + + $db->setQuery( + $db->getQuery(true) + ->delete($db->quoteName('#__mokobackup_records')) + ->where($db->quoteName('id') . ' = ' . (int) $record->id) + ); + $db->execute(); + } + + $deleted++; + } + } + + if ($deleted === 0) { + $io->success('No backups to clean up.'); + } else { + $io->success(($dryRun ? 'Would delete ' : 'Deleted ') . $deleted . ' backup record(s).'); + } + + return self::SUCCESS; + } +} diff --git a/src/packages/plg_console_mokobackup/src/Command/ListCommand.php b/src/packages/plg_console_mokobackup/src/Command/ListCommand.php new file mode 100644 index 0000000..8fcdddd --- /dev/null +++ b/src/packages/plg_console_mokobackup/src/Command/ListCommand.php @@ -0,0 +1,87 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Plugin\Console\MokoBackup\Command; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\Console\Command\AbstractCommand; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class ListCommand extends AbstractCommand +{ + protected static $defaultName = 'mokobackup:list'; + + protected function configure(): void + { + $this->setDescription('List backup records'); + $this->addOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Number of records to show', '20'); + $this->addOption('status', 's', InputOption::VALUE_OPTIONAL, 'Filter by status (complete, fail, running)'); + } + + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $limit = (int) $input->getOption('limit'); + $status = $input->getOption('status'); + + $io->title('MokoJoomBackup — Backup Records'); + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('r.id, r.description, r.status, r.origin, r.backup_type, r.total_size, r.backupstart, r.backupend') + ->select($db->quoteName('p.title', 'profile_title')) + ->from($db->quoteName('#__mokobackup_records', 'r')) + ->join('LEFT', $db->quoteName('#__mokobackup_profiles', 'p') . ' ON p.id = r.profile_id') + ->order($db->quoteName('r.backupstart') . ' DESC'); + + if ($status) { + $query->where($db->quoteName('r.status') . ' = ' . $db->quote($status)); + } + + $db->setQuery($query, 0, $limit); + $records = $db->loadObjectList(); + + if (empty($records)) { + $io->info('No backup records found.'); + + return self::SUCCESS; + } + + $rows = []; + + foreach ($records as $record) { + $size = $record->total_size > 0 + ? round($record->total_size / 1048576, 2) . ' MB' + : '—'; + + $rows[] = [ + $record->id, + $record->profile_title ?: '—', + $record->status, + $record->backup_type, + $size, + $record->origin, + $record->backupstart, + ]; + } + + $io->table( + ['ID', 'Profile', 'Status', 'Type', 'Size', 'Origin', 'Started'], + $rows + ); + + return self::SUCCESS; + } +} diff --git a/src/packages/plg_console_mokobackup/src/Command/ProfilesCommand.php b/src/packages/plg_console_mokobackup/src/Command/ProfilesCommand.php new file mode 100644 index 0000000..4d81616 --- /dev/null +++ b/src/packages/plg_console_mokobackup/src/Command/ProfilesCommand.php @@ -0,0 +1,68 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Plugin\Console\MokoBackup\Command; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\Console\Command\AbstractCommand; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class ProfilesCommand extends AbstractCommand +{ + protected static $defaultName = 'mokobackup:profiles'; + + protected function configure(): void + { + $this->setDescription('List available backup profiles'); + } + + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $io->title('MokoJoomBackup — Backup Profiles'); + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('id, title, backup_type, published, ordering') + ->from($db->quoteName('#__mokobackup_profiles')) + ->order($db->quoteName('ordering') . ' ASC'); + $db->setQuery($query); + $profiles = $db->loadObjectList(); + + if (empty($profiles)) { + $io->info('No backup profiles found.'); + + return self::SUCCESS; + } + + $rows = []; + + foreach ($profiles as $profile) { + $rows[] = [ + $profile->id, + $profile->title, + $profile->backup_type, + $profile->published ? 'Yes' : 'No', + ]; + } + + $io->table( + ['ID', 'Title', 'Type', 'Published'], + $rows + ); + + return self::SUCCESS; + } +} diff --git a/src/packages/plg_console_mokobackup/src/Command/RestoreCommand.php b/src/packages/plg_console_mokobackup/src/Command/RestoreCommand.php new file mode 100644 index 0000000..fb857d4 --- /dev/null +++ b/src/packages/plg_console_mokobackup/src/Command/RestoreCommand.php @@ -0,0 +1,101 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Plugin\Console\MokoBackup\Command; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\Component\MokoBackup\Administrator\Engine\RestoreEngine; +use Joomla\Console\Command\AbstractCommand; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class RestoreCommand extends AbstractCommand +{ + protected static $defaultName = 'mokobackup:restore'; + + protected function configure(): void + { + $this->setDescription('Restore a backup by record ID'); + $this->addArgument('id', InputArgument::REQUIRED, 'Backup record ID to restore'); + } + + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $recordId = (int) $input->getArgument('id'); + + $io->title('MokoJoomBackup — Restore Backup'); + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__mokobackup_records')) + ->where($db->quoteName('id') . ' = ' . $recordId); + $db->setQuery($query); + $record = $db->loadObject(); + + if (!$record) { + $io->error('Backup record not found: ' . $recordId); + + return self::FAILURE; + } + + if ($record->status !== 'complete') { + $io->error('Cannot restore — backup status is: ' . $record->status); + + return self::FAILURE; + } + + if (empty($record->absolute_path) || !is_file($record->absolute_path)) { + $io->error('Backup archive not found: ' . ($record->absolute_path ?: 'no path')); + + return self::FAILURE; + } + + $io->warning('This will overwrite the current site files and/or database.'); + $io->text('Archive: ' . $record->absolute_path); + $io->text('Type: ' . $record->backup_type); + + if (!$io->confirm('Are you sure you want to continue?', false)) { + $io->info('Restore cancelled.'); + + return self::SUCCESS; + } + + $engineFile = JPATH_ADMINISTRATOR . '/components/com_mokobackup/src/Engine/RestoreEngine.php'; + + if (!file_exists($engineFile)) { + $io->error('RestoreEngine not found. Is the component fully installed?'); + + return self::FAILURE; + } + + if (!class_exists(RestoreEngine::class)) { + require_once $engineFile; + } + + $engine = new RestoreEngine(); + $result = $engine->restore($record->absolute_path, $record->backup_type); + + if ($result['success']) { + $io->success($result['message']); + + return self::SUCCESS; + } + + $io->error($result['message']); + + return self::FAILURE; + } +} diff --git a/src/packages/plg_console_mokobackup/src/Command/RunCommand.php b/src/packages/plg_console_mokobackup/src/Command/RunCommand.php new file mode 100644 index 0000000..52185b9 --- /dev/null +++ b/src/packages/plg_console_mokobackup/src/Command/RunCommand.php @@ -0,0 +1,68 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Plugin\Console\MokoBackup\Command; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\Component\MokoBackup\Administrator\Engine\BackupEngine; +use Joomla\Console\Command\AbstractCommand; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +class RunCommand extends AbstractCommand +{ + protected static $defaultName = 'mokobackup:run'; + + protected function configure(): void + { + $this->setDescription('Run a backup using a specified profile'); + $this->addOption('profile', 'p', InputOption::VALUE_REQUIRED, 'Profile ID to use', '1'); + $this->addOption('description', 'd', InputOption::VALUE_OPTIONAL, 'Backup description', ''); + } + + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $profileId = (int) $input->getOption('profile'); + $desc = $input->getOption('description') ?: ''; + + $io->title('MokoJoomBackup — Run Backup'); + $io->text('Profile ID: ' . $profileId); + + $engineFile = JPATH_ADMINISTRATOR . '/components/com_mokobackup/src/Engine/BackupEngine.php'; + + if (!file_exists($engineFile)) { + $io->error('MokoJoomBackup component not installed.'); + + return self::FAILURE; + } + + if (!class_exists(BackupEngine::class)) { + require_once $engineFile; + } + + $engine = new BackupEngine(); + $result = $engine->run($profileId, $desc ?: 'CLI backup', 'cli'); + + if ($result['success']) { + $io->success($result['message']); + + return self::SUCCESS; + } + + $io->error($result['message']); + + return self::FAILURE; + } +} diff --git a/src/packages/plg_console_mokobackup/src/Extension/MokoBackupConsole.php b/src/packages/plg_console_mokobackup/src/Extension/MokoBackupConsole.php new file mode 100644 index 0000000..5c2ee4f --- /dev/null +++ b/src/packages/plg_console_mokobackup/src/Extension/MokoBackupConsole.php @@ -0,0 +1,45 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Plugin\Console\MokoBackup\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Event\Event; +use Joomla\Event\SubscriberInterface; +use Joomla\Plugin\Console\MokoBackup\Command\CleanupCommand; +use Joomla\Plugin\Console\MokoBackup\Command\ListCommand; +use Joomla\Plugin\Console\MokoBackup\Command\ProfilesCommand; +use Joomla\Plugin\Console\MokoBackup\Command\RestoreCommand; +use Joomla\Plugin\Console\MokoBackup\Command\RunCommand; + +final class MokoBackupConsole extends CMSPlugin implements SubscriberInterface +{ + protected $autoloadLanguage = true; + + public static function getSubscribedEvents(): array + { + return [ + \Joomla\Application\Event\ApplicationEvents::BEFORE_EXECUTE => 'registerCommands', + ]; + } + + public function registerCommands(Event $event): void + { + $app = $this->getApplication(); + + $app->addCommand(new RunCommand()); + $app->addCommand(new ListCommand()); + $app->addCommand(new ProfilesCommand()); + $app->addCommand(new RestoreCommand()); + $app->addCommand(new CleanupCommand()); + } +} diff --git a/src/packages/plg_content_mokobackup/language/en-GB/plg_content_mokobackup.ini b/src/packages/plg_content_mokobackup/language/en-GB/plg_content_mokobackup.ini new file mode 100644 index 0000000..5f23262 --- /dev/null +++ b/src/packages/plg_content_mokobackup/language/en-GB/plg_content_mokobackup.ini @@ -0,0 +1,9 @@ +; MokoJoomBackup — Content Plugin language file (en-GB) +PLG_CONTENT_MOKOBACKUP="Content - MokoJoomBackup" +PLG_CONTENT_MOKOBACKUP_DESCRIPTION="Automatically triggers a backup before extension installs or updates." +PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_INSTALL="Backup Before Install" +PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_INSTALL_DESC="Run an automatic backup before a new extension is installed." +PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_UPDATE="Backup Before Update" +PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_UPDATE_DESC="Run an automatic backup before an extension is updated." +PLG_CONTENT_MOKOBACKUP_FIELD_PROFILE="Backup Profile" +PLG_CONTENT_MOKOBACKUP_FIELD_PROFILE_DESC="Which backup profile to use for automatic backups." diff --git a/src/packages/plg_content_mokobackup/language/en-GB/plg_content_mokobackup.sys.ini b/src/packages/plg_content_mokobackup/language/en-GB/plg_content_mokobackup.sys.ini new file mode 100644 index 0000000..3d79871 --- /dev/null +++ b/src/packages/plg_content_mokobackup/language/en-GB/plg_content_mokobackup.sys.ini @@ -0,0 +1,3 @@ +; MokoJoomBackup — Content Plugin system language file (en-GB) +PLG_CONTENT_MOKOBACKUP="Content - MokoJoomBackup" +PLG_CONTENT_MOKOBACKUP_DESCRIPTION="Automatically triggers a backup before extension installs or updates." diff --git a/src/packages/plg_content_mokobackup/language/en-US/plg_content_mokobackup.ini b/src/packages/plg_content_mokobackup/language/en-US/plg_content_mokobackup.ini new file mode 100644 index 0000000..1bac9a8 --- /dev/null +++ b/src/packages/plg_content_mokobackup/language/en-US/plg_content_mokobackup.ini @@ -0,0 +1,9 @@ +; MokoJoomBackup — Content Plugin language file (en-US) +PLG_CONTENT_MOKOBACKUP="Content - MokoJoomBackup" +PLG_CONTENT_MOKOBACKUP_DESCRIPTION="Automatically triggers a backup before extension installs or updates." +PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_INSTALL="Backup Before Install" +PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_INSTALL_DESC="Run an automatic backup before a new extension is installed." +PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_UPDATE="Backup Before Update" +PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_UPDATE_DESC="Run an automatic backup before an extension is updated." +PLG_CONTENT_MOKOBACKUP_FIELD_PROFILE="Backup Profile" +PLG_CONTENT_MOKOBACKUP_FIELD_PROFILE_DESC="Which backup profile to use for automatic backups." diff --git a/src/packages/plg_content_mokobackup/language/en-US/plg_content_mokobackup.sys.ini b/src/packages/plg_content_mokobackup/language/en-US/plg_content_mokobackup.sys.ini new file mode 100644 index 0000000..7a612b3 --- /dev/null +++ b/src/packages/plg_content_mokobackup/language/en-US/plg_content_mokobackup.sys.ini @@ -0,0 +1,3 @@ +; MokoJoomBackup — Content Plugin system language file (en-US) +PLG_CONTENT_MOKOBACKUP="Content - MokoJoomBackup" +PLG_CONTENT_MOKOBACKUP_DESCRIPTION="Automatically triggers a backup before extension installs or updates." diff --git a/src/packages/plg_content_mokobackup/mokobackup.php b/src/packages/plg_content_mokobackup/mokobackup.php new file mode 100644 index 0000000..2dd15e4 --- /dev/null +++ b/src/packages/plg_content_mokobackup/mokobackup.php @@ -0,0 +1,11 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +defined('_JEXEC') or die; diff --git a/src/packages/plg_content_mokobackup/mokobackup.xml b/src/packages/plg_content_mokobackup/mokobackup.xml new file mode 100644 index 0000000..a337b10 --- /dev/null +++ b/src/packages/plg_content_mokobackup/mokobackup.xml @@ -0,0 +1,71 @@ + + + + plg_content_mokobackup + 01.01.04-dev + 2026-06-04 + Moko Consulting + hello@mokoconsulting.tech + https://mokoconsulting.tech + Copyright (C) 2026 Moko Consulting. All rights reserved. + GPL-3.0-or-later + PLG_CONTENT_MOKOBACKUP_DESCRIPTION + + Joomla\Plugin\Content\MokoBackup + + + mokobackup.php + services + src + + + + language/en-GB/plg_content_mokobackup.ini + language/en-GB/plg_content_mokobackup.sys.ini + + + + +
+ + + + + + + + + + + +
+
+
+
diff --git a/src/packages/plg_content_mokobackup/services/provider.php b/src/packages/plg_content_mokobackup/services/provider.php new file mode 100644 index 0000000..4635162 --- /dev/null +++ b/src/packages/plg_content_mokobackup/services/provider.php @@ -0,0 +1,37 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Extension\PluginInterface; +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Event\DispatcherInterface; +use Joomla\Plugin\Content\MokoBackup\Extension\MokoBackupContent; + +return new class () implements ServiceProviderInterface { + public function register(Container $container): void + { + $container->set( + PluginInterface::class, + function (Container $container) { + $plugin = new MokoBackupContent( + $container->get(DispatcherInterface::class), + (array) PluginHelper::getPlugin('content', 'mokobackup') + ); + $plugin->setApplication(Factory::getApplication()); + + return $plugin; + } + ); + } +}; diff --git a/src/packages/plg_content_mokobackup/src/Extension/MokoBackupContent.php b/src/packages/plg_content_mokobackup/src/Extension/MokoBackupContent.php new file mode 100644 index 0000000..b27d119 --- /dev/null +++ b/src/packages/plg_content_mokobackup/src/Extension/MokoBackupContent.php @@ -0,0 +1,95 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Plugin\Content\MokoBackup\Extension; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\Component\MokoBackup\Administrator\Engine\BackupEngine; +use Joomla\Event\Event; +use Joomla\Event\SubscriberInterface; + +final class MokoBackupContent extends CMSPlugin implements SubscriberInterface +{ + protected $autoloadLanguage = true; + + public static function getSubscribedEvents(): array + { + return [ + 'onExtensionBeforeInstall' => 'onExtensionBeforeInstall', + 'onExtensionBeforeUpdate' => 'onExtensionBeforeUpdate', + ]; + } + + /** + * Trigger a backup before a new extension is installed. + */ + public function onExtensionBeforeInstall(Event $event): void + { + if (!(int) $this->params->get('backup_before_install', 0)) { + return; + } + + $this->triggerAutoBackup('Pre-install backup'); + } + + /** + * Trigger a backup before an extension is updated. + */ + public function onExtensionBeforeUpdate(Event $event): void + { + if (!(int) $this->params->get('backup_before_update', 1)) { + return; + } + + $this->triggerAutoBackup('Pre-update backup'); + } + + /** + * Run a backup using the configured profile. + */ + private function triggerAutoBackup(string $description): void + { + $profileId = (int) $this->params->get('profile_id', 1); + + // Throttle: only one auto-backup per hour via session + $session = Factory::getSession(); + $lastRun = $session->get('mokobackup.content_last_autobackup', 0); + + if (time() - $lastRun < 3600) { + return; + } + + $session->set('mokobackup.content_last_autobackup', time()); + + $engineFile = JPATH_ADMINISTRATOR . '/components/com_mokobackup/src/Engine/BackupEngine.php'; + + if (!file_exists($engineFile)) { + return; + } + + if (!class_exists(BackupEngine::class)) { + require_once $engineFile; + } + + try { + $engine = new BackupEngine(); + $engine->run($profileId, $description, 'backend'); + } catch (\Throwable $e) { + // Non-fatal — log and continue with the install/update + Factory::getApplication()->enqueueMessage( + 'MokoJoomBackup auto-backup failed: ' . $e->getMessage(), + 'warning' + ); + } + } +} diff --git a/src/pkg_mokobackup.xml b/src/pkg_mokobackup.xml index d02c244..b370006 100644 --- a/src/pkg_mokobackup.xml +++ b/src/pkg_mokobackup.xml @@ -25,6 +25,9 @@ plg_task_mokobackup.zip plg_quickicon_mokobackup.zip plg_webservices_mokobackup.zip + plg_console_mokobackup.zip + plg_content_mokobackup.zip + plg_actionlog_mokobackup.zip
diff --git a/src/script.php b/src/script.php index ea6ed40..409a88c 100644 --- a/src/script.php +++ b/src/script.php @@ -108,6 +108,39 @@ class Pkg_MokoBackupInstallerScript $db->setQuery($query); $db->execute(); + // Enable the console plugin automatically + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('console')) + ->where($db->quoteName('element') . ' = ' . $db->quote('mokobackup')); + + $db->setQuery($query); + $db->execute(); + + // Enable the content plugin automatically + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('content')) + ->where($db->quoteName('element') . ' = ' . $db->quote('mokobackup')); + + $db->setQuery($query); + $db->execute(); + + // Enable the actionlog plugin automatically + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('actionlog')) + ->where($db->quoteName('element') . ' = ' . $db->quote('mokobackup')); + + $db->setQuery($query); + $db->execute(); + // Create default backup directory $backupDir = JPATH_ADMINISTRATOR . '/components/com_mokobackup/backups'; -- 2.52.0 From 5ab6296ad5ec4ae43394a070557d1c4755812f16 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Thu, 4 Jun 2026 16:29:55 +0000 Subject: [PATCH 30/42] chore(version): auto-bump 01.01.05-dev [skip ci] --- .mokogitea/manifest.xml | 2 +- .mokogitea/workflows/issue-branch.yml | 2 +- .mokogitea/workflows/pr-check.yml | 1016 ++++++++--------- README.md | 2 +- src/packages/com_mokobackup/mokobackup.xml | 2 +- .../plg_actionlog_mokobackup/mokobackup.xml | 2 +- .../plg_console_mokobackup/mokobackup.xml | 2 +- .../plg_content_mokobackup/mokobackup.xml | 2 +- .../plg_quickicon_mokobackup/mokobackup.xml | 2 +- .../plg_system_mokobackup/mokobackup.xml | 2 +- .../plg_task_mokobackup/mokobackup.xml | 2 +- .../plg_webservices_mokobackup/mokobackup.xml | 2 +- src/pkg_mokobackup.xml | 2 +- 13 files changed, 520 insertions(+), 520 deletions(-) diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 3e76d1d..512869a 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -5,7 +5,7 @@ Package - MokoJoomBackup MokoConsulting Full-site backup and restore for Joomla — database, files, and configuration - 01.01.04-dev + 01.01.05-dev GNU General Public License v3 diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 413ef84..09a1a30 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: 01.01.04 +# VERSION: 01.01.05 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml index 4d78d7a..6625857 100644 --- a/.mokogitea/workflows/pr-check.yml +++ b/.mokogitea/workflows/pr-check.yml @@ -1,508 +1,508 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.CI -# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform -# PATH: /templates/workflows/universal/pr-check.yml.template -# VERSION: 09.23.00 -# BRIEF: PR gate — branch policy + code validation before merge - -name: "Universal: PR Check" - -on: - pull_request: - types: [opened, synchronize, reopened, edited] - -permissions: - contents: read - pull-requests: write - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - # ── Branch Policy ────────────────────────────────────────────────────── - branch-policy: - name: Branch Policy - runs-on: ubuntu-latest - steps: - - name: Check branch merge target - run: | - HEAD="${{ github.head_ref }}" - BASE="${{ github.base_ref }}" - - echo "PR: ${HEAD} → ${BASE}" - - ALLOWED=true - REASON="" - - case "$HEAD" in - feature/*|feat/*) - if [ "$BASE" != "dev" ]; then - ALLOWED=false - REASON="Feature branches must target 'dev', not '${BASE}'" - fi - ;; - fix/*|bugfix/*) - if [ "$BASE" != "dev" ]; then - ALLOWED=false - REASON="Fix branches must target 'dev', not '${BASE}'" - fi - ;; - patch/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then - ALLOWED=false - REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" - fi - ;; - hotfix/*) - if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" - fi - ;; - rc) - if [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="RC branch can only merge into 'main', not '${BASE}'" - fi - ;; - dev) - if [ "$BASE" != "main" ]; then - ALLOWED=false - REASON="Dev branch can only merge into 'main', not '${BASE}'" - fi - ;; - esac - - if [ "$ALLOWED" = false ]; then - echo "::error::${REASON}" - echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "${REASON}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY - echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY - echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY - echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY - echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - echo "Branch policy: OK (${HEAD} → ${BASE})" - echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY - - # ── Code Validation ──────────────────────────────────────────────────── - validate: - name: Validate PR - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Check for merge conflict markers - run: | - CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) - if [ -n "$CONFLICTS" ]; then - echo "::error::Merge conflict markers found in source files" - echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "No conflict markers found" - - - name: Detect platform - id: platform - run: | - # Read platform from XML manifest ( tag) or plain text fallback - PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1) - [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]') - [ -z "$PLATFORM" ] && PLATFORM="generic" - echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" - - - name: Setup PHP - if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' - run: | - if ! command -v php &> /dev/null; then - sudo apt-get update -qq - sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 - fi - - - name: PHP syntax check - if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' - run: | - ERRORS=0 - while IFS= read -r -d '' file; do - if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then - ERRORS=$((ERRORS + 1)) - fi - done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0) - echo "PHP lint: ${ERRORS} error(s)" - [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } - - - name: Joomla JEXEC guard check - if: steps.platform.outputs.platform == 'joomla' - run: | - ERRORS=0 - while IFS= read -r -d '' file; do - # Skip vendor, node_modules, and index.html stub files - case "$file" in ./vendor/*|./node_modules/*) continue ;; esac - # Check first 10 lines for JEXEC or JPATH guard - if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then - echo "::error file=${file}::Missing JEXEC guard: ${file}" - ERRORS=$((ERRORS + 1)) - fi - done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0) - if [ "$ERRORS" -gt 0 ]; then - echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard" - echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY - echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "JEXEC guard: OK" - - - name: Joomla directory listing protection - if: steps.platform.outputs.platform == 'joomla' - run: | - MISSING=0 - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && exit 0 - while IFS= read -r dir; do - if [ ! -f "${dir}/index.html" ]; then - echo "::warning::Missing index.html in ${dir} (directory listing protection)" - MISSING=$((MISSING + 1)) - fi - done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*") - if [ "$MISSING" -gt 0 ]; then - echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY - echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY - fi - echo "Directory protection: ${MISSING} missing (advisory)" - - - name: Joomla script file and asset checks - if: steps.platform.outputs.platform == 'joomla' - run: | - ERRORS=0 - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) - [ -z "$MANIFEST" ] && exit 0 - MANIFEST_DIR=$(dirname "$MANIFEST") - - # Check scriptfile exists if declared - SCRIPTFILE=$(sed -n 's/.*\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null) - if [ -n "$SCRIPTFILE" ]; then - if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then - echo "::error::Manifest declares ${SCRIPTFILE} but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}" - ERRORS=$((ERRORS + 1)) - else - echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)" - fi - fi - - # Require joomla.asset.json and validate it - ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1) - if [ -z "$ASSET_JSON" ]; then - echo "::error::joomla.asset.json not found — Joomla asset system is required" - ERRORS=$((ERRORS + 1)) - else - if command -v php &> /dev/null; then - php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || { - echo "::error::joomla.asset.json is not valid JSON" - ERRORS=$((ERRORS + 1)) - } - fi - echo "joomla.asset.json: valid" - fi - - # Validate all XML files in src/ are well-formed - XML_ERRORS=0 - if command -v php &> /dev/null; then - while IFS= read -r -d '' xmlfile; do - if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then - XML_ERRORS=$((XML_ERRORS + 1)) - fi - done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0) - fi - if [ "$XML_ERRORS" -gt 0 ]; then - echo "::error::${XML_ERRORS} XML file(s) are malformed" - ERRORS=$((ERRORS + 1)) - else - echo "XML well-formedness: OK" - fi - - [ "$ERRORS" -gt 0 ] && exit 1 - echo "Joomla asset checks: OK" - - - name: Validate platform manifest - run: | - PLATFORM="${{ steps.platform.outputs.platform }}" - case "$PLATFORM" in - joomla) - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) - if [ -z "$MANIFEST" ]; then - echo "::warning::No Joomla manifest found (WaaS site)" - exit 0 - fi - echo "Manifest: ${MANIFEST}" - if command -v php &> /dev/null; then - php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; } - fi - for ELEMENT in name version description; do - grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; } - done - # Block legacy raw/branch update server URLs on MokoGitea - RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true) - if [ -n "$RAW_URLS" ]; then - echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)" - echo "$RAW_URLS" - exit 1 - fi - echo "Joomla manifest valid" - ;; - dolibarr) - MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) - if [ -z "$MOD_FILE" ]; then - echo "::error::No mod*.class.php found" - exit 1 - fi - echo "Dolibarr module: ${MOD_FILE}" - ;; - *) - echo "Generic platform — no manifest validation" - ;; - esac - - - name: Check update stream format - run: | - PLATFORM="${{ steps.platform.outputs.platform }}" - case "$PLATFORM" in - joomla) - if [ -f "updates.xml" ]; then - if command -v php &> /dev/null; then - php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; } - fi - echo "updates.xml valid" - fi - ;; - dolibarr) - [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt" - ;; - esac - - - name: Validate Joomla language files - if: steps.platform.outputs.platform == 'joomla' - run: | - ERRORS=0 - WARNINGS=0 - - # Require both en-GB and en-US language directories - LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1) - if [ -z "$LANG_ROOT" ]; then - echo "No language/ directory found — skipping" - exit 0 - fi - - if [ ! -d "$LANG_ROOT/en-GB" ]; then - echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)" - ERRORS=$((ERRORS + 1)) - fi - if [ ! -d "$LANG_ROOT/en-US" ]; then - echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)" - ERRORS=$((ERRORS + 1)) - fi - - # Check that en-GB and en-US have matching .ini files - if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then - for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do - [ ! -f "$GB_INI" ] && continue - US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")" - if [ ! -f "$US_INI" ]; then - echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US" - ERRORS=$((ERRORS + 1)) - fi - done - for US_INI in "$LANG_ROOT/en-US"/*.ini; do - [ ! -f "$US_INI" ] && continue - GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")" - if [ ! -f "$GB_INI" ]; then - echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB" - ERRORS=$((ERRORS + 1)) - fi - done - fi - - # Find all .ini language files - INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null) - if [ -z "$INI_FILES" ]; then - echo "No .ini language files found" - [ "$ERRORS" -gt 0 ] && exit 1 - exit 0 - fi - - echo "Found $(echo "$INI_FILES" | wc -l) language file(s)" - - for FILE in $INI_FILES; do - FNAME=$(basename "$FILE") - LINENUM=0 - SEEN_KEYS="" - - while IFS= read -r line || [ -n "$line" ]; do - LINENUM=$((LINENUM + 1)) - - # Skip empty lines and comments - [ -z "$line" ] && continue - echo "$line" | grep -qE '^\s*;' && continue - echo "$line" | grep -qE '^\s*$' && continue - - # Must match KEY="VALUE" format - if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then - echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}" - ERRORS=$((ERRORS + 1)) - continue - fi - - # Extract key and check for duplicates - KEY=$(echo "$line" | sed 's/=.*//') - if echo "$SEEN_KEYS" | grep -qx "$KEY"; then - echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}" - ERRORS=$((ERRORS + 1)) - fi - SEEN_KEYS="${SEEN_KEYS} - ${KEY}" - done < "$FILE" - - echo " ${FILE}: checked ${LINENUM} lines" - done - - # Cross-check en-GB vs en-US key consistency - GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1) - US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1) - - if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then - for GB_FILE in "$GB_DIR"/*.ini; do - [ ! -f "$GB_FILE" ] && continue - FNAME=$(basename "$GB_FILE") - US_FILE="$US_DIR/$FNAME" - [ ! -f "$US_FILE" ] && continue - - GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort) - US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort) - - # Keys in en-GB but not en-US - MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS")) - if [ -n "$MISSING_US" ]; then - echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:" - echo "$MISSING_US" | while read -r k; do echo " - $k"; done - WARNINGS=$((WARNINGS + 1)) - fi - - # Keys in en-US but not en-GB - MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS")) - if [ -n "$MISSING_GB" ]; then - echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:" - echo "$MISSING_GB" | while read -r k; do echo " - $k"; done - WARNINGS=$((WARNINGS + 1)) - fi - done - fi - - { - echo "### Language File Validation" - echo "| Metric | Count |" - echo "|---|---|" - echo "| Files checked | $(echo "$INI_FILES" | wc -l) |" - echo "| Errors | ${ERRORS} |" - echo "| Warnings | ${WARNINGS} |" - } >> $GITHUB_STEP_SUMMARY - - if [ "$ERRORS" -gt 0 ]; then - echo "::error::Language validation failed with ${ERRORS} error(s)" - exit 1 - fi - echo "Language files: OK (${WARNINGS} warning(s))" - - - name: Check changelog has unreleased entry - run: | - if [ ! -f "CHANGELOG.md" ]; then - echo "::warning::No CHANGELOG.md found" - exit 0 - fi - # Check for content under [Unreleased] section - if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then - echo "::error::CHANGELOG.md missing [Unreleased] section" - exit 1 - fi - # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased - UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true) - if [ "$UNRELEASED_CONTENT" -eq 0 ]; then - echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes." - echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY - echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY - echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]" - - - name: Verify package source - run: | - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - if [ ! -d "$SOURCE_DIR" ]; then - echo "::warning::No src/ or htdocs/ directory" - exit 0 - fi - FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) - echo "Source: ${FILE_COUNT} files" - [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } - - # ── Pre-Release RC Build ───────────────────────────────────────────────── - pre-release: - name: Build RC Package - runs-on: ubuntu-latest - needs: [branch-policy, validate] - - steps: - - name: Trigger RC pre-release - env: - GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - REPO: ${{ github.repository }} - BRANCH: ${{ github.head_ref }} - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - run: | - curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" - echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY - echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY - - # ── Issue Reporter ────────────────────────────────────────────────────── - report-issues: - name: Report Issues - runs-on: ubuntu-latest - needs: [branch-policy, validate] - if: >- - always() && - needs.validate.result == 'failure' - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - sparse-checkout: automation/ci-issue-reporter.sh - sparse-checkout-cone-mode: false - - - name: "File issue for PR validation failure" - env: - GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - run: | - chmod +x automation/ci-issue-reporter.sh - ./automation/ci-issue-reporter.sh \ - --gate "PR Validation" \ - --workflow "PR Check" \ - --severity error \ - --details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed." +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: moko-platform.CI +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform +# PATH: /templates/workflows/universal/pr-check.yml.template +# VERSION: 09.23.00 +# BRIEF: PR gate — branch policy + code validation before merge + +name: "Universal: PR Check" + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +permissions: + contents: read + pull-requests: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + # ── Branch Policy ────────────────────────────────────────────────────── + branch-policy: + name: Branch Policy + runs-on: ubuntu-latest + steps: + - name: Check branch merge target + run: | + HEAD="${{ github.head_ref }}" + BASE="${{ github.base_ref }}" + + echo "PR: ${HEAD} → ${BASE}" + + ALLOWED=true + REASON="" + + case "$HEAD" in + feature/*|feat/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Feature branches must target 'dev', not '${BASE}'" + fi + ;; + fix/*|bugfix/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Fix branches must target 'dev', not '${BASE}'" + fi + ;; + patch/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then + ALLOWED=false + REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" + fi + ;; + hotfix/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" + fi + ;; + rc) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="RC branch can only merge into 'main', not '${BASE}'" + fi + ;; + dev) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Dev branch can only merge into 'main', not '${BASE}'" + fi + ;; + esac + + if [ "$ALLOWED" = false ]; then + echo "::error::${REASON}" + echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "${REASON}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY + echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "Branch policy: OK (${HEAD} → ${BASE})" + echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY + + # ── Code Validation ──────────────────────────────────────────────────── + validate: + name: Validate PR + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check for merge conflict markers + run: | + CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true) + if [ -n "$CONFLICTS" ]; then + echo "::error::Merge conflict markers found in source files" + echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "No conflict markers found" + + - name: Detect platform + id: platform + run: | + # Read platform from XML manifest ( tag) or plain text fallback + PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1) + [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]') + [ -z "$PLATFORM" ] && PLATFORM="generic" + echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" + + - name: Setup PHP + if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 + fi + + - name: PHP syntax check + if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' + run: | + ERRORS=0 + while IFS= read -r -d '' file; do + if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then + ERRORS=$((ERRORS + 1)) + fi + done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0) + echo "PHP lint: ${ERRORS} error(s)" + [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } + + - name: Joomla JEXEC guard check + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + while IFS= read -r -d '' file; do + # Skip vendor, node_modules, and index.html stub files + case "$file" in ./vendor/*|./node_modules/*) continue ;; esac + # Check first 10 lines for JEXEC or JPATH guard + if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then + echo "::error file=${file}::Missing JEXEC guard: ${file}" + ERRORS=$((ERRORS + 1)) + fi + done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0) + if [ "$ERRORS" -gt 0 ]; then + echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard" + echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY + echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "JEXEC guard: OK" + + - name: Joomla directory listing protection + if: steps.platform.outputs.platform == 'joomla' + run: | + MISSING=0 + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && exit 0 + while IFS= read -r dir; do + if [ ! -f "${dir}/index.html" ]; then + echo "::warning::Missing index.html in ${dir} (directory listing protection)" + MISSING=$((MISSING + 1)) + fi + done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*") + if [ "$MISSING" -gt 0 ]; then + echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY + echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY + fi + echo "Directory protection: ${MISSING} missing (advisory)" + + - name: Joomla script file and asset checks + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + [ -z "$MANIFEST" ] && exit 0 + MANIFEST_DIR=$(dirname "$MANIFEST") + + # Check scriptfile exists if declared + SCRIPTFILE=$(sed -n 's/.*\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null) + if [ -n "$SCRIPTFILE" ]; then + if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then + echo "::error::Manifest declares ${SCRIPTFILE} but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}" + ERRORS=$((ERRORS + 1)) + else + echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)" + fi + fi + + # Require joomla.asset.json and validate it + ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1) + if [ -z "$ASSET_JSON" ]; then + echo "::error::joomla.asset.json not found — Joomla asset system is required" + ERRORS=$((ERRORS + 1)) + else + if command -v php &> /dev/null; then + php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || { + echo "::error::joomla.asset.json is not valid JSON" + ERRORS=$((ERRORS + 1)) + } + fi + echo "joomla.asset.json: valid" + fi + + # Validate all XML files in src/ are well-formed + XML_ERRORS=0 + if command -v php &> /dev/null; then + while IFS= read -r -d '' xmlfile; do + if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then + XML_ERRORS=$((XML_ERRORS + 1)) + fi + done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0) + fi + if [ "$XML_ERRORS" -gt 0 ]; then + echo "::error::${XML_ERRORS} XML file(s) are malformed" + ERRORS=$((ERRORS + 1)) + else + echo "XML well-formedness: OK" + fi + + [ "$ERRORS" -gt 0 ] && exit 1 + echo "Joomla asset checks: OK" + + - name: Validate platform manifest + run: | + PLATFORM="${{ steps.platform.outputs.platform }}" + case "$PLATFORM" in + joomla) + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "::warning::No Joomla manifest found (WaaS site)" + exit 0 + fi + echo "Manifest: ${MANIFEST}" + if command -v php &> /dev/null; then + php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; } + fi + for ELEMENT in name version description; do + grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; } + done + # Block legacy raw/branch update server URLs on MokoGitea + RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true) + if [ -n "$RAW_URLS" ]; then + echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)" + echo "$RAW_URLS" + exit 1 + fi + echo "Joomla manifest valid" + ;; + dolibarr) + MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) + if [ -z "$MOD_FILE" ]; then + echo "::error::No mod*.class.php found" + exit 1 + fi + echo "Dolibarr module: ${MOD_FILE}" + ;; + *) + echo "Generic platform — no manifest validation" + ;; + esac + + - name: Check update stream format + run: | + PLATFORM="${{ steps.platform.outputs.platform }}" + case "$PLATFORM" in + joomla) + if [ -f "updates.xml" ]; then + if command -v php &> /dev/null; then + php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; } + fi + echo "updates.xml valid" + fi + ;; + dolibarr) + [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt" + ;; + esac + + - name: Validate Joomla language files + if: steps.platform.outputs.platform == 'joomla' + run: | + ERRORS=0 + WARNINGS=0 + + # Require both en-GB and en-US language directories + LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1) + if [ -z "$LANG_ROOT" ]; then + echo "No language/ directory found — skipping" + exit 0 + fi + + if [ ! -d "$LANG_ROOT/en-GB" ]; then + echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)" + ERRORS=$((ERRORS + 1)) + fi + if [ ! -d "$LANG_ROOT/en-US" ]; then + echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)" + ERRORS=$((ERRORS + 1)) + fi + + # Check that en-GB and en-US have matching .ini files + if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then + for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do + [ ! -f "$GB_INI" ] && continue + US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")" + if [ ! -f "$US_INI" ]; then + echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US" + ERRORS=$((ERRORS + 1)) + fi + done + for US_INI in "$LANG_ROOT/en-US"/*.ini; do + [ ! -f "$US_INI" ] && continue + GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")" + if [ ! -f "$GB_INI" ]; then + echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB" + ERRORS=$((ERRORS + 1)) + fi + done + fi + + # Find all .ini language files + INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null) + if [ -z "$INI_FILES" ]; then + echo "No .ini language files found" + [ "$ERRORS" -gt 0 ] && exit 1 + exit 0 + fi + + echo "Found $(echo "$INI_FILES" | wc -l) language file(s)" + + for FILE in $INI_FILES; do + FNAME=$(basename "$FILE") + LINENUM=0 + SEEN_KEYS="" + + while IFS= read -r line || [ -n "$line" ]; do + LINENUM=$((LINENUM + 1)) + + # Skip empty lines and comments + [ -z "$line" ] && continue + echo "$line" | grep -qE '^\s*;' && continue + echo "$line" | grep -qE '^\s*$' && continue + + # Must match KEY="VALUE" format + if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then + echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}" + ERRORS=$((ERRORS + 1)) + continue + fi + + # Extract key and check for duplicates + KEY=$(echo "$line" | sed 's/=.*//') + if echo "$SEEN_KEYS" | grep -qx "$KEY"; then + echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}" + ERRORS=$((ERRORS + 1)) + fi + SEEN_KEYS="${SEEN_KEYS} + ${KEY}" + done < "$FILE" + + echo " ${FILE}: checked ${LINENUM} lines" + done + + # Cross-check en-GB vs en-US key consistency + GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1) + US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1) + + if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then + for GB_FILE in "$GB_DIR"/*.ini; do + [ ! -f "$GB_FILE" ] && continue + FNAME=$(basename "$GB_FILE") + US_FILE="$US_DIR/$FNAME" + [ ! -f "$US_FILE" ] && continue + + GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort) + US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort) + + # Keys in en-GB but not en-US + MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS")) + if [ -n "$MISSING_US" ]; then + echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:" + echo "$MISSING_US" | while read -r k; do echo " - $k"; done + WARNINGS=$((WARNINGS + 1)) + fi + + # Keys in en-US but not en-GB + MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS")) + if [ -n "$MISSING_GB" ]; then + echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:" + echo "$MISSING_GB" | while read -r k; do echo " - $k"; done + WARNINGS=$((WARNINGS + 1)) + fi + done + fi + + { + echo "### Language File Validation" + echo "| Metric | Count |" + echo "|---|---|" + echo "| Files checked | $(echo "$INI_FILES" | wc -l) |" + echo "| Errors | ${ERRORS} |" + echo "| Warnings | ${WARNINGS} |" + } >> $GITHUB_STEP_SUMMARY + + if [ "$ERRORS" -gt 0 ]; then + echo "::error::Language validation failed with ${ERRORS} error(s)" + exit 1 + fi + echo "Language files: OK (${WARNINGS} warning(s))" + + - name: Check changelog has unreleased entry + run: | + if [ ! -f "CHANGELOG.md" ]; then + echo "::warning::No CHANGELOG.md found" + exit 0 + fi + # Check for content under [Unreleased] section + if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then + echo "::error::CHANGELOG.md missing [Unreleased] section" + exit 1 + fi + # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased + UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true) + if [ "$UNRELEASED_CONTENT" -eq 0 ]; then + echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes." + echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY + echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY + echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]" + + - name: Verify package source + run: | + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ ! -d "$SOURCE_DIR" ]; then + echo "::warning::No src/ or htdocs/ directory" + exit 0 + fi + FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) + echo "Source: ${FILE_COUNT} files" + [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } + + # ── Pre-Release RC Build ───────────────────────────────────────────────── + pre-release: + name: Build RC Package + runs-on: ubuntu-latest + needs: [branch-policy, validate] + + steps: + - name: Trigger RC pre-release + env: + GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + REPO: ${{ github.repository }} + BRANCH: ${{ github.head_ref }} + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" + echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY + echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY + + # ── Issue Reporter ────────────────────────────────────────────────────── + report-issues: + name: Report Issues + runs-on: ubuntu-latest + needs: [branch-policy, validate] + if: >- + always() && + needs.validate.result == 'failure' + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + sparse-checkout: automation/ci-issue-reporter.sh + sparse-checkout-cone-mode: false + + - name: "File issue for PR validation failure" + env: + GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + run: | + chmod +x automation/ci-issue-reporter.sh + ./automation/ci-issue-reporter.sh \ + --gate "PR Validation" \ + --workflow "PR Check" \ + --severity error \ + --details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed." diff --git a/README.md b/README.md index ca6b7b9..741420b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoJoomBackup - + Full-site backup and restore for Joomla — database, files, and configuration. diff --git a/src/packages/com_mokobackup/mokobackup.xml b/src/packages/com_mokobackup/mokobackup.xml index f715793..08a2744 100644 --- a/src/packages/com_mokobackup/mokobackup.xml +++ b/src/packages/com_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> com_mokobackup - 01.01.04-dev + 01.01.05-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_actionlog_mokobackup/mokobackup.xml b/src/packages/plg_actionlog_mokobackup/mokobackup.xml index b5fe605..43d280b 100644 --- a/src/packages/plg_actionlog_mokobackup/mokobackup.xml +++ b/src/packages/plg_actionlog_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_actionlog_mokobackup - 01.01.04-dev + 01.01.05-dev 2026-06-04 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_console_mokobackup/mokobackup.xml b/src/packages/plg_console_mokobackup/mokobackup.xml index c641770..9992f42 100644 --- a/src/packages/plg_console_mokobackup/mokobackup.xml +++ b/src/packages/plg_console_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_console_mokobackup - 01.01.04-dev + 01.01.05-dev 2026-06-04 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_content_mokobackup/mokobackup.xml b/src/packages/plg_content_mokobackup/mokobackup.xml index a337b10..b0610f3 100644 --- a/src/packages/plg_content_mokobackup/mokobackup.xml +++ b/src/packages/plg_content_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_content_mokobackup - 01.01.04-dev + 01.01.05-dev 2026-06-04 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_quickicon_mokobackup/mokobackup.xml b/src/packages/plg_quickicon_mokobackup/mokobackup.xml index 2856a6b..5a666ac 100644 --- a/src/packages/plg_quickicon_mokobackup/mokobackup.xml +++ b/src/packages/plg_quickicon_mokobackup/mokobackup.xml @@ -1,7 +1,7 @@ plg_quickicon_mokobackup - 01.01.04-dev + 01.01.05-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_system_mokobackup/mokobackup.xml b/src/packages/plg_system_mokobackup/mokobackup.xml index 1746c42..64ab1cc 100644 --- a/src/packages/plg_system_mokobackup/mokobackup.xml +++ b/src/packages/plg_system_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_system_mokobackup - 01.01.04-dev + 01.01.05-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_task_mokobackup/mokobackup.xml b/src/packages/plg_task_mokobackup/mokobackup.xml index 1b72d8d..bf9d222 100644 --- a/src/packages/plg_task_mokobackup/mokobackup.xml +++ b/src/packages/plg_task_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_task_mokobackup - 01.01.04-dev + 01.01.05-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_webservices_mokobackup/mokobackup.xml b/src/packages/plg_webservices_mokobackup/mokobackup.xml index f7ebd45..28b70fe 100644 --- a/src/packages/plg_webservices_mokobackup/mokobackup.xml +++ b/src/packages/plg_webservices_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_webservices_mokobackup - 01.01.04-dev + 01.01.05-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/pkg_mokobackup.xml b/src/pkg_mokobackup.xml index b370006..af4eb8b 100644 --- a/src/pkg_mokobackup.xml +++ b/src/pkg_mokobackup.xml @@ -8,7 +8,7 @@ Package - MokoJoomBackup mokobackup - 01.01.04-dev + 01.01.05-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech -- 2.52.0 From 5460c7b211e13e03a5a4d67024b189dfab4b513a Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 12:50:46 -0500 Subject: [PATCH 31/42] chore: consolidate changelog entries to minor versions only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapsed all patch-level entries (01.00.00, 01.01.01–01.01.04) into two minor version sections: 01.00 and 01.01. Deduplicated and grouped entries by Added/Changed/Fixed within each. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 65 +++++++++++++++++++++++----------------------------- 1 file changed, 29 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f228f01..009ae6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,62 +2,55 @@ ## [Unreleased] -## 01.00.00 — 2026-06-02 +## 01.01 — 2026-06-04 + +### Added +- Admin dashboard view as default landing page with status cards, quick actions, and system health checklist (#28) +- Console plugin (plg_console_mokobackup) — CLI commands: run, list, profiles, restore, cleanup (#29) +- Content plugin (plg_content_mokobackup) — auto-backup before extension install/update (#30) +- Actionlog plugin (plg_actionlog_mokobackup) — logs backup and profile actions to User Action Logs (#31) +- BackupEngine dispatches onMokoBackupAfterRun event for plugin listeners +- Update site notice on dashboard and post-install + +### Changed +- Renamed Kickstart to MokoRestore throughout + +### Fixed +- SQL update migration and error handling +- Removed orphaned scriptfile from component manifest +- Consolidated admin files into single files block + +## 01.00 — 2026-06-02 ### Added - Initial package structure with component, system plugin, task plugin, and webservices plugin - Joomla Scheduled Tasks integration (plg_task_mokobackup) — create multiple tasks, each running a different backup profile on its own schedule - Individual form fields for all profile settings (no raw JSON) - FTP/FTPS uploader with recursive directory creation, passive mode, SSL, and size verification -- Google Drive uploader using OAuth2 refresh tokens and resumable upload API (5 MB chunks) +- Google Drive uploader using OAuth2 refresh tokens and resumable upload API +- S3-compatible remote storage: AWS S3, Wasabi, Backblaze B2, MinIO (#16) - RemoteUploaderInterface for pluggable storage backends -- Remote upload integrated into BackupEngine as Step 3 after archive creation -- Option to delete local copy after successful remote upload (per-profile setting) +- Remote upload integrated into BackupEngine with option to delete local copy after upload - Restore engine with file restoration and database import -- MokoRestore standalone restore script (restore.php) — self-contained site restoration without Joomla, -- "Include Restore Script" toggle per profile — wraps backup with restore.php + site-backup.zip -- FileRestorer class with protected file handling (preserves configuration.php, .htaccess) +- MokoRestore standalone restore script — self-contained site restoration without Joomla +- "Include Restore Script" toggle per profile +- FileRestorer with protected file handling (preserves configuration.php, .htaccess) - DatabaseImporter with streaming line-by-line SQL execution and error tolerance - Admin dashboard quickicon widget — backup status at a glance with warnings (#18) - Differential backups — only back up files changed since last full backup (#19) -- DifferentialScanner: builds file manifests (path/size/mtime) and compares against base -- File manifest stored in backup record for future differential comparisons -- Automatic full-backup fallback when no base manifest exists +- DifferentialScanner with file manifests stored in backup records - JPA archive format import for Akeeba Backup migration (#20) -- JpaUnarchiver: parses Akeeba JPA binary format (headers, gzip, permissions) -- RestoreEngine auto-detects JPA vs ZIP format - AES-256 archive encryption with per-profile password (#17) -- Encrypted archive support in RestoreEngine (password parameter) -- Encrypted archive support in MokoRestore restore.php (password field in UI) -- SHA-256 checksum computed and stored after archive creation (#15) -- "Verify Integrity" toolbar button re-computes hash and compares against stored checksum -- S3-compatible remote storage: AWS S3, Wasabi, Backblaze B2, MinIO (#16) -- S3 uploader with AWS Signature V4, single PUT for files <= 100 MB, multipart for larger -- S3 fields in profile form with showon conditional visibility -- Akeeba importer now maps S3 credentials from Akeeba profiles +- SHA-256 checksum verification for backup integrity (#15) - Email notifications on backup success/failure via Joomla mailer (#14) -- Per-profile notification settings: recipient emails, notify on success/failure -- Failure emails include last 30 lines of backup log for debugging -- mcp_mokobackup MCP server updated with MokoBackupClient for dual-backend support (#21) -- Akeeba Backup Pro importer — imports profiles, filters, remote storage settings, and backup history +- Akeeba Backup Pro importer — profiles, filters, remote storage, and backup history - Auto-disables Akeeba plugins and scheduled tasks after successful import -- "Import from Akeeba" toolbar button in Profiles view (only shown when Akeeba tables detected) -- Supports both INI-format and JSON-format Akeeba configuration parsing -- Maps Akeeba filter format (per-root, nested) to newline-separated exclusion fields -- Profile selector dropdown in Backup Records view for choosing which profile to run - AJAX step-based backup engine for shared hosting (overcomes max_execution_time) -- SteppedBackupEngine: breaks backup into per-table DB dumps and file batches -- SteppedSession: persistent state between AJAX requests via temp JSON files - Progress bar modal in admin UI with real-time phase/percentage updates -- AjaxController for init/step endpoints with CSRF protection - Per-profile archive settings: format, compression level, split size, backup directory -- Backup engine with step-based execution for large sites -- Database dumper with table-level granularity -- File scanner with directory exclusion filters -- ZIP archive builder +- Backup engine with database dumper, file scanner, and ZIP archive builder - Backup profiles with independent configurations - Backup record management (list, download, delete) -- Admin dashboard with backup history - CLI script for cron/scheduled backups - REST API compatible with MokoBackup MCP server - System plugin for automatic backup cleanup with configurable retention -- 2.52.0 From c5d4445bc15f3309d2412a41116e26f1a5065197 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 13:40:29 -0500 Subject: [PATCH 32/42] fix: rewrite Makefile for CI-based releases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove local build/package targets — all packaging is handled by the pre-release and auto-release CI workflows. The release target now dispatches the pre-release workflow via Gitea API. Added release-rc target and validate-xml check. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile | 214 +++++++++++++++++++++++-------------------------------- 1 file changed, 88 insertions(+), 126 deletions(-) diff --git a/Makefile b/Makefile index 50e3eae..df8aa3e 100644 --- a/Makefile +++ b/Makefile @@ -3,43 +3,29 @@ # SPDX-License-Identifier: GPL-3.0-or-later # # MokoJoomBackup — Full-site backup and restore for Joomla +# +# Builds and releases are handled by CI workflows (pre-release.yml, +# auto-release.yml). This Makefile provides local validation helpers +# and workflow dispatch shortcuts. # ============================================================================== -# CONFIGURATION - Customize these for your extension +# CONFIGURATION # ============================================================================== -# Extension Configuration EXTENSION_NAME := mokobackup EXTENSION_TYPE := package -# Options: module, plugin, component, package, template -EXTENSION_VERSION := 1.0.0 -# Module Configuration (for modules only) -MODULE_TYPE := site -# Options: site, admin - -# Plugin Configuration (for plugins only) -PLUGIN_GROUP := system -# Options: system, content, user, authentication, etc. - -# Directories SRC_DIR := src -BUILD_DIR := build -DIST_DIR := dist -DOCS_DIR := docs -# Joomla Installation (for local testing - customize paths) -JOOMLA_ROOT := /var/www/html/joomla -JOOMLA_VERSION := 4 +# Gitea +GITEA_URL := https://git.mokoconsulting.tech +GITEA_ORG := MokoConsulting +GITEA_REPO := MokoJoomBackup # Tools PHP := php COMPOSER := composer -NPM := npm PHPCS := vendor/bin/phpcs -PHPCBF := vendor/bin/phpcbf -PHPUNIT := vendor/bin/phpunit -ZIP := zip # Coding Standards PHPCS_STANDARD := Joomla @@ -58,146 +44,122 @@ COLOR_RED := \033[31m .PHONY: help help: ## Show this help message @echo "$(COLOR_BLUE)╔════════════════════════════════════════════════════════════╗$(COLOR_RESET)" - @echo "$(COLOR_BLUE)║ Joomla Extension Makefile ║$(COLOR_RESET)" + @echo "$(COLOR_BLUE)║ MokoJoomBackup Makefile ║$(COLOR_RESET)" @echo "$(COLOR_BLUE)╚════════════════════════════════════════════════════════════╝$(COLOR_RESET)" @echo "" - @echo "Extension: $(EXTENSION_NAME) ($(EXTENSION_TYPE)) v$(EXTENSION_VERSION)" - @echo "" @echo "$(COLOR_GREEN)Available targets:$(COLOR_RESET)" @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " $(COLOR_BLUE)%-20s$(COLOR_RESET) %s\n", $$1, $$2}' @echo "" -.PHONY: install-deps -install-deps: ## Install all dependencies (Composer + npm) - @echo "$(COLOR_BLUE)Installing dependencies...$(COLOR_RESET)" - @if [ -f "composer.json" ]; then \ - $(COMPOSER) install; \ - echo "$(COLOR_GREEN)✓ Composer dependencies installed$(COLOR_RESET)"; \ - fi +# -- Validation ---------------------------------------------------------------- .PHONY: lint -lint: ## Run PHP linter (syntax check) +lint: ## Run PHP syntax check on all source files @echo "$(COLOR_BLUE)Running PHP linter...$(COLOR_RESET)" - @find . -name "*.php" ! -path "./vendor/*" ! -path "./node_modules/*" ! -path "./$(BUILD_DIR)/*" \ - -exec $(PHP) -l {} \; | grep -v "No syntax errors" || true + @ERROR=0; \ + find $(SRC_DIR) -name "*.php" -exec $(PHP) -l {} \; 2>&1 | grep -v "No syntax errors" || true; \ + if find $(SRC_DIR) -name "*.php" -exec $(PHP) -l {} \; 2>&1 | grep -q "Parse error"; then \ + echo "$(COLOR_RED)✗ Syntax errors found$(COLOR_RESET)"; exit 1; \ + fi @echo "$(COLOR_GREEN)✓ PHP linting complete$(COLOR_RESET)" .PHONY: phpcs phpcs: ## Run PHP CodeSniffer (Joomla standards) @echo "$(COLOR_BLUE)Running PHP CodeSniffer...$(COLOR_RESET)" @if [ -f "$(PHPCS)" ]; then \ - $(PHPCS) --standard=$(PHPCS_STANDARD) --extensions=php --ignore=vendor,node_modules,$(BUILD_DIR) .; \ + $(PHPCS) --standard=$(PHPCS_STANDARD) --extensions=php $(SRC_DIR); \ else \ - echo "$(COLOR_YELLOW)⚠ PHP CodeSniffer not installed. Run: make install-deps$(COLOR_RESET)"; \ + echo "$(COLOR_YELLOW)⚠ PHP CodeSniffer not installed. Run: composer install$(COLOR_RESET)"; \ fi .PHONY: validate -validate: lint phpcs ## Run all validation checks - @echo "$(COLOR_GREEN)✓ All validation checks passed$(COLOR_RESET)" +validate: lint ## Run all local validation checks + @echo "$(COLOR_GREEN)✓ Validation passed$(COLOR_RESET)" -.PHONY: clean -clean: ## Clean build artifacts - @echo "$(COLOR_BLUE)Cleaning build artifacts...$(COLOR_RESET)" - @rm -rf $(BUILD_DIR) $(DIST_DIR) - @echo "$(COLOR_GREEN)✓ Build artifacts cleaned$(COLOR_RESET)" +.PHONY: validate-xml +validate-xml: ## Validate all XML manifests are well-formed + @echo "$(COLOR_BLUE)Validating XML manifests...$(COLOR_RESET)" + @ERROR=0; \ + for f in $$(find $(SRC_DIR) -name "*.xml"); do \ + $(PHP) -r "new SimpleXMLElement(file_get_contents('$$f'));" 2>/dev/null \ + || { echo "$(COLOR_RED)✗ Invalid XML: $$f$(COLOR_RESET)"; ERROR=1; }; \ + done; \ + [ $$ERROR -eq 0 ] && echo "$(COLOR_GREEN)✓ All XML manifests valid$(COLOR_RESET)" || exit 1 + +# -- Dependencies -------------------------------------------------------------- + +.PHONY: install-deps +install-deps: ## Install PHP dependencies via Composer + @echo "$(COLOR_BLUE)Installing dependencies...$(COLOR_RESET)" + @if [ -f "composer.json" ]; then \ + $(COMPOSER) install; \ + echo "$(COLOR_GREEN)✓ Composer dependencies installed$(COLOR_RESET)"; \ + fi + +.PHONY: security-check +security-check: ## Run security audit on dependencies + @echo "$(COLOR_BLUE)Running security checks...$(COLOR_RESET)" + @if [ -f "composer.json" ]; then \ + $(COMPOSER) audit || echo "$(COLOR_YELLOW)⚠ Vulnerabilities found$(COLOR_RESET)"; \ + fi + +# -- Minify -------------------------------------------------------------------- MOKO_PLATFORM ?= $(or $(wildcard ../moko-platform),$(wildcard $(HOME)/moko-platform),$(wildcard /opt/moko-platform)) MINIFY_SCRIPT := $(MOKO_PLATFORM)/build/minify.js .PHONY: minify minify: ## Minify CSS/JS assets - @echo "Minifying assets..." + @echo "$(COLOR_BLUE)Minifying assets...$(COLOR_RESET)" @if [ -f "$(MINIFY_SCRIPT)" ]; then \ node "$(MINIFY_SCRIPT)" $(SRC_DIR); \ elif [ -f "scripts/minify.js" ]; then \ node scripts/minify.js; \ else \ - echo "No minify script found"; \ + echo "$(COLOR_YELLOW)⚠ No minify script found$(COLOR_RESET)"; \ fi -.PHONY: build -build: clean validate minify ## Build extension package - @echo "$(COLOR_BLUE)Building Joomla extension package...$(COLOR_RESET)" - @mkdir -p $(DIST_DIR) $(BUILD_DIR) - - # Determine package prefix based on extension type - @case "$(EXTENSION_TYPE)" in \ - module) \ - PACKAGE_PREFIX="mod_$(EXTENSION_NAME)"; \ - BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \ - ;; \ - plugin) \ - PACKAGE_PREFIX="plg_$(PLUGIN_GROUP)_$(EXTENSION_NAME)"; \ - BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \ - ;; \ - component) \ - PACKAGE_PREFIX="com_$(EXTENSION_NAME)"; \ - BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \ - ;; \ - package) \ - PACKAGE_PREFIX="pkg_$(EXTENSION_NAME)"; \ - BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \ - ;; \ - template) \ - PACKAGE_PREFIX="tpl_$(EXTENSION_NAME)"; \ - BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \ - ;; \ - *) \ - echo "$(COLOR_RED)✗ Unknown extension type: $(EXTENSION_TYPE)$(COLOR_RESET)"; \ - exit 1; \ - ;; \ - esac; \ - \ - mkdir -p "$$BUILD_TARGET"; \ - \ - echo "Building $$PACKAGE_PREFIX..."; \ - \ - rsync -av --progress \ - --exclude='$(BUILD_DIR)' \ - --exclude='$(DIST_DIR)' \ - --exclude='.git*' \ - --exclude='vendor/' \ - --exclude='node_modules/' \ - --exclude='tests/' \ - --exclude='Makefile' \ - --exclude='composer.json' \ - --exclude='composer.lock' \ - --exclude='package.json' \ - --exclude='package-lock.json' \ - --exclude='phpunit.xml' \ - --exclude='*.md' \ - --exclude='.editorconfig' \ - . "$$BUILD_TARGET/"; \ - \ - cd $(BUILD_DIR) && $(ZIP) -r "../$(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip" "$${PACKAGE_PREFIX}"; \ - \ - echo "$(COLOR_GREEN)✓ Package created: $(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip$(COLOR_RESET)" - -.PHONY: package -package: build ## Alias for build - @echo "$(COLOR_GREEN)✓ Package ready for distribution$(COLOR_RESET)" +# -- Release (CI workflow dispatch) -------------------------------------------- .PHONY: release -release: validate build ## Create a release (validate + build) - @echo "$(COLOR_GREEN)✓ Release package ready$(COLOR_RESET)" +release: validate validate-xml ## Trigger pre-release build via CI workflow + @echo "$(COLOR_BLUE)Triggering pre-release workflow...$(COLOR_RESET)" + @if ! command -v curl >/dev/null 2>&1; then \ + echo "$(COLOR_RED)✗ curl required$(COLOR_RESET)"; exit 1; \ + fi + @if [ -z "$$MOKOGITEA_TOKEN" ]; then \ + echo "$(COLOR_RED)✗ MOKOGITEA_TOKEN not set$(COLOR_RESET)"; exit 1; \ + fi + @BRANCH=$$(git rev-parse --abbrev-ref HEAD); \ + curl -sf -X POST \ + -H "Authorization: token $$MOKOGITEA_TOKEN" \ + -H "Content-Type: application/json" \ + "$(GITEA_URL)/api/v1/repos/$(GITEA_ORG)/$(GITEA_REPO)/actions/workflows/pre-release.yml/dispatches" \ + -d "{\"ref\":\"$$BRANCH\",\"inputs\":{\"stability\":\"development\"}}" \ + && echo "$(COLOR_GREEN)✓ Pre-release dispatched on $$BRANCH (development channel)$(COLOR_RESET)" \ + || { echo "$(COLOR_RED)✗ Dispatch failed$(COLOR_RESET)"; exit 1; } + +.PHONY: release-rc +release-rc: validate validate-xml ## Trigger release-candidate build via CI workflow + @echo "$(COLOR_BLUE)Triggering RC pre-release workflow...$(COLOR_RESET)" + @if [ -z "$$MOKOGITEA_TOKEN" ]; then \ + echo "$(COLOR_RED)✗ MOKOGITEA_TOKEN not set$(COLOR_RESET)"; exit 1; \ + fi + @BRANCH=$$(git rev-parse --abbrev-ref HEAD); \ + curl -sf -X POST \ + -H "Authorization: token $$MOKOGITEA_TOKEN" \ + -H "Content-Type: application/json" \ + "$(GITEA_URL)/api/v1/repos/$(GITEA_ORG)/$(GITEA_REPO)/actions/workflows/pre-release.yml/dispatches" \ + -d "{\"ref\":\"$$BRANCH\",\"inputs\":{\"stability\":\"release-candidate\"}}" \ + && echo "$(COLOR_GREEN)✓ Pre-release dispatched on $$BRANCH (release-candidate channel)$(COLOR_RESET)" \ + || { echo "$(COLOR_RED)✗ Dispatch failed$(COLOR_RESET)"; exit 1; } + +# -- Info ---------------------------------------------------------------------- .PHONY: version -version: ## Display version information - @echo "$(COLOR_BLUE)Extension Information:$(COLOR_RESET)" - @echo " Name: $(EXTENSION_NAME)" - @echo " Type: $(EXTENSION_TYPE)" - @echo " Version: $(EXTENSION_VERSION)" - -.PHONY: security-check -security-check: ## Run security checks on dependencies - @echo "$(COLOR_BLUE)Running security checks...$(COLOR_RESET)" - @if [ -f "composer.json" ]; then \ - $(COMPOSER) audit || echo "$(COLOR_YELLOW)⚠ Vulnerabilities found$(COLOR_RESET)"; \ - fi - -.PHONY: all -all: install-deps validate build ## Run complete build pipeline - @echo "$(COLOR_GREEN)✓ Complete build pipeline finished$(COLOR_RESET)" +version: ## Display version from package manifest + @VERSION=$$(grep '' $(SRC_DIR)/pkg_mokobackup.xml | sed 's/.*\(.*\)<\/version>.*/\1/'); \ + echo "$(COLOR_BLUE)$(EXTENSION_NAME)$(COLOR_RESET) v$$VERSION ($(EXTENSION_TYPE))" # Default target .DEFAULT_GOAL := help -- 2.52.0 From a805351dd126700ecd4ecc2f4c4370de58b3c67f Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 14:13:32 -0500 Subject: [PATCH 33/42] fix: move postinstall string to package sys.ini COM_MOKOBACKUP_POSTINSTALL_UPDATE_SITE was showing as a raw key after install because the component language file isn't loaded during package installation. Moved to PKG_MOKOBACKUP_POSTINSTALL_UPDATE_SITE in pkg_mokobackup.sys.ini where the installer can resolve it. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- src/language/en-GB/pkg_mokobackup.sys.ini | 1 + src/language/en-US/pkg_mokobackup.sys.ini | 1 + src/script.php | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/language/en-GB/pkg_mokobackup.sys.ini b/src/language/en-GB/pkg_mokobackup.sys.ini index 071172a..75457fd 100644 --- a/src/language/en-GB/pkg_mokobackup.sys.ini +++ b/src/language/en-GB/pkg_mokobackup.sys.ini @@ -7,3 +7,4 @@ PKG_MOKOBACKUP="Package - MokoJoomBackup" PKG_MOKOBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration. Includes admin component, system plugin, and REST API." PKG_MOKOBACKUP_PHP_VERSION_ERROR="MokoJoomBackup requires PHP %s or later." +PKG_MOKOBACKUP_POSTINSTALL_UPDATE_SITE="MokoJoomBackup installed successfully. Configure your Update Site to receive automatic updates." diff --git a/src/language/en-US/pkg_mokobackup.sys.ini b/src/language/en-US/pkg_mokobackup.sys.ini index 9a32545..4936ac7 100644 --- a/src/language/en-US/pkg_mokobackup.sys.ini +++ b/src/language/en-US/pkg_mokobackup.sys.ini @@ -7,3 +7,4 @@ PKG_MOKOBACKUP="Package - MokoJoomBackup" PKG_MOKOBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration. Includes admin component, system plugin, and REST API." PKG_MOKOBACKUP_PHP_VERSION_ERROR="MokoJoomBackup requires PHP %s or later." +PKG_MOKOBACKUP_POSTINSTALL_UPDATE_SITE="MokoJoomBackup installed successfully. Configure your Update Site to receive automatic updates." diff --git a/src/script.php b/src/script.php index 409a88c..d5cc000 100644 --- a/src/script.php +++ b/src/script.php @@ -194,7 +194,7 @@ class Pkg_MokoBackupInstallerScript ); Factory::getApplication()->enqueueMessage( - Text::sprintf('COM_MOKOBACKUP_POSTINSTALL_UPDATE_SITE', $editUrl), + Text::sprintf('PKG_MOKOBACKUP_POSTINSTALL_UPDATE_SITE', $editUrl), 'info' ); } -- 2.52.0 From e4d704dd843f6b7f7d9d77cc002c19bc6b32e38a Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Thu, 4 Jun 2026 19:13:45 +0000 Subject: [PATCH 34/42] chore(version): auto-bump 01.01.06-dev [skip ci] --- .mokogitea/manifest.xml | 2 +- .mokogitea/workflows/issue-branch.yml | 2 +- README.md | 2 +- src/packages/com_mokobackup/mokobackup.xml | 2 +- src/packages/plg_actionlog_mokobackup/mokobackup.xml | 2 +- src/packages/plg_console_mokobackup/mokobackup.xml | 2 +- src/packages/plg_content_mokobackup/mokobackup.xml | 2 +- src/packages/plg_quickicon_mokobackup/mokobackup.xml | 2 +- src/packages/plg_system_mokobackup/mokobackup.xml | 2 +- src/packages/plg_task_mokobackup/mokobackup.xml | 2 +- src/packages/plg_webservices_mokobackup/mokobackup.xml | 2 +- src/pkg_mokobackup.xml | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 512869a..4d18fbf 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -5,7 +5,7 @@ Package - MokoJoomBackup MokoConsulting Full-site backup and restore for Joomla — database, files, and configuration - 01.01.05-dev + 01.01.06-dev GNU General Public License v3 diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 09a1a30..37661e6 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: 01.01.05 +# VERSION: 01.01.06 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/README.md b/README.md index 741420b..30f88ed 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoJoomBackup - + Full-site backup and restore for Joomla — database, files, and configuration. diff --git a/src/packages/com_mokobackup/mokobackup.xml b/src/packages/com_mokobackup/mokobackup.xml index 08a2744..4de00c9 100644 --- a/src/packages/com_mokobackup/mokobackup.xml +++ b/src/packages/com_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> com_mokobackup - 01.01.05-dev + 01.01.06-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_actionlog_mokobackup/mokobackup.xml b/src/packages/plg_actionlog_mokobackup/mokobackup.xml index 43d280b..d0f39c5 100644 --- a/src/packages/plg_actionlog_mokobackup/mokobackup.xml +++ b/src/packages/plg_actionlog_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_actionlog_mokobackup - 01.01.05-dev + 01.01.06-dev 2026-06-04 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_console_mokobackup/mokobackup.xml b/src/packages/plg_console_mokobackup/mokobackup.xml index 9992f42..225ff1d 100644 --- a/src/packages/plg_console_mokobackup/mokobackup.xml +++ b/src/packages/plg_console_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_console_mokobackup - 01.01.05-dev + 01.01.06-dev 2026-06-04 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_content_mokobackup/mokobackup.xml b/src/packages/plg_content_mokobackup/mokobackup.xml index b0610f3..fbfcdfd 100644 --- a/src/packages/plg_content_mokobackup/mokobackup.xml +++ b/src/packages/plg_content_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_content_mokobackup - 01.01.05-dev + 01.01.06-dev 2026-06-04 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_quickicon_mokobackup/mokobackup.xml b/src/packages/plg_quickicon_mokobackup/mokobackup.xml index 5a666ac..25b5361 100644 --- a/src/packages/plg_quickicon_mokobackup/mokobackup.xml +++ b/src/packages/plg_quickicon_mokobackup/mokobackup.xml @@ -1,7 +1,7 @@ plg_quickicon_mokobackup - 01.01.05-dev + 01.01.06-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_system_mokobackup/mokobackup.xml b/src/packages/plg_system_mokobackup/mokobackup.xml index 64ab1cc..0804d35 100644 --- a/src/packages/plg_system_mokobackup/mokobackup.xml +++ b/src/packages/plg_system_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_system_mokobackup - 01.01.05-dev + 01.01.06-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_task_mokobackup/mokobackup.xml b/src/packages/plg_task_mokobackup/mokobackup.xml index bf9d222..1bd377d 100644 --- a/src/packages/plg_task_mokobackup/mokobackup.xml +++ b/src/packages/plg_task_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_task_mokobackup - 01.01.05-dev + 01.01.06-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_webservices_mokobackup/mokobackup.xml b/src/packages/plg_webservices_mokobackup/mokobackup.xml index 28b70fe..aeb32e9 100644 --- a/src/packages/plg_webservices_mokobackup/mokobackup.xml +++ b/src/packages/plg_webservices_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_webservices_mokobackup - 01.01.05-dev + 01.01.06-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/pkg_mokobackup.xml b/src/pkg_mokobackup.xml index af4eb8b..2dc8c30 100644 --- a/src/pkg_mokobackup.xml +++ b/src/pkg_mokobackup.xml @@ -8,7 +8,7 @@ Package - MokoJoomBackup mokobackup - 01.01.05-dev + 01.01.06-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech -- 2.52.0 From 807da034c9faeac77b3565d38f1f325574ebcbb4 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 16:21:33 -0500 Subject: [PATCH 35/42] fix: add config.xml for component Options The Options toolbar button was crashing with "Form::loadForm could not load file" because config.xml did not exist. Added config.xml with global settings: default backup dir, default profile, update notice toggle, cleanup defaults, notification defaults, and permissions. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- src/packages/com_mokobackup/config.xml | 108 ++++++++++++++++++ .../language/en-GB/com_mokobackup.ini | 21 ++++ .../language/en-US/com_mokobackup.ini | 19 +++ 3 files changed, 148 insertions(+) create mode 100644 src/packages/com_mokobackup/config.xml diff --git a/src/packages/com_mokobackup/config.xml b/src/packages/com_mokobackup/config.xml new file mode 100644 index 0000000..d18904e --- /dev/null +++ b/src/packages/com_mokobackup/config.xml @@ -0,0 +1,108 @@ + + + +
+ + + + + + + + +
+ +
+ + +
+ +
+ + + + + + + + + +
+ +
+ +
+
diff --git a/src/packages/com_mokobackup/language/en-GB/com_mokobackup.ini b/src/packages/com_mokobackup/language/en-GB/com_mokobackup.ini index 442c06a..0924e01 100644 --- a/src/packages/com_mokobackup/language/en-GB/com_mokobackup.ini +++ b/src/packages/com_mokobackup/language/en-GB/com_mokobackup.ini @@ -209,6 +209,27 @@ COM_MOKOBACKUP_UPDATE_SITE_NOTICE="To receive automatic updates, configure your COM_MOKOBACKUP_UPDATE_SITE_MISSING="MokoJoomBackup update site not found. Reinstall the package to register the update server." COM_MOKOBACKUP_POSTINSTALL_UPDATE_SITE="MokoJoomBackup installed successfully. Configure your Update Site to receive automatic updates." +; Component Options (config.xml) +COM_MOKOBACKUP_CONFIG_GENERAL="General" +COM_MOKOBACKUP_CONFIG_DEFAULT_BACKUP_DIR="Default Backup Directory" +COM_MOKOBACKUP_CONFIG_DEFAULT_BACKUP_DIR_DESC="Default directory for backup archives, relative to Joomla root. Can be overridden per profile." +COM_MOKOBACKUP_CONFIG_DEFAULT_PROFILE="Default Profile" +COM_MOKOBACKUP_CONFIG_DEFAULT_PROFILE_DESC="Default backup profile used by quick actions and CLI when no profile is specified." +COM_MOKOBACKUP_CONFIG_SHOW_UPDATE_NOTICE="Show Update Site Notice" +COM_MOKOBACKUP_CONFIG_SHOW_UPDATE_NOTICE_DESC="Display the update site configuration notice on the Backup Records view." +COM_MOKOBACKUP_CONFIG_CLEANUP="Cleanup Defaults" +COM_MOKOBACKUP_CONFIG_MAX_AGE="Max Backup Age (days)" +COM_MOKOBACKUP_CONFIG_MAX_AGE_DESC="Default maximum age for backup records. Used by the system plugin and CLI cleanup command." +COM_MOKOBACKUP_CONFIG_MAX_BACKUPS="Max Backup Count" +COM_MOKOBACKUP_CONFIG_MAX_BACKUPS_DESC="Default maximum number of completed backups to retain." +COM_MOKOBACKUP_CONFIG_NOTIFICATIONS="Notifications" +COM_MOKOBACKUP_CONFIG_NOTIFY_EMAIL="Global Notification Email(s)" +COM_MOKOBACKUP_CONFIG_NOTIFY_EMAIL_DESC="Comma-separated list of email addresses for global backup notifications. Per-profile settings override this." +COM_MOKOBACKUP_CONFIG_NOTIFY_SUCCESS="Notify on Success" +COM_MOKOBACKUP_CONFIG_NOTIFY_SUCCESS_DESC="Send email when any backup completes successfully (unless overridden by profile)." +COM_MOKOBACKUP_CONFIG_NOTIFY_FAILURE="Notify on Failure" +COM_MOKOBACKUP_CONFIG_NOTIFY_FAILURE_DESC="Send email when any backup fails (unless overridden by profile)." + ; Errors COM_MOKOBACKUP_ERROR_FILE_NOT_FOUND="Backup archive file not found or has been deleted." COM_MOKOBACKUP_ERROR_NO_RECORD_SELECTED="No backup record selected for restore." diff --git a/src/packages/com_mokobackup/language/en-US/com_mokobackup.ini b/src/packages/com_mokobackup/language/en-US/com_mokobackup.ini index 577580b..7a87412 100644 --- a/src/packages/com_mokobackup/language/en-US/com_mokobackup.ini +++ b/src/packages/com_mokobackup/language/en-US/com_mokobackup.ini @@ -29,3 +29,22 @@ COM_MOKOBACKUP_NO_PROFILES="No backup profiles found." COM_MOKOBACKUP_UPDATE_SITE_NOTICE="To receive automatic updates, configure your Update Site with your download key." COM_MOKOBACKUP_UPDATE_SITE_MISSING="MokoJoomBackup update site not found. Reinstall the package to register the update server." COM_MOKOBACKUP_POSTINSTALL_UPDATE_SITE="MokoJoomBackup installed successfully. Configure your Update Site to receive automatic updates." +COM_MOKOBACKUP_CONFIG_GENERAL="General" +COM_MOKOBACKUP_CONFIG_DEFAULT_BACKUP_DIR="Default Backup Directory" +COM_MOKOBACKUP_CONFIG_DEFAULT_BACKUP_DIR_DESC="Default directory for backup archives, relative to Joomla root. Can be overridden per profile." +COM_MOKOBACKUP_CONFIG_DEFAULT_PROFILE="Default Profile" +COM_MOKOBACKUP_CONFIG_DEFAULT_PROFILE_DESC="Default backup profile used by quick actions and CLI when no profile is specified." +COM_MOKOBACKUP_CONFIG_SHOW_UPDATE_NOTICE="Show Update Site Notice" +COM_MOKOBACKUP_CONFIG_SHOW_UPDATE_NOTICE_DESC="Display the update site configuration notice on the Backup Records view." +COM_MOKOBACKUP_CONFIG_CLEANUP="Cleanup Defaults" +COM_MOKOBACKUP_CONFIG_MAX_AGE="Max Backup Age (days)" +COM_MOKOBACKUP_CONFIG_MAX_AGE_DESC="Default maximum age for backup records. Used by the system plugin and CLI cleanup command." +COM_MOKOBACKUP_CONFIG_MAX_BACKUPS="Max Backup Count" +COM_MOKOBACKUP_CONFIG_MAX_BACKUPS_DESC="Default maximum number of completed backups to retain." +COM_MOKOBACKUP_CONFIG_NOTIFICATIONS="Notifications" +COM_MOKOBACKUP_CONFIG_NOTIFY_EMAIL="Global Notification Email(s)" +COM_MOKOBACKUP_CONFIG_NOTIFY_EMAIL_DESC="Comma-separated list of email addresses for global backup notifications. Per-profile settings override this." +COM_MOKOBACKUP_CONFIG_NOTIFY_SUCCESS="Notify on Success" +COM_MOKOBACKUP_CONFIG_NOTIFY_SUCCESS_DESC="Send email when any backup completes successfully (unless overridden by profile)." +COM_MOKOBACKUP_CONFIG_NOTIFY_FAILURE="Notify on Failure" +COM_MOKOBACKUP_CONFIG_NOTIFY_FAILURE_DESC="Send email when any backup fails (unless overridden by profile)." -- 2.52.0 From 8d80c218da288f164a9874723eade67283e3a757 Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Thu, 4 Jun 2026 21:23:01 +0000 Subject: [PATCH 36/42] chore(version): auto-bump 01.01.07-dev [skip ci] --- .mokogitea/manifest.xml | 2 +- .mokogitea/workflows/issue-branch.yml | 2 +- README.md | 2 +- src/packages/com_mokobackup/mokobackup.xml | 2 +- src/packages/plg_actionlog_mokobackup/mokobackup.xml | 2 +- src/packages/plg_console_mokobackup/mokobackup.xml | 2 +- src/packages/plg_content_mokobackup/mokobackup.xml | 2 +- src/packages/plg_quickicon_mokobackup/mokobackup.xml | 2 +- src/packages/plg_system_mokobackup/mokobackup.xml | 2 +- src/packages/plg_task_mokobackup/mokobackup.xml | 2 +- src/packages/plg_webservices_mokobackup/mokobackup.xml | 2 +- src/pkg_mokobackup.xml | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml index 4d18fbf..88620a1 100644 --- a/.mokogitea/manifest.xml +++ b/.mokogitea/manifest.xml @@ -5,7 +5,7 @@ Package - MokoJoomBackup MokoConsulting Full-site backup and restore for Joomla — database, files, and configuration - 01.01.06-dev + 01.01.07-dev GNU General Public License v3 diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml index 37661e6..825b392 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: 01.01.06 +# VERSION: 01.01.07 # BRIEF: Auto-create feature branch when an issue is opened name: "Universal: Issue Branch" diff --git a/README.md b/README.md index 30f88ed..547489b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MokoJoomBackup - + Full-site backup and restore for Joomla — database, files, and configuration. diff --git a/src/packages/com_mokobackup/mokobackup.xml b/src/packages/com_mokobackup/mokobackup.xml index 4de00c9..9bf599f 100644 --- a/src/packages/com_mokobackup/mokobackup.xml +++ b/src/packages/com_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> com_mokobackup - 01.01.06-dev + 01.01.07-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_actionlog_mokobackup/mokobackup.xml b/src/packages/plg_actionlog_mokobackup/mokobackup.xml index d0f39c5..343dc1a 100644 --- a/src/packages/plg_actionlog_mokobackup/mokobackup.xml +++ b/src/packages/plg_actionlog_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_actionlog_mokobackup - 01.01.06-dev + 01.01.07-dev 2026-06-04 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_console_mokobackup/mokobackup.xml b/src/packages/plg_console_mokobackup/mokobackup.xml index 225ff1d..262f1c7 100644 --- a/src/packages/plg_console_mokobackup/mokobackup.xml +++ b/src/packages/plg_console_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_console_mokobackup - 01.01.06-dev + 01.01.07-dev 2026-06-04 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_content_mokobackup/mokobackup.xml b/src/packages/plg_content_mokobackup/mokobackup.xml index fbfcdfd..8548bda 100644 --- a/src/packages/plg_content_mokobackup/mokobackup.xml +++ b/src/packages/plg_content_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_content_mokobackup - 01.01.06-dev + 01.01.07-dev 2026-06-04 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_quickicon_mokobackup/mokobackup.xml b/src/packages/plg_quickicon_mokobackup/mokobackup.xml index 25b5361..4341a06 100644 --- a/src/packages/plg_quickicon_mokobackup/mokobackup.xml +++ b/src/packages/plg_quickicon_mokobackup/mokobackup.xml @@ -1,7 +1,7 @@ plg_quickicon_mokobackup - 01.01.06-dev + 01.01.07-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_system_mokobackup/mokobackup.xml b/src/packages/plg_system_mokobackup/mokobackup.xml index 0804d35..899f088 100644 --- a/src/packages/plg_system_mokobackup/mokobackup.xml +++ b/src/packages/plg_system_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_system_mokobackup - 01.01.06-dev + 01.01.07-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_task_mokobackup/mokobackup.xml b/src/packages/plg_task_mokobackup/mokobackup.xml index 1bd377d..cfb1ffc 100644 --- a/src/packages/plg_task_mokobackup/mokobackup.xml +++ b/src/packages/plg_task_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_task_mokobackup - 01.01.06-dev + 01.01.07-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/packages/plg_webservices_mokobackup/mokobackup.xml b/src/packages/plg_webservices_mokobackup/mokobackup.xml index aeb32e9..5ae3520 100644 --- a/src/packages/plg_webservices_mokobackup/mokobackup.xml +++ b/src/packages/plg_webservices_mokobackup/mokobackup.xml @@ -8,7 +8,7 @@ --> plg_webservices_mokobackup - 01.01.06-dev + 01.01.07-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/src/pkg_mokobackup.xml b/src/pkg_mokobackup.xml index 2dc8c30..7f11b69 100644 --- a/src/pkg_mokobackup.xml +++ b/src/pkg_mokobackup.xml @@ -8,7 +8,7 @@ Package - MokoJoomBackup mokobackup - 01.01.06-dev + 01.01.07-dev 2026-06-02 Moko Consulting hello@mokoconsulting.tech -- 2.52.0 From 26635f933c41c3df2fe7763a2d89579b5e9e6b87 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Thu, 4 Jun 2026 22:02:33 +0000 Subject: [PATCH 37/42] chore: add dlid and blockChildUninstall to package manifest [skip ci] --- src/pkg_mokobackup.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pkg_mokobackup.xml b/src/pkg_mokobackup.xml index 7f11b69..86dd028 100644 --- a/src/pkg_mokobackup.xml +++ b/src/pkg_mokobackup.xml @@ -37,4 +37,6 @@ https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/updates.xml + + true -- 2.52.0 From d19274d88bdbc2cd4cefe3c23887362c15e71f98 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 19:06:06 -0500 Subject: [PATCH 38/42] chore: remove deprecated update-server.yml workflow [skip ci] Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .mokogitea/workflows/update-server.yml | 302 ------------------------- 1 file changed, 302 deletions(-) delete mode 100644 .mokogitea/workflows/update-server.yml diff --git a/.mokogitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml deleted file mode 100644 index ac5c9a5..0000000 --- a/.mokogitea/workflows/update-server.yml +++ /dev/null @@ -1,302 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# FILE INFORMATION -# DEFGROUP: Gitea.Workflow -# INGROUP: moko-platform.Universal -# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform -# PATH: /templates/workflows/update-server.yml -# VERSION: 09.23.00 -# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches -# -# Thin wrapper around moko-platform CLI tools. -# Builds packages, updates updates.xml, and optionally deploys via SFTP. -# -# Joomla filters update entries by the user's "Minimum Stability" setting. - -name: "Update Server" - -on: - push: - branches: - - 'dev' - - 'dev/**' - - 'alpha/**' - - 'beta/**' - - 'rc/**' - paths: - - 'src/**' - - 'htdocs/**' - pull_request: - types: [closed] - branches: - - 'dev' - - 'dev/**' - - 'alpha/**' - - 'beta/**' - - 'rc/**' - paths: - - 'src/**' - - 'htdocs/**' - workflow_dispatch: - inputs: - stability: - description: 'Stability tag' - required: true - default: 'development' - type: choice - options: - - development - - alpha - - beta - - rc - - stable - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} - GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} - GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} - -permissions: - contents: write - -jobs: - update-xml: - name: Update Server - runs-on: release - if: >- - github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push' - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - token: ${{ secrets.MOKOGITEA_TOKEN }} - fetch-depth: 0 - - - name: Setup moko-platform tools - env: - MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting - COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}' - run: | - if ! command -v composer &> /dev/null; then - sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 - fi - # Always fetch latest CLI tools — never use stale cache from previous runs - rm -rf /tmp/moko-platform - git clone --depth 1 --branch main --quiet \ - "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ - /tmp/moko-platform 2>/dev/null || true - if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then - cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true - fi - echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV" - - - name: Detect platform - id: platform - run: php ${MOKO_CLI}/manifest_read.php --path . --github-output - - - name: Resolve stability and bump version - id: meta - run: | - BRANCH="${{ github.ref_name }}" - - # Configure git for bot pushes - git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" - 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" - - # Determine stability from branch or manual input - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - STABILITY="${{ inputs.stability }}" - elif [[ "$BRANCH" == rc/* ]]; then - STABILITY="rc" - elif [[ "$BRANCH" == beta/* ]]; then - STABILITY="beta" - elif [[ "$BRANCH" == alpha/* ]]; then - STABILITY="alpha" - else - STABILITY="development" - fi - - # Gitea release tag per stability - case "$STABILITY" in - development) TAG="development" ;; - alpha) TAG="alpha" ;; - beta) TAG="beta" ;; - rc) TAG="release-candidate" ;; - *) TAG="stable" ;; - esac - - # Bump patch, set platform suffix, fix consistency — version_bump preserves suffix - php ${MOKO_CLI}/version_set_platform.php \ - --path . --version "$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo '00.00.01')" \ - --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true - php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true - php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true - - # Read final version (includes suffix, e.g. 01.02.15-dev) - VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01") - - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" - echo "tag=${TAG}" >> "$GITHUB_OUTPUT" - - # Commit version bump if changed - git add -A - git diff --cached --quiet || { - git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \ - --author="gitea-actions[bot] " - git push - } - - - name: Create release and upload package - id: package - run: | - VERSION="${{ steps.meta.outputs.version }}" - TAG="${{ steps.meta.outputs.tag }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - # Create or update Gitea release - php ${MOKO_CLI}/release_create.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease - - # Build package and upload - php ${MOKO_CLI}/release_package.php \ - --path . --version "$VERSION" --tag "$TAG" \ - --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ - --repo "${GITEA_REPO}" --output /tmp || true - - - name: Update updates.xml - if: steps.platform.outputs.platform == 'joomla' - run: | - VERSION="${{ steps.meta.outputs.version }}" - STABILITY="${{ steps.meta.outputs.stability }}" - SHA256="${{ steps.package.outputs.sha256_zip }}" - - if [ ! -f "updates.xml" ]; then - echo "No updates.xml — skipping" - exit 0 - fi - - SHA_FLAG="" - [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}" - - php ${MOKO_CLI}/updates_xml_build.php \ - --path . --version "${VERSION}" --stability "${STABILITY}" \ - --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \ - ${SHA_FLAG} - - # Commit and push updates.xml - git add updates.xml - git diff --cached --quiet || { - git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" - git push - } - - - name: Sync updates.xml to main - if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla' - run: | - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" - - FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \ - "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) - - if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then - python3 -c " - import base64, json, urllib.request, sys - with open('updates.xml', 'rb') as f: - content = base64.b64encode(f.read()).decode() - payload = json.dumps({ - 'content': content, - 'sha': '${FILE_SHA}', - 'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]', - 'branch': 'main' - }).encode() - req = urllib.request.Request( - '${API_BASE}/contents/updates.xml', - data=payload, method='PUT', - headers={ - 'Authorization': 'token ${GITEA_TOKEN}', - 'Content-Type': 'application/json' - }) - try: - urllib.request.urlopen(req) - print('updates.xml synced to main') - except Exception as e: - print(f'WARNING: sync to main failed: {e}', file=sys.stderr) - " - fi - - - name: SFTP deploy to dev server - if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev' - env: - DEV_HOST: ${{ vars.DEV_FTP_HOST }} - DEV_PATH: ${{ vars.DEV_FTP_PATH }} - DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} - DEV_USER: ${{ vars.DEV_FTP_USERNAME }} - DEV_PORT: ${{ vars.DEV_FTP_PORT }} - DEV_KEY: ${{ secrets.DEV_FTP_KEY }} - DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }} - run: | - # Permission check: admin or maintain role required - ACTOR="${{ github.actor }}" - API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ - "${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \ - python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read") - case "$PERMISSION" in - admin|maintain|write) ;; - *) - echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write" - exit 0 - ;; - esac - - [ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; } - - SOURCE_DIR="src" - [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" - [ ! -d "$SOURCE_DIR" ] && exit 0 - - PORT="${DEV_PORT:-22}" - REMOTE="${DEV_PATH%/}" - [ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}" - - printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \ - "$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json - if [ -n "$DEV_KEY" ]; then - echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key - printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json - else - printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json - fi - - PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true) - if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then - php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json - elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then - php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json - fi - rm -f /tmp/deploy_key /tmp/sftp-config.json - echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY - - - name: Summary - if: always() - run: | - VERSION="${{ steps.meta.outputs.version }}" - STABILITY="${{ steps.meta.outputs.stability }}" - DISPLAY="${VERSION}" - echo "## Update Server" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY - echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY -- 2.52.0 From fea1800e06ea0a536181ef462af84bf20d957033 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 19:57:36 -0500 Subject: [PATCH 39/42] fix: console plugin namespace and quickicon translation - Console: use Joomla\Application\ApplicationEvents (was wrong namespace Joomla\Application\Event\ApplicationEvents causing ClassNotFoundError on CLI) - Quickicon: translate text key with Text::_() so language strings render instead of showing raw keys Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plg_console_mokobackup/src/Extension/MokoBackupConsole.php | 2 +- .../src/Extension/MokoBackupQuickicon.php | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/packages/plg_console_mokobackup/src/Extension/MokoBackupConsole.php b/src/packages/plg_console_mokobackup/src/Extension/MokoBackupConsole.php index 5c2ee4f..fca96ac 100644 --- a/src/packages/plg_console_mokobackup/src/Extension/MokoBackupConsole.php +++ b/src/packages/plg_console_mokobackup/src/Extension/MokoBackupConsole.php @@ -28,7 +28,7 @@ final class MokoBackupConsole extends CMSPlugin implements SubscriberInterface public static function getSubscribedEvents(): array { return [ - \Joomla\Application\Event\ApplicationEvents::BEFORE_EXECUTE => 'registerCommands', + \Joomla\Application\ApplicationEvents::BEFORE_EXECUTE => 'registerCommands', ]; } diff --git a/src/packages/plg_quickicon_mokobackup/src/Extension/MokoBackupQuickicon.php b/src/packages/plg_quickicon_mokobackup/src/Extension/MokoBackupQuickicon.php index 5d0bd9a..c72cda5 100644 --- a/src/packages/plg_quickicon_mokobackup/src/Extension/MokoBackupQuickicon.php +++ b/src/packages/plg_quickicon_mokobackup/src/Extension/MokoBackupQuickicon.php @@ -15,6 +15,7 @@ namespace Joomla\Plugin\Quickicon\MokoBackup\Extension; defined('_JEXEC') or die; use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; use Joomla\CMS\Plugin\CMSPlugin; use Joomla\Event\Event; use Joomla\Event\SubscriberInterface; @@ -96,7 +97,7 @@ final class MokoBackupQuickicon extends CMSPlugin implements SubscriberInterface 'link' => 'index.php?option=com_mokobackup&view=backups', 'image' => $warning ? 'icon-warning' : 'icon-database', 'icon' => $warning ? 'icon-warning' : 'icon-database', - 'text' => $text, + 'text' => Text::_($text), 'linkadd' => $subtitle ? '
' . htmlspecialchars($subtitle) . '' : '', 'id' => 'plg_quickicon_mokobackup', 'group' => 'MOD_QUICKICON_MAINTENANCE', -- 2.52.0 From 368461577e23e20c4e4c8edeff86e9fc16051897 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 20:15:52 -0500 Subject: [PATCH 40/42] fix: CLI exit codes and SQL schema defaults - Console commands: replace self::SUCCESS/FAILURE with 0/1 (Joomla's AbstractCommand doesn't define these Symfony constants) - SQL: make manifest and log columns DEFAULT NULL to prevent "doesn't have a default value" on INSERT Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- src/packages/com_mokobackup/sql/install.mysql.sql | 4 ++-- .../com_mokobackup/sql/updates/mysql/01.01.08.sql | 4 ++++ .../src/Command/CleanupCommand.php | 2 +- .../src/Command/ListCommand.php | 4 ++-- .../src/Command/ProfilesCommand.php | 4 ++-- .../src/Command/RestoreCommand.php | 14 +++++++------- .../src/Command/RunCommand.php | 6 +++--- 7 files changed, 21 insertions(+), 17 deletions(-) create mode 100644 src/packages/com_mokobackup/sql/updates/mysql/01.01.08.sql diff --git a/src/packages/com_mokobackup/sql/install.mysql.sql b/src/packages/com_mokobackup/sql/install.mysql.sql index b565a01..877e5f2 100644 --- a/src/packages/com_mokobackup/sql/install.mysql.sql +++ b/src/packages/com_mokobackup/sql/install.mysql.sql @@ -63,8 +63,8 @@ CREATE TABLE IF NOT EXISTS `#__mokobackup_records` ( `remote_filename` VARCHAR(512) NOT NULL DEFAULT '', `checksum` VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'SHA-256 hash of archive', `base_record_id` INT(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Base full backup ID for differential', - `manifest` LONGTEXT NOT NULL COMMENT 'JSON file manifest for differential comparison', - `log` MEDIUMTEXT NOT NULL COMMENT 'Step-by-step backup log', + `manifest` LONGTEXT DEFAULT NULL COMMENT 'JSON file manifest for differential comparison', + `log` MEDIUMTEXT DEFAULT NULL COMMENT 'Step-by-step backup log', PRIMARY KEY (`id`), KEY `idx_profile` (`profile_id`), KEY `idx_status` (`status`), diff --git a/src/packages/com_mokobackup/sql/updates/mysql/01.01.08.sql b/src/packages/com_mokobackup/sql/updates/mysql/01.01.08.sql new file mode 100644 index 0000000..29716e0 --- /dev/null +++ b/src/packages/com_mokobackup/sql/updates/mysql/01.01.08.sql @@ -0,0 +1,4 @@ +-- MokoJoomBackup 01.01.08 +-- Fix: allow NULL defaults for manifest and log columns +ALTER TABLE `#__mokobackup_records` MODIFY `manifest` LONGTEXT DEFAULT NULL; +ALTER TABLE `#__mokobackup_records` MODIFY `log` MEDIUMTEXT DEFAULT NULL; diff --git a/src/packages/plg_console_mokobackup/src/Command/CleanupCommand.php b/src/packages/plg_console_mokobackup/src/Command/CleanupCommand.php index b26a2a5..1a8509a 100644 --- a/src/packages/plg_console_mokobackup/src/Command/CleanupCommand.php +++ b/src/packages/plg_console_mokobackup/src/Command/CleanupCommand.php @@ -120,6 +120,6 @@ class CleanupCommand extends AbstractCommand $io->success(($dryRun ? 'Would delete ' : 'Deleted ') . $deleted . ' backup record(s).'); } - return self::SUCCESS; + return 0; } } diff --git a/src/packages/plg_console_mokobackup/src/Command/ListCommand.php b/src/packages/plg_console_mokobackup/src/Command/ListCommand.php index 8fcdddd..9586339 100644 --- a/src/packages/plg_console_mokobackup/src/Command/ListCommand.php +++ b/src/packages/plg_console_mokobackup/src/Command/ListCommand.php @@ -56,7 +56,7 @@ class ListCommand extends AbstractCommand if (empty($records)) { $io->info('No backup records found.'); - return self::SUCCESS; + return 0; } $rows = []; @@ -82,6 +82,6 @@ class ListCommand extends AbstractCommand $rows ); - return self::SUCCESS; + return 0; } } diff --git a/src/packages/plg_console_mokobackup/src/Command/ProfilesCommand.php b/src/packages/plg_console_mokobackup/src/Command/ProfilesCommand.php index 4d81616..8f4b21c 100644 --- a/src/packages/plg_console_mokobackup/src/Command/ProfilesCommand.php +++ b/src/packages/plg_console_mokobackup/src/Command/ProfilesCommand.php @@ -44,7 +44,7 @@ class ProfilesCommand extends AbstractCommand if (empty($profiles)) { $io->info('No backup profiles found.'); - return self::SUCCESS; + return 0; } $rows = []; @@ -63,6 +63,6 @@ class ProfilesCommand extends AbstractCommand $rows ); - return self::SUCCESS; + return 0; } } diff --git a/src/packages/plg_console_mokobackup/src/Command/RestoreCommand.php b/src/packages/plg_console_mokobackup/src/Command/RestoreCommand.php index fb857d4..e5f9082 100644 --- a/src/packages/plg_console_mokobackup/src/Command/RestoreCommand.php +++ b/src/packages/plg_console_mokobackup/src/Command/RestoreCommand.php @@ -48,19 +48,19 @@ class RestoreCommand extends AbstractCommand if (!$record) { $io->error('Backup record not found: ' . $recordId); - return self::FAILURE; + return 1; } if ($record->status !== 'complete') { $io->error('Cannot restore — backup status is: ' . $record->status); - return self::FAILURE; + return 1; } if (empty($record->absolute_path) || !is_file($record->absolute_path)) { $io->error('Backup archive not found: ' . ($record->absolute_path ?: 'no path')); - return self::FAILURE; + return 1; } $io->warning('This will overwrite the current site files and/or database.'); @@ -70,7 +70,7 @@ class RestoreCommand extends AbstractCommand if (!$io->confirm('Are you sure you want to continue?', false)) { $io->info('Restore cancelled.'); - return self::SUCCESS; + return 0; } $engineFile = JPATH_ADMINISTRATOR . '/components/com_mokobackup/src/Engine/RestoreEngine.php'; @@ -78,7 +78,7 @@ class RestoreCommand extends AbstractCommand if (!file_exists($engineFile)) { $io->error('RestoreEngine not found. Is the component fully installed?'); - return self::FAILURE; + return 1; } if (!class_exists(RestoreEngine::class)) { @@ -91,11 +91,11 @@ class RestoreCommand extends AbstractCommand if ($result['success']) { $io->success($result['message']); - return self::SUCCESS; + return 0; } $io->error($result['message']); - return self::FAILURE; + return 1; } } diff --git a/src/packages/plg_console_mokobackup/src/Command/RunCommand.php b/src/packages/plg_console_mokobackup/src/Command/RunCommand.php index 52185b9..d187737 100644 --- a/src/packages/plg_console_mokobackup/src/Command/RunCommand.php +++ b/src/packages/plg_console_mokobackup/src/Command/RunCommand.php @@ -45,7 +45,7 @@ class RunCommand extends AbstractCommand if (!file_exists($engineFile)) { $io->error('MokoJoomBackup component not installed.'); - return self::FAILURE; + return 1; } if (!class_exists(BackupEngine::class)) { @@ -58,11 +58,11 @@ class RunCommand extends AbstractCommand if ($result['success']) { $io->success($result['message']); - return self::SUCCESS; + return 0; } $io->error($result['message']); - return self::FAILURE; + return 1; } } -- 2.52.0 From cf50551595f3622fb94c3bec1392e76c781526a7 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 20:30:28 -0500 Subject: [PATCH 41/42] feat: folder picker field and default directory dashboard warning - Custom FolderPickerField with AJAX server-side directory browser (Browse button opens collapsible tree, click to navigate, double-click to select). Uses safe DOM methods instead of innerHTML. - AjaxController::browseDir() endpoint lists subdirectories - Dashboard shows warning when any profile uses the default backup directory inside the web root - Profile form and config.xml use FolderPicker for backup_dir field Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- src/packages/com_mokobackup/config.xml | 4 +- src/packages/com_mokobackup/forms/profile.xml | 4 +- .../language/en-GB/com_mokobackup.ini | 9 + .../language/en-US/com_mokobackup.ini | 5 + .../src/Controller/AjaxController.php | 57 ++++++ .../src/Field/FolderPickerField.php | 170 ++++++++++++++++++ .../src/Model/DashboardModel.php | 22 +++ .../src/View/Dashboard/HtmlView.php | 12 +- .../com_mokobackup/tmpl/dashboard/default.php | 13 ++ 9 files changed, 287 insertions(+), 9 deletions(-) create mode 100644 src/packages/com_mokobackup/src/Field/FolderPickerField.php diff --git a/src/packages/com_mokobackup/config.xml b/src/packages/com_mokobackup/config.xml index d18904e..d1a0011 100644 --- a/src/packages/com_mokobackup/config.xml +++ b/src/packages/com_mokobackup/config.xml @@ -10,11 +10,11 @@
sendJson($result); } + /** + * Browse server directories for the folder picker field. + * POST: task=ajax.browseDir&path=/some/path + */ + public function browseDir(): void + { + if (!Session::checkToken('get') && !Session::checkToken('post')) { + $this->sendJson(['error' => true, 'message' => 'Invalid token']); + + return; + } + + $path = $this->input->getString('path', JPATH_ROOT); + $path = realpath($path) ?: $path; + + if (!is_dir($path)) { + $this->sendJson(['error' => true, 'message' => 'Directory not found: ' . $path]); + + return; + } + + // Security: only allow browsing within JPATH_ROOT or parent directories + // that could contain a backup folder (e.g., /home/user/backups) + $dirs = []; + $handle = @opendir($path); + + if ($handle) { + while (($entry = readdir($handle)) !== false) { + if ($entry === '.' || $entry === '..') { + continue; + } + + $fullPath = $path . '/' . $entry; + + if (is_dir($fullPath) && $entry[0] !== '.') { + $dirs[] = [ + 'name' => $entry, + 'path' => $fullPath, + ]; + } + } + + closedir($handle); + } + + usort($dirs, fn($a, $b) => strcasecmp($a['name'], $b['name'])); + + $parent = dirname($path); + + $this->sendJson([ + 'error' => false, + 'current' => $path, + 'parent' => ($parent !== $path) ? $parent : null, + 'dirs' => $dirs, + ]); + } + /** * Send a JSON response and close the application. */ diff --git a/src/packages/com_mokobackup/src/Field/FolderPickerField.php b/src/packages/com_mokobackup/src/Field/FolderPickerField.php new file mode 100644 index 0000000..447725d --- /dev/null +++ b/src/packages/com_mokobackup/src/Field/FolderPickerField.php @@ -0,0 +1,170 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Component\MokoBackup\Administrator\Field; + +defined('_JEXEC') or die; + +use Joomla\CMS\Form\FormField; +use Joomla\CMS\Language\Text; + +class FolderPickerField extends FormField +{ + protected $type = 'FolderPicker'; + + protected function getInput(): string + { + $value = htmlspecialchars($this->value ?: $this->default, ENT_QUOTES, 'UTF-8'); + $id = htmlspecialchars($this->id, ENT_QUOTES, 'UTF-8'); + $name = htmlspecialchars($this->name, ENT_QUOTES, 'UTF-8'); + $jRoot = JPATH_ROOT; + + // Resolve to absolute for display + $rawValue = $this->value ?: $this->default; + + if ($rawValue && $rawValue[0] !== '/') { + $absPath = $jRoot . '/' . $rawValue; + } else { + $absPath = $rawValue; + } + + $exists = is_dir($absPath); + $statusClass = $exists ? 'text-success' : 'text-danger'; + $statusIcon = $exists ? 'icon-publish' : 'icon-unpublish'; + $statusText = $exists + ? Text::_('COM_MOKOBACKUP_FOLDER_EXISTS') + : Text::_('COM_MOKOBACKUP_FOLDER_NOT_FOUND'); + $absPathSafe = htmlspecialchars($absPath, ENT_QUOTES, 'UTF-8'); + + return << + + + +
+ + + {$statusText}: {$absPathSafe} + +
+ + +HTML; + } +} diff --git a/src/packages/com_mokobackup/src/Model/DashboardModel.php b/src/packages/com_mokobackup/src/Model/DashboardModel.php index d10a2b2..1b56de5 100644 --- a/src/packages/com_mokobackup/src/Model/DashboardModel.php +++ b/src/packages/com_mokobackup/src/Model/DashboardModel.php @@ -143,6 +143,28 @@ class DashboardModel extends BaseDatabaseModel return $checks; } + /** + * Check if any profiles use the default (web-root) backup directory. + * + * @return bool + */ + public function isUsingDefaultBackupDir(): bool + { + $db = $this->getDatabase(); + $default = 'administrator/components/com_mokobackup/backups'; + + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokobackup_profiles')) + ->where($db->quoteName('published') . ' = 1') + ->where('(' . $db->quoteName('backup_dir') . ' = ' . $db->quote($default) + . ' OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('') + . ' OR ' . $db->quoteName('backup_dir') . ' IS NULL)'); + $db->setQuery($query); + + return (int) $db->loadResult() > 0; + } + /** * Get published backup profiles for the quick-action selector. * diff --git a/src/packages/com_mokobackup/src/View/Dashboard/HtmlView.php b/src/packages/com_mokobackup/src/View/Dashboard/HtmlView.php index bbfa660..ac24790 100644 --- a/src/packages/com_mokobackup/src/View/Dashboard/HtmlView.php +++ b/src/packages/com_mokobackup/src/View/Dashboard/HtmlView.php @@ -23,17 +23,19 @@ class HtmlView extends BaseHtmlView public object $stats; public array $systemHealth = []; public array $profiles = []; + public bool $defaultDirWarning = false; public function display($tpl = null): void { /** @var \Joomla\Component\MokoBackup\Administrator\Model\DashboardModel $model */ $model = $this->getModel(); - $this->lastBackup = $model->getLastBackup(); - $this->nextScheduled = $model->getNextScheduled(); - $this->stats = $model->getStats(); - $this->systemHealth = $model->getSystemHealth(); - $this->profiles = $model->getProfiles(); + $this->lastBackup = $model->getLastBackup(); + $this->nextScheduled = $model->getNextScheduled(); + $this->stats = $model->getStats(); + $this->systemHealth = $model->getSystemHealth(); + $this->profiles = $model->getProfiles(); + $this->defaultDirWarning = $model->isUsingDefaultBackupDir(); $this->addToolbar(); diff --git a/src/packages/com_mokobackup/tmpl/dashboard/default.php b/src/packages/com_mokobackup/tmpl/dashboard/default.php index 8d7d43c..5f33f87 100644 --- a/src/packages/com_mokobackup/tmpl/dashboard/default.php +++ b/src/packages/com_mokobackup/tmpl/dashboard/default.php @@ -18,6 +18,19 @@ use Joomla\CMS\Session\Session; $ajaxToken = Session::getFormToken(); $ajaxUrl = Route::_('index.php?option=com_mokobackup&format=json', false); ?> +defaultDirWarning) : ?> + + +
-- 2.52.0 From 75e72c248a5cd3aeba40d853c871f1934f14e318 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 20:40:07 -0500 Subject: [PATCH 42/42] feat: tar.gz archives, table checkbox excludes, user group notifications (#32, #33, #34) Archive formats (#32): - ArchiverInterface abstraction with ZipArchiver and TarGzArchiver - BackupEngine uses archiver factory based on profile archive_format - tar.gz uses PharData (bundled with PHP, no extra extensions) - RestoreEngine detects and extracts tar.gz via PharData - AES-256 encryption skipped for non-ZIP formats with log warning Exclude fields (#33): - ExcludeListField: dynamic table with add/remove rows for dirs and files - DatabaseTablesField: auto-populated checkbox list of all site tables - Replaces textarea-based exclusion fields in profile form User group notifications (#34): - usergrouplist field added to profile notifications fieldset - NotificationSender resolves group members to emails at send time - Combined with manual email addresses, deduplicated - SQL migration adds notify_user_groups column Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- src/packages/com_mokobackup/forms/profile.xml | 26 ++-- .../language/en-GB/com_mokobackup.ini | 8 ++ .../language/en-US/com_mokobackup.ini | 7 + .../com_mokobackup/sql/install.mysql.sql | 1 + .../sql/updates/mysql/01.01.08.sql | 3 + .../src/Engine/ArchiverInterface.php | 41 ++++++ .../src/Engine/BackupEngine.php | 41 ++++-- .../src/Engine/NotificationSender.php | 50 +++++++- .../src/Engine/RestoreEngine.php | 15 ++- .../src/Engine/TarGzArchiver.php | 63 +++++++++ .../com_mokobackup/src/Engine/ZipArchiver.php | 47 +++++++ .../src/Field/DatabaseTablesField.php | 105 +++++++++++++++ .../src/Field/ExcludeListField.php | 120 ++++++++++++++++++ 13 files changed, 500 insertions(+), 27 deletions(-) create mode 100644 src/packages/com_mokobackup/src/Engine/ArchiverInterface.php create mode 100644 src/packages/com_mokobackup/src/Engine/TarGzArchiver.php create mode 100644 src/packages/com_mokobackup/src/Engine/ZipArchiver.php create mode 100644 src/packages/com_mokobackup/src/Field/DatabaseTablesField.php create mode 100644 src/packages/com_mokobackup/src/Field/ExcludeListField.php diff --git a/src/packages/com_mokobackup/forms/profile.xml b/src/packages/com_mokobackup/forms/profile.xml index 837464c..827f6cd 100644 --- a/src/packages/com_mokobackup/forms/profile.xml +++ b/src/packages/com_mokobackup/forms/profile.xml @@ -39,6 +39,7 @@ default="zip" > +
@@ -176,6 +176,14 @@ maxlength="512" hint="admin@example.com, backup@example.com" /> + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Component\MokoBackup\Administrator\Engine; + +defined('_JEXEC') or die; + +interface ArchiverInterface +{ + /** + * Open or create the archive at the given path. + */ + public function open(string $path): void; + + /** + * Add a string as a file inside the archive. + */ + public function addFromString(string $localName, string $contents): void; + + /** + * Add a file from disk into the archive. + */ + public function addFile(string $filePath, string $localName): void; + + /** + * Finalize and close the archive. + */ + public function close(): void; + + /** + * Return the file extension for this archive type (e.g. 'zip', 'tar.gz'). + */ + public function getExtension(): string; +} diff --git a/src/packages/com_mokobackup/src/Engine/BackupEngine.php b/src/packages/com_mokobackup/src/Engine/BackupEngine.php index cf456cd..1b239fd 100644 --- a/src/packages/com_mokobackup/src/Engine/BackupEngine.php +++ b/src/packages/com_mokobackup/src/Engine/BackupEngine.php @@ -71,7 +71,10 @@ class BackupEngine $now = date('Y-m-d H:i:s'); $tag = date('Ymd_His'); $hostname = preg_replace('/[^a-zA-Z0-9._-]/', '', $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? php_uname('n')); - $archiveName = $hostname . '_' . $tag . '_profile' . $profileId . '.zip'; + $archiveFormat = $profile->archive_format ?? 'zip'; + $archiver = $this->createArchiver($archiveFormat); + $archiveExt = $archiver->getExtension(); + $archiveName = $hostname . '_' . $tag . '_profile' . $profileId . '.' . $archiveExt; if (empty($description)) { $description = $profile->title . ' — ' . $now; @@ -105,12 +108,8 @@ class BackupEngine $this->log('Backup started: ' . $description); $archivePath = $this->backupDir . '/' . $archiveName; - // Create ZIP archive - $zip = new \ZipArchive(); - - if ($zip->open($archivePath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) { - throw new \RuntimeException('Cannot create archive: ' . $archivePath); - } + // Create archive + $archiver->open($archivePath); $dbSize = 0; $filesCount = 0; @@ -121,7 +120,7 @@ class BackupEngine $this->log('Starting database dump...'); $dumper = new DatabaseDumper($excludeTables); $sqlDump = $dumper->dump(); - $zip->addFromString('database.sql', $sqlDump); + $archiver->addFromString('database.sql', $sqlDump); $dbSize = strlen($sqlDump); $tablesCount = $dumper->getTablesCount(); $this->log('Database dump complete: ' . $tablesCount . ' tables, ' . number_format($dbSize) . ' bytes'); @@ -157,7 +156,7 @@ class BackupEngine $fullPath = JPATH_ROOT . '/' . $relativePath; if (is_file($fullPath) && is_readable($fullPath)) { - $zip->addFile($fullPath, $relativePath); + $archiver->addFile($fullPath, $relativePath); } } @@ -170,15 +169,19 @@ class BackupEngine } } - $zip->close(); + $archiver->close(); // Step 1.5: Apply AES-256 encryption (if configured) $encryptionPassword = $profile->encryption_password ?? ''; if (!empty($encryptionPassword)) { - $this->log('Encrypting archive with AES-256...'); - $this->encryptArchive($archivePath, $encryptionPassword); - $this->log('Archive encrypted'); + if ($archiveFormat !== 'zip') { + $this->log('WARNING: AES-256 encryption only supported for ZIP archives — skipping encryption'); + } else { + $this->log('Encrypting archive with AES-256...'); + $this->encryptArchive($archivePath, $encryptionPassword); + $this->log('Archive encrypted'); + } } // Record archive size and compute checksum (after encryption) @@ -361,6 +364,18 @@ class BackupEngine return true; } + /** + * Create the appropriate archiver based on the archive format. + */ + private function createArchiver(string $format): ArchiverInterface + { + return match ($format) { + 'zip' => new ZipArchiver(), + 'tar.gz' => new TarGzArchiver(), + default => new ZipArchiver(), + }; + } + /** * Create the appropriate remote uploader based on the storage type. */ diff --git a/src/packages/com_mokobackup/src/Engine/NotificationSender.php b/src/packages/com_mokobackup/src/Engine/NotificationSender.php index 82808a0..9008aa2 100644 --- a/src/packages/com_mokobackup/src/Engine/NotificationSender.php +++ b/src/packages/com_mokobackup/src/Engine/NotificationSender.php @@ -33,9 +33,13 @@ class NotificationSender */ public static function send(object $profile, object $record, bool $success, string $logText = ''): bool { - $notifyEmail = trim($profile->notify_email ?? ''); + $notifyEmail = trim($profile->notify_email ?? ''); + $notifyUserGroups = $profile->notify_user_groups ?? ''; - if (empty($notifyEmail)) { + // Resolve user group members to email addresses + $groupEmails = self::resolveUserGroupEmails($notifyUserGroups); + + if (empty($notifyEmail) && empty($groupEmails)) { return false; } @@ -54,9 +58,10 @@ class NotificationSender $siteName = $config->get('sitename', 'Joomla Site'); $siteUrl = Uri::root(); - // Parse recipient list (comma-separated) + // Parse recipient list (comma-separated) + user group emails $recipients = array_map('trim', explode(',', $notifyEmail)); - $recipients = array_filter($recipients, fn($e) => filter_var($e, FILTER_VALIDATE_EMAIL)); + $recipients = array_merge($recipients, $groupEmails); + $recipients = array_unique(array_filter($recipients, fn($e) => filter_var($e, FILTER_VALIDATE_EMAIL))); if (empty($recipients)) { return false; @@ -133,4 +138,41 @@ class NotificationSender return false; } } + + /** + * Resolve user group IDs to email addresses of group members. + * + * @param string|array $groups Comma-separated group IDs or array + * + * @return array Email addresses + */ + private static function resolveUserGroupEmails(string|array $groups): array + { + if (empty($groups)) { + return []; + } + + if (\is_string($groups)) { + $groups = array_filter(array_map('intval', explode(',', $groups))); + } + + if (empty($groups)) { + return []; + } + + try { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('u.email')) + ->from($db->quoteName('#__users', 'u')) + ->join('INNER', $db->quoteName('#__user_usergroup_map', 'ugm') . ' ON ugm.user_id = u.id') + ->where($db->quoteName('u.block') . ' = 0') + ->whereIn($db->quoteName('ugm.group_id'), $groups); + $db->setQuery($query); + + return $db->loadColumn() ?: []; + } catch (\Throwable $e) { + return []; + } + } } diff --git a/src/packages/com_mokobackup/src/Engine/RestoreEngine.php b/src/packages/com_mokobackup/src/Engine/RestoreEngine.php index eb33467..7099957 100644 --- a/src/packages/com_mokobackup/src/Engine/RestoreEngine.php +++ b/src/packages/com_mokobackup/src/Engine/RestoreEngine.php @@ -89,12 +89,15 @@ class RestoreEngine // Step 1: Extract archive to staging $this->log('Extracting archive: ' . basename($archivePath)); - // Detect format: JPA or ZIP + // Detect format: JPA, tar.gz, or ZIP if (JpaUnarchiver::isJpaFile($archivePath)) { $this->log('Detected JPA format (Akeeba Backup archive)'); $jpa = new JpaUnarchiver($archivePath, $this->stagingDir); $count = $jpa->extract(); $this->log('Extracted ' . $count . ' files from JPA'); + } elseif (str_ends_with($archivePath, '.tar.gz') || str_ends_with($archivePath, '.tgz')) { + $this->log('Detected tar.gz format'); + $this->extractTarGz($archivePath); } else { $this->extractArchive($archivePath, $password); } @@ -200,6 +203,16 @@ class RestoreEngine $zip->close(); } + /** + * Extract a tar.gz archive to the staging directory. + */ + private function extractTarGz(string $archivePath): void + { + $phar = new \PharData($archivePath); + $phar->extractTo($this->stagingDir, null, true); + $this->log('Extracted tar.gz archive'); + } + /** * Recursively delete a directory and all its contents. */ diff --git a/src/packages/com_mokobackup/src/Engine/TarGzArchiver.php b/src/packages/com_mokobackup/src/Engine/TarGzArchiver.php new file mode 100644 index 0000000..fdce0ce --- /dev/null +++ b/src/packages/com_mokobackup/src/Engine/TarGzArchiver.php @@ -0,0 +1,63 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Component\MokoBackup\Administrator\Engine; + +defined('_JEXEC') or die; + +class TarGzArchiver implements ArchiverInterface +{ + private \PharData $tar; + private string $tarPath; + + public function open(string $path): void + { + // PharData creates .tar first, then we compress to .tar.gz + // Strip .gz to get the .tar path for initial creation + $this->tarPath = preg_replace('/\.gz$/', '', $path); + + // Remove existing files to avoid "already exists" errors + if (is_file($this->tarPath)) { + @unlink($this->tarPath); + } + + if (is_file($path)) { + @unlink($path); + } + + $this->tar = new \PharData($this->tarPath); + } + + public function addFromString(string $localName, string $contents): void + { + $this->tar->addFromString($localName, $contents); + } + + public function addFile(string $filePath, string $localName): void + { + $this->tar->addFile($filePath, $localName); + } + + public function close(): void + { + // Compress the .tar to .tar.gz + $this->tar->compress(\Phar::GZ); + + // Remove the uncompressed .tar + if (is_file($this->tarPath)) { + @unlink($this->tarPath); + } + } + + public function getExtension(): string + { + return 'tar.gz'; + } +} diff --git a/src/packages/com_mokobackup/src/Engine/ZipArchiver.php b/src/packages/com_mokobackup/src/Engine/ZipArchiver.php new file mode 100644 index 0000000..e161035 --- /dev/null +++ b/src/packages/com_mokobackup/src/Engine/ZipArchiver.php @@ -0,0 +1,47 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Component\MokoBackup\Administrator\Engine; + +defined('_JEXEC') or die; + +class ZipArchiver implements ArchiverInterface +{ + private \ZipArchive $zip; + + public function open(string $path): void + { + $this->zip = new \ZipArchive(); + + if ($this->zip->open($path, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) { + throw new \RuntimeException('Cannot create ZIP archive: ' . $path); + } + } + + public function addFromString(string $localName, string $contents): void + { + $this->zip->addFromString($localName, $contents); + } + + public function addFile(string $filePath, string $localName): void + { + $this->zip->addFile($filePath, $localName); + } + + public function close(): void + { + $this->zip->close(); + } + + public function getExtension(): string + { + return 'zip'; + } +} diff --git a/src/packages/com_mokobackup/src/Field/DatabaseTablesField.php b/src/packages/com_mokobackup/src/Field/DatabaseTablesField.php new file mode 100644 index 0000000..e563017 --- /dev/null +++ b/src/packages/com_mokobackup/src/Field/DatabaseTablesField.php @@ -0,0 +1,105 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Component\MokoBackup\Administrator\Field; + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Form\FormField; +use Joomla\CMS\Language\Text; + +class DatabaseTablesField extends FormField +{ + protected $type = 'DatabaseTables'; + + protected function getInput(): string + { + $db = Factory::getDbo(); + $tables = $db->getTableList(); + $prefix = $db->getPrefix(); + + // Parse current exclusions (newline-separated) + $excluded = []; + + if (!empty($this->value)) { + $excluded = array_filter(array_map('trim', explode("\n", str_replace("\r", '', $this->value)))); + } + + // Normalize: replace literal #__ with actual prefix for comparison + $excludedNormalized = array_map(function ($t) use ($prefix) { + return str_replace('#__', $prefix, $t); + }, $excluded); + + $id = htmlspecialchars($this->id, ENT_QUOTES, 'UTF-8'); + $name = htmlspecialchars($this->name, ENT_QUOTES, 'UTF-8'); + + $html = '
'; + $html .= ''; + $html .= '
' . Text::_('COM_MOKOBACKUP_FIELD_EXCLUDE_TABLES_HELP') . '
'; + $html .= '
'; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + + foreach ($tables as $table) { + $isExcluded = \in_array($table, $excludedNormalized, true); + + // Convert to #__ notation for storage + $storeValue = $table; + + if (str_starts_with($table, $prefix)) { + $storeValue = '#__' . substr($table, \strlen($prefix)); + } + + $safeValue = htmlspecialchars($storeValue, ENT_QUOTES, 'UTF-8'); + $safeTable = htmlspecialchars($table, ENT_QUOTES, 'UTF-8'); + $checked = $isExcluded ? ' checked' : ''; + + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + + $html .= '
' . Text::_('COM_MOKOBACKUP_FIELD_TABLE_NAME') . '
' . $safeTable . '
'; + + // Script to sync checkboxes to hidden field + $html .= << +SCRIPT; + + return $html; + } +} diff --git a/src/packages/com_mokobackup/src/Field/ExcludeListField.php b/src/packages/com_mokobackup/src/Field/ExcludeListField.php new file mode 100644 index 0000000..483e68c --- /dev/null +++ b/src/packages/com_mokobackup/src/Field/ExcludeListField.php @@ -0,0 +1,120 @@ + + * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. + * @license GNU General Public License version 3 or later; see LICENSE + */ + +namespace Joomla\Component\MokoBackup\Administrator\Field; + +defined('_JEXEC') or die; + +use Joomla\CMS\Form\FormField; +use Joomla\CMS\Language\Text; + +class ExcludeListField extends FormField +{ + protected $type = 'ExcludeList'; + + protected function getInput(): string + { + $id = htmlspecialchars($this->id, ENT_QUOTES, 'UTF-8'); + $name = htmlspecialchars($this->name, ENT_QUOTES, 'UTF-8'); + $placeholder = htmlspecialchars((string) ($this->element['hint'] ?? ''), ENT_QUOTES, 'UTF-8'); + + // Parse current values (newline-separated) + $items = []; + + if (!empty($this->value)) { + $items = array_values(array_filter(array_map('trim', explode("\n", str_replace("\r", '', $this->value))))); + } + + $html = '
'; + $html .= ''; + $html .= ''; + $html .= ''; + + foreach ($items as $item) { + $safeItem = htmlspecialchars($item, ENT_QUOTES, 'UTF-8'); + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + + $html .= '
'; + $html .= ''; + $html .= '
'; + + $html .= << +SCRIPT; + + return $html; + } +} -- 2.52.0