Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
748cd855f5 | ||
|
|
30660cfee8 | ||
|
|
5bd66387e3 | ||
|
|
55d6a3ff7c | ||
|
|
7c43a9fe64 | ||
|
|
b7ab26c999 | ||
|
|
b965070f3f | ||
|
|
084245e9c1 | ||
|
|
602d3f69bc | ||
|
|
61aa54df78 | ||
|
|
8430ea6804 | ||
|
|
ad1070be03 | ||
|
|
8db0a4fa88 | ||
|
|
fb8d91c716 | ||
|
|
455f5825db | ||
|
|
c9222b4c31 | ||
|
|
314a4683ae | ||
|
|
c1e6a5f42d | ||
|
|
44b823d4f7 | ||
| a581da7bdc | |||
|
|
67f6b61bf2 |
517
.gitea/workflows/release.yml
Normal file
517
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,517 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# This file is part of a Moko Consulting project.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoOnyx.Release
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
||||||
|
# PATH: /.gitea/workflows/release.yml
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Joomla release — build ZIP, publish to Gitea, mirror to GitHub
|
||||||
|
|
||||||
|
name: Create Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '[0-9][0-9].[0-9][0-9].[0-9][0-9]'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Release version (e.g., 01.00.00)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
prerelease:
|
||||||
|
description: 'Mark as pre-release'
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
stability:
|
||||||
|
description: 'Stability tag (development, alpha, beta, rc, stable)'
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: 'development'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_URL: https://git.mokoconsulting.tech
|
||||||
|
GITEA_ORG: MokoConsulting
|
||||||
|
GITEA_REPO: MokoOnyx
|
||||||
|
EXT_ELEMENT: mokoonyx
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build Release Package
|
||||||
|
runs-on: release
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
run: |
|
||||||
|
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
|
||||||
|
php -v
|
||||||
|
composer --version
|
||||||
|
|
||||||
|
- name: Get version and stability
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||||
|
VERSION="${{ inputs.version }}"
|
||||||
|
STABILITY="${{ inputs.stability }}"
|
||||||
|
PRERELEASE="${{ inputs.prerelease }}"
|
||||||
|
else
|
||||||
|
VERSION=${GITHUB_REF#refs/tags/}
|
||||||
|
STABILITY="stable"
|
||||||
|
PRERELEASE="false"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Derive suffix and tag from stability
|
||||||
|
case "$STABILITY" in
|
||||||
|
development) SUFFIX="-dev"; TAG_NAME="development" ;;
|
||||||
|
alpha) SUFFIX="-alpha"; TAG_NAME="alpha" ;;
|
||||||
|
beta) SUFFIX="-beta"; TAG_NAME="beta" ;;
|
||||||
|
rc) SUFFIX="-rc"; TAG_NAME="release-candidate" ;;
|
||||||
|
stable) SUFFIX=""; TAG_NAME="v${VERSION%%.*}" ;;
|
||||||
|
*) SUFFIX="-dev"; TAG_NAME="development" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
|
||||||
|
|
||||||
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "prerelease=${PRERELEASE}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag_name=${TAG_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Building: ${ZIP_NAME} (${STABILITY})"
|
||||||
|
|
||||||
|
- name: Auto-bump patch version
|
||||||
|
id: bump
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
INPUT_VERSION: ${{ steps.meta.outputs.version }}
|
||||||
|
INPUT_STABILITY: ${{ steps.meta.outputs.stability }}
|
||||||
|
INPUT_SUFFIX: ${{ steps.meta.outputs.suffix }}
|
||||||
|
run: |
|
||||||
|
# Read current version from README.md
|
||||||
|
CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
|
||||||
|
if [ -z "$CURRENT" ]; then
|
||||||
|
echo "No VERSION in README.md — using input version"
|
||||||
|
echo "version=${INPUT_VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "zip_name=${EXT_ELEMENT}-${INPUT_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Bump patch: XX.YY.ZZ → XX.YY.(ZZ+1)
|
||||||
|
MAJOR=$(echo "$CURRENT" | cut -d. -f1)
|
||||||
|
MINOR=$(echo "$CURRENT" | cut -d. -f2)
|
||||||
|
PATCH=$(echo "$CURRENT" | cut -d. -f3)
|
||||||
|
NEW_PATCH=$(printf "%02d" $((10#$PATCH + 1)))
|
||||||
|
NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}"
|
||||||
|
|
||||||
|
echo "Bumping: ${CURRENT} → ${NEW_VERSION}"
|
||||||
|
|
||||||
|
# Update README.md
|
||||||
|
sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${NEW_VERSION}/" README.md
|
||||||
|
|
||||||
|
# Update templateDetails.xml / manifest
|
||||||
|
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
|
if [ -n "$MANIFEST" ]; then
|
||||||
|
sed -i "s|<version>${CURRENT}</version>|<version>${NEW_VERSION}</version>|" "$MANIFEST"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update only the matching stability channel in updates.xml
|
||||||
|
if [ -f "updates.xml" ]; then
|
||||||
|
export PY_OLD="$CURRENT" PY_NEW="$NEW_VERSION" PY_STABILITY="$INPUT_STABILITY"
|
||||||
|
python3 << 'PYEOF'
|
||||||
|
import re, os
|
||||||
|
old = os.environ["PY_OLD"]
|
||||||
|
new = os.environ["PY_NEW"]
|
||||||
|
stability = os.environ["PY_STABILITY"]
|
||||||
|
with open("updates.xml") as f:
|
||||||
|
content = f.read()
|
||||||
|
pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(stability) + r"</tag>.*?</update>)"
|
||||||
|
match = re.search(pattern, content, re.DOTALL)
|
||||||
|
if match:
|
||||||
|
block = match.group(1)
|
||||||
|
updated = block.replace(old, new)
|
||||||
|
content = content.replace(block, updated)
|
||||||
|
with open("updates.xml", "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
print(f"Updated {stability} channel: {old} -> {new}")
|
||||||
|
PYEOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Commit bump
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://jmiller:${GA_TOKEN}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
git add -A
|
||||||
|
git diff --cached --quiet || {
|
||||||
|
git commit -m "chore(version): bump ${CURRENT} → ${NEW_VERSION} [skip ci]" \
|
||||||
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
||||||
|
git push
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "version=${NEW_VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "zip_name=${EXT_ELEMENT}-${NEW_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
env:
|
||||||
|
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}'
|
||||||
|
run: |
|
||||||
|
if [ -f "composer.json" ]; then
|
||||||
|
composer install --no-dev --optimize-autoloader --no-interaction
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Create package
|
||||||
|
run: |
|
||||||
|
mkdir -p build/package
|
||||||
|
rsync -av \
|
||||||
|
--exclude='sftp-config*' \
|
||||||
|
--exclude='.ftpignore' \
|
||||||
|
--exclude='*.ppk' \
|
||||||
|
--exclude='*.pem' \
|
||||||
|
--exclude='*.key' \
|
||||||
|
--exclude='.env*' \
|
||||||
|
--exclude='*.local' \
|
||||||
|
src/ build/package/
|
||||||
|
|
||||||
|
- name: Build ZIP
|
||||||
|
id: zip
|
||||||
|
run: |
|
||||||
|
ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
|
||||||
|
cd build/package
|
||||||
|
zip -r "../${ZIP_NAME}" .
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1)
|
||||||
|
SIZE=$(stat -c%s "${ZIP_NAME}")
|
||||||
|
|
||||||
|
echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "size=${SIZE}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "SHA-256: ${SHA256}"
|
||||||
|
echo "Size: ${SIZE} bytes"
|
||||||
|
|
||||||
|
# ── Gitea Release (PRIMARY) ──────────────────────────────────────
|
||||||
|
- name: "Gitea: Delete existing release"
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.meta.outputs.tag_name }}"
|
||||||
|
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# Find and delete existing release by tag (may not exist — ignore 404)
|
||||||
|
RELEASE_ID=$(curl -s -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API}/releases/tags/${TAG}" 2>/dev/null | jq -r '.id // empty')
|
||||||
|
|
||||||
|
if [ -n "$RELEASE_ID" ]; then
|
||||||
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API}/releases/${RELEASE_ID}" || true
|
||||||
|
echo "Deleted existing release id=${RELEASE_ID}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Delete existing tag
|
||||||
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API}/tags/${TAG}" 2>/dev/null || true
|
||||||
|
|
||||||
|
- name: "Gitea: Create release"
|
||||||
|
id: gitea_release
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.meta.outputs.tag_name }}"
|
||||||
|
VERSION="${{ steps.bump.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
PRERELEASE="${{ steps.meta.outputs.prerelease }}"
|
||||||
|
SHA256="${{ steps.zip.outputs.sha256 }}"
|
||||||
|
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# Build release body
|
||||||
|
BODY="## ${EXT_ELEMENT} ${VERSION} (${STABILITY})
|
||||||
|
|
||||||
|
### SHA-256
|
||||||
|
\`${SHA256}\`"
|
||||||
|
|
||||||
|
# Extract changelog if available
|
||||||
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
|
NOTES=$(awk "/## \[${VERSION}\]/,/## \[/{if(/## \[${VERSION}\]/)next;if(/## \[/)exit;print}" CHANGELOG.md)
|
||||||
|
if [ -n "$NOTES" ]; then
|
||||||
|
BODY="## ${EXT_ELEMENT} ${VERSION} (${STABILITY})
|
||||||
|
|
||||||
|
${NOTES}
|
||||||
|
|
||||||
|
### SHA-256
|
||||||
|
\`${SHA256}\`"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
IS_PRE="true"
|
||||||
|
if [ "$STABILITY" = "stable" ]; then
|
||||||
|
IS_PRE="false"
|
||||||
|
fi
|
||||||
|
|
||||||
|
RESULT=$(curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/releases" \
|
||||||
|
-d "$(jq -n \
|
||||||
|
--arg tag "$TAG" \
|
||||||
|
--arg target "${{ github.ref_name }}" \
|
||||||
|
--arg name "${EXT_ELEMENT} ${VERSION} ${STABILITY^}" \
|
||||||
|
--arg body "$BODY" \
|
||||||
|
--argjson pre "$IS_PRE" \
|
||||||
|
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: $pre}'
|
||||||
|
)")
|
||||||
|
|
||||||
|
RELEASE_ID=$(echo "$RESULT" | jq -r '.id')
|
||||||
|
echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Gitea release created: id=${RELEASE_ID}, tag=${TAG}"
|
||||||
|
|
||||||
|
- name: "Gitea: Upload ZIP"
|
||||||
|
run: |
|
||||||
|
RELEASE_ID="${{ steps.gitea_release.outputs.release_id }}"
|
||||||
|
ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
|
||||||
|
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
"${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
|
||||||
|
--data-binary "@build/${ZIP_NAME}"
|
||||||
|
|
||||||
|
echo "Uploaded ${ZIP_NAME} to Gitea release ${RELEASE_ID}"
|
||||||
|
|
||||||
|
# ── GitHub Mirror (BACKUP) ───────────────────────────────────────
|
||||||
|
- name: "GitHub: Mirror release (stable/rc only)"
|
||||||
|
if: ${{ steps.meta.outputs.stability == 'stable' || steps.meta.outputs.stability == 'rc' }}
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.meta.outputs.tag_name }}"
|
||||||
|
VERSION="${{ steps.bump.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
|
||||||
|
SHA256="${{ steps.zip.outputs.sha256 }}"
|
||||||
|
TOKEN="${{ secrets.GH_TOKEN }}"
|
||||||
|
GH_REPO="mokoconsulting-tech/${GITEA_REPO}"
|
||||||
|
GH_API="https://api.github.com/repos/${GH_REPO}"
|
||||||
|
|
||||||
|
IS_PRE="true"
|
||||||
|
[ "$STABILITY" = "stable" ] && IS_PRE="false"
|
||||||
|
|
||||||
|
# Delete existing release by tag
|
||||||
|
EXISTING=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${GH_API}/releases/tags/${TAG}" 2>/dev/null | jq -r '.id // empty')
|
||||||
|
if [ -n "$EXISTING" ]; then
|
||||||
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${GH_API}/releases/${EXISTING}" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Delete tag
|
||||||
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${GH_API}/git/refs/tags/${TAG}" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Create release
|
||||||
|
RELEASE_ID=$(curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${GH_API}/releases" \
|
||||||
|
-d "$(jq -n \
|
||||||
|
--arg tag "$TAG" \
|
||||||
|
--arg target "${{ github.sha }}" \
|
||||||
|
--arg name "${EXT_ELEMENT} ${VERSION} ${STABILITY^} (mirror)" \
|
||||||
|
--arg body "Mirror of Gitea release. SHA-256: \`${SHA256}\`" \
|
||||||
|
--argjson pre "$IS_PRE" \
|
||||||
|
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: $pre, draft: false}'
|
||||||
|
)" | jq -r '.id')
|
||||||
|
|
||||||
|
# Upload ZIP
|
||||||
|
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "null" ]; then
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
"https://uploads.github.com/repos/${GH_REPO}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
|
||||||
|
--data-binary "@build/${ZIP_NAME}"
|
||||||
|
echo "GitHub mirror: uploaded ${ZIP_NAME}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Update updates.xml ──────────────────────────────────────────
|
||||||
|
- name: "Update updates.xml for this channel"
|
||||||
|
run: |
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
VERSION="${{ steps.bump.outputs.version }}"
|
||||||
|
SHA256="${{ steps.zip.outputs.sha256 }}"
|
||||||
|
ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
|
||||||
|
TAG="${{ steps.meta.outputs.tag_name }}"
|
||||||
|
DATE=$(date +%Y-%m-%d)
|
||||||
|
|
||||||
|
if [ ! -f "updates.xml" ] || [ -z "$SHA256" ]; then
|
||||||
|
echo "No updates.xml or no SHA — skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
export PY_STABILITY="$STABILITY" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \
|
||||||
|
PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \
|
||||||
|
PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO"
|
||||||
|
python3 << 'PYEOF'
|
||||||
|
import re, os
|
||||||
|
|
||||||
|
stability = os.environ["PY_STABILITY"]
|
||||||
|
version = os.environ["PY_VERSION"]
|
||||||
|
sha256 = os.environ["PY_SHA256"]
|
||||||
|
zip_name = os.environ["PY_ZIP_NAME"]
|
||||||
|
tag = os.environ["PY_TAG"]
|
||||||
|
date = os.environ["PY_DATE"]
|
||||||
|
gitea_org = os.environ["PY_GITEA_ORG"]
|
||||||
|
gitea_repo = os.environ["PY_GITEA_REPO"]
|
||||||
|
|
||||||
|
# Map stability to the <tag> value in updates.xml
|
||||||
|
tag_map = {
|
||||||
|
"development": "development",
|
||||||
|
"alpha": "alpha",
|
||||||
|
"beta": "beta",
|
||||||
|
"rc": "rc",
|
||||||
|
"stable": "stable",
|
||||||
|
}
|
||||||
|
xml_tag = tag_map.get(stability, "development")
|
||||||
|
|
||||||
|
with open("updates.xml", "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Build regex to find the specific <update> block for this stability tag
|
||||||
|
# Use negative lookahead to avoid matching across multiple <update> blocks
|
||||||
|
block_pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(xml_tag) + r"</tag>.*?</update>)"
|
||||||
|
match = re.search(block_pattern, content, re.DOTALL)
|
||||||
|
|
||||||
|
if not match:
|
||||||
|
print(f"No <update> block found for <tag>{xml_tag}</tag>")
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
block = match.group(1)
|
||||||
|
original_block = block
|
||||||
|
|
||||||
|
# Update version
|
||||||
|
block = re.sub(r"<version>[^<]*</version>", f"<version>{version}</version>", block)
|
||||||
|
|
||||||
|
# Update creation date
|
||||||
|
block = re.sub(r"<creationDate>[^<]*</creationDate>", f"<creationDate>{date}</creationDate>", block)
|
||||||
|
|
||||||
|
# Update SHA-256
|
||||||
|
block = re.sub(r"<sha256>[^<]*</sha256>", f"<sha256>{sha256}</sha256>", block)
|
||||||
|
|
||||||
|
# Update Gitea download URL
|
||||||
|
gitea_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}"
|
||||||
|
block = re.sub(
|
||||||
|
r"(<downloadurl[^>]*>)https://git\.mokoconsulting\.tech/[^<]*(</downloadurl>)",
|
||||||
|
rf"\g<1>{gitea_url}\g<2>",
|
||||||
|
block
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update GitHub download URL only for RC and stable (others are Gitea-only)
|
||||||
|
if stability in ("rc", "stable"):
|
||||||
|
gh_url = f"https://github.com/mokoconsulting-tech/{gitea_repo}/releases/download/{tag}/{zip_name}"
|
||||||
|
block = re.sub(
|
||||||
|
r"(<downloadurl[^>]*>)https://github\.com/[^<]*(</downloadurl>)",
|
||||||
|
rf"\g<1>{gh_url}\g<2>",
|
||||||
|
block
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Remove any GitHub download URL for dev/alpha/beta
|
||||||
|
block = re.sub(
|
||||||
|
r"\n\s*<downloadurl[^>]*>https://github\.com/[^<]*</downloadurl>",
|
||||||
|
"",
|
||||||
|
block
|
||||||
|
)
|
||||||
|
|
||||||
|
content = content.replace(original_block, block)
|
||||||
|
|
||||||
|
with open("updates.xml", "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
print(f"Updated {xml_tag} channel: version={version}, sha={sha256[:16]}..., date={date}")
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
- name: "Commit updates.xml to current branch and main"
|
||||||
|
run: |
|
||||||
|
if git diff --quiet updates.xml 2>/dev/null; then
|
||||||
|
echo "No changes to updates.xml"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
VERSION="${{ steps.bump.outputs.version }}"
|
||||||
|
CURRENT_BRANCH="${{ github.ref_name }}"
|
||||||
|
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
|
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git add updates.xml
|
||||||
|
git commit -m "chore: update ${STABILITY} SHA-256 for ${VERSION} [skip ci]" \
|
||||||
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
||||||
|
|
||||||
|
# Set push URL with GA_TOKEN for authenticated pushes (branch protection requires jmiller)
|
||||||
|
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
|
||||||
|
# Push to current branch
|
||||||
|
git push || true
|
||||||
|
|
||||||
|
# Also update updates.xml on main via Gitea API (git push blocked by branch protection)
|
||||||
|
if [ "$CURRENT_BRANCH" != "main" ]; then
|
||||||
|
GA_TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
|
||||||
|
# Get current file SHA on main (required for update)
|
||||||
|
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty')
|
||||||
|
|
||||||
|
if [ -n "$FILE_SHA" ]; then
|
||||||
|
# Base64-encode the updates.xml content from working tree (has updated SHA)
|
||||||
|
CONTENT=$(base64 -w0 updates.xml)
|
||||||
|
|
||||||
|
RESPONSE=$(curl -s -w "\n%{http_code}" -X PUT -H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/contents/updates.xml" \
|
||||||
|
-d "$(jq -n \
|
||||||
|
--arg content "$CONTENT" \
|
||||||
|
--arg sha "$FILE_SHA" \
|
||||||
|
--arg msg "chore: update ${STABILITY} channel to ${VERSION} on main [skip ci]" \
|
||||||
|
--arg branch "main" \
|
||||||
|
'{content: $content, sha: $sha, message: $msg, branch: $branch}'
|
||||||
|
)")
|
||||||
|
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
|
||||||
|
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then
|
||||||
|
echo "updates.xml synced to main via API (HTTP ${HTTP_CODE})"
|
||||||
|
else
|
||||||
|
echo "WARNING: failed to sync updates.xml to main (HTTP ${HTTP_CODE})"
|
||||||
|
echo "$RESPONSE" | head -5
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "WARNING: could not get file SHA for updates.xml on main"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
|
||||||
|
SHA256="${{ steps.zip.outputs.sha256 }}"
|
||||||
|
TAG="${{ steps.meta.outputs.tag_name }}"
|
||||||
|
|
||||||
|
echo "### Release Created" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Stability | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Tag | \`${TAG}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| SHA-256 | \`${SHA256}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Gitea | [Release](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${TAG}) |" >> $GITHUB_STEP_SUMMARY
|
||||||
221
.github/CLAUDE.md
vendored
221
.github/CLAUDE.md
vendored
@@ -6,60 +6,31 @@ This file is part of a Moko Consulting project.
|
|||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
DEFGROUP: MokoStandards.Templates.GitHub
|
DEFGROUP: MokoOnyx.GitHub
|
||||||
INGROUP: MokoStandards.Templates
|
INGROUP: MokoOnyx
|
||||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
||||||
PATH: /templates/github/copilot-instructions.joomla.md.template
|
PATH: /.github/CLAUDE.md
|
||||||
VERSION: XX.YY.ZZ
|
VERSION: 01.00.05
|
||||||
BRIEF: GitHub Copilot custom instructions template for Joomla/MokoWaaS governed repositories
|
BRIEF: Claude Code custom instructions for MokoOnyx template
|
||||||
NOTE: Synced to .github/copilot-instructions.md in all Joomla/WaaS repos via bulk sync.
|
|
||||||
Tokens replaced at sync time: MokoOnyx, https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx, {{EXTENSION_NAME}},
|
|
||||||
{{EXTENSION_TYPE}}, {{EXTENSION_ELEMENT}}
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
> [!IMPORTANT]
|
# MokoOnyx — Claude Code Instructions
|
||||||
> **🔧 AI Self-Update Required on First Use**
|
|
||||||
>
|
|
||||||
> This file may contain unfilled `{{TOKEN}}` placeholders. The MokoStandards bulk-sync system
|
|
||||||
> replaces them automatically at sync time; if you can still see them, sync has not yet run for
|
|
||||||
> this repository — or this is a brand-new repo.
|
|
||||||
>
|
|
||||||
> **On your very first interaction in this repository, before answering any other question:**
|
|
||||||
>
|
|
||||||
> 1. Check whether any `{{TOKEN}}` placeholders remain in this file.
|
|
||||||
> 2. If they do, locate the values as described in the table below and replace every occurrence
|
|
||||||
> in **both** `.github/copilot-instructions.md` and `.github/CLAUDE.md`.
|
|
||||||
> 3. After replacing all tokens, **delete this entire notice block** (from `> [!IMPORTANT]`
|
|
||||||
> through the closing `> ---` line) in both files.
|
|
||||||
> 4. Tell the developer: *"I've self-updated the AI context files with this repository's
|
|
||||||
> values — all set."*
|
|
||||||
>
|
|
||||||
> | Placeholder | Where to find the value |
|
|
||||||
> |---|---|
|
|
||||||
> | `MokoOnyx` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) |
|
|
||||||
> | `https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx` | Full GitHub URL, e.g. `https://git.mokoconsulting.tech/MokoConsulting/<repo-name>` |
|
|
||||||
> | `{{EXTENSION_NAME}}` | The `<name>` element in `manifest.xml` at the repository root |
|
|
||||||
> | `{{EXTENSION_TYPE}}` | The `type` attribute of the `<extension>` tag in `manifest.xml` (`component`, `module`, `plugin`, or `template`) |
|
|
||||||
> | `{{EXTENSION_ELEMENT}}` | The `<element>` tag in `manifest.xml`, or the filename prefix (e.g. `com_myextension`, `mod_mymodule`) |
|
|
||||||
>
|
|
||||||
> ---
|
|
||||||
|
|
||||||
# MokoOnyx — GitHub Copilot Custom Instructions
|
|
||||||
|
|
||||||
## What This Repo Is
|
## What This Repo Is
|
||||||
|
|
||||||
This is a **Moko Consulting MokoWaaS** (Joomla) repository governed by [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync.
|
This is a **Moko Consulting MokoWaaS** (Joomla) repository governed by [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync.
|
||||||
|
|
||||||
Repository URL: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
Repository URL: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
||||||
Extension name: **{{EXTENSION_NAME}}**
|
Extension name: **MokoOnyx**
|
||||||
Extension type: **{{EXTENSION_TYPE}}** (`{{EXTENSION_ELEMENT}}`)
|
Extension type: **template** (`mokoonyx`)
|
||||||
Platform: **Joomla 4.x / MokoWaaS**
|
Platform: **Joomla 5.x / 6.x / MokoWaaS**
|
||||||
|
Successor to: **MokoCassiopeia** (renamed in v01.00.00)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Primary Language
|
## Primary Language
|
||||||
|
|
||||||
**PHP** (≥ 7.4) is the primary language for this Joomla extension. JavaScript may be used for frontend enhancements. YAML uses 2-space indentation. All other text files use tabs per `.editorconfig`.
|
**PHP** (≥ 8.1) is the primary language for this Joomla template. JavaScript may be used for frontend enhancements. YAML uses 2-space indentation. All other text files use tabs per `.editorconfig`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -77,7 +48,7 @@ Every new file needs a copyright header as its first content.
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: MokoOnyx.{{EXTENSION_TYPE}}
|
* DEFGROUP: MokoOnyx.Template
|
||||||
* INGROUP: MokoOnyx
|
* INGROUP: MokoOnyx
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
||||||
* PATH: /path/to/file.php
|
* PATH: /path/to/file.php
|
||||||
@@ -115,139 +86,93 @@ BRIEF: One-line description
|
|||||||
|
|
||||||
**`README.md` is the single source of truth for the repository version.**
|
**`README.md` is the single source of truth for the repository version.**
|
||||||
|
|
||||||
- **Bump the patch version on every PR** — increment `XX.YY.ZZ` (e.g. `01.02.03` → `01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`.
|
- **Patch version is auto-bumped by the release workflow** — `release.yml` reads the current version from `README.md`, increments the patch (`XX.YY.ZZ` → `XX.YY.(ZZ+1)`), updates `README.md`, `templateDetails.xml`, and the matching channel in `updates.xml`, commits, pushes, then builds the ZIP. Manual bumping is no longer required.
|
||||||
- The `VERSION: XX.YY.ZZ` field in `README.md` governs all other version references.
|
- The `VERSION: XX.YY.ZZ` field in `README.md` governs all other version references.
|
||||||
- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `01.02.03`).
|
- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `01.00.05`).
|
||||||
- Never hardcode a specific version in document body text — use the badge or FILE INFORMATION header only.
|
- Never hardcode a specific version in document body text — use the badge or FILE INFORMATION header only.
|
||||||
|
|
||||||
### Joomla Version Alignment
|
### Joomla Version Alignment
|
||||||
|
|
||||||
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `updates.xml`. The `make release` command / release workflow updates all three automatically.
|
The version in `README.md` **must always match** the `<version>` tag in `templateDetails.xml` and the matching channel entry in `updates.xml`. The release workflow updates all three automatically.
|
||||||
|
|
||||||
|
### Multi-Channel updates.xml
|
||||||
|
|
||||||
|
`updates.xml` contains separate `<update>` blocks per stability channel (development, alpha, beta, rc, stable). Each release workflow only modifies its own channel using targeted Python regex replacement — other channels are preserved untouched. Joomla filters by the user's "Minimum Stability" setting.
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<!-- In manifest.xml — must match README.md version -->
|
|
||||||
<version>01.02.04</version>
|
|
||||||
|
|
||||||
<!-- In updates.xml — prepend a new <update> block for every release.
|
|
||||||
Note: the backslash in version="4\.[0-9]+" is a literal backslash character
|
|
||||||
in the XML attribute value. Joomla's update server treats the value as a
|
|
||||||
regular expression, so \. matches a literal dot. -->
|
|
||||||
<updates>
|
<updates>
|
||||||
<update>
|
<!-- 1. DEVELOPMENT --> <update>...<tag>development</tag>...</update>
|
||||||
<name>{{EXTENSION_NAME}}</name>
|
<!-- 2. ALPHA --> <update>...<tag>alpha</tag>...</update>
|
||||||
<version>01.02.04</version>
|
<!-- 3. BETA --> <update>...<tag>beta</tag>...</update>
|
||||||
<downloads>
|
<!-- 4. RC --> <update>...<tag>rc</tag>...</update>
|
||||||
<downloadurl type="full" format="zip">
|
<!-- 5. STABLE --> <update>...<tag>stable</tag>...</update>
|
||||||
https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/01.02.04/{{EXTENSION_ELEMENT}}-01.02.04.zip
|
|
||||||
</downloadurl>
|
|
||||||
</downloads>
|
|
||||||
<targetplatform name="joomla" version="4\.[0-9]+" />
|
|
||||||
</update>
|
|
||||||
<!-- … older entries preserved below … -->
|
|
||||||
</updates>
|
</updates>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Key rules:**
|
||||||
|
- SHA-256 must be raw hex (no `sha256:` prefix)
|
||||||
|
- Version format must be `XX.YY.ZZ`, not tag names like `v01`
|
||||||
|
- Download URLs must point to Gitea (not GitHub) for all pre-release channels
|
||||||
|
- **Always push updates.xml to main** — Joomla sites read from main, not dev
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Joomla Extension Structure
|
## MokoCassiopeia Migration (v01.x only)
|
||||||
|
|
||||||
|
MokoOnyx v01.x includes a migration system for MokoCassiopeia users:
|
||||||
|
|
||||||
|
- **`helper/migrate.php`** — runs on first page load via `index.php` bootstrap
|
||||||
|
- Creates `.migrated` marker file so it only runs once
|
||||||
|
- Copies template style params from MokoCassiopeia → MokoOnyx
|
||||||
|
- Creates matching styles, copies user files, redirects update server
|
||||||
|
- **Joomla 6 does NOT call `<scriptfile>` for templates** — that's why migration runs from `index.php` instead of `script.php`
|
||||||
|
- The migration script will be **removed in v02.00.00** (see ROADMAP.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Template Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
MokoOnyx/
|
MokoOnyx/
|
||||||
├── manifest.xml # Joomla installer manifest (root — required)
|
├── src/
|
||||||
├── updates.xml # Update server manifest (root — required, see below)
|
│ ├── templateDetails.xml # Joomla installer manifest
|
||||||
├── site/ # Frontend (site) code
|
│ ├── script.php # Install/update script (limited in Joomla 6 for templates)
|
||||||
│ ├── controller.php
|
│ ├── index.php # Main template file (includes migration bootstrap)
|
||||||
│ ├── controllers/
|
│ ├── helper/
|
||||||
│ ├── models/
|
│ │ ├── migrate.php # MokoCassiopeia → MokoOnyx migration (v01.x)
|
||||||
│ └── views/
|
│ │ ├── favicon.php # Favicon generator
|
||||||
├── admin/ # Backend (admin) code
|
│ │ └── minify.php # Asset minification
|
||||||
│ ├── controller.php
|
│ ├── language/ # Language INI files (en-GB, en-US)
|
||||||
│ ├── controllers/
|
│ ├── media/ # CSS, JS, images, vendor assets
|
||||||
│ ├── models/
|
│ └── templates/ # Custom CSS palette starters, theme test
|
||||||
│ ├── views/
|
├── updates.xml # Update server manifest (root — required)
|
||||||
│ └── sql/
|
├── .gitea/workflows/ # Gitea Actions workflows
|
||||||
├── language/ # Language INI files
|
├── docs/ # Documentation
|
||||||
├── media/ # CSS, JS, images (deployed to /media/{{EXTENSION_ELEMENT}}/)
|
├── README.md # Version source of truth
|
||||||
├── docs/ # Technical documentation
|
|
||||||
├── tests/ # Test suite
|
|
||||||
├── .github/
|
|
||||||
│ ├── workflows/
|
|
||||||
│ ├── copilot-instructions.md # This file
|
|
||||||
│ └── CLAUDE.md
|
|
||||||
├── README.md # Version source of truth
|
|
||||||
├── CHANGELOG.md
|
├── CHANGELOG.md
|
||||||
├── CONTRIBUTING.md
|
└── LICENSE
|
||||||
├── LICENSE # GPL-3.0-or-later
|
|
||||||
└── Makefile # Build automation
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## updates.xml — Required in Repo Root
|
## Gitea Actions — Token Usage
|
||||||
|
|
||||||
`updates.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
|
Every workflow must use **`secrets.GA_TOKEN`** (the org-level Personal Access Token).
|
||||||
|
|
||||||
The `manifest.xml` must reference it via:
|
|
||||||
```xml
|
|
||||||
<updateservers>
|
|
||||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
|
||||||
https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/main/updates.xml
|
|
||||||
</server>
|
|
||||||
</updateservers>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Rules:**
|
|
||||||
- Every release must prepend a new `<update>` block at the top of `updates.xml` — old entries must be preserved below.
|
|
||||||
- The `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
|
|
||||||
- The `<downloadurl>` must be a publicly accessible direct download link (GitHub Releases asset URL).
|
|
||||||
- `<targetplatform name="joomla" version="4\.[0-9]+">` — the backslash is a **literal backslash character** in the XML attribute value; Joomla's update-server parser treats the value as a regular expression, so `\.` matches a literal dot and `[0-9]+` matches one or more digits. Do not double-escape it.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## manifest.xml Rules
|
|
||||||
|
|
||||||
- Lives at the repo root as `manifest.xml` (not inside `site/` or `admin/`).
|
|
||||||
- `<version>` tag must be kept in sync with `README.md` version and `updates.xml`.
|
|
||||||
- Must include `<updateservers>` block pointing to this repo's `updates.xml`.
|
|
||||||
- Must include `<files folder="site">` and `<administration>` sections.
|
|
||||||
- Joomla 4.x requires `<namespace path="src">Moko\{{EXTENSION_NAME}}</namespace>` for namespaced extensions.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## GitHub Actions — Token Usage
|
|
||||||
|
|
||||||
Every workflow must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token).
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# ✅ Correct
|
# ✅ Correct
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GH_TOKEN }}
|
token: ${{ secrets.GA_TOKEN }}
|
||||||
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# ❌ Wrong — never use these in workflows
|
# ❌ Wrong
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
Note: Workflows are in `.gitea/workflows/` (not `.github/workflows/`).
|
||||||
|
|
||||||
## MokoStandards Reference
|
|
||||||
|
|
||||||
This repository is governed by [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards). Authoritative policies:
|
|
||||||
|
|
||||||
| Document | Purpose |
|
|
||||||
|----------|---------|
|
|
||||||
| [file-header-standards.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type |
|
|
||||||
| [coding-style-guide.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions |
|
|
||||||
| [branching-strategy.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
|
|
||||||
| [merge-strategy.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR title/body conventions |
|
|
||||||
| [changelog-standards.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
|
|
||||||
| [joomla-development-guide.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoWaaS Joomla extension development guide |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -260,7 +185,7 @@ This repository is governed by [MokoStandards](https://git.mokoconsulting.tech/M
|
|||||||
| PHP variable | `$snake_case` | `$item_id` |
|
| PHP variable | `$snake_case` | `$item_id` |
|
||||||
| PHP constant | `UPPER_SNAKE_CASE` | `MAX_ITEMS` |
|
| PHP constant | `UPPER_SNAKE_CASE` | `MAX_ITEMS` |
|
||||||
| PHP class file | `PascalCase.php` | `ItemModel.php` |
|
| PHP class file | `PascalCase.php` | `ItemModel.php` |
|
||||||
| YAML workflow | `kebab-case.yml` | `ci-joomla.yml` |
|
| YAML workflow | `kebab-case.yml` | `release.yml` |
|
||||||
| Markdown doc | `kebab-case.md` | `installation-guide.md` |
|
| Markdown doc | `kebab-case.md` | `installation-guide.md` |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -286,11 +211,11 @@ Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `d
|
|||||||
| Change type | Documentation to update |
|
| Change type | Documentation to update |
|
||||||
|-------------|------------------------|
|
|-------------|------------------------|
|
||||||
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
|
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
|
||||||
| New or changed manifest.xml | Update `updates.xml` version; bump README.md version |
|
| New or changed templateDetails.xml | Release workflow auto-bumps version across README.md, templateDetails.xml, and updates.xml |
|
||||||
| New release | Prepend `<update>` block to `updates.xml`; update CHANGELOG.md; bump README.md version |
|
| New release | Trigger `release.yml` — auto-bumps patch, builds ZIP, updates matching channel in `updates.xml` |
|
||||||
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
|
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
|
||||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
||||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
|
| **Every release** | **Patch auto-bumped** by `release.yml` — no manual version bump needed |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -300,5 +225,7 @@ Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `d
|
|||||||
- Never skip the FILE INFORMATION block on a new file
|
- Never skip the FILE INFORMATION block on a new file
|
||||||
- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests — only to web-accessible PHP files
|
- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests — only to web-accessible PHP files
|
||||||
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
|
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
|
||||||
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
|
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GA_TOKEN`
|
||||||
- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync
|
- Never let `templateDetails.xml` version, `updates.xml` version, and `README.md` version go out of sync
|
||||||
|
- Always push `updates.xml` to main after updating on dev (Joomla reads from main)
|
||||||
|
- Always update documentation when changing MokoStandards-API or MokoStandards repos
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
INGROUP: MokoOnyx.Documentation
|
INGROUP: MokoOnyx.Documentation
|
||||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
||||||
FILE: ./README.md
|
FILE: ./README.md
|
||||||
VERSION: 01.00.00
|
VERSION: 01.00.07
|
||||||
BRIEF: Documentation for MokoOnyx template
|
BRIEF: Documentation for MokoOnyx template
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
@@ -144,6 +144,25 @@ This document provides a comprehensive, version-specific roadmap for the MokoOny
|
|||||||
|
|
||||||
The following versions represent our planned annual major releases, each building upon the previous version's foundation.
|
The following versions represent our planned annual major releases, each building upon the previous version's foundation.
|
||||||
|
|
||||||
|
#### v02.00.00 (Q3 2026) - Clean Slate
|
||||||
|
**Status**: Planned
|
||||||
|
**Target Release**: Q3 2026
|
||||||
|
|
||||||
|
**Breaking Changes**:
|
||||||
|
- **Remove MokoCassiopeia migration script** — the `helper/migrate.php` bootstrap in `index.php` and the `.migrated` marker file will be removed. All MokoCassiopeia users must migrate before upgrading to v02.
|
||||||
|
- **Remove MokoCassiopeia references** — all `str_replace(mokocassiopeia, mokoonyx)` logic, old name constants, and legacy compatibility code will be cleaned out.
|
||||||
|
- **Remove `script.php` bridge logic** — the `postflight()` migration code for MokoCassiopeia will be removed.
|
||||||
|
|
||||||
|
**New Features**:
|
||||||
|
- Clean codebase with no migration overhead
|
||||||
|
- Performance improvements from removing first-load migration check
|
||||||
|
- Fresh start for MokoOnyx-native development
|
||||||
|
|
||||||
|
**Migration Notice**:
|
||||||
|
Users still running MokoCassiopeia must install MokoOnyx v01.x first to migrate their settings before upgrading to v02. The v01.x line will remain available for download.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
#### v04.00.00 (Q4 2027) - Enhanced Accessibility & Performance
|
#### v04.00.00 (Q4 2027) - Enhanced Accessibility & Performance
|
||||||
**Status**: Planned
|
**Status**: Planned
|
||||||
**Target Release**: December 2027
|
**Target Release**: December 2027
|
||||||
|
|||||||
205
src/helper/migrate.php
Normal file
205
src/helper/migrate.php
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* One-time migration from MokoCassiopeia → MokoOnyx.
|
||||||
|
* Called from index.php on first page load. Creates a .migrated
|
||||||
|
* marker file so it only runs once.
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Factory;
|
||||||
|
use Joomla\CMS\Log\Log;
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
$markerFile = __DIR__ . '/../.migrated';
|
||||||
|
|
||||||
|
// Already migrated
|
||||||
|
if (file_exists($markerFile)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = Factory::getDbo();
|
||||||
|
$app = Factory::getApplication();
|
||||||
|
|
||||||
|
$oldName = 'mokocassiopeia';
|
||||||
|
$newName = 'mokoonyx';
|
||||||
|
$oldDisplay = 'MokoCassiopeia';
|
||||||
|
$newDisplay = 'MokoOnyx';
|
||||||
|
|
||||||
|
// Init logger
|
||||||
|
Log::addLogger(
|
||||||
|
['text_file' => 'mokoonyx_migrate.log.php'],
|
||||||
|
Log::ALL,
|
||||||
|
['mokoonyx_migrate']
|
||||||
|
);
|
||||||
|
|
||||||
|
$log = function (string $msg, int $level = Log::INFO) {
|
||||||
|
Log::add($msg, $level, 'mokoonyx_migrate');
|
||||||
|
};
|
||||||
|
|
||||||
|
$log('=== MokoOnyx migration started (index.php bootstrap) ===');
|
||||||
|
|
||||||
|
// Check if MokoCassiopeia has styles to migrate
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select('*')
|
||||||
|
->from('#__template_styles')
|
||||||
|
->where($db->quoteName('template') . ' = ' . $db->quote($oldName))
|
||||||
|
->where($db->quoteName('client_id') . ' = 0');
|
||||||
|
$oldStyles = $db->setQuery($query)->loadObjectList();
|
||||||
|
|
||||||
|
if (empty($oldStyles)) {
|
||||||
|
$log('No MokoCassiopeia styles found — fresh install, nothing to migrate.');
|
||||||
|
@file_put_contents($markerFile, date('Y-m-d H:i:s') . ' fresh install');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$log('Found ' . count($oldStyles) . ' MokoCassiopeia style(s) to migrate.');
|
||||||
|
|
||||||
|
// Get the default MokoOnyx style (created by Joomla installer)
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select('id')
|
||||||
|
->from('#__template_styles')
|
||||||
|
->where($db->quoteName('template') . ' = ' . $db->quote($newName))
|
||||||
|
->where($db->quoteName('client_id') . ' = 0')
|
||||||
|
->order($db->quoteName('id') . ' ASC');
|
||||||
|
$defaultOnyxId = (int) $db->setQuery($query, 0, 1)->loadResult();
|
||||||
|
|
||||||
|
$isFirst = true;
|
||||||
|
|
||||||
|
foreach ($oldStyles as $old) {
|
||||||
|
$newTitle = str_replace($oldDisplay, $newDisplay, $old->title);
|
||||||
|
$newTitle = str_replace($oldName, $newName, $newTitle);
|
||||||
|
$newParams = is_string($old->params)
|
||||||
|
? str_replace($oldName, $newName, $old->params)
|
||||||
|
: $old->params;
|
||||||
|
|
||||||
|
if ($isFirst && $defaultOnyxId) {
|
||||||
|
// Apply params to the installer-created default MokoOnyx style
|
||||||
|
$update = $db->getQuery(true)
|
||||||
|
->update('#__template_styles')
|
||||||
|
->set($db->quoteName('title') . ' = ' . $db->quote($newTitle))
|
||||||
|
->set($db->quoteName('params') . ' = ' . $db->quote($newParams))
|
||||||
|
->where('id = ' . $defaultOnyxId);
|
||||||
|
$db->setQuery($update)->execute();
|
||||||
|
|
||||||
|
// Set as default if MokoCassiopeia was default
|
||||||
|
if ($old->home == 1) {
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->update('#__template_styles')
|
||||||
|
->set($db->quoteName('home') . ' = 1')
|
||||||
|
->where('id = ' . $defaultOnyxId)
|
||||||
|
)->execute();
|
||||||
|
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->update('#__template_styles')
|
||||||
|
->set($db->quoteName('home') . ' = 0')
|
||||||
|
->where('id = ' . (int) $old->id)
|
||||||
|
)->execute();
|
||||||
|
|
||||||
|
$log('Set MokoOnyx as default site template.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$log("Applied params to default MokoOnyx style: {$newTitle}");
|
||||||
|
$isFirst = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional styles: check if already exists
|
||||||
|
$check = $db->getQuery(true)
|
||||||
|
->select('COUNT(*)')
|
||||||
|
->from('#__template_styles')
|
||||||
|
->where($db->quoteName('template') . ' = ' . $db->quote($newName))
|
||||||
|
->where($db->quoteName('title') . ' = ' . $db->quote($newTitle));
|
||||||
|
if ((int) $db->setQuery($check)->loadResult() > 0) {
|
||||||
|
$log("Style '{$newTitle}' already exists — skipping.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new MokoOnyx style copy
|
||||||
|
$new = clone $old;
|
||||||
|
unset($new->id);
|
||||||
|
$new->template = $newName;
|
||||||
|
$new->title = $newTitle;
|
||||||
|
$new->params = $newParams;
|
||||||
|
$new->home = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db->insertObject('#__template_styles', $new, 'id');
|
||||||
|
$log("Created MokoOnyx style: {$newTitle}");
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$log("Failed to create style '{$newTitle}': " . $e->getMessage(), Log::WARNING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy user files from MokoCassiopeia media
|
||||||
|
$oldMedia = JPATH_ROOT . '/media/templates/site/' . $oldName;
|
||||||
|
$newMedia = JPATH_ROOT . '/media/templates/site/' . $newName;
|
||||||
|
|
||||||
|
if (is_dir($oldMedia) && is_dir($newMedia)) {
|
||||||
|
$userFiles = [
|
||||||
|
'css/theme/light.custom.css',
|
||||||
|
'css/theme/dark.custom.css',
|
||||||
|
'css/theme/light.custom.min.css',
|
||||||
|
'css/theme/dark.custom.min.css',
|
||||||
|
'css/user.css',
|
||||||
|
'css/user.min.css',
|
||||||
|
'js/user.js',
|
||||||
|
'js/user.min.js',
|
||||||
|
];
|
||||||
|
|
||||||
|
$copied = 0;
|
||||||
|
foreach ($userFiles as $rel) {
|
||||||
|
$src = $oldMedia . '/' . $rel;
|
||||||
|
$dst = $newMedia . '/' . $rel;
|
||||||
|
if (is_file($src) && !is_file($dst)) {
|
||||||
|
$dir = dirname($dst);
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
mkdir($dir, 0755, true);
|
||||||
|
}
|
||||||
|
copy($src, $dst);
|
||||||
|
$copied++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($copied > 0) {
|
||||||
|
$log("Copied {$copied} user file(s) from MokoCassiopeia.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the update server
|
||||||
|
try {
|
||||||
|
$onyxUpdatesUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml';
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->update('#__update_sites')
|
||||||
|
->set($db->quoteName('location') . ' = ' . $db->quote($onyxUpdatesUrl))
|
||||||
|
->set($db->quoteName('name') . ' = ' . $db->quote($newDisplay))
|
||||||
|
->where($db->quoteName('location') . ' LIKE ' . $db->quote('%MokoCassiopeia%'));
|
||||||
|
$db->setQuery($query)->execute();
|
||||||
|
$n = $db->getAffectedRows();
|
||||||
|
if ($n > 0) {
|
||||||
|
$log("Redirected {$n} update site(s) to MokoOnyx.");
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$log('Update server redirect failed: ' . $e->getMessage(), Log::WARNING);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write marker file
|
||||||
|
@file_put_contents($markerFile, date('Y-m-d H:i:s') . " migrated {$oldName} → {$newName}");
|
||||||
|
|
||||||
|
$log('=== Migration completed ===');
|
||||||
|
|
||||||
|
// Enqueue message for admin
|
||||||
|
if ($app->isClient('administrator')) {
|
||||||
|
$app->enqueueMessage(
|
||||||
|
'<strong>MokoOnyx has imported your MokoCassiopeia settings.</strong><br>'
|
||||||
|
. 'You can safely uninstall MokoCassiopeia from Extensions → Manage.',
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -9,6 +9,11 @@
|
|||||||
|
|
||||||
defined('_JEXEC') or die;
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
// One-time migration from MokoCassiopeia (runs once, creates .migrated marker)
|
||||||
|
if (!file_exists(__DIR__ . '/.migrated')) {
|
||||||
|
require_once __DIR__ . '/helper/migrate.php';
|
||||||
|
}
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
use Joomla\CMS\Factory;
|
||||||
use Joomla\CMS\HTML\HTMLHelper;
|
use Joomla\CMS\HTML\HTMLHelper;
|
||||||
use Joomla\CMS\Language\Text;
|
use Joomla\CMS\Language\Text;
|
||||||
|
|||||||
@@ -267,6 +267,13 @@ TPL_MOKOONYX_THEME_PREVIEW_FIELDSET_LABEL="Theme Preview"
|
|||||||
TPL_MOKOONYX_THEME_PREVIEW_INTRO="<p>Live preview of all CSS variables, hero variants, block colours, and Bootstrap components rendered with your active theme. Use the <strong>Toggle Light / Dark</strong> button inside the preview to switch modes. This page is also available as a standalone file at <code>templates/mokoonyx/templates/theme-test.html</code>.</p>"
|
TPL_MOKOONYX_THEME_PREVIEW_INTRO="<p>Live preview of all CSS variables, hero variants, block colours, and Bootstrap components rendered with your active theme. Use the <strong>Toggle Light / Dark</strong> button inside the preview to switch modes. This page is also available as a standalone file at <code>templates/mokoonyx/templates/theme-test.html</code>.</p>"
|
||||||
TPL_MOKOONYX_THEME_PREVIEW_FRAME="<iframe src='../templates/mokoonyx/templates/theme-test.html' style='width:100%;height:80vh;border:1px solid #dee2e6;border-radius:.375rem;' loading='lazy' title='Theme test sheet preview'></iframe>"
|
TPL_MOKOONYX_THEME_PREVIEW_FRAME="<iframe src='../templates/mokoonyx/templates/theme-test.html' style='width:100%;height:80vh;border:1px solid #dee2e6;border-radius:.375rem;' loading='lazy' title='Theme test sheet preview'></iframe>"
|
||||||
|
|
||||||
|
; ===== Migration =====
|
||||||
|
TPL_MOKOONYX_MIGRATION_FIELDSET_LABEL="Migration"
|
||||||
|
TPL_MOKOONYX_MIGRATION_NOTE_LABEL="MokoCassiopeia Migration"
|
||||||
|
TPL_MOKOONYX_MIGRATION_NOTE_DESC="MokoOnyx automatically imports settings from MokoCassiopeia on first page load. If you need to re-run the migration, delete the file <code>templates/mokoonyx/.migrated</code> and visit any frontend page. Check <code>administrator/logs/mokoonyx_migrate.log.php</code> to confirm."
|
||||||
|
TPL_MOKOONYX_MIGRATION_RUN_LABEL="Re-run Migration"
|
||||||
|
TPL_MOKOONYX_MIGRATION_RUN_DESC="<strong>To re-run the migration:</strong> Delete <code>templates/mokoonyx/.migrated</code> via FTP or file manager, then visit any page on your site. The migration will run again automatically.<br><br><strong>To uninstall MokoCassiopeia:</strong> Go to <a href='index.php?option=com_installer&view=manage&filter[search]=mokocassiopeia'>Extensions → Manage</a>, find MokoCassiopeia, and click Uninstall."
|
||||||
|
|
||||||
; ===== Misc =====
|
; ===== Misc =====
|
||||||
MOD_BREADCRUMBS_HERE="YOU ARE HERE:"
|
MOD_BREADCRUMBS_HERE="YOU ARE HERE:"
|
||||||
|
|
||||||
|
|||||||
@@ -267,6 +267,13 @@ TPL_MOKOONYX_THEME_PREVIEW_FIELDSET_LABEL="Theme Preview"
|
|||||||
TPL_MOKOONYX_THEME_PREVIEW_INTRO="<p>Live preview of all CSS variables, hero variants, block colors, and Bootstrap components rendered with your active theme. Use the <strong>Toggle Light / Dark</strong> button inside the preview to switch modes. This page is also available as a standalone file at <code>templates/mokoonyx/templates/theme-test.html</code>.</p>"
|
TPL_MOKOONYX_THEME_PREVIEW_INTRO="<p>Live preview of all CSS variables, hero variants, block colors, and Bootstrap components rendered with your active theme. Use the <strong>Toggle Light / Dark</strong> button inside the preview to switch modes. This page is also available as a standalone file at <code>templates/mokoonyx/templates/theme-test.html</code>.</p>"
|
||||||
TPL_MOKOONYX_THEME_PREVIEW_FRAME="<iframe src='../templates/mokoonyx/templates/theme-test.html' style='width:100%;height:80vh;border:1px solid #dee2e6;border-radius:.375rem;' loading='lazy' title='Theme test sheet preview'></iframe>"
|
TPL_MOKOONYX_THEME_PREVIEW_FRAME="<iframe src='../templates/mokoonyx/templates/theme-test.html' style='width:100%;height:80vh;border:1px solid #dee2e6;border-radius:.375rem;' loading='lazy' title='Theme test sheet preview'></iframe>"
|
||||||
|
|
||||||
|
; ===== Migration =====
|
||||||
|
TPL_MOKOONYX_MIGRATION_FIELDSET_LABEL="Migration"
|
||||||
|
TPL_MOKOONYX_MIGRATION_NOTE_LABEL="MokoCassiopeia Migration"
|
||||||
|
TPL_MOKOONYX_MIGRATION_NOTE_DESC="MokoOnyx automatically imports settings from MokoCassiopeia on first page load. If you need to re-run the migration, delete the file <code>templates/mokoonyx/.migrated</code> and visit any frontend page. Check <code>administrator/logs/mokoonyx_migrate.log.php</code> to confirm."
|
||||||
|
TPL_MOKOONYX_MIGRATION_RUN_LABEL="Re-run Migration"
|
||||||
|
TPL_MOKOONYX_MIGRATION_RUN_DESC="<strong>To re-run the migration:</strong> Delete <code>templates/mokoonyx/.migrated</code> via FTP or file manager, then visit any page on your site. The migration will run again automatically.<br><br><strong>To uninstall MokoCassiopeia:</strong> Go to <a href='index.php?option=com_installer&view=manage&filter[search]=mokocassiopeia'>Extensions → Manage</a>, find MokoCassiopeia, and click Uninstall."
|
||||||
|
|
||||||
; ===== Misc =====
|
; ===== Misc =====
|
||||||
MOD_BREADCRUMBS_HERE="YOU ARE HERE:"
|
MOD_BREADCRUMBS_HERE="YOU ARE HERE:"
|
||||||
|
|
||||||
|
|||||||
253
src/script.php
253
src/script.php
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
*
|
*
|
||||||
* This file is part of a Moko Consulting project.
|
* This file is part of a Moko Consulting project.
|
||||||
*
|
*
|
||||||
@@ -12,45 +12,33 @@
|
|||||||
* Joomla calls the methods in this class automatically during template
|
* Joomla calls the methods in this class automatically during template
|
||||||
* install, update, and uninstall via the <scriptfile> element in
|
* install, update, and uninstall via the <scriptfile> element in
|
||||||
* templateDetails.xml.
|
* templateDetails.xml.
|
||||||
* Joomla 5 and 6 compatible — uses the InstallerScriptInterface when
|
*
|
||||||
* available, falls back to the legacy class-based approach otherwise.
|
* On first install, detects MokoCassiopeia and migrates template styles,
|
||||||
|
* parameters, menu assignments, and user files automatically.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
use Joomla\CMS\Factory;
|
||||||
use Joomla\CMS\Installer\InstallerAdapter;
|
use Joomla\CMS\Installer\InstallerAdapter;
|
||||||
|
use Joomla\CMS\Installer\InstallerScriptInterface;
|
||||||
use Joomla\CMS\Log\Log;
|
use Joomla\CMS\Log\Log;
|
||||||
|
|
||||||
class Tpl_MokoonyxInstallerScript
|
class Tpl_MokoonyxInstallerScript implements InstallerScriptInterface
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Minimum PHP version required by this template.
|
|
||||||
*/
|
|
||||||
private const MIN_PHP = '8.1.0';
|
private const MIN_PHP = '8.1.0';
|
||||||
|
|
||||||
/**
|
|
||||||
* Minimum Joomla version required by this template.
|
|
||||||
*/
|
|
||||||
private const MIN_JOOMLA = '4.4.0';
|
private const MIN_JOOMLA = '4.4.0';
|
||||||
|
|
||||||
/**
|
private const OLD_NAME = 'mokocassiopeia';
|
||||||
* Called before install/update/uninstall.
|
private const NEW_NAME = 'mokoonyx';
|
||||||
*
|
private const OLD_DISPLAY = 'MokoCassiopeia';
|
||||||
* @param string $type install, update, discover_install, or uninstall.
|
private const NEW_DISPLAY = 'MokoOnyx';
|
||||||
* @param InstallerAdapter $parent The adapter calling this method.
|
|
||||||
*
|
|
||||||
* @return bool True to proceed, false to abort.
|
|
||||||
*/
|
|
||||||
public function preflight(string $type, InstallerAdapter $parent): bool
|
public function preflight(string $type, InstallerAdapter $parent): bool
|
||||||
{
|
{
|
||||||
if (version_compare(PHP_VERSION, self::MIN_PHP, '<')) {
|
if (version_compare(PHP_VERSION, self::MIN_PHP, '<')) {
|
||||||
Factory::getApplication()->enqueueMessage(
|
Factory::getApplication()->enqueueMessage(
|
||||||
sprintf(
|
sprintf('MokoOnyx requires PHP %s or later. You are running PHP %s.', self::MIN_PHP, PHP_VERSION),
|
||||||
'MokoOnyx requires PHP %s or later. You are running PHP %s.',
|
|
||||||
self::MIN_PHP,
|
|
||||||
PHP_VERSION
|
|
||||||
),
|
|
||||||
'error'
|
'error'
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
@@ -58,11 +46,7 @@ class Tpl_MokoonyxInstallerScript
|
|||||||
|
|
||||||
if (version_compare(JVERSION, self::MIN_JOOMLA, '<')) {
|
if (version_compare(JVERSION, self::MIN_JOOMLA, '<')) {
|
||||||
Factory::getApplication()->enqueueMessage(
|
Factory::getApplication()->enqueueMessage(
|
||||||
sprintf(
|
sprintf('MokoOnyx requires Joomla %s or later. You are running Joomla %s.', self::MIN_JOOMLA, JVERSION),
|
||||||
'MokoOnyx requires Joomla %s or later. You are running Joomla %s.',
|
|
||||||
self::MIN_JOOMLA,
|
|
||||||
JVERSION
|
|
||||||
),
|
|
||||||
'error'
|
'error'
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
@@ -71,37 +55,17 @@ class Tpl_MokoonyxInstallerScript
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Called after a successful install.
|
|
||||||
*
|
|
||||||
* @param InstallerAdapter $parent The adapter calling this method.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function install(InstallerAdapter $parent): bool
|
public function install(InstallerAdapter $parent): bool
|
||||||
{
|
{
|
||||||
$this->logMessage('MokoOnyx template installed.');
|
$this->logMessage('MokoOnyx template installed.');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Called after a successful update.
|
|
||||||
*
|
|
||||||
* This is where the CSS variable sync runs — it detects variables that
|
|
||||||
* were added in the new version and injects them into the user's custom
|
|
||||||
* palette files without overwriting existing values.
|
|
||||||
*
|
|
||||||
* @param InstallerAdapter $parent The adapter calling this method.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function update(InstallerAdapter $parent): bool
|
public function update(InstallerAdapter $parent): bool
|
||||||
{
|
{
|
||||||
$this->logMessage('MokoOnyx template updated.');
|
$this->logMessage('MokoOnyx template updated.');
|
||||||
|
|
||||||
// Run CSS variable sync to inject any new variables into user's custom palettes.
|
|
||||||
$synced = $this->syncCustomVariables($parent);
|
$synced = $this->syncCustomVariables($parent);
|
||||||
|
|
||||||
if ($synced > 0) {
|
if ($synced > 0) {
|
||||||
Factory::getApplication()->enqueueMessage(
|
Factory::getApplication()->enqueueMessage(
|
||||||
sprintf(
|
sprintf(
|
||||||
@@ -116,47 +80,171 @@ class Tpl_MokoonyxInstallerScript
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Called after a successful uninstall.
|
|
||||||
*
|
|
||||||
* @param InstallerAdapter $parent The adapter calling this method.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function uninstall(InstallerAdapter $parent): bool
|
public function uninstall(InstallerAdapter $parent): bool
|
||||||
{
|
{
|
||||||
$this->logMessage('MokoOnyx template uninstalled.');
|
$this->logMessage('MokoOnyx template uninstalled.');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Called after install/update completes (regardless of type).
|
|
||||||
*
|
|
||||||
* @param string $type install, update, or discover_install.
|
|
||||||
* @param InstallerAdapter $parent The adapter calling this method.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function postflight(string $type, InstallerAdapter $parent): bool
|
public function postflight(string $type, InstallerAdapter $parent): bool
|
||||||
{
|
{
|
||||||
|
// On install or update: migrate from MokoCassiopeia if it exists
|
||||||
|
if ($type === 'install' || $type === 'update') {
|
||||||
|
$this->migrateFromCassiopeia();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the CSS variable sync utility.
|
* Detect MokoCassiopeia and create matching MokoOnyx styles with the same params.
|
||||||
*
|
* Creates a MokoOnyx style copy for each MokoCassiopeia style.
|
||||||
* Loads sync_custom_vars.php from the template directory and calls
|
|
||||||
* MokoCssVarSync::run() to detect and inject missing variables.
|
|
||||||
*
|
|
||||||
* @param InstallerAdapter $parent The adapter calling this method.
|
|
||||||
*
|
|
||||||
* @return int Number of variables added across all files.
|
|
||||||
*/
|
*/
|
||||||
|
private function migrateFromCassiopeia(): void
|
||||||
|
{
|
||||||
|
$db = Factory::getDbo();
|
||||||
|
|
||||||
|
// Get all MokoCassiopeia styles
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select('*')
|
||||||
|
->from('#__template_styles')
|
||||||
|
->where($db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME))
|
||||||
|
->where($db->quoteName('client_id') . ' = 0');
|
||||||
|
$oldStyles = $db->setQuery($query)->loadObjectList();
|
||||||
|
|
||||||
|
if (empty($oldStyles)) {
|
||||||
|
$this->logMessage('No MokoCassiopeia styles found — fresh install.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logMessage('MokoCassiopeia detected — creating ' . count($oldStyles) . ' matching MokoOnyx style(s).');
|
||||||
|
|
||||||
|
// Get the installer-created default MokoOnyx style (to apply params to it)
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select('id')
|
||||||
|
->from('#__template_styles')
|
||||||
|
->where($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME))
|
||||||
|
->where($db->quoteName('client_id') . ' = 0')
|
||||||
|
->order($db->quoteName('id') . ' ASC');
|
||||||
|
$defaultOnyxId = (int) $db->setQuery($query, 0, 1)->loadResult();
|
||||||
|
|
||||||
|
$firstStyle = true;
|
||||||
|
|
||||||
|
foreach ($oldStyles as $oldStyle) {
|
||||||
|
$newTitle = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $oldStyle->title);
|
||||||
|
$newTitle = str_replace(self::OLD_NAME, self::NEW_NAME, $newTitle);
|
||||||
|
|
||||||
|
$params = is_string($oldStyle->params)
|
||||||
|
? str_replace(self::OLD_NAME, self::NEW_NAME, $oldStyle->params)
|
||||||
|
: $oldStyle->params;
|
||||||
|
|
||||||
|
if ($firstStyle && $defaultOnyxId) {
|
||||||
|
// Update the installer-created default style with the first MokoCassiopeia style's params
|
||||||
|
$update = $db->getQuery(true)
|
||||||
|
->update('#__template_styles')
|
||||||
|
->set($db->quoteName('params') . ' = ' . $db->quote($params))
|
||||||
|
->set($db->quoteName('title') . ' = ' . $db->quote($newTitle))
|
||||||
|
->where('id = ' . $defaultOnyxId);
|
||||||
|
$db->setQuery($update)->execute();
|
||||||
|
|
||||||
|
// Set as default if MokoCassiopeia was default
|
||||||
|
if ($oldStyle->home == 1) {
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->update('#__template_styles')
|
||||||
|
->set($db->quoteName('home') . ' = 1')
|
||||||
|
->where('id = ' . $defaultOnyxId)
|
||||||
|
)->execute();
|
||||||
|
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->update('#__template_styles')
|
||||||
|
->set($db->quoteName('home') . ' = 0')
|
||||||
|
->where('id = ' . (int) $oldStyle->id)
|
||||||
|
)->execute();
|
||||||
|
|
||||||
|
$this->logMessage('Set MokoOnyx as default site template.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logMessage("Updated default MokoOnyx style with params: {$newTitle}");
|
||||||
|
$firstStyle = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For additional styles: create new MokoOnyx style copies
|
||||||
|
$newStyle = clone $oldStyle;
|
||||||
|
unset($newStyle->id);
|
||||||
|
$newStyle->template = self::NEW_NAME;
|
||||||
|
$newStyle->title = $newTitle;
|
||||||
|
$newStyle->home = 0;
|
||||||
|
$newStyle->params = $params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db->insertObject('#__template_styles', $newStyle, 'id');
|
||||||
|
$this->logMessage("Created MokoOnyx style: {$newTitle}");
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->logMessage("Failed to create style {$newTitle}: " . $e->getMessage(), 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Copy user files (custom themes, user.css, user.js)
|
||||||
|
$this->copyUserFiles();
|
||||||
|
|
||||||
|
// 3. Notify admin
|
||||||
|
Factory::getApplication()->enqueueMessage(
|
||||||
|
'<strong>MokoOnyx has been installed as a replacement for MokoCassiopeia.</strong><br>'
|
||||||
|
. 'Your template settings and custom files have been migrated automatically. '
|
||||||
|
. 'MokoOnyx is now your active site template. '
|
||||||
|
. 'You can safely uninstall MokoCassiopeia from Extensions → Manage.',
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy user-specific files from MokoCassiopeia media to MokoOnyx media.
|
||||||
|
*/
|
||||||
|
private function copyUserFiles(): void
|
||||||
|
{
|
||||||
|
$oldMedia = JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME;
|
||||||
|
$newMedia = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME;
|
||||||
|
|
||||||
|
if (!is_dir($oldMedia) || !is_dir($newMedia)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userFiles = [
|
||||||
|
'css/theme/light.custom.css',
|
||||||
|
'css/theme/dark.custom.css',
|
||||||
|
'css/theme/light.custom.min.css',
|
||||||
|
'css/theme/dark.custom.min.css',
|
||||||
|
'css/user.css',
|
||||||
|
'css/user.min.css',
|
||||||
|
'js/user.js',
|
||||||
|
'js/user.min.js',
|
||||||
|
];
|
||||||
|
|
||||||
|
$copied = 0;
|
||||||
|
foreach ($userFiles as $relPath) {
|
||||||
|
$src = $oldMedia . '/' . $relPath;
|
||||||
|
$dst = $newMedia . '/' . $relPath;
|
||||||
|
if (is_file($src) && !is_file($dst)) {
|
||||||
|
$dstDir = dirname($dst);
|
||||||
|
if (!is_dir($dstDir)) {
|
||||||
|
mkdir($dstDir, 0755, true);
|
||||||
|
}
|
||||||
|
copy($src, $dst);
|
||||||
|
$copied++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($copied > 0) {
|
||||||
|
$this->logMessage("Copied {$copied} user file(s) from MokoCassiopeia.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function syncCustomVariables(InstallerAdapter $parent): int
|
private function syncCustomVariables(InstallerAdapter $parent): int
|
||||||
{
|
{
|
||||||
$templateDir = $parent->getParent()->getPath('source');
|
$templateDir = $parent->getParent()->getPath('source');
|
||||||
|
|
||||||
// The sync script lives alongside this script in the template root.
|
|
||||||
$syncScript = $templateDir . '/sync_custom_vars.php';
|
$syncScript = $templateDir . '/sync_custom_vars.php';
|
||||||
|
|
||||||
if (!is_file($syncScript)) {
|
if (!is_file($syncScript)) {
|
||||||
@@ -172,20 +260,13 @@ class Tpl_MokoonyxInstallerScript
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$joomlaRoot = JPATH_ROOT;
|
$results = MokoCssVarSync::run(JPATH_ROOT);
|
||||||
$results = MokoCssVarSync::run($joomlaRoot);
|
|
||||||
|
|
||||||
$totalAdded = 0;
|
$totalAdded = 0;
|
||||||
foreach ($results as $filePath => $result) {
|
foreach ($results as $filePath => $result) {
|
||||||
$totalAdded += count($result['added']);
|
$totalAdded += count($result['added']);
|
||||||
if (!empty($result['added'])) {
|
if (!empty($result['added'])) {
|
||||||
$this->logMessage(
|
$this->logMessage(sprintf('CSS sync: added %d variable(s) to %s', count($result['added']), basename($filePath)));
|
||||||
sprintf(
|
|
||||||
'CSS sync: added %d variable(s) to %s',
|
|
||||||
count($result['added']),
|
|
||||||
basename($filePath)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,12 +277,6 @@ class Tpl_MokoonyxInstallerScript
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Log a message to Joomla's log system.
|
|
||||||
*
|
|
||||||
* @param string $message The log message.
|
|
||||||
* @param string $priority Log priority (info, warning, error).
|
|
||||||
*/
|
|
||||||
private function logMessage(string $message, string $priority = 'info'): void
|
private function logMessage(string $message, string $priority = 'info'): void
|
||||||
{
|
{
|
||||||
$priorities = [
|
$priorities = [
|
||||||
|
|||||||
@@ -39,13 +39,13 @@
|
|||||||
</server>
|
</server>
|
||||||
</updateservers>
|
</updateservers>
|
||||||
<name>MokoOnyx</name>
|
<name>MokoOnyx</name>
|
||||||
<version>01.00.00</version>
|
<version>01.00.07</version>
|
||||||
<scriptfile>script.php</scriptfile>
|
<scriptfile>script.php</scriptfile>
|
||||||
<creationDate>2026-04-15</creationDate>
|
<creationDate>2026-04-15</creationDate>
|
||||||
<author>Jonathan Miller || Moko Consulting</author>
|
<author>Jonathan Miller || Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<copyright>(C)GNU General Public License Version 3 - 2026 Moko Consulting</copyright>
|
<copyright>(C)GNU General Public License Version 3 - 2026 Moko Consulting</copyright>
|
||||||
<description><![CDATA[<p><img src="https://img.shields.io/badge/version-01.00.00-blue.svg?logo=v&logoColor=white" alt="Version 01.00.00" /> <img src="https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&logoColor=white" alt="License" /> <img src="https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&logoColor=white" alt="Joomla" /> <img src="https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&logoColor=white" alt="PHP" /></p> <h3>MokoOnyx Template Description</h3> <p> <strong>MokoOnyx</strong> continues Joomla's tradition of space-themed default templates— building on the legacy of <em>Solarflare</em> (Joomla 1.0), <em>Milkyway</em> (Joomla 1.5), and <em>Protostar</em> (Joomla 3.0). </p> <p> This template is a customized fork of the <strong>Cassiopeia</strong> template introduced in Joomla 4, preserving its modern, accessible, and mobile-first foundation while introducing new stylistic enhancements and structural refinements specifically tailored for use by Moko Consulting. </p> <h4>Custom Colour Themes</h4> <p> Starter palette files are included with the template. To create a custom colour scheme, copy <code>templates/mokoonyx/templates/light.custom.css</code> to <code>media/templates/site/mokoonyx/css/theme/light.custom.css</code>, or <code>templates/mokoonyx/templates/dark.custom.css</code> to <code>media/templates/site/mokoonyx/css/theme/dark.custom.css</code>. Customise the CSS variables to match your brand, then activate your palette in <em>System → Site Templates → MokoOnyx → Theme tab</em> by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the <em>CSS Variables</em> tab in template options. </p> <h4>Custom CSS & JavaScript</h4> <p> For site-specific styles and scripts that should survive template updates, create the following files: </p> <ul> <li><code>media/templates/site/mokoonyx/css/user.css</code> — loaded on every page for custom CSS overrides.</li> <li><code>media/templates/site/mokoonyx/js/user.js</code> — loaded on every page for custom JavaScript.</li> </ul> <p> These files are gitignored and will not be overwritten by template updates. </p> <h4>Code Attribution</h4> <p> This template is based on the original <strong>Cassiopeia</strong> template developed by the <a href="https://www.joomla.org" target="_blank" rel="noopener">Joomla! Project</a> and released under the GNU General Public License. </p> <p> Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards. </p> <p> It includes integration with <a href="https://afeld.github.io/bootstrap-toc/" target="_blank" rel="noopener">Bootstrap TOC</a>, an open-source table of contents generator by A. Feld, licensed under the MIT License. </p> <p> All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable. </p>]]></description>
|
<description><![CDATA[<div style="padding:1rem;border:2px solid #059669;border-radius:8px;background:#ecfdf5;margin-bottom:1rem;"> <h3 style="color:#065f46;margin:0 0 .5rem;">Migrating from MokoCassiopeia?</h3> <p style="margin:0 0 .75rem;">MokoOnyx automatically imports your MokoCassiopeia settings on first use. To trigger the migration:</p> <ol style="margin:0 0 .75rem 1.5rem;"> <li>Install MokoOnyx via <strong>System → Install → Extensions</strong></li> <li>Go to <strong>System → Site Templates</strong> and set <strong>MokoOnyx</strong> as your default template</li> <li><strong>Visit any page on your site</strong> — the migration runs automatically on first page load</li> <li>Check <strong>administrator/logs/mokoonyx_migrate.log.php</strong> to confirm migration completed</li> <li>Uninstall MokoCassiopeia from <strong>Extensions → Manage</strong></li> </ol> <p style="margin:0;font-size:.9em;color:#065f46;"> <strong>What gets migrated:</strong> template style params, custom colour palettes (light.custom.css, dark.custom.css), user.css, user.js, and update server URL. To re-run the migration, delete <code>templates/mokoonyx/.migrated</code> and reload any page. </p></div> <p><img src="https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&logoColor=white" alt="License" /> <img src="https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&logoColor=white" alt="Joomla" /> <img src="https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&logoColor=white" alt="PHP" /></p> <h3>MokoOnyx</h3> <p> <strong>MokoOnyx</strong> (formerly MokoCassiopeia) is a modern, lightweight enhancement layer built on Joomla's Cassiopeia template. It adds Font Awesome 7, Bootstrap 5 helpers, automatic Table of Contents, advanced Dark Mode theming, and optional Google Tag Manager / GA4 integration — all with minimal core overrides for maximum upgrade compatibility. </p> <h4>Custom Colour Themes</h4> <p> Copy <code>templates/mokoonyx/templates/light.custom.css</code> to <code>media/templates/site/mokoonyx/css/theme/light.custom.css</code> (or dark equivalent), customise the CSS variables, then select "Custom" in the Theme tab. </p> <h4>Custom CSS & JavaScript</h4> <ul> <li><code>media/templates/site/mokoonyx/css/user.css</code> — custom CSS overrides</li> <li><code>media/templates/site/mokoonyx/js/user.js</code> — custom JavaScript</li> </ul> <p>These files survive template updates.</p>]]></description>
|
||||||
<inheritable>1</inheritable>
|
<inheritable>1</inheritable>
|
||||||
<files>
|
<files>
|
||||||
<filename>component.php</filename>
|
<filename>component.php</filename>
|
||||||
@@ -108,6 +108,10 @@
|
|||||||
|
|
||||||
<fields name="params">
|
<fields name="params">
|
||||||
<!-- Advanced tab (non-theme/system options only) -->
|
<!-- Advanced tab (non-theme/system options only) -->
|
||||||
|
<fieldset name="migration" label="TPL_MOKOONYX_MIGRATION_FIELDSET_LABEL">
|
||||||
|
<field name="migration_note" type="note" label="TPL_MOKOONYX_MIGRATION_NOTE_LABEL" description="TPL_MOKOONYX_MIGRATION_NOTE_DESC" />
|
||||||
|
<field name="migration_run" type="note" label="TPL_MOKOONYX_MIGRATION_RUN_LABEL" description="TPL_MOKOONYX_MIGRATION_RUN_DESC" />
|
||||||
|
</fieldset>
|
||||||
<fieldset name="advanced">
|
<fieldset name="advanced">
|
||||||
<field name="developmentmode" type="radio" label="TPL_MOKOONYX_DEVELOPMENTMODE_LABEL" description="TPL_MOKOONYX_DEVELOPMENTMODE_DESC" default="0" layout="joomla.form.field.radio.switcher" filter="boolean">
|
<field name="developmentmode" type="radio" label="TPL_MOKOONYX_DEVELOPMENTMODE_LABEL" description="TPL_MOKOONYX_DEVELOPMENTMODE_DESC" default="0" layout="joomla.form.field.radio.switcher" filter="boolean">
|
||||||
<option value="0">JNO</option>
|
<option value="0">JNO</option>
|
||||||
|
|||||||
10
updates.xml
10
updates.xml
@@ -13,13 +13,13 @@
|
|||||||
<element>mokoonyx</element>
|
<element>mokoonyx</element>
|
||||||
<type>template</type>
|
<type>template</type>
|
||||||
<client>site</client>
|
<client>site</client>
|
||||||
<version>01.00.00</version>
|
<version>01.00.06</version>
|
||||||
<creationDate>2026-04-19</creationDate>
|
<creationDate>2026-04-21</creationDate>
|
||||||
<infourl title='MokoOnyx Dev'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/development</infourl>
|
<infourl title='MokoOnyx Dev'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/development</infourl>
|
||||||
<downloads>
|
<downloads>
|
||||||
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/development/mokoonyx-01.00.00-dev.zip</downloadurl>
|
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/development/mokoonyx-01.00.06-dev.zip</downloadurl>
|
||||||
</downloads>
|
</downloads>
|
||||||
<sha256></sha256>
|
<sha256>db041f2dedd84d1119b860ddd27fd613ab0c86ff7303a154b321926d2f369d11</sha256>
|
||||||
<tags><tag>development</tag></tags>
|
<tags><tag>development</tag></tags>
|
||||||
<maintainer>Moko Consulting</maintainer>
|
<maintainer>Moko Consulting</maintainer>
|
||||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/v01/mokoonyx-01.00.00.zip</downloadurl>
|
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/v01/mokoonyx-01.00.00.zip</downloadurl>
|
||||||
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoOnyx/releases/download/v01/mokoonyx-01.00.00.zip</downloadurl>
|
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoOnyx/releases/download/v01/mokoonyx-01.00.00.zip</downloadurl>
|
||||||
</downloads>
|
</downloads>
|
||||||
<sha256></sha256>
|
<sha256>954c26f29af533c58658ed312b4b6261cc9e783dcf0cd9d879d34df6e8a421f4</sha256>
|
||||||
<tags><tag>stable</tag></tags>
|
<tags><tag>stable</tag></tags>
|
||||||
<maintainer>Moko Consulting</maintainer>
|
<maintainer>Moko Consulting</maintainer>
|
||||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||||
|
|||||||
Reference in New Issue
Block a user