From 31941e80c34c14192476f32774bc967651fa2863 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Thu, 4 Jun 2026 12:13:17 +0000
Subject: [PATCH 01/14] chore: sync updates.xml 01.02.00-rc from rc [skip ci]
---
updates.xml | 38 +++++++++++++++++++++++++-------------
1 file changed, 25 insertions(+), 13 deletions(-)
diff --git a/updates.xml b/updates.xml
index 5ba8cf1..44efdc7 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 rc build.
+ pkg_mokobackup
+ package
+ site
+ 01.02.00-rc
+ 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.02.00-rc.zip
+
+ e6d3f859a1e0c8ad2240efd8eeb2a1365c35e9959043d086a43ccca07ce117d4
+ rc
+ https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/raw/branch/main/CHANGELOG.md
+ Moko Consulting
+ https://mokoconsulting.tech
+
+
From d383d1fc09f0771818db6783a0320c1b28ae3316 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Thu, 4 Jun 2026 12:41:04 +0000
Subject: [PATCH 02/14] chore: sync updates.xml from development [skip ci]
---
updates.xml | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/updates.xml b/updates.xml
index 44efdc7..daf6dce 100644
--- a/updates.xml
+++ b/updates.xml
@@ -1,24 +1,24 @@
Package - MokoJoomBackup
- Package - MokoJoomBackup rc build.
+ Package - MokoJoomBackup development build.
pkg_mokobackup
package
site
- 01.02.00-rc
+ 01.01.01-dev
2026-06-04
- https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/tag/release-candidate
+ https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/tag/development
- https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/download/release-candidate/pkg_mokobackup-01.02.00-rc.zip
+ https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/download/development/pkg_mokobackup-01.01.01-dev.zip
- e6d3f859a1e0c8ad2240efd8eeb2a1365c35e9959043d086a43ccca07ce117d4
- rc
+ 4c2702d04479863bbffef705f55fe10f8ffbe01c598d9fb4b410bb117fac1ff2
+ dev
https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/raw/branch/main/CHANGELOG.md
Moko Consulting
https://mokoconsulting.tech
From 83b47ce8496199f11dd62fe5863970cffecd2dc5 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Thu, 4 Jun 2026 13:02:06 +0000
Subject: [PATCH 03/14] chore: sync updates.xml from development [skip ci]
---
updates.xml | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/updates.xml b/updates.xml
index daf6dce..deb391e 100644
--- a/updates.xml
+++ b/updates.xml
@@ -1,7 +1,7 @@
@@ -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
From 8ebbfa7aedaac4c049a214e69a31ce95d36cb6aa Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Thu, 4 Jun 2026 13:16:03 +0000
Subject: [PATCH 04/14] chore: sync updates.xml from development [skip ci]
---
updates.xml | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/updates.xml b/updates.xml
index deb391e..8dd2da5 100644
--- a/updates.xml
+++ b/updates.xml
@@ -1,7 +1,7 @@
@@ -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
From b40482d8a52cc6238994e03cccbab6ef1faaca6a Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Thu, 4 Jun 2026 13:47:45 +0000
Subject: [PATCH 05/14] chore: sync .mokogitea/workflows/repo-health.yml from
moko-platform [skip ci]
---
.mokogitea/workflows/repo-health.yml | 1528 ++++++++++++--------------
1 file changed, 711 insertions(+), 817 deletions(-)
diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml
index e3653eb..8d57aaf 100644
--- a/.mokogitea/workflows/repo-health.yml
+++ b/.mokogitea/workflows/repo-health.yml
@@ -1,817 +1,711 @@
-# ============================================================================
-# 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 scripts governance, tooling availability, and core repository health artifacts.
+# ============================================================================
+
+name: "Generic: Repo Health"
+
+defaults:
+ run:
+ shell: bash
+
+on:
+ workflow_dispatch:
+ inputs:
+ profile:
+ description: 'Validation profile: all, scripts, or repo'
+ required: true
+ default: all
+ type: choice
+ options:
+ - all
+ - scripts
+ - repo
+ pull_request:
+ push:
+
+permissions:
+ contents: read
+
+env:
+ # Scripts governance policy
+ SCRIPTS_REQUIRED_DIRS:
+ SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
+
+ # Repo health policy
+ REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/
+ REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
+ REPO_DISALLOWED_DIRS:
+ REPO_DISALLOWED_FILES: TODO.md,todo.md
+
+ # Extended checks toggles
+ EXTENDED_CHECKS: "true"
+
+ # File / directory variables
+ DOCS_INDEX: docs/docs-index.md
+ SCRIPT_DIR: scripts
+ WORKFLOWS_DIR: .mokogitea/workflows
+ SHELLCHECK_PATTERN: '*.sh'
+ SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+
+jobs:
+ access_check:
+ name: Access control
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ permissions:
+ contents: read
+
+ outputs:
+ allowed: ${{ steps.perm.outputs.allowed }}
+ permission: ${{ steps.perm.outputs.permission }}
+
+ steps:
+ - name: Check actor permission (admin only)
+ id: perm
+ env:
+ TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
+ REPO: ${{ github.repository }}
+ ACTOR: ${{ github.actor }}
+ run: |
+ set -euo pipefail
+ ALLOWED=false
+ PERMISSION=unknown
+ METHOD=""
+
+ # Hardcoded authorized users — always allowed
+ case "$ACTOR" in
+ jmiller|gitea-actions[bot])
+ ALLOWED=true
+ PERMISSION=admin
+ METHOD="hardcoded allowlist"
+ ;;
+ *)
+ # Detect platform and check permissions via API
+ API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}"
+ RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \
+ "${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}')
+ PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown")
+ if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then
+ ALLOWED=true
+ fi
+ METHOD="collaborator API"
+ ;;
+ esac
+
+ echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT"
+ echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT"
+
+ {
+ echo "## Access Authorization"
+ echo ""
+ echo "| Field | Value |"
+ echo "|-------|-------|"
+ echo "| **Actor** | \`${ACTOR}\` |"
+ echo "| **Repository** | \`${REPO}\` |"
+ echo "| **Permission** | \`${PERMISSION}\` |"
+ echo "| **Method** | ${METHOD} |"
+ echo "| **Authorized** | ${ALLOWED} |"
+ echo ""
+ if [ "$ALLOWED" = "true" ]; then
+ echo "${ACTOR} authorized (${METHOD})"
+ else
+ echo "${ACTOR} is NOT authorized. Requires admin or maintain role."
+ fi
+ } >> "${GITHUB_STEP_SUMMARY}"
+
+ - name: Deny execution when not permitted
+ if: ${{ steps.perm.outputs.allowed != 'true' }}
+ run: |
+ set -euo pipefail
+ printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
+ exit 1
+
+ scripts_governance:
+ name: Scripts governance
+ needs: access_check
+ if: ${{ needs.access_check.outputs.allowed == 'true' }}
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ fetch-depth: 0
+
+ - name: Scripts folder checks
+ env:
+ PROFILE_RAW: ${{ github.event.inputs.profile }}
+ run: |
+ set -euo pipefail
+
+ profile="${PROFILE_RAW:-all}"
+ case "${profile}" in
+ all|scripts|repo) ;;
+ *)
+ printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
+ exit 1
+ ;;
+ esac
+
+ if [ "${profile}" = 'repo' ]; then
+ {
+ printf '%s\n' '### Scripts governance'
+ printf '%s\n' "Profile: ${profile}"
+ printf '%s\n' 'Status: SKIPPED'
+ printf '%s\n' 'Reason: profile excludes scripts governance'
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ exit 0
+ fi
+
+ if [ ! -d "${SCRIPT_DIR}" ]; then
+ {
+ printf '%s\n' '### Scripts governance'
+ printf '%s\n' 'Status: OK (advisory)'
+ printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.'
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ exit 0
+ fi
+
+ if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi
+ IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
+
+ missing_dirs=()
+ unapproved_dirs=()
+
+ for d in "${required_dirs[@]}"; do
+ req="${d%/}"
+ [ ! -d "${req}" ] && missing_dirs+=("${req}/")
+ done
+
+ while IFS= read -r d; do
+ allowed=false
+ for a in "${allowed_dirs[@]}"; do
+ a_norm="${a%/}"
+ [ "${d%/}" = "${a_norm}" ] && allowed=true
+ done
+ [ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/")
+ done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##')
+
+ {
+ printf '%s\n' '### Scripts governance'
+ printf '%s\n' "Profile: ${profile}"
+ printf '%s\n' '| Area | Status | Notes |'
+ printf '%s\n' '|---|---|---|'
+
+ if [ "${#missing_dirs[@]}" -gt 0 ]; then
+ printf '%s\n' '| Required directories | Warning | Missing required subfolders |'
+ else
+ printf '%s\n' '| Required directories | OK | All required subfolders present |'
+ fi
+
+ if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
+ printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |'
+ else
+ printf '%s\n' '| Directory policy | OK | No unapproved directories |'
+ fi
+
+ printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |'
+ printf '\n'
+
+ if [ "${#missing_dirs[@]}" -gt 0 ]; then
+ printf '%s\n' 'Missing required script directories:'
+ for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done
+ printf '\n'
+ else
+ printf '%s\n' 'Missing required script directories: none.'
+ printf '\n'
+ fi
+
+ if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
+ printf '%s\n' 'Unapproved script directories detected:'
+ for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done
+ printf '\n'
+ else
+ printf '%s\n' 'Unapproved script directories detected: none.'
+ printf '\n'
+ fi
+
+ printf '%s\n' 'Scripts governance completed in advisory mode.'
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+
+ repo_health:
+ name: Repository health
+ needs: access_check
+ if: ${{ needs.access_check.outputs.allowed == 'true' }}
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ fetch-depth: 0
+
+ - name: Repository health checks
+ env:
+ PROFILE_RAW: ${{ github.event.inputs.profile }}
+ run: |
+ set -euo pipefail
+
+ profile="${PROFILE_RAW:-all}"
+ case "${profile}" in
+ all|scripts|repo) ;;
+ *)
+ printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
+ exit 1
+ ;;
+ esac
+
+ if [ "${profile}" = 'scripts' ]; then
+ {
+ printf '%s\n' '### Repository health'
+ printf '%s\n' "Profile: ${profile}"
+ printf '%s\n' 'Status: SKIPPED'
+ printf '%s\n' 'Reason: profile excludes repository health'
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ exit 0
+ fi
+
+ IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
+ IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
+ if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi
+ IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}"
+
+ missing_required=()
+ missing_optional=()
+
+ # Source directory: src/ or htdocs/ (either is valid for extension repos)
+ SOURCE_DIR=""
+ if [ -d "src" ]; then
+ SOURCE_DIR="src"
+ elif [ -d "htdocs" ]; then
+ SOURCE_DIR="htdocs"
+ elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
+ # Platform/tooling repos don't need src/
+ SOURCE_DIR=""
+ else
+ missing_required+=("src/ or htdocs/ (source directory required)")
+ fi
+
+ for item in "${required_artifacts[@]}"; do
+ if printf '%s' "${item}" | grep -q '/$'; then
+ d="${item%/}"
+ [ ! -d "${d}" ] && missing_required+=("${item}")
+ else
+ [ ! -f "${item}" ] && missing_required+=("${item}")
+ fi
+ done
+
+ for f in "${optional_files[@]}"; do
+ if printf '%s' "${f}" | grep -q '/$'; then
+ d="${f%/}"
+ [ ! -d "${d}" ] && missing_optional+=("${f}")
+ else
+ [ ! -f "${f}" ] && missing_optional+=("${f}")
+ fi
+ done
+
+ for d in "${disallowed_dirs[@]}"; do
+ d_norm="${d%/}"
+ [ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)")
+ done
+
+ for f in "${disallowed_files[@]}"; do
+ [ -f "${f}" ] && missing_required+=("${f} (disallowed)")
+ done
+
+ git fetch origin --prune
+
+ dev_paths=()
+ dev_branches=()
+
+ while IFS= read -r b; do
+ name="${b#origin/}"
+ if [ "${name}" = 'dev' ]; then
+ dev_branches+=("${name}")
+ else
+ dev_paths+=("${name}")
+ fi
+ done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
+
+ if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then
+ missing_required+=("dev or dev/* branch")
+ fi
+
+ content_warnings=()
+
+ if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then
+ content_warnings+=("CHANGELOG.md missing '# Changelog' header")
+ fi
+
+ if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then
+ content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)")
+ fi
+
+ if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then
+ content_warnings+=("LICENSE does not look like a GPL text")
+ fi
+
+ if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then
+ content_warnings+=("README.md missing expected brand keyword")
+ fi
+
+ export PROFILE_RAW="${profile}"
+ export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")"
+ export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
+ export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
+
+ report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}")
+
+ {
+ printf '%s\n' '### Repository health'
+ printf '%s\n' "Profile: ${profile}"
+ printf '%s\n' '| Metric | Value |'
+ printf '%s\n' '|---|---|'
+ printf '%s\n' "| Missing required | ${#missing_required[@]} |"
+ printf '%s\n' "| Missing optional | ${#missing_optional[@]} |"
+ printf '%s\n' "| Content warnings | ${#content_warnings[@]} |"
+ printf '\n'
+
+ printf '%s\n' '### Guardrails report (JSON)'
+ printf '%s\n' '```json'
+ printf '%s\n' "${report_json}"
+ printf '%s\n' '```'
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+
+ if [ "${#missing_required[@]}" -gt 0 ]; then
+ {
+ printf '%s\n' '### Missing required repo artifacts'
+ for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done
+ printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.'
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ exit 1
+ fi
+
+ if [ "${#missing_optional[@]}" -gt 0 ]; then
+ {
+ printf '%s\n' '### Missing optional repo artifacts'
+ for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ fi
+
+ if [ "${#content_warnings[@]}" -gt 0 ]; then
+ {
+ printf '%s\n' '### Repo content warnings'
+ for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ fi
+
+ # -- Joomla-specific checks --
+ joomla_findings=()
+
+ MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '/dev/null | head -1 || true)"
+ if [ -z "${MANIFEST}" ]; then
+ joomla_findings+=("Joomla XML manifest not found (no *.xml with tag)")
+ else
+ if ! grep -qP '' "${MANIFEST}"; then
+ joomla_findings+=("XML manifest: tag missing")
+ fi
+ if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then
+ joomla_findings+=("XML manifest: type attribute missing or invalid")
+ fi
+ if ! grep -qP '' "${MANIFEST}"; then
+ joomla_findings+=("XML manifest: tag missing")
+ fi
+ if ! grep -qP '' "${MANIFEST}"; then
+ joomla_findings+=("XML manifest: tag missing")
+ fi
+ if ! grep -qP ' missing (required for Joomla 5+)")
+ fi
+ fi
+
+ INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)"
+ if [ "${INI_COUNT}" -eq 0 ]; then
+ joomla_findings+=("No .ini language files found")
+ fi
+
+ if [ ! -f 'updates.xml' ]; then
+ joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
+ fi
+
+ if [ -n "${SOURCE_DIR}" ]; then
+ INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
+ for dir in "${INDEX_DIRS[@]}"; do
+ if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
+ joomla_findings+=("${dir}/index.html missing (directory listing protection)")
+ fi
+ done
+ fi
+
+ if [ "${#joomla_findings[@]}" -gt 0 ]; then
+ {
+ printf '%s\n' '### Joomla extension checks'
+ printf '%s\n' '| Check | Status |'
+ printf '%s\n' '|---|---|'
+ for f in "${joomla_findings[@]}"; do
+ printf '%s\n' "| ${f} | Warning |"
+ done
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ else
+ {
+ printf '%s\n' '### Joomla extension checks'
+ printf '%s\n' 'All Joomla-specific checks passed.'
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ fi
+
+ extended_enabled="${EXTENDED_CHECKS:-true}"
+ extended_findings=()
+
+ if [ "${extended_enabled}" = 'true' ]; then
+ if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then
+ :
+ else
+ extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)")
+ fi
+
+ if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then
+ bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)"
+ if [ -n "${bad_refs}" ]; then
+ extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt")
+ {
+ printf '%s\n' '### Workflow pinning advisory'
+ printf '%s\n' 'Found uses: entries pinned to main/master:'
+ printf '%s\n' '```'
+ printf '%s\n' "${bad_refs}"
+ printf '%s\n' '```'
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ fi
+ fi
+
+ if [ -f "${DOCS_INDEX}" ]; then
+ missing_links=""
+ while IFS= read -r docline; do
+ for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do
+ case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac
+ linkpath="${link%%#*}"
+ linkpath="${linkpath%%\?*}"
+ [ -z "$linkpath" ] && continue
+ if [ "${linkpath:0:1}" = "/" ]; then
+ testpath="${linkpath#/}"
+ else
+ testpath="$(dirname "${DOCS_INDEX}")/${linkpath}"
+ fi
+ [ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} "
+ done
+ done < "${DOCS_INDEX}"
+ if [ -n "${missing_links}" ]; then
+ extended_findings+=("docs/docs-index.md contains broken relative links")
+ {
+ printf '%s\n' '### Docs index link integrity'
+ printf '%s\n' 'Broken relative links:'
+ for bl in ${missing_links}; do
+ printf '%s\n' "- ${bl}"
+ done
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ fi
+ fi
+
+ if [ -d "${SCRIPT_DIR}" ]; then
+ if ! command -v shellcheck >/dev/null 2>&1; then
+ sudo apt-get update -qq
+ sudo apt-get install -y shellcheck >/dev/null
+ fi
+
+ sc_out=''
+ while IFS= read -r shf; do
+ [ -z "${shf}" ] && continue
+ out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)"
+ if [ -n "${out_one}" ]; then
+ sc_out="${sc_out}${out_one}\n"
+ fi
+ done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort)
+
+ if [ -n "${sc_out}" ]; then
+ extended_findings+=("ShellCheck warnings detected (advisory)")
+ sc_head="$(printf '%s' "${sc_out}" | head -n 200)"
+ {
+ printf '%s\n' '### ShellCheck (advisory)'
+ printf '%s\n' '```'
+ printf '%s\n' "${sc_head}"
+ printf '%s\n' '```'
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ fi
+ fi
+
+ spdx_missing=()
+ IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}"
+ spdx_args=()
+ for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done
+
+ while IFS= read -r f; do
+ [ -z "${f}" ] && continue
+ if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then
+ spdx_missing+=("${f}")
+ fi
+ done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true)
+
+ if [ "${#spdx_missing[@]}" -gt 0 ]; then
+ extended_findings+=("SPDX header missing in some tracked files (advisory)")
+ {
+ printf '%s\n' '### SPDX header advisory'
+ printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):'
+ for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ fi
+
+ stale_cutoff_days=180
+ stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)"
+ if [ -n "${stale_branches}" ]; then
+ extended_findings+=("Stale remote branches detected (advisory)")
+ {
+ printf '%s\n' '### Git hygiene advisory'
+ printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):"
+ while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}"
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ fi
+ fi
+
+ {
+ printf '%s\n' '### Guardrails coverage matrix'
+ printf '%s\n' '| Domain | Status | Notes |'
+ printf '%s\n' '|---|---|---|'
+ printf '%s\n' '| Access control | OK | Admin-only execution gate |'
+ printf '%s\n' '| Release policy | N/A | Releases handled by MokoGitea |'
+ printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
+ printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
+ printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
+ if [ "${extended_enabled}" = 'true' ]; then
+ if [ "${#extended_findings[@]}" -gt 0 ]; then
+ printf '%s\n' '| Extended checks | Warning | See extended findings below |'
+ else
+ printf '%s\n' '| Extended checks | OK | No findings |'
+ fi
+ else
+ printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |'
+ fi
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+
+ if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then
+ {
+ printf '%s\n' '### Extended findings (advisory)'
+ for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ fi
+
+ printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
+
+
+ site-health:
+ name: Site Health
+ runs-on: ubuntu-latest
+ if: github.event_name == 'workflow_dispatch'
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.3'
+
+ - name: Uptime check
+ if: env.URLS != ''
+ run: |
+ echo "$URLS" > /tmp/urls.txt
+ php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down"
+ rm -f /tmp/urls.txt
+ env:
+ URLS: ${{ vars.MONITORED_URLS }}
+
+ - name: SSL certificate check
+ if: env.DOMAINS != ''
+ run: |
+ echo "$DOMAINS" > /tmp/domains.txt
+ php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon"
+ rm -f /tmp/domains.txt
+ env:
+ DOMAINS: ${{ vars.MONITORED_DOMAINS }}
+
+ - name: Summary
+ if: always()
+ run: |
+ echo "### Site Health" >> $GITHUB_STEP_SUMMARY
+ echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY
+
+ # ═══════════════════════════════════════════════════════════════════════
+ # Issue Reporter — file issues for failed gates
+ # ═══════════════════════════════════════════════════════════════════════
+ report-issues:
+ name: "Report Issues"
+ runs-on: ubuntu-latest
+ needs: [access_check, scripts_governance, repo_health]
+ if: >-
+ always() &&
+ (needs.scripts_governance.result == 'failure' ||
+ needs.repo_health.result == 'failure')
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ sparse-checkout: automation/ci-issue-reporter.sh
+ sparse-checkout-cone-mode: false
+
+ - name: "File issues for failed gates"
+ env:
+ GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
+ GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
+ run: |
+ chmod +x automation/ci-issue-reporter.sh
+ REPORTER="./automation/ci-issue-reporter.sh"
+ WF="Repo Health"
+
+ report_gate() {
+ local gate="$1" result="$2" details="$3"
+ if [ "$result" = "failure" ]; then
+ "$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error
+ fi
+ }
+
+ report_gate "Scripts Governance" \
+ "${{ needs.scripts_governance.result }}" \
+ "Scripts directory policy violations detected. Review required and allowed directories."
+
+ report_gate "Repository Health" \
+ "${{ needs.repo_health.result }}" \
+ "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
From a6d843fd9b3d7b5f755e4dec8e5a428d827dd685 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Thu, 4 Jun 2026 13:57:48 +0000
Subject: [PATCH 06/14] chore: sync updates.xml from development [skip ci]
---
updates.xml | 27 +++++++++++++++++++++++----
1 file changed, 23 insertions(+), 4 deletions(-)
diff --git a/updates.xml b/updates.xml
index 8dd2da5..dd2a367 100644
--- a/updates.xml
+++ b/updates.xml
@@ -1,7 +1,7 @@
@@ -11,17 +11,36 @@
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/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
+ 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
+
+
From 01139c6fd4342de9dba8ec56df95717a4cca5c25 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Thu, 4 Jun 2026 14:24:01 +0000
Subject: [PATCH 07/14] chore: sync .mokogitea/workflows/auto-release.yml from
moko-platform [skip ci]
---
.mokogitea/workflows/auto-release.yml | 568 +++++++++++++-------------
1 file changed, 285 insertions(+), 283 deletions(-)
diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml
index 78dec4b..44a2d64 100644
--- a/.mokogitea/workflows/auto-release.yml
+++ b/.mokogitea/workflows/auto-release.yml
@@ -1,283 +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 }}"
-
- - 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: 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 }}"
-
- # -- 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
From 003e9617a0a200a6b31519dd7efa5585453b05cf Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Thu, 4 Jun 2026 14:34:14 +0000
Subject: [PATCH 08/14] 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 334846c..7834c45 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
From eb5513f4af1dc0340e37d97b3404d29f32c26505 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Thu, 4 Jun 2026 15:19:46 +0000
Subject: [PATCH 09/14] 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."
From 03b53d937ad2befe67745ec65173be70844cf616 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Thu, 4 Jun 2026 15:27:06 +0000
Subject: [PATCH 10/14] 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 dd2a367..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
-
- c3163a6abdd8cfb63982e40ce8750a8743bd8e153d8537c144ccc534ddaa2bf3
- 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
-
-
-
From 8fb3262eb34ab62496562ab21f225a31d1580252 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Thu, 4 Jun 2026 15:32:55 +0000
Subject: [PATCH 11/14] 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
From 6aebfc195316660c25ee7b189d581e34ded8e230 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Thu, 4 Jun 2026 15:41:45 +0000
Subject: [PATCH 12/14] 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 }}"
From 02d8bfb089a94c2492db5b8d52823d3562a16adf Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Thu, 4 Jun 2026 15:58:52 +0000
Subject: [PATCH 13/14] 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)
From af82b46fe0d656206f5ef4046e86ef4bfb0421ae Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Thu, 4 Jun 2026 22:02:32 +0000
Subject: [PATCH 14/14] 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 7834c45..ec51a29 100644
--- a/src/pkg_mokobackup.xml
+++ b/src/pkg_mokobackup.xml
@@ -34,4 +34,6 @@
https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/updates.xml
+
+ true