Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d957022fc1 | |||
| 82d5beb0f0 | |||
| bf202a4a50 | |||
| 9968c81660 | |||
| 6802b256d0 | |||
| 7ebd2e6385 | |||
| d0ea5e43c0 | |||
| e0ef643c04 | |||
| 7b9ea92faa | |||
| 9f628201ad | |||
| 86f341bd72 | |||
| 4cdf7234d9 | |||
| e19766cf00 | |||
| 368d5eb2ed | |||
| 6c9538e36c | |||
| 08ec94953a | |||
| 9b30244361 | |||
| 121219a810 | |||
| 6cb8751f89 | |||
| 3b118d7b49 | |||
| fccd80da3c | |||
| 58fb87bb05 | |||
| 9680004f78 | |||
| 86faa5086f | |||
| 696c5a45c4 | |||
| cbdbdcd553 | |||
| a5bab02fd1 | |||
| c9a2cfa15c | |||
| af76a8f2cb | |||
| 1001df963e | |||
| ed7d356e99 | |||
| 396b60902a | |||
| c7d55e7fdd | |||
| ad7fc53fb9 | |||
| ad6a7e90e9 | |||
| 041111b8ab | |||
| b65dd95dc4 | |||
| 8428f29058 | |||
| 9e6cebb9aa | |||
| 595ee55499 | |||
| 6e05b6aea0 | |||
| 35bc550e0c | |||
| c6b52100de | |||
| c9470a45fe | |||
| 7962b370fa | |||
| 0e4b0d55cb | |||
| 2f7cbb57f9 | |||
| 006b47bbdf | |||
| 0cc2fea1bc | |||
| 5a9e8d86a1 | |||
| 0cdd186eae | |||
| 5e77328b66 | |||
| ac05b97262 | |||
| b1391c6d03 | |||
| ec75322bed | |||
| 6a6a8d7c0c | |||
| 3d059a0ad8 | |||
| 28b4916d94 | |||
| 2addc3bdb4 | |||
| b573c6e762 | |||
| 0a1e788f00 | |||
| b051e49320 | |||
| 4b0c024299 | |||
| 951221bcc0 | |||
| 3d357d1bd3 | |||
| 9062a3141e | |||
| 41d8ce7a2a | |||
| 0e9703f8b0 | |||
| 8abadc1709 | |||
| 706f3137b7 | |||
| d28877fc0c | |||
| 43a7432f08 | |||
| a12c6ece42 | |||
| adba742cfe | |||
| 99ab99dfe2 | |||
| 0e4e329770 | |||
| 35f79bc53e | |||
| 9ace43be1a |
@@ -9,7 +9,7 @@
|
||||
<display-name>Template - MokoOnyx</display-name>
|
||||
<org>MokoConsulting</org>
|
||||
<description>MokoOnyx - Joomla site template (successor to MokoCassiopeia)</description>
|
||||
<version>02.18.03</version>
|
||||
<version>02.19.06</version>
|
||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||
</identity>
|
||||
<governance>
|
||||
|
||||
@@ -102,13 +102,14 @@ jobs:
|
||||
run: |
|
||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||
--path . --stability rc --bump minor --branch rc \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
--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 + lesser stream releases built, updates.xml synced" >> $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:
|
||||
@@ -167,7 +168,8 @@ jobs:
|
||||
run: |
|
||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||
--path . --stability stable --bump minor --branch main \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--skip-update-stream
|
||||
|
||||
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||
- name: "Step 9: Mirror release to GitHub"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Automation
|
||||
# VERSION: 02.18.03
|
||||
# VERSION: 02.19.06
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
@@ -105,6 +105,19 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for merge conflict markers
|
||||
run: |
|
||||
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||
if [ -n "$CONFLICTS" ]; then
|
||||
echo "::error::Merge conflict markers found in source files"
|
||||
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
echo "No conflict markers found"
|
||||
|
||||
- name: Detect platform
|
||||
id: platform
|
||||
run: |
|
||||
@@ -134,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 '<extension' {} \; 2>/dev/null | head -1)
|
||||
[ -z "$MANIFEST" ] && exit 0
|
||||
MANIFEST_DIR=$(dirname "$MANIFEST")
|
||||
|
||||
# Check scriptfile exists if declared
|
||||
SCRIPTFILE=$(sed -n 's/.*<scriptfile>\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null)
|
||||
if [ -n "$SCRIPTFILE" ]; then
|
||||
if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then
|
||||
echo "::error::Manifest declares <scriptfile>${SCRIPTFILE}</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 }}"
|
||||
@@ -151,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)
|
||||
@@ -183,6 +295,138 @@ jobs:
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Validate Joomla language files
|
||||
if: steps.platform.outputs.platform == 'joomla'
|
||||
run: |
|
||||
ERRORS=0
|
||||
WARNINGS=0
|
||||
|
||||
# Require both en-GB and en-US language directories
|
||||
LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||
if [ -z "$LANG_ROOT" ]; then
|
||||
echo "No language/ directory found — skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -d "$LANG_ROOT/en-GB" ]; then
|
||||
echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
if [ ! -d "$LANG_ROOT/en-US" ]; then
|
||||
echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
|
||||
# Check that en-GB and en-US have matching .ini files
|
||||
if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then
|
||||
for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do
|
||||
[ ! -f "$GB_INI" ] && continue
|
||||
US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")"
|
||||
if [ ! -f "$US_INI" ]; then
|
||||
echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
done
|
||||
for US_INI in "$LANG_ROOT/en-US"/*.ini; do
|
||||
[ ! -f "$US_INI" ] && continue
|
||||
GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")"
|
||||
if [ ! -f "$GB_INI" ]; then
|
||||
echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Find all .ini language files
|
||||
INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null)
|
||||
if [ -z "$INI_FILES" ]; then
|
||||
echo "No .ini language files found"
|
||||
[ "$ERRORS" -gt 0 ] && exit 1
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found $(echo "$INI_FILES" | wc -l) language file(s)"
|
||||
|
||||
for FILE in $INI_FILES; do
|
||||
FNAME=$(basename "$FILE")
|
||||
LINENUM=0
|
||||
SEEN_KEYS=""
|
||||
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
LINENUM=$((LINENUM + 1))
|
||||
|
||||
# Skip empty lines and comments
|
||||
[ -z "$line" ] && continue
|
||||
echo "$line" | grep -qE '^\s*;' && continue
|
||||
echo "$line" | grep -qE '^\s*$' && continue
|
||||
|
||||
# Must match KEY="VALUE" format
|
||||
if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then
|
||||
echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Extract key and check for duplicates
|
||||
KEY=$(echo "$line" | sed 's/=.*//')
|
||||
if echo "$SEEN_KEYS" | grep -qx "$KEY"; then
|
||||
echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
SEEN_KEYS="${SEEN_KEYS}
|
||||
${KEY}"
|
||||
done < "$FILE"
|
||||
|
||||
echo " ${FILE}: checked ${LINENUM} lines"
|
||||
done
|
||||
|
||||
# Cross-check en-GB vs en-US key consistency
|
||||
GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||
US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||
|
||||
if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then
|
||||
for GB_FILE in "$GB_DIR"/*.ini; do
|
||||
[ ! -f "$GB_FILE" ] && continue
|
||||
FNAME=$(basename "$GB_FILE")
|
||||
US_FILE="$US_DIR/$FNAME"
|
||||
[ ! -f "$US_FILE" ] && continue
|
||||
|
||||
GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort)
|
||||
US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort)
|
||||
|
||||
# Keys in en-GB but not en-US
|
||||
MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
|
||||
if [ -n "$MISSING_US" ]; then
|
||||
echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:"
|
||||
echo "$MISSING_US" | while read -r k; do echo " - $k"; done
|
||||
WARNINGS=$((WARNINGS + 1))
|
||||
fi
|
||||
|
||||
# Keys in en-US but not en-GB
|
||||
MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
|
||||
if [ -n "$MISSING_GB" ]; then
|
||||
echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:"
|
||||
echo "$MISSING_GB" | while read -r k; do echo " - $k"; done
|
||||
WARNINGS=$((WARNINGS + 1))
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
{
|
||||
echo "### Language File Validation"
|
||||
echo "| Metric | Count |"
|
||||
echo "|---|---|"
|
||||
echo "| Files checked | $(echo "$INI_FILES" | wc -l) |"
|
||||
echo "| Errors | ${ERRORS} |"
|
||||
echo "| Warnings | ${WARNINGS} |"
|
||||
} >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [ "$ERRORS" -gt 0 ]; then
|
||||
echo "::error::Language validation failed with ${ERRORS} error(s)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Language files: OK (${WARNINGS} warning(s))"
|
||||
|
||||
- name: Check changelog has unreleased entry
|
||||
run: |
|
||||
if [ ! -f "CHANGELOG.md" ]; then
|
||||
|
||||
@@ -17,6 +17,10 @@ on:
|
||||
types: [closed]
|
||||
branches:
|
||||
- dev
|
||||
pull_request_target:
|
||||
types: [synchronize, opened, reopened]
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
stability:
|
||||
@@ -43,7 +47,8 @@ jobs:
|
||||
runs-on: release
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') ||
|
||||
(github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -51,6 +56,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
@@ -60,7 +66,7 @@ jobs:
|
||||
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
|
||||
# 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" \
|
||||
@@ -76,7 +82,12 @@ jobs:
|
||||
- name: Resolve metadata and bump version
|
||||
id: meta
|
||||
run: |
|
||||
STABILITY="${{ inputs.stability || 'development' }}"
|
||||
# Auto-detect stability: RC for PRs targeting main, else use input or default to development
|
||||
if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then
|
||||
STABILITY="release-candidate"
|
||||
else
|
||||
STABILITY="${{ inputs.stability || 'development' }}"
|
||||
fi
|
||||
|
||||
case "$STABILITY" in
|
||||
development) SUFFIX="-dev"; TAG="development" ;;
|
||||
@@ -144,6 +155,21 @@ jobs:
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GITEA_REPO}" --branch dev --prerelease
|
||||
|
||||
- name: Ensure prerelease flag
|
||||
run: |
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
# Get release ID by tag and force prerelease=true
|
||||
RELEASE_ID=$(curl -s "${API_BASE}/releases/tags/${TAG}" \
|
||||
-H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" | jq -r '.id // empty')
|
||||
if [ -n "$RELEASE_ID" ]; then
|
||||
curl -s -X PATCH "${API_BASE}/releases/${RELEASE_ID}" \
|
||||
-H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"prerelease": true}'
|
||||
echo "Marked release ${TAG} (id=${RELEASE_ID}) as prerelease"
|
||||
fi
|
||||
|
||||
- name: Build package and upload
|
||||
id: package
|
||||
run: |
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
|
||||
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
|
||||
# ============================================================================
|
||||
|
||||
name: "Generic: Repo Health"
|
||||
@@ -24,13 +24,12 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
profile:
|
||||
description: 'Validation profile: all, release, scripts, or repo'
|
||||
description: 'Validation profile: all, scripts, or repo'
|
||||
required: true
|
||||
default: all
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- release
|
||||
- scripts
|
||||
- repo
|
||||
pull_request:
|
||||
@@ -40,10 +39,6 @@ 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
|
||||
@@ -138,101 +133,6 @@ jobs:
|
||||
printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
|
||||
exit 1
|
||||
|
||||
release_config:
|
||||
name: Release configuration
|
||||
needs: access_check
|
||||
if: ${{ needs.access_check.outputs.allowed == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Guardrails release vars
|
||||
env:
|
||||
PROFILE_RAW: ${{ github.event.inputs.profile }}
|
||||
RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }}
|
||||
DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
profile="${PROFILE_RAW:-all}"
|
||||
case "${profile}" in
|
||||
all|release|scripts|repo) ;;
|
||||
*)
|
||||
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then
|
||||
{
|
||||
printf '%s\n' '### Release configuration (Repository Variables)'
|
||||
printf '%s\n' "Profile: ${profile}"
|
||||
printf '%s\n' 'Status: SKIPPED'
|
||||
printf '%s\n' 'Reason: profile excludes release validation'
|
||||
printf '\n'
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}"
|
||||
IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}"
|
||||
|
||||
missing=()
|
||||
missing_optional=()
|
||||
|
||||
for k in "${required[@]}"; do
|
||||
v="${!k:-}"
|
||||
[ -z "${v}" ] && missing+=("${k}")
|
||||
done
|
||||
|
||||
for k in "${optional[@]}"; do
|
||||
v="${!k:-}"
|
||||
[ -z "${v}" ] && missing_optional+=("${k}")
|
||||
done
|
||||
|
||||
{
|
||||
printf '%s\n' '### Release configuration (Repository Variables)'
|
||||
printf '%s\n' "Profile: ${profile}"
|
||||
printf '%s\n' '| Variable | Status |'
|
||||
printf '%s\n' '|---|---|'
|
||||
printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |"
|
||||
printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |"
|
||||
printf '\n'
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
if [ "${#missing_optional[@]}" -gt 0 ]; then
|
||||
{
|
||||
printf '%s\n' '### Missing optional repository variables'
|
||||
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
|
||||
printf '\n'
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
|
||||
if [ "${#missing[@]}" -gt 0 ]; then
|
||||
{
|
||||
printf '%s\n' '### Missing required repository variables'
|
||||
for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done
|
||||
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.'
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
{
|
||||
printf '%s\n' '### Repository variables validation result'
|
||||
printf '%s\n' 'Status: OK'
|
||||
printf '%s\n' 'All required repository variables present.'
|
||||
printf '%s\n' ''
|
||||
printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.'
|
||||
printf '\n'
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
scripts_governance:
|
||||
name: Scripts governance
|
||||
needs: access_check
|
||||
@@ -256,14 +156,14 @@ jobs:
|
||||
|
||||
profile="${PROFILE_RAW:-all}"
|
||||
case "${profile}" in
|
||||
all|release|scripts|repo) ;;
|
||||
all|scripts|repo) ;;
|
||||
*)
|
||||
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then
|
||||
if [ "${profile}" = 'repo' ]; then
|
||||
{
|
||||
printf '%s\n' '### Scripts governance'
|
||||
printf '%s\n' "Profile: ${profile}"
|
||||
@@ -370,14 +270,14 @@ jobs:
|
||||
|
||||
profile="${PROFILE_RAW:-all}"
|
||||
case "${profile}" in
|
||||
all|release|scripts|repo) ;;
|
||||
all|scripts|repo) ;;
|
||||
*)
|
||||
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then
|
||||
if [ "${profile}" = 'scripts' ]; then
|
||||
{
|
||||
printf '%s\n' '### Repository health'
|
||||
printf '%s\n' "Profile: ${profile}"
|
||||
@@ -704,7 +604,7 @@ jobs:
|
||||
printf '%s\n' '| Domain | Status | Notes |'
|
||||
printf '%s\n' '|---|---|---|'
|
||||
printf '%s\n' '| Access control | OK | Admin-only execution gate |'
|
||||
printf '%s\n' '| Release variables | OK | Repository variables validation |'
|
||||
printf '%s\n' '| Release policy | N/A | Releases handled by MokoGitea |'
|
||||
printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
|
||||
printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
|
||||
printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
|
||||
@@ -773,11 +673,10 @@ jobs:
|
||||
report-issues:
|
||||
name: "Report Issues"
|
||||
runs-on: ubuntu-latest
|
||||
needs: [access_check, release_config, scripts_governance, repo_health]
|
||||
needs: [access_check, scripts_governance, repo_health]
|
||||
if: >-
|
||||
always() &&
|
||||
(needs.release_config.result == 'failure' ||
|
||||
needs.scripts_governance.result == 'failure' ||
|
||||
(needs.scripts_governance.result == 'failure' ||
|
||||
needs.repo_health.result == 'failure')
|
||||
|
||||
steps:
|
||||
@@ -803,10 +702,6 @@ jobs:
|
||||
fi
|
||||
}
|
||||
|
||||
report_gate "Release Configuration" \
|
||||
"${{ needs.release_config.result }}" \
|
||||
"Required repository variables are missing (RS_FTP_PATH_SUFFIX). Check repository settings."
|
||||
|
||||
report_gate "Scripts Governance" \
|
||||
"${{ needs.scripts_governance.result }}" \
|
||||
"Scripts directory policy violations detected. Review required and allowed directories."
|
||||
@@ -814,4 +709,3 @@ jobs:
|
||||
report_gate "Repository Health" \
|
||||
"${{ needs.repo_health.result }}" \
|
||||
"Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
|
||||
|
||||
|
||||
@@ -1,312 +0,0 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /templates/workflows/update-server.yml
|
||||
# VERSION: 05.00.00
|
||||
# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
|
||||
#
|
||||
# Thin wrapper around moko-platform CLI tools.
|
||||
# Builds packages, updates updates.xml, and optionally deploys via SFTP.
|
||||
#
|
||||
# Joomla filters update entries by the user's "Minimum Stability" setting.
|
||||
|
||||
name: "Update Server"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'dev'
|
||||
- 'dev/**'
|
||||
- 'alpha/**'
|
||||
- 'beta/**'
|
||||
- 'rc/**'
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'htdocs/**'
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches:
|
||||
- 'dev'
|
||||
- 'dev/**'
|
||||
- 'alpha/**'
|
||||
- 'beta/**'
|
||||
- 'rc/**'
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'htdocs/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
stability:
|
||||
description: 'Stability tag'
|
||||
required: true
|
||||
default: 'development'
|
||||
type: choice
|
||||
options:
|
||||
- development
|
||||
- alpha
|
||||
- beta
|
||||
- rc
|
||||
- stable
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-xml:
|
||||
name: Update Server
|
||||
runs-on: release
|
||||
if: >-
|
||||
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
|
||||
run: |
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||
rm -rf /tmp/moko-platform
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||
/tmp/moko-platform 2>/dev/null || true
|
||||
if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
|
||||
cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||
fi
|
||||
echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Detect platform
|
||||
id: platform
|
||||
run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||
|
||||
- name: Resolve stability and bump version
|
||||
id: meta
|
||||
run: |
|
||||
BRANCH="${{ github.ref_name }}"
|
||||
|
||||
# Configure git for bot pushes
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
# Auto-bump patch version
|
||||
php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
|
||||
|
||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0")
|
||||
|
||||
# Strip any existing suffix before applying stability
|
||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||
|
||||
# Determine stability from branch or manual input
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
STABILITY="${{ inputs.stability }}"
|
||||
elif [[ "$BRANCH" == rc/* ]]; then
|
||||
STABILITY="rc"
|
||||
elif [[ "$BRANCH" == beta/* ]]; then
|
||||
STABILITY="beta"
|
||||
elif [[ "$BRANCH" == alpha/* ]]; then
|
||||
STABILITY="alpha"
|
||||
else
|
||||
STABILITY="development"
|
||||
fi
|
||||
|
||||
# Version suffix per stability stream
|
||||
case "$STABILITY" in
|
||||
development) SUFFIX="-dev"; TAG="development" ;;
|
||||
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
||||
beta) SUFFIX="-beta"; TAG="beta" ;;
|
||||
rc) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||
*) SUFFIX=""; TAG="stable" ;;
|
||||
esac
|
||||
|
||||
# Propagate version with stability suffix to all manifest files
|
||||
php ${MOKO_CLI}/version_set_platform.php \
|
||||
--path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
|
||||
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||
|
||||
# Re-read version (now includes suffix from version_set_platform)
|
||||
if [ -n "$SUFFIX" ]; then
|
||||
VERSION="${VERSION}${SUFFIX}"
|
||||
fi
|
||||
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Commit version bump if changed
|
||||
git add -A
|
||||
git diff --cached --quiet || {
|
||||
git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
|
||||
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
||||
git push
|
||||
}
|
||||
|
||||
- name: Create release and upload package
|
||||
id: package
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
# Create or update Gitea release
|
||||
php ${MOKO_CLI}/release_create.php \
|
||||
--path . --version "$VERSION" --tag "$TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
||||
|
||||
# Build package and upload
|
||||
php ${MOKO_CLI}/release_package.php \
|
||||
--path . --version "$VERSION" --tag "$TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GITEA_REPO}" --output /tmp || true
|
||||
|
||||
- name: Update updates.xml
|
||||
if: steps.platform.outputs.platform == 'joomla'
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||
|
||||
if [ ! -f "updates.xml" ]; then
|
||||
echo "No updates.xml — skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SHA_FLAG=""
|
||||
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
|
||||
|
||||
php ${MOKO_CLI}/updates_xml_build.php \
|
||||
--path . --version "${VERSION}" --stability "${STABILITY}" \
|
||||
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
||||
${SHA_FLAG}
|
||||
|
||||
# Commit and push updates.xml
|
||||
git add updates.xml
|
||||
git diff --cached --quiet || {
|
||||
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
||||
git push
|
||||
}
|
||||
|
||||
- name: Sync updates.xml to main
|
||||
if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
|
||||
python3 -c "
|
||||
import base64, json, urllib.request, sys
|
||||
with open('updates.xml', 'rb') as f:
|
||||
content = base64.b64encode(f.read()).decode()
|
||||
payload = json.dumps({
|
||||
'content': content,
|
||||
'sha': '${FILE_SHA}',
|
||||
'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
|
||||
'branch': 'main'
|
||||
}).encode()
|
||||
req = urllib.request.Request(
|
||||
'${API_BASE}/contents/updates.xml',
|
||||
data=payload, method='PUT',
|
||||
headers={
|
||||
'Authorization': 'token ${GITEA_TOKEN}',
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
try:
|
||||
urllib.request.urlopen(req)
|
||||
print('updates.xml synced to main')
|
||||
except Exception as e:
|
||||
print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
|
||||
"
|
||||
fi
|
||||
|
||||
- name: SFTP deploy to dev server
|
||||
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
|
||||
env:
|
||||
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
|
||||
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
|
||||
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
||||
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
|
||||
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
|
||||
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
|
||||
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
||||
run: |
|
||||
# Permission check: admin or maintain role required
|
||||
ACTOR="${{ github.actor }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
|
||||
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
|
||||
case "$PERMISSION" in
|
||||
admin|maintain|write) ;;
|
||||
*)
|
||||
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
|
||||
|
||||
SOURCE_DIR="src"
|
||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||
[ ! -d "$SOURCE_DIR" ] && exit 0
|
||||
|
||||
PORT="${DEV_PORT:-22}"
|
||||
REMOTE="${DEV_PATH%/}"
|
||||
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
|
||||
|
||||
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
||||
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
|
||||
if [ -n "$DEV_KEY" ]; then
|
||||
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
|
||||
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
||||
else
|
||||
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
|
||||
fi
|
||||
|
||||
PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
|
||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
|
||||
php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||
elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
|
||||
php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||
fi
|
||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||
DISPLAY="${{ steps.meta.outputs.display_version }}"
|
||||
echo "## Update Server" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -1,106 +0,0 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# BRIEF: Update MokoOnyx submodule in MokoWaas on main and dev branches
|
||||
|
||||
name: "Update MokoWaas Submodule"
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
WAAS_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||
WAAS_REPO: MokoWaaS
|
||||
# Path where MokoOnyx lives as a submodule inside MokoWaaS
|
||||
SUBMODULE_PATH: src/packages/tpl_mokoonyx
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
update-submodule:
|
||||
name: "Update submodule on ${{ matrix.branch }}"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
branch: [main, dev]
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- name: Get release info
|
||||
id: release
|
||||
run: |
|
||||
TAG="${{ github.event.release.tag_name }}"
|
||||
IS_PRE="${{ github.event.release.prerelease }}"
|
||||
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
||||
echo "prerelease=${IS_PRE}" >> $GITHUB_OUTPUT
|
||||
echo "Release tag: ${TAG} (prerelease: ${IS_PRE})"
|
||||
|
||||
# Skip pre-releases (RC) — only update on stable releases
|
||||
- name: Skip pre-releases
|
||||
if: steps.release.outputs.prerelease == 'true'
|
||||
run: |
|
||||
echo "Skipping pre-release ${TAG} — only stable releases update MokoWaas"
|
||||
exit 0
|
||||
|
||||
- name: Clone MokoWaas
|
||||
if: steps.release.outputs.prerelease != 'true'
|
||||
run: |
|
||||
git clone --branch "${{ matrix.branch }}" --depth 1 --recurse-submodules \
|
||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${WAAS_ORG}/${WAAS_REPO}.git" \
|
||||
waas
|
||||
env:
|
||||
GIT_TERMINAL_PROMPT: 0
|
||||
|
||||
- name: Configure git
|
||||
if: steps.release.outputs.prerelease != 'true'
|
||||
working-directory: waas
|
||||
run: |
|
||||
git config user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config user.name "gitea-actions[bot]"
|
||||
git remote set-url origin \
|
||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${WAAS_ORG}/${WAAS_REPO}.git"
|
||||
|
||||
- name: Update submodule to release tag
|
||||
if: steps.release.outputs.prerelease != 'true'
|
||||
working-directory: waas
|
||||
run: |
|
||||
TAG="${{ steps.release.outputs.tag }}"
|
||||
SUBPATH="${SUBMODULE_PATH}"
|
||||
|
||||
# Verify submodule exists
|
||||
if [ ! -f ".gitmodules" ] || ! grep -q "${SUBPATH}" .gitmodules; then
|
||||
echo "::error::Submodule '${SUBPATH}' not found in .gitmodules on ${{ matrix.branch }}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Fetch the tag in the submodule and update to it
|
||||
cd "${SUBPATH}"
|
||||
git fetch origin "refs/tags/${TAG}:refs/tags/${TAG}" --depth 1
|
||||
git checkout "${TAG}"
|
||||
cd -
|
||||
|
||||
# Stage the submodule pointer change
|
||||
git add "${SUBPATH}"
|
||||
|
||||
# Only commit if there's actually a change
|
||||
if git diff --cached --quiet; then
|
||||
echo "Submodule already at ${TAG} on ${{ matrix.branch }} — nothing to do"
|
||||
else
|
||||
git commit -m "chore: update MokoOnyx submodule to ${TAG}
|
||||
|
||||
Authored-by: Moko Consulting"
|
||||
git push origin "${{ matrix.branch }}"
|
||||
echo "Updated MokoOnyx to ${TAG} on ${{ matrix.branch }}"
|
||||
fi
|
||||
|
||||
- name: Summary
|
||||
if: always() && steps.release.outputs.prerelease != 'true'
|
||||
run: |
|
||||
TAG="${{ steps.release.outputs.tag }}"
|
||||
echo "## MokoWaas Submodule Update (${{ matrix.branch }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Updated \`${SUBMODULE_PATH}\` to \`${TAG}\`" >> $GITHUB_STEP_SUMMARY
|
||||
+15
-30
@@ -8,41 +8,26 @@
|
||||
DEFGROUP: Joomla.Template.Site
|
||||
INGROUP: MokoOnyx.Documentation
|
||||
PATH: ./CHANGELOG.md
|
||||
VERSION: 02.18.03
|
||||
VERSION: 02.19.06
|
||||
BRIEF: Changelog file documenting version history of MokoOnyx
|
||||
-->
|
||||
|
||||
# Changelog — MokoOnyx (VERSION: 02.18.03)
|
||||
# Changelog — MokoOnyx (VERSION: 02.19.06)
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- Strip Joomla-injected `p-2` padding class from Font Awesome icons in all menu overrides (default, mainmenu, horizontal)
|
||||
|
||||
### Changed
|
||||
- Migrated update server URL from raw file endpoint to Gitea Pages
|
||||
- Release workflow no longer manages updates.xml (decoupled to Gitea Pages)
|
||||
- Added conflict-marker guard to PR check and release workflows
|
||||
- Added Joomla language file validation (syntax, duplicates, en-GB/en-US consistency)
|
||||
- Added JEXEC guard, joomla.asset.json, XML well-formedness, and script file CI checks
|
||||
- Removed RS_FTP_PATH_SUFFIX from repo health requirements
|
||||
|
||||
## [02.20.00] --- 2026-06-04
|
||||
|
||||
## [02.18.00] --- 2026-06-02
|
||||
|
||||
## [02.17.00] --- 2026-06-02
|
||||
|
||||
## [02.15.00] --- 2026-05-30
|
||||
## [02.14.00] --- 2026-05-30
|
||||
|
||||
## [02.13.00] --- 2026-05-30
|
||||
|
||||
### Added
|
||||
- Hero image (`hero.jpg`) to template images
|
||||
- `.fa-solid`, `.fa-regular`, `.fa-brands`, `.fa-light` icon margin spacing
|
||||
- `.blog-item .item-image` fixed 250px height with object-fit cover
|
||||
- Hide header on home page option (`hideHeaderHome` template param)
|
||||
- Hide main menu on home page option (`hideMenuHome` template param)
|
||||
- Three distinct menu overrides: mainmenu (collapsible), horizontal (always visible), default (vertical)
|
||||
- `<php_minimum>8.1.0</php_minimum>` to templateDetails.xml
|
||||
- `<changelogurl>` support in updates.xml
|
||||
- CONTRIBUTING.md with universal workflow and version policy
|
||||
|
||||
### Changed
|
||||
- Release pipeline rework: independent update streams, CLI-driven workflows
|
||||
- Version bumps only trigger on `src/` changes (not docs/config)
|
||||
- Branch protection: CI bot only for push, force push disabled
|
||||
- Auto-bump supports dev, rc, feature/*, patch/* branches
|
||||
|
||||
### Fixed
|
||||
- Joomla update loop caused by version mismatch between ZIP and updates.xml
|
||||
- Duplicate hamburger menu on mobile
|
||||
- Stacked version suffixes (-dev-dev-dev)
|
||||
- Template name doubling (Template - Template - MokoOnyx)
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
INGROUP: MokoOnyx.Governance
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
||||
FILE: SECURITY.md
|
||||
VERSION: 02.18.03
|
||||
VERSION: 02.19.06
|
||||
BRIEF: Security policy and vulnerability reporting process for MokoOnyx.
|
||||
PATH: /SECURITY.md
|
||||
NOTE: This policy is process oriented and does not replace secure engineering practices.
|
||||
|
||||
@@ -236,11 +236,11 @@ use Joomla\CMS\Log\Log;
|
||||
|
||||
// Update the update server
|
||||
try {
|
||||
$onyxUpdatesUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml';
|
||||
$onyxUpdatesUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/updates.xml';
|
||||
$query = $db->getQuery(true)
|
||||
->update('#__update_sites')
|
||||
->set($db->quoteName('location') . ' = ' . $db->quote($onyxUpdatesUrl))
|
||||
->set($db->quoteName('name') . ' = ' . $db->quote($newDisplay))
|
||||
->set($db->quoteName('name') . ' = ' . $db->quote('Template - MokoOnyx'))
|
||||
->where($db->quoteName('location') . ' LIKE ' . $db->quote('%MokoCassiopeia%'));
|
||||
$db->setQuery($query)->execute();
|
||||
$n = $db->getAffectedRows();
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: MokoOnyx
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
||||
* PATH: /html/layouts/joomla/module/card.php
|
||||
* VERSION: 02.18.03
|
||||
* VERSION: 02.19.06
|
||||
* BRIEF: Custom card module chrome — renders module titles for all modules
|
||||
*/
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* INGROUP: MokoOnyx.Layouts
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
||||
* PATH: /src/html/layouts/mokoonyx/article-metadata.php
|
||||
* VERSION: 02.18.03
|
||||
* VERSION: 02.19.06
|
||||
* BRIEF: Article metadata footer layout -- renders jcfields grouped by field group
|
||||
*/
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ if ($item->anchor_rel) {
|
||||
$linktype = $item->title;
|
||||
|
||||
if ($item->menu_icon) {
|
||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||
if ($itemParams->get('menu_text', 1)) {
|
||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||
} else {
|
||||
|
||||
@@ -19,6 +19,8 @@ $anchor_css = $item->anchor_css ?: '';
|
||||
$linktype = $item->title;
|
||||
|
||||
if ($item->menu_icon) {
|
||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||
if ($itemParams->get('menu_text', 1)) {
|
||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||
} else {
|
||||
|
||||
@@ -19,6 +19,8 @@ $anchor_css = $item->anchor_css ?: '';
|
||||
$linktype = $item->title;
|
||||
|
||||
if ($item->menu_icon) {
|
||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||
if ($itemParams->get('menu_text', 1)) {
|
||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||
} else {
|
||||
|
||||
@@ -33,6 +33,8 @@ if ($item->anchor_rel) {
|
||||
$linktype = $item->title;
|
||||
|
||||
if ($item->menu_icon) {
|
||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||
if ($itemParams->get('menu_text', 1)) {
|
||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||
} else {
|
||||
|
||||
@@ -33,6 +33,8 @@ if ($item->anchor_rel) {
|
||||
$linktype = $item->title;
|
||||
|
||||
if ($item->menu_icon) {
|
||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||
if ($itemParams->get('menu_text', 1)) {
|
||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||
} else {
|
||||
|
||||
@@ -19,6 +19,8 @@ $anchor_css = $item->anchor_css ?: '';
|
||||
$linktype = $item->title;
|
||||
|
||||
if ($item->menu_icon) {
|
||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||
if ($itemParams->get('menu_text', 1)) {
|
||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||
} else {
|
||||
|
||||
@@ -19,6 +19,8 @@ $anchor_css = $item->anchor_css ?: '';
|
||||
$linktype = $item->title;
|
||||
|
||||
if ($item->menu_icon) {
|
||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||
if ($itemParams->get('menu_text', 1)) {
|
||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||
} else {
|
||||
|
||||
@@ -33,6 +33,8 @@ if ($item->anchor_rel) {
|
||||
$linktype = $item->title;
|
||||
|
||||
if ($item->menu_icon) {
|
||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||
if ($itemParams->get('menu_text', 1)) {
|
||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||
} else {
|
||||
|
||||
@@ -33,6 +33,8 @@ if ($item->anchor_rel) {
|
||||
$linktype = $item->title;
|
||||
|
||||
if ($item->menu_icon) {
|
||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||
// The link is an icon
|
||||
if ($itemParams->get('menu_text', 1)) {
|
||||
// If the link text is to be displayed, the icon is added with aria-hidden
|
||||
|
||||
@@ -19,6 +19,8 @@ $anchor_css = $item->anchor_css ?: '';
|
||||
$linktype = $item->title;
|
||||
|
||||
if ($item->menu_icon) {
|
||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||
// The link is an icon
|
||||
if ($itemParams->get('menu_text', 1)) {
|
||||
// If the link text is to be displayed, the icon is added with aria-hidden
|
||||
|
||||
@@ -19,6 +19,8 @@ $anchor_css = $item->anchor_css ?: '';
|
||||
$linktype = $item->title;
|
||||
|
||||
if ($item->menu_icon) {
|
||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||
// The link is an icon
|
||||
if ($itemParams->get('menu_text', 1)) {
|
||||
// If the link text is to be displayed, the icon is added with aria-hidden
|
||||
|
||||
@@ -33,6 +33,8 @@ if ($item->anchor_rel) {
|
||||
$linktype = $item->title;
|
||||
|
||||
if ($item->menu_icon) {
|
||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||
// The link is an icon
|
||||
if ($itemParams->get('menu_text', 1)) {
|
||||
// If the link text is to be displayed, the icon is added with aria-hidden
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* INGROUP: MokoOnyx.Accessibility
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
||||
* PATH: ./media/css/a11y-high-contrast.css
|
||||
* VERSION: 02.18.03
|
||||
* VERSION: 02.19.06
|
||||
* BRIEF: High-contrast stylesheet for accessibility toolbar
|
||||
*/
|
||||
|
||||
|
||||
+88
-126
@@ -65,8 +65,6 @@ class Tpl_MokoonyxInstallerScript implements InstallerScriptInterface
|
||||
{
|
||||
$this->logMessage('MokoOnyx template updated.');
|
||||
|
||||
$this->migrateUpdateServer();
|
||||
|
||||
$synced = $this->syncCustomVariables($parent);
|
||||
if ($synced > 0) {
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
@@ -96,6 +94,7 @@ class Tpl_MokoonyxInstallerScript implements InstallerScriptInterface
|
||||
$this->clearFaviconStamp();
|
||||
$this->cleanMediaFolder();
|
||||
$this->removeDeletedFiles();
|
||||
$this->removeDuplicateExtensions();
|
||||
$this->lockExtension();
|
||||
}
|
||||
|
||||
@@ -486,161 +485,124 @@ class Tpl_MokoonyxInstallerScript implements InstallerScriptInterface
|
||||
}
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// LICENSE & UPDATE SERVER MIGRATION
|
||||
// ====================================================================
|
||||
|
||||
/**
|
||||
* New update server URL (MokoGitea license system).
|
||||
* Remove duplicate MokoOnyx extension entries from #__extensions.
|
||||
*
|
||||
* TODO: Replace with the actual licensed update server endpoint once
|
||||
* the MokoGitea license system is configured. The URL should
|
||||
* accept a `dlid` query parameter for download-key auth.
|
||||
*
|
||||
* Example: https://updates.mokoconsulting.tech/joomla/mokoonyx/updates.xml
|
||||
* Re-installs or migrations can leave ghost rows. We keep the one
|
||||
* that is locked (the active template) and delete any extras.
|
||||
* Also removes stale MokoCassiopeia entries if present.
|
||||
*/
|
||||
private const NEW_UPDATE_URL = ''; // TODO: set final URL
|
||||
|
||||
/**
|
||||
* Old update server URLs that should be removed during migration.
|
||||
*/
|
||||
private const OLD_UPDATE_URLS = [
|
||||
'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml',
|
||||
];
|
||||
|
||||
/**
|
||||
* Migrate the update server from the old raw-branch URL to the new
|
||||
* MokoGitea license system.
|
||||
*
|
||||
* 1. Find existing update site entries for this template
|
||||
* 2. Remove old entries pointing to the raw-branch URL
|
||||
* 3. (When NEW_UPDATE_URL is set) create the new update site entry
|
||||
* 4. Warn the admin if no download key is configured
|
||||
*
|
||||
* Safe to run multiple times — skips if already migrated.
|
||||
*/
|
||||
private function migrateUpdateServer(): void
|
||||
private function removeDuplicateExtensions(): void
|
||||
{
|
||||
if (empty(self::NEW_UPDATE_URL)) {
|
||||
// Migration not yet active — URL not configured
|
||||
return;
|
||||
}
|
||||
|
||||
$db = Factory::getDbo();
|
||||
|
||||
// Find the extension ID for this template
|
||||
$extId = (int) $db->setQuery(
|
||||
// Find all MokoOnyx template entries
|
||||
$rows = $db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('extension_id')
|
||||
->select(['extension_id', 'locked'])
|
||||
->from('#__extensions')
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote(self::NEW_NAME))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('template'))
|
||||
)->loadResult();
|
||||
->order('locked DESC, extension_id ASC')
|
||||
)->loadObjectList();
|
||||
|
||||
if (!$extId) {
|
||||
return;
|
||||
if (count($rows) > 1) {
|
||||
$keep = (int) $rows[0]->extension_id;
|
||||
$removed = 0;
|
||||
|
||||
for ($i = 1; $i < count($rows); $i++) {
|
||||
$staleId = (int) $rows[$i]->extension_id;
|
||||
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete('#__update_sites_extensions')
|
||||
->where('extension_id = ' . $staleId)
|
||||
)->execute();
|
||||
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete('#__extensions')
|
||||
->where('extension_id = ' . $staleId)
|
||||
)->execute();
|
||||
|
||||
$removed++;
|
||||
}
|
||||
|
||||
if ($removed > 0) {
|
||||
$this->logMessage("Removed {$removed} duplicate MokoOnyx extension(s). Kept ID {$keep}.");
|
||||
}
|
||||
}
|
||||
|
||||
// Check if already migrated (new URL exists)
|
||||
$alreadyMigrated = (int) $db->setQuery(
|
||||
// Remove faulty "template-mokoonyx" duplicate (wrong element name from bad install)
|
||||
$faultyId = (int) $db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from('#__update_sites AS us')
|
||||
->join('INNER', '#__update_sites_extensions AS use ON us.update_site_id = use.update_site_id')
|
||||
->where('use.extension_id = ' . $extId)
|
||||
->where($db->quoteName('us.location') . ' = ' . $db->quote(self::NEW_UPDATE_URL))
|
||||
->select('extension_id')
|
||||
->from('#__extensions')
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('template-mokoonyx'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('template'))
|
||||
)->loadResult();
|
||||
|
||||
if ($alreadyMigrated) {
|
||||
$this->checkDownloadKey($extId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove old update site entries
|
||||
$oldSiteIds = $db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('us.update_site_id')
|
||||
->from('#__update_sites AS us')
|
||||
->join('INNER', '#__update_sites_extensions AS use ON us.update_site_id = use.update_site_id')
|
||||
->where('use.extension_id = ' . $extId)
|
||||
->whereIn($db->quoteName('us.location'), array_map([$db, 'quote'], self::OLD_UPDATE_URLS), true)
|
||||
)->loadColumn();
|
||||
|
||||
if (!empty($oldSiteIds)) {
|
||||
$ids = implode(',', array_map('intval', $oldSiteIds));
|
||||
|
||||
if ($faultyId) {
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete('#__update_sites_extensions')
|
||||
->whereIn('update_site_id', $oldSiteIds)
|
||||
->where('extension_id = ' . $faultyId)
|
||||
)->execute();
|
||||
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete('#__update_sites')
|
||||
->whereIn('update_site_id', $oldSiteIds)
|
||||
->delete('#__extensions')
|
||||
->where('extension_id = ' . $faultyId)
|
||||
)->execute();
|
||||
|
||||
$this->logMessage('Removed ' . count($oldSiteIds) . ' old update site(s).');
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete('#__template_styles')
|
||||
->where($db->quoteName('template') . ' = ' . $db->quote('template-mokoonyx'))
|
||||
)->execute();
|
||||
|
||||
$this->logMessage('Removed faulty template-mokoonyx duplicate extension.');
|
||||
}
|
||||
|
||||
// Create new update site entry
|
||||
$newSite = (object) [
|
||||
'name' => 'MokoOnyx Updates (Licensed)',
|
||||
'type' => 'extension',
|
||||
'location' => self::NEW_UPDATE_URL,
|
||||
'enabled' => 1,
|
||||
'last_check_timestamp' => 0,
|
||||
'extra_query' => '',
|
||||
];
|
||||
|
||||
$db->insertObject('#__update_sites', $newSite, 'update_site_id');
|
||||
$newSiteId = (int) $newSite->update_site_id;
|
||||
|
||||
if ($newSiteId) {
|
||||
$link = (object) [
|
||||
'update_site_id' => $newSiteId,
|
||||
'extension_id' => $extId,
|
||||
];
|
||||
$db->insertObject('#__update_sites_extensions', $link);
|
||||
$this->logMessage('Created new licensed update site (ID: ' . $newSiteId . ').');
|
||||
}
|
||||
|
||||
$this->checkDownloadKey($extId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a download key is configured for this extension's
|
||||
* update site and warn the admin if not.
|
||||
*/
|
||||
private function checkDownloadKey(int $extId): void
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
|
||||
$row = $db->setQuery(
|
||||
// Remove stale MokoCassiopeia if not set as default
|
||||
$oldExt = (int) $db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select(['us.update_site_id', 'us.extra_query'])
|
||||
->from('#__update_sites AS us')
|
||||
->join('INNER', '#__update_sites_extensions AS use ON us.update_site_id = use.update_site_id')
|
||||
->where('use.extension_id = ' . $extId)
|
||||
->where($db->quoteName('us.location') . ' = ' . $db->quote(self::NEW_UPDATE_URL))
|
||||
)->loadObject();
|
||||
->select('extension_id')
|
||||
->from('#__extensions')
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('template'))
|
||||
)->loadResult();
|
||||
|
||||
if (!$row) {
|
||||
return;
|
||||
}
|
||||
if ($oldExt) {
|
||||
$isDefault = (int) $db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from('#__template_styles')
|
||||
->where($db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME))
|
||||
->where($db->quoteName('home') . ' = 1')
|
||||
)->loadResult();
|
||||
|
||||
// Joomla stores the download key in extra_query as "dlid=XXXXX"
|
||||
if (empty($row->extra_query) || strpos($row->extra_query, 'dlid=') === false) {
|
||||
$editUrl = 'index.php?option=com_installer&view=updatesites&task=updatesite.edit&update_site_id='
|
||||
. (int) $row->update_site_id;
|
||||
if ($isDefault === 0) {
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete('#__update_sites_extensions')
|
||||
->where('extension_id = ' . $oldExt)
|
||||
)->execute();
|
||||
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
'<strong>MokoOnyx — Download key required.</strong><br>'
|
||||
. 'A download key is needed to receive updates. '
|
||||
. '<a href="' . $editUrl . '">Enter your download key here</a>.',
|
||||
'warning'
|
||||
);
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete('#__extensions')
|
||||
->where('extension_id = ' . $oldExt)
|
||||
)->execute();
|
||||
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete('#__template_styles')
|
||||
->where($db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME))
|
||||
)->execute();
|
||||
|
||||
$this->logMessage('Removed stale MokoCassiopeia extension and styles.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,12 +31,11 @@
|
||||
-->
|
||||
<extension type="template" client="site" method="upgrade">
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="MokoOnyx Update Server (MokoGitea)">
|
||||
https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml
|
||||
</server>
|
||||
<server type="extension" name="Template - MokoOnyx">https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/updates.xml</server>
|
||||
</updateservers>
|
||||
<dlid prefix="dlid=" suffix=""/>
|
||||
<name>mokoonyx</name>
|
||||
<version>02.18.03-dev</version>
|
||||
<version>02.19.06-dev</version>
|
||||
<scriptfile>script.php</scriptfile>
|
||||
<creationDate>2026-05-16</creationDate>
|
||||
<author>Jonathan Miller || Moko Consulting</author>
|
||||
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
VERSION: 02.20.00
|
||||
-->
|
||||
|
||||
<updates>
|
||||
<update>
|
||||
<name>Template - MokoOnyx</name>
|
||||
<description>Template - MokoOnyx stable build.</description>
|
||||
<element>mokoonyx</element>
|
||||
<type>template</type>
|
||||
<client>site</client>
|
||||
<version>02.20.00</version>
|
||||
<creationDate>2026-06-04</creationDate>
|
||||
<infourl title='Template - MokoOnyx'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/stable</infourl>
|
||||
<downloads>
|
||||
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/stable/tpl_mokoonyx-02.20.00.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>6b0397fce0e1f9e15f3318a200ad4679a0777940c6cfa3a703b875ca14f9979a</sha256>
|
||||
<tags><tag>stable</tag></tags>
|
||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
<targetplatform name="joomla" version="(5|6)\..*" />
|
||||
<php_minimum>8.1.0</php_minimum>
|
||||
</update>
|
||||
</updates>
|
||||
Reference in New Issue
Block a user