Compare commits
246 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3dcadc130b | |||
| eaf686b37e | |||
| 3ab70375d1 | |||
| 4aa30180bb | |||
| 1cd79d4d5f | |||
| a084dc4d72 | |||
| 8ea8a96766 | |||
| 94bddceda4 | |||
| 42c21a62fe | |||
| ec13a78444 | |||
| 7a1ed7548b | |||
| e53918bc6a | |||
| 0d705b968f | |||
| a9573e1b21 | |||
| efb7bf50a5 | |||
| 658fdcca75 | |||
| 66c433db2c | |||
| e425d0f898 | |||
| d48415ab5a | |||
| aa03286613 | |||
| f20cbdd720 | |||
| ac8594f43a | |||
| dfd50cc48d | |||
| c58539bd90 | |||
| 7745d98bdc | |||
| 29eb66e921 | |||
| 73e5c51b69 | |||
| 43b3e204d3 | |||
| a992215ba5 | |||
| 1eecb79289 | |||
| c312761148 | |||
| 1b64b0d156 | |||
| 319b43d63d | |||
| 479daf4a43 | |||
| e932cccbf6 | |||
| 1a4c02a098 | |||
| 33b34e6250 | |||
| 99f3bd47e0 | |||
| 6907046dae | |||
| 7e597674ac | |||
| 47aeb98201 | |||
| 58f2571dc4 | |||
| e3ba98499e | |||
| 5b17f5c5ec | |||
| 9b9e8764da | |||
| 26646eac57 | |||
| e0518c20fe | |||
| 278e5d45f6 | |||
| 0d24862302 | |||
| 94d45169ef | |||
| 17fd3d6b0e | |||
| f26595bed4 | |||
| 70748938d2 | |||
| dcdc3debb8 | |||
| e2782b4fb7 | |||
| 178ca0499e | |||
| 324baff9b9 | |||
| 22f0bb9a6f | |||
| 616e82ae26 | |||
| ec5a22b37f | |||
| 445f5e7060 | |||
| eaf46e7ea3 | |||
| 303af17971 | |||
| 7e0aa36ffa | |||
| 102bea980b | |||
| ed95dcb7af | |||
| 56abe3af7f | |||
| 5b5245c170 | |||
| 167a7c0dfd | |||
| 62788853ea | |||
| 6f7cb11e39 | |||
| df22d7f7c0 | |||
| 5984529569 | |||
| 8be05b75b7 | |||
| a02e466456 | |||
| 2ede62b8b9 | |||
| f5d06e6e25 | |||
| 7370757e46 | |||
| 3aa7364783 | |||
| df711f9a17 | |||
| 1d2252e8b4 | |||
| c19d4da411 | |||
| 45e9091fd0 | |||
| fb0ca184b9 | |||
| c1e668e644 | |||
| 985034650b | |||
| 0fdc91d50c | |||
| 9a7d5b8359 | |||
| 9c9a1a7b52 | |||
| 21de2fa115 | |||
| 9f1848d218 | |||
| ecb456d91e | |||
| d9ce74cf38 | |||
| 91e9465233 | |||
| 3bbaee7c86 | |||
| d494e7366e | |||
| 05c3f5fd1f | |||
| c91b44ad34 | |||
| e86cc2b48b | |||
| c28c2de936 | |||
| 1a81267d38 | |||
| 343ef64ea2 | |||
| f47a4d3c77 | |||
| cfb05c5964 | |||
| ebbd1058f3 | |||
| 9e356fa4b5 | |||
| e030d85886 | |||
| ea9ac21d1a | |||
| e256acbcbb | |||
| b4d11df2a2 | |||
| 2c0ed08368 | |||
| 12fe6c196d | |||
| 0415972c7d | |||
| 6c7bb35ac3 | |||
| 834b1325b5 | |||
| 4a1b2ea143 | |||
| a748ee863c | |||
| 0546e1eaae | |||
| 4595db209e | |||
| 3f3ff49573 | |||
| 14318c90c2 | |||
| 3d79fe9aeb | |||
| d2e24741af | |||
| cb6582ef16 | |||
| d0c3a563d1 | |||
| 70b5c8de08 | |||
| a2eaf549af | |||
| c97c29f9ed | |||
| ea48f61f8c | |||
| d92df704c4 | |||
| ad4c658b3d | |||
| 0788e8e2ab | |||
| a68e90df9d | |||
| bacc0eba19 | |||
| c8f4e38f6b | |||
| 0dcb8a4a1d | |||
| fa31455619 | |||
| bf4dfac2a0 | |||
| d3ceea0e80 | |||
| 49a7418830 | |||
| 1b9fc4e0f8 | |||
| 426853aef7 | |||
| 3f20ad985c | |||
| ffa50f6460 | |||
| 08e2f171eb | |||
| 9d49968272 | |||
| be98c55e46 | |||
| c6c9b217a1 | |||
| 657928a01a | |||
| 91ad0353a6 | |||
| 431c907391 | |||
| 38d5a8eb90 | |||
| 19ab206f56 | |||
| 642aca10fe | |||
| a5b6d7a42a | |||
| 9003570c5a | |||
| c241463bb1 | |||
| 317c4e900a | |||
| e948074c6a | |||
| 848f07429c | |||
| 203327f5ed | |||
| 92261be464 | |||
| 28e61b8f8a | |||
| 188db2d4b8 | |||
| 1f0b4596ff | |||
| 1ed11dca03 | |||
| ecc5d624d5 | |||
| dac39212d7 | |||
| 43abc6514e | |||
| 8d42ef40c5 | |||
| 0546dde89f | |||
| 598ec0712c | |||
| 6f9df77f79 | |||
| 89aaef14e7 | |||
| f72cafe4d7 | |||
| a965bcf0ef | |||
| bd6eec88af | |||
| ce7e36f779 | |||
| 46b1469121 | |||
| 1e936a67c4 | |||
| 0903a4b335 | |||
| a7823c6440 | |||
| ed720b2ea9 | |||
| 263ac78515 | |||
| b9f83c43bc | |||
| f4609088e3 | |||
| d9326ea34b | |||
| 6589adcf75 | |||
| 2e2c1b82b3 | |||
| 0451fa2138 | |||
| 66b90754f8 | |||
| e66b7e9a79 | |||
| 4f056763e9 | |||
| de70224728 | |||
| 6f69af666f | |||
| 1f7278022c | |||
| b5e8d3dfe2 | |||
| 3edec0687c | |||
| a503e12ef9 | |||
| ea60ac60ba | |||
| 825820f7b9 | |||
| ba4a806cd7 | |||
| effd1fd588 | |||
| bf2b01df2d | |||
| 4581088a0a | |||
| 863dbb02f4 | |||
| 8fd8015b19 | |||
| 83ddbf0d73 | |||
| fad0170cef | |||
| a734d381ac | |||
| 0b8f492613 | |||
| 11c3488438 | |||
| cc709a0231 | |||
| 0a0d998208 | |||
| 03839601bb | |||
| 3e28dd4fae | |||
| 2674111e0b | |||
| 7488225aa6 | |||
| c1a9816c57 | |||
| 2d1932719a | |||
| 315be81e20 | |||
| 65d9aa3e9f | |||
| 8243e8c49d | |||
| c9d31b3ba4 | |||
| 29cfee7154 | |||
| bbae842fdb | |||
| 85e966a3f4 | |||
| 3d8bfb6112 | |||
| 7822064045 | |||
| 906861638f | |||
| 78dd453a9b | |||
| 204520d9c9 | |||
| 72b967c0ab | |||
| 781266885f | |||
| a869619fcd | |||
| 625965e129 | |||
| 91504c663b | |||
| 6cd690b737 | |||
| 3b2fe37ce1 | |||
| 8fea27e8b6 | |||
| cbea5752d1 | |||
| 34e789298b | |||
| 62c49eab5a | |||
| 2f8c81792d | |||
| 9a356cdd04 | |||
| 69ff510bac |
@@ -8,6 +8,7 @@
|
|||||||
<name>MokoWaaS</name>
|
<name>MokoWaaS</name>
|
||||||
<org>MokoConsulting</org>
|
<org>MokoConsulting</org>
|
||||||
<description>White-label identity, security hardening, and tenant restriction layer for WaaS-managed Joomla environments</description>
|
<description>White-label identity, security hardening, and tenant restriction layer for WaaS-managed Joomla environments</description>
|
||||||
|
<version>02.16.00-dev</version>
|
||||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||||
</identity>
|
</identity>
|
||||||
<governance>
|
<governance>
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# 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
|
||||||
@@ -27,13 +27,19 @@ name: "Universal: Build & Release"
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [closed]
|
types: [opened, closed]
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
|
||||||
- 'src/**'
|
|
||||||
- 'htdocs/**'
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
action:
|
||||||
|
description: 'Action to perform'
|
||||||
|
required: false
|
||||||
|
type: choice
|
||||||
|
default: release
|
||||||
|
options:
|
||||||
|
- release
|
||||||
|
- promote-rc
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
@@ -45,29 +51,94 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
# ── Draft PR → Promote highest pre-release to RC ─────────────────────────────
|
||||||
name: Build & Release Pipeline
|
promote-rc:
|
||||||
|
name: Promote Pre-Release to RC
|
||||||
runs-on: release
|
runs-on: release
|
||||||
if: >-
|
if: >-
|
||||||
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
|
(github.event.action == 'opened' && github.event.pull_request.draft == true) ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
- name: Setup moko-platform tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_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-api
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
|
/tmp/moko-platform-api
|
||||||
|
cd /tmp/moko-platform-api
|
||||||
|
composer install --no-dev --no-interaction --quiet
|
||||||
|
|
||||||
|
- name: Promote to release-candidate
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php /tmp/moko-platform-api/cli/release_promote.php \
|
||||||
|
--from auto --to release-candidate \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
--api-base "${API_BASE}" \
|
||||||
|
--branch "${{ github.event.pull_request.head.ref || 'dev' }}"
|
||||||
|
|
||||||
|
- name: Cascade lesser channels
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php /tmp/moko-platform-api/cli/release_cascade.php \
|
||||||
|
--stability release-candidate \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
--api-base "${API_BASE}"
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Draft PR opened — promoted highest pre-release to RC" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||||
|
release:
|
||||||
|
name: Build & Release Pipeline
|
||||||
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
github.event.pull_request.merged == true ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure git for bot pushes
|
||||||
|
run: |
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
|
||||||
|
- name: Setup moko-platform tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||||
run: |
|
run: |
|
||||||
# Ensure PHP + Composer are available
|
# Ensure PHP + Composer are available
|
||||||
if ! command -v composer &> /dev/null; then
|
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
|
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
|
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 \
|
git clone --depth 1 --branch main --quiet \
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
/tmp/moko-platform-api
|
/tmp/moko-platform-api
|
||||||
@@ -94,20 +165,44 @@ jobs:
|
|||||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
# Strip any pre-release suffix merged from dev (e.g. 01.02.20-dev → 01.02.20)
|
||||||
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
MAJOR=$(echo "$VERSION" | cut -d. -f1)
|
MAJOR=$(echo "$VERSION" | cut -d. -f1)
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT"
|
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
|
||||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT"
|
echo "branch=main" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: "Step 1b: Bump version"
|
# -- CHECK FOR RC PROMOTION ------------------------------------------------
|
||||||
id: bump
|
- name: "Check for RC release"
|
||||||
|
id: rc
|
||||||
if: steps.version.outputs.skip != 'true'
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
RC_JSON=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/tags/release-candidate" 2>/dev/null || echo "{}")
|
||||||
|
RC_ID=$(echo "$RC_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$RC_ID" ] && [ "$RC_ID" != "None" ] && [ "$RC_ID" != "" ]; then
|
||||||
|
echo "promote=true" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "release_id=${RC_ID}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "::notice::RC release found (id: ${RC_ID}) — will promote to stable"
|
||||||
|
else
|
||||||
|
echo "promote=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "::notice::No RC release — full build pipeline"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: "Step 1b: Minor bump version"
|
||||||
|
id: bump
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.rc.outputs.promote != 'true'
|
||||||
run: |
|
run: |
|
||||||
MOKO_API="/tmp/moko-platform-api/cli"
|
MOKO_API="/tmp/moko-platform-api/cli"
|
||||||
BUMP=$(php ${MOKO_API}/version_bump.php --path . --minor)
|
php ${MOKO_API}/version_bump.php --path . --minor 2>&1 || true
|
||||||
VERSION=$(echo "$BUMP" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true)
|
VERSION=$(php ${MOKO_API}/version_read.php --path .)
|
||||||
[ -z "$VERSION" ] && VERSION=$(php ${MOKO_API}/version_read.php --path .)
|
# Strip any pre-release suffix — stable releases have no suffix
|
||||||
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
echo "Bumped to: ${VERSION}"
|
echo "Bumped to: ${VERSION}"
|
||||||
|
|
||||||
@@ -137,95 +232,8 @@ jobs:
|
|||||||
steps.check.outputs.already_released != 'true'
|
steps.check.outputs.already_released != 'true'
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
ERRORS=0
|
php /tmp/moko-platform-api/cli/release_validate.php \
|
||||||
|
--path . --version "$VERSION" --output-summary --github-output || true
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
|
||||||
MANIFEST="${{ steps.platform.outputs.manifest }}"
|
|
||||||
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
|
|
||||||
echo "## Pre-Release Sanity Checks (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
# -- Version drift check (must pass before release) --------
|
|
||||||
README_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
|
|
||||||
if [ "$README_VER" != "$VERSION" ]; then
|
|
||||||
echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS+1))
|
|
||||||
else
|
|
||||||
echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check CHANGELOG version matches
|
|
||||||
CL_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' CHANGELOG.md 2>/dev/null | head -1)
|
|
||||||
if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then
|
|
||||||
echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS+1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check composer.json version if present
|
|
||||||
if [ -f "composer.json" ]; then
|
|
||||||
COMP_VER=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' composer.json 2>/dev/null | head -1)
|
|
||||||
if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then
|
|
||||||
echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS+1))
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Common checks
|
|
||||||
if [ ! -f "LICENSE" ]; then
|
|
||||||
echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS+1))
|
|
||||||
else
|
|
||||||
echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -d "src" ] && [ ! -d "htdocs" ]; then
|
|
||||||
echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "- Source directory present" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
# -- Platform-specific checks --------
|
|
||||||
case "$PLATFORM" in
|
|
||||||
joomla)
|
|
||||||
if [ -n "$MANIFEST" ]; then
|
|
||||||
XML_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
|
|
||||||
if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then
|
|
||||||
echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS+1))
|
|
||||||
else
|
|
||||||
echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null)
|
|
||||||
echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "- No Joomla XML manifest (WaaS site)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi ;;
|
|
||||||
dolibarr)
|
|
||||||
if [ -n "$MOD_FILE" ]; then
|
|
||||||
MOD_VER=$(sed -n "s/.*\\\$this->version = '\([^']*\)'.*/\1/p" "$MOD_FILE" 2>/dev/null | head -1)
|
|
||||||
if [ -n "$MOD_VER" ] && [ "$MOD_VER" != "$VERSION" ]; then
|
|
||||||
echo "- Module drift: \`${MOD_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS+1))
|
|
||||||
else
|
|
||||||
echo "- Module version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "- No mod*.class.php found" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS+1))
|
|
||||||
fi
|
|
||||||
if [ ! -f "update.txt" ]; then
|
|
||||||
echo "- Missing update.txt" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS+1))
|
|
||||||
fi ;;
|
|
||||||
*) echo "- Generic platform � no manifest checks" >> $GITHUB_STEP_SUMMARY ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "$ERRORS" -gt 0 ]; then
|
|
||||||
echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
# -- STEP 2: Create or update version/XX.YY archive branch ---------------
|
# -- STEP 2: Create or update version/XX.YY archive branch ---------------
|
||||||
# Always runs — every version change on main archives to version/XX.YY
|
# Always runs — every version change on main archives to version/XX.YY
|
||||||
@@ -263,17 +271,21 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true
|
php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true
|
||||||
|
php /tmp/moko-platform-api/cli/version_check.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
- name: "Step 5: Write update stream"
|
# Step 5 (updates.xml) moved after Step 8 to include SHA-256 checksum
|
||||||
|
|
||||||
|
- name: "Step 4b: Promote and prune CHANGELOG"
|
||||||
if: >-
|
if: >-
|
||||||
steps.version.outputs.skip != 'true' &&
|
steps.version.outputs.skip != 'true' &&
|
||||||
steps.platform.outputs.platform == 'joomla'
|
steps.check.outputs.already_released != 'true'
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
php /tmp/moko-platform-api/cli/updates_xml_build.php \
|
MOKO_API="/tmp/moko-platform-api/cli"
|
||||||
--path . --version "${VERSION}" --stability stable \
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
php ${MOKO_API}/changelog_promote.php --path . --version "$VERSION" 2>&1 || true
|
||||||
--github-output
|
php ${MOKO_API}/changelog_prune.php --path . --keep 5 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Commit release changes
|
- name: Commit release changes
|
||||||
if: >-
|
if: >-
|
||||||
@@ -285,21 +297,16 @@ jobs:
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "gitea-actions[bot]"
|
|
||||||
# Set push URL with token for branch-protected repos
|
|
||||||
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
|
||||||
git add -A
|
git add -A
|
||||||
git commit -m "chore(release): build ${VERSION} [skip ci]" \
|
git commit -m "chore(release): build ${VERSION} [skip ci]" \
|
||||||
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
||||||
git push -u origin HEAD
|
# Detached HEAD on PR merge — push explicitly to main
|
||||||
|
git push origin HEAD:refs/heads/main
|
||||||
|
|
||||||
# -- STEP 6: Create tag ---------------------------------------------------
|
# -- STEP 6: Create tag ---------------------------------------------------
|
||||||
- name: "Step 6: Create git tag"
|
- name: "Step 6: Create git tag"
|
||||||
if: >-
|
if: >-
|
||||||
steps.version.outputs.skip != 'true' &&
|
steps.version.outputs.skip != 'true'
|
||||||
steps.check.outputs.tag_exists != 'true' &&
|
|
||||||
steps.version.outputs.is_minor == 'true'
|
|
||||||
run: |
|
run: |
|
||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
# Only create the major release tag if it doesn't exist yet
|
# Only create the major release tag if it doesn't exist yet
|
||||||
@@ -312,384 +319,126 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
|
echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# -- STEP 7: Create or update Gitea Release --------------------------------
|
# -- STEP 7a: Promote RC to stable (skip build) ----------------------------
|
||||||
- name: "Step 7: Gitea Release"
|
- name: "Step 7a: Promote RC to stable"
|
||||||
if: >-
|
if: >-
|
||||||
steps.version.outputs.skip != 'true'
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.rc.outputs.promote == 'true'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php /tmp/moko-platform-api/cli/release_promote.php \
|
||||||
|
--from release-candidate --to stable \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
--api-base "${API_BASE}" \
|
||||||
|
--path . --branch main
|
||||||
|
echo "Promoted RC → stable (${VERSION})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# -- STEP 7b: Create or update Gitea Release (full build path) -------------
|
||||||
|
- name: "Step 7b: Gitea Release"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.rc.outputs.promote != 'true'
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
BRANCH="${{ steps.version.outputs.branch }}"
|
|
||||||
MAJOR="${{ steps.version.outputs.major }}"
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php /tmp/moko-platform-api/cli/release_create.php \
|
||||||
|
--path . --version "$VERSION" --tag "$RELEASE_TAG" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--repo "${GITEA_REPO}" --branch main
|
||||||
|
echo "Release created: ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# Reuse metadata from Step 5 (single source of truth)
|
# -- STEP 8: Build packages and upload to release ----------------------------
|
||||||
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
|
- name: "Step 8: Build package and upload"
|
||||||
EXT_NAME="${{ steps.updates.outputs.ext_name }}"
|
id: package
|
||||||
EXT_TYPE="${{ steps.updates.outputs.ext_type }}"
|
|
||||||
EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}"
|
|
||||||
|
|
||||||
# Fallbacks if Step 5 was skipped
|
|
||||||
if [ -z "$EXT_ELEMENT" ]; then
|
|
||||||
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
|
||||||
fi
|
|
||||||
[ -z "$EXT_NAME" ] && EXT_NAME="${GITEA_REPO}"
|
|
||||||
|
|
||||||
NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null)
|
|
||||||
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
|
||||||
|
|
||||||
# Build release name: "Pretty Name VERSION (type_element-VERSION)"
|
|
||||||
# Strip existing type prefix to prevent duplication
|
|
||||||
EXT_ELEMENT=$(echo "$EXT_ELEMENT" | sed -E 's/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)//')
|
|
||||||
TYPE_PREFIX=""
|
|
||||||
case "${EXT_TYPE}" in
|
|
||||||
plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
|
|
||||||
module) TYPE_PREFIX="mod_" ;;
|
|
||||||
component) TYPE_PREFIX="com_" ;;
|
|
||||||
template) TYPE_PREFIX="tpl_" ;;
|
|
||||||
library) TYPE_PREFIX="lib_" ;;
|
|
||||||
package) TYPE_PREFIX="pkg_" ;;
|
|
||||||
esac
|
|
||||||
RELEASE_NAME="${EXT_NAME} ${VERSION} (${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION})"
|
|
||||||
|
|
||||||
# Delete existing release if present (overwrite, not append)
|
|
||||||
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
|
|
||||||
EXISTING_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -n "$EXISTING_ID" ]; then
|
|
||||||
curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/${EXISTING_ID}" 2>/dev/null || true
|
|
||||||
curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
"${API_BASE}/tags/${RELEASE_TAG}" 2>/dev/null || true
|
|
||||||
echo "Deleted previous stable release (id: ${EXISTING_ID})"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create fresh release
|
|
||||||
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API_BASE}/releases" \
|
|
||||||
-d "$(python3 -c "import json; print(json.dumps({
|
|
||||||
'tag_name': '${RELEASE_TAG}',
|
|
||||||
'name': '${RELEASE_NAME}',
|
|
||||||
'body': '''## ${VERSION} ($(date +%Y-%m-%d))\n${NOTES}''',
|
|
||||||
'target_commitish': '${BRANCH}'
|
|
||||||
}))")"
|
|
||||||
echo "Release created: ${RELEASE_NAME}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
# -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------
|
|
||||||
- name: "Step 8: Build package and update checksum"
|
|
||||||
if: >-
|
if: >-
|
||||||
steps.version.outputs.skip != 'true'
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
steps.rc.outputs.promote != 'true'
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
REPO="${{ github.repository }}"
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php /tmp/moko-platform-api/cli/release_package.php \
|
||||||
|
--path . --version "$VERSION" --tag "$RELEASE_TAG" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--repo "${GITEA_REPO}" --output /tmp || true
|
||||||
|
|
||||||
# All ZIPs upload to the major release tag (vXX)
|
# -- STEP 5: Write update stream (after build so SHA-256 is available) -----
|
||||||
RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
- name: "Step 5: Write update stream"
|
||||||
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
|
|
||||||
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
||||||
if [ -z "$RELEASE_ID" ]; then
|
|
||||||
echo "No release ${RELEASE_TAG} found — skipping ZIP upload"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Find extension element name from manifest
|
|
||||||
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
|
|
||||||
[ -z "$MANIFEST" ] && exit 0
|
|
||||||
|
|
||||||
# Reuse element from Step 5, with same fallback chain
|
|
||||||
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
|
|
||||||
if [ -z "$EXT_ELEMENT" ]; then
|
|
||||||
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
|
|
||||||
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
|
|
||||||
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
|
|
||||||
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
|
||||||
fi
|
|
||||||
# ZIP name: type_folder_element-VERSION (e.g. plg_system_mokojgdpc-01.01.00.zip)
|
|
||||||
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
# For packages, prefer <packagename> over filename-derived element
|
|
||||||
if [ "$EXT_TYPE" = "package" ]; then
|
|
||||||
PKG_NAME=$(sed -n 's/.*<packagename>\([^<]*\)<\/packagename>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
|
|
||||||
[ -n "$PKG_NAME" ] && EXT_ELEMENT="$PKG_NAME"
|
|
||||||
fi
|
|
||||||
# Strip existing type prefix to prevent duplication (e.g. pkg_mokowaas → mokowaas)
|
|
||||||
EXT_ELEMENT=$(echo "$EXT_ELEMENT" | sed -E 's/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)//')
|
|
||||||
TYPE_PREFIX=""
|
|
||||||
case "${EXT_TYPE}" in
|
|
||||||
plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
|
|
||||||
module) TYPE_PREFIX="mod_" ;;
|
|
||||||
component) TYPE_PREFIX="com_" ;;
|
|
||||||
template) TYPE_PREFIX="tpl_" ;;
|
|
||||||
library) TYPE_PREFIX="lib_" ;;
|
|
||||||
package) TYPE_PREFIX="pkg_" ;;
|
|
||||||
esac
|
|
||||||
ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
|
|
||||||
TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
|
|
||||||
|
|
||||||
# -- Build install packages from src/ ----------------------------
|
|
||||||
SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/"; exit 0; }
|
|
||||||
|
|
||||||
# ZIP package (type-aware via moko-platform PHP API)
|
|
||||||
php /tmp/moko-platform-api/cli/joomla_build.php --path . --version "${VERSION}" --output /tmp
|
|
||||||
# Match the expected ZIP_NAME for upload
|
|
||||||
BUILT_ZIP=$(ls /tmp/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip 2>/dev/null | head -1 || true)
|
|
||||||
if [ -n "$BUILT_ZIP" ] && [ "$BUILT_ZIP" != "/tmp/${ZIP_NAME}" ]; then
|
|
||||||
mv "$BUILT_ZIP" "/tmp/${ZIP_NAME}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# tar.gz package (flat source archive)
|
|
||||||
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" --exclude='.ftpignore' --exclude='sftp-config*' --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
|
|
||||||
|
|
||||||
ZIP_SIZE=$(stat -c%s "/tmp/${ZIP_NAME}" 2>/dev/null || stat -f%z "/tmp/${ZIP_NAME}" 2>/dev/null || echo "unknown")
|
|
||||||
TAR_SIZE=$(stat -c%s "/tmp/${TAR_NAME}" 2>/dev/null || stat -f%z "/tmp/${TAR_NAME}" 2>/dev/null || echo "unknown")
|
|
||||||
|
|
||||||
# -- Calculate SHA-256 for both ----------------------------------
|
|
||||||
SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1)
|
|
||||||
SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1)
|
|
||||||
|
|
||||||
# -- Delete existing assets with same name before uploading ------
|
|
||||||
ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]")
|
|
||||||
for ASSET_NAME in "$ZIP_NAME" "$TAR_NAME"; do
|
|
||||||
ASSET_ID=$(echo "$ASSETS" | python3 -c "
|
|
||||||
import sys,json
|
|
||||||
assets = json.load(sys.stdin)
|
|
||||||
for a in assets:
|
|
||||||
if a['name'] == '${ASSET_NAME}':
|
|
||||||
print(a['id']); break
|
|
||||||
" 2>/dev/null || true)
|
|
||||||
if [ -n "$ASSET_ID" ]; then
|
|
||||||
curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# -- Upload both to release tag ----------------------------------
|
|
||||||
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
-H "Content-Type: application/octet-stream" \
|
|
||||||
--data-binary @"/tmp/${ZIP_NAME}" \
|
|
||||||
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" > /dev/null 2>&1 || true
|
|
||||||
|
|
||||||
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
-H "Content-Type: application/octet-stream" \
|
|
||||||
--data-binary @"/tmp/${TAR_NAME}" \
|
|
||||||
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true
|
|
||||||
|
|
||||||
# -- Update updates.xml with both download formats ---------------
|
|
||||||
if [ -f "updates.xml" ]; then
|
|
||||||
ZIP_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}"
|
|
||||||
TAR_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${TAR_NAME}"
|
|
||||||
|
|
||||||
# Use Python to update only the stable entry's downloads + sha256
|
|
||||||
export PY_ZIP_URL="$ZIP_URL" PY_TAR_URL="$TAR_URL" PY_SHA="$SHA256_ZIP"
|
|
||||||
python3 << 'PYEOF'
|
|
||||||
import re, os
|
|
||||||
|
|
||||||
with open("updates.xml") as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
zip_url = os.environ["PY_ZIP_URL"]
|
|
||||||
tar_url = os.environ["PY_TAR_URL"]
|
|
||||||
sha = os.environ["PY_SHA"]
|
|
||||||
|
|
||||||
# Find the stable update block and replace its downloads + sha256
|
|
||||||
def replace_stable(m):
|
|
||||||
block = m.group(0)
|
|
||||||
# Replace downloads block
|
|
||||||
new_downloads = (
|
|
||||||
" <downloads>\n"
|
|
||||||
f" <downloadurl type=\"full\" format=\"zip\">{zip_url}</downloadurl>\n"
|
|
||||||
" </downloads>"
|
|
||||||
)
|
|
||||||
block = re.sub(r' <downloads>.*?</downloads>', new_downloads, block, flags=re.DOTALL)
|
|
||||||
# Add or replace sha256
|
|
||||||
if '<sha256>' in block:
|
|
||||||
block = re.sub(r' <sha256>.*?</sha256>', f' <sha256>{sha}</sha256>', block)
|
|
||||||
else:
|
|
||||||
block = block.replace('</downloads>', f'</downloads>\n <sha256>{sha}</sha256>')
|
|
||||||
return block
|
|
||||||
|
|
||||||
content = re.sub(
|
|
||||||
r' <update>.*?<tag>stable</tag>.*?</update>',
|
|
||||||
replace_stable,
|
|
||||||
content,
|
|
||||||
flags=re.DOTALL
|
|
||||||
)
|
|
||||||
|
|
||||||
with open("updates.xml", "w") as f:
|
|
||||||
f.write(content)
|
|
||||||
PYEOF
|
|
||||||
|
|
||||||
CURRENT_BRANCH="${{ github.ref_name }}"
|
|
||||||
git add updates.xml
|
|
||||||
git commit -m "chore(release): ZIP + tar.gz for ${VERSION} [skip ci]" \
|
|
||||||
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" || true
|
|
||||||
git push || true
|
|
||||||
|
|
||||||
# Sync updates.xml to main via direct API (always runs — may be on version/XX branch)
|
|
||||||
GA_TOKEN="${{ secrets.GA_TOKEN }}"
|
|
||||||
API="${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}"
|
|
||||||
|
|
||||||
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
|
|
||||||
"${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty')
|
|
||||||
|
|
||||||
if [ -n "$FILE_SHA" ]; then
|
|
||||||
CONTENT=$(base64 -w0 updates.xml)
|
|
||||||
curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API}/contents/updates.xml" \
|
|
||||||
-d "$(jq -n \
|
|
||||||
--arg content "$CONTENT" \
|
|
||||||
--arg sha "$FILE_SHA" \
|
|
||||||
--arg msg "chore: sync updates.xml ${VERSION} [skip ci]" \
|
|
||||||
--arg branch "main" \
|
|
||||||
'{content: $content, sha: $sha, message: $msg, branch: $branch}'
|
|
||||||
)" > /dev/null 2>&1 \
|
|
||||||
&& echo "updates.xml synced to main via API" \
|
|
||||||
|| echo "WARNING: failed to sync updates.xml to main"
|
|
||||||
else
|
|
||||||
echo "WARNING: could not get updates.xml SHA from main"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "### Packages" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Package | Size | SHA-256 |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Download | [${ZIP_NAME}](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}) |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
# -- STEP 8b: Update release description with changelog + SHA ----------------
|
|
||||||
- name: "Step 8b: Update release body with changelog and SHA"
|
|
||||||
if: steps.version.outputs.skip != 'true'
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||||
|
|
||||||
|
# Fetch latest updates.xml from main so preserve logic has all channels
|
||||||
|
GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/contents/updates.xml?ref=main" 2>/dev/null | \
|
||||||
|
python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin)['content']).decode())" \
|
||||||
|
> updates.xml 2>/dev/null || true
|
||||||
|
|
||||||
|
SHA_FLAG=""
|
||||||
|
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
|
||||||
|
|
||||||
|
php /tmp/moko-platform-api/cli/updates_xml_build.php \
|
||||||
|
--path . --version "${VERSION}" --stability stable \
|
||||||
|
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
||||||
|
${SHA_FLAG} --github-output
|
||||||
|
|
||||||
|
# Commit updates.xml if changed
|
||||||
|
if ! git diff --quiet updates.xml 2>/dev/null; then
|
||||||
|
git add updates.xml
|
||||||
|
git commit -m "chore: update stable channel ${VERSION} [skip ci]" \
|
||||||
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
||||||
|
git push origin HEAD:refs/heads/main 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- STEP 8b: Update release description with changelog ----------------------
|
||||||
|
- name: "Step 8b: Update release body"
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
php /tmp/moko-platform-api/cli/release_body_update.php \
|
||||||
EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
|
--path . --version "${VERSION}" --tag "${RELEASE_TAG}" \
|
||||||
EXT_TYPE="${{ steps.updates.outputs.ext_type }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}"
|
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
||||||
|
2>&1 || true
|
||||||
# Build TYPE_PREFIX to match Step 8's ZIP naming
|
echo "Release body updated" >> $GITHUB_STEP_SUMMARY
|
||||||
EXT_ELEMENT=$(echo "$EXT_ELEMENT" | sed -E 's/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)//')
|
|
||||||
TYPE_PREFIX=""
|
|
||||||
case "${EXT_TYPE}" in
|
|
||||||
plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
|
|
||||||
module) TYPE_PREFIX="mod_" ;;
|
|
||||||
component) TYPE_PREFIX="com_" ;;
|
|
||||||
template) TYPE_PREFIX="tpl_" ;;
|
|
||||||
library) TYPE_PREFIX="lib_" ;;
|
|
||||||
package) TYPE_PREFIX="pkg_" ;;
|
|
||||||
esac
|
|
||||||
ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
|
|
||||||
TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
|
|
||||||
|
|
||||||
# Get SHA from the built files
|
|
||||||
SHA256_ZIP=""
|
|
||||||
[ -f "/tmp/${ZIP_NAME}" ] && SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1)
|
|
||||||
SHA256_TAR=""
|
|
||||||
[ -f "/tmp/${TAR_NAME}" ] && SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1)
|
|
||||||
|
|
||||||
# Extract latest changelog entry (strip the ## header to avoid duplicate)
|
|
||||||
CHANGELOG=""
|
|
||||||
if [ -f "CHANGELOG.md" ]; then
|
|
||||||
CHANGELOG=$(sed -n "/^## \[*${VERSION}/,/^## \[*[0-9]/p" CHANGELOG.md | sed '$d' | sed '1d')
|
|
||||||
[ -z "$CHANGELOG" ] && CHANGELOG=$(sed -n '/^## /,/^## /p' CHANGELOG.md | sed '$d' | sed '1d' | head -30)
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build release body (single header, no duplicate from changelog)
|
|
||||||
BODY="## ${VERSION} ($(date +%Y-%m-%d))\n\n"
|
|
||||||
if [ -n "$CHANGELOG" ]; then
|
|
||||||
BODY="${BODY}${CHANGELOG}\n\n"
|
|
||||||
fi
|
|
||||||
BODY="${BODY}---\n\n### Checksums\n\n"
|
|
||||||
BODY="${BODY}| File | SHA-256 |\n|------|--------|\n"
|
|
||||||
[ -n "$SHA256_ZIP" ] && BODY="${BODY}| \`${ZIP_NAME}\` | \`${SHA256_ZIP}\` |\n"
|
|
||||||
[ -n "$SHA256_TAR" ] && BODY="${BODY}| \`${TAR_NAME}\` | \`${SHA256_TAR}\` |\n"
|
|
||||||
|
|
||||||
# Get release ID and update body
|
|
||||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null | \
|
|
||||||
python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then
|
|
||||||
python3 -c "
|
|
||||||
import json, urllib.request
|
|
||||||
body = '''$(printf '%b' "$BODY")'''
|
|
||||||
data = json.dumps({'body': body}).encode()
|
|
||||||
req = urllib.request.Request(
|
|
||||||
'${API_BASE}/releases/${RELEASE_ID}',
|
|
||||||
data=data,
|
|
||||||
headers={'Authorization': 'token ${{ secrets.GA_TOKEN }}', 'Content-Type': 'application/json'},
|
|
||||||
method='PATCH'
|
|
||||||
)
|
|
||||||
urllib.request.urlopen(req)
|
|
||||||
" 2>/dev/null && echo "Release body updated with changelog + SHA" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||||
- name: "Step 9: Mirror release to GitHub"
|
- name: "Step 9: Mirror release to GitHub"
|
||||||
if: >-
|
if: >-
|
||||||
steps.version.outputs.skip != 'true' &&
|
steps.version.outputs.skip != 'true' &&
|
||||||
steps.version.outputs.stability == 'stable' &&
|
secrets.GH_MIRROR_TOKEN != ''
|
||||||
secrets.GH_TOKEN != ''
|
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
MAJOR="${{ steps.version.outputs.major }}"
|
|
||||||
BRANCH="${{ steps.version.outputs.branch }}"
|
|
||||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true)
|
php /tmp/moko-platform-api/cli/release_mirror.php \
|
||||||
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||||
echo "$NOTES" > /tmp/release_notes.md
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
||||||
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".tag_name // empty" || true)
|
--branch main 2>&1 || true
|
||||||
|
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
|
||||||
if [ -z "$EXISTING" ]; then
|
|
||||||
gh release create "$RELEASE_TAG" \
|
|
||||||
--repo "$GH_REPO" \
|
|
||||||
--title "v${MAJOR} (latest: ${VERSION})" \
|
|
||||||
--notes-file /tmp/release_notes.md \
|
|
||||||
--target "$BRANCH" || true
|
|
||||||
else
|
|
||||||
gh release edit "$RELEASE_TAG" \
|
|
||||||
--repo "$GH_REPO" \
|
|
||||||
--title "v${MAJOR} (latest: ${VERSION})" || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Upload assets to GitHub mirror
|
|
||||||
for PKG in /tmp/${EXT_ELEMENT:-pkg}-${VERSION}.*; do
|
|
||||||
if [ -f "$PKG" ]; then
|
|
||||||
_RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".id // empty")
|
|
||||||
[ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo "GitHub mirror updated: ${GH_REPO} ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
|
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
|
||||||
- name: "Step 10: Push main to GitHub mirror"
|
- name: "Step 10: Push main to GitHub mirror"
|
||||||
if: >-
|
if: >-
|
||||||
steps.version.outputs.skip != 'true' &&
|
steps.version.outputs.skip != 'true' &&
|
||||||
secrets.GH_TOKEN != ''
|
secrets.GH_MIRROR_TOKEN != ''
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
|
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
|
||||||
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
|
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
|
||||||
git remote add github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
|
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
|
||||||
git remote set-url github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
|
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
|
||||||
git fetch origin main --depth=1
|
git fetch origin main --depth=1
|
||||||
git push github origin/main:refs/heads/main --force 2>/dev/null \
|
git push github origin/main:refs/heads/main --force 2>/dev/null \
|
||||||
&& echo "main branch pushed to GitHub mirror" \
|
&& echo "main branch pushed to GitHub mirror" \
|
||||||
@@ -700,18 +449,20 @@ jobs:
|
|||||||
- name: "Delete lesser pre-release channels"
|
- name: "Delete lesser pre-release channels"
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
php /tmp/moko-platform-api/cli/release_cascade.php \
|
php /tmp/moko-platform-api/cli/release_cascade.php \
|
||||||
--stability stable \
|
--stability stable \
|
||||||
--token "${{ secrets.GA_TOKEN }}" \
|
--version "${VERSION}" \
|
||||||
--org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
--gitea-url "${GITEA_URL}" 2>/dev/null || true
|
--api-base "${API_BASE}" 2>/dev/null || true
|
||||||
|
|
||||||
- name: "Step 11: Delete and recreate dev branch from main"
|
- name: "Step 11: Delete and recreate dev branch from main"
|
||||||
if: steps.version.outputs.skip != 'true'
|
if: steps.version.outputs.skip != 'true'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
TOKEN="${{ secrets.GA_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
# Delete dev branch
|
# Delete dev branch
|
||||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
@@ -725,28 +476,35 @@ jobs:
|
|||||||
|
|
||||||
echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY
|
echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
- name: "Step 12: Create version branch from main"
|
||||||
# -- Dolibarr post-release: Reset dev version -----------------------------
|
if: steps.version.outputs.skip != 'true'
|
||||||
- name: "Dolibarr: Reset dev version"
|
|
||||||
if: >-
|
|
||||||
steps.version.outputs.skip != 'true' &&
|
|
||||||
steps.platform.outputs.platform == 'dolibarr' &&
|
|
||||||
steps.platform.outputs.mod_file != ''
|
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
TOKEN="${{ secrets.GA_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
ENCODED_PATH=$(echo "$MOD_FILE" | sed 's|^\./||' | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))")
|
BRANCH_NAME="version/${VERSION}"
|
||||||
FILE_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/${ENCODED_PATH}?ref=dev" 2>/dev/null || true)
|
MAIN_SHA=$(git rev-parse HEAD)
|
||||||
FILE_SHA=$(echo "$FILE_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
|
|
||||||
FILE_CONTENT=$(echo "$FILE_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true)
|
# Delete old version branch if it exists (same version re-release)
|
||||||
if [ -n "$FILE_SHA" ] && [ -n "$FILE_CONTENT" ]; then
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
|
||||||
UPDATED=$(echo "$FILE_CONTENT" | sed "s/\$this->version = '[^']*'/\$this->version = 'development'/")
|
|
||||||
ENCODED=$(echo "$UPDATED" | base64 -w0)
|
# Create version/XX.YY.ZZ from main
|
||||||
curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/contents/${ENCODED_PATH}" \
|
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
|
||||||
-d "$(jq -n --arg content \"$ENCODED\" --arg sha \"$FILE_SHA\" --arg msg \"chore(version): reset dev version [skip ci]\" --arg branch \"dev\" '{content:$content,sha:$sha,message:$msg,branch:$branch}')" > /dev/null 2>&1 || true
|
|
||||||
fi
|
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# -- Dolibarr post-release: Reset dev version -----------------------------
|
||||||
|
- name: "Post-release: Reset dev version"
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php /tmp/moko-platform-api/cli/version_reset_dev.php \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||||
|
--branch dev --path . 2>&1 || true
|
||||||
|
|
||||||
# -- Summary --------------------------------------------------------------
|
# -- Summary --------------------------------------------------------------
|
||||||
- name: Pipeline Summary
|
- name: Pipeline Summary
|
||||||
|
|||||||
@@ -0,0 +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
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Maintenance
|
# INGROUP: moko-platform.Maintenance
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
# PATH: /templates/workflows/cascade-dev.yml.template
|
# PATH: /templates/workflows/cascade-dev.yml.template
|
||||||
# VERSION: 02.00.00
|
# VERSION: 02.00.00
|
||||||
# BRIEF: Forward-merge main → all open branches after every push to main
|
# BRIEF: Forward-merge main → all open branches after every push to main
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
- name: Discover target branches
|
- name: Discover target branches
|
||||||
id: branches
|
id: branches
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
ALL_BRANCHES=""
|
ALL_BRANCHES=""
|
||||||
while true; do
|
while true; do
|
||||||
BATCH=$(curl -sS \
|
BATCH=$(curl -sS \
|
||||||
-H "Authorization: token ${GA_TOKEN}" \
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
"${API}/branches?page=${PAGE}&limit=50" \
|
"${API}/branches?page=${PAGE}&limit=50" \
|
||||||
| jq -r '.[].name // empty')
|
| jq -r '.[].name // empty')
|
||||||
[ -z "$BATCH" ] && break
|
[ -z "$BATCH" ] && break
|
||||||
@@ -93,7 +93,7 @@ jobs:
|
|||||||
- name: Cascade to all target branches
|
- name: Cascade to all target branches
|
||||||
if: steps.branches.outputs.targets != ''
|
if: steps.branches.outputs.targets != ''
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
SHORT_SHA="${GITHUB_SHA:0:7}"
|
SHORT_SHA="${GITHUB_SHA:0:7}"
|
||||||
@@ -111,7 +111,7 @@ jobs:
|
|||||||
# Check if branch is already up to date
|
# Check if branch is already up to date
|
||||||
ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g')
|
ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g')
|
||||||
RESPONSE=$(curl -sS \
|
RESPONSE=$(curl -sS \
|
||||||
-H "Authorization: token ${GA_TOKEN}" \
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
"${API}/compare/${ENCODED_BRANCH}...main")
|
"${API}/compare/${ENCODED_BRANCH}...main")
|
||||||
|
|
||||||
AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0')
|
AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0')
|
||||||
@@ -126,7 +126,7 @@ jobs:
|
|||||||
|
|
||||||
# Check for existing cascade PR
|
# Check for existing cascade PR
|
||||||
EXISTING=$(curl -sS \
|
EXISTING=$(curl -sS \
|
||||||
-H "Authorization: token ${GA_TOKEN}" \
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
"${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1")
|
"${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1")
|
||||||
|
|
||||||
EXISTING_COUNT=$(echo "$EXISTING" | jq 'length')
|
EXISTING_COUNT=$(echo "$EXISTING" | jq 'length')
|
||||||
@@ -139,7 +139,7 @@ jobs:
|
|||||||
# Create cascade PR
|
# Create cascade PR
|
||||||
PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
||||||
-X POST \
|
-X POST \
|
||||||
-H "Authorization: token ${GA_TOKEN}" \
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{
|
-d "{
|
||||||
\"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\",
|
\"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\",
|
||||||
@@ -165,7 +165,7 @@ jobs:
|
|||||||
|
|
||||||
# Try auto-merge
|
# Try auto-merge
|
||||||
PR_DATA=$(curl -sS \
|
PR_DATA=$(curl -sS \
|
||||||
-H "Authorization: token ${GA_TOKEN}" \
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
"${API}/pulls/${PR_NUMBER}")
|
"${API}/pulls/${PR_NUMBER}")
|
||||||
|
|
||||||
MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false')
|
MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false')
|
||||||
@@ -178,7 +178,7 @@ jobs:
|
|||||||
|
|
||||||
MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
||||||
-X POST \
|
-X POST \
|
||||||
-H "Authorization: token ${GA_TOKEN}" \
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{
|
-d "{
|
||||||
\"Do\": \"merge\",
|
\"Do\": \"merge\",
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Clone MokoStandards
|
- name: Clone MokoStandards
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
|
||||||
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --branch main --quiet \
|
git clone --depth 1 --branch main --quiet \
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
env:
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
|
||||||
run: |
|
run: |
|
||||||
if [ -f "composer.json" ]; then
|
if [ -f "composer.json" ]; then
|
||||||
composer install \
|
composer install \
|
||||||
@@ -346,7 +346,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
env:
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
|
||||||
run: |
|
run: |
|
||||||
if [ -f "composer.json" ]; then
|
if [ -f "composer.json" ]; then
|
||||||
composer install \
|
composer install \
|
||||||
@@ -391,7 +391,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
env:
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
|
||||||
run: |
|
run: |
|
||||||
if [ -f "composer.json" ]; then
|
if [ -f "composer.json" ]; then
|
||||||
composer install --no-interaction --prefer-dist --optimize-autoloader
|
composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||||
@@ -458,10 +458,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Trigger pre-release build
|
- name: Trigger pre-release build
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
BRANCH: ${{ github.head_ref }}
|
BRANCH: ${{ github.head_ref }}
|
||||||
run: |
|
run: |
|
||||||
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\"}}"
|
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\"}}"
|
||||||
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Maintenance
|
# INGROUP: moko-platform.Maintenance
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /.gitea/workflows/cleanup.yml
|
# PATH: /.gitea/workflows/cleanup.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
||||||
@@ -33,17 +33,17 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.GA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
|
||||||
- name: Delete merged branches
|
- name: Delete merged branches
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "=== Merged Branch Cleanup ==="
|
echo "=== Merged Branch Cleanup ==="
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
|
||||||
# List branches via API
|
# List branches via API
|
||||||
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
BRANCHES=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
"${API}/branches?limit=50" | jq -r '.[].name')
|
"${API}/branches?limit=50" | jq -r '.[].name')
|
||||||
|
|
||||||
DELETED=0
|
DELETED=0
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
# Check if branch is merged into main
|
# Check if branch is merged into main
|
||||||
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
||||||
echo " Deleting merged branch: ${BRANCH}"
|
echo " Deleting merged branch: ${BRANCH}"
|
||||||
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
||||||
DELETED=$((DELETED + 1))
|
DELETED=$((DELETED + 1))
|
||||||
fi
|
fi
|
||||||
@@ -66,20 +66,20 @@ jobs:
|
|||||||
|
|
||||||
- name: Clean old workflow runs
|
- name: Clean old workflow runs
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "=== Workflow Run Cleanup ==="
|
echo "=== Workflow Run Cleanup ==="
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
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)
|
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
|
# Get old completed runs
|
||||||
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
RUNS=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
"${API}/actions/runs?status=completed&limit=50" | \
|
"${API}/actions/runs?status=completed&limit=50" | \
|
||||||
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
||||||
|
|
||||||
DELETED=0
|
DELETED=0
|
||||||
for RUN_ID in $RUNS; do
|
for RUN_ID in $RUNS; do
|
||||||
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
||||||
DELETED=$((DELETED + 1))
|
DELETED=$((DELETED + 1))
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Security
|
# INGROUP: moko-platform.Security
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
# PATH: /templates/workflows/gitleaks.yml.template
|
# PATH: /templates/workflows/gitleaks.yml.template
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: moko-platform.Automation
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
|
name: "Universal: Issue Branch"
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create-branch:
|
||||||
|
name: Create feature branch
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Create branch and comment
|
||||||
|
run: |
|
||||||
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
ISSUE_NUM="${{ github.event.issue.number }}"
|
||||||
|
ISSUE_TITLE="${{ github.event.issue.title }}"
|
||||||
|
|
||||||
|
# Build slug from title: lowercase, replace non-alnum with dash, trim
|
||||||
|
SLUG=$(echo "${ISSUE_TITLE}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-40)
|
||||||
|
BRANCH="feature/${ISSUE_NUM}-${SLUG}"
|
||||||
|
|
||||||
|
# Check dev branch exists
|
||||||
|
DEV_EXISTS=$(curl -sf -o /dev/null -w '%{http_code}' \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API}/branches/dev" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [ "${DEV_EXISTS}" != "200" ]; then
|
||||||
|
echo "No dev branch -- skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create branch from dev
|
||||||
|
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/branches" \
|
||||||
|
-d "{\"new_branch_name\":\"${BRANCH}\",\"old_branch_name\":\"dev\"}" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [ "${HTTP}" = "201" ]; then
|
||||||
|
echo "Created branch: ${BRANCH}"
|
||||||
|
|
||||||
|
# Comment on issue with branch link
|
||||||
|
REPO_URL="${GITEA_URL}/${{ github.repository }}"
|
||||||
|
BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`"
|
||||||
|
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/issues/${ISSUE_NUM}/comments" \
|
||||||
|
-d "{\"body\":\"${BODY}\"}" > /dev/null 2>&1
|
||||||
|
|
||||||
|
echo "Commented on issue #${ISSUE_NUM}"
|
||||||
|
else
|
||||||
|
echo "Failed to create branch (HTTP ${HTTP}) -- may already exist"
|
||||||
|
fi
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Notifications
|
# INGROUP: moko-platform.Notifications
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /.gitea/workflows/notify.yml
|
# PATH: /.gitea/workflows/notify.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||||
@@ -18,7 +18,6 @@ on:
|
|||||||
- "Joomla Build & Release"
|
- "Joomla Build & Release"
|
||||||
- "Joomla Extension CI"
|
- "Joomla Extension CI"
|
||||||
- "Deploy"
|
- "Deploy"
|
||||||
- "Cascade Main → Dev"
|
|
||||||
types:
|
types:
|
||||||
- completed
|
- completed
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.CI
|
# INGROUP: moko-platform.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
# PATH: /templates/workflows/universal/pr-check.yml.template
|
# PATH: /templates/workflows/universal/pr-check.yml.template
|
||||||
# VERSION: 05.00.00
|
# VERSION: 05.00.00
|
||||||
# BRIEF: PR gate — branch policy + code validation before merge
|
# BRIEF: PR gate — branch policy + code validation before merge
|
||||||
@@ -108,8 +108,9 @@ jobs:
|
|||||||
- name: Detect platform
|
- name: Detect platform
|
||||||
id: platform
|
id: platform
|
||||||
run: |
|
run: |
|
||||||
# Parse manifest for platform detection
|
# Read platform from XML manifest (<platform> tag) or plain text fallback
|
||||||
PLATFORM=$(php /tmp/mokostandards-api/cli/manifest_read.php --path . --field platform 2>/dev/null)
|
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:]')
|
||||||
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
||||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
@@ -194,35 +195,6 @@ jobs:
|
|||||||
echo "Source: ${FILE_COUNT} files"
|
echo "Source: ${FILE_COUNT} files"
|
||||||
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
|
[ "$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 RC Build ─────────────────────────────────────────────────
|
||||||
pre-release:
|
pre-release:
|
||||||
name: Build RC Package
|
name: Build RC Package
|
||||||
@@ -232,11 +204,11 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Trigger RC pre-release
|
- name: Trigger RC pre-release
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
BRANCH: ${{ github.head_ref }}
|
BRANCH: ${{ github.head_ref }}
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
run: |
|
run: |
|
||||||
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\"}}"
|
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
||||||
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|||||||
@@ -7,12 +7,16 @@
|
|||||||
# INGROUP: moko-platform.Release
|
# INGROUP: moko-platform.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||||
# VERSION: 05.00.00
|
# VERSION: 05.01.00
|
||||||
# BRIEF: Manual pre-release — builds dev/alpha/beta/rc packages from any branch
|
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
||||||
|
|
||||||
name: "Universal: Pre-Release"
|
name: "Universal: Pre-Release"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
stability:
|
stability:
|
||||||
@@ -35,40 +39,44 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: "Build Pre-Release (${{ inputs.stability }})"
|
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
|
||||||
runs-on: release
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
(github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.GA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
run: |
|
|
||||||
if ! command -v php &> /dev/null; then
|
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
- name: Setup moko-platform tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --branch main --quiet "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" /tmp/moko-platform-api
|
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
|
- name: Detect platform
|
||||||
id: platform
|
id: platform
|
||||||
run: |
|
run: |
|
||||||
php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output
|
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||||
|
|
||||||
- name: Resolve metadata
|
- name: Resolve metadata and bump version
|
||||||
id: meta
|
id: meta
|
||||||
run: |
|
run: |
|
||||||
STABILITY="${{ inputs.stability }}"
|
STABILITY="${{ inputs.stability || 'development' }}"
|
||||||
MOKO_API="/tmp/moko-platform-api/cli"
|
|
||||||
|
|
||||||
case "$STABILITY" in
|
case "$STABILITY" in
|
||||||
development) SUFFIX="-dev"; TAG="development" ;;
|
development) SUFFIX="-dev"; TAG="development" ;;
|
||||||
@@ -77,34 +85,44 @@ jobs:
|
|||||||
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Read current version from manifest (priority) or README — no bump yet
|
# Read current version (bump already handled by push workflow)
|
||||||
VERSION=$(php ${MOKO_API}/version_read.php --path .)
|
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
|
||||||
echo "Version: ${VERSION}"
|
[ -z "$VERSION" ] && VERSION="00.00.01"
|
||||||
|
|
||||||
# Ensure platform-specific manifest matches
|
# Strip any existing suffix from version before applying stability
|
||||||
php ${MOKO_API}/version_set_platform.php --path . --version "${VERSION}"
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
|
|
||||||
# Git setup for later commits
|
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.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
git config --local user.name "gitea-actions[bot]"
|
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 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
|
||||||
|
}
|
||||||
|
|
||||||
# Detect element from Joomla/Dolibarr manifest
|
# Auto-detect element via manifest_element.php
|
||||||
set +o pipefail
|
php ${MOKO_CLI}/manifest_element.php \
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
--path . --version "$VERSION" --stability "$STABILITY" \
|
||||||
EXT_ELEMENT=$(php ${MOKO_API}/manifest_read.php --path . --field name 2>/dev/null | tr -d ' ' | tr '[:upper:]' '[:lower:]' || true)
|
--repo "${GITEA_REPO}" --github-output
|
||||||
# For Joomla, prefer <element> tag
|
|
||||||
if [ "$PLATFORM" = "joomla" ]; then
|
# Read back element outputs
|
||||||
MANIFEST=$(find . -maxdepth 4 -name "*.xml" ! -path "./.git/*" -print0 2>/dev/null | xargs -0 grep -l '<extension' 2>/dev/null | head -1 || true)
|
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||||
if [ -n "$MANIFEST" ]; then
|
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||||
ELEM=$(grep -oP "<element>\K[^<]+" "$MANIFEST" 2>/dev/null | head -1 || true)
|
|
||||||
[ -n "$ELEM" ] && EXT_ELEMENT="$ELEM"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||||
set -o pipefail
|
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
|
||||||
|
|
||||||
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
|
|
||||||
|
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||||
@@ -115,146 +133,101 @@ jobs:
|
|||||||
|
|
||||||
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
||||||
|
|
||||||
- name: Build package
|
- name: Create release
|
||||||
id: zip
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
SUFFIX="${{ steps.meta.outputs.suffix }}"
|
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
|
||||||
|
|
||||||
if [ "$PLATFORM" = "joomla" ]; then
|
|
||||||
php /tmp/moko-platform-api/cli/joomla_build.php --path . --version "${VERSION}" --suffix "${SUFFIX}" --output build --github-output
|
|
||||||
else
|
|
||||||
# Generic build: zip src/ directory
|
|
||||||
SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && { echo "::error::No src/ or htdocs/"; exit 1; }
|
|
||||||
EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}"
|
|
||||||
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
|
|
||||||
mkdir -p build
|
|
||||||
cd "$SOURCE_DIR" && zip -r "../build/${ZIP_NAME}" . && cd ..
|
|
||||||
SHA256=$(sha256sum "build/${ZIP_NAME}" | cut -d' ' -f1)
|
|
||||||
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "zip_path=build/${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Create or replace Gitea release
|
|
||||||
id: release
|
id: release
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
run: |
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
SHA256="${{ steps.zip.outputs.sha256 }}"
|
php ${MOKO_CLI}/release_create.php \
|
||||||
ZIP_NAME="${{ steps.zip.outputs.zip_name }}"
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
TOKEN="${{ secrets.GA_TOKEN }}"
|
--repo "${GITEA_REPO}" --branch dev --prerelease
|
||||||
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
BRANCH=$(git branch --show-current)
|
|
||||||
|
|
||||||
BODY="## ${VERSION} ($(date +%Y-%m-%d))
|
- name: Build package and upload
|
||||||
**Channel:** ${STABILITY}
|
id: package
|
||||||
**SHA-256:** \`${SHA256}\`"
|
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
|
||||||
|
|
||||||
# Delete existing release
|
- name: Update updates.xml
|
||||||
EXISTING_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API}/releases/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null)
|
|
||||||
if [ -n "$EXISTING_ID" ]; then
|
|
||||||
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API}/releases/${EXISTING_ID}" 2>/dev/null || true
|
|
||||||
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API}/tags/${TAG}" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create release
|
|
||||||
RELEASE_ID=$(curl -sS -X POST -H "Authorization: token ${TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API}/releases" \
|
|
||||||
-d "$(jq -n \
|
|
||||||
--arg tag "$TAG" \
|
|
||||||
--arg target "$BRANCH" \
|
|
||||||
--arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \
|
|
||||||
--arg body "$BODY" \
|
|
||||||
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: true}'
|
|
||||||
)" | jq -r '.id')
|
|
||||||
|
|
||||||
echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# Upload ZIP
|
|
||||||
curl -sS -X POST -H "Authorization: token ${TOKEN}" \
|
|
||||||
-H "Content-Type: application/octet-stream" \
|
|
||||||
"${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
|
|
||||||
--data-binary "@${{ steps.zip.outputs.zip_path }}"
|
|
||||||
|
|
||||||
echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})"
|
|
||||||
|
|
||||||
- name: "Update updates.xml"
|
|
||||||
if: steps.platform.outputs.platform == 'joomla'
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
SHA256="${{ steps.zip.outputs.sha256 }}"
|
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||||
php /tmp/moko-platform-api/cli/updates_xml_build.php --path . --version "$VERSION" --stability "$STABILITY" --sha "$SHA256" --gitea-url "$GITEA_URL" --org "$GITEA_ORG" --repo "$GITEA_REPO"
|
|
||||||
|
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
|
if ! git diff --quiet updates.xml 2>/dev/null; then
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
git config --local user.name "gitea-actions[bot]"
|
git config --local user.name "gitea-actions[bot]"
|
||||||
git add updates.xml
|
git add updates.xml
|
||||||
git commit -m "chore: update $STABILITY channel $VERSION [skip ci]"
|
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
||||||
git push origin HEAD 2>&1 || echo "WARNING: push failed"
|
git push origin HEAD 2>&1 || echo "WARNING: push failed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: "Sync updates.xml to all branches"
|
- name: "Sync updates.xml to all branches"
|
||||||
if: steps.platform.outputs.platform == 'joomla'
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
run: |
|
run: |
|
||||||
php /tmp/moko-platform-api/cli/updates_xml_sync.php --path . --current "${{ github.ref_name }}" --branches main,dev --version "${{ steps.meta.outputs.version }}" --token "${{ secrets.GA_TOKEN }}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" --gitea-url "${GITEA_URL}"
|
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)"
|
- name: "Delete lesser pre-release channels (cascade)"
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
TOKEN="${{ secrets.GA_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
|
||||||
|
|
||||||
# Cascade: rc → beta,alpha,dev | beta → alpha,dev | alpha → dev | dev → nothing
|
php ${MOKO_CLI}/release_cascade.php \
|
||||||
case "$STABILITY" in
|
--stability "${{ steps.meta.outputs.stability }}" \
|
||||||
release-candidate) TAGS_TO_DELETE="beta alpha development" ;;
|
--token "${TOKEN}" \
|
||||||
beta) TAGS_TO_DELETE="alpha development" ;;
|
--api-base "${API_BASE}"
|
||||||
alpha) TAGS_TO_DELETE="development" ;;
|
|
||||||
*) TAGS_TO_DELETE="" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
[ -z "$TAGS_TO_DELETE" ] && exit 0
|
- name: Summary
|
||||||
|
if: always()
|
||||||
for TAG in $TAGS_TO_DELETE; do
|
|
||||||
RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \
|
|
||||||
python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then
|
|
||||||
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true
|
|
||||||
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API_BASE}/tags/${TAG}" 2>/dev/null || true
|
|
||||||
echo "Deleted: ${TAG} (id: ${RELEASE_ID})"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: "Post-release version bump"
|
|
||||||
run: |
|
run: |
|
||||||
MOKO_API="/tmp/moko-platform-api/cli"
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
# Bump patch for next dev cycle
|
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
||||||
BUMP_OUTPUT=$(php ${MOKO_API}/version_bump.php --path .)
|
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||||
NEXT=$(echo "$BUMP_OUTPUT" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true)
|
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
[ -z "$NEXT" ] && exit 0
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||||
# Update platform-specific manifest to next version
|
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||||
php ${MOKO_API}/version_set_platform.php --path . --version "${NEXT}"
|
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
|
||||||
git add -A
|
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
git diff --cached --quiet || {
|
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
git commit -m "chore: update development channel ${VERSION} [skip ci]"
|
|
||||||
git push origin HEAD 2>&1
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,18 +7,14 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Validation
|
# INGROUP: moko-platform.Validation
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
||||||
# VERSION: 04.06.00
|
# VERSION: 04.06.00
|
||||||
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
|
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
name: "Joomla: Repo Health"
|
name: "Generic: Repo Health"
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: repo-health-${{ github.repository }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -53,7 +49,7 @@ env:
|
|||||||
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
|
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
|
||||||
|
|
||||||
# Repo health policy
|
# Repo health policy
|
||||||
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.gitea/workflows/
|
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/
|
||||||
REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
|
REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
|
||||||
REPO_DISALLOWED_DIRS:
|
REPO_DISALLOWED_DIRS:
|
||||||
REPO_DISALLOWED_FILES: TODO.md,todo.md
|
REPO_DISALLOWED_FILES: TODO.md,todo.md
|
||||||
@@ -64,7 +60,7 @@ env:
|
|||||||
# File / directory variables
|
# File / directory variables
|
||||||
DOCS_INDEX: docs/docs-index.md
|
DOCS_INDEX: docs/docs-index.md
|
||||||
SCRIPT_DIR: scripts
|
SCRIPT_DIR: scripts
|
||||||
WORKFLOWS_DIR: .gitea/workflows
|
WORKFLOWS_DIR: .mokogitea/workflows
|
||||||
SHELLCHECK_PATTERN: '*.sh'
|
SHELLCHECK_PATTERN: '*.sh'
|
||||||
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
|
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
@@ -85,7 +81,7 @@ jobs:
|
|||||||
- name: Check actor permission (admin only)
|
- name: Check actor permission (admin only)
|
||||||
id: perm
|
id: perm
|
||||||
env:
|
env:
|
||||||
TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
|
||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
ACTOR: ${{ github.actor }}
|
ACTOR: ${{ github.actor }}
|
||||||
run: |
|
run: |
|
||||||
@@ -288,7 +284,7 @@ jobs:
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"
|
if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi
|
||||||
IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
|
IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
|
||||||
|
|
||||||
missing_dirs=()
|
missing_dirs=()
|
||||||
@@ -392,23 +388,27 @@ jobs:
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Source directory: src/ or htdocs/ (either is valid)
|
IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
|
||||||
|
IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
|
||||||
|
if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi
|
||||||
|
IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}"
|
||||||
|
|
||||||
|
missing_required=()
|
||||||
|
missing_optional=()
|
||||||
|
|
||||||
|
# Source directory: src/ or htdocs/ (either is valid for extension repos)
|
||||||
|
SOURCE_DIR=""
|
||||||
if [ -d "src" ]; then
|
if [ -d "src" ]; then
|
||||||
SOURCE_DIR="src"
|
SOURCE_DIR="src"
|
||||||
elif [ -d "htdocs" ]; then
|
elif [ -d "htdocs" ]; then
|
||||||
SOURCE_DIR="htdocs"
|
SOURCE_DIR="htdocs"
|
||||||
|
elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
|
||||||
|
# Platform/tooling repos don't need src/
|
||||||
|
SOURCE_DIR=""
|
||||||
else
|
else
|
||||||
missing_required+=("src/ or htdocs/ (source directory required)")
|
missing_required+=("src/ or htdocs/ (source directory required)")
|
||||||
fi
|
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
|
for item in "${required_artifacts[@]}"; do
|
||||||
if printf '%s' "${item}" | grep -q '/$'; then
|
if printf '%s' "${item}" | grep -q '/$'; then
|
||||||
d="${item%/}"
|
d="${item%/}"
|
||||||
@@ -450,12 +450,8 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
|
done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
|
||||||
|
|
||||||
if [ "${#dev_paths[@]}" -eq 0 ]; then
|
if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then
|
||||||
missing_required+=("dev/* branch (e.g. dev/01.00.00)")
|
missing_required+=("dev or dev/* branch")
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "${#dev_branches[@]}" -gt 0 ]; then
|
|
||||||
missing_required+=("invalid branch dev (must be dev/<version>)")
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
content_warnings=()
|
content_warnings=()
|
||||||
@@ -481,26 +477,7 @@ jobs:
|
|||||||
export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
|
export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
|
||||||
export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
|
export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
|
||||||
|
|
||||||
report_json="$(python3 - <<'PY'
|
report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}")
|
||||||
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'
|
printf '%s\n' '### Repository health'
|
||||||
@@ -578,12 +555,14 @@ jobs:
|
|||||||
joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
|
joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
|
if [ -n "${SOURCE_DIR}" ]; then
|
||||||
for dir in "${INDEX_DIRS[@]}"; do
|
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
|
||||||
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
|
for dir in "${INDEX_DIRS[@]}"; do
|
||||||
joomla_findings+=("${dir}/index.html missing (directory listing protection)")
|
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
|
||||||
fi
|
joomla_findings+=("${dir}/index.html missing (directory listing protection)")
|
||||||
done
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "${#joomla_findings[@]}" -gt 0 ]; then
|
if [ "${#joomla_findings[@]}" -gt 0 ]; then
|
||||||
{
|
{
|
||||||
@@ -629,43 +608,29 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f "${DOCS_INDEX}" ]; then
|
if [ -f "${DOCS_INDEX}" ]; then
|
||||||
missing_links="$(python3 - <<'PY'
|
missing_links=""
|
||||||
import os
|
while IFS= read -r docline; do
|
||||||
import re
|
for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do
|
||||||
|
case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac
|
||||||
idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md')
|
linkpath="${link%%#*}"
|
||||||
base = os.getcwd()
|
linkpath="${linkpath%%\?*}"
|
||||||
|
[ -z "$linkpath" ] && continue
|
||||||
bad = []
|
if [ "${linkpath:0:1}" = "/" ]; then
|
||||||
pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)')
|
testpath="${linkpath#/}"
|
||||||
|
else
|
||||||
with open(idx, 'r', encoding='utf-8') as f:
|
testpath="$(dirname "${DOCS_INDEX}")/${linkpath}"
|
||||||
for line in f:
|
fi
|
||||||
for m in pat.findall(line):
|
[ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} "
|
||||||
link = m.strip()
|
done
|
||||||
if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'):
|
done < "${DOCS_INDEX}"
|
||||||
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
|
if [ -n "${missing_links}" ]; then
|
||||||
extended_findings+=("docs/docs-index.md contains broken relative links")
|
extended_findings+=("docs/docs-index.md contains broken relative links")
|
||||||
{
|
{
|
||||||
printf '%s\n' '### Docs index link integrity'
|
printf '%s\n' '### Docs index link integrity'
|
||||||
printf '%s\n' 'Broken relative links:'
|
printf '%s\n' 'Broken relative links:'
|
||||||
while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}"
|
for bl in ${missing_links}; do
|
||||||
|
printf '%s\n' "- ${bl}"
|
||||||
|
done
|
||||||
printf '\n'
|
printf '\n'
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
fi
|
fi
|
||||||
@@ -764,3 +729,41 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Security
|
# INGROUP: moko-platform.Security
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /.gitea/workflows/security-audit.yml
|
# PATH: /.gitea/workflows/security-audit.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
||||||
@@ -80,3 +80,19 @@ jobs:
|
|||||||
-H "Priority: high" \
|
-H "Priority: high" \
|
||||||
-d "Security audit found vulnerabilities. Review dependency updates." \
|
-d "Security audit found vulnerabilities. Review dependency updates." \
|
||||||
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
"${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 }}
|
||||||
|
|
||||||
|
|||||||
@@ -4,20 +4,18 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Joomla
|
# INGROUP: moko-platform.Universal
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /templates/workflows/joomla/update-server.yml.template
|
# PATH: /templates/workflows/update-server.yml
|
||||||
# VERSION: 04.06.00
|
# VERSION: 05.00.00
|
||||||
# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries
|
# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
|
||||||
#
|
#
|
||||||
# Writes updates.xml with multiple <update> entries:
|
# Thin wrapper around moko-platform CLI tools.
|
||||||
# - <tag>stable</tag> on push to main (from auto-release)
|
# Builds packages, updates updates.xml, and optionally deploys via SFTP.
|
||||||
# - <tag>rc</tag> on push to rc/**
|
|
||||||
# - <tag>development</tag> on push to dev or dev/**
|
|
||||||
#
|
#
|
||||||
# Joomla filters by user's "Minimum Stability" setting.
|
# Joomla filters update entries by the user's "Minimum Stability" setting.
|
||||||
|
|
||||||
name: "Joomla: Update Server"
|
name: "Update Server"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -66,55 +64,60 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update-xml:
|
update-xml:
|
||||||
name: Update updates.xml
|
name: Update Server
|
||||||
runs-on: release
|
runs-on: release
|
||||||
if: >-
|
if: >-
|
||||||
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
|
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup MokoStandards tools
|
- name: Setup moko-platform tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}'
|
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
|
||||||
run: |
|
run: |
|
||||||
if ! command -v composer &> /dev/null; then
|
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
|
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
|
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 \
|
git clone --depth 1 --branch main --quiet \
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
/tmp/mokostandards-api 2>/dev/null || true
|
/tmp/moko-platform 2>/dev/null || true
|
||||||
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
|
if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
|
||||||
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Generate updates.xml entry
|
- name: Detect platform
|
||||||
id: update
|
id: platform
|
||||||
|
run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||||
|
|
||||||
|
- name: Resolve stability and bump version
|
||||||
|
id: meta
|
||||||
run: |
|
run: |
|
||||||
BRANCH="${{ github.ref_name }}"
|
BRANCH="${{ github.ref_name }}"
|
||||||
REPO="${{ github.repository }}"
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0")
|
|
||||||
|
|
||||||
# Auto-bump patch on all branches (dev, alpha, beta, rc)
|
# Configure git for bot pushes
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
git config --local user.name "gitea-actions[bot]"
|
git config --local user.name "gitea-actions[bot]"
|
||||||
BUMPED=$(php /tmp/mokostandards-api/cli/version_bump.php --path . 2>/dev/null || true)
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
if [ -n "$BUMPED" ]; then
|
|
||||||
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION")
|
|
||||||
git add -A
|
|
||||||
git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
|
|
||||||
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" 2>/dev/null || true
|
|
||||||
git push 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Determine stability from branch or input
|
# 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
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||||
STABILITY="${{ inputs.stability }}"
|
STABILITY="${{ inputs.stability }}"
|
||||||
elif [[ "$BRANCH" == rc/* ]]; then
|
elif [[ "$BRANCH" == rc/* ]]; then
|
||||||
@@ -123,277 +126,122 @@ jobs:
|
|||||||
STABILITY="beta"
|
STABILITY="beta"
|
||||||
elif [[ "$BRANCH" == alpha/* ]]; then
|
elif [[ "$BRANCH" == alpha/* ]]; then
|
||||||
STABILITY="alpha"
|
STABILITY="alpha"
|
||||||
elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then
|
else
|
||||||
STABILITY="development"
|
STABILITY="development"
|
||||||
else
|
|
||||||
STABILITY="stable"
|
|
||||||
fi
|
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 "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
# Parse manifest (portable — no grep -P)
|
# Commit version bump if changed
|
||||||
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
git add -A
|
||||||
if [ -z "$MANIFEST" ]; then
|
|
||||||
echo "No Joomla manifest found — skipping"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract fields using sed (works on all runners)
|
|
||||||
EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_CLIENT=$(sed -n 's/.*<extension[^>]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_VERSION=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
|
|
||||||
# Fallbacks
|
|
||||||
[ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
|
|
||||||
[ -z "$EXT_TYPE" ] && EXT_TYPE="component"
|
|
||||||
|
|
||||||
# Derive element if not in manifest: try XML filename, then repo name
|
|
||||||
if [ -z "$EXT_ELEMENT" ]; then
|
|
||||||
EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
|
|
||||||
case "$EXT_ELEMENT" in
|
|
||||||
templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use manifest version if README version is empty
|
|
||||||
[ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION"
|
|
||||||
|
|
||||||
[ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" %s>' "/")
|
|
||||||
|
|
||||||
CLIENT_TAG=""
|
|
||||||
[ -n "$EXT_CLIENT" ] && CLIENT_TAG="<client>${EXT_CLIENT}</client>"
|
|
||||||
[ -z "$CLIENT_TAG" ] && ([ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]) && CLIENT_TAG="<client>site</client>"
|
|
||||||
|
|
||||||
FOLDER_TAG=""
|
|
||||||
[ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
|
|
||||||
|
|
||||||
PHP_TAG=""
|
|
||||||
[ -n "$PHP_MINIMUM" ] && PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>"
|
|
||||||
|
|
||||||
# Version suffix for non-stable
|
|
||||||
DISPLAY_VERSION="$VERSION"
|
|
||||||
case "$STABILITY" in
|
|
||||||
development) DISPLAY_VERSION="${VERSION}-dev" ;;
|
|
||||||
alpha) DISPLAY_VERSION="${VERSION}-alpha" ;;
|
|
||||||
beta) DISPLAY_VERSION="${VERSION}-beta" ;;
|
|
||||||
rc) DISPLAY_VERSION="${VERSION}-rc" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
|
|
||||||
|
|
||||||
# Each stability level has its own release tag
|
|
||||||
case "$STABILITY" in
|
|
||||||
development) RELEASE_TAG="development" ;;
|
|
||||||
alpha) RELEASE_TAG="alpha" ;;
|
|
||||||
beta) RELEASE_TAG="beta" ;;
|
|
||||||
rc) RELEASE_TAG="release-candidate" ;;
|
|
||||||
*) RELEASE_TAG="v${MAJOR}" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip"
|
|
||||||
DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}"
|
|
||||||
INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
|
|
||||||
# -- Build install packages (ZIP + tar.gz) --------------------
|
|
||||||
SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
||||||
if [ -d "$SOURCE_DIR" ]; then
|
|
||||||
EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
|
|
||||||
TAR_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz"
|
|
||||||
|
|
||||||
cd "$SOURCE_DIR"
|
|
||||||
zip -r "/tmp/${PACKAGE_NAME}" . -x $EXCLUDES
|
|
||||||
cd ..
|
|
||||||
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \
|
|
||||||
--exclude='.ftpignore' --exclude='sftp-config*' \
|
|
||||||
--exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
|
|
||||||
|
|
||||||
SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1)
|
|
||||||
|
|
||||||
# Ensure release exists on Gitea
|
|
||||||
RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
|
|
||||||
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -z "$RELEASE_ID" ]; then
|
|
||||||
# Create release
|
|
||||||
RELEASE_JSON=$(curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API_BASE}/releases" \
|
|
||||||
-d "$(python3 -c "import json; print(json.dumps({
|
|
||||||
'tag_name': '${RELEASE_TAG}',
|
|
||||||
'name': '${RELEASE_TAG} (${DISPLAY_VERSION})',
|
|
||||||
'body': '${STABILITY} release',
|
|
||||||
'prerelease': True,
|
|
||||||
'target_commitish': 'main'
|
|
||||||
}))")" 2>/dev/null || true)
|
|
||||||
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$RELEASE_ID" ]; then
|
|
||||||
# Delete existing assets with same name before uploading
|
|
||||||
ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]")
|
|
||||||
for ASSET_FILE in "$PACKAGE_NAME" "$TAR_NAME"; do
|
|
||||||
ASSET_ID=$(echo "$ASSETS" | python3 -c "
|
|
||||||
import sys,json
|
|
||||||
assets = json.load(sys.stdin)
|
|
||||||
for a in assets:
|
|
||||||
if a['name'] == '${ASSET_FILE}':
|
|
||||||
print(a['id']); break
|
|
||||||
" 2>/dev/null || true)
|
|
||||||
if [ -n "$ASSET_ID" ]; then
|
|
||||||
curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Upload both formats
|
|
||||||
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
-H "Content-Type: application/octet-stream" \
|
|
||||||
--data-binary @"/tmp/${PACKAGE_NAME}" \
|
|
||||||
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${PACKAGE_NAME}" > /dev/null 2>&1 || true
|
|
||||||
|
|
||||||
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
-H "Content-Type: application/octet-stream" \
|
|
||||||
--data-binary @"/tmp/${TAR_NAME}" \
|
|
||||||
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
SHA256=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# -- Build the new entry (canonical format matching release.yml) --
|
|
||||||
NEW_ENTRY=""
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <update>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <name>${EXT_NAME}</name>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <description>${EXT_NAME} ${STABILITY} build.</description>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <element>${EXT_ELEMENT}</element>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <type>${EXT_TYPE}</type>\n"
|
|
||||||
[ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n"
|
|
||||||
[ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <version>${VERSION}</version>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <creationDate>$(date +%Y-%m-%d)</creationDate>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <infourl title='${EXT_NAME}'>https://git.mokoconsulting.tech/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${RELEASE_TAG}</infourl>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <downloads>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <downloadurl type='full' format='zip'>${DOWNLOAD_URL}</downloadurl>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} </downloads>\n"
|
|
||||||
[ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} <sha256>${SHA256}</sha256>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <tags><tag>${STABILITY}</tag></tags>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <maintainer>Moko Consulting</maintainer>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <maintainerurl>https://mokoconsulting.tech</maintainerurl>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <targetplatform name='joomla' version='(5|6).*'/>\n"
|
|
||||||
[ -n "$PHP_MINIMUM" ] && NEW_ENTRY="${NEW_ENTRY} <php_minimum>${PHP_MINIMUM}</php_minimum>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} </update>"
|
|
||||||
|
|
||||||
# -- Write new entry to temp file --------------------------------
|
|
||||||
printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml
|
|
||||||
|
|
||||||
# -- Merge into updates.xml ----------------------------------------
|
|
||||||
# Cascade: stable→all | rc→rc+lower | beta→beta+lower | alpha→alpha+dev | dev→dev
|
|
||||||
CASCADE_MAP="stable:development,alpha,beta,rc,stable rc:development,alpha,beta,rc beta:development,alpha,beta alpha:development,alpha development:development"
|
|
||||||
TARGETS=""
|
|
||||||
for entry in $CASCADE_MAP; do
|
|
||||||
key="${entry%%:*}"
|
|
||||||
vals="${entry#*:}"
|
|
||||||
if [ "$key" = "${STABILITY}" ]; then
|
|
||||||
TARGETS="$vals"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
[ -z "$TARGETS" ] && TARGETS="${STABILITY}"
|
|
||||||
|
|
||||||
echo "Cascade: ${STABILITY} → ${TARGETS}"
|
|
||||||
|
|
||||||
# Create updates.xml if missing
|
|
||||||
if [ ! -f "updates.xml" ]; then
|
|
||||||
printf '%s\n' "<?xml version='1.0' encoding='UTF-8'?>" > updates.xml
|
|
||||||
printf '%s\n' "<!-- Copyright (C) $(date +%Y) Moko Consulting -->" >> updates.xml
|
|
||||||
printf '%s\n' "<updates>" >> updates.xml
|
|
||||||
printf '%s\n' "</updates>" >> updates.xml
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update existing blocks or create missing ones
|
|
||||||
export PY_TARGETS="$TARGETS" PY_VERSION="$VERSION" PY_DATE="$(date +%Y-%m-%d)"
|
|
||||||
python3 << 'PYEOF'
|
|
||||||
import re, os
|
|
||||||
|
|
||||||
targets = os.environ["PY_TARGETS"].split(",")
|
|
||||||
version = os.environ["PY_VERSION"]
|
|
||||||
date = os.environ["PY_DATE"]
|
|
||||||
|
|
||||||
with open("updates.xml") as f:
|
|
||||||
content = f.read()
|
|
||||||
with open("/tmp/new_entry.xml") as f:
|
|
||||||
new_entry_template = f.read()
|
|
||||||
|
|
||||||
for tag in targets:
|
|
||||||
tag = tag.strip()
|
|
||||||
# Build entry with this tag's name
|
|
||||||
new_entry = re.sub(r"<tag>[^<]*</tag>", f"<tag>{tag}</tag>", new_entry_template)
|
|
||||||
|
|
||||||
# Try to find existing block (handles both single-line and multi-line <tags>)
|
|
||||||
block_pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(tag) + r"</tag>.*?</update>)"
|
|
||||||
match = re.search(block_pattern, content, re.DOTALL)
|
|
||||||
|
|
||||||
if match:
|
|
||||||
# Update in place — replace entire block
|
|
||||||
content = content.replace(match.group(1), new_entry.strip())
|
|
||||||
print(f" UPDATED: <tag>{tag}</tag> → {version}")
|
|
||||||
else:
|
|
||||||
# Create — insert before </updates>
|
|
||||||
content = content.replace("</updates>", "\n" + new_entry.strip() + "\n\n</updates>")
|
|
||||||
print(f" CREATED: <tag>{tag}</tag> → {version}")
|
|
||||||
|
|
||||||
# Clean up excessive blank lines
|
|
||||||
content = re.sub(r"\n{3,}", "\n\n", content)
|
|
||||||
|
|
||||||
with open("updates.xml", "w") as f:
|
|
||||||
f.write(content)
|
|
||||||
PYEOF
|
|
||||||
|
|
||||||
# Commit
|
|
||||||
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 diff --cached --quiet || {
|
||||||
git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \
|
git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
|
||||||
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
||||||
git push
|
git push
|
||||||
}
|
}
|
||||||
|
|
||||||
# -- Sync updates.xml to main (for non-main branches) ----------------------
|
- 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
|
- name: Sync updates.xml to main
|
||||||
if: github.ref_name != 'main'
|
if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
GA_TOKEN="${{ secrets.GA_TOKEN }}"
|
GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_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)
|
"${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
|
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
|
||||||
CONTENT=$(base64 -w0 updates.xml)
|
python3 -c "
|
||||||
curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \
|
import base64, json, urllib.request, sys
|
||||||
-H "Content-Type: application/json" \
|
with open('updates.xml', 'rb') as f:
|
||||||
"${API_BASE}/contents/updates.xml" \
|
content = base64.b64encode(f.read()).decode()
|
||||||
-d "$(python3 -c "import json; print(json.dumps({
|
payload = json.dumps({
|
||||||
'content': '${CONTENT}',
|
'content': content,
|
||||||
'sha': '${FILE_SHA}',
|
'sha': '${FILE_SHA}',
|
||||||
'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]',
|
'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
|
||||||
'branch': 'main'
|
'branch': 'main'
|
||||||
}))")" > /dev/null 2>&1 \
|
}).encode()
|
||||||
&& echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \
|
req = urllib.request.Request(
|
||||||
|| echo "WARNING: failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY
|
'${API_BASE}/contents/updates.xml',
|
||||||
else
|
data=payload, method='PUT',
|
||||||
echo "WARNING: could not get updates.xml SHA from main" >> $GITHUB_STEP_SUMMARY
|
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
|
fi
|
||||||
|
|
||||||
- name: SFTP deploy to dev server
|
- name: SFTP deploy to dev server
|
||||||
@@ -407,12 +255,11 @@ jobs:
|
|||||||
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
|
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
|
||||||
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
||||||
run: |
|
run: |
|
||||||
# -- Permission check: admin or maintain role required --------
|
# Permission check: admin or maintain role required
|
||||||
ACTOR="${{ github.actor }}"
|
ACTOR="${{ github.actor }}"
|
||||||
REPO="${{ github.repository }}"
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
|
"${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")
|
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
|
||||||
case "$PERMISSION" in
|
case "$PERMISSION" in
|
||||||
@@ -442,11 +289,11 @@ jobs:
|
|||||||
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
|
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
|
PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
|
||||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
|
if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
|
||||||
php /tmp/mokostandards-api/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||||
elif [ -f "/tmp/mokostandards-api/deploy/deploy-sftp.php" ]; then
|
elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
|
||||||
php /tmp/mokostandards-api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||||
fi
|
fi
|
||||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||||
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
|
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -454,11 +301,12 @@ jobs:
|
|||||||
- name: Summary
|
- name: Summary
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY
|
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 "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
|
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|||||||
@@ -27,10 +27,71 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Migrated all workflow and template paths from `.github/` to `.mokogitea/`
|
||||||
|
- Template source paths updated: `templates/gitea/` to `templates/mokogitea/`
|
||||||
|
- HCL definition files removed -- Template repos are now the canonical source
|
||||||
|
|
||||||
|
### 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
|
### Planned
|
||||||
- License/subscription check
|
- License/subscription check
|
||||||
- System email template branding (DB approach)
|
- System email template branding (DB approach)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Trusted IPs: configurable repeatable rows of IP addresses, CIDR ranges, and wildcards that bypass admin session timeout
|
||||||
|
- Supports exact IPs (192.168.1.100), CIDR (10.0.0.0/24), and wildcards (192.168.1.*)
|
||||||
|
- Each entry has a label and enabled toggle for easy management
|
||||||
|
- Current IP display above trusted IPs table so admins can easily add their own IP
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Trusted IP session bypass: moved from `onAfterInitialise` to `boot()` so Joomla's session lifetime is extended before the session handler validates it (was too late, Joomla expired the session first)
|
||||||
|
- 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
|
## [02.03.10] - 2026-05-24
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -368,3 +429,4 @@ When adding entries to this changelog:
|
|||||||
---
|
---
|
||||||
|
|
||||||
**Note:** For detailed technical documentation, see the `/docs/` directory and [README.md](README.md).
|
**Note:** For detailed technical documentation, see the `/docs/` directory and [README.md](README.md).
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
DEFGROUP: Joomla.Plugin
|
DEFGROUP: Joomla.Plugin
|
||||||
INGROUP: MokoWaaS
|
INGROUP: MokoWaaS
|
||||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
|
||||||
VERSION: 02.04.00
|
VERSION: 02.16.03-dev
|
||||||
PATH: /README.md
|
PATH: /README.md
|
||||||
BRIEF: MokoWaaS platform plugin for Joomla
|
BRIEF: MokoWaaS platform plugin for Joomla
|
||||||
-->
|
-->
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<license>GPL-3.0-or-later</license>
|
<license>GPL-3.0-or-later</license>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
<version>02.04.00</version>
|
<version>02.16.03-dev</version>
|
||||||
<description>Minimal API-only component for MokoWaaS. Provides REST endpoints for site health, cache, updates, and backups.</description>
|
<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>
|
<namespace path="api/src">Moko\Component\MokoWaaS\Api</namespace>
|
||||||
<administration>
|
<administration>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ namespace Moko\Plugin\System\MokoWaaS\Extension;
|
|||||||
|
|
||||||
defined('_JEXEC') or die;
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Extension\BootableExtensionInterface;
|
||||||
use Joomla\CMS\Factory;
|
use Joomla\CMS\Factory;
|
||||||
use Joomla\CMS\Log\Log;
|
use Joomla\CMS\Log\Log;
|
||||||
use Joomla\CMS\Plugin\CMSPlugin;
|
use Joomla\CMS\Plugin\CMSPlugin;
|
||||||
@@ -38,6 +39,7 @@ use Joomla\CMS\Router\Route;
|
|||||||
use Joomla\CMS\Language\Language;
|
use Joomla\CMS\Language\Language;
|
||||||
use Joomla\CMS\Uri\Uri;
|
use Joomla\CMS\Uri\Uri;
|
||||||
use Joomla\CMS\User\UserHelper;
|
use Joomla\CMS\User\UserHelper;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MokoWaaS Brand System Plugin
|
* MokoWaaS Brand System Plugin
|
||||||
@@ -47,7 +49,7 @@ use Joomla\CMS\User\UserHelper;
|
|||||||
*
|
*
|
||||||
* @since 01.04.00
|
* @since 01.04.00
|
||||||
*/
|
*/
|
||||||
class MokoWaaS extends CMSPlugin
|
class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Obfuscated Grafana URL (XOR + base64).
|
* Obfuscated Grafana URL (XOR + base64).
|
||||||
@@ -114,6 +116,37 @@ class MokoWaaS extends CMSPlugin
|
|||||||
*/
|
*/
|
||||||
protected $app;
|
protected $app;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boot the extension — runs BEFORE Joomla creates the session.
|
||||||
|
*
|
||||||
|
* Extends the Joomla session lifetime for trusted IPs so the
|
||||||
|
* session handler does not destroy the session before
|
||||||
|
* onAfterInitialise can run.
|
||||||
|
*
|
||||||
|
* @param ContainerInterface $container The DI container.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @since 02.11.00
|
||||||
|
*/
|
||||||
|
public function boot(ContainerInterface $container): void
|
||||||
|
{
|
||||||
|
$timeout = (int) $this->params->get('admin_session_timeout', 0);
|
||||||
|
|
||||||
|
if ($timeout <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->ipIsTrusted())
|
||||||
|
{
|
||||||
|
// Set both PHP and Joomla session lifetimes before the
|
||||||
|
// session handler runs its expiry check.
|
||||||
|
ini_set('session.gc_maxlifetime', 315360000);
|
||||||
|
Factory::getConfig()->set('lifetime', 525600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event triggered after the framework has loaded and the application initialise method has been called.
|
* Event triggered after the framework has loaded and the application initialise method has been called.
|
||||||
*
|
*
|
||||||
@@ -129,6 +162,11 @@ class MokoWaaS extends CMSPlugin
|
|||||||
// Security: HTTPS redirect (runs for all clients)
|
// Security: HTTPS redirect (runs for all clients)
|
||||||
$this->enforceHttps();
|
$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)
|
// MokoWaaS API endpoints (run before routing)
|
||||||
$mokoAction = $this->app->input->get('mokowaas', '');
|
$mokoAction = $this->app->input->get('mokowaas', '');
|
||||||
|
|
||||||
@@ -653,7 +691,7 @@ class MokoWaaS extends CMSPlugin
|
|||||||
return [
|
return [
|
||||||
'{{BRAND_NAME}}' => $this->params->get('brand_name', 'MokoWaaS'),
|
'{{BRAND_NAME}}' => $this->params->get('brand_name', 'MokoWaaS'),
|
||||||
'{{COMPANY_NAME}}' => $this->params->get('company_name', 'Moko Consulting'),
|
'{{COMPANY_NAME}}' => $this->params->get('company_name', 'Moko Consulting'),
|
||||||
'{{SUPPORT_URL}}' => $this->params->get('support_url', 'https://mokoconsulting.tech'),
|
'{{SUPPORT_URL}}' => $this->params->get('support_url', 'https://mokoconsulting.tech/support'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -893,9 +931,6 @@ class MokoWaaS extends CMSPlugin
|
|||||||
*/
|
*/
|
||||||
public function onAfterRoute()
|
public function onAfterRoute()
|
||||||
{
|
{
|
||||||
// Site alias handling: offline page and backend redirect
|
|
||||||
$this->handleSiteAlias();
|
|
||||||
|
|
||||||
if (!$this->app->isClient('administrator'))
|
if (!$this->app->isClient('administrator'))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -936,6 +971,7 @@ class MokoWaaS extends CMSPlugin
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->injectFavicon($doc);
|
$this->injectFavicon($doc);
|
||||||
|
$this->redirectHelpMenu($doc);
|
||||||
|
|
||||||
// Hide MokoWaaS from plugin list for non-master users
|
// Hide MokoWaaS from plugin list for non-master users
|
||||||
if (!$this->isMasterUser())
|
if (!$this->isMasterUser())
|
||||||
@@ -975,6 +1011,32 @@ class MokoWaaS extends CMSPlugin
|
|||||||
");
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect the admin Help menu link to the configured support URL.
|
||||||
|
*
|
||||||
|
* Joomla's Atum template hardcodes the Help link to help.joomla.org.
|
||||||
|
* This replaces it with the WaaS support URL via JS injection.
|
||||||
|
*
|
||||||
|
* @param \Joomla\CMS\Document\HtmlDocument $doc Document object
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @since 02.10.00
|
||||||
|
*/
|
||||||
|
protected function redirectHelpMenu($doc)
|
||||||
|
{
|
||||||
|
$supportUrl = $this->params->get('support_url', 'https://mokoconsulting.tech/support');
|
||||||
|
|
||||||
|
$doc->addScriptDeclaration("
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
document.querySelectorAll('a[href*=\"help.joomla.org\"], a[href*=\"docs.joomla.org\"]').forEach(function(link) {
|
||||||
|
link.href = " . json_encode($supportUrl) . ";
|
||||||
|
link.target = '_blank';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Protect the plugin from being disabled or uninstalled by non-master users.
|
* Protect the plugin from being disabled or uninstalled by non-master users.
|
||||||
* Does NOT self-heal (no lock) — master users can still disable if needed.
|
* Does NOT self-heal (no lock) — master users can still disable if needed.
|
||||||
@@ -1025,6 +1087,31 @@ class MokoWaaS extends CMSPlugin
|
|||||||
$this->app->redirect('index.php?option=com_plugins');
|
$this->app->redirect('index.php?option=com_plugins');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Block non-master from viewing or editing MokoWaaS plugin settings
|
||||||
|
if ($option === 'com_plugins')
|
||||||
|
{
|
||||||
|
$view = $this->app->input->get('view', '');
|
||||||
|
$layout = $this->app->input->get('layout', '');
|
||||||
|
$extensionId = (int) $this->app->input->get('extension_id', 0);
|
||||||
|
|
||||||
|
if (($view === 'plugin' || $layout === 'edit') && $extensionId > 0)
|
||||||
|
{
|
||||||
|
$db = Factory::getDbo();
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select('COUNT(*)')
|
||||||
|
->from($db->quoteName('#__extensions'))
|
||||||
|
->where($db->quoteName('extension_id') . ' = ' . $extensionId)
|
||||||
|
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas'))
|
||||||
|
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'));
|
||||||
|
|
||||||
|
if ((int) $db->setQuery($query)->loadResult() > 0)
|
||||||
|
{
|
||||||
|
$this->app->enqueueMessage('MokoWaaS settings are restricted to the master user.', 'warning');
|
||||||
|
$this->app->redirect('index.php?option=com_plugins');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1042,6 +1129,8 @@ class MokoWaaS extends CMSPlugin
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
$db = Factory::getDbo();
|
$db = Factory::getDbo();
|
||||||
|
|
||||||
|
// Set protected=1, locked=0 on MokoWaaS extensions
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
->update($db->quoteName('#__extensions'))
|
->update($db->quoteName('#__extensions'))
|
||||||
->set($db->quoteName('protected') . ' = 1')
|
->set($db->quoteName('protected') . ' = 1')
|
||||||
@@ -1051,6 +1140,18 @@ class MokoWaaS extends CMSPlugin
|
|||||||
->where($db->quoteName('protected') . ' = 0');
|
->where($db->quoteName('protected') . ' = 0');
|
||||||
$db->setQuery($query);
|
$db->setQuery($query);
|
||||||
$db->execute();
|
$db->execute();
|
||||||
|
|
||||||
|
// Ensure update site stays enabled (protected extensions get their update site disabled by Joomla)
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->update($db->quoteName('#__update_sites') . ' AS us')
|
||||||
|
->join('INNER', $db->quoteName('#__update_sites_extensions') . ' AS use2 ON us.update_site_id = use2.update_site_id')
|
||||||
|
->join('INNER', $db->quoteName('#__extensions') . ' AS e ON use2.extension_id = e.extension_id')
|
||||||
|
->set('us.enabled = 1')
|
||||||
|
->where('us.enabled = 0')
|
||||||
|
->where('(' . $db->quoteName('e.element') . ' = ' . $db->quote('mokowaas')
|
||||||
|
. ' OR ' . $db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokowaas') . ')');
|
||||||
|
$db->setQuery($query);
|
||||||
|
$db->execute();
|
||||||
}
|
}
|
||||||
catch (\Throwable $e)
|
catch (\Throwable $e)
|
||||||
{
|
{
|
||||||
@@ -2747,11 +2848,36 @@ class MokoWaaS extends CMSPlugin
|
|||||||
|
|
||||||
file_put_contents($tmpFile, $zipData);
|
file_put_contents($tmpFile, $zipData);
|
||||||
|
|
||||||
|
// Extract ZIP to temp directory
|
||||||
|
$extractDir = $this->app->getConfig()->get('tmp_path', JPATH_ROOT . '/tmp')
|
||||||
|
. '/mokowaas_extract_' . md5($url);
|
||||||
|
|
||||||
|
if (is_dir($extractDir))
|
||||||
|
{
|
||||||
|
$this->rmdirRecursive($extractDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir($extractDir, 0755, true);
|
||||||
|
|
||||||
|
$zip = new \ZipArchive();
|
||||||
|
|
||||||
|
if ($zip->open($tmpFile) !== true)
|
||||||
|
{
|
||||||
|
@unlink($tmpFile);
|
||||||
|
$this->sendHealthResponse(500, ['error' => 'Failed to open ZIP']);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip->extractTo($extractDir);
|
||||||
|
$zip->close();
|
||||||
|
@unlink($tmpFile);
|
||||||
|
|
||||||
// Install using Joomla's installer
|
// Install using Joomla's installer
|
||||||
$installer = \Joomla\CMS\Installer\Installer::getInstance();
|
$installer = \Joomla\CMS\Installer\Installer::getInstance();
|
||||||
$result = $installer->install($tmpFile);
|
$result = $installer->install($extractDir);
|
||||||
|
|
||||||
@unlink($tmpFile);
|
$this->rmdirRecursive($extractDir);
|
||||||
|
|
||||||
if ($result)
|
if ($result)
|
||||||
{
|
{
|
||||||
@@ -2774,6 +2900,11 @@ class MokoWaaS extends CMSPlugin
|
|||||||
{
|
{
|
||||||
@unlink($tmpFile ?? '');
|
@unlink($tmpFile ?? '');
|
||||||
|
|
||||||
|
if (!empty($extractDir) && is_dir($extractDir))
|
||||||
|
{
|
||||||
|
$this->rmdirRecursive($extractDir);
|
||||||
|
}
|
||||||
|
|
||||||
$this->sendHealthResponse(500, [
|
$this->sendHealthResponse(500, [
|
||||||
'error' => 'Install exception',
|
'error' => 'Install exception',
|
||||||
'message' => $e->getMessage(),
|
'message' => $e->getMessage(),
|
||||||
@@ -2782,6 +2913,42 @@ class MokoWaaS extends CMSPlugin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively remove a directory.
|
||||||
|
*
|
||||||
|
* @param string $dir Directory path
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @since 02.06.00
|
||||||
|
*/
|
||||||
|
protected function rmdirRecursive(string $dir): void
|
||||||
|
{
|
||||||
|
if (!is_dir($dir))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = new \RecursiveIteratorIterator(
|
||||||
|
new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
|
||||||
|
\RecursiveIteratorIterator::CHILD_FIRST
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($items as $item)
|
||||||
|
{
|
||||||
|
if ($item->isDir())
|
||||||
|
{
|
||||||
|
rmdir($item->getPathname());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
unlink($item->getPathname());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rmdir($dir);
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Site Alias handling
|
// Site Alias handling
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
@@ -2916,7 +3083,8 @@ class MokoWaaS extends CMSPlugin
|
|||||||
/**
|
/**
|
||||||
* Handle site alias logic: offline page and backend redirect.
|
* Handle site alias logic: offline page and backend redirect.
|
||||||
*
|
*
|
||||||
* Runs early in onAfterInitialise before routing occurs.
|
* Runs in onAfterInitialise so that Joomla's offline check in
|
||||||
|
* SiteApplication::doExecute() sees the updated config value.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*
|
*
|
||||||
@@ -2944,25 +3112,33 @@ class MokoWaaS extends CMSPlugin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Offline: use Joomla's native offline mode for frontend requests
|
// Offline: use Joomla's native offline mode for frontend requests
|
||||||
if (!empty($alias->offline) && (string) $alias->offline === '1'
|
if ($this->app->isClient('site'))
|
||||||
&& $this->app->isClient('site'))
|
|
||||||
{
|
{
|
||||||
// Allow health API to still respond
|
if (!empty($alias->offline) && (string) $alias->offline === '1')
|
||||||
if ($this->app->input->get('mokowaas', '') !== '')
|
|
||||||
{
|
{
|
||||||
return;
|
// Allow health API to still respond
|
||||||
|
if ($this->app->input->get('mokowaas', '') !== '')
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set custom offline message if provided
|
||||||
|
$message = $alias->offline_message ?? '';
|
||||||
|
|
||||||
|
if (!empty($message))
|
||||||
|
{
|
||||||
|
$this->app->getConfig()->set('offline_message', $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable Joomla's native offline mode
|
||||||
|
$this->app->getConfig()->set('offline', 1);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
// Set custom offline message if provided
|
|
||||||
$message = $alias->offline_message ?? '';
|
|
||||||
|
|
||||||
if (!empty($message))
|
|
||||||
{
|
{
|
||||||
$this->app->getConfig()->set('offline_message', $message);
|
// Alias is NOT offline — override Joomla's global offline setting
|
||||||
|
// This allows access via the alias domain even when the main site is offline
|
||||||
|
$this->app->getConfig()->set('offline', 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable Joomla's native offline mode — renders through the template's offline.php
|
|
||||||
$this->app->getConfig()->set('offline', 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3203,6 +3379,12 @@ class MokoWaaS extends CMSPlugin
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trusted IPs — session lifetime already extended in boot()
|
||||||
|
if ($this->ipIsTrusted())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$session = Factory::getSession();
|
$session = Factory::getSession();
|
||||||
$lastHit = $session->get('mokowaas.last_activity', 0);
|
$lastHit = $session->get('mokowaas.last_activity', 0);
|
||||||
$now = time();
|
$now = time();
|
||||||
@@ -3220,6 +3402,93 @@ class MokoWaaS extends CMSPlugin
|
|||||||
$session->set('mokowaas.last_activity', $now);
|
$session->set('mokowaas.last_activity', $now);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the current request IP matches any trusted IP entry.
|
||||||
|
*
|
||||||
|
* Supports exact IPs, CIDR notation (e.g. 10.0.0.0/8), and
|
||||||
|
* wildcard patterns (e.g. 192.168.1.*).
|
||||||
|
*
|
||||||
|
* @return bool True if the current IP is in the trusted list.
|
||||||
|
*
|
||||||
|
* @since 02.11.00
|
||||||
|
*/
|
||||||
|
protected function ipIsTrusted(): bool
|
||||||
|
{
|
||||||
|
$entries = $this->params->get('trusted_ips', '');
|
||||||
|
|
||||||
|
if (empty($entries))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subform stores as JSON string or array
|
||||||
|
if (\is_string($entries))
|
||||||
|
{
|
||||||
|
$entries = json_decode($entries, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\is_array($entries))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ip = $this->app
|
||||||
|
? $this->app->input->server->getString('REMOTE_ADDR', '')
|
||||||
|
: ($_SERVER['REMOTE_ADDR'] ?? '');
|
||||||
|
$ipLong = ip2long($ip);
|
||||||
|
|
||||||
|
if ($ipLong === false)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($entries as $entry)
|
||||||
|
{
|
||||||
|
if (empty($entry['enabled']) || empty($entry['ip']))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$range = trim($entry['ip']);
|
||||||
|
|
||||||
|
// Wildcard: 192.168.1.*
|
||||||
|
if (str_contains($range, '*'))
|
||||||
|
{
|
||||||
|
$pattern = '/^' . str_replace(['.', '*'], ['\\.', '\\d+'], $range) . '$/';
|
||||||
|
|
||||||
|
if (preg_match($pattern, $ip))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CIDR: 10.0.0.0/8
|
||||||
|
if (str_contains($range, '/'))
|
||||||
|
{
|
||||||
|
[$subnet, $bits] = explode('/', $range, 2);
|
||||||
|
$subnetLong = ip2long($subnet);
|
||||||
|
$mask = -1 << (32 - (int) $bits);
|
||||||
|
|
||||||
|
if ($subnetLong !== false && ($ipLong & $mask) === ($subnetLong & $mask))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exact match
|
||||||
|
if ($ip === $range)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override Joomla upload restrictions at runtime.
|
* Override Joomla upload restrictions at runtime.
|
||||||
@@ -3328,12 +3597,18 @@ class MokoWaaS extends CMSPlugin
|
|||||||
*/
|
*/
|
||||||
protected function enforceAdminRestrictions()
|
protected function enforceAdminRestrictions()
|
||||||
{
|
{
|
||||||
|
// Master user bypasses ALL restrictions
|
||||||
|
if ($this->isMasterUser())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$input = $this->app->input;
|
$input = $this->app->input;
|
||||||
$option = $input->get('option', '');
|
$option = $input->get('option', '');
|
||||||
$view = $input->get('view', '');
|
$view = $input->get('view', '');
|
||||||
$task = $input->get('task', '');
|
$task = $input->get('task', '');
|
||||||
|
|
||||||
// Disable install-from-URL for ALL users (safety net)
|
// Disable install-from-URL for non-master users
|
||||||
if ($this->params->get('disable_install_url', 1)
|
if ($this->params->get('disable_install_url', 1)
|
||||||
&& $option === 'com_installer'
|
&& $option === 'com_installer'
|
||||||
&& stripos($task, 'install') !== false
|
&& stripos($task, 'install') !== false
|
||||||
@@ -3344,12 +3619,6 @@ class MokoWaaS extends CMSPlugin
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remaining restrictions only apply to non-master users
|
|
||||||
if ($this->isMasterUser())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$blocked = [];
|
$blocked = [];
|
||||||
|
|
||||||
if ($this->params->get('restrict_installer', 1))
|
if ($this->params->get('restrict_installer', 1))
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
*
|
||||||
|
* SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* FILE INFORMATION
|
||||||
|
* DEFGROUP: Joomla.Plugin
|
||||||
|
* INGROUP: MokoWaaS
|
||||||
|
* VERSION: 02.11.00
|
||||||
|
* PATH: /src/Field/CurrentIpField.php
|
||||||
|
* BRIEF: Read-only field that displays the current user's IP address
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Moko\Plugin\System\MokoWaaS\Field;
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Form\FormField;
|
||||||
|
|
||||||
|
class CurrentIpField extends FormField
|
||||||
|
{
|
||||||
|
protected $type = 'CurrentIp';
|
||||||
|
|
||||||
|
protected function getInput()
|
||||||
|
{
|
||||||
|
$currentIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||||
|
|
||||||
|
return '<div class="alert alert-info mb-0 py-2">'
|
||||||
|
. '<strong>Your current IP:</strong> '
|
||||||
|
. '<code>' . htmlspecialchars($currentIp) . '</code> '
|
||||||
|
. '<small class="text-muted">— add this to the table below to keep your session alive.</small>'
|
||||||
|
. '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getLabel()
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<form>
|
||||||
|
<field
|
||||||
|
name="ip"
|
||||||
|
type="text"
|
||||||
|
label="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_LABEL"
|
||||||
|
description="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_DESC"
|
||||||
|
required="true"
|
||||||
|
hint="e.g. 192.168.1.100 or 10.0.0.0/24"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="label"
|
||||||
|
type="text"
|
||||||
|
label="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_LABEL"
|
||||||
|
description="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_DESC"
|
||||||
|
hint="e.g. Office network"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="enabled"
|
||||||
|
type="radio"
|
||||||
|
label="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ENABLED_LABEL"
|
||||||
|
default="1"
|
||||||
|
class="btn-group btn-group-yesno"
|
||||||
|
>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
</field>
|
||||||
|
</form>
|
||||||
@@ -120,6 +120,13 @@ PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_LABEL="Force HTTPS"
|
|||||||
PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_DESC="Redirect all HTTP requests to HTTPS. Supports reverse proxy setups."
|
PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_DESC="Redirect all HTTP requests to HTTPS. Supports reverse proxy setups."
|
||||||
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_LABEL="Admin Session Timeout"
|
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_LABEL="Admin Session Timeout"
|
||||||
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_DESC="Minutes of idle time before admin sessions expire. 0 uses the Joomla default."
|
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_DESC="Minutes of idle time before admin sessions expire. 0 uses the Joomla default."
|
||||||
|
PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_LABEL="Trusted IPs (No Session Timeout)"
|
||||||
|
PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_DESC="Sessions from these IP addresses or ranges will never time out. Supports exact IPs, CIDR notation (e.g. 10.0.0.0/24), and wildcards (e.g. 192.168.1.*)."
|
||||||
|
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_LABEL="IP / CIDR"
|
||||||
|
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_DESC="An IP address, CIDR range, or wildcard pattern."
|
||||||
|
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_LABEL="Label"
|
||||||
|
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_DESC="A descriptive label for this entry (e.g. Office, VPN)."
|
||||||
|
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ENABLED_LABEL="Enabled"
|
||||||
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_LABEL="Minimum Password Length"
|
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_LABEL="Minimum Password Length"
|
||||||
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_DESC="Minimum number of characters required for user passwords."
|
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_DESC="Minimum number of characters required for user passwords."
|
||||||
PLG_SYSTEM_MOKOWAAS_PASSWORD_UPPER_LABEL="Require Uppercase"
|
PLG_SYSTEM_MOKOWAAS_PASSWORD_UPPER_LABEL="Require Uppercase"
|
||||||
|
|||||||
@@ -120,6 +120,13 @@ PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_LABEL="Force HTTPS"
|
|||||||
PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_DESC="Redirect all HTTP requests to HTTPS. Supports reverse proxy setups."
|
PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_DESC="Redirect all HTTP requests to HTTPS. Supports reverse proxy setups."
|
||||||
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_LABEL="Admin Session Timeout"
|
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_LABEL="Admin Session Timeout"
|
||||||
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_DESC="Minutes of idle time before admin sessions expire. 0 uses the Joomla default."
|
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_DESC="Minutes of idle time before admin sessions expire. 0 uses the Joomla default."
|
||||||
|
PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_LABEL="Trusted IPs (No Session Timeout)"
|
||||||
|
PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_DESC="Sessions from these IP addresses or ranges will never time out. Supports exact IPs, CIDR notation (e.g. 10.0.0.0/24), and wildcards (e.g. 192.168.1.*)."
|
||||||
|
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_LABEL="IP / CIDR"
|
||||||
|
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_DESC="An IP address, CIDR range, or wildcard pattern."
|
||||||
|
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_LABEL="Label"
|
||||||
|
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_DESC="A descriptive label for this entry (e.g. Office, VPN)."
|
||||||
|
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ENABLED_LABEL="Enabled"
|
||||||
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_LABEL="Minimum Password Length"
|
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_LABEL="Minimum Password Length"
|
||||||
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_DESC="Minimum number of characters required for user passwords."
|
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_DESC="Minimum number of characters required for user passwords."
|
||||||
PLG_SYSTEM_MOKOWAAS_PASSWORD_UPPER_LABEL="Require Uppercase"
|
PLG_SYSTEM_MOKOWAAS_PASSWORD_UPPER_LABEL="Require Uppercase"
|
||||||
|
|||||||
@@ -30,16 +30,11 @@
|
|||||||
<license>GNU General Public License version 3 or later; see LICENSE.md</license>
|
<license>GNU General Public License version 3 or later; see LICENSE.md</license>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
<version>02.04.00</version>
|
<version>02.16.03-dev</version>
|
||||||
<description>This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform.</description>
|
<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>
|
<namespace path=".">Moko\Plugin\System\MokoWaaS</namespace>
|
||||||
<scriptfile>script.php</scriptfile>
|
<scriptfile>script.php</scriptfile>
|
||||||
|
|
||||||
<!-- Update server configuration -->
|
|
||||||
<updateservers>
|
|
||||||
<server type="extension" priority="1" name="MokoWaaS Update Server (Gitea)">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/updates.xml</server>
|
|
||||||
</updateservers>
|
|
||||||
|
|
||||||
<files>
|
<files>
|
||||||
<filename plugin="mokowaas">script.php</filename>
|
<filename plugin="mokowaas">script.php</filename>
|
||||||
<folder>Extension</folder>
|
<folder>Extension</folder>
|
||||||
@@ -108,7 +103,7 @@
|
|||||||
type="url"
|
type="url"
|
||||||
label="PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_LABEL"
|
label="PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_LABEL"
|
||||||
description="PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_DESC"
|
description="PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_DESC"
|
||||||
default="https://mokoconsulting.tech"
|
default="https://mokoconsulting.tech/support"
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset name="waas_access"
|
<fieldset name="waas_access"
|
||||||
@@ -310,6 +305,7 @@
|
|||||||
<fieldset name="security"
|
<fieldset name="security"
|
||||||
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_LABEL"
|
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_LABEL"
|
||||||
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_DESC"
|
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_DESC"
|
||||||
|
addfieldprefix="Moko\Plugin\System\MokoWaaS\Field"
|
||||||
>
|
>
|
||||||
<field name="force_https" type="radio" default="1"
|
<field name="force_https" type="radio" default="1"
|
||||||
label="PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_LABEL"
|
label="PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_LABEL"
|
||||||
@@ -322,6 +318,22 @@
|
|||||||
label="PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_LABEL"
|
label="PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_LABEL"
|
||||||
description="PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_DESC"
|
description="PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_DESC"
|
||||||
default="60" hint="Minutes (0 = Joomla default)" />
|
default="60" hint="Minutes (0 = Joomla default)" />
|
||||||
|
<field
|
||||||
|
name="current_ip_display"
|
||||||
|
type="CurrentIp"
|
||||||
|
label=""
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="trusted_ips"
|
||||||
|
type="subform"
|
||||||
|
label="PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_LABEL"
|
||||||
|
description="PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_DESC"
|
||||||
|
formsource="plugins/system/mokowaas/forms/trusted_ip_entry.xml"
|
||||||
|
multiple="true"
|
||||||
|
layout="joomla.form.field.subform.repeatable-table"
|
||||||
|
groupByFieldset="false"
|
||||||
|
buttons="add,remove,move"
|
||||||
|
/>
|
||||||
<field name="password_min_length" type="number" default="12"
|
<field name="password_min_length" type="number" default="12"
|
||||||
label="PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_LABEL"
|
label="PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_LABEL"
|
||||||
description="PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_DESC" />
|
description="PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_DESC" />
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface
|
|||||||
if ($type === 'install' || $type === 'update')
|
if ($type === 'install' || $type === 'update')
|
||||||
{
|
{
|
||||||
$this->enableAndLockPlugin();
|
$this->enableAndLockPlugin();
|
||||||
|
$this->cleanupPluginUpdateSite();
|
||||||
$this->ensureMokoCassiopeia();
|
$this->ensureMokoCassiopeia();
|
||||||
$this->installLanguageOverrides();
|
$this->installLanguageOverrides();
|
||||||
$this->updateLoginSupportUrls();
|
$this->updateLoginSupportUrls();
|
||||||
@@ -210,6 +211,77 @@ class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface
|
|||||||
$db->execute();
|
$db->execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the plugin-level update site so only the package-level one remains.
|
||||||
|
*
|
||||||
|
* Earlier versions registered an update server in the plugin manifest
|
||||||
|
* (plg_system_mokowaas) in addition to the package manifest (pkg_mokowaas).
|
||||||
|
* This caused Joomla to check for plugin-level updates that don't exist,
|
||||||
|
* leading to failed downloads. Only the package update site should exist.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @since 02.11.02
|
||||||
|
*/
|
||||||
|
private function cleanupPluginUpdateSite()
|
||||||
|
{
|
||||||
|
$db = Factory::getDbo();
|
||||||
|
|
||||||
|
// Find the extension_id for the plugin
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select($db->quoteName('extension_id'))
|
||||||
|
->from($db->quoteName('#__extensions'))
|
||||||
|
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas'))
|
||||||
|
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||||
|
->where($db->quoteName('folder') . ' = ' . $db->quote('system'));
|
||||||
|
$db->setQuery($query);
|
||||||
|
$pluginId = (int) $db->loadResult();
|
||||||
|
|
||||||
|
if (!$pluginId)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find update_site_ids linked to the plugin (not the package)
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select($db->quoteName('update_site_id'))
|
||||||
|
->from($db->quoteName('#__update_sites_extensions'))
|
||||||
|
->where($db->quoteName('extension_id') . ' = ' . $pluginId);
|
||||||
|
$db->setQuery($query);
|
||||||
|
$siteIds = $db->loadColumn();
|
||||||
|
|
||||||
|
if (empty($siteIds))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the link rows
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->delete($db->quoteName('#__update_sites_extensions'))
|
||||||
|
->where($db->quoteName('extension_id') . ' = ' . $pluginId)
|
||||||
|
)->execute();
|
||||||
|
|
||||||
|
// Delete orphaned update_sites rows (only if no other extension uses them)
|
||||||
|
foreach ($siteIds as $siteId)
|
||||||
|
{
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select('COUNT(*)')
|
||||||
|
->from($db->quoteName('#__update_sites_extensions'))
|
||||||
|
->where($db->quoteName('update_site_id') . ' = ' . (int) $siteId);
|
||||||
|
$db->setQuery($query);
|
||||||
|
|
||||||
|
if ((int) $db->loadResult() === 0)
|
||||||
|
{
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->delete($db->quoteName('#__update_sites'))
|
||||||
|
->where($db->quoteName('update_site_id') . ' = ' . (int) $siteId)
|
||||||
|
)->execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure MokoOnyx is installed, locked, and set as default.
|
* Ensure MokoOnyx is installed, locked, and set as default.
|
||||||
*
|
*
|
||||||
@@ -482,7 +554,7 @@ class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface
|
|||||||
return [
|
return [
|
||||||
'{{BRAND_NAME}}' => $params->get('brand_name', 'MokoWaaS'),
|
'{{BRAND_NAME}}' => $params->get('brand_name', 'MokoWaaS'),
|
||||||
'{{COMPANY_NAME}}' => $params->get('company_name', 'Moko Consulting'),
|
'{{COMPANY_NAME}}' => $params->get('company_name', 'Moko Consulting'),
|
||||||
'{{SUPPORT_URL}}' => $params->get('support_url', 'https://mokoconsulting.tech'),
|
'{{SUPPORT_URL}}' => $params->get('support_url', 'https://mokoconsulting.tech/support'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<license>GPL-3.0-or-later</license>
|
<license>GPL-3.0-or-later</license>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
<version>02.04.00</version>
|
<version>02.16.03-dev</version>
|
||||||
<description>Joomla Web Services API routes for MokoWaaS site management — health checks, cache, updates, backups, and site info.</description>
|
<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>
|
<namespace path="src">Moko\Plugin\WebServices\MokoWaaS</namespace>
|
||||||
<files>
|
<files>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace Moko\Plugin\WebServices\MokoWaaS\Extension;
|
|||||||
defined('_JEXEC') or die;
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
use Joomla\CMS\Plugin\CMSPlugin;
|
use Joomla\CMS\Plugin\CMSPlugin;
|
||||||
|
use Joomla\CMS\Event\Application\BeforeApiRouteEvent;
|
||||||
use Joomla\CMS\Router\ApiRouter;
|
use Joomla\CMS\Router\ApiRouter;
|
||||||
use Joomla\Event\SubscriberInterface;
|
use Joomla\Event\SubscriberInterface;
|
||||||
|
|
||||||
@@ -36,14 +37,16 @@ final class MokoWaaSApi extends CMSPlugin implements SubscriberInterface
|
|||||||
/**
|
/**
|
||||||
* Register API routes for MokoWaaS.
|
* Register API routes for MokoWaaS.
|
||||||
*
|
*
|
||||||
* @param ApiRouter $router The API router
|
* @param BeforeApiRouteEvent $event The API route event (Joomla 6 typed event)
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*
|
*
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
*/
|
*/
|
||||||
public function onBeforeApiRoute(&$router): void
|
public function onBeforeApiRoute(BeforeApiRouteEvent $event): void
|
||||||
{
|
{
|
||||||
|
$router = $event->getRouter();
|
||||||
|
|
||||||
$router->createCRUDRoutes(
|
$router->createCRUDRoutes(
|
||||||
'v1/mokowaas/health',
|
'v1/mokowaas/health',
|
||||||
'health',
|
'health',
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?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.16.03-dev</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>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,539 @@
|
|||||||
|
<?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,7 +2,7 @@
|
|||||||
<extension type="package" method="upgrade">
|
<extension type="package" method="upgrade">
|
||||||
<name>MokoWaaS</name>
|
<name>MokoWaaS</name>
|
||||||
<packagename>mokowaas</packagename>
|
<packagename>mokowaas</packagename>
|
||||||
<version>02.04.00</version>
|
<version>02.16.03-dev</version>
|
||||||
<creationDate>2026-05-23</creationDate>
|
<creationDate>2026-05-23</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
@@ -12,10 +12,11 @@
|
|||||||
<description>MokoWaaS site management suite — branding, health monitoring, tenant restrictions, and REST API.</description>
|
<description>MokoWaaS site management suite — branding, health monitoring, tenant restrictions, and REST API.</description>
|
||||||
<scriptfile>script.php</scriptfile>
|
<scriptfile>script.php</scriptfile>
|
||||||
|
|
||||||
<files>
|
<files folder="packages">
|
||||||
<file type="plugin" id="plg_system_mokowaas" group="system">plg_system_mokowaas.zip</file>
|
<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="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_mokowaas" group="webservices">plg_webservices_mokowaas.zip</file>
|
||||||
|
<file type="plugin" id="plg_webservices_perfectpublisher" group="webservices">plg_webservices_perfectpublisher.zip</file>
|
||||||
</files>
|
</files>
|
||||||
|
|
||||||
<updateservers>
|
<updateservers>
|
||||||
|
|||||||
+59
-44
@@ -1,83 +1,98 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
VERSION: 02.04.00
|
VERSION: 02.16.00-dev
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<updates>
|
<updates>
|
||||||
<update>
|
<update>
|
||||||
<name>MokoWaaS</name>
|
<name>Package - MokoWaaS</name>
|
||||||
<description>MokoWaaS update</description>
|
<description>Package - MokoWaaS development build.</description>
|
||||||
<element>mokowaas</element>
|
<element>pkg_mokowaas</element>
|
||||||
<type>package</type>
|
<type>package</type>
|
||||||
<version>02.04.00-dev</version>
|
<client>site</client>
|
||||||
<tags><tag>development</tag></tags>
|
<version>02.16.00-dev</version>
|
||||||
<infourl title="MokoWaaS">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/development</infourl>
|
<creationDate>2026-05-28</creationDate>
|
||||||
|
<infourl title='Package - MokoWaaS'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/stable</infourl>
|
||||||
<downloads>
|
<downloads>
|
||||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/pkg_mokowaas-02.04.00-dev.zip</downloadurl>
|
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.16.00-dev.zip</downloadurl>
|
||||||
</downloads>
|
</downloads>
|
||||||
<targetplatform name="joomla" version="(5|6)\..*" />
|
<sha256>f233e73694b4e4ce0b4d71e828a158438257c1aec6fdb63d4b0482d23fd3d431</sha256>
|
||||||
|
<tags><tag>dev</tag></tags>
|
||||||
<maintainer>Moko Consulting</maintainer>
|
<maintainer>Moko Consulting</maintainer>
|
||||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||||
|
<targetplatform name="joomla" version="(5|6)\..*" />
|
||||||
</update>
|
</update>
|
||||||
<update>
|
<update>
|
||||||
<name>MokoWaaS</name>
|
<name>Package - MokoWaaS</name>
|
||||||
<description>MokoWaaS update</description>
|
<description>Package - MokoWaaS alpha build.</description>
|
||||||
<element>mokowaas</element>
|
<element>pkg_mokowaas</element>
|
||||||
<type>package</type>
|
<type>package</type>
|
||||||
<version>02.04.00-alpha</version>
|
<client>site</client>
|
||||||
|
<version>02.16.00-dev</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.16.00-dev.zip</downloadurl>
|
||||||
|
</downloads>
|
||||||
|
<sha256>f233e73694b4e4ce0b4d71e828a158438257c1aec6fdb63d4b0482d23fd3d431</sha256>
|
||||||
<tags><tag>alpha</tag></tags>
|
<tags><tag>alpha</tag></tags>
|
||||||
<infourl title="MokoWaaS">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/alpha</infourl>
|
|
||||||
<downloads>
|
|
||||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/alpha/pkg_mokowaas-02.04.00-alpha.zip</downloadurl>
|
|
||||||
</downloads>
|
|
||||||
<targetplatform name="joomla" version="(5|6)\..*" />
|
|
||||||
<maintainer>Moko Consulting</maintainer>
|
<maintainer>Moko Consulting</maintainer>
|
||||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||||
|
<targetplatform name="joomla" version="(5|6)\..*" />
|
||||||
</update>
|
</update>
|
||||||
<update>
|
<update>
|
||||||
<name>MokoWaaS</name>
|
<name>Package - MokoWaaS</name>
|
||||||
<description>MokoWaaS update</description>
|
<description>Package - MokoWaaS beta build.</description>
|
||||||
<element>mokowaas</element>
|
<element>pkg_mokowaas</element>
|
||||||
<type>package</type>
|
<type>package</type>
|
||||||
<version>02.04.00-beta</version>
|
<client>site</client>
|
||||||
|
<version>02.16.00-dev</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.16.00-dev.zip</downloadurl>
|
||||||
|
</downloads>
|
||||||
|
<sha256>f233e73694b4e4ce0b4d71e828a158438257c1aec6fdb63d4b0482d23fd3d431</sha256>
|
||||||
<tags><tag>beta</tag></tags>
|
<tags><tag>beta</tag></tags>
|
||||||
<infourl title="MokoWaaS">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/beta</infourl>
|
|
||||||
<downloads>
|
|
||||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/beta/pkg_mokowaas-02.04.00-beta.zip</downloadurl>
|
|
||||||
</downloads>
|
|
||||||
<targetplatform name="joomla" version="(5|6)\..*" />
|
|
||||||
<maintainer>Moko Consulting</maintainer>
|
<maintainer>Moko Consulting</maintainer>
|
||||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||||
|
<targetplatform name="joomla" version="(5|6)\..*" />
|
||||||
</update>
|
</update>
|
||||||
<update>
|
<update>
|
||||||
<name>MokoWaaS</name>
|
<name>Package - MokoWaaS</name>
|
||||||
<description>MokoWaaS update</description>
|
<description>Package - MokoWaaS rc build.</description>
|
||||||
<element>mokowaas</element>
|
<element>pkg_mokowaas</element>
|
||||||
<type>package</type>
|
<type>package</type>
|
||||||
<version>02.04.00-rc</version>
|
<client>site</client>
|
||||||
|
<version>02.16.00-dev</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.16.00-dev.zip</downloadurl>
|
||||||
|
</downloads>
|
||||||
|
<sha256>f233e73694b4e4ce0b4d71e828a158438257c1aec6fdb63d4b0482d23fd3d431</sha256>
|
||||||
<tags><tag>rc</tag></tags>
|
<tags><tag>rc</tag></tags>
|
||||||
<infourl title="MokoWaaS">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/rc</infourl>
|
|
||||||
<downloads>
|
|
||||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/rc/pkg_mokowaas-02.04.00-rc.zip</downloadurl>
|
|
||||||
</downloads>
|
|
||||||
<targetplatform name="joomla" version="(5|6)\..*" />
|
|
||||||
<maintainer>Moko Consulting</maintainer>
|
<maintainer>Moko Consulting</maintainer>
|
||||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||||
|
<targetplatform name="joomla" version="(5|6)\..*" />
|
||||||
</update>
|
</update>
|
||||||
<update>
|
<update>
|
||||||
<name>MokoWaaS</name>
|
<name>Package - MokoWaaS</name>
|
||||||
<description>MokoWaaS update</description>
|
<description>Package - MokoWaaS stable build.</description>
|
||||||
<element>mokowaas</element>
|
<element>pkg_mokowaas</element>
|
||||||
<type>package</type>
|
<type>package</type>
|
||||||
<version>02.04.00</version>
|
<client>site</client>
|
||||||
<tags><tag>stable</tag></tags>
|
<version>02.16.00-dev</version>
|
||||||
<infourl title="MokoWaaS">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/stable</infourl>
|
<creationDate>2026-05-28</creationDate>
|
||||||
|
<infourl title='Package - MokoWaaS'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/stable</infourl>
|
||||||
<downloads>
|
<downloads>
|
||||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.04.00.zip</downloadurl>
|
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.16.00-dev.zip</downloadurl>
|
||||||
</downloads>
|
</downloads>
|
||||||
<targetplatform name="joomla" version="(5|6)\..*" />
|
<sha256>f233e73694b4e4ce0b4d71e828a158438257c1aec6fdb63d4b0482d23fd3d431</sha256>
|
||||||
|
<tags><tag>stable</tag></tags>
|
||||||
<maintainer>Moko Consulting</maintainer>
|
<maintainer>Moko Consulting</maintainer>
|
||||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||||
|
<targetplatform name="joomla" version="(5|6)\..*" />
|
||||||
</update>
|
</update>
|
||||||
</updates>
|
</updates>
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# 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 |
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# 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 |
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
# 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 |
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# 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)
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# 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.
|
||||||
Reference in New Issue
Block a user