Compare commits

..

1 Commits

Author SHA1 Message Date
gitea-actions[bot] ec751b6ddb chore(release): build 02.13.00 [skip ci] 2026-05-28 16:01:47 +00:00
35 changed files with 1724 additions and 3007 deletions
+2 -2
View File
@@ -8,14 +8,14 @@
<name>MokoWaaS</name>
<org>MokoConsulting</org>
<description>White-label identity, security hardening, and tenant restriction layer for WaaS-managed Joomla environments</description>
<version>02.20.00</version>
<version>02.13.00</version>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity>
<governance>
<platform>joomla</platform>
<standards-version>05.00.00</standards-version>
<standards-source>https://git.mokoconsulting.tech/MokoConsulting/moko-platform</standards-source>
<last-synced>2026-05-28T20:00:00+00:00</last-synced>
<last-synced>2026-05-21T20:48:00+00:00</last-synced>
</governance>
<build>
<language>PHP</language>
+84 -85
View File
@@ -1,85 +1,84 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.mokogitea/workflows/auto-bump.yml
# VERSION: 09.02.00
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
name: "Universal: Auto Version Bump"
on:
push:
branches:
- dev
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
permissions:
contents: write
jobs:
bump:
name: Version Bump
runs-on: release
if: >-
!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip bump]') &&
!startsWith(github.event.head_commit.message, 'Merge pull request')
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup moko-platform tools
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
if [ -d "/opt/moko-platform/cli" ]; then
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
else
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
fi
- name: Bump version
run: |
BUMP=$(php ${MOKO_CLI}/version_bump.php --path . 2>&1) || true
echo "$BUMP"
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) || true
[ -z "$VERSION" ] && { echo "No version found — skipping"; exit 0; }
# Propagate to platform manifests with -dev suffix
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch dev --stability dev 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
VERSION="${VERSION}-dev"
# Commit if anything changed
if git diff --quiet && git diff --cached --quiet; then
echo "No version changes to commit"
exit 0
fi
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push origin dev
echo "Bumped to ${VERSION}" >> $GITHUB_STEP_SUMMARY
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.mokogitea/workflows/auto-bump.yml
# VERSION: 09.02.00
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
name: "Universal: Auto Version Bump"
on:
push:
branches:
- dev
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
permissions:
contents: write
jobs:
bump:
name: Version Bump
runs-on: release
if: >-
!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip bump]') &&
!startsWith(github.event.head_commit.message, 'Merge pull request')
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.GA_TOKEN }}
fetch-depth: 1
- name: Setup moko-platform tools
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
if [ -d "/opt/moko-platform/cli" ]; then
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
else
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
fi
- name: Bump version
run: |
BUMP=$(php ${MOKO_CLI}/version_bump.php --path . 2>&1) || true
echo "$BUMP"
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) || true
[ -z "$VERSION" ] && { echo "No version found — skipping"; exit 0; }
# Propagate to platform manifests
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch dev 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Commit if anything changed
if git diff --quiet && git diff --cached --quiet; then
echo "No version changes to commit"
exit 0
fi
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:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git commit -m "chore(version): patch bump to ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push origin dev
echo "Bumped to ${VERSION}" >> $GITHUB_STEP_SUMMARY
File diff suppressed because it is too large Load Diff
+48 -48
View File
@@ -1,48 +1,48 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.mokogitea/workflows/branch-cleanup.yml
# VERSION: 01.00.00
# BRIEF: Delete feature branches after PR merge
name: "Branch Cleanup"
on:
pull_request:
types: [closed]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
cleanup:
name: Delete merged branch
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == true &&
github.event.pull_request.head.ref != 'dev' &&
github.event.pull_request.head.ref != 'main'
steps:
- name: Delete source branch
run: |
BRANCH="${{ github.event.pull_request.head.ref }}"
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BRANCH}', safe=''))")
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API}/${ENCODED}" 2>/dev/null || true)
if [ "$STATUS" = "204" ]; then
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
elif [ "$STATUS" = "404" ]; then
echo "Branch already deleted: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
else
echo "::warning::Failed to delete branch ${BRANCH} (HTTP ${STATUS})"
fi
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.mokogitea/workflows/branch-cleanup.yml
# VERSION: 01.00.00
# BRIEF: Delete feature branches after PR merge
name: "Branch Cleanup"
on:
pull_request:
types: [closed]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
cleanup:
name: Delete merged branch
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == true &&
github.event.pull_request.head.ref != 'dev' &&
github.event.pull_request.head.ref != 'main'
steps:
- name: Delete source branch
run: |
BRANCH="${{ github.event.pull_request.head.ref }}"
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BRANCH}', safe=''))")
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API}/${ENCODED}" 2>/dev/null || true)
if [ "$STATUS" = "204" ]; then
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
elif [ "$STATUS" = "404" ]; then
echo "Branch already deleted: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
else
echo "::warning::Failed to delete branch ${BRANCH} (HTTP ${STATUS})"
fi
+10 -10
View File
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Maintenance
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# INGROUP: MokoStandards.Maintenance
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/cascade-dev.yml.template
# VERSION: 02.00.00
# BRIEF: Forward-merge main → all open branches after every push to main
@@ -52,7 +52,7 @@ jobs:
- name: Discover target branches
id: branches
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
@@ -61,7 +61,7 @@ jobs:
ALL_BRANCHES=""
while true; do
BATCH=$(curl -sS \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/branches?page=${PAGE}&limit=50" \
| jq -r '.[].name // empty')
[ -z "$BATCH" ] && break
@@ -93,7 +93,7 @@ jobs:
- name: Cascade to all target branches
if: steps.branches.outputs.targets != ''
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
SHORT_SHA="${GITHUB_SHA:0:7}"
@@ -111,7 +111,7 @@ jobs:
# Check if branch is already up to date
ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g')
RESPONSE=$(curl -sS \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/compare/${ENCODED_BRANCH}...main")
AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0')
@@ -126,7 +126,7 @@ jobs:
# Check for existing cascade PR
EXISTING=$(curl -sS \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1")
EXISTING_COUNT=$(echo "$EXISTING" | jq 'length')
@@ -139,7 +139,7 @@ jobs:
# Create cascade PR
PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \
-X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\",
@@ -165,7 +165,7 @@ jobs:
# Try auto-merge
PR_DATA=$(curl -sS \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/pulls/${PR_NUMBER}")
MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false')
@@ -178,7 +178,7 @@ jobs:
MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \
-X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"Do\": \"merge\",
+8 -8
View File
@@ -43,9 +43,9 @@ jobs:
- name: Clone MokoStandards
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
run: |
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
@@ -53,7 +53,7 @@ jobs:
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install \
@@ -346,7 +346,7 @@ jobs:
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install \
@@ -391,7 +391,7 @@ jobs:
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install --no-interaction --prefer-dist --optimize-autoloader
@@ -458,10 +458,10 @@ jobs:
steps:
- name: Trigger pre-release build
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GA_TOKEN: ${{ secrets.GA_TOKEN }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }}
run: |
curl -s -X POST "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
curl -s -X POST "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
+9 -9
View File
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# INGROUP: MokoStandards.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/cleanup.yml
# VERSION: 01.00.00
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
@@ -33,17 +33,17 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
token: ${{ secrets.GA_TOKEN }}
- name: Delete merged branches
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
echo "=== Merged Branch Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
# List branches via API
BRANCHES=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
"${API}/branches?limit=50" | jq -r '.[].name')
DELETED=0
@@ -56,7 +56,7 @@ jobs:
# Check if branch is merged into main
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
echo " Deleting merged branch: ${BRANCH}"
curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
"${API}/branches/${BRANCH}" 2>/dev/null || true
DELETED=$((DELETED + 1))
fi
@@ -66,20 +66,20 @@ jobs:
- name: Clean old workflow runs
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
echo "=== Workflow Run Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
# Get old completed runs
RUNS=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
"${API}/actions/runs?status=completed&limit=50" | \
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
DELETED=0
for RUN_ID in $RUNS; do
curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
DELETED=$((DELETED + 1))
done
+2 -2
View File
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Security
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/gitleaks.yml.template
# VERSION: 01.00.00
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
steps:
- name: Create branch and comment
run: |
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
ISSUE_NUM="${{ github.event.issue.number }}"
ISSUE_TITLE="${{ github.event.issue.title }}"
+3 -2
View File
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# INGROUP: MokoStandards.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/notify.yml
# VERSION: 01.00.00
# BRIEF: Push notifications via ntfy on release success or workflow failure
@@ -18,6 +18,7 @@ on:
- "Joomla Build & Release"
- "Joomla Extension CI"
- "Deploy"
- "Cascade Main → Dev"
types:
- completed
+35 -7
View File
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/universal/pr-check.yml.template
# VERSION: 05.00.00
# BRIEF: PR gate — branch policy + code validation before merge
@@ -108,9 +108,8 @@ jobs:
- name: Detect platform
id: platform
run: |
# Read platform from XML manifest (<platform> tag) or plain text fallback
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
# Parse manifest for platform detection
PLATFORM=$(php /tmp/mokostandards-api/cli/manifest_read.php --path . --field platform 2>/dev/null)
[ -z "$PLATFORM" ] && PLATFORM="generic"
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
@@ -195,6 +194,35 @@ jobs:
echo "Source: ${FILE_COUNT} files"
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
# ── Changelog Gate ────────────────────────────────────────────────────
changelog:
name: Changelog Updated
runs-on: ubuntu-latest
if: github.base_ref == 'main'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check CHANGELOG.md was updated
run: |
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
if git diff --name-only "$BASE" "$HEAD" | grep -q "^CHANGELOG.md$"; then
echo "CHANGELOG.md updated"
else
# Allow [skip changelog] in PR title or body
PR_TITLE="${{ github.event.pull_request.title }}"
PR_BODY="${{ github.event.pull_request.body }}"
if echo "$PR_TITLE $PR_BODY" | grep -qi "\[skip changelog\]"; then
echo "::warning::Changelog skip requested via [skip changelog]"
exit 0
fi
echo "::error::CHANGELOG.md must be updated before merging to main. Add [skip changelog] to the PR title to bypass."
exit 1
fi
# ── Pre-Release RC Build ─────────────────────────────────────────────────
pre-release:
name: Build RC Package
@@ -204,11 +232,11 @@ jobs:
steps:
- name: Trigger RC pre-release
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GA_TOKEN: ${{ secrets.GA_TOKEN }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
+223 -233
View File
@@ -1,233 +1,223 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.01.00
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
name: "Universal: Pre-Release"
on:
pull_request:
types: [closed]
branches:
- dev
workflow_dispatch:
inputs:
stability:
description: 'Pre-release channel'
required: true
type: choice
options:
- development
- alpha
- beta
- release-candidate
permissions:
contents: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
jobs:
build:
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
runs-on: release
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
- name: Detect platform
id: platform
run: |
php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve metadata and bump version
id: meta
run: |
STABILITY="${{ inputs.stability || 'development' }}"
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac
# Read current version (bump already handled by push workflow)
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
[ -z "$VERSION" ] && VERSION="00.00.01"
# Strip any existing suffix from version before applying stability
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
# Verify version consistency across all files
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Update VERSION variable with suffix
if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}"
fi
# Commit version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
git push origin HEAD 2>&1
}
# Auto-detect element via manifest_element.php
php ${MOKO_CLI}/manifest_element.php \
--path . --version "$VERSION" --stability "$STABILITY" \
--repo "${GITEA_REPO}" --github-output
# Read back element outputs
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
- name: Create release
id: release
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch dev --prerelease
- name: Build package and upload
id: package
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_package.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true
- name: Update updates.xml
if: steps.platform.outputs.platform == 'joomla'
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
if [ ! -f "updates.xml" ]; then
echo "No updates.xml -- skipping"
exit 0
fi
SHA_FLAG=""
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
php ${MOKO_CLI}/updates_xml_build.php \
--path . --version "${VERSION}" --stability "${STABILITY}" \
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
${SHA_FLAG}
# Commit and push
if ! git diff --quiet updates.xml 2>/dev/null; then
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git add updates.xml
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
git push origin HEAD 2>&1 || echo "WARNING: push failed"
fi
- name: "Sync updates.xml to all branches"
if: steps.platform.outputs.platform == 'joomla'
run: |
CURRENT_BRANCH="${{ github.ref_name }}"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
for BRANCH in main dev; do
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
echo "Syncing updates.xml -> ${BRANCH}"
git fetch origin "${BRANCH}" 2>/dev/null || continue
git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue
git checkout "${CURRENT_BRANCH}" -- updates.xml
if ! git diff --quiet updates.xml 2>/dev/null; then
git add updates.xml
git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
fi
git checkout "${CURRENT_BRANCH}" 2>/dev/null
done
- name: "Delete lesser pre-release channels (cascade)"
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
php ${MOKO_CLI}/release_cascade.php \
--stability "${{ steps.meta.outputs.stability }}" \
--token "${TOKEN}" \
--api-base "${API_BASE}"
- name: Summary
if: always()
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.01.00
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
name: "Universal: Pre-Release"
on:
pull_request:
types: [closed]
branches:
- dev
workflow_dispatch:
inputs:
stability:
description: 'Pre-release channel'
required: true
type: choice
options:
- development
- alpha
- beta
- release-candidate
permissions:
contents: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
jobs:
build:
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
runs-on: release
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GA_TOKEN }}
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
- name: Detect platform
id: platform
run: |
php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve metadata and bump version
id: meta
run: |
STABILITY="${{ inputs.stability || 'development' }}"
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac
# Read current version (bump already handled by push workflow)
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
[ -z "$VERSION" ] && VERSION="00.00.01"
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch "${{ github.ref_name }}" 2>/dev/null || true
# Verify version consistency across all files
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Commit version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
git push origin HEAD 2>&1
}
# Auto-detect element via manifest_element.php
php ${MOKO_CLI}/manifest_element.php \
--path . --version "$VERSION" --stability "$STABILITY" \
--repo "${GITEA_REPO}" --github-output
# Read back element outputs
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
- name: Create release
id: release
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch dev --prerelease
- name: Build package and upload
id: package
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_package.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true
- name: Update updates.xml
if: steps.platform.outputs.platform == 'joomla'
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
if [ ! -f "updates.xml" ]; then
echo "No updates.xml -- skipping"
exit 0
fi
SHA_FLAG=""
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
php ${MOKO_CLI}/updates_xml_build.php \
--path . --version "${VERSION}" --stability "${STABILITY}" \
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
${SHA_FLAG}
# Commit and push
if ! git diff --quiet updates.xml 2>/dev/null; then
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git add updates.xml
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
git push origin HEAD 2>&1 || echo "WARNING: push failed"
fi
- name: "Sync updates.xml to all branches"
if: steps.platform.outputs.platform == 'joomla'
run: |
CURRENT_BRANCH="${{ github.ref_name }}"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
for BRANCH in main dev; do
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
echo "Syncing updates.xml -> ${BRANCH}"
git fetch origin "${BRANCH}" 2>/dev/null || continue
git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue
git checkout "${CURRENT_BRANCH}" -- updates.xml
if ! git diff --quiet updates.xml 2>/dev/null; then
git add updates.xml
git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
fi
git checkout "${CURRENT_BRANCH}" 2>/dev/null
done
- name: "Delete lesser pre-release channels (cascade)"
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.GA_TOKEN }}"
php ${MOKO_CLI}/release_cascade.php \
--stability "${{ steps.meta.outputs.stability }}" \
--token "${TOKEN}" \
--api-base "${API_BASE}"
- name: Summary
if: always()
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
+84 -87
View File
@@ -7,14 +7,18 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Validation
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# INGROUP: MokoStandards.Validation
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/joomla/repo_health.yml.template
# VERSION: 04.06.00
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
# ============================================================================
name: "Generic: Repo Health"
name: "Joomla: Repo Health"
concurrency:
group: repo-health-${{ github.repository }}-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
@@ -49,7 +53,7 @@ env:
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
# Repo health policy
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.gitea/workflows/
REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
REPO_DISALLOWED_DIRS:
REPO_DISALLOWED_FILES: TODO.md,todo.md
@@ -60,7 +64,7 @@ env:
# File / directory variables
DOCS_INDEX: docs/docs-index.md
SCRIPT_DIR: scripts
WORKFLOWS_DIR: .mokogitea/workflows
WORKFLOWS_DIR: .gitea/workflows
SHELLCHECK_PATTERN: '*.sh'
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
@@ -81,7 +85,7 @@ jobs:
- name: Check actor permission (admin only)
id: perm
env:
TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
REPO: ${{ github.repository }}
ACTOR: ${{ github.actor }}
run: |
@@ -284,7 +288,7 @@ jobs:
exit 0
fi
if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi
IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"
IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
missing_dirs=()
@@ -388,27 +392,23 @@ jobs:
exit 0
fi
IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi
IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}"
missing_required=()
missing_optional=()
# Source directory: src/ or htdocs/ (either is valid for extension repos)
SOURCE_DIR=""
# Source directory: src/ or htdocs/ (either is valid)
if [ -d "src" ]; then
SOURCE_DIR="src"
elif [ -d "htdocs" ]; then
SOURCE_DIR="htdocs"
elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
# Platform/tooling repos don't need src/
SOURCE_DIR=""
else
missing_required+=("src/ or htdocs/ (source directory required)")
fi
IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"
IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}"
missing_required=()
missing_optional=()
for item in "${required_artifacts[@]}"; do
if printf '%s' "${item}" | grep -q '/$'; then
d="${item%/}"
@@ -450,8 +450,12 @@ jobs:
fi
done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then
missing_required+=("dev or dev/* branch")
if [ "${#dev_paths[@]}" -eq 0 ]; then
missing_required+=("dev/* branch (e.g. dev/01.00.00)")
fi
if [ "${#dev_branches[@]}" -gt 0 ]; then
missing_required+=("invalid branch dev (must be dev/<version>)")
fi
content_warnings=()
@@ -477,7 +481,26 @@ jobs:
export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}")
report_json="$(python3 - <<'PY'
import json
import os
profile = os.environ.get('PROFILE_RAW') or 'all'
missing_required = os.environ.get('MISSING_REQUIRED', '').splitlines() if os.environ.get('MISSING_REQUIRED') else []
missing_optional = os.environ.get('MISSING_OPTIONAL', '').splitlines() if os.environ.get('MISSING_OPTIONAL') else []
content_warnings = os.environ.get('CONTENT_WARNINGS', '').splitlines() if os.environ.get('CONTENT_WARNINGS') else []
out = {
'profile': profile,
'missing_required': [x for x in missing_required if x],
'missing_optional': [x for x in missing_optional if x],
'content_warnings': [x for x in content_warnings if x],
}
print(json.dumps(out, indent=2))
PY
)"
{
printf '%s\n' '### Repository health'
@@ -555,14 +578,12 @@ jobs:
joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
fi
if [ -n "${SOURCE_DIR}" ]; then
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
for dir in "${INDEX_DIRS[@]}"; do
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
joomla_findings+=("${dir}/index.html missing (directory listing protection)")
fi
done
fi
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
for dir in "${INDEX_DIRS[@]}"; do
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
joomla_findings+=("${dir}/index.html missing (directory listing protection)")
fi
done
if [ "${#joomla_findings[@]}" -gt 0 ]; then
{
@@ -608,29 +629,43 @@ jobs:
fi
if [ -f "${DOCS_INDEX}" ]; then
missing_links=""
while IFS= read -r docline; do
for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do
case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac
linkpath="${link%%#*}"
linkpath="${linkpath%%\?*}"
[ -z "$linkpath" ] && continue
if [ "${linkpath:0:1}" = "/" ]; then
testpath="${linkpath#/}"
else
testpath="$(dirname "${DOCS_INDEX}")/${linkpath}"
fi
[ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} "
done
done < "${DOCS_INDEX}"
missing_links="$(python3 - <<'PY'
import os
import re
idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md')
base = os.getcwd()
bad = []
pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)')
with open(idx, 'r', encoding='utf-8') as f:
for line in f:
for m in pat.findall(line):
link = m.strip()
if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'):
continue
if link.startswith('/'):
rel = link.lstrip('/')
else:
rel = os.path.normpath(os.path.join(os.path.dirname(idx), link))
rel = rel.split('#', 1)[0]
rel = rel.split('?', 1)[0]
if not rel:
continue
p = os.path.join(base, rel)
if not os.path.exists(p):
bad.append(rel)
print('\n'.join(sorted(set(bad))))
PY
)"
if [ -n "${missing_links}" ]; then
extended_findings+=("docs/docs-index.md contains broken relative links")
{
printf '%s\n' '### Docs index link integrity'
printf '%s\n' 'Broken relative links:'
for bl in ${missing_links}; do
printf '%s\n' "- ${bl}"
done
while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}"
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
@@ -729,41 +764,3 @@ jobs:
fi
printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
site-health:
name: Site Health
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch'
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- name: Uptime check
if: env.URLS != ''
run: |
echo "$URLS" > /tmp/urls.txt
php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down"
rm -f /tmp/urls.txt
env:
URLS: ${{ vars.MONITORED_URLS }}
- name: SSL certificate check
if: env.DOMAINS != ''
run: |
echo "$DOMAINS" > /tmp/domains.txt
php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon"
rm -f /tmp/domains.txt
env:
DOMAINS: ${{ vars.MONITORED_DOMAINS }}
- name: Summary
if: always()
run: |
echo "### Site Health" >> $GITHUB_STEP_SUMMARY
echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY
+2 -18
View File
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/security-audit.yml
# VERSION: 01.00.00
# BRIEF: Dependency vulnerability scanning for composer and npm packages
@@ -80,19 +80,3 @@ jobs:
-H "Priority: high" \
-d "Security audit found vulnerabilities. Review dependency updates." \
"${NTFY_URL}/${NTFY_TOPIC}" || true
- name: Joomla version audit
if: always()
run: |
if [ -f "monitoring/joomla-version-audit.php" ] && [ -n "$JOOMLA_SITES" ]; then
echo "$JOOMLA_SITES" > /tmp/sites.json
php monitoring/joomla-version-audit.php --sites /tmp/sites.json || true
echo "### Joomla Version Audit" >> $GITHUB_STEP_SUMMARY
rm -f /tmp/sites.json
else
echo "Joomla audit skipped (no script or JOOMLA_SITES_JSON not configured)"
fi
env:
JOOMLA_SITES: ${{ vars.JOOMLA_SITES_JSON }}
+304 -312
View File
@@ -1,312 +1,304 @@
# 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
# 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.GA_TOKEN }}
fetch-depth: 0
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_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
if [ -d "/tmp/moko-platform" ]; then
echo "moko-platform already available — skipping clone"
else
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
fi
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 }}"
# Auto-bump patch version
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
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")
# Propagate version to all manifest files
php ${MOKO_CLI}/version_set_platform.php --path . --version "$VERSION" --branch "$BRANCH" 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# 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
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
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "display_version=${VERSION}${SUFFIX}" >> "$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.GA_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.GA_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 config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
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}"
GA_TOKEN="${{ secrets.GA_TOKEN }}"
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_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 ${GA_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.GA_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
+379 -12
View File
@@ -19,23 +19,13 @@
-->
# Changelog
## [Unreleased]
## [02.20.00] --- 2026-05-28
## [02.20.00] --- 2026-05-28
## [02.19.00] --- 2026-05-28
## [02.18.00] --- 2026-05-28
All notable changes to the MokoWaaS plugin will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [02.17.00] --- 2026-05-28
## [Unreleased]
### Changed
- Migrated all workflow and template paths from `.github/` to `.mokogitea/`
@@ -44,7 +34,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- `branch-cleanup.yml`: auto-delete merged feature branches after PR merge
- `plg_webservices_perfectpublisher`: REST API for Perfect Publisher (com_autotweet) — channels, posts, requests, rules, feeds, and stats
### Planned
- License/subscription check
@@ -61,3 +50,381 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- updates.xml: removed stale pre-release entries pointing to non-existent dev artifacts, legacy plugin update entry that caused stable sites to attempt dev downloads
- Removed duplicate `<updateservers>` from inner plugin manifest — only the package-level manifest should register the update server
- Auto-cleanup of stale plugin-level update site entries on install/update (cleans `#__update_sites` and `#__update_sites_extensions`)
## [02.06.00] - 2026-05-25
### Added
- Alias offline bypass: aliases with offline=No override Joomla's global offline setting, allowing access via alias domain while main site is down
- Block non-master users from viewing or editing MokoWaaS plugin settings
- Master user bypasses ALL tenant restrictions (install from URL, global config, sysinfo, installer, templates)
- Admin Help menu redirected to configured support URL (replaces help.joomla.org)
### Fixed
- Install API endpoint: extract ZIP to temp directory before passing to Joomla Installer (was passing ZIP path directly)
- Clean up extracted temp directory on success or failure
- Update site disabled by Joomla when protected=1 — ensureProtectedFlag() now re-enables it
- CI: auto-release fetches updates.xml from main before building (preserves all channels)
- CI: version_check.php --fix runs after version bump in both workflows
- CI: checksums attached as `[filename].sha256` files instead of in release body
- CI: auto-release cleaned up — removed duplicate asset loops, inline Python updater, dead code
- CI: Step 8b uses `release_body_update.php` CLI instead of inline Python
- CI: Step 6 tag creation no longer gated by `is_minor` (was never set)
### Changed
- CI: auto-release uses stream tag `stable` instead of version tag `vXX`
- CI: pre-release package build uses absolute paths (fixes relative path zip error)
## [02.05.00] - 2026-05-24
### Added
- Joomla `protected=1` flag on all MokoWaaS extensions (framework-level disable/uninstall prevention)
- Self-healing protected flag — restored each admin session if cleared
- Block non-master disable via plugin list toggle (`plugins.publish`)
- Package script sets `protected=1, locked=0` on every install/update
- Legacy plugin entry in updates.xml for sites upgrading from standalone plugin
### Fixed
- CI: auto-release workflow `pkg_pkg_` duplication in release names, ZIP filenames, and SHA256 paths
- CI: auto-release now strips existing type prefix and uses `<packagename>` for packages
- CI: `updates_xml_build` was cascading entries for all lower channels on stable release — now writes only current channel
- CI: `targetplatform` regex `((5.[0-9])|(6.[0-9]))` caused Gitea 500 on XML render — simplified to `(5|6)\..*`
- updates.xml stable entry now has correct `<tag>stable</tag>` and download URL
- README slimmed to overview, detailed content moved to wiki
## [02.03.10] - 2026-05-24
### Added
- Canonical URL injection for alias domains (prevents SEO duplication)
- Primary Domain config field in Site Aliases tab
- Heartbeat registration for alias domains (each alias gets Grafana datasource)
- Plugin protection: hidden from non-master users, disable/uninstall blocked
- Dynamic plugin version read from manifest XML (no more hardcoded strings)
- Package structure: `pkg_mokowaas` with system plugin, webservices plugin, and component
### Changed
- Alias offline mode uses Joomla's native template offline.php (not custom HTML)
- Alias detection simplified: direct lookup in aliases list (no primary host comparison)
- handleSiteAlias() moved to onAfterRoute (client type resolved at that point)
- Package script.php enables plugins on every install/update and sends heartbeat
### Fixed
- Alias domain matching: strip trailing slashes, handle Joomla subform stdClass format
- Backend redirect: use primary_domain setting instead of Uri::root() (returned alias domain on mirrors)
- CI: version_bump reads manifest XML with priority over README.md VERSION header
- CI: version bump occurs after release build, not before
- CI: pipefail disabled during element detection (SIGPIPE on find|head)
- CI: pkg_pkg_ prefix duplication in zip names and updates.xml URLs
- CI: updates_xml_build preserves existing channel entries (stable not wiped by dev releases)
### Removed
- deploy-manual.yml workflow — using Joomla update server for distribution
- Accidentally committed profile.ps1 and TODO.md
## [02.01.43] - 2026-05-23
### Added
- Site Aliases tab with Joomla subform repeatable-table UI
- Per-alias offline toggle with custom maintenance message (503 response)
- Per-alias robots meta directive (index/noindex/follow/nofollow/none)
- Per-alias backend redirect (admin panel redirects to primary domain)
- 6 MokoWaaS API endpoints: health, install, update, cache, backup, info
- Remote plugin install via `/?mokowaas=install` endpoint
- Remote update trigger via `/?mokowaas=update` endpoint
- Remote cache clear via `/?mokowaas=cache` endpoint (site + admin + opcache)
- Remote Akeeba Backup trigger via `/?mokowaas=backup` endpoint
- Compact site info via `/?mokowaas=info` endpoint
### Changed
- Site aliases moved from comma-separated text field to structured subform
- Each alias now stores domain, offline, offline_message, robots, redirect_backend
- Heartbeat provisioning updated for subform alias format
- Grafana datasource names use domain-only (removed "MokoWaaS - " prefix)
### Fixed
- Heartbeat receiver accepts any 200 status (registered/updated/ok)
- script.php uses heartbeat receiver instead of Grafana API (fixes 403 RBAC)
## [02.01.37] - 2026-05-23
### Added
- Health check endpoint at `/?mokowaas=health` with 16 diagnostic checks (#54)
- Core checks: database latency, filesystem writability/size, cache, extensions
- Backup checks: Akeeba Backup last backup date/status/size, days since, frequency
- Security checks: Admin Tools WAF status, blocked requests 24h/7d
- SSL certificate: expiry date, days left, issuer (degraded <30d, error <7d)
- Scheduled tasks: Joomla task scheduler status, failed tasks 24h
- Error log: PHP error log size, recent errors, last error message
- Database size: total MB, table count, top 5 largest tables
- Content stats: articles, categories, menu items, modules
- User activity: total users, active sessions, failed logins 24h, last login
- Mail system: mailer type, from address, SMTP host, queue count
- SEO health: robots.txt, sitemap, htaccess, SEF status
- Template info: site/admin template names, override count
- Configuration drift: debug mode, error reporting, force SSL, caching
- Human-readable `reason` field explaining degraded/error status
- Site size reporting (images, media, tmp, cache, logs directories)
- Heartbeat provisioning via receiver at bench.mokoconsulting.tech
- Grafana datasource auto-provisioning via YAML (no API token needed)
- ntfy notifications on heartbeat registration (mokowaas-heartbeat topic)
- Grafana dashboard with 9 rows covering all 16 health checks
- Auto-generated health API token (separate from Joomla user tokens)
### Changed
- Health endpoint always enabled — no config toggle needed
- Grafana provisioning uses heartbeat receiver pattern (replaces direct API)
- Removed config fields: enable_health_endpoint, grafana_url, grafana_api_key
- Migrated .gitea/ to .mokogitea/ directory standard
- Updated all references from MokoStandards to moko-platform
- Renamed Gitea references to MokoGitea in docs
### Fixed
- SSL verification disabled for Grafana cURL calls (shared hosting)
- cURL follow redirects enabled
- updates.xml download URL uses correct `development` tag
### Security
- Plugin hidden from plugin list for non-master users
- Plugin settings restricted to master user only
- Self-healing lock (enforceLocked) runs every page load
- Uninstall blocked in preflight
- Health endpoint requires HTTPS + bearer token
- Heartbeat shared secret for receiver authentication
## [02.01.08] - 2026-04-07
### Added
- Template-based language overrides with `{{BRAND_NAME}}`, `{{COMPANY_NAME}}`, `{{SUPPORT_URL}}` placeholders
- Configurable brand name, company name, and support URL via plugin params
- Sentinel-block merge pattern that preserves existing site overrides
- Install respects user-defined overrides (non-overwrite)
- ~50 override keys across admin and frontend
- Powered by links with anchor tag to support URL
- Login support URL enforcement (mokoconsulting.tech/support, /kb, /news)
- Atum template branding via params (logoBrandLarge, logoBrandSmall, loginLogo)
- Shipped media assets: logo.png, favicon.ico, favicon.svg, favicon_256.png
- Favicon injection (SVG + ICO + Apple touch icon)
- Admin color scheme via Atum template style params (hue, link-color, special-color)
- Custom CSS textarea injection
- Master user enforcement (persistent super admin — "Webmaster")
- Emergency access (DB password + file verification two-factor)
- IP whitelist via configuration.php (empty blocks access)
- IP whitelist display in plugin config (shows current IPs + your IP)
- All emergency access attempts logged to Joomla Action Logs
- Email notification on successful emergency login
- Tenant restrictions: Extension Installer, System Info, Global Configuration, Template code editor
- Dynamic admin menu hiding via onPreprocessMenuItems
- Disable install-from-URL for all users
- Force HTTPS redirect (supports reverse proxy)
- Admin session idle timeout (default 60 min, master user exempt)
- Password policy (min length, uppercase, number, special character)
- Upload type and size restrictions (default 100MB)
- Maintenance actions: reset all hits, delete all versions
- Auto-enable plugin on first install
- Action log extension registration in #__action_logs_extensions and #__action_log_config
- Custom AllowedIpsField form field for IP whitelist display
- Joomla 5.x and 6.x compatibility
### Fixed
- Column heading overrides removed (broke module/plugin list views)
- RegularLabs Position column workaround
- Nested `<a>` tags in login support overrides
- Emergency access moved from onUserAuthenticate to onAfterInitialise (Joomla uses isolated auth dispatcher)
- Session created directly for emergency login (bypasses auth dispatcher)
- Auto-complete emergency login after verify file deletion (no re-entering credentials)
### Changed
- Version bumped to 02.01.08 across all files
- Configuration guide fully rewritten with all fieldsets documented
- Testing guide with 17 test suites
- README updated with Usage section, new features, Joomla 5/6 badges
## [01.04.00] - 2026-02-22
### Added
- Complete Joomla 5.x system plugin implementation with modern architecture
- Main plugin class (`src/mokowaas.php`) with event handlers:
- `onAfterInitialise` event hook for framework initialization
- `onAfterRoute` event hook for routing integration
- Plugin manifest (`src/mokowaas.xml`) with Joomla 5.x namespace support
- Namespace: `Moko\Plugin\System\MokoWaaS`
- Configuration parameter for enabling/disabling branding
- Dependency injection service provider (`src/services/provider.php`)
- DI container registration for Joomla 5.x compatibility
- Plugin language files in `src/language/en-GB/`:
- `plg_system_mokowaas.ini` - Plugin UI strings
- `plg_system_mokowaas.sys.ini` - System/installation strings
- Enhanced language overrides (57+ strings):
- Installation sample data branding
- Site name labels
- Admin-specific UI elements
- Version and About sections
- Security `index.html` files throughout directory structure
- Comprehensive README.md with:
- Badges for version, license, Joomla, and PHP compatibility
- Table of contents with 12+ major sections
- Detailed installation instructions (2 methods)
- Technical implementation documentation
- Repository structure overview
- Development and build instructions
### Changed
- Updated all documentation to version 01.04.00
- Enhanced language overrides with more comprehensive coverage
- Improved plugin configuration options
### Fixed
- Typo in language override: "ERROR OCCURED" → "ERROR OCCURRED"
- Repository references updated from placeholders to actual GitHub URLs
### Technical
- Integrates with Joomla's native language override system
- No programmatic string loading (performance optimization)
- Event-driven architecture for minimal overhead
- PSR-4 autoloading through service provider
## [01.03.00] - 2025-12-11
### Changed
- General cleanup and code organization
- Documentation structure improvements
## [01.02.01] - 2025-12-11
### Changed
- Version bump for release alignment
## [01.02.00] - 2025-12-11
### Added
- Documentation directory (`/docs/`) with comprehensive guides:
- Installation guide
- Configuration guide
- Build guide
- Operations guide
- Troubleshooting guide
- Upgrade and versioning guide
- Rollback and recovery guide
- GitHub workflow for automated builds (`.github/workflows/build.yml`)
- Image and favicon replacement feature for complete branding
### Changed
- Improved documentation structure and organization
## [01.01.05] - 2025-12-11
### Changed
- Version bump for release coordination
## [01.01.04] - 2025-12-11
### Fixed
- Plugin manifest corrections and validation fixes
## [01.01.03] - 2025-12-11
### Fixed
- Administrator language file location corrected
- Language override path alignment with Joomla standards
## [01.01.02] - 2025-12-11
### Changed
- Moved plugin code to `/src/` directory for better organization
- Aligned repository structure with release deployment pipeline
- Improved packaging workflow
### Added
- Release deployment pipeline integration
- Automated build and validation scripts
## [1.0.0] - 2025-12-11
### Added
- Initial release of MokoWaaS plugin
- Basic language override system for Joomla rebranding
- Frontend language overrides (en-GB, en-US)
- Administrator language overrides (en-GB, en-US)
- Core branding replacements:
- Footer "Powered by" text
- Control panel welcome messages
- Help and documentation links
- Generic Joomla→MokoWaaS replacements
- Basic plugin structure and manifest
- License (GPL-3.0-or-later)
- Contributing guidelines
- Code of conduct
### Technical Details
- Joomla 5.x compatible
- PHP 8.1+ requirement
- Language override mechanism using Joomla's native system
---
## Version History Summary
| Version | Date | Type | Summary |
|------------|------------|-----------|-------------------------------------------|
| 01.04.00 | 2026-02-22 | Major | Complete plugin implementation & enhanced docs |
| 01.03.00 | 2025-12-11 | Minor | Cleanup and organization |
| 01.02.01 | 2025-12-11 | Patch | Version alignment |
| 01.02.00 | 2025-12-11 | Minor | Documentation and build system |
| 01.01.05 | 2025-12-11 | Patch | Version coordination |
| 01.01.04 | 2025-12-11 | Patch | Manifest fixes |
| 01.01.03 | 2025-12-11 | Patch | Language location fix |
| 01.01.02 | 2025-12-11 | Patch | Repository restructuring |
| 1.0.0 | 2025-12-11 | Major | Initial release |
---
## Upgrade Notes
### Upgrading to 01.04.00
**Breaking Changes:** None
**New Features:**
- Complete Joomla 5.x plugin implementation
- Dependency injection support
- Enhanced language overrides (14+ new strings)
**Installation:**
1. Backup your current installation
2. Download the latest release package
3. Install via Joomla Extension Manager
4. Clear Joomla cache
5. Verify branding appears correctly
### Upgrading to 01.02.00
**New Features:**
- Comprehensive documentation in `/docs/`
- Automated build workflows
**Notes:**
- Review new documentation for operational guidance
- Check GitHub workflows for automated builds
---
## Contributing
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
When adding entries to this changelog:
1. Add new changes under `[Unreleased]` section
2. Use categories: Added, Changed, Deprecated, Removed, Fixed, Security
3. Include clear, concise descriptions
4. Reference issue numbers where applicable
5. Move items from Unreleased to versioned section upon release
## Links
- [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) - Coding and documentation standards
- [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Changelog format specification
- [Semantic Versioning](https://semver.org/spec/v2.0.0.html) - Version numbering specification
- [Repository](https://github.com/mokoconsulting-tech/mokowaas) - Project repository
---
**Note:** For detailed technical documentation, see the `/docs/` directory and [README.md](README.md).
+1 -1
View File
@@ -9,7 +9,7 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
VERSION: 02.21.00
VERSION: 02.13.00
PATH: /README.md
BRIEF: MokoWaaS platform plugin for Joomla
-->
+1 -1
View File
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.20.00</version>
<version>02.13.00</version>
<description>Minimal API-only component for MokoWaaS. Provides REST endpoints for site health, cache, updates, and backups.</description>
<namespace path="api/src">Moko\Component\MokoWaaS\Api</namespace>
<administration>
@@ -162,11 +162,6 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
// Security: HTTPS redirect (runs for all clients)
$this->enforceHttps();
// Site alias handling: offline page and backend redirect.
// Must run in onAfterInitialise (not onAfterRoute) so that
// Joomla's offline check in doExecute() sees the updated config.
$this->handleSiteAlias();
// MokoWaaS API endpoints (run before routing)
$mokoAction = $this->app->input->get('mokowaas', '');
@@ -931,6 +926,9 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
*/
public function onAfterRoute()
{
// Site alias handling: offline page and backend redirect
$this->handleSiteAlias();
if (!$this->app->isClient('administrator'))
{
return;
@@ -3083,8 +3081,7 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
/**
* Handle site alias logic: offline page and backend redirect.
*
* Runs in onAfterInitialise so that Joomla's offline check in
* SiteApplication::doExecute() sees the updated config value.
* Runs early in onAfterInitialise before routing occurs.
*
* @return void
*
@@ -30,7 +30,7 @@
<license>GNU General Public License version 3 or later; see LICENSE.md</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.20.00</version>
<version>02.13.00</version>
<description>This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform.</description>
<namespace path=".">Moko\Plugin\System\MokoWaaS</namespace>
<scriptfile>script.php</scriptfile>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.20.00</version>
<version>02.13.00</version>
<description>Joomla Web Services API routes for MokoWaaS site management — health checks, cache, updates, backups, and site info.</description>
<namespace path="src">Moko\Plugin\WebServices\MokoWaaS</namespace>
<files>
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="webservices" method="upgrade">
<name>Web Services - Perfect Publisher</name>
<author>Moko Consulting</author>
<creationDate>2026-05-28</creationDate>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.20.00</version>
<description>Joomla Web Services API routes for Perfect Publisher (com_autotweet) — channels, posts, requests, rules, and feeds.</description>
<namespace path="src">Moko\Plugin\WebServices\PerfectPublisher</namespace>
<files>
<folder plugin="perfectpublisher">services</folder>
<folder>src</folder>
</files>
</extension>
@@ -1,42 +0,0 @@
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_webservices_perfectpublisher/services/provider.php
* VERSION: 02.13.01
* BRIEF: DI service provider for Perfect Publisher Web Services plugin
*/
defined('_JEXEC') or die;
use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Moko\Plugin\WebServices\PerfectPublisher\Extension\PerfectPublisherApi;
return new class implements ServiceProviderInterface
{
public function register(Container $container): void
{
$container->set(
PluginInterface::class,
function (Container $container) {
$dispatcher = $container->get(DispatcherInterface::class);
$plugin = new PerfectPublisherApi(
$dispatcher,
(array) PluginHelper::getPlugin('webservices', 'perfectpublisher')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};
@@ -1,539 +0,0 @@
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php
* VERSION: 02.13.01
* BRIEF: Web Services API plugin for Perfect Publisher (com_autotweet)
*/
namespace Moko\Plugin\WebServices\PerfectPublisher\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Event\Application\BeforeApiRouteEvent;
use Joomla\CMS\Router\ApiRouter;
use Joomla\Event\SubscriberInterface;
/**
* Perfect Publisher Web Services API Plugin
*
* Registers REST API routes for Perfect Publisher (com_autotweet) data.
* Provides read access to channels, posts, requests, rules, and feeds.
* Provides write access to create publish requests.
*
* Routes:
* GET /v1/perfectpublisher/channels List social channels
* GET /v1/perfectpublisher/channels/:id Get channel detail
* GET /v1/perfectpublisher/posts List published posts
* GET /v1/perfectpublisher/posts/:id Get post detail
* GET /v1/perfectpublisher/requests List pending requests
* POST /v1/perfectpublisher/requests Create a publish request
* GET /v1/perfectpublisher/rules List publishing rules
* GET /v1/perfectpublisher/feeds List RSS feeds
* GET /v1/perfectpublisher/channeltypes List channel type definitions
* GET /v1/perfectpublisher/stats Dashboard statistics
*
* @since 02.13.01
*/
final class PerfectPublisherApi extends CMSPlugin implements SubscriberInterface
{
/**
* @return array
*/
public static function getSubscribedEvents(): array
{
return [
'onBeforeApiRoute' => 'onBeforeApiRoute',
];
}
/**
* Register API routes.
*
* @param BeforeApiRouteEvent $event The API route event
*
* @return void
*/
public function onBeforeApiRoute(BeforeApiRouteEvent $event): void
{
$router = $event->getRouter();
// All routes are handled by this plugin directly via custom callbacks
// because com_autotweet uses FOF, not standard Joomla MVC
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/channels',
[$this, 'getChannels']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/channels/:id',
[$this, 'getChannel']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/posts',
[$this, 'getPosts']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/posts/:id',
[$this, 'getPost']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/requests',
[$this, 'getRequests']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['POST'],
'v1/perfectpublisher/requests',
[$this, 'createRequest']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/rules',
[$this, 'getRules']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/feeds',
[$this, 'getFeeds']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/channeltypes',
[$this, 'getChannelTypes']
)
);
$router->addRoute(
new \Joomla\Router\Route(
['GET'],
'v1/perfectpublisher/stats',
[$this, 'getStats']
)
);
}
/**
* GET /v1/perfectpublisher/channels
*
* @return void
*/
public function getChannels(): void
{
$db = Factory::getDbo();
$app = Factory::getApplication();
$limit = (int) $app->input->get('limit', 20);
$offset = (int) $app->input->get('offset', 0);
$query = $db->getQuery(true)
->select('c.*, ct.name AS channeltype_name, ct.max_chars')
->from($db->quoteName('#__autotweet_channels', 'c'))
->leftJoin(
$db->quoteName('#__autotweet_channeltypes', 'ct')
. ' ON ' . $db->quoteName('c.channeltype_id')
. ' = ' . $db->quoteName('ct.id')
)
->order($db->quoteName('c.ordering') . ' ASC');
$published = $app->input->get('published', null);
if ($published !== null) {
$query->where($db->quoteName('c.published') . ' = ' . (int) $published);
}
$db->setQuery($query, $offset, $limit);
$this->sendJsonResponse($db->loadObjectList());
}
/**
* GET /v1/perfectpublisher/channels/:id
*
* @return void
*/
public function getChannel(): void
{
$id = (int) Factory::getApplication()->input->get('id', 0);
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('c.*, ct.name AS channeltype_name, ct.max_chars, ct.description AS channeltype_desc')
->from($db->quoteName('#__autotweet_channels', 'c'))
->leftJoin(
$db->quoteName('#__autotweet_channeltypes', 'ct')
. ' ON ' . $db->quoteName('c.channeltype_id')
. ' = ' . $db->quoteName('ct.id')
)
->where($db->quoteName('c.id') . ' = ' . $id);
$db->setQuery($query);
$result = $db->loadObject();
if (!$result) {
$this->sendJsonError('Channel not found', 404);
return;
}
// Strip sensitive OAuth params
if (isset($result->params)) {
$params = json_decode($result->params, true);
if (is_array($params)) {
foreach (['access_token', 'access_secret', 'client_secret', 'api_secret', 'password'] as $key) {
if (isset($params[$key])) {
$params[$key] = '***';
}
}
$result->params = json_encode($params);
}
}
$this->sendJsonResponse($result);
}
/**
* GET /v1/perfectpublisher/posts
*
* @return void
*/
public function getPosts(): void
{
$db = Factory::getDbo();
$app = Factory::getApplication();
$limit = (int) $app->input->get('limit', 20);
$offset = (int) $app->input->get('offset', 0);
$query = $db->getQuery(true)
->select('p.*, c.name AS channel_name')
->from($db->quoteName('#__autotweet_posts', 'p'))
->leftJoin(
$db->quoteName('#__autotweet_channels', 'c')
. ' ON ' . $db->quoteName('p.channel_id')
. ' = ' . $db->quoteName('c.id')
)
->order($db->quoteName('p.postdate') . ' DESC');
$pubstate = $app->input->get('pubstate', '');
if ($pubstate !== '') {
$query->where($db->quoteName('p.pubstate') . ' = ' . $db->quote($pubstate));
}
$channel = (int) $app->input->get('channel_id', 0);
if ($channel > 0) {
$query->where($db->quoteName('p.channel_id') . ' = ' . $channel);
}
$db->setQuery($query, $offset, $limit);
$this->sendJsonResponse($db->loadObjectList());
}
/**
* GET /v1/perfectpublisher/posts/:id
*
* @return void
*/
public function getPost(): void
{
$id = (int) Factory::getApplication()->input->get('id', 0);
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('p.*, c.name AS channel_name, ct.name AS channeltype_name')
->from($db->quoteName('#__autotweet_posts', 'p'))
->leftJoin(
$db->quoteName('#__autotweet_channels', 'c')
. ' ON ' . $db->quoteName('p.channel_id')
. ' = ' . $db->quoteName('c.id')
)
->leftJoin(
$db->quoteName('#__autotweet_channeltypes', 'ct')
. ' ON ' . $db->quoteName('c.channeltype_id')
. ' = ' . $db->quoteName('ct.id')
)
->where($db->quoteName('p.id') . ' = ' . $id);
$db->setQuery($query);
$result = $db->loadObject();
if (!$result) {
$this->sendJsonError('Post not found', 404);
return;
}
$this->sendJsonResponse($result);
}
/**
* GET /v1/perfectpublisher/requests
*
* @return void
*/
public function getRequests(): void
{
$db = Factory::getDbo();
$app = Factory::getApplication();
$limit = (int) $app->input->get('limit', 20);
$offset = (int) $app->input->get('offset', 0);
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__autotweet_requests'))
->order($db->quoteName('publish_up') . ' ASC');
$published = $app->input->get('published', null);
if ($published !== null) {
$query->where($db->quoteName('published') . ' = ' . (int) $published);
}
$db->setQuery($query, $offset, $limit);
$this->sendJsonResponse($db->loadObjectList());
}
/**
* POST /v1/perfectpublisher/requests
*
* Create a new publish request. Required fields: description.
* Optional: url, image_url, publish_up, plugin, priority.
*
* @return void
*/
public function createRequest(): void
{
$app = Factory::getApplication();
$db = Factory::getDbo();
$data = json_decode($app->input->json->getRaw(), true);
if (empty($data['description'])) {
$this->sendJsonError('Field "description" is required', 400);
return;
}
$now = Factory::getDate()->toSql();
$user = Factory::getUser();
$row = (object) [
'ref_id' => $data['ref_id'] ?? null,
'plugin' => $data['plugin'] ?? 'manual-api',
'priority' => (int) ($data['priority'] ?? 5),
'publish_up' => $data['publish_up'] ?? $now,
'description' => $data['description'],
'typeinfo' => (int) ($data['typeinfo'] ?? 0),
'url' => $data['url'] ?? null,
'image_url' => $data['image_url'] ?? null,
'created' => $now,
'created_by' => $user->id,
'params' => json_encode($data['params'] ?? []),
'published' => (int) ($data['published'] ?? 1),
];
$db->insertObject('#__autotweet_requests', $row, 'id');
$this->sendJsonResponse(
['id' => $row->id, 'status' => 'created'],
201
);
}
/**
* GET /v1/perfectpublisher/rules
*
* @return void
*/
public function getRules(): void
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('r.*, rt.name AS ruletype_name, rt.description AS ruletype_desc, c.name AS channel_name')
->from($db->quoteName('#__autotweet_rules', 'r'))
->leftJoin(
$db->quoteName('#__autotweet_ruletypes', 'rt')
. ' ON ' . $db->quoteName('r.ruletype_id')
. ' = ' . $db->quoteName('rt.id')
)
->leftJoin(
$db->quoteName('#__autotweet_channels', 'c')
. ' ON ' . $db->quoteName('r.channel_id')
. ' = ' . $db->quoteName('c.id')
)
->order($db->quoteName('r.ordering') . ' ASC');
$db->setQuery($query);
$this->sendJsonResponse($db->loadObjectList());
}
/**
* GET /v1/perfectpublisher/feeds
*
* @return void
*/
public function getFeeds(): void
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__autotweet_feeds'))
->order($db->quoteName('ordering') . ' ASC');
$db->setQuery($query);
$this->sendJsonResponse($db->loadObjectList());
}
/**
* GET /v1/perfectpublisher/channeltypes
*
* @return void
*/
public function getChannelTypes(): void
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__autotweet_channeltypes'))
->order($db->quoteName('id') . ' ASC');
$db->setQuery($query);
$this->sendJsonResponse($db->loadObjectList());
}
/**
* GET /v1/perfectpublisher/stats
*
* Dashboard statistics: post counts by status, channel counts, recent activity.
*
* @return void
*/
public function getStats(): void
{
$db = Factory::getDbo();
// Posts by status
$db->setQuery(
$db->getQuery(true)
->select('pubstate, COUNT(*) AS total')
->from($db->quoteName('#__autotweet_posts'))
->group($db->quoteName('pubstate'))
);
$postsByStatus = $db->loadObjectList('pubstate');
// Active channels
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*) AS total')
->from($db->quoteName('#__autotweet_channels'))
->where($db->quoteName('published') . ' = 1')
);
$activeChannels = (int) $db->loadResult();
// Pending requests
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*) AS total')
->from($db->quoteName('#__autotweet_requests'))
->where($db->quoteName('published') . ' = 1')
);
$pendingRequests = (int) $db->loadResult();
// Posts last 24h
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*) AS total')
->from($db->quoteName('#__autotweet_posts'))
->where($db->quoteName('postdate') . ' >= DATE_SUB(NOW(), INTERVAL 1 DAY)')
);
$posts24h = (int) $db->loadResult();
// Posts last 7d
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*) AS total')
->from($db->quoteName('#__autotweet_posts'))
->where($db->quoteName('postdate') . ' >= DATE_SUB(NOW(), INTERVAL 7 DAY)')
);
$posts7d = (int) $db->loadResult();
$this->sendJsonResponse([
'posts_by_status' => $postsByStatus,
'active_channels' => $activeChannels,
'pending_requests' => $pendingRequests,
'posts_24h' => $posts24h,
'posts_7d' => $posts7d,
]);
}
/**
* Send a JSON API response.
*
* @param mixed $data Response data
* @param int $status HTTP status code
*
* @return void
*/
private function sendJsonResponse($data, int $status = 200): void
{
$app = Factory::getApplication();
$app->setHeader('Content-Type', 'application/json; charset=utf-8');
$app->setHeader('Status', (string) $status);
echo json_encode(['data' => $data], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
$app->close();
}
/**
* Send a JSON error response.
*
* @param string $message Error message
* @param int $status HTTP status code
*
* @return void
*/
private function sendJsonError(string $message, int $status = 400): void
{
$app = Factory::getApplication();
$app->setHeader('Content-Type', 'application/json; charset=utf-8');
$app->setHeader('Status', (string) $status);
echo json_encode(['error' => $message], JSON_UNESCAPED_SLASHES);
$app->close();
}
}
+2 -3
View File
@@ -2,7 +2,7 @@
<extension type="package" method="upgrade">
<name>MokoWaaS</name>
<packagename>mokowaas</packagename>
<version>02.20.00</version>
<version>02.13.00</version>
<creationDate>2026-05-23</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -12,11 +12,10 @@
<description>MokoWaaS site management suite — branding, health monitoring, tenant restrictions, and REST API.</description>
<scriptfile>script.php</scriptfile>
<files folder="packages">
<files>
<file type="plugin" id="plg_system_mokowaas" group="system">plg_system_mokowaas.zip</file>
<file type="component" id="com_mokowaas">com_mokowaas.zip</file>
<file type="plugin" id="plg_webservices_mokowaas" group="webservices">plg_webservices_mokowaas.zip</file>
<file type="plugin" id="plg_webservices_perfectpublisher" group="webservices">plg_webservices_perfectpublisher.zip</file>
</files>
<updateservers>
+16 -16
View File
@@ -1,7 +1,7 @@
<?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
VERSION: 02.13.00
-->
<updates>
@@ -11,13 +11,13 @@
<element>pkg_mokowaas</element>
<type>package</type>
<client>site</client>
<version>02.20.00</version>
<version>02.13.00</version>
<creationDate>2026-05-28</creationDate>
<infourl title='Package - MokoWaaS'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/stable</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.20.00.zip</downloadurl>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.13.00.zip</downloadurl>
</downloads>
<sha256>4f893125c1f66d5c8545191d081a3f8abf555a7c0fe84f00b3be3cdda532b38f</sha256>
<sha256>55508a6bc63f9f72d5f8b7c728c8061a9b50a3fc1db9948fb61f985578dd5e65</sha256>
<tags><tag>dev</tag></tags>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
@@ -29,13 +29,13 @@
<element>pkg_mokowaas</element>
<type>package</type>
<client>site</client>
<version>02.20.00</version>
<version>02.13.00</version>
<creationDate>2026-05-28</creationDate>
<infourl title='Package - MokoWaaS'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/stable</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.20.00.zip</downloadurl>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.13.00.zip</downloadurl>
</downloads>
<sha256>4f893125c1f66d5c8545191d081a3f8abf555a7c0fe84f00b3be3cdda532b38f</sha256>
<sha256>55508a6bc63f9f72d5f8b7c728c8061a9b50a3fc1db9948fb61f985578dd5e65</sha256>
<tags><tag>alpha</tag></tags>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
@@ -47,13 +47,13 @@
<element>pkg_mokowaas</element>
<type>package</type>
<client>site</client>
<version>02.20.00</version>
<version>02.13.00</version>
<creationDate>2026-05-28</creationDate>
<infourl title='Package - MokoWaaS'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/stable</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.20.00.zip</downloadurl>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.13.00.zip</downloadurl>
</downloads>
<sha256>4f893125c1f66d5c8545191d081a3f8abf555a7c0fe84f00b3be3cdda532b38f</sha256>
<sha256>55508a6bc63f9f72d5f8b7c728c8061a9b50a3fc1db9948fb61f985578dd5e65</sha256>
<tags><tag>beta</tag></tags>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
@@ -65,13 +65,13 @@
<element>pkg_mokowaas</element>
<type>package</type>
<client>site</client>
<version>02.20.00</version>
<version>02.13.00</version>
<creationDate>2026-05-28</creationDate>
<infourl title='Package - MokoWaaS'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/stable</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.20.00.zip</downloadurl>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.13.00.zip</downloadurl>
</downloads>
<sha256>4f893125c1f66d5c8545191d081a3f8abf555a7c0fe84f00b3be3cdda532b38f</sha256>
<sha256>55508a6bc63f9f72d5f8b7c728c8061a9b50a3fc1db9948fb61f985578dd5e65</sha256>
<tags><tag>rc</tag></tags>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
@@ -83,13 +83,13 @@
<element>pkg_mokowaas</element>
<type>package</type>
<client>site</client>
<version>02.20.00</version>
<version>02.13.00</version>
<creationDate>2026-05-28</creationDate>
<infourl title='Package - MokoWaaS'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/stable</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.20.00.zip</downloadurl>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.13.00.zip</downloadurl>
</downloads>
<sha256>4f893125c1f66d5c8545191d081a3f8abf555a7c0fe84f00b3be3cdda532b38f</sha256>
<sha256>55508a6bc63f9f72d5f8b7c728c8061a9b50a3fc1db9948fb61f985578dd5e65</sha256>
<tags><tag>stable</tag></tags>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
-181
View File
@@ -1,181 +0,0 @@
# API Endpoints
MokoWaaS provides 6 remote management endpoints accessible via query string parameter. All endpoints require HTTPS and Bearer token authentication.
## Authentication
All endpoints require the `health_api_token` as a Bearer token in the Authorization header:
```
Authorization: Bearer <health_api_token>
```
The token is auto-generated during plugin installation and stored as a read-only parameter in the plugin configuration. It can also be passed as a `token` query parameter as a fallback.
Token validation uses `hash_equals()` for timing-safe comparison. If no token is configured, the endpoint returns HTTP 503. An invalid token returns HTTP 401.
## Endpoints
### 1. Health Check
```
GET /?mokowaas=health
```
Runs 16 diagnostic checks and returns a comprehensive health report. See [Health Monitoring](Health-Monitoring) for full documentation of all checks and response format.
**Response**: JSON object with `status` (`ok`/`degraded`/`error`), `reason`, `timestamp`, `checks`, and `meta`.
**HTTP Status**: 200 (ok/degraded), 503 (error).
---
### 2. Site Info
```
GET /?mokowaas=info
```
Returns a compact summary of the Joomla site.
**Response**:
| Field | Description |
|---|---|
| `site_name` | Joomla site name |
| `site_url` | Site root URL |
| `joomla_version` | Joomla CMS version |
| `php_version` | PHP version |
| `db_type` | Database driver (e.g. `pdomysql`) |
| `debug` | Whether debug mode is on |
| `sef` | Whether SEF URLs are enabled |
| `caching` | Whether caching is enabled |
| `articles` | Total article count |
| `users` | Total user count |
| `extensions` | Number of enabled extensions |
| `brand` | Configured brand name |
| `plugin_version` | MokoWaaS plugin version |
---
### 3. Remote Install
```
POST /?mokowaas=install
Content-Type: application/json
{"url": "https://example.com/extension.zip"}
```
Downloads and installs a Joomla extension from the provided URL. The extension is downloaded to a temporary directory, extracted, and installed using Joomla's installer API.
**Response**: JSON object with `status`, `extension` name, and `message`.
**HTTP Status**: 200 (success), 400 (missing URL), 405 (not POST), 500 (install failed).
---
### 4. Update Check
```
POST /?mokowaas=update
```
Clears the Joomla update cache and triggers a fresh update check via `Updater::findUpdates()`.
**Response**:
| Field | Description |
|---|---|
| `status` | `ok` |
| `updates_found` | Number of available updates |
| `message` | Human-readable summary |
**HTTP Status**: 200 (success), 405 (not POST), 500 (failed).
---
### 5. Cache Clear
```
POST /?mokowaas=cache
```
Clears the Joomla site cache, admin cache, and PHP OPcache (if available).
**Response**:
| Field | Description |
|---|---|
| `status` | `ok` |
| `message` | `Cache cleared` |
**HTTP Status**: 200 (success), 405 (not POST), 500 (failed).
---
### 6. Backup (Akeeba)
```
POST /?mokowaas=backup
Content-Type: application/json
{"profile": 1}
```
Triggers an Akeeba Backup using the specified profile (defaults to profile 1). Requires Akeeba Backup to be installed.
**Response**:
| Field | Description |
|---|---|
| `status` | `started` |
| `profile` | Backup profile ID used |
| `message` | `Backup started` |
**HTTP Status**: 200 (started), 404 (Akeeba not installed), 405 (not POST), 500 (failed), 501 (Akeeba Engine not loadable).
## Error Responses
All endpoints return errors in a consistent format:
```json
{
"error": "Error description",
"message": "Additional detail (optional)"
}
```
### Common Error Codes
| HTTP Status | Meaning |
|---|---|
| 400 | Bad request (unknown action, missing parameters) |
| 401 | Invalid or missing authentication token |
| 405 | Wrong HTTP method (e.g. GET when POST is required) |
| 500 | Server error during operation |
| 503 | No API token configured |
## Unknown Actions
Requesting an unknown action returns HTTP 400 with the list of available actions:
```json
{
"error": "Unknown action",
"action": "invalid",
"available": ["health", "install", "update", "cache", "backup", "info"]
}
```
## Joomla REST API Routes
In addition to the query-string endpoints above, MokoWaaS registers standard Joomla API routes via the `plg_webservices_mokowaas` plugin:
| Route | Controller |
|---|---|
| `GET /api/v1/mokowaas/health` | HealthController |
| `POST /api/v1/mokowaas/cache` | CacheController |
| `POST /api/v1/mokowaas/update` | UpdateController |
These routes use Joomla's standard API authentication (API token in `X-Joomla-Token` header) and are useful for integrations that already use the Joomla API framework.
-94
View File
@@ -1,94 +0,0 @@
# Configuration
All MokoWaaS settings are managed in the Joomla plugin configuration under **System > Plugins > System - MokoWaaS**. Settings are organized into tabs (fieldsets).
## Basic (Branding)
| Parameter | Type | Default | Description |
|---|---|---|---|
| `enable_branding` | Yes/No | Yes | Enable white-label branding (language overrides, logos, colors) |
| `brand_name` | Text | `MokoWaaS` | Brand name displayed throughout the admin interface |
| `company_name` | Text | `Moko Consulting` | Company name used in footers and copyright notices |
| `support_url` | URL | `https://mokoconsulting.tech` | Support link shown on the admin login page and dashboard |
## WaaS Access
Controls the master user system that designates a single operator account.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `enforce_master_user` | Yes/No | Yes | Enable master user enforcement; non-master Super Admins are restricted |
| `master_username` | Text | `mokoconsulting` | Username of the designated master operator |
| `master_email` | Email | `webmaster@mokoconsulting.tech` | Email address of the master user (for verification) |
| `emergency_access` | Yes/No | Yes | Enable emergency access via database password + file-based 2FA |
| `allowed_ips_display` | Display | -- | Read-only display of whitelisted IP addresses for emergency access |
## Maintenance
| Parameter | Type | Default | Description |
|---|---|---|---|
| `dev_mode` | Yes/No | No | Disable Joomla caching at runtime (does not modify `configuration.php`) |
| `reset_hits` | Yes/No | No | Reset article hit counters on next admin load |
| `delete_versions` | Yes/No | No | Purge content version history on next admin load |
## Visual Branding
| Parameter | Type | Default | Description |
|---|---|---|---|
| `color_primary` | Color | `#1a2744` | Primary brand color (buttons, accents) |
| `color_sidebar` | Color | `#0f1b2d` | Admin sidebar background color |
| `color_header` | Color | `#1a2744` | Admin header bar color |
| `color_link` | Color | `#0051ad` | Link text color |
| `brand_icon` | Text | -- | FontAwesome unicode code point (e.g. `f6d5`) for the brand icon |
| `custom_css` | Textarea | -- | Custom CSS injected into every admin page |
## Tenant Restrictions
Controls what non-master Super Admin users can access.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `restrict_installer` | Yes/No | Yes | Block access to Extension Manager for non-master users |
| `hide_sysinfo` | Yes/No | Yes | Hide System Information page from non-master users |
| `restrict_global_config` | Yes/No | Yes | Block access to Global Configuration for non-master users |
| `restrict_template_editing` | Yes/No | Yes | Prevent non-master users from editing template files |
| `disable_install_url` | Yes/No | Yes | Remove the "Install from URL" tab in Extension Manager |
| `hidden_menu_items` | Textarea | -- | Comma-separated list of admin menu item IDs to hide from non-master users |
## Site Aliases
Multi-domain support. See [Site Aliases](Site-Aliases) for full documentation.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `primary_domain` | Text | -- | The canonical domain for the site (e.g. `waas.dev.mokoconsulting.tech`) |
| `site_aliases` | Subform | -- | Repeatable table of alias domains with per-alias settings |
Each alias entry contains:
| Field | Type | Default | Description |
|---|---|---|---|
| `domain` | Text | -- | Alias domain name (e.g. `www.example.com`) |
| `offline` | Yes/No | No | Show offline page for this alias |
| `offline_message` | Textarea | -- | Custom offline message (shown when `offline` is Yes) |
| `robots` | List | `index, follow` | Robots meta directive for this alias |
| `redirect_backend` | Yes/No | Yes | Redirect admin requests on this alias to the primary domain |
## Diagnostics
| Parameter | Type | Default | Description |
|---|---|---|---|
| `health_api_token` | Text (read-only) | -- | Auto-generated Bearer token for API authentication. Provisioned on install/update. Cannot be manually edited. |
## Security
| Parameter | Type | Default | Description |
|---|---|---|---|
| `force_https` | Yes/No | Yes | Redirect all HTTP requests to HTTPS (301 redirect) |
| `admin_session_timeout` | Number | `60` | Idle timeout in minutes for admin sessions (0 = use Joomla default). Master user is exempt. |
| `password_min_length` | Number | `12` | Minimum password length for user accounts |
| `password_require_uppercase` | Yes/No | Yes | Require at least one uppercase letter |
| `password_require_number` | Yes/No | Yes | Require at least one digit |
| `password_require_special` | Yes/No | Yes | Require at least one special character |
| `upload_allowed_types` | Text | `jpg,jpeg,png,gif,webp,svg,pdf,doc,docx,xls,xlsx` | Comma-separated list of allowed upload file extensions |
| `upload_max_size_mb` | Number | `100` | Maximum upload file size in megabytes |
-127
View File
@@ -1,127 +0,0 @@
# Grafana Integration
MokoWaaS integrates with a Grafana monitoring stack hosted at `bench.mokoconsulting.tech`. The integration is automatic: on install or update, the plugin sends a heartbeat that provisions a Grafana datasource for the site.
## Architecture
```
MokoWaaS Plugin (Joomla)
|
| POST /api/waas-heartbeat/register
v
Heartbeat Receiver (bench.mokoconsulting.tech)
|
|-- Writes Grafana Infinity datasource YAML
|-- Restarts Grafana to pick up new datasource
|-- Sends ntfy notification (mokowaas-heartbeat topic)
v
Grafana Dashboard
|
| GET /?mokowaas=health (per site, on schedule)
v
Health JSON from each registered site
```
## Heartbeat Registration
### When It Fires
The heartbeat is sent automatically during:
- Plugin installation (`postflight` with type `install`)
- Plugin update (`postflight` with type `update`)
- Package installation (via `Pkg_MokowaasInstallerScript::sendHeartbeat()`)
### Payload
The plugin sends a POST request to `https://bench.mokoconsulting.tech/api/waas-heartbeat/register` with:
```json
{
"site_url": "https://example.com",
"site_name": "Example Site",
"health_token": "<health_api_token>",
"action": "register"
}
```
Authentication uses a shared secret sent in the `X-MokoWaaS-Key` header.
### What the Receiver Does
On receiving a registration request, the heartbeat receiver:
1. Validates the `X-MokoWaaS-Key` header
2. Generates a unique datasource UID from the site URL
3. Writes a Grafana Infinity datasource YAML file to the Grafana provisioning directory
4. Restarts Grafana to load the new datasource
5. Sends an ntfy notification to the `mokowaas-heartbeat` topic with registration details
The datasource YAML configures a Grafana Infinity datasource that polls `/?mokowaas=health` on the registered site using the provided Bearer token.
### Response
On success (HTTP 200):
```json
{
"status": "ok",
"ds_uid": "mokowaas-example-com"
}
```
The `ds_uid` is logged in the Joomla admin message queue for reference.
## Grafana Dashboard
The MokoWaaS Grafana dashboard is organized into 9 rows covering all health metrics:
| Row | Panels |
|---|---|
| 1. Overview | Overall status, uptime, plugin version, Joomla version |
| 2. Database | Connectivity, latency, driver, user count |
| 3. Filesystem | Disk space, writable directories, site size |
| 4. Extensions | Extension counts by type, pending updates |
| 5. Backup | Last backup status, age, Akeeba health |
| 6. Security | Admin Tools WAF, SSL certificate, blocked requests |
| 7. Content | Article counts, categories, user activity |
| 8. Infrastructure | Cache status, mail config, scheduled tasks, error log |
| 9. Configuration | SEO settings, template info, config drift |
Each row contains panels that query the site's Infinity datasource using JSONPath expressions to extract values from the health check response.
## ntfy Notifications
Registration events trigger a notification to the `mokowaas-heartbeat` ntfy topic. Notifications include:
- Site URL
- Site name
- Registration action (new or update)
- Datasource UID
Subscribe to notifications at `https://ntfy.sh/mokowaas-heartbeat` or use the ntfy app.
## Troubleshooting
### Heartbeat failed: connection error
The receiver at `bench.mokoconsulting.tech` may be unreachable. Check:
- DNS resolution for `bench.mokoconsulting.tech`
- Outbound HTTPS connectivity from the Joomla server
- Firewall rules allowing outbound port 443
Heartbeat failures are logged as warnings in Joomla's log and displayed in the admin message queue. They do not block plugin installation.
### Datasource not appearing in Grafana
- Verify the heartbeat completed successfully (check Joomla admin messages after install)
- Check the Grafana provisioning directory on `bench.mokoconsulting.tech`
- Ensure Grafana was restarted after provisioning
- Verify the health endpoint is accessible from the Grafana server
### Health data not loading in dashboard
- Confirm the `health_api_token` matches between the plugin configuration and the Grafana datasource
- Test the health endpoint directly: `curl -sk -H "Authorization: Bearer <token>" "https://example.com/?mokowaas=health"`
- Check for SSL certificate issues between the Grafana server and the monitored site
-33
View File
@@ -1,33 +0,0 @@
# Health Endpoint
## Stable Release: 02.01.37
16 diagnostic checks via /?mokowaas=health (token-authenticated, HTTPS-only).
### Checks
Core: database, filesystem, cache, extensions
Security: backup (Akeeba), security (Admin Tools), SSL certificate
Operations: scheduled tasks, error log, database size, mail
Content: articles, categories, users, sessions, failed logins
Config: SEO, templates, debug mode, force SSL, caching
### Grafana Dashboard (9 rows)
Site Overview | Health Metrics | Infrastructure | Backup | Security | SSL/Cron | Content/Users | Mail/SEO/Config | DB/Errors
### Heartbeat
Auto-registers with Grafana via bench.mokoconsulting.tech/api/waas-heartbeat/register
ntfy notifications on mokowaas-heartbeat topic
### Plugin Protection
Hidden from non-master users, settings blocked, self-healing lock, uninstall blocked.
---
| Field | Value |
|---|---|
| Minimum Version | 02.01.37 |
| Platform | joomla |
-267
View File
@@ -1,267 +0,0 @@
# Health Monitoring
MokoWaaS includes a built-in health monitoring system that runs 16 diagnostic checks against the Joomla site. Results are returned as a JSON payload via the `/?mokowaas=health` endpoint.
## Endpoint
```
GET https://example.com/?mokowaas=health
Authorization: Bearer <health_api_token>
```
The `health_api_token` is auto-generated during plugin installation and stored as a read-only plugin parameter. See [API Endpoints](API-Endpoints) for authentication details.
## Response Structure
The response includes an overall status, a human-readable reason string, a UTC timestamp, individual check results, and instance metadata.
| Field | Description |
|---|---|
| `status` | Overall status: `ok`, `degraded`, or `error` |
| `reason` | Human-readable summary of issues (null when status is `ok`) |
| `timestamp` | ISO 8601 UTC timestamp |
| `checks` | Object containing all 16 check results |
| `meta` | Instance metadata (brand, versions, server name) |
### Status Determination
- If any check returns `error`, the overall status is `error` and the HTTP status code is **503**.
- If any check returns `degraded` (and none are `error`), the overall status is `degraded` with HTTP **200**.
- Otherwise the overall status is `ok` with HTTP **200**.
## The 16 Checks
### 1. database
Tests database connectivity and query latency.
| Field | Description |
|---|---|
| `status` | `ok` or `error` |
| `latency_ms` | Query round-trip time in milliseconds |
| `driver` | Database driver name (e.g. `mysqli`, `pdomysql`) |
| `users` | Total user count (sanity check) |
### 2. filesystem
Checks writable directories and disk space.
| Field | Description |
|---|---|
| `status` | `ok`, `degraded` (low disk), or `error` (not writable) |
| `tmp_writable` | Whether `/tmp` is writable |
| `log_writable` | Whether `/administrator/logs` is writable |
| `cache_writable` | Whether `/cache` is writable |
| `free_disk_mb` | Free disk space in MB |
| `total_disk_mb` | Total disk space in MB |
| `site_size_mb` | Estimated site size in MB (images, media, tmp, cache, logs) |
Degraded when free disk is below 100 MB. Error when required directories are not writable.
### 3. cache
Reports Joomla cache configuration.
| Field | Description |
|---|---|
| `status` | Always `ok` |
| `enabled` | Whether Joomla caching is active |
| `handler` | Cache handler type (e.g. `file`, `redis`) |
### 4. extensions
Counts enabled extensions by type and checks for pending updates.
| Field | Description |
|---|---|
| `status` | `ok` or `degraded` (pending updates) |
| `by_type` | Object with counts per extension type |
| `pending_updates` | Number of available extension updates |
### 5. backup (Akeeba)
Checks Akeeba Backup status.
| Field | Description |
|---|---|
| `status` | `ok`, `degraded`, or `error` |
| `last_status` | Status of the last backup record (`complete`, `fail`, etc.) |
| `days_since` | Days since the last backup |
| `message` | Human-readable backup status |
Degraded when the last backup is older than 7 days or did not complete successfully.
### 6. security (Admin Tools)
Checks Admin Tools WAF status if installed.
| Field | Description |
|---|---|
| `status` | `ok`, `degraded`, or `error` |
| `waf_enabled` | Whether the Web Application Firewall is active |
| `blocked_24h` | Number of blocked requests in the last 24 hours |
### 7. ssl
Checks SSL certificate validity and expiration.
| Field | Description |
|---|---|
| `status` | `ok`, `degraded`, or `error` |
| `days_left` | Days until certificate expiration |
| `issuer` | Certificate issuer |
| `valid_from` | Certificate start date |
| `valid_to` | Certificate expiration date |
Degraded when the certificate expires within 30 days.
### 8. cron (Scheduled Tasks)
Checks Joomla scheduled task execution.
| Field | Description |
|---|---|
| `status` | `ok` or `degraded` |
| `total_tasks` | Total number of scheduled tasks |
| `failed_24h` | Tasks that failed in the last 24 hours |
### 9. errors (Error Log)
Analyzes recent Joomla error log entries.
| Field | Description |
|---|---|
| `status` | `ok` or `degraded` |
| `recent_errors` | Count of recent error log entries |
| `last_error` | Most recent error message |
### 10. db_size
Reports database size metrics.
| Field | Description |
|---|---|
| `status` | `ok` or `degraded` |
| `total_mb` | Total database size in MB |
| `tables` | Number of database tables |
### 11. content
Reports content statistics.
| Field | Description |
|---|---|
| `status` | Always `ok` |
| `articles` | Total article count |
| `categories` | Total category count |
| `published` | Number of published articles |
| `unpublished` | Number of unpublished articles |
### 12. users (User Activity)
Reports user statistics.
| Field | Description |
|---|---|
| `status` | Always `ok` |
| `total` | Total user count |
| `active_30d` | Users active in the last 30 days |
| `blocked` | Number of blocked user accounts |
### 13. mail
Checks Joomla mail configuration.
| Field | Description |
|---|---|
| `status` | `ok` or `degraded` |
| `mailer` | Mail handler type (e.g. `smtp`, `mail`, `sendmail`) |
| `from` | Configured sender address |
### 14. seo
Checks SEO configuration.
| Field | Description |
|---|---|
| `status` | `ok` or `degraded` |
| `sef` | Whether SEF URLs are enabled |
| `sef_rewrite` | Whether URL rewriting is enabled |
| `sitemap` | Whether a sitemap is detected |
### 15. template
Reports active template information.
| Field | Description |
|---|---|
| `status` | Always `ok` |
| `site_template` | Active frontend template name |
| `admin_template` | Active admin template name |
### 16. config (Config Drift)
Detects configuration anomalies.
| Field | Description |
|---|---|
| `status` | `ok` or `degraded` |
| `issues` | Array of detected configuration problems |
Checks for issues such as debug mode enabled in production, error reporting set too high, or default database prefix still in use.
## Example Response
```json
{
"status": "degraded",
"reason": "2 extension updates available; SSL expires in 14 days",
"timestamp": "2026-05-24T12:00:00Z",
"checks": {
"database": {
"status": "ok",
"latency_ms": 1.23,
"driver": "pdomysql",
"users": 5
},
"filesystem": {
"status": "ok",
"tmp_writable": true,
"log_writable": true,
"cache_writable": true,
"free_disk_mb": 4500,
"total_disk_mb": 20000,
"site_size_mb": 320
},
"ssl": {
"status": "degraded",
"days_left": 14,
"issuer": "Let's Encrypt",
"valid_to": "2026-06-07"
}
},
"meta": {
"brand": "MokoWaaS",
"plugin_version": "02.03.11",
"joomla_version": "5.2.4",
"php_version": "8.2.20",
"server_name": "Example Site",
"server_time": "2026-05-24T12:00:00Z"
}
}
```
(Remaining checks omitted for brevity.)
## Metadata
The `meta` object is included in every health response:
| Field | Description |
|---|---|
| `brand` | Configured brand name |
| `plugin_version` | MokoWaaS plugin version |
| `joomla_version` | Joomla CMS version |
| `php_version` | PHP version |
| `server_name` | Joomla site name |
| `server_time` | Server UTC time |
-52
View File
@@ -1,52 +0,0 @@
# MokoWaaS
MokoWaaS is a Joomla 5.x / 6.x extension package that provides a configurable white-label identity layer, tenant management, health monitoring, and remote administration API for the MokoWaaS platform.
Developed by [Moko Consulting](https://mokoconsulting.tech). Licensed under GPL-3.0-or-later.
## Features
- **White-label branding** -- customizable brand name, colors, favicon, login page, and admin template theming
- **Master user enforcement** -- designate a single operator account with elevated privileges; restrict other Super Admins
- **Tenant restrictions** -- hide system info, block installer access, restrict global config and template editing, hide menu items
- **Site aliases** -- multi-domain support with per-alias offline mode, robots directives, and backend redirects
- **Health monitoring** -- 16 diagnostic checks exposed via authenticated JSON API
- **Remote management API** -- 6 endpoints for health, info, install, update, cache clear, and backup
- **Grafana integration** -- automatic heartbeat registration with Grafana Infinity datasource provisioning
- **Plugin protection** -- protected flag, self-healing, hidden from non-master users, blocks disable/uninstall
- **Security hardening** -- forced HTTPS, session timeouts, password policy, upload restrictions
- **Emergency access** -- file-based two-factor verification for master user recovery
- **Automatic updates** -- via Joomla update server hosted on Gitea
## Requirements
| Requirement | Minimum |
|---|---|
| Joomla | 5.0.0+ (5.x and 6.x supported) |
| PHP | 8.1.0+ |
## Package Contents
The `pkg_mokowaas` package installs three extensions:
| Extension | Type | Purpose |
|---|---|---|
| `plg_system_mokowaas` | System Plugin | Core branding, restrictions, health checks, API endpoints |
| `com_mokowaas` | Component | REST API controllers (health, cache, update) |
| `plg_webservices_mokowaas` | Webservices Plugin | Registers Joomla API routes for `v1/mokowaas/*` |
## Wiki Pages
- [Configuration](Configuration) -- All plugin settings organized by tab
- [Health Monitoring](Health-Monitoring) -- The 16 diagnostic checks
- [Site Aliases](Site-Aliases) -- Multi-domain management
- [API Endpoints](API-Endpoints) -- The 6 remote management endpoints
- [Grafana Integration](Grafana-Integration) -- Monitoring dashboard setup
- [Plugin Protection](Plugin-Protection) -- Security measures preventing disable/uninstall
- [Installation](Installation) -- Step-by-step install and upgrade guide
## Links
- **Repository**: [MokoWaaS on Gitea](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS)
- **Update Server**: [updates.xml](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/updates.xml)
- **Author**: [Moko Consulting](https://mokoconsulting.tech)
-100
View File
@@ -1,100 +0,0 @@
# Installation
MokoWaaS is distributed as a Joomla package (`pkg_mokowaas`) containing three extensions. It can be installed via the standard Joomla Extension Manager.
## Requirements
| Requirement | Minimum |
|---|---|
| Joomla | 5.0.0+ |
| PHP | 8.1.0+ |
The installer checks both requirements during `preflight` and aborts with an error message if either is not met.
## Method 1: Upload Package File
1. Download the latest `pkg_mokowaas.zip` from the [Gitea Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases) page.
2. In Joomla admin, navigate to **System > Install > Extensions**.
3. On the **Upload Package File** tab, drag and drop or browse to select `pkg_mokowaas.zip`.
4. Click **Upload & Install**.
5. Verify the success message. The package installs three extensions:
- `plg_system_mokowaas` (System Plugin)
- `com_mokowaas` (Component)
- `plg_webservices_mokowaas` (Webservices Plugin)
## Method 2: Install from URL
1. In Joomla admin, navigate to **System > Install > Extensions**.
2. On the **Install from URL** tab, enter the direct download URL for the package ZIP, e.g.:
```
https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-latest.zip
```
3. Click **Install**.
4. Verify the success message.
## Post-Installation Verification
After installation, verify the following:
1. **Plugin enabled**: Navigate to **System > Plugins** and confirm "System - MokoWaaS" is enabled (it is auto-enabled during installation).
2. **Health token generated**: Open the plugin configuration and check the **Diagnostics** tab. The `health_api_token` field should contain an auto-generated token.
3. **Branding applied**: The admin login page and dashboard should reflect MokoWaaS branding (logo, colors, footer text).
4. **Health endpoint**: Test the health endpoint:
```
curl -sk -H "Authorization: Bearer <token>" "https://yoursite.com/?mokowaas=health"
```
5. **Grafana heartbeat**: Check the admin message queue for a Grafana heartbeat confirmation message.
## What the Installer Does
During `postflight`, the installer script performs these operations:
| Step | Description |
|---|---|
| Enable and protect plugin | Sets `enabled=1`, `protected=1`, `locked=0` in `#__extensions` |
| Install MokoOnyx template | Installs the bundled MokoOnyx site template from the plugin payload, sets it as default |
| Language overrides | Deploys language override files for en-GB and en-US |
| Login support URLs | Updates `mod_loginsupport` to point to Moko Consulting support/docs/news URLs |
| Atum branding | Applies brand colors to the Atum admin template |
| Action log registration | Registers MokoWaaS in the Joomla action log system |
| Health token provisioning | Generates a random API token if one does not exist |
| Heartbeat | Sends a registration heartbeat to the Grafana monitoring receiver |
## Automatic Updates
MokoWaaS includes an update server configuration that enables automatic update notifications through Joomla's built-in update system.
The update server URL is:
```
https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/updates.xml
```
When a new version is available:
1. Joomla's update checker detects the new version from `updates.xml`.
2. A notification appears in the admin dashboard.
3. Navigate to **System > Update > Extensions** to install the update.
4. The `postflight` script runs again, re-applying all configuration steps.
## Upgrading from Standalone Plugin to Package
If you previously installed the standalone `plg_system_mokowaas` plugin (before it was packaged as `pkg_mokowaas`), the package installer handles migration automatically:
1. Install the `pkg_mokowaas.zip` package using either method above.
2. The package installer detects the existing standalone plugin and upgrades it in place.
3. The additional extensions (`com_mokowaas`, `plg_webservices_mokowaas`) are installed alongside.
4. All existing plugin settings are preserved.
5. The `protected` flag is set on all package extensions.
No manual cleanup of the old standalone plugin is required.
## Uninstallation
Uninstallation is restricted to the master user. See [Plugin Protection](Plugin-Protection) for details.
When the master user uninstalls via the Extension Manager:
1. Language override files are removed from Joomla's global override directories.
2. Action log registration is cleaned up.
3. An uninstall notification is sent to the monitoring system.
4. The package and all sub-extensions are removed.
-89
View File
@@ -1,89 +0,0 @@
# Plugin Protection
MokoWaaS uses multiple layers of protection to prevent accidental or unauthorized disabling and uninstallation. The master user retains full control over the plugin at all times.
## Protection Layers
### 1. Protected Flag (`protected=1`)
During installation and on every admin session, MokoWaaS sets the `protected` column to `1` in the `#__extensions` database table for both `mokowaas` and `pkg_mokowaas` entries.
The Joomla framework itself enforces this flag: protected extensions cannot be disabled or uninstalled through the standard admin interface.
The `locked` column is set to `0` so the extension can still receive updates and configuration changes.
### 2. Self-Healing
The `ensureProtectedFlag()` method runs once per admin session (using a static flag to avoid repeated queries). If the `protected` column has been reset to `0` (e.g., by a database modification), it is automatically restored to `1`.
This runs in the `protectPlugin()` method, which is called from `onBeforeRender()` on every admin page load.
### 3. Hidden from Plugin List
For non-master users, MokoWaaS injects JavaScript on the `com_plugins` and `com_installer` pages that hides any table row containing "mokowaas" or "MokoWaaS". This prevents non-master users from seeing the plugin in the extension list.
The `hidePluginFromList()` method adds an inline script that runs on `DOMContentLoaded`:
```javascript
document.querySelectorAll('tr').forEach(function(row) {
var text = row.textContent || '';
if (text.indexOf('mokowaas') !== -1 || text.indexOf('MokoWaaS') !== -1) {
row.style.display = 'none';
}
});
```
### 4. onExtensionBeforeSave Interception
The `onExtensionBeforeSave` event handler intercepts save attempts on the plugin configuration. If a non-master user attempts to save the plugin with `enabled = 0`, the handler:
1. Displays an error message: "MokoWaaS cannot be disabled."
2. Forces `enabled` back to `1` on the table object
3. Returns `true` to allow the save to proceed (with the corrected value)
### 5. protectPlugin() -- Uninstall and Disable Blocking
The `protectPlugin()` method runs on every admin page request and checks for active uninstall or disable attempts:
**Uninstall blocking**: If the current request is to `com_installer` with task `manage.remove`, and the extension IDs include any MokoWaaS extension, the request is blocked with an error message and a redirect back to the installer manage view.
**Disable blocking**: If the current request is to `com_plugins` with task `plugins.publish`, and the extension IDs include MokoWaaS, the request is blocked with an error message and a redirect back to the plugins list.
The `isOurExtension()` helper method checks extension IDs against the database to determine if they belong to MokoWaaS (matching on element name `mokowaas` or `pkg_mokowaas`).
## Master User Exemption
All protection checks call `isMasterUser()` first. If the current user is the designated master user (matching the configured `master_username`), all protections are bypassed. The master user can:
- See MokoWaaS in the plugin and extension lists
- Disable the plugin via the configuration page
- Uninstall the plugin via the Extension Manager
- Modify all plugin settings
## Installation-Time Protection
The package installer script (`Pkg_MokowaasInstallerScript`) and the plugin installer script both set `protected=1` during `postflight`:
- `enableAndLockPlugin()` sets `enabled=1`, `locked=1`, `protected=1` on the system plugin
- `protectExtensions()` sets `protected=1`, `locked=0` on all MokoWaaS extensions (plugin and package)
This ensures protection is active immediately after installation, before the first admin page load triggers the self-healing logic.
## Summary of Protection Flow
```
Installation
-> postflight sets protected=1, enabled=1
Every admin page load (onBeforeRender)
-> protectPlugin()
-> ensureProtectedFlag() (once per session, restores protected=1 if needed)
-> if not master user:
-> block uninstall attempts (com_installer manage.remove)
-> block disable attempts (com_plugins plugins.publish)
-> hidePluginFromList() for non-master users
Plugin config save (onExtensionBeforeSave)
-> if not master user and enabled=0:
-> force enabled=1, show error
```
-69
View File
@@ -1,69 +0,0 @@
# Site Aliases
MokoWaaS supports multi-domain configurations through the Site Aliases system. This allows a single Joomla installation to respond to multiple domain names, each with independent settings for offline mode, robots directives, and backend access.
## Configuration
Site aliases are configured in the plugin settings under the **Site Aliases** tab.
### Primary Domain
Set `primary_domain` to the canonical domain for the site (e.g. `waas.dev.mokoconsulting.tech`). This is the domain that:
- Serves as the canonical URL for SEO purposes
- Hosts the admin backend (when `redirect_backend` is enabled on aliases)
- Is used in heartbeat registration with Grafana
### Alias Entries
Add alias domains using the repeatable subform table. Each alias entry has the following options:
| Field | Type | Default | Description |
|---|---|---|---|
| `domain` | Text | (required) | The alias domain name, e.g. `www.example.com` |
| `offline` | Yes/No | No | When Yes, visitors to this alias see the offline page |
| `offline_message` | Textarea | -- | Custom message displayed when the alias is offline (only shown when `offline` is Yes) |
| `robots` | List | `index, follow` | Robots meta tag directive for this alias |
| `redirect_backend` | Yes/No | Yes | When Yes, admin URLs (`/administrator`) on this alias redirect to the primary domain |
### Robots Options
Each alias can have its own robots directive:
| Value | Effect |
|---|---|
| `index, follow` | Normal indexing (default) |
| `noindex, follow` | Do not index pages, but follow links |
| `index, nofollow` | Index pages, do not follow links |
| `noindex, nofollow` | Do not index or follow |
| `none` | Equivalent to `noindex, nofollow` |
## How Canonical URLs Work
When a request arrives on an alias domain, MokoWaaS:
1. Matches the `HTTP_HOST` against configured alias domains
2. Applies the alias-specific robots meta tag
3. If the alias is marked offline, renders the offline page with the custom message
4. If `redirect_backend` is enabled and the request is for `/administrator`, issues a 301 redirect to the primary domain's admin
5. Sets the canonical URL to the primary domain equivalent of the current page
This prevents duplicate content issues when the same site is accessible from multiple domains.
## Grafana Monitoring for Aliases
When the plugin sends a heartbeat to the Grafana monitoring receiver, it registers both the primary domain and all alias domains. The monitoring dashboard can then track health status for each domain independently.
Each alias appears as a separate entry in the Grafana Infinity datasource, pointing to the same health endpoint but accessed via the alias domain. This ensures SSL certificate checks and DNS resolution are validated per-domain.
## DreamHost Mirror Setup
For sites hosted on DreamHost, alias domains are typically configured as "mirror" domains in the DreamHost panel:
1. In DreamHost panel, add the alias domain as a **Mirror** of the primary domain
2. Ensure DNS for the alias domain points to the DreamHost server
3. Add the alias domain to the MokoWaaS Site Aliases configuration
4. Set `redirect_backend` to Yes (recommended) so admin access always uses the primary domain
5. Set `robots` to `noindex, nofollow` if the alias is a staging or preview domain
DreamHost mirrors serve the same filesystem, so no additional Joomla configuration is needed beyond the MokoWaaS alias entry.