From 350ccc7ae7f94f76cd68f21fc9f617aa38698be4 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Sun, 31 May 2026 03:13:20 +0000
Subject: [PATCH 01/10] chore: sync updates.xml 02.27.00 from main [skip ci]
---
updates.xml | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/updates.xml b/updates.xml
index b1b647c..7a61813 100644
--- a/updates.xml
+++ b/updates.xml
@@ -1,7 +1,7 @@
@@ -13,16 +13,16 @@
site
02.26.18-dev
2026-05-31
- https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/development
+ https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/development
- https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/pkg_mokowaas-02.26.18-dev.zip
+ https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/pkg_mokowaas-02.26.18-dev.zip
d337997fedcb5b4b10286a41b1779869bd01dc5fe198389fedc27bc27d159489
dev
https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/CHANGELOG.md
Moko Consulting
https://mokoconsulting.tech
-
+
Package - MokoWaaS
@@ -89,15 +89,15 @@
site
02.27.00
2026-05-31
- https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/stable
+ https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/stable
- https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.27.00.zip
+ https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.27.00.zip
- ea91809b8b2ac4b21d1b678fe404c185eefc353664ccc0a88f5f8e3ebd3eaae9
+ 2183e59d64af7017e89858d4c102e41d3d26642728fda23c4aaf4d1e782bcb60
stable
https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/CHANGELOG.md
Moko Consulting
https://mokoconsulting.tech
-
+
--
2.52.0
From aa98456554bb7b1b70f0a0c1db8ba6846578a280 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Sun, 31 May 2026 03:13:40 +0000
Subject: [PATCH 02/10] chore: sync updates.xml 02.27.00 from main [skip ci]
---
updates.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/updates.xml b/updates.xml
index 7a61813..55fb93f 100644
--- a/updates.xml
+++ b/updates.xml
@@ -93,7 +93,7 @@
https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.27.00.zip
- 2183e59d64af7017e89858d4c102e41d3d26642728fda23c4aaf4d1e782bcb60
+ 88bb39655e84f469c62c40cef5a429daee99ca1dc1cc4504c33f6cd227641477
stable
https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/CHANGELOG.md
Moko Consulting
--
2.52.0
From 639ac84c0825bbea76ca112a50be991fc728d78e Mon Sep 17 00:00:00 2001
From: Jonathan Miller
Date: Sat, 30 May 2026 22:45:54 -0500
Subject: [PATCH 03/10] feat: add content sync task plugin, fix countdown, CB
tables, remove workflows (02.28.00)
- Add plg_task_mokowaassync scheduled task plugin for automated content sync
- Fix demo banner countdown to show weeks/days/months for longer intervals
- Add Community Builder tables to DemoResetService safe reset list
- Remove all CI/CD workflow files (manual release process)
- Bump version to 02.28.00 across all manifests and updates.xml
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.mokogitea/auto-release.yml | 949 ------------------
.mokogitea/branch-protection.yml | 251 -----
.mokogitea/cascade-dev.yml | 213 ----
.mokogitea/ci-joomla.yml | 450 ---------
.mokogitea/cleanup.yml | 87 --
.mokogitea/deploy-manual.yml | 126 ---
.mokogitea/gitleaks.yml | 96 --
.mokogitea/notify.yml | 71 --
.mokogitea/pr-branch-check.yml | 90 --
.mokogitea/pr-check.yml | 106 --
.mokogitea/pre-release.yml | 341 -------
.mokogitea/release.yml | 600 -----------
.mokogitea/repo-health.yml | 766 --------------
.mokogitea/security-audit.yml | 82 --
.mokogitea/update-server.yml | 464 ---------
.mokogitea/workflows/auto-bump.yml | 66 --
.mokogitea/workflows/auto-release.yml | 270 -----
.mokogitea/workflows/branch-cleanup.yml | 48 -
.mokogitea/workflows/cascade-dev.yml | 10 -
.mokogitea/workflows/ci-joomla.yml | 467 ---------
.mokogitea/workflows/cleanup.yml | 87 --
.mokogitea/workflows/gitleaks.yml | 96 --
.mokogitea/workflows/issue-branch.yml | 73 --
.mokogitea/workflows/notify.yml | 70 --
.mokogitea/workflows/pr-check.yml | 236 -----
.mokogitea/workflows/pre-release.yml | 233 -----
.mokogitea/workflows/repo-health.yml | 769 --------------
.mokogitea/workflows/security-audit.yml | 98 --
.mokogitea/workflows/update-server.yml | 312 ------
CHANGELOG.md | 10 +-
src/packages/com_mokowaas/mokowaas.xml | 2 +-
.../Extension/MokoWaaS.php | 28 +-
.../Service/DemoResetService.php | 10 +
src/packages/plg_system_mokowaas/mokowaas.xml | 2 +-
.../plg_task_mokowaasdemo/mokowaasdemo.xml | 2 +-
.../forms/sync_params.xml | 8 +
.../language/en-GB/plg_task_mokowaassync.ini | 8 +
.../en-GB/plg_task_mokowaassync.sys.ini | 6 +
.../plg_task_mokowaassync/mokowaassync.xml | 31 +
.../services/provider.php | 37 +
.../src/Extension/ContentSync.php | 164 +++
.../plg_webservices_mokowaas/mokowaas.xml | 2 +-
.../perfectpublisher.xml | 2 +-
src/pkg_mokowaas.xml | 3 +-
src/script.php | 2 +
updates.xml | 83 +-
46 files changed, 308 insertions(+), 7619 deletions(-)
delete mode 100644 .mokogitea/auto-release.yml
delete mode 100644 .mokogitea/branch-protection.yml
delete mode 100644 .mokogitea/cascade-dev.yml
delete mode 100644 .mokogitea/ci-joomla.yml
delete mode 100644 .mokogitea/cleanup.yml
delete mode 100644 .mokogitea/deploy-manual.yml
delete mode 100644 .mokogitea/gitleaks.yml
delete mode 100644 .mokogitea/notify.yml
delete mode 100644 .mokogitea/pr-branch-check.yml
delete mode 100644 .mokogitea/pr-check.yml
delete mode 100644 .mokogitea/pre-release.yml
delete mode 100644 .mokogitea/release.yml
delete mode 100644 .mokogitea/repo-health.yml
delete mode 100644 .mokogitea/security-audit.yml
delete mode 100644 .mokogitea/update-server.yml
delete mode 100644 .mokogitea/workflows/auto-bump.yml
delete mode 100644 .mokogitea/workflows/auto-release.yml
delete mode 100644 .mokogitea/workflows/branch-cleanup.yml
delete mode 100644 .mokogitea/workflows/cascade-dev.yml
delete mode 100644 .mokogitea/workflows/ci-joomla.yml
delete mode 100644 .mokogitea/workflows/cleanup.yml
delete mode 100644 .mokogitea/workflows/gitleaks.yml
delete mode 100644 .mokogitea/workflows/issue-branch.yml
delete mode 100644 .mokogitea/workflows/notify.yml
delete mode 100644 .mokogitea/workflows/pr-check.yml
delete mode 100644 .mokogitea/workflows/pre-release.yml
delete mode 100644 .mokogitea/workflows/repo-health.yml
delete mode 100644 .mokogitea/workflows/security-audit.yml
delete mode 100644 .mokogitea/workflows/update-server.yml
create mode 100644 src/packages/plg_task_mokowaassync/forms/sync_params.xml
create mode 100644 src/packages/plg_task_mokowaassync/language/en-GB/plg_task_mokowaassync.ini
create mode 100644 src/packages/plg_task_mokowaassync/language/en-GB/plg_task_mokowaassync.sys.ini
create mode 100644 src/packages/plg_task_mokowaassync/mokowaassync.xml
create mode 100644 src/packages/plg_task_mokowaassync/services/provider.php
create mode 100644 src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php
diff --git a/.mokogitea/auto-release.yml b/.mokogitea/auto-release.yml
deleted file mode 100644
index 279bc5e..0000000
--- a/.mokogitea/auto-release.yml
+++ /dev/null
@@ -1,949 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: MokoStandards.Release
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
-# PATH: /templates/workflows/joomla/auto-release.yml.template
-# VERSION: 04.06.00
-# BRIEF: Joomla build & release — ZIP package, updates.xml, SHA-256 checksum
-#
-# +========================================================================+
-# | BUILD & RELEASE PIPELINE (JOOMLA) |
-# +========================================================================+
-# | |
-# | Triggers on push to main (skips bot commits + [skip ci]): |
-# | |
-# | Every push: |
-# | 1. Read version from README.md |
-# | 3. Set platform version (Joomla ) |
-# | 4. Update [VERSION: XX.YY.ZZ] badges in markdown files |
-# | 5. Write updates.xml (Joomla update server XML) |
-# | 6. Create git tag vXX.YY.ZZ |
-# | 7a. Patch: update existing Gitea Release for this minor |
-# | 8. Build ZIP, upload asset, write SHA-256 to updates.xml |
-# | |
-# | Every version change: archives main -> version/XX.YY branch |
-# | All patches release (including 00). Patch 00/01 = full pipeline. |
-# | First release only (patch == 01): |
-# | 7b. Create new Gitea Release |
-# | |
-# | GitHub mirror: stable/rc releases only (continue-on-error) |
-# | |
-# +========================================================================+
-
-name: Build & Release
-
-on:
- pull_request:
- types: [closed]
- branches:
- - main
- paths:
- - 'src/**'
- - 'htdocs/**'
- workflow_dispatch:
-
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
- GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
-
-permissions:
- contents: write
-
-jobs:
- release:
- name: Build & Release Pipeline
- runs-on: release
- if: >-
- github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- token: ${{ secrets.GA_TOKEN }}
- fetch-depth: 0
-
- - name: Setup MokoStandards tools
- env:
- MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
- MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
- COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}'
- run: |
- # Ensure PHP + Composer are available
- if ! command -v composer &> /dev/null; then
- sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
- fi
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
- /tmp/mokostandards-api
- cd /tmp/mokostandards-api
- composer install --no-dev --no-interaction --quiet
-
- # -- STEP 1: Read version -----------------------------------------------
- - name: "Step 1: Read version from README.md"
- id: version
- run: |
- VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null)
- if [ -z "$VERSION" ]; then
- echo "No VERSION in README.md — skipping release"
- echo "skip=true" >> "$GITHUB_OUTPUT"
- exit 0
- fi
- # Derive major.minor for branch naming (patches update existing branch)
- MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}')
- PATCH=$(echo "$VERSION" | awk -F. '{print $3}')
-
- MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
- MINOR_NUM=$(echo "$VERSION" | awk -F. '{print $2}')
-
- echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT"
- echo "minor=$MINOR" >> "$GITHUB_OUTPUT"
- echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
- echo "release_tag=stable" >> "$GITHUB_OUTPUT"
- echo "stability=stable" >> "$GITHUB_OUTPUT"
- echo "skip=false" >> "$GITHUB_OUTPUT"
- if [ "$PATCH" = "00" ] || [ "$PATCH" = "01" ]; then
- echo "is_minor=true" >> "$GITHUB_OUTPUT"
- echo "Version: $VERSION (first release for this minor — full pipeline)"
- else
- echo "is_minor=false" >> "$GITHUB_OUTPUT"
- echo "Version: $VERSION (patch — platform version + badges only)"
- fi
-
- # -- STEP 1b: Bump minor version (stable = minor bump, reset patch) ------
- - name: "Step 1b: Bump minor version for stable release"
- if: steps.version.outputs.skip != 'true'
- id: bump
- run: |
- CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
- [ -z "$CURRENT" ] && { echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0; }
-
- MAJOR=$((10#$(echo "$CURRENT" | cut -d. -f1)))
- MINOR=$((10#$(echo "$CURRENT" | cut -d. -f2)))
-
- # Minor bump, reset patch. Rollover if minor > 99
- MINOR=$((MINOR + 1))
- if [ $MINOR -gt 99 ]; then
- MINOR=0
- MAJOR=$((MAJOR + 1))
- fi
-
- VERSION=$(printf "%02d.%02d.00" $MAJOR $MINOR)
- TODAY=$(date +%Y-%m-%d)
-
- echo "Stable bump: ${CURRENT} → ${VERSION} (minor)"
-
- # Update README.md
- sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${VERSION}/" README.md
-
- # Update manifest
- MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1)
- if [ -n "$MANIFEST" ]; then
- MANIFEST_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
- [ -n "$MANIFEST_VER" ] && sed -i "s|${MANIFEST_VER}|${VERSION}|" "$MANIFEST"
- sed -i "s|[^<]*|${TODAY}|" "$MANIFEST"
- fi
-
- # Promote [Unreleased] section in CHANGELOG.md to new version
- if [ -f "CHANGELOG.md" ] && grep -qi "Unreleased" CHANGELOG.md; then
- sed -i "s|## \[Unreleased\]|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md
- sed -i "s|## Unreleased|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md
- sed -i "2i ## [Unreleased]" CHANGELOG.md
- sed -i "3i \\ " CHANGELOG.md
- echo "CHANGELOG promoted to [${VERSION}]"
- fi
-
- # Commit and push
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- git add -A
- git diff --cached --quiet || {
- git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]"
- git push origin HEAD:main 2>&1
- }
-
- # Override version output for rest of pipeline
- echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- echo "major=$(printf "%02d" $MAJOR)" >> "$GITHUB_OUTPUT"
-
- - name: Check if already released
- if: steps.version.outputs.skip != 'true'
- id: check
- run: |
- TAG="${{ steps.version.outputs.release_tag }}"
- BRANCH="${{ steps.version.outputs.branch }}"
-
- TAG_EXISTS=false
- BRANCH_EXISTS=false
-
- git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true
- git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true
-
- echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT"
- echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT"
-
- # Tag and branch may persist across patch releases — never skip
- echo "already_released=false" >> "$GITHUB_OUTPUT"
-
- # -- SANITY CHECKS -------------------------------------------------------
- - name: "Sanity: Pre-release validation"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.check.outputs.already_released != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- ERRORS=0
-
- echo "## Pre-Release Sanity Checks (Joomla)" >> $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
-
- # -- Joomla: manifest version drift --------
- MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1)
- if [ -n "$MANIFEST" ]; then
- XML_VER=$(sed -n 's/.*\([^<]*\)<\/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
- fi
-
- # -- Joomla: XML manifest existence --------
- if [ -z "$MANIFEST" ]; then
- echo "- No Joomla XML manifest found" >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS+1))
- else
- echo "- Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
-
- # -- Joomla: extension type check --------
- TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null)
- echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY
- fi
-
- 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 ---------------
- # Always runs — every version change on main archives to version/XX.YY
- - name: "Step 2: Version archive branch"
- if: steps.check.outputs.already_released != 'true'
- run: |
- BRANCH="${{ steps.version.outputs.branch }}"
- IS_MINOR="${{ steps.version.outputs.is_minor }}"
- PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}')
-
- # Check if branch exists
- if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
- git push origin HEAD:"$BRANCH" --force
- echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY
- else
- git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH"
- git push origin "$BRANCH" --force
- echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
- fi
-
- # -- STEP 3: Set platform version ----------------------------------------
- - name: "Step 3: Set platform version"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.check.outputs.already_released != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- php /tmp/mokostandards-api/cli/version_set_platform.php \
- --path . --version "$VERSION" --branch main
-
- # -- STEP 4: Update version badges ----------------------------------------
- - name: "Step 4: Update version badges"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.check.outputs.already_released != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" | while read -r f; do
- if grep -q '\[VERSION:' "$f" 2>/dev/null; then
- sed -i "s/\[VERSION:[[:space:]]*[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\]/[VERSION: ${VERSION}]/" "$f"
- fi
- done
-
- # -- STEP 5: Write updates.xml (Joomla update server) ---------------------
- - name: "Step 5: Write updates.xml"
- id: updates
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.check.outputs.already_released != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- REPO="${{ github.repository }}"
-
- # -- Parse extension metadata from XML manifest ----------------
- MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1)
- if [ -z "$MANIFEST" ]; then
- echo "Warning: No Joomla XML manifest found — skipping updates.xml" >> $GITHUB_STEP_SUMMARY
- exit 0
- fi
-
- # Extract fields using sed (portable — no grep -P)
- EXT_NAME=$(sed -n 's/.*\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
- EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
- EXT_CLIENT=$(sed -n 's/.*]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- TARGET_PLATFORM=$(sed -n 's/.*\(\).*/\1/p' "$MANIFEST" | head -1)
- PHP_MINIMUM=$(sed -n 's/.*\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
-
- # If EXT_NAME is a language key (e.g. PLG_SYSTEM_MOKOJGDPC), resolve from .ini
- if echo "$EXT_NAME" | grep -qE '^[A-Z_]+$'; then
- INI_NAME=$(find . -name "*.sys.ini" -path "*/en-GB/*" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2)
- [ -z "$INI_NAME" ] && INI_NAME=$(find . -name "*.sys.ini" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2)
- [ -n "$INI_NAME" ] && EXT_NAME="$INI_NAME"
- fi
-
- # Fallbacks
- [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
- [ -z "$EXT_TYPE" ] && EXT_TYPE="component"
-
- # Derive element if not in manifest:
- # 1. plugin="xxx" attribute (plugins)
- # 2. module="xxx" attribute (modules)
- # 3. XML filename (components, packages)
- # 4. Repo name fallback (templates, anything else)
- if [ -z "$EXT_ELEMENT" ]; then
- EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- fi
- if [ -z "$EXT_ELEMENT" ]; then
- EXT_ELEMENT=$(sed -n 's/.*module="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- fi
- if [ -z "$EXT_ELEMENT" ]; then
- FNAME=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
- # If filename is generic (templateDetails, manifest), use repo name
- case "$FNAME" in
- templatedetails|manifest) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
- *) EXT_ELEMENT="$FNAME" ;;
- esac
- fi
- # Final fallback
- [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
-
- # Save for Steps 7, 8, 8b
- echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
- echo "ext_name=${EXT_NAME}" >> "$GITHUB_OUTPUT"
- echo "ext_type=${EXT_TYPE}" >> "$GITHUB_OUTPUT"
- echo "ext_folder=${EXT_FOLDER}" >> "$GITHUB_OUTPUT"
-
- # Build client tag: plugins and frontend modules need site
- CLIENT_TAG=""
- if [ -n "$EXT_CLIENT" ]; then
- CLIENT_TAG="${EXT_CLIENT}"
- elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then
- CLIENT_TAG="site"
- fi
-
- # Build folder tag for plugins (required for Joomla to match the update)
- FOLDER_TAG=""
- if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then
- FOLDER_TAG="${EXT_FOLDER}"
- fi
-
- # Build targetplatform (fallback to Joomla 5 if not in manifest)
- if [ -z "$TARGET_PLATFORM" ]; then
- TARGET_PLATFORM=$(printf '' "/")
- fi
-
- # Build php_minimum tag
- PHP_TAG=""
- if [ -n "$PHP_MINIMUM" ]; then
- PHP_TAG="${PHP_MINIMUM}"
- fi
-
- # Build TYPE_PREFIX for download URL
- 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
-
- DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/stable/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
- INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/stable"
-
- # -- Build update entry for a given stability tag
- build_entry() {
- local TAG_NAME="$1"
- printf '%s\n' ' '
- printf '%s\n' " ${EXT_NAME}"
- printf '%s\n' " ${EXT_NAME} update"
- printf '%s\n' " ${EXT_ELEMENT}"
- printf '%s\n' " ${EXT_TYPE}"
- printf '%s\n' " ${VERSION}"
- [ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}"
- [ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}"
- printf '%s\n' " ${TAG_NAME}"
- printf '%s\n' " ${INFO_URL}"
- printf '%s\n' ' '
- printf '%s\n' " ${DOWNLOAD_URL}"
- printf '%s\n' ' '
- printf '%s\n' " ${TARGET_PLATFORM}"
- [ -n "$PHP_TAG" ] && printf '%s\n' " ${PHP_TAG}"
- printf '%s\n' ' Moko Consulting'
- printf '%s\n' ' https://mokoconsulting.tech'
- printf '%s\n' ' '
- }
-
- # -- Write updates.xml with cascading channels
- # Stable release updates ALL channels (development, alpha, beta, rc, stable)
- {
- printf '%s\n' ""
- printf '%s\n' ""
- printf '%s\n' ""
- printf '%s\n' ''
- build_entry "development"
- build_entry "alpha"
- build_entry "beta"
- build_entry "rc"
- build_entry "stable"
- printf '%s\n' ''
- } > updates.xml
-
- echo "updates.xml: ${VERSION} (all channels updated to stable)" >> $GITHUB_STEP_SUMMARY
-
- # -- Commit all changes ---------------------------------------------------
- - name: Commit release changes
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.check.outputs.already_released != 'true'
- run: |
- if git diff --quiet && git diff --cached --quiet; then
- echo "No changes to commit"
- exit 0
- fi
- 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 commit -m "chore(release): build ${VERSION} [skip ci]" \
- --author="gitea-actions[bot] "
- git push -u origin HEAD
-
- # -- STEP 6: Create tag ---------------------------------------------------
- - name: "Step 6: Create git tag"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.check.outputs.tag_exists != 'true' &&
- steps.version.outputs.is_minor == 'true'
- run: |
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- # Only create the major release tag if it doesn't exist yet
- if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then
- git tag "$RELEASE_TAG"
- git push origin "$RELEASE_TAG"
- echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
- else
- echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY
- fi
- echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
-
- # -- STEP 7: Create or update Gitea Release --------------------------------
- - name: "Step 7: Gitea Release"
- if: >-
- steps.version.outputs.skip != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- 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}"
-
- # Reuse metadata from Step 5 (single source of truth)
- EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
- EXT_NAME="${{ steps.updates.outputs.ext_name }}"
- 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/mokostandards-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)"
- 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 Joomla package and update checksum"
- if: >-
- steps.version.outputs.skip != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- REPO="${{ github.repository }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
-
- # All ZIPs upload to the major release tag (vXX)
- 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
- 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 '/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>.*/\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/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- 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/ — skipping package"; exit 0; }
-
- EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
-
- # ZIP package
- cd "$SOURCE_DIR"
- zip -r "/tmp/${ZIP_NAME}" . -x $EXCLUDES
- cd ..
-
- # tar.gz package
- 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 = (
- " \n"
- f" {zip_url}\n"
- " "
- )
- block = re.sub(r' .*?', new_downloads, block, flags=re.DOTALL)
- # Add or replace sha256
- if '' in block:
- block = re.sub(r' .*?', f' {sha}', block)
- else:
- block = block.replace('', f'\n {sha}')
- return block
-
- content = re.sub(
- r' .*?stable.*?',
- 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] " || 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 "### Joomla 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'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
- EXT_TYPE="${{ steps.updates.outputs.ext_type }}"
- EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}"
-
- # Build TYPE_PREFIX to match Step 8's ZIP naming
- 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) --------------------------------
- - name: "Step 9: Mirror release to GitHub"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.version.outputs.stability == 'stable' &&
- secrets.GH_TOKEN != ''
- continue-on-error: true
- env:
- GH_TOKEN: ${{ secrets.GH_TOKEN }}
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- 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 }}"
-
- NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true)
- [ -z "$NOTES" ] && NOTES="Release ${VERSION}"
- echo "$NOTES" > /tmp/release_notes.md
-
- 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)
-
- 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 ----------------------------
- - name: "Step 10: Push main to GitHub mirror"
- if: >-
- steps.version.outputs.skip != 'true' &&
- secrets.GH_TOKEN != ''
- continue-on-error: true
- run: |
- GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
- GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
- GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
- git remote add github "https://x-access-token:${{ secrets.GH_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 fetch origin main --depth=1
- git push github origin/main:refs/heads/main --force 2>/dev/null \
- && echo "main branch pushed to GitHub mirror" \
- || echo "WARNING: GitHub mirror push failed"
-
- # -- Clean up lesser pre-releases (cascade) ---------------------------------
- # stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev
- - name: "Delete lesser pre-release channels"
- if: steps.version.outputs.skip != 'true'
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- TOKEN="${{ secrets.GA_TOKEN }}"
-
- # Stable deletes all pre-release channels
- TAGS_TO_DELETE="development alpha beta release-candidate"
-
- DELETED=0
- 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})"
- DELETED=$((DELETED + 1))
- fi
- done
- echo "Cleaned up ${DELETED} pre-release channel(s)" >> $GITHUB_STEP_SUMMARY
-
- # -- STEP 11: Reset dev branch from main ------------------------------------
- - name: "Step 11: Delete and recreate dev branch from main"
- if: steps.version.outputs.skip != 'true'
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- TOKEN="${{ secrets.GA_TOKEN }}"
-
- # Delete dev branch
- curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
- "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
-
- # Recreate dev from main (now includes version bump + changelog promotion)
- curl -sf -X POST -H "Authorization: token ${TOKEN}" \
- -H "Content-Type: application/json" \
- "${API_BASE}/branches" \
- -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
-
- echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY
-
- # -- Summary --------------------------------------------------------------
- - name: Pipeline Summary
- if: always()
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
- echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
- echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
- elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
- echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
- else
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "## Build & Release Complete (Joomla)" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
- echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
- echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
- fi
diff --git a/.mokogitea/branch-protection.yml b/.mokogitea/branch-protection.yml
deleted file mode 100644
index 2dff8b9..0000000
--- a/.mokogitea/branch-protection.yml
+++ /dev/null
@@ -1,251 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-# SPDX-License-Identifier: GPL-3.0-or-later
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Automation
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
-# PATH: /.gitea/workflows/branch-protection.yml
-# BRIEF: Apply standardised branch protection rules to all governed repositories
-#
-# +========================================================================+
-# | BRANCH PROTECTION SETUP |
-# +========================================================================+
-# | |
-# | Applies protection rules for: main, dev, rc, beta, alpha |
-# | |
-# | main — Require PR, block rejected reviews, no force push |
-# | dev — Allow push, no force push, no delete |
-# | rc — Allow push, no force push, no delete |
-# | beta — Allow push, no force push, no delete |
-# | alpha — Allow push, no force push, no delete |
-# | |
-# | jmiller has override authority on all branches. |
-# | |
-# +========================================================================+
-
-name: Branch Protection Setup
-
-on:
- schedule:
- - cron: '0 2 * * 1' # Weekly Monday 02:00 UTC
- workflow_dispatch:
- inputs:
- dry_run:
- description: 'Preview mode (no changes)'
- required: false
- type: boolean
- default: false
- repos:
- description: 'Comma-separated repo names (empty = all governed repos)'
- required: false
- type: string
- default: ''
-
-env:
- GITEA_URL: https://git.mokoconsulting.tech
- GITEA_ORG: MokoConsulting
-
-permissions:
- contents: read
-
-jobs:
- protect:
- name: Apply Branch Protection Rules
- runs-on: ubuntu-latest
-
- steps:
- - name: Determine target repos
- id: repos
- env:
- GA_TOKEN: ${{ secrets.GA_TOKEN }}
- run: |
- API="${GITEA_URL}/api/v1"
-
- # Platform/standards/infra repos to exclude
- EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private MokoStandards moko-platform MokoTesting"
- EXCLUDE="$EXCLUDE MokoStandards-Template-Client MokoStandards-Template-Dolibarr MokoStandards-Template-Generic MokoStandards-Template-Joomla MokoDoliProjTemplate"
-
- if [ -n "${{ inputs.repos }}" ]; then
- # User-specified repos
- REPOS=$(echo "${{ inputs.repos }}" | tr ',' ' ')
- else
- # Fetch all org repos
- PAGE=1
- REPOS=""
- while true; do
- BATCH=$(curl -sS \
- -H "Authorization: token ${GA_TOKEN}" \
- "${API}/orgs/${GITEA_ORG}/repos?page=${PAGE}&limit=50" \
- | jq -r '.[].name // empty')
- [ -z "$BATCH" ] && break
- REPOS="$REPOS $BATCH"
- PAGE=$((PAGE + 1))
- done
-
- # Filter out excluded repos
- FILTERED=""
- for REPO in $REPOS; do
- SKIP=false
- for EX in $EXCLUDE; do
- if [ "$REPO" = "$EX" ]; then
- SKIP=true
- break
- fi
- done
- if [ "$SKIP" = "false" ]; then
- FILTERED="$FILTERED $REPO"
- fi
- done
- REPOS="$FILTERED"
- fi
-
- echo "repos=$REPOS" >> "$GITHUB_OUTPUT"
- COUNT=$(echo "$REPOS" | wc -w)
- echo "📋 Target repos (${COUNT}): $REPOS"
-
- - name: Apply protection rules
- env:
- GA_TOKEN: ${{ secrets.GA_TOKEN }}
- DRY_RUN: ${{ inputs.dry_run || 'false' }}
- run: |
- API="${GITEA_URL}/api/v1"
- REPOS="${{ steps.repos.outputs.repos }}"
-
- SUCCESS=0
- FAILED=0
- SKIPPED=0
-
- # ── Rule definitions ──────────────────────────────────────
- # Only the CI bot (jmiller token) can push directly.
- # All human contributors must use PRs.
- # Force push disabled on all branches.
-
- RULE_MAIN='{
- "rule_name": "main",
- "enable_push": true,
- "enable_push_whitelist": true,
- "push_whitelist_usernames": ["jmiller"],
- "enable_force_push": false,
- "enable_force_push_allowlist": false,
- "force_push_allowlist_usernames": [],
- "enable_merge_whitelist": false,
- "required_approvals": 0,
- "dismiss_stale_approvals": true,
- "block_on_rejected_reviews": true,
- "block_on_outdated_branch": false,
- "priority": 1
- }'
-
- RULE_DEV='{
- "rule_name": "dev",
- "enable_push": true,
- "enable_push_whitelist": true,
- "push_whitelist_usernames": ["jmiller"],
- "enable_force_push": false,
- "enable_force_push_allowlist": false,
- "force_push_allowlist_usernames": [],
- "enable_merge_whitelist": false,
- "required_approvals": 0,
- "block_on_rejected_reviews": false,
- "priority": 2
- }'
-
- RULE_RC='{
- "rule_name": "rc",
- "enable_push": true,
- "enable_push_whitelist": true,
- "push_whitelist_usernames": ["jmiller"],
- "enable_force_push": false,
- "enable_force_push_allowlist": false,
- "force_push_allowlist_usernames": [],
- "enable_merge_whitelist": false,
- "required_approvals": 0,
- "block_on_rejected_reviews": false,
- "priority": 3
- }'
-
- RULE_BETA='{
- "rule_name": "beta",
- "enable_push": true,
- "enable_push_whitelist": true,
- "push_whitelist_usernames": ["jmiller"],
- "enable_force_push": false,
- "enable_force_push_allowlist": false,
- "force_push_allowlist_usernames": [],
- "enable_merge_whitelist": false,
- "required_approvals": 0,
- "block_on_rejected_reviews": false,
- "priority": 4
- }'
-
- RULE_ALPHA='{
- "rule_name": "alpha",
- "enable_push": true,
- "enable_push_whitelist": true,
- "push_whitelist_usernames": ["jmiller"],
- "enable_force_push": false,
- "enable_force_push_allowlist": false,
- "force_push_allowlist_usernames": [],
- "enable_merge_whitelist": false,
- "required_approvals": 0,
- "block_on_rejected_reviews": false,
- "priority": 5
- }'
-
- RULES=("$RULE_MAIN" "$RULE_DEV" "$RULE_RC" "$RULE_BETA" "$RULE_ALPHA")
- RULE_NAMES=("main" "dev" "rc" "beta" "alpha")
-
- # ── Apply rules to each repo ──────────────────────────────
- for REPO in $REPOS; do
- echo ""
- echo "═══ ${REPO} ═══"
-
- for i in "${!RULES[@]}"; do
- RULE="${RULES[$i]}"
- NAME="${RULE_NAMES[$i]}"
-
- if [ "$DRY_RUN" = "true" ]; then
- echo " [DRY RUN] Would apply rule: ${NAME}"
- SKIPPED=$((SKIPPED + 1))
- continue
- fi
-
- # Delete existing rule if present (idempotent recreate)
- ENCODED_NAME=$(echo "$NAME" | sed 's|/|%2F|g')
- curl -sS -o /dev/null -w "" \
- -X DELETE \
- -H "Authorization: token ${GA_TOKEN}" \
- "${API}/repos/${GITEA_ORG}/${REPO}/branch_protections/${ENCODED_NAME}" 2>/dev/null || true
-
- # Create rule
- RESPONSE=$(curl -sS -w "\n%{http_code}" \
- -X POST \
- -H "Authorization: token ${GA_TOKEN}" \
- -H "Content-Type: application/json" \
- -d "$RULE" \
- "${API}/repos/${GITEA_ORG}/${REPO}/branch_protections")
-
- HTTP=$(echo "$RESPONSE" | tail -1)
- BODY=$(echo "$RESPONSE" | sed '$d')
-
- if [ "$HTTP" = "201" ]; then
- echo " ✅ ${NAME}"
- SUCCESS=$((SUCCESS + 1))
- else
- echo " ❌ ${NAME} (HTTP ${HTTP}): $(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1)"
- FAILED=$((FAILED + 1))
- fi
- done
- done
-
- # ── Summary ───────────────────────────────────────────────
- echo ""
- echo "════════════════════════════════════════"
- echo " ✅ Success: ${SUCCESS}"
- echo " ❌ Failed: ${FAILED}"
- echo " ⏭️ Skipped: ${SKIPPED}"
- echo "════════════════════════════════════════"
-
- if [ "$FAILED" -gt 0 ]; then
- echo "::warning::${FAILED} rule(s) failed to apply"
- fi
diff --git a/.mokogitea/cascade-dev.yml b/.mokogitea/cascade-dev.yml
deleted file mode 100644
index d4780b1..0000000
--- a/.mokogitea/cascade-dev.yml
+++ /dev/null
@@ -1,213 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: MokoStandards.Maintenance
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
-# PATH: /templates/workflows/cascade-dev.yml.template
-# VERSION: 02.00.00
-# BRIEF: Forward-merge main → all open branches after every push to main
-#
-# +========================================================================+
-# | CASCADE MAIN → ALL BRANCHES |
-# +========================================================================+
-# | |
-# | Triggers on every push to main (PR merges, bot commits, etc.) |
-# | |
-# | 1. List all branches matching: dev, rc/*, beta/*, alpha/* |
-# | 2. For each: create PR (main → branch), auto-merge if clean |
-# | 3. On conflict: leave PR open for manual resolution |
-# | |
-# +========================================================================+
-
-name: Cascade Main → Dev
-
-on:
- push:
- branches:
- - main
- workflow_dispatch:
-
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
- GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
-
-permissions:
- contents: write
- pull-requests: write
-
-jobs:
- cascade:
- name: Cascade main → branches
- runs-on: ubuntu-latest
- if: >-
- !contains(github.event.head_commit.message, '[skip ci]') &&
- !contains(github.event.head_commit.message, '[skip cascade]')
-
- steps:
- - name: Discover target branches
- id: branches
- env:
- GA_TOKEN: ${{ secrets.GA_TOKEN }}
- run: |
- API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
-
- # Fetch all branches (paginated)
- PAGE=1
- ALL_BRANCHES=""
- while true; do
- BATCH=$(curl -sS \
- -H "Authorization: token ${GA_TOKEN}" \
- "${API}/branches?page=${PAGE}&limit=50" \
- | jq -r '.[].name // empty')
- [ -z "$BATCH" ] && break
- ALL_BRANCHES="$ALL_BRANCHES $BATCH"
- PAGE=$((PAGE + 1))
- done
-
- # Filter to cascade targets: dev, dev/*, rc/*, beta/*, alpha/*
- TARGETS=""
- for BRANCH in $ALL_BRANCHES; do
- case "$BRANCH" in
- dev|dev/*|rc/*|beta/*|alpha/*)
- TARGETS="$TARGETS $BRANCH"
- ;;
- esac
- done
-
- TARGETS=$(echo "$TARGETS" | xargs) # trim whitespace
-
- if [ -z "$TARGETS" ]; then
- echo "targets=" >> "$GITHUB_OUTPUT"
- echo "ℹ️ No cascade target branches found"
- else
- echo "targets=$TARGETS" >> "$GITHUB_OUTPUT"
- COUNT=$(echo "$TARGETS" | wc -w)
- echo "📋 Found ${COUNT} target branch(es): ${TARGETS}"
- fi
-
- - name: Cascade to all target branches
- if: steps.branches.outputs.targets != ''
- env:
- GA_TOKEN: ${{ secrets.GA_TOKEN }}
- run: |
- API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- SHORT_SHA="${GITHUB_SHA:0:7}"
- TARGETS="${{ steps.branches.outputs.targets }}"
-
- SUCCESS=0
- CONFLICTS=0
- SKIPPED=0
- FAILED=0
-
- for BRANCH in $TARGETS; do
- echo ""
- echo "═══ main → ${BRANCH} ═══"
-
- # Check if branch is already up to date
- ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g')
- RESPONSE=$(curl -sS \
- -H "Authorization: token ${GA_TOKEN}" \
- "${API}/compare/${ENCODED_BRANCH}...main")
-
- AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0')
-
- if [ "$AHEAD" -eq 0 ]; then
- echo " ✅ Already up to date"
- SKIPPED=$((SKIPPED + 1))
- continue
- fi
-
- echo " ℹ️ main is ${AHEAD} commit(s) ahead"
-
- # Check for existing cascade PR
- EXISTING=$(curl -sS \
- -H "Authorization: token ${GA_TOKEN}" \
- "${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1")
-
- EXISTING_COUNT=$(echo "$EXISTING" | jq 'length')
- PR_NUMBER=""
-
- if [ "$EXISTING_COUNT" -gt 0 ]; then
- PR_NUMBER=$(echo "$EXISTING" | jq -r '.[0].number')
- echo " ℹ️ Reusing existing PR #${PR_NUMBER}"
- else
- # Create cascade PR
- PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \
- -X POST \
- -H "Authorization: token ${GA_TOKEN}" \
- -H "Content-Type: application/json" \
- -d "{
- \"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\",
- \"body\": \"## Automatic cascade\\n\\nForward-merging \`main\` (${SHORT_SHA}) into \`${BRANCH}\`.\\n\\nIf conflicts exist, resolve manually and merge.\\n\\n> Auto-created by **Cascade Main → Dev**.\",
- \"head\": \"main\",
- \"base\": \"${BRANCH}\"
- }" \
- "${API}/pulls")
-
- HTTP_CODE=$(echo "$PR_RESPONSE" | tail -1)
- BODY=$(echo "$PR_RESPONSE" | sed '$d')
- PR_NUMBER=$(echo "$BODY" | jq -r '.number // empty')
-
- if [ "$HTTP_CODE" != "201" ] || [ -z "$PR_NUMBER" ]; then
- MSG=$(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1)
- echo " ❌ Failed to create PR (HTTP ${HTTP_CODE}): ${MSG}"
- FAILED=$((FAILED + 1))
- continue
- fi
-
- echo " ✅ Created PR #${PR_NUMBER}"
- fi
-
- # Try auto-merge
- PR_DATA=$(curl -sS \
- -H "Authorization: token ${GA_TOKEN}" \
- "${API}/pulls/${PR_NUMBER}")
-
- MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false')
-
- if [ "$MERGEABLE" != "true" ]; then
- echo " ⚠️ Conflicts — PR #${PR_NUMBER} left open"
- CONFLICTS=$((CONFLICTS + 1))
- continue
- fi
-
- MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \
- -X POST \
- -H "Authorization: token ${GA_TOKEN}" \
- -H "Content-Type: application/json" \
- -d "{
- \"Do\": \"merge\",
- \"merge_message_field\": \"chore: cascade main → ${BRANCH} [skip ci]\",
- \"delete_branch_after_merge\": false
- }" \
- "${API}/pulls/${PR_NUMBER}/merge")
-
- MERGE_HTTP=$(echo "$MERGE_RESPONSE" | tail -1)
-
- if [ "$MERGE_HTTP" = "200" ] || [ "$MERGE_HTTP" = "204" ]; then
- echo " ✅ Merged — ${BRANCH} is in sync"
- SUCCESS=$((SUCCESS + 1))
- else
- MERGE_BODY=$(echo "$MERGE_RESPONSE" | sed '$d')
- echo " ⚠️ Merge failed (HTTP ${MERGE_HTTP}) — PR #${PR_NUMBER} left open"
- CONFLICTS=$((CONFLICTS + 1))
- fi
- done
-
- # Summary
- echo ""
- echo "════════════════════════════════════════"
- echo " ✅ Merged: ${SUCCESS}"
- echo " ⚠️ Conflicts: ${CONFLICTS}"
- echo " ⏭️ Up to date: ${SKIPPED}"
- echo " ❌ Failed: ${FAILED}"
- echo "════════════════════════════════════════"
-
- if [ "$FAILED" -gt 0 ]; then
- exit 1
- fi
diff --git a/.mokogitea/ci-joomla.yml b/.mokogitea/ci-joomla.yml
deleted file mode 100644
index 28cee48..0000000
--- a/.mokogitea/ci-joomla.yml
+++ /dev/null
@@ -1,450 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# This file is part of a Moko Consulting project.
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow.Template
-# INGROUP: MokoStandards.CI
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
-# PATH: /templates/workflows/joomla/ci-joomla.yml.template
-# VERSION: 04.06.00
-# BRIEF: CI workflow for Joomla extensions — lint, validate, test
-
-name: Joomla Extension CI
-
-on:
- pull_request:
- branches:
- - main
- - 'dev/**'
- workflow_dispatch:
-
-permissions:
- contents: read
- pull-requests: write
-
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
-
-jobs:
- lint-and-validate:
- name: Lint & Validate
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
-
- - name: Setup PHP
- run: |
- php -v && composer --version
-
- - name: Clone MokoStandards
- env:
- GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
- MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
- MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
- run: |
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
- /tmp/mokostandards-api
-
- - name: Install dependencies
- env:
- COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
- run: |
- if [ -f "composer.json" ]; then
- composer install \
- --no-interaction \
- --prefer-dist \
- --optimize-autoloader
- else
- echo "No composer.json found — skipping dependency install"
- fi
-
- - name: PHP syntax check
- run: |
- ERRORS=0
- for DIR in src/ htdocs/; do
- if [ -d "$DIR" ]; then
- FOUND=1
- while IFS= read -r -d '' FILE; do
- OUTPUT=$(php -l "$FILE" 2>&1)
- if echo "$OUTPUT" | grep -q "Parse error"; then
- echo "::error file=${FILE}::${OUTPUT}"
- ERRORS=$((ERRORS + 1))
- fi
- done < <(find "$DIR" -name "*.php" -print0)
- fi
- done
- echo "### PHP Syntax Check" >> $GITHUB_STEP_SUMMARY
- if [ "${ERRORS}" -gt 0 ]; then
- echo "**${ERRORS} syntax error(s) found.**" >> $GITHUB_STEP_SUMMARY
- exit 1
- else
- echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
- fi
-
- - name: XML manifest validation
- run: |
- echo "### XML Manifest Validation" >> $GITHUB_STEP_SUMMARY
- ERRORS=0
-
- # Find the extension manifest (XML with /dev/null; then
- MANIFEST="$XML_FILE"
- break
- fi
- done
-
- if [ -z "$MANIFEST" ]; then
- echo "No Joomla extension manifest found (XML file with \`> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- else
- echo "Manifest found: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
-
- # Validate well-formed XML
- php -r "
- \$xml = @simplexml_load_file('$MANIFEST');
- if (\$xml === false) {
- echo 'INVALID';
- exit(1);
- }
- echo 'VALID';
- " > /tmp/xml_result 2>&1
- XML_RESULT=$(cat /tmp/xml_result)
- if [ "$XML_RESULT" != "VALID" ]; then
- echo "Manifest is not well-formed XML." >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- else
- echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY
- fi
-
- # Check required tags: name, version, author, namespace (Joomla 5+)
- for TAG in name version author namespace; do
- if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then
- echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- else
- echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
- fi
- done
- fi
-
- if [ "${ERRORS}" -gt 0 ]; then
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "**${ERRORS} manifest issue(s) found.**" >> $GITHUB_STEP_SUMMARY
- exit 1
- else
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY
- fi
-
- - name: Check language files referenced in manifest
- run: |
- echo "### Language File Check" >> $GITHUB_STEP_SUMMARY
- ERRORS=0
-
- MANIFEST=""
- for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
- if grep -q "/dev/null; then
- MANIFEST="$XML_FILE"
- break
- fi
- done
-
- if [ -n "$MANIFEST" ]; then
- # Extract language file references from manifest
- LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
- if [ -z "$LANG_FILES" ]; then
- echo "No language file references found in manifest — skipping." >> $GITHUB_STEP_SUMMARY
- else
- while IFS= read -r LANG_FILE; do
- LANG_FILE=$(echo "$LANG_FILE" | xargs)
- if [ -z "$LANG_FILE" ]; then
- continue
- fi
- # Check in common locations
- FOUND=0
- for BASE in "." "src" "htdocs"; do
- if [ -f "${BASE}/${LANG_FILE}" ]; then
- FOUND=1
- break
- fi
- done
- if [ "$FOUND" -eq 0 ]; then
- echo "Missing language file: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- else
- echo "Language file present: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY
- fi
- done <<< "$LANG_FILES"
- fi
- else
- echo "No manifest found — skipping language check." >> $GITHUB_STEP_SUMMARY
- fi
-
- if [ "${ERRORS}" -gt 0 ]; then
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "**${ERRORS} missing language file(s).**" >> $GITHUB_STEP_SUMMARY
- exit 1
- else
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "**Language file check passed.**" >> $GITHUB_STEP_SUMMARY
- fi
-
- - name: Check index.html files in directories
- run: |
- echo "### Index.html Check" >> $GITHUB_STEP_SUMMARY
- MISSING=0
- CHECKED=0
-
- for DIR in src/ htdocs/; do
- if [ -d "$DIR" ]; then
- while IFS= read -r -d '' SUBDIR; do
- CHECKED=$((CHECKED + 1))
- if [ ! -f "${SUBDIR}/index.html" ]; then
- echo "Missing index.html in: \`${SUBDIR}\`" >> $GITHUB_STEP_SUMMARY
- MISSING=$((MISSING + 1))
- fi
- done < <(find "$DIR" -type d -print0)
- fi
- done
-
- if [ "${CHECKED}" -eq 0 ]; then
- echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY
- elif [ "${MISSING}" -gt 0 ]; then
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY
- exit 1
- else
- echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY
- fi
-
- release-readiness:
- name: Release Readiness Check
- runs-on: ubuntu-latest
- if: github.event_name == 'pull_request' && github.base_ref == 'main'
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
-
- - name: Validate release readiness
- run: |
- echo "## Release Readiness" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- ERRORS=0
-
- # Extract version from README.md
- README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1)
- if [ -z "$README_VERSION" ]; then
- echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- else
- echo "README version: \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY
- fi
-
- # Find the extension manifest
- MANIFEST=""
- for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
- if grep -q "/dev/null; then
- MANIFEST="$XML_FILE"
- break
- fi
- done
-
- if [ -z "$MANIFEST" ]; then
- echo "No Joomla extension manifest found." >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- else
- echo "Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
-
- # Check matches README VERSION
- MANIFEST_VERSION=$(grep -oP '\K[^<]+' "$MANIFEST" | head -1)
- if [ -z "$MANIFEST_VERSION" ]; then
- echo "No \`\` tag in manifest." >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- elif [ -n "$README_VERSION" ] && [ "$MANIFEST_VERSION" != "$README_VERSION" ]; then
- echo "Manifest version \`${MANIFEST_VERSION}\` does not match README \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- else
- echo "Manifest version: \`${MANIFEST_VERSION}\`" >> $GITHUB_STEP_SUMMARY
- fi
-
- # Check extension type, element, client attributes
- EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1)
- if [ -z "$EXT_TYPE" ]; then
- echo "Missing \`type\` attribute on \`\` tag." >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- else
- echo "Extension type: \`${EXT_TYPE}\`" >> $GITHUB_STEP_SUMMARY
- fi
-
- # Element check (component/module/plugin name)
- HAS_ELEMENT=$(grep -cP '<(element|name)>' "$MANIFEST" 2>/dev/null || echo "0")
- if [ "$HAS_ELEMENT" -eq 0 ]; then
- echo "Missing \`\` or \`\` in manifest." >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- fi
-
- # Client attribute for site/admin modules and plugins
- if echo "$EXT_TYPE" | grep -qP "^(module|plugin)$"; then
- HAS_CLIENT=$(grep -cP ']*\bclient=' "$MANIFEST" 2>/dev/null || echo "0")
- if [ "$HAS_CLIENT" -eq 0 ]; then
- echo "Missing \`client\` attribute for ${EXT_TYPE} extension." >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- fi
- fi
- fi
-
- # Check updates.xml exists
- if [ -f "updates.xml" ] || [ -f "updates.xml" ]; then
- echo "Update XML present." >> $GITHUB_STEP_SUMMARY
- else
- echo "No updates.xml found." >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- fi
-
- # Check CHANGELOG.md exists
- if [ -f "CHANGELOG.md" ]; then
- echo "CHANGELOG.md present." >> $GITHUB_STEP_SUMMARY
- else
- echo "No CHANGELOG.md found." >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- fi
-
- echo "" >> $GITHUB_STEP_SUMMARY
- if [ $ERRORS -gt 0 ]; then
- echo "**${ERRORS} issue(s) must be resolved before release.**" >> $GITHUB_STEP_SUMMARY
- exit 1
- else
- echo "**Extension is ready for release.**" >> $GITHUB_STEP_SUMMARY
- fi
-
- test:
- name: Tests (PHP ${{ matrix.php }})
- runs-on: ubuntu-latest
- needs: lint-and-validate
-
- strategy:
- fail-fast: false
- matrix:
- php: ['8.2', '8.3']
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
-
- - name: Setup PHP ${{ matrix.php }}
- run: |
- php -v && composer --version
-
- - name: Install dependencies
- env:
- COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
- run: |
- if [ -f "composer.json" ]; then
- composer install \
- --no-interaction \
- --prefer-dist \
- --optimize-autoloader
- else
- echo "No composer.json found — skipping dependency install"
- fi
-
- - name: Run tests
- run: |
- echo "### Test Results (PHP ${{ matrix.php }})" >> $GITHUB_STEP_SUMMARY
- if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
- vendor/bin/phpunit --testdox 2>&1 | tee /tmp/test-output.log
- EXIT=${PIPESTATUS[0]}
- if [ $EXIT -eq 0 ]; then
- echo "All tests passed." >> $GITHUB_STEP_SUMMARY
- else
- echo "Test failures detected — see log." >> $GITHUB_STEP_SUMMARY
- echo '```' >> $GITHUB_STEP_SUMMARY
- cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY
- echo '```' >> $GITHUB_STEP_SUMMARY
- fi
- exit $EXIT
- else
- echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY
- fi
-
- static-analysis:
- name: PHPStan Analysis
- runs-on: ubuntu-latest
- needs: lint-and-validate
- continue-on-error: true
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
-
- - name: Setup PHP
- run: php -v && composer --version
-
- - name: Install dependencies
- env:
- COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
- run: |
- if [ -f "composer.json" ]; then
- composer install --no-interaction --prefer-dist --optimize-autoloader
- fi
-
- - name: Install PHPStan
- run: |
- if ! command -v vendor/bin/phpstan &> /dev/null; then
- composer require --dev phpstan/phpstan --no-interaction 2>/dev/null || \
- composer global require phpstan/phpstan --no-interaction
- fi
-
- - name: Run PHPStan
- run: |
- echo "### PHPStan Static Analysis" >> $GITHUB_STEP_SUMMARY
- PHPSTAN="vendor/bin/phpstan"
- if [ ! -f "$PHPSTAN" ]; then
- PHPSTAN=$(composer global config bin-dir --absolute 2>/dev/null)/phpstan
- fi
-
- # Determine source directory
- SRC_DIR=""
- for DIR in src/ htdocs/ lib/; do
- if [ -d "$DIR" ]; then
- SRC_DIR="$DIR"
- break
- fi
- done
-
- if [ -z "$SRC_DIR" ]; then
- echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY
- exit 0
- fi
-
- # Use repo phpstan.neon if present, otherwise use baseline config
- ARGS="analyse ${SRC_DIR} --memory-limit=512M --no-progress --error-format=table"
- if [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ]; then
- echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY
- else
- ARGS="$ARGS --level=3"
- echo "No phpstan.neon found — using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY
- fi
-
- $PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt
- EXIT=${PIPESTATUS[0]}
-
- if [ $EXIT -eq 0 ]; then
- echo "**No errors found.**" >> $GITHUB_STEP_SUMMARY
- else
- ERRORS=$(grep -c "ERROR" /tmp/phpstan-output.txt 2>/dev/null || echo "some")
- echo "**${ERRORS} error(s) found.** Review output above." >> $GITHUB_STEP_SUMMARY
- echo '```' >> $GITHUB_STEP_SUMMARY
- tail -30 /tmp/phpstan-output.txt >> $GITHUB_STEP_SUMMARY
- echo '```' >> $GITHUB_STEP_SUMMARY
- fi
- exit $EXIT
diff --git a/.mokogitea/cleanup.yml b/.mokogitea/cleanup.yml
deleted file mode 100644
index 78aa0c3..0000000
--- a/.mokogitea/cleanup.yml
+++ /dev/null
@@ -1,87 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: MokoStandards.Maintenance
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
-# PATH: /.gitea/workflows/cleanup.yml
-# VERSION: 01.00.00
-# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
-
-name: Repository Cleanup
-
-on:
- schedule:
- - cron: '0 3 * * 0' # Weekly on Sunday at 03:00 UTC
- workflow_dispatch:
-
-permissions:
- contents: write
-
-env:
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
-
-jobs:
- cleanup:
- name: Clean Merged Branches
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
- token: ${{ secrets.GA_TOKEN }}
-
- - name: Delete merged branches
- env:
- GA_TOKEN: ${{ secrets.GA_TOKEN }}
- run: |
- echo "=== Merged Branch Cleanup ==="
- API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
-
- # List branches via API
- BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
- "${API}/branches?limit=50" | jq -r '.[].name')
-
- DELETED=0
- for BRANCH in $BRANCHES; do
- # Skip protected branches
- case "$BRANCH" in
- main|master|develop|release/*|hotfix/*) continue ;;
- esac
-
- # Check if branch is merged into main
- if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
- echo " Deleting merged branch: ${BRANCH}"
- curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
- "${API}/branches/${BRANCH}" 2>/dev/null || true
- DELETED=$((DELETED + 1))
- fi
- done
-
- echo "Deleted ${DELETED} merged branch(es)"
-
- - name: Clean old workflow runs
- env:
- GA_TOKEN: ${{ secrets.GA_TOKEN }}
- run: |
- echo "=== Workflow Run Cleanup ==="
- API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
- CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
-
- # Get old completed runs
- RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
- "${API}/actions/runs?status=completed&limit=50" | \
- jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
-
- DELETED=0
- for RUN_ID in $RUNS; do
- curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
- "${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
- DELETED=$((DELETED + 1))
- done
-
- echo "Deleted ${DELETED} old workflow run(s)"
diff --git a/.mokogitea/deploy-manual.yml b/.mokogitea/deploy-manual.yml
deleted file mode 100644
index a81cfa5..0000000
--- a/.mokogitea/deploy-manual.yml
+++ /dev/null
@@ -1,126 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: MokoStandards.Deploy
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
-# PATH: /templates/workflows/joomla/deploy-manual.yml.template
-# VERSION: 04.07.00
-# BRIEF: Manual SFTP deploy to dev server for Joomla repos
-
-name: Deploy to Dev (Manual)
-
-on:
- workflow_dispatch:
- inputs:
- clear_remote:
- description: 'Delete all remote files before uploading'
- required: false
- default: 'false'
- type: boolean
-
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
-
-permissions:
- contents: read
-
-jobs:
- deploy:
- name: SFTP Deploy to Dev
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
-
- - name: Setup PHP
- run: |
- php -v && composer --version
-
- - name: Setup MokoStandards tools
- env:
- GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
- MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
- MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
- COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
- run: |
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
- /tmp/mokostandards-api 2>/dev/null || true
- if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
- cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
- fi
-
- - name: Check FTP configuration
- id: check
- env:
- HOST: ${{ vars.DEV_FTP_HOST }}
- PATH_VAR: ${{ vars.DEV_FTP_PATH }}
- PORT: ${{ vars.DEV_FTP_PORT }}
- run: |
- if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
- echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
- echo "skip=true" >> "$GITHUB_OUTPUT"
- exit 0
- fi
- echo "skip=false" >> "$GITHUB_OUTPUT"
- echo "host=$HOST" >> "$GITHUB_OUTPUT"
-
- REMOTE="${PATH_VAR%/}"
- echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
-
- [ -z "$PORT" ] && PORT="22"
- echo "port=$PORT" >> "$GITHUB_OUTPUT"
-
- - name: Deploy via SFTP
- if: steps.check.outputs.skip != 'true'
- env:
- SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
- SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
- SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
- run: |
- SOURCE_DIR="src"
- [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
- [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
-
- printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
- "${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
- > /tmp/sftp-config.json
-
- if [ -n "$SFTP_KEY" ]; then
- echo "$SFTP_KEY" > /tmp/deploy_key
- chmod 600 /tmp/deploy_key
- printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
- else
- printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
- fi
-
- DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
- [ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
-
- PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
- if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
- php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
- else
- php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
- fi
-
- rm -f /tmp/deploy_key /tmp/sftp-config.json
-
- - name: Summary
- if: always()
- run: |
- if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
- echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
- else
- echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
- echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
- echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
- fi
diff --git a/.mokogitea/gitleaks.yml b/.mokogitea/gitleaks.yml
deleted file mode 100644
index b29f881..0000000
--- a/.mokogitea/gitleaks.yml
+++ /dev/null
@@ -1,96 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: MokoStandards.Security
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
-# PATH: /templates/workflows/gitleaks.yml.template
-# VERSION: 01.00.00
-# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
-#
-# +========================================================================+
-# | SECRET SCANNING |
-# +========================================================================+
-# | |
-# | Scans commits for leaked secrets using Gitleaks. |
-# | |
-# | - PR scan: only new commits in the PR |
-# | - Scheduled: full repo scan weekly |
-# | - Alerts via ntfy on findings |
-# | |
-# +========================================================================+
-
-name: Secret Scanning
-
-on:
- pull_request:
- branches:
- - main
- - 'dev/**'
- schedule:
- - cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
- workflow_dispatch:
-
-permissions:
- contents: read
-
-env:
- NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
- NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
-
-jobs:
- gitleaks:
- name: Gitleaks Secret Scan
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Install Gitleaks
- run: |
- GITLEAKS_VERSION="8.21.2"
- curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
- | tar -xz -C /usr/local/bin gitleaks
- gitleaks version
-
- - name: Scan for secrets
- id: scan
- run: |
- echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY
- ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json"
-
- if [ "${{ github.event_name }}" = "pull_request" ]; then
- # Scan only PR commits
- ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
- echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY
- else
- echo "Full repository scan" >> $GITHUB_STEP_SUMMARY
- fi
-
- if gitleaks detect $ARGS 2>&1; then
- echo "result=clean" >> "$GITHUB_OUTPUT"
- echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY
- else
- echo "result=found" >> "$GITHUB_OUTPUT"
- FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown")
- echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY
- exit 1
- fi
-
- - name: Notify on findings
- if: failure() && steps.scan.outputs.result == 'found'
- run: |
- REPO="${{ github.event.repository.name }}"
- curl -sS \
- -H "Title: ${REPO} — secrets detected in code" \
- -H "Tags: rotating_light,key" \
- -H "Priority: urgent" \
- -d "Gitleaks found potential secrets. Review and rotate credentials immediately." \
- "${NTFY_URL}/${NTFY_TOPIC}" || true
diff --git a/.mokogitea/notify.yml b/.mokogitea/notify.yml
deleted file mode 100644
index 8cc8382..0000000
--- a/.mokogitea/notify.yml
+++ /dev/null
@@ -1,71 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: MokoStandards.Notifications
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
-# PATH: /.gitea/workflows/notify.yml
-# VERSION: 01.00.00
-# BRIEF: Push notifications via ntfy on release success or workflow failure
-
-name: Notifications
-
-on:
- workflow_run:
- workflows:
- - "Joomla Build & Release"
- - "Joomla Extension CI"
- - "Deploy"
- - "Cascade Main → Dev"
- types:
- - completed
-
-permissions:
- contents: read
-
-env:
- NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
- NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }}
-
-jobs:
- notify:
- name: Send Notification
- runs-on: ubuntu-latest
- if: >-
- github.event.workflow_run.conclusion == 'success' ||
- github.event.workflow_run.conclusion == 'failure'
-
- steps:
- - name: Notify on success (releases only)
- if: >-
- github.event.workflow_run.conclusion == 'success' &&
- contains(github.event.workflow_run.name, 'Release')
- run: |
- REPO="${{ github.event.repository.name }}"
- WORKFLOW="${{ github.event.workflow_run.name }}"
- URL="${{ github.event.workflow_run.html_url }}"
-
- curl -sS \
- -H "Title: ${REPO} released" \
- -H "Tags: white_check_mark,package" \
- -H "Priority: default" \
- -H "Click: ${URL}" \
- -d "${WORKFLOW} completed successfully." \
- "${NTFY_URL}/${NTFY_TOPIC}"
-
- - name: Notify on failure
- if: github.event.workflow_run.conclusion == 'failure'
- run: |
- REPO="${{ github.event.repository.name }}"
- WORKFLOW="${{ github.event.workflow_run.name }}"
- URL="${{ github.event.workflow_run.html_url }}"
-
- curl -sS \
- -H "Title: ${REPO} workflow failed" \
- -H "Tags: x,warning" \
- -H "Priority: high" \
- -H "Click: ${URL}" \
- -d "${WORKFLOW} failed. Check the run for details." \
- "${NTFY_URL}/${NTFY_TOPIC}"
diff --git a/.mokogitea/pr-branch-check.yml b/.mokogitea/pr-branch-check.yml
deleted file mode 100644
index b8d9742..0000000
--- a/.mokogitea/pr-branch-check.yml
+++ /dev/null
@@ -1,90 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# Enforces branch merge policy:
-# feature/* → dev only
-# fix/* → dev only
-# hotfix/* → dev or main (emergency)
-# dev → main only
-# alpha/* → dev only
-# beta/* → dev only
-# rc/* → main only
-
-name: Branch Policy Check
-
-on:
- pull_request:
- types: [opened, synchronize, reopened, edited]
-
-jobs:
- check-target:
- name: Verify merge target
- runs-on: ubuntu-latest
- steps:
- - name: Check branch policy
- run: |
- HEAD="${{ github.head_ref }}"
- BASE="${{ github.base_ref }}"
-
- echo "PR: ${HEAD} → ${BASE}"
-
- ALLOWED=true
- REASON=""
-
- case "$HEAD" in
- feature/*|feat/*)
- if [ "$BASE" != "dev" ]; then
- ALLOWED=false
- REASON="Feature branches must target 'dev', not '${BASE}'"
- fi
- ;;
- fix/*|bugfix/*)
- if [ "$BASE" != "dev" ]; then
- ALLOWED=false
- REASON="Fix branches must target 'dev', not '${BASE}'"
- fi
- ;;
- hotfix/*)
- if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
- ALLOWED=false
- REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
- fi
- ;;
- alpha/*|beta/*)
- if [ "$BASE" != "dev" ]; then
- ALLOWED=false
- REASON="Pre-release branches must target 'dev', not '${BASE}'"
- fi
- ;;
- rc/*)
- if [ "$BASE" != "main" ]; then
- ALLOWED=false
- REASON="Release candidate branches must target 'main', not '${BASE}'"
- fi
- ;;
- dev)
- if [ "$BASE" != "main" ]; then
- ALLOWED=false
- REASON="Dev branch can only merge into 'main', not '${BASE}'"
- fi
- ;;
- esac
-
- if [ "$ALLOWED" = false ]; then
- echo "::error::${REASON}"
- echo ""
- echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "${REASON}" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
- echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
- exit 1
- fi
-
- echo "Branch policy: OK (${HEAD} → ${BASE})"
- echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
diff --git a/.mokogitea/pr-check.yml b/.mokogitea/pr-check.yml
deleted file mode 100644
index 0220500..0000000
--- a/.mokogitea/pr-check.yml
+++ /dev/null
@@ -1,106 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: MokoStandards.CI
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
-# PATH: /.gitea/workflows/pr-check.yml
-# VERSION: 01.00.00
-# BRIEF: PR gate — validates code quality and manifest before merge to main
-
-name: PR Check
-
-on:
- pull_request:
- branches:
- - main
- types: [opened, synchronize, reopened]
-
-permissions:
- contents: read
- pull-requests: write
-
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
-
-jobs:
- validate:
- name: Validate PR
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - 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 >/dev/null 2>&1
- fi
-
- - name: PHP syntax check
- run: |
- echo "=== PHP Lint ==="
- ERRORS=0
- while IFS= read -r -d '' file; do
- if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
- ERRORS=$((ERRORS + 1))
- fi
- done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
- echo "Checked files, errors: ${ERRORS}"
- [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
-
- - name: Validate Joomla manifest
- run: |
- echo "=== Manifest Validation ==="
- MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1)
- if [ -z "$MANIFEST" ]; then
- echo "::warning::No Joomla manifest found"
- exit 0
- fi
- echo "Manifest: ${MANIFEST}"
-
- # Check well-formed XML
- if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}"; then
- echo "::error::Manifest XML is malformed"
- exit 1
- fi
-
- # Check required elements
- for ELEMENT in name version description; do
- if ! grep -q "<${ELEMENT}>" "$MANIFEST"; then
- echo "::error::Missing <${ELEMENT}> in manifest"
- exit 1
- fi
- done
- echo "Manifest valid"
-
- - name: Check updates.xml format
- run: |
- if [ ! -f "updates.xml" ]; then
- echo "No updates.xml — skipping"
- exit 0
- fi
- echo "=== updates.xml Validation ==="
- if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}"; then
- echo "::error::updates.xml is malformed"
- exit 1
- fi
- echo "updates.xml valid"
-
- - name: Verify package builds
- run: |
- echo "=== Package Build Test ==="
- SOURCE_DIR="src"
- [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
- if [ ! -d "$SOURCE_DIR" ]; then
- echo "::warning::No src/ or htdocs/ directory"
- exit 0
- fi
- # Dry-run: ensure zip would succeed
- FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
- echo "Source contains ${FILE_COUNT} files — package will build"
- [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
diff --git a/.mokogitea/pre-release.yml b/.mokogitea/pre-release.yml
deleted file mode 100644
index 30c9bcf..0000000
--- a/.mokogitea/pre-release.yml
+++ /dev/null
@@ -1,341 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: MokoStandards.Release
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
-# PATH: /.gitea/workflows/pre-release.yml
-# VERSION: 01.00.00
-# BRIEF: Manual pre-release — builds dev/alpha/beta/rc packages from any branch
-
-name: Pre-Release
-
-on:
- workflow_dispatch:
- inputs:
- stability:
- description: 'Pre-release channel'
- required: true
- type: choice
- options:
- - development
- - alpha
- - beta
- - release-candidate
-
-permissions:
- contents: write
-
-env:
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
- GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
-
-jobs:
- build:
- name: "Build Pre-Release (${{ inputs.stability }})"
- runs-on: release
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
- token: ${{ secrets.GA_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 >/dev/null 2>&1
- fi
-
- - name: Resolve metadata
- id: meta
- run: |
- STABILITY="${{ inputs.stability }}"
-
- case "$STABILITY" in
- development) SUFFIX="-dev"; TAG="development" ;;
- alpha) SUFFIX="-alpha"; TAG="alpha" ;;
- beta) SUFFIX="-beta"; TAG="beta" ;;
- release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
- esac
-
- # Read and bump patch version (with rollover)
- CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
- [ -z "$CURRENT" ] && CURRENT="00.00.00"
-
- MAJOR=$(echo "$CURRENT" | cut -d. -f1)
- MINOR=$(echo "$CURRENT" | cut -d. -f2)
- PATCH=$(echo "$CURRENT" | cut -d. -f3)
-
- # Patch bump with rollover: ZZ=99 → bump minor, YY=99 → bump major
- NEW_PATCH=$((10#$PATCH + 1))
- NEW_MINOR=$((10#$MINOR))
- NEW_MAJOR=$((10#$MAJOR))
-
- if [ $NEW_PATCH -gt 99 ]; then
- NEW_PATCH=0
- NEW_MINOR=$((NEW_MINOR + 1))
- fi
- if [ $NEW_MINOR -gt 99 ]; then
- NEW_MINOR=0
- NEW_MAJOR=$((NEW_MAJOR + 1))
- fi
-
- VERSION=$(printf "%02d.%02d.%02d" $NEW_MAJOR $NEW_MINOR $NEW_PATCH)
- TODAY=$(date +%Y-%m-%d)
-
- echo "Bumping: ${CURRENT} → ${VERSION} (patch)"
-
- # Update README.md
- sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${VERSION}/" README.md
-
- # Update manifest
- MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1)
- if [ -n "$MANIFEST" ]; then
- MANIFEST_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
- sed -i "s|${MANIFEST_VER}|${VERSION}|" "$MANIFEST"
- sed -i "s|[^<]*|${TODAY}|" "$MANIFEST"
- fi
-
- # Commit version bump
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- git add -A
- git diff --cached --quiet || {
- git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]"
- git push origin HEAD 2>&1
- }
-
- # Auto-detect element from manifest
- MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1)
- EXT_ELEMENT=""
- if [ -n "$MANIFEST" ]; then
- EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
- if [ -z "$EXT_ELEMENT" ]; then
- EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
- case "$EXT_ELEMENT" in
- templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
- esac
- fi
- else
- EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
- fi
-
- ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
-
- echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
- echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
- echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
- echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
- echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
- echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
-
- echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
-
- - name: Build package
- run: |
- SOURCE_DIR="src"
- [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
- if [ ! -d "$SOURCE_DIR" ]; then
- echo "::error::No src/ or htdocs/ directory"
- exit 1
- fi
-
- mkdir -p build/package
- rsync -a \
- --exclude='sftp-config*' \
- --exclude='.ftpignore' \
- --exclude='*.ppk' \
- --exclude='*.pem' \
- --exclude='*.key' \
- --exclude='.env*' \
- --exclude='*.local' \
- --exclude='.build-trigger' \
- "${SOURCE_DIR}/" build/package/
-
- - name: Create ZIP
- id: zip
- run: |
- ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
- cd build/package
- zip -r "../${ZIP_NAME}" .
- cd ..
-
- SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1)
- echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
- echo "ZIP: ${ZIP_NAME} (SHA: ${SHA256:0:16}...)"
-
- - name: Create or replace Gitea release
- id: release
- run: |
- TAG="${{ steps.meta.outputs.tag }}"
- VERSION="${{ steps.meta.outputs.version }}"
- STABILITY="${{ steps.meta.outputs.stability }}"
- SHA256="${{ steps.zip.outputs.sha256 }}"
- ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
- EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}"
- TOKEN="${{ secrets.GA_TOKEN }}"
- API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- BRANCH=$(git branch --show-current)
-
- BODY="## ${VERSION} ($(date +%Y-%m-%d))
- **Channel:** ${STABILITY}
- **SHA-256:** \`${SHA256}\`"
-
- # Delete existing release
- 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 "@build/${ZIP_NAME}"
-
- echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})"
-
- - name: Update updates.xml
- run: |
- STABILITY="${{ steps.meta.outputs.stability }}"
- VERSION="${{ steps.meta.outputs.version }}"
- SHA256="${{ steps.zip.outputs.sha256 }}"
- ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
- TAG="${{ steps.meta.outputs.tag }}"
- DATE=$(date +%Y-%m-%d)
-
- if [ ! -f "updates.xml" ]; then
- echo "No updates.xml — skipping"
- exit 0
- fi
-
- export PY_STABILITY="$STABILITY" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \
- PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \
- PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO"
- python3 << 'PYEOF'
- import re, os
-
- stability = os.environ["PY_STABILITY"]
- version = os.environ["PY_VERSION"]
- sha256 = os.environ["PY_SHA256"]
- zip_name = os.environ["PY_ZIP_NAME"]
- tag = os.environ["PY_TAG"]
- date = os.environ["PY_DATE"]
- gitea_org = os.environ["PY_GITEA_ORG"]
- gitea_repo = os.environ["PY_GITEA_REPO"]
- download_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}"
-
- with open("updates.xml", "r") as f:
- content = f.read()
-
- # Map stability to XML tag name
- tag_map = {"development": "development", "alpha": "alpha", "beta": "beta", "release-candidate": "rc"}
- xml_tag = tag_map.get(stability, stability)
-
- pattern = r"((?:(?!).)*?" + re.escape(xml_tag) + r".*?)"
- match = re.search(pattern, content, re.DOTALL)
- if match:
- block = match.group(1)
- updated = re.sub(r"[^<]*", f"{version}", block)
- updated = re.sub(r"[^<]*", f"{date}", updated)
- if "" in updated:
- updated = re.sub(r"[^<]*", f"{sha256}", updated)
- else:
- updated = updated.replace("", f"\n {sha256}")
- updated = re.sub(r"(]*>)[^<]*()", rf"\g<1>{download_url}\g<2>", updated)
- content = content.replace(block, updated)
- print(f"Updated {xml_tag} channel: version={version}")
- else:
- print(f"WARNING: No {xml_tag} block in updates.xml")
-
- with open("updates.xml", "w") as f:
- f.write(content)
- PYEOF
-
- # Commit and push to current branch
- if ! git diff --quiet updates.xml 2>/dev/null; then
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git add updates.xml
- git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
- git push origin HEAD 2>&1 || echo "WARNING: push failed"
- fi
-
- - name: "Sync updates.xml to all branches"
- run: |
- CURRENT_BRANCH="${{ github.ref_name }}"
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
-
- # Sync updates.xml to main and dev (whichever isn't current)
- 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}" -- . 2>/dev/null || continue
- git checkout "${CURRENT_BRANCH}" -- updates.xml
- if ! git diff --quiet updates.xml 2>/dev/null; then
- git add updates.xml
- git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
- git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
- fi
- git checkout "${CURRENT_BRANCH}" 2>/dev/null
- done
-
- - name: "Delete lesser pre-release channels (cascade)"
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- TOKEN="${{ secrets.GA_TOKEN }}"
- STABILITY="${{ steps.meta.outputs.stability }}"
-
- # Cascade: rc → beta,alpha,dev | beta → alpha,dev | alpha → dev | dev → nothing
- case "$STABILITY" in
- release-candidate) TAGS_TO_DELETE="beta alpha development" ;;
- beta) TAGS_TO_DELETE="alpha development" ;;
- alpha) TAGS_TO_DELETE="development" ;;
- *) TAGS_TO_DELETE="" ;;
- esac
-
- [ -z "$TAGS_TO_DELETE" ] && exit 0
-
- 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
diff --git a/.mokogitea/release.yml b/.mokogitea/release.yml
deleted file mode 100644
index 07d1b24..0000000
--- a/.mokogitea/release.yml
+++ /dev/null
@@ -1,600 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# This file is part of a Moko Consulting project.
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: MokoStandards.Joomla
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
-# PATH: /.gitea/workflows/release.yml
-# VERSION: 02.00.00
-# BRIEF: Generic Joomla release — auto-detects element from manifest, stream tags, cascade
-
-name: Create Release
-
-on:
- push:
- tags:
- - 'stable'
- - 'release-candidate'
- - 'beta'
- - 'alpha'
- - 'development'
- workflow_dispatch:
- inputs:
- stability:
- description: 'Stability tag'
- required: true
- default: 'stable'
- type: choice
- options:
- - stable
- - release-candidate
- - beta
- - alpha
- - development
-
-permissions:
- contents: write
-
-env:
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
- GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
-
-jobs:
- build:
- name: Build Release Package
- runs-on: release
-
- steps:
- # Always checkout main for tag triggers (avoids detached HEAD).
- # For workflow_dispatch, checkout whatever branch was selected.
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- ref: ${{ github.event_name == 'push' && 'main' || github.ref }}
- fetch-depth: 0
- token: ${{ secrets.GA_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 composer >/dev/null 2>&1
- fi
- echo "PHP: $(php -v | head -1)"
- echo "Composer: $(composer --version 2>&1 | head -1)"
-
- - name: Get version and stability
- id: meta
- run: |
- echo "=== Meta ==="
- echo "event_name: ${{ github.event_name }}"
- echo "ref: ${{ github.ref }}"
- echo "ref_name: ${{ github.ref_name }}"
- echo "sha: ${{ github.sha }}"
-
- # Derive stability from tag name or dispatch input
- if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
- STABILITY="${{ inputs.stability }}"
- else
- TAG_PUSHED="${GITHUB_REF#refs/tags/}"
- case "$TAG_PUSHED" in
- stable) STABILITY="stable" ;;
- release-candidate) STABILITY="rc" ;;
- beta) STABILITY="beta" ;;
- alpha) STABILITY="alpha" ;;
- development) STABILITY="development" ;;
- *) STABILITY="stable" ;;
- esac
- fi
-
- # Read version from README.md (will be bumped in next step)
- VERSION=$(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)
- [ -z "$VERSION" ] && VERSION="00.00.00"
-
- # Auto-detect extension element from Joomla manifest
- # Search depth 3 covers src/admin/com_xxx.xml and similar nested structures
- MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '/dev/null | head -1)
- EXT_ELEMENT=""
- if [ -n "$MANIFEST" ]; then
- EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
- # If no tag, derive from manifest filename or repo name
- if [ -z "$EXT_ELEMENT" ]; then
- EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
- case "$EXT_ELEMENT" in
- templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
- esac
- fi
- echo "Manifest: ${MANIFEST}, element: ${EXT_ELEMENT}"
- else
- EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
- echo "No manifest found, using repo name: ${EXT_ELEMENT}"
- fi
-
- case "$STABILITY" in
- development) SUFFIX="-dev"; TAG_NAME="development" ;;
- alpha) SUFFIX="-alpha"; TAG_NAME="alpha" ;;
- beta) SUFFIX="-beta"; TAG_NAME="beta" ;;
- rc) SUFFIX="-rc"; TAG_NAME="release-candidate" ;;
- stable) SUFFIX=""; TAG_NAME="stable" ;;
- *) SUFFIX="-dev"; TAG_NAME="development" ;;
- esac
-
- PRERELEASE="true"
- [ "$STABILITY" = "stable" ] && PRERELEASE="false"
-
- ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
-
- echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
- echo "prerelease=${PRERELEASE}" >> "$GITHUB_OUTPUT"
- echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
- echo "tag_name=${TAG_NAME}" >> "$GITHUB_OUTPUT"
- echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
- echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
-
- echo "=== Resolved ==="
- echo "VERSION=${VERSION}"
- echo "STABILITY=${STABILITY}"
- echo "TAG_NAME=${TAG_NAME}"
- echo "ZIP_NAME=${ZIP_NAME}"
- echo "Branch: $(git branch --show-current)"
-
- - name: Auto-bump patch version
- id: bump
- env:
- GA_TOKEN: ${{ secrets.GA_TOKEN }}
- INPUT_VERSION: ${{ steps.meta.outputs.version }}
- INPUT_STABILITY: ${{ steps.meta.outputs.stability }}
- INPUT_SUFFIX: ${{ steps.meta.outputs.suffix }}
- EXT_ELEMENT: ${{ steps.meta.outputs.ext_element }}
- run: |
- BRANCH=$(git branch --show-current)
- GITEA_API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
-
- echo "=== Version Bump ==="
- echo "On branch: ${BRANCH}"
-
- # Read current version from README.md
- CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
- echo "Current version in README: ${CURRENT}"
-
- if [ -z "$CURRENT" ]; then
- echo "No VERSION in README.md — using input version"
- echo "version=${INPUT_VERSION}" >> "$GITHUB_OUTPUT"
- echo "zip_name=${EXT_ELEMENT}-${INPUT_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT"
- exit 0
- fi
-
- # Bump patch: XX.YY.ZZ → XX.YY.(ZZ+1)
- MAJOR=$(echo "$CURRENT" | cut -d. -f1)
- MINOR=$(echo "$CURRENT" | cut -d. -f2)
- PATCH=$(echo "$CURRENT" | cut -d. -f3)
- NEW_PATCH=$(printf "%02d" $((10#$PATCH + 1)))
- NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}"
- TODAY=$(date +%Y-%m-%d)
-
- echo "Bumping: ${CURRENT} → ${NEW_VERSION} (date: ${TODAY})"
-
- # Update README.md
- sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${NEW_VERSION}/" README.md
-
- # Update manifest (templateDetails.xml / *.xml with )
- MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '/dev/null | head -1)
- if [ -n "$MANIFEST" ]; then
- echo "Manifest: ${MANIFEST}"
- sed -i "s|${CURRENT}|${NEW_VERSION}|" "$MANIFEST"
- sed -i "s|[^<]*|${TODAY}|" "$MANIFEST"
- fi
-
- # Update matching stability channel in updates.xml
- if [ -f "updates.xml" ]; then
- export PY_OLD="$CURRENT" PY_NEW="$NEW_VERSION" PY_STABILITY="$INPUT_STABILITY" PY_DATE="$TODAY"
- python3 << 'PYEOF'
- import re, os
- old = os.environ["PY_OLD"]
- new = os.environ["PY_NEW"]
- stability = os.environ["PY_STABILITY"]
- date = os.environ["PY_DATE"]
- with open("updates.xml") as f:
- content = f.read()
- pattern = r"((?:(?!).)*?" + re.escape(stability) + r".*?)"
- match = re.search(pattern, content, re.DOTALL)
- if match:
- block = match.group(1)
- updated = block.replace(old, new)
- updated = re.sub(r"[^<]*", f"{date}", updated)
- content = content.replace(block, updated)
- print(f"Updated {stability} channel: {old} -> {new}")
- else:
- print(f"WARNING: No block found for {stability}")
- with open("updates.xml", "w") as f:
- f.write(content)
- PYEOF
- fi
-
- # Commit and push version bump
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git remote set-url origin "https://jmiller:${GA_TOKEN}@git.mokoconsulting.tech/${{ github.repository }}.git"
- git add -A
- git diff --cached --quiet && echo "No changes to commit" || {
- git commit -m "chore(version): bump ${CURRENT} → ${NEW_VERSION} [skip ci]" \
- --author="gitea-actions[bot] "
- echo "Pushing version bump to ${BRANCH}..."
- git push origin HEAD:${BRANCH} 2>&1
- echo "Push exit code: $?"
- }
-
- # For stable releases from non-main: merge to main via Gitea API
- if [ "$INPUT_STABILITY" = "stable" ] && [ "$BRANCH" != "main" ]; then
- echo "Merging ${BRANCH} → main via Gitea API..."
- HTTP_CODE=$(curl -sS -o /tmp/merge_response.json -w "%{http_code}" \
- -X POST -H "Authorization: token ${GA_TOKEN}" \
- -H "Content-Type: application/json" \
- "${GITEA_API}/merges" \
- -d "$(jq -n \
- --arg base "main" \
- --arg head "${BRANCH}" \
- --arg msg "chore(release): merge ${BRANCH} for stable ${NEW_VERSION} [skip ci]" \
- '{base: $base, head: $head, merge_message_field: $msg}'
- )")
- echo "Merge response (HTTP ${HTTP_CODE}):"
- cat /tmp/merge_response.json 2>/dev/null; echo
- fi
-
- echo "version=${NEW_VERSION}" >> "$GITHUB_OUTPUT"
- echo "zip_name=${EXT_ELEMENT}-${NEW_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT"
- echo "=== Bump complete: ${NEW_VERSION} ==="
-
- - name: Install dependencies
- env:
- COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}'
- run: |
- if [ -f "composer.json" ]; then
- echo "Installing composer dependencies..."
- composer install --no-dev --optimize-autoloader --no-interaction 2>&1
- else
- echo "No composer.json — skipping"
- fi
-
- - name: Setup Node.js
- uses: actions/setup-node@v4
- with:
- node-version: '20'
-
- - name: Minify CSS and JS
- run: |
- if [ -f "package.json" ] && [ -f "scripts/minify.js" ]; then
- npm ci --ignore-scripts
- node scripts/minify.js
- else
- echo "No minify setup — skipping"
- fi
-
- - name: Create package
- run: |
- # Detect source directory (src/ or htdocs/)
- SOURCE_DIR="src"
- [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
- if [ ! -d "$SOURCE_DIR" ]; then
- echo "::error::No src/ or htdocs/ directory found"
- exit 1
- fi
- echo "Source directory: ${SOURCE_DIR}"
-
- mkdir -p build/package
- rsync -av \
- --exclude='sftp-config*' \
- --exclude='.ftpignore' \
- --exclude='*.ppk' \
- --exclude='*.pem' \
- --exclude='*.key' \
- --exclude='.env*' \
- --exclude='*.local' \
- --exclude='.build-trigger' \
- --exclude='.beta-trigger' \
- --exclude='.rc-trigger' \
- "${SOURCE_DIR}/" build/package/
- echo "Package contents:"
- ls -la build/package/ | head -20
-
- - name: Build ZIP
- id: zip
- run: |
- ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
- echo "Building: ${ZIP_NAME}"
- cd build/package
- zip -r "../${ZIP_NAME}" .
- cd ..
-
- SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1)
- SIZE=$(stat -c%s "${ZIP_NAME}")
-
- echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
- echo "size=${SIZE}" >> "$GITHUB_OUTPUT"
- echo "=== Package Built ==="
- echo "ZIP: ${ZIP_NAME}"
- echo "SHA-256: ${SHA256}"
- echo "Size: ${SIZE} bytes"
-
- # ── Gitea Release (PRIMARY) ─────────────────────────���────────────
- - name: "Gitea: Create or update release"
- id: gitea_release
- env:
- EXT_ELEMENT: ${{ steps.meta.outputs.ext_element }}
- run: |
- TAG="${{ steps.meta.outputs.tag_name }}"
- VERSION="${{ steps.bump.outputs.version }}"
- STABILITY="${{ steps.meta.outputs.stability }}"
- SHA256="${{ steps.zip.outputs.sha256 }}"
- TOKEN="${{ secrets.GA_TOKEN }}"
- API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- BRANCH=$(git branch --show-current)
- MAX_HISTORY=5
-
- IS_PRE="true"
- [ "$STABILITY" = "stable" ] && IS_PRE="false"
-
- # Build this version's entry
- NEW_ENTRY="## ${VERSION} ($(date +%Y-%m-%d))
- **SHA-256:** \`${SHA256}\`"
-
- if [ -f "CHANGELOG.md" ]; then
- NOTES=$(awk "/## \[${VERSION}\]/,/## \[/{if(/## \[${VERSION}\]/)next;if(/## \[/)exit;print}" CHANGELOG.md)
- [ -n "$NOTES" ] && NEW_ENTRY="## ${VERSION} ($(date +%Y-%m-%d))
- ${NOTES}
- **SHA-256:** \`${SHA256}\`"
- fi
-
- # Check for existing release — keep last N versions in body
- EXISTING_BODY=""
- EXISTING_ID=""
- RELEASE_JSON=$(curl -sS -H "Authorization: token ${TOKEN}" \
- "${API}/releases/tags/${TAG}" 2>/dev/null)
- EXISTING_ID=$(echo "$RELEASE_JSON" | jq -r '.id // empty')
-
- if [ -n "$EXISTING_ID" ]; then
- echo "Existing release found: id=${EXISTING_ID}"
- EXISTING_BODY=$(echo "$RELEASE_JSON" | jq -r '.body // ""')
-
- # Keep only last (MAX_HISTORY - 1) version entries to make room for new one
- TRIMMED_BODY=$(echo "$EXISTING_BODY" | python3 -c "
- import sys, re
- content = sys.stdin.read()
- # Split on version headers (## XX.YY.ZZ)
- parts = re.split(r'(?=^## \d)', content, flags=re.MULTILINE)
- # Keep only version entries (skip any preamble)
- versions = [p for p in parts if re.match(r'^## \d', p)]
- # Keep last $((MAX_HISTORY - 1)) entries
- kept = versions[:$((MAX_HISTORY - 1))]
- print('\n---\n'.join(kept))
- " 2>/dev/null || echo "")
-
- # Delete old release and tag so we can recreate
- 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
-
- # Compose full body: new entry + previous entries
- if [ -n "$TRIMMED_BODY" ]; then
- FULL_BODY="${NEW_ENTRY}
-
- ---
-
- ${TRIMMED_BODY}"
- else
- FULL_BODY="${NEW_ENTRY}"
- fi
-
- echo "=== Create Release ==="
- echo "TAG=${TAG} VERSION=${VERSION} BRANCH=${BRANCH} PRE=${IS_PRE} HISTORY=${MAX_HISTORY}"
-
- HTTP_CODE=$(curl -sS -o /tmp/create_release.json -w "%{http_code}" \
- -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 "$FULL_BODY" \
- --argjson pre "$IS_PRE" \
- '{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: $pre}'
- )")
-
- echo "Response (HTTP ${HTTP_CODE}):"
- cat /tmp/create_release.json | jq . 2>/dev/null || cat /tmp/create_release.json
- echo
-
- RELEASE_ID=$(jq -r '.id // empty' /tmp/create_release.json)
- if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
- echo "::error::Failed to create release (HTTP ${HTTP_CODE})"
- exit 1
- fi
-
- echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
- echo "Release created: id=${RELEASE_ID}"
-
- - name: "Gitea: Upload ZIP"
- run: |
- RELEASE_ID="${{ steps.gitea_release.outputs.release_id }}"
- ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
- TOKEN="${{ secrets.GA_TOKEN }}"
- API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
-
- echo "Uploading ${ZIP_NAME} to release ${RELEASE_ID}..."
- HTTP_CODE=$(curl -sS -o /tmp/upload_response.json -w "%{http_code}" \
- -X POST \
- -H "Authorization: token ${TOKEN}" \
- -H "Content-Type: application/octet-stream" \
- "${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
- --data-binary "@build/${ZIP_NAME}")
-
- echo "Upload response (HTTP ${HTTP_CODE}):"
- cat /tmp/upload_response.json | jq . 2>/dev/null || cat /tmp/upload_response.json
- echo
-
- if [ "$HTTP_CODE" -ge 400 ]; then
- echo "::error::Upload failed (HTTP ${HTTP_CODE})"
- exit 1
- fi
- echo "Uploaded ${ZIP_NAME}"
-
- # ── Update updates.xml ──────────────────────────────────────────
- - name: "Update updates.xml with SHA and sync to main"
- run: |
- STABILITY="${{ steps.meta.outputs.stability }}"
- VERSION="${{ steps.bump.outputs.version }}"
- SHA256="${{ steps.zip.outputs.sha256 }}"
- ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
- TAG="${{ steps.meta.outputs.tag_name }}"
- DATE=$(date +%Y-%m-%d)
- BRANCH=$(git branch --show-current)
-
- echo "=== Update updates.xml ==="
- echo "STABILITY=${STABILITY} VERSION=${VERSION} SHA=${SHA256:0:16}..."
-
- if [ ! -f "updates.xml" ] || [ -z "$SHA256" ]; then
- echo "No updates.xml or no SHA — skipping"
- exit 0
- fi
-
- # Cascade map: each stability level updates itself + all lower levels
- # stable → all | rc → rc,beta,alpha,dev | beta → beta,alpha,dev | alpha → alpha,dev | dev → dev
- case "$STABILITY" in
- stable) CASCADE="development,alpha,beta,rc,stable" ;;
- rc) CASCADE="development,alpha,beta,rc" ;;
- beta) CASCADE="development,alpha,beta" ;;
- alpha) CASCADE="development,alpha" ;;
- development) CASCADE="development" ;;
- *) CASCADE="$STABILITY" ;;
- esac
-
- echo "Cascade: ${STABILITY} → ${CASCADE}"
-
- export PY_CASCADE="$CASCADE" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \
- PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \
- PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO"
- python3 << 'PYEOF'
- import re, os
-
- cascade = os.environ["PY_CASCADE"].split(",")
- version = os.environ["PY_VERSION"]
- sha256 = os.environ["PY_SHA256"]
- zip_name = os.environ["PY_ZIP_NAME"]
- tag = os.environ["PY_TAG"]
- date = os.environ["PY_DATE"]
- gitea_org = os.environ["PY_GITEA_ORG"]
- gitea_repo = os.environ["PY_GITEA_REPO"]
-
- gitea_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}"
-
- with open("updates.xml", "r") as f:
- content = f.read()
-
- for xml_tag in cascade:
- xml_tag = xml_tag.strip()
- block_pattern = r"((?:(?!).)*?" + re.escape(xml_tag) + r".*?)"
- match = re.search(block_pattern, content, re.DOTALL)
-
- if not match:
- print(f" SKIP: no {xml_tag} block found")
- continue
-
- block = match.group(1)
- original_block = block
-
- # Update version and date
- block = re.sub(r"[^<]*", f"{version}", block)
- block = re.sub(r"[^<]*", f"{date}", block)
-
- # Set SHA — add if missing, update if present, never leave empty
- if "" in block:
- block = re.sub(r"[^<]*", f"{sha256}", block)
- else:
- block = block.replace("", f"\n {sha256}")
-
- # Update download URL
- block = re.sub(
- r"(]*>)https://git\.mokoconsulting\.tech/[^<]*()",
- rf"\g<1>{gitea_url}\g<2>",
- block
- )
-
- content = content.replace(original_block, block)
- print(f" OK: {xml_tag} → version={version}, sha={sha256[:16]}...")
-
- with open("updates.xml", "w") as f:
- f.write(content)
-
- print(f"Cascaded {len(cascade)} channel(s)")
- PYEOF
-
- # Commit and push
- if git diff --quiet updates.xml 2>/dev/null; then
- echo "No changes to updates.xml"
- exit 0
- fi
-
- git add updates.xml
- git commit -m "chore: update ${STABILITY} SHA-256 for ${VERSION} [skip ci]" \
- --author="gitea-actions[bot] "
- echo "Pushing updates.xml to ${BRANCH}..."
- git push origin HEAD:${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
-
- # Always sync updates.xml to main via API (Joomla reads from main)
- GA_TOKEN="${{ secrets.GA_TOKEN }}"
- API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
-
- echo "Syncing updates.xml to main via API..."
- FILE_SHA=$(curl -sS -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)
- HTTP_CODE=$(curl -sS -o /tmp/sync_response.json -w "%{http_code}" \
- -X PUT -H "Authorization: token ${GA_TOKEN}" \
- -H "Content-Type: application/json" \
- "${API}/contents/updates.xml" \
- -d "$(jq -n \
- --arg content "$CONTENT" \
- --arg sha "$FILE_SHA" \
- --arg msg "chore: sync updates.xml ${STABILITY} ${VERSION} [skip ci]" \
- --arg branch "main" \
- '{content: $content, sha: $sha, message: $msg, branch: $branch}'
- )")
- echo "Sync response (HTTP ${HTTP_CODE}):"
- cat /tmp/sync_response.json | jq -r '.content.name // .message // "unknown"' 2>/dev/null
- if [ "$HTTP_CODE" -ge 400 ]; then
- echo "::warning::Sync to main failed (HTTP ${HTTP_CODE})"
- fi
- else
- echo "::warning::Could not get updates.xml SHA from main"
- fi
-
- - name: Summary
- if: always()
- run: |
- VERSION="${{ steps.bump.outputs.version }}"
- STABILITY="${{ steps.meta.outputs.stability }}"
- ZIP_NAME="${{ steps.bump.outputs.zip_name }}"
- SHA256="${{ steps.zip.outputs.sha256 }}"
- TAG="${{ steps.meta.outputs.tag_name }}"
-
- echo "### Release Created" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
- echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
- echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Stability | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
- echo "| Tag | \`${TAG}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| SHA-256 | \`${SHA256}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Gitea | [Release](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${TAG}) |" >> $GITHUB_STEP_SUMMARY
diff --git a/.mokogitea/repo-health.yml b/.mokogitea/repo-health.yml
deleted file mode 100644
index 57b11ef..0000000
--- a/.mokogitea/repo-health.yml
+++ /dev/null
@@ -1,766 +0,0 @@
-# ============================================================================
-# Copyright (C) 2025 Moko Consulting
-#
-# This file is part of a Moko Consulting project.
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: MokoStandards.Validation
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
-# PATH: /templates/workflows/joomla/repo_health.yml.template
-# VERSION: 04.06.00
-# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
-# ============================================================================
-
-name: Repo Health
-
-concurrency:
- group: repo-health-${{ github.repository }}-${{ github.ref }}
- cancel-in-progress: true
-
-defaults:
- run:
- shell: bash
-
-on:
- workflow_dispatch:
- inputs:
- profile:
- description: 'Validation profile: all, release, scripts, or repo'
- required: true
- default: all
- type: choice
- options:
- - all
- - release
- - scripts
- - repo
- pull_request:
- push:
-
-permissions:
- contents: read
-
-env:
- # Release policy - Repository Variables Only
- RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX
- RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX
-
- # Scripts governance policy
- SCRIPTS_REQUIRED_DIRS:
- SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
-
- # Repo health policy
- REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.gitea/workflows/
- REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
- REPO_DISALLOWED_DIRS:
- REPO_DISALLOWED_FILES: TODO.md,todo.md
-
- # Extended checks toggles
- EXTENDED_CHECKS: "true"
-
- # File / directory variables
- DOCS_INDEX: docs/docs-index.md
- SCRIPT_DIR: scripts
- WORKFLOWS_DIR: .gitea/workflows
- SHELLCHECK_PATTERN: '*.sh'
- SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
-
-jobs:
- access_check:
- name: Access control
- runs-on: ubuntu-latest
- timeout-minutes: 10
- permissions:
- contents: read
-
- outputs:
- allowed: ${{ steps.perm.outputs.allowed }}
- permission: ${{ steps.perm.outputs.permission }}
-
- steps:
- - name: Check actor permission (admin only)
- id: perm
- env:
- TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
- REPO: ${{ github.repository }}
- ACTOR: ${{ github.actor }}
- run: |
- set -euo pipefail
- ALLOWED=false
- PERMISSION=unknown
- METHOD=""
-
- # Hardcoded authorized users — always allowed
- case "$ACTOR" in
- jmiller|gitea-actions[bot])
- ALLOWED=true
- PERMISSION=admin
- METHOD="hardcoded allowlist"
- ;;
- *)
- # Detect platform and check permissions via API
- API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}"
- RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \
- "${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}')
- PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown")
- if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then
- ALLOWED=true
- fi
- METHOD="collaborator API"
- ;;
- esac
-
- echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT"
- echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT"
-
- {
- echo "## Access Authorization"
- echo ""
- echo "| Field | Value |"
- echo "|-------|-------|"
- echo "| **Actor** | \`${ACTOR}\` |"
- echo "| **Repository** | \`${REPO}\` |"
- echo "| **Permission** | \`${PERMISSION}\` |"
- echo "| **Method** | ${METHOD} |"
- echo "| **Authorized** | ${ALLOWED} |"
- echo ""
- if [ "$ALLOWED" = "true" ]; then
- echo "${ACTOR} authorized (${METHOD})"
- else
- echo "${ACTOR} is NOT authorized. Requires admin or maintain role."
- fi
- } >> "${GITHUB_STEP_SUMMARY}"
-
- - name: Deny execution when not permitted
- if: ${{ steps.perm.outputs.allowed != 'true' }}
- run: |
- set -euo pipefail
- printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
- exit 1
-
- release_config:
- name: Release configuration
- needs: access_check
- if: ${{ needs.access_check.outputs.allowed == 'true' }}
- runs-on: ubuntu-latest
- timeout-minutes: 20
- permissions:
- contents: read
-
- steps:
- - name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- fetch-depth: 0
-
- - name: Guardrails release vars
- env:
- PROFILE_RAW: ${{ github.event.inputs.profile }}
- RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }}
- DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
- run: |
- set -euo pipefail
-
- profile="${PROFILE_RAW:-all}"
- case "${profile}" in
- all|release|scripts|repo) ;;
- *)
- printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
- exit 1
- ;;
- esac
-
- if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then
- {
- printf '%s\n' '### Release configuration (Repository Variables)'
- printf '%s\n' "Profile: ${profile}"
- printf '%s\n' 'Status: SKIPPED'
- printf '%s\n' 'Reason: profile excludes release validation'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- exit 0
- fi
-
- IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}"
- IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}"
-
- missing=()
- missing_optional=()
-
- for k in "${required[@]}"; do
- v="${!k:-}"
- [ -z "${v}" ] && missing+=("${k}")
- done
-
- for k in "${optional[@]}"; do
- v="${!k:-}"
- [ -z "${v}" ] && missing_optional+=("${k}")
- done
-
- {
- printf '%s\n' '### Release configuration (Repository Variables)'
- printf '%s\n' "Profile: ${profile}"
- printf '%s\n' '| Variable | Status |'
- printf '%s\n' '|---|---|'
- printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |"
- printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |"
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
-
- if [ "${#missing_optional[@]}" -gt 0 ]; then
- {
- printf '%s\n' '### Missing optional repository variables'
- for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
-
- if [ "${#missing[@]}" -gt 0 ]; then
- {
- printf '%s\n' '### Missing required repository variables'
- for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done
- printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.'
- } >> "${GITHUB_STEP_SUMMARY}"
- exit 1
- fi
-
- {
- printf '%s\n' '### Repository variables validation result'
- printf '%s\n' 'Status: OK'
- printf '%s\n' 'All required repository variables present.'
- printf '%s\n' ''
- printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
-
- scripts_governance:
- name: Scripts governance
- needs: access_check
- if: ${{ needs.access_check.outputs.allowed == 'true' }}
- runs-on: ubuntu-latest
- timeout-minutes: 15
- permissions:
- contents: read
-
- steps:
- - name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- fetch-depth: 0
-
- - name: Scripts folder checks
- env:
- PROFILE_RAW: ${{ github.event.inputs.profile }}
- run: |
- set -euo pipefail
-
- profile="${PROFILE_RAW:-all}"
- case "${profile}" in
- all|release|scripts|repo) ;;
- *)
- printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
- exit 1
- ;;
- esac
-
- if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then
- {
- printf '%s\n' '### Scripts governance'
- printf '%s\n' "Profile: ${profile}"
- printf '%s\n' 'Status: SKIPPED'
- printf '%s\n' 'Reason: profile excludes scripts governance'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- exit 0
- fi
-
- if [ ! -d "${SCRIPT_DIR}" ]; then
- {
- printf '%s\n' '### Scripts governance'
- printf '%s\n' 'Status: OK (advisory)'
- printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- exit 0
- fi
-
- IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"
- IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
-
- missing_dirs=()
- unapproved_dirs=()
-
- for d in "${required_dirs[@]}"; do
- req="${d%/}"
- [ ! -d "${req}" ] && missing_dirs+=("${req}/")
- done
-
- while IFS= read -r d; do
- allowed=false
- for a in "${allowed_dirs[@]}"; do
- a_norm="${a%/}"
- [ "${d%/}" = "${a_norm}" ] && allowed=true
- done
- [ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/")
- done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##')
-
- {
- printf '%s\n' '### Scripts governance'
- printf '%s\n' "Profile: ${profile}"
- printf '%s\n' '| Area | Status | Notes |'
- printf '%s\n' '|---|---|---|'
-
- if [ "${#missing_dirs[@]}" -gt 0 ]; then
- printf '%s\n' '| Required directories | Warning | Missing required subfolders |'
- else
- printf '%s\n' '| Required directories | OK | All required subfolders present |'
- fi
-
- if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
- printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |'
- else
- printf '%s\n' '| Directory policy | OK | No unapproved directories |'
- fi
-
- printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |'
- printf '\n'
-
- if [ "${#missing_dirs[@]}" -gt 0 ]; then
- printf '%s\n' 'Missing required script directories:'
- for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done
- printf '\n'
- else
- printf '%s\n' 'Missing required script directories: none.'
- printf '\n'
- fi
-
- if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
- printf '%s\n' 'Unapproved script directories detected:'
- for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done
- printf '\n'
- else
- printf '%s\n' 'Unapproved script directories detected: none.'
- printf '\n'
- fi
-
- printf '%s\n' 'Scripts governance completed in advisory mode.'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
-
- repo_health:
- name: Repository health
- needs: access_check
- if: ${{ needs.access_check.outputs.allowed == 'true' }}
- runs-on: ubuntu-latest
- timeout-minutes: 20
- permissions:
- contents: read
-
- steps:
- - name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- fetch-depth: 0
-
- - name: Repository health checks
- env:
- PROFILE_RAW: ${{ github.event.inputs.profile }}
- run: |
- set -euo pipefail
-
- profile="${PROFILE_RAW:-all}"
- case "${profile}" in
- all|release|scripts|repo) ;;
- *)
- printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
- exit 1
- ;;
- esac
-
- if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then
- {
- printf '%s\n' '### Repository health'
- printf '%s\n' "Profile: ${profile}"
- printf '%s\n' 'Status: SKIPPED'
- printf '%s\n' 'Reason: profile excludes repository health'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- exit 0
- fi
-
- # Source directory: src/ or htdocs/ (either is valid)
- if [ -d "src" ]; then
- SOURCE_DIR="src"
- elif [ -d "htdocs" ]; then
- SOURCE_DIR="htdocs"
- else
- missing_required+=("src/ or htdocs/ (source directory required)")
- fi
-
- IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
- IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
- IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"
- IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}"
-
- missing_required=()
- missing_optional=()
-
- for item in "${required_artifacts[@]}"; do
- if printf '%s' "${item}" | grep -q '/$'; then
- d="${item%/}"
- [ ! -d "${d}" ] && missing_required+=("${item}")
- else
- [ ! -f "${item}" ] && missing_required+=("${item}")
- fi
- done
-
- for f in "${optional_files[@]}"; do
- if printf '%s' "${f}" | grep -q '/$'; then
- d="${f%/}"
- [ ! -d "${d}" ] && missing_optional+=("${f}")
- else
- [ ! -f "${f}" ] && missing_optional+=("${f}")
- fi
- done
-
- for d in "${disallowed_dirs[@]}"; do
- d_norm="${d%/}"
- [ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)")
- done
-
- for f in "${disallowed_files[@]}"; do
- [ -f "${f}" ] && missing_required+=("${f} (disallowed)")
- done
-
- git fetch origin --prune
-
- dev_paths=()
- dev_branches=()
-
- while IFS= read -r b; do
- name="${b#origin/}"
- if [ "${name}" = 'dev' ]; then
- dev_branches+=("${name}")
- else
- dev_paths+=("${name}")
- fi
- done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
-
- if [ "${#dev_paths[@]}" -eq 0 ]; then
- missing_required+=("dev/* branch (e.g. dev/01.00.00)")
- fi
-
- if [ "${#dev_branches[@]}" -gt 0 ]; then
- missing_required+=("invalid branch dev (must be dev/)")
- fi
-
- content_warnings=()
-
- if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then
- content_warnings+=("CHANGELOG.md missing '# Changelog' header")
- fi
-
- if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then
- content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)")
- fi
-
- if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then
- content_warnings+=("LICENSE does not look like a GPL text")
- fi
-
- if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then
- content_warnings+=("README.md missing expected brand keyword")
- fi
-
- export PROFILE_RAW="${profile}"
- export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")"
- export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
- export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
-
- report_json="$(python3 - <<'PY'
- import json
- import os
-
- profile = os.environ.get('PROFILE_RAW') or 'all'
-
- missing_required = os.environ.get('MISSING_REQUIRED', '').splitlines() if os.environ.get('MISSING_REQUIRED') else []
- missing_optional = os.environ.get('MISSING_OPTIONAL', '').splitlines() if os.environ.get('MISSING_OPTIONAL') else []
- content_warnings = os.environ.get('CONTENT_WARNINGS', '').splitlines() if os.environ.get('CONTENT_WARNINGS') else []
-
- out = {
- 'profile': profile,
- 'missing_required': [x for x in missing_required if x],
- 'missing_optional': [x for x in missing_optional if x],
- 'content_warnings': [x for x in content_warnings if x],
- }
-
- print(json.dumps(out, indent=2))
- PY
- )"
-
- {
- printf '%s\n' '### Repository health'
- printf '%s\n' "Profile: ${profile}"
- printf '%s\n' '| Metric | Value |'
- printf '%s\n' '|---|---|'
- printf '%s\n' "| Missing required | ${#missing_required[@]} |"
- printf '%s\n' "| Missing optional | ${#missing_optional[@]} |"
- printf '%s\n' "| Content warnings | ${#content_warnings[@]} |"
- printf '\n'
-
- printf '%s\n' '### Guardrails report (JSON)'
- printf '%s\n' '```json'
- printf '%s\n' "${report_json}"
- printf '%s\n' '```'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
-
- if [ "${#missing_required[@]}" -gt 0 ]; then
- {
- printf '%s\n' '### Missing required repo artifacts'
- for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done
- printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- exit 1
- fi
-
- if [ "${#missing_optional[@]}" -gt 0 ]; then
- {
- printf '%s\n' '### Missing optional repo artifacts'
- for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
-
- if [ "${#content_warnings[@]}" -gt 0 ]; then
- {
- printf '%s\n' '### Repo content warnings'
- for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
-
- # -- Joomla-specific checks --
- joomla_findings=()
-
- MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '/dev/null | head -1 || true)"
- if [ -z "${MANIFEST}" ]; then
- joomla_findings+=("Joomla XML manifest not found (no *.xml with tag)")
- else
- if ! grep -qP '' "${MANIFEST}"; then
- joomla_findings+=("XML manifest: tag missing")
- fi
- if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then
- joomla_findings+=("XML manifest: type attribute missing or invalid")
- fi
- if ! grep -qP '' "${MANIFEST}"; then
- joomla_findings+=("XML manifest: tag missing")
- fi
- if ! grep -qP '' "${MANIFEST}"; then
- joomla_findings+=("XML manifest: tag missing")
- fi
- if ! grep -qP ' missing (required for Joomla 5+)")
- fi
- fi
-
- INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)"
- if [ "${INI_COUNT}" -eq 0 ]; then
- joomla_findings+=("No .ini language files found")
- fi
-
- if [ ! -f 'updates.xml' ]; then
- joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
- fi
-
- INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
- for dir in "${INDEX_DIRS[@]}"; do
- if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
- joomla_findings+=("${dir}/index.html missing (directory listing protection)")
- fi
- done
-
- if [ "${#joomla_findings[@]}" -gt 0 ]; then
- {
- printf '%s\n' '### Joomla extension checks'
- printf '%s\n' '| Check | Status |'
- printf '%s\n' '|---|---|'
- for f in "${joomla_findings[@]}"; do
- printf '%s\n' "| ${f} | Warning |"
- done
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- else
- {
- printf '%s\n' '### Joomla extension checks'
- printf '%s\n' 'All Joomla-specific checks passed.'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
-
- extended_enabled="${EXTENDED_CHECKS:-true}"
- extended_findings=()
-
- if [ "${extended_enabled}" = 'true' ]; then
- if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then
- :
- else
- extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)")
- fi
-
- if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then
- bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)"
- if [ -n "${bad_refs}" ]; then
- extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt")
- {
- printf '%s\n' '### Workflow pinning advisory'
- printf '%s\n' 'Found uses: entries pinned to main/master:'
- printf '%s\n' '```'
- printf '%s\n' "${bad_refs}"
- printf '%s\n' '```'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
- fi
-
- if [ -f "${DOCS_INDEX}" ]; then
- missing_links="$(python3 - <<'PY'
- import os
- import re
-
- idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md')
- base = os.getcwd()
-
- bad = []
- pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)')
-
- with open(idx, 'r', encoding='utf-8') as f:
- for line in f:
- for m in pat.findall(line):
- link = m.strip()
- if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'):
- continue
- if link.startswith('/'):
- rel = link.lstrip('/')
- else:
- rel = os.path.normpath(os.path.join(os.path.dirname(idx), link))
- rel = rel.split('#', 1)[0]
- rel = rel.split('?', 1)[0]
- if not rel:
- continue
- p = os.path.join(base, rel)
- if not os.path.exists(p):
- bad.append(rel)
-
- print('\n'.join(sorted(set(bad))))
- PY
- )"
- if [ -n "${missing_links}" ]; then
- extended_findings+=("docs/docs-index.md contains broken relative links")
- {
- printf '%s\n' '### Docs index link integrity'
- printf '%s\n' 'Broken relative links:'
- while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}"
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
- fi
-
- if [ -d "${SCRIPT_DIR}" ]; then
- if ! command -v shellcheck >/dev/null 2>&1; then
- sudo apt-get update -qq
- sudo apt-get install -y shellcheck >/dev/null
- fi
-
- sc_out=''
- while IFS= read -r shf; do
- [ -z "${shf}" ] && continue
- out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)"
- if [ -n "${out_one}" ]; then
- sc_out="${sc_out}${out_one}\n"
- fi
- done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort)
-
- if [ -n "${sc_out}" ]; then
- extended_findings+=("ShellCheck warnings detected (advisory)")
- sc_head="$(printf '%s' "${sc_out}" | head -n 200)"
- {
- printf '%s\n' '### ShellCheck (advisory)'
- printf '%s\n' '```'
- printf '%s\n' "${sc_head}"
- printf '%s\n' '```'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
- fi
-
- spdx_missing=()
- IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}"
- spdx_args=()
- for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done
-
- while IFS= read -r f; do
- [ -z "${f}" ] && continue
- if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then
- spdx_missing+=("${f}")
- fi
- done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true)
-
- if [ "${#spdx_missing[@]}" -gt 0 ]; then
- extended_findings+=("SPDX header missing in some tracked files (advisory)")
- {
- printf '%s\n' '### SPDX header advisory'
- printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):'
- for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
-
- stale_cutoff_days=180
- stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)"
- if [ -n "${stale_branches}" ]; then
- extended_findings+=("Stale remote branches detected (advisory)")
- {
- printf '%s\n' '### Git hygiene advisory'
- printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):"
- while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}"
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
- fi
-
- {
- printf '%s\n' '### Guardrails coverage matrix'
- printf '%s\n' '| Domain | Status | Notes |'
- printf '%s\n' '|---|---|---|'
- printf '%s\n' '| Access control | OK | Admin-only execution gate |'
- printf '%s\n' '| Release variables | OK | Repository variables validation |'
- printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
- printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
- printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
- if [ "${extended_enabled}" = 'true' ]; then
- if [ "${#extended_findings[@]}" -gt 0 ]; then
- printf '%s\n' '| Extended checks | Warning | See extended findings below |'
- else
- printf '%s\n' '| Extended checks | OK | No findings |'
- fi
- else
- printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |'
- fi
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
-
- if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then
- {
- printf '%s\n' '### Extended findings (advisory)'
- for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
-
- printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
diff --git a/.mokogitea/security-audit.yml b/.mokogitea/security-audit.yml
deleted file mode 100644
index ff6de4c..0000000
--- a/.mokogitea/security-audit.yml
+++ /dev/null
@@ -1,82 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: MokoStandards.Security
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
-# PATH: /.gitea/workflows/security-audit.yml
-# VERSION: 01.00.00
-# BRIEF: Dependency vulnerability scanning for composer and npm packages
-
-name: Security Audit
-
-on:
- schedule:
- - cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC
- pull_request:
- branches:
- - main
- paths:
- - 'composer.json'
- - 'composer.lock'
- - 'package.json'
- - 'package-lock.json'
- workflow_dispatch:
-
-permissions:
- contents: read
-
-env:
- NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
- NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
-
-jobs:
- audit:
- name: Dependency Audit
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Composer audit
- if: hashFiles('composer.lock') != ''
- run: |
- echo "=== Composer Security Audit ==="
- if ! command -v composer &> /dev/null; then
- sudo apt-get update -qq
- sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1
- fi
- composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt
- RESULT=$?
- if [ $RESULT -ne 0 ]; then
- echo "::warning::Composer vulnerabilities found"
- echo "composer_vulnerable=true" >> "$GITHUB_ENV"
- else
- echo "No known vulnerabilities in composer dependencies"
- fi
-
- - name: NPM audit
- if: hashFiles('package-lock.json') != ''
- run: |
- echo "=== NPM Security Audit ==="
- npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true
- if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then
- echo "No known vulnerabilities in npm dependencies"
- else
- echo "::warning::NPM vulnerabilities found"
- echo "npm_vulnerable=true" >> "$GITHUB_ENV"
- fi
-
- - name: Notify on vulnerabilities
- if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true'
- run: |
- REPO="${{ github.event.repository.name }}"
- curl -sS \
- -H "Title: ${REPO} has vulnerable dependencies" \
- -H "Tags: lock,warning" \
- -H "Priority: high" \
- -d "Security audit found vulnerabilities. Review dependency updates." \
- "${NTFY_URL}/${NTFY_TOPIC}" || true
diff --git a/.mokogitea/update-server.yml b/.mokogitea/update-server.yml
deleted file mode 100644
index e6a1924..0000000
--- a/.mokogitea/update-server.yml
+++ /dev/null
@@ -1,464 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: MokoStandards.Joomla
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
-# PATH: /templates/workflows/joomla/update-server.yml.template
-# VERSION: 04.06.00
-# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries
-#
-# Writes updates.xml with multiple entries:
-# - stable on push to main (from auto-release)
-# - rc on push to rc/**
-# - development on push to dev or dev/**
-#
-# Joomla filters by user's "Minimum Stability" setting.
-
-name: Update Joomla Update Server XML Feed
-
-on:
- push:
- branches:
- - 'dev'
- - 'dev/**'
- - 'alpha/**'
- - 'beta/**'
- - 'rc/**'
- paths:
- - 'src/**'
- - 'htdocs/**'
- pull_request:
- types: [closed]
- branches:
- - 'dev'
- - 'dev/**'
- - 'alpha/**'
- - 'beta/**'
- - 'rc/**'
- paths:
- - 'src/**'
- - 'htdocs/**'
- workflow_dispatch:
- inputs:
- stability:
- description: 'Stability tag'
- required: true
- default: 'development'
- type: choice
- options:
- - development
- - alpha
- - beta
- - rc
- - stable
-
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
- GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
-
-permissions:
- contents: write
-
-jobs:
- update-xml:
- name: Update updates.xml
- runs-on: release
- if: >-
- github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- token: ${{ secrets.GA_TOKEN }}
- fetch-depth: 0
-
- - name: Setup MokoStandards tools
- env:
- MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
- MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
- COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}'
- run: |
- if ! command -v composer &> /dev/null; then
- sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
- fi
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
- /tmp/mokostandards-api 2>/dev/null || true
- if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
- cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
- fi
-
- - name: Generate updates.xml entry
- id: update
- run: |
- 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)
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- BUMPED=$(php /tmp/mokostandards-api/cli/version_bump.php --path . 2>/dev/null || true)
- 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] " 2>/dev/null || true
- git push 2>/dev/null || true
- fi
-
- # Determine stability from branch or input
- if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
- STABILITY="${{ inputs.stability }}"
- elif [[ "$BRANCH" == rc/* ]]; then
- STABILITY="rc"
- elif [[ "$BRANCH" == beta/* ]]; then
- STABILITY="beta"
- elif [[ "$BRANCH" == alpha/* ]]; then
- STABILITY="alpha"
- elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then
- STABILITY="development"
- else
- STABILITY="stable"
- fi
-
- echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
-
- # Parse manifest (portable — no grep -P)
- MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '/dev/null | head -1)
- 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>.*/\1/p' "$MANIFEST" | head -1)
- EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
- EXT_CLIENT=$(sed -n 's/.*]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- EXT_VERSION=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
- TARGET_PLATFORM=$(sed -n 's/.*\(\).*/\1/p' "$MANIFEST" | head -1)
- PHP_MINIMUM=$(sed -n 's/.*\([^<]*\)<\/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 '' "/")
-
- CLIENT_TAG=""
- [ -n "$EXT_CLIENT" ] && CLIENT_TAG="${EXT_CLIENT}"
- [ -z "$CLIENT_TAG" ] && ([ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]) && CLIENT_TAG="site"
-
- FOLDER_TAG=""
- [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="${EXT_FOLDER}"
-
- PHP_TAG=""
- [ -n "$PHP_MINIMUM" ] && PHP_TAG="${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} \n"
- NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME}\n"
- NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME} ${STABILITY} build.\n"
- NEW_ENTRY="${NEW_ENTRY} ${EXT_ELEMENT}\n"
- NEW_ENTRY="${NEW_ENTRY} ${EXT_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}\n"
- NEW_ENTRY="${NEW_ENTRY} $(date +%Y-%m-%d)\n"
- NEW_ENTRY="${NEW_ENTRY} https://git.mokoconsulting.tech/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${RELEASE_TAG}\n"
- NEW_ENTRY="${NEW_ENTRY} \n"
- NEW_ENTRY="${NEW_ENTRY} ${DOWNLOAD_URL}\n"
- NEW_ENTRY="${NEW_ENTRY} \n"
- [ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} ${SHA256}\n"
- NEW_ENTRY="${NEW_ENTRY} ${STABILITY}\n"
- NEW_ENTRY="${NEW_ENTRY} Moko Consulting\n"
- NEW_ENTRY="${NEW_ENTRY} https://mokoconsulting.tech\n"
- NEW_ENTRY="${NEW_ENTRY} \n"
- [ -n "$PHP_MINIMUM" ] && NEW_ENTRY="${NEW_ENTRY} ${PHP_MINIMUM}\n"
- NEW_ENTRY="${NEW_ENTRY} "
-
- # -- 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' "" > updates.xml
- printf '%s\n' "" >> updates.xml
- printf '%s\n' "" >> updates.xml
- printf '%s\n' "" >> 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"[^<]*", f"{tag}", new_entry_template)
-
- # Try to find existing block (handles both single-line and multi-line )
- block_pattern = r"((?:(?!).)*?" + re.escape(tag) + r".*?)"
- 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} → {version}")
- else:
- # Create — insert before
- content = content.replace("", "\n" + new_entry.strip() + "\n\n")
- print(f" CREATED: {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 commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \
- --author="gitea-actions[bot] "
- git push
- }
-
- # -- Sync updates.xml to main (for non-main branches) ----------------------
- - name: Sync updates.xml to main
- if: github.ref_name != 'main'
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- GA_TOKEN="${{ secrets.GA_TOKEN }}"
-
- FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
- "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
-
- if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
- CONTENT=$(base64 -w0 updates.xml)
- curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \
- -H "Content-Type: application/json" \
- "${API_BASE}/contents/updates.xml" \
- -d "$(python3 -c "import json; print(json.dumps({
- 'content': '${CONTENT}',
- 'sha': '${FILE_SHA}',
- 'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]',
- 'branch': 'main'
- }))")" > /dev/null 2>&1 \
- && echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \
- || echo "WARNING: failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY
- else
- echo "WARNING: could not get updates.xml SHA from main" >> $GITHUB_STEP_SUMMARY
- fi
-
- - name: SFTP deploy to dev server
- if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
- env:
- DEV_HOST: ${{ vars.DEV_FTP_HOST }}
- DEV_PATH: ${{ vars.DEV_FTP_PATH }}
- DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
- DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
- DEV_PORT: ${{ vars.DEV_FTP_PORT }}
- DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
- DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
- run: |
- # -- Permission check: admin or maintain role required --------
- ACTOR="${{ github.actor }}"
- REPO="${{ github.repository }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
-
- PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- "${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
- python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
- case "$PERMISSION" in
- admin|maintain|write) ;;
- *)
- echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
- exit 0
- ;;
- esac
-
- [ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
-
- SOURCE_DIR="src"
- [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
- [ ! -d "$SOURCE_DIR" ] && exit 0
-
- PORT="${DEV_PORT:-22}"
- REMOTE="${DEV_PATH%/}"
- [ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
-
- printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
- "$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
- if [ -n "$DEV_KEY" ]; then
- echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
- printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
- else
- printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
- fi
-
- PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
- if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
- php /tmp/mokostandards-api/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
- elif [ -f "/tmp/mokostandards-api/deploy/deploy-sftp.php" ]; then
- php /tmp/mokostandards-api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
- fi
- rm -f /tmp/deploy_key /tmp/sftp-config.json
- echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
-
- - name: Summary
- if: always()
- run: |
- echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
- echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
- echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY
diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml
deleted file mode 100644
index fb9dc82..0000000
--- a/.mokogitea/workflows/auto-bump.yml
+++ /dev/null
@@ -1,66 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Release
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/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
- - rc
- - 'feature/**'
- - 'patch/**'
-
-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: |
- php ${MOKO_CLI}/version_auto_bump.php \
- --path . --branch "${GITHUB_REF_NAME}" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" \
- --repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml
deleted file mode 100644
index 1227ff8..0000000
--- a/.mokogitea/workflows/auto-release.yml
+++ /dev/null
@@ -1,270 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Release
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
-# PATH: /templates/workflows/universal/auto-release.yml.template
-# VERSION: 05.00.00
-# BRIEF: Universal build & release � detects platform from manifest.xml
-#
-# +========================================================================+
-# | UNIVERSAL BUILD & RELEASE PIPELINE |
-# +========================================================================+
-# | |
-# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
-# | |
-# | Platform-specific: |
-# | joomla: XML manifest, updates.xml, type-prefixed packages |
-# | dolibarr: mod*.class.php, update.txt, dev version reset |
-# | generic: README-only, no update stream |
-# | |
-# +========================================================================+
-
-name: "Universal: Build & Release"
-
-on:
- pull_request:
- types: [opened, closed]
- branches:
- - main
- workflow_dispatch:
- inputs:
- action:
- description: 'Action to perform'
- required: false
- type: choice
- default: release
- options:
- - release
- - promote-rc
-
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
- GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
-
-permissions:
- contents: write
-
-jobs:
- # ── PR Opened → Rename branch to RC and build RC release ─────────────────────
- promote-rc:
- name: Promote to RC
- runs-on: release
- if: >-
- (github.event.action == 'opened' && github.event.pull_request.merged != true) ||
- (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- token: ${{ secrets.MOKOGITEA_TOKEN }}
- fetch-depth: 1
-
- - name: Setup moko-platform tools
- env:
- MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
- MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
- run: |
- if ! command -v composer &> /dev/null; then
- sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
- fi
- # Always fetch latest CLI tools — never use stale cache from previous runs
- rm -rf /tmp/moko-platform-api
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
- /tmp/moko-platform-api
- cd /tmp/moko-platform-api
- composer install --no-dev --no-interaction --quiet
-
- - name: Rename branch to rc
- run: |
- php /tmp/moko-platform-api/cli/branch_rename.php \
- --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" \
- --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
- --pr "${{ github.event.pull_request.number }}"
-
- - name: Checkout rc and configure git
- run: |
- git fetch origin rc
- git checkout rc
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
-
- - name: Publish RC release
- run: |
- php /tmp/moko-platform-api/cli/release_publish.php \
- --path . --stability rc --bump minor --branch rc \
- --token "${{ secrets.MOKOGITEA_TOKEN }}"
-
- - name: Summary
- if: always()
- run: |
- echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
- echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY
-
- # ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
- release:
- name: Build & Release Pipeline
- runs-on: release
- if: >-
- github.event.pull_request.merged == true ||
- (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- token: ${{ secrets.MOKOGITEA_TOKEN }}
- fetch-depth: 0
-
- - name: Configure git for bot pushes
- run: |
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
-
- - name: Setup moko-platform tools
- env:
- MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
- MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
- COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
- run: |
- # Ensure PHP + Composer are available
- if ! command -v composer &> /dev/null; then
- sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
- fi
- # Always fetch latest CLI tools — never use stale cache from previous runs
- rm -rf /tmp/moko-platform-api
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
- /tmp/moko-platform-api
- cd /tmp/moko-platform-api
- composer install --no-dev --no-interaction --quiet
-
-
- - name: "Publish stable release"
- run: |
- php /tmp/moko-platform-api/cli/release_publish.php \
- --path . --stability stable --bump minor --branch main \
- --token "${{ secrets.MOKOGITEA_TOKEN }}"
-
- # -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- - name: "Step 9: Mirror release to GitHub"
- if: >-
- steps.version.outputs.skip != 'true' &&
- secrets.GH_MIRROR_TOKEN != ''
- continue-on-error: true
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/release_mirror.php \
- --version "$VERSION" --tag "$RELEASE_TAG" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
- --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
- --branch main 2>&1 || true
- echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
-
- # -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- - name: "Step 10: Push main to GitHub mirror"
- if: >-
- steps.version.outputs.skip != 'true' &&
- secrets.GH_MIRROR_TOKEN != ''
- continue-on-error: true
- run: |
- GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
- GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
- GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
- git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
- git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
- git fetch origin main --depth=1
- git push github origin/main:refs/heads/main --force 2>/dev/null \
- && echo "main branch pushed to GitHub mirror" \
- || echo "WARNING: GitHub mirror push failed"
-
- - name: "Step 11: Delete rc branch and recreate dev from main"
- if: steps.version.outputs.skip != 'true'
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
-
- # Delete rc branch (ephemeral — created by promote-rc)
- curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
- "${API_BASE}/branches/rc" 2>/dev/null \
- && echo "Deleted rc branch" || echo "rc branch not found"
-
- # Delete dev branch
- curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
- "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
-
- # Recreate dev from main (now includes version bump + changelog promotion)
- curl -sf -X POST -H "Authorization: token ${TOKEN}" \
- -H "Content-Type: application/json" \
- "${API_BASE}/branches" \
- -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
-
- echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
-
- - name: "Step 12: Create version branch from main"
- if: steps.version.outputs.skip != 'true'
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- BRANCH_NAME="version/${VERSION}"
- MAIN_SHA=$(git rev-parse HEAD)
-
- # Delete old version branch if it exists (same version re-release)
- curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
-
- # Create version/XX.YY.ZZ from main
- curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
-
- echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
-
-
-
- # -- Dolibarr post-release: Reset dev version -----------------------------
- - name: "Post-release: Reset dev version"
- if: steps.version.outputs.skip != 'true'
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/version_reset_dev.php \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
- --branch dev --path . 2>&1 || true
-
- # -- Summary --------------------------------------------------------------
- - name: Pipeline Summary
- if: always()
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- PLATFORM="${{ steps.platform.outputs.platform }}"
- if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
- echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
- echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
- elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
- echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
- else
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
- echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
- echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
- fi
diff --git a/.mokogitea/workflows/branch-cleanup.yml b/.mokogitea/workflows/branch-cleanup.yml
deleted file mode 100644
index e0ba128..0000000
--- a/.mokogitea/workflows/branch-cleanup.yml
+++ /dev/null
@@ -1,48 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# 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=$(php -r "echo rawurlencode('${BRANCH}');")
-
- 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
diff --git a/.mokogitea/workflows/cascade-dev.yml b/.mokogitea/workflows/cascade-dev.yml
deleted file mode 100644
index 5f7c1d7..0000000
--- a/.mokogitea/workflows/cascade-dev.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-# DISABLED — auto-release Step 11 recreates dev from main after every release.
-# Cascade-dev is redundant and causes version conflicts when both main and dev
-# have different version numbers in templateDetails.xml / manifest.xml.
-name: "Cascade Main → Dev (DISABLED)"
-on: workflow_dispatch
-jobs:
- noop:
- runs-on: ubuntu-latest
- steps:
- - run: echo "Cascade disabled — auto-release handles dev recreation"
diff --git a/.mokogitea/workflows/ci-joomla.yml b/.mokogitea/workflows/ci-joomla.yml
deleted file mode 100644
index de2d9eb..0000000
--- a/.mokogitea/workflows/ci-joomla.yml
+++ /dev/null
@@ -1,467 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# This file is part of a Moko Consulting project.
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow.Template
-# INGROUP: MokoStandards.CI
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
-# PATH: /templates/workflows/joomla/ci-joomla.yml.template
-# VERSION: 04.06.00
-# BRIEF: CI workflow for Joomla extensions — lint, validate, test
-
-name: "Joomla: Extension CI"
-
-on:
- pull_request:
- branches:
- - main
- - 'dev/**'
- workflow_dispatch:
-
-permissions:
- contents: read
- pull-requests: write
-
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
-
-jobs:
- lint-and-validate:
- name: Lint & Validate
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
-
- - name: Setup PHP
- run: |
- php -v && composer --version
-
- - name: Clone MokoStandards
- env:
- GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
- MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
- MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
- run: |
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
- /tmp/mokostandards-api
-
- - name: Install dependencies
- env:
- COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
- run: |
- if [ -f "composer.json" ]; then
- composer install \
- --no-interaction \
- --prefer-dist \
- --optimize-autoloader
- else
- echo "No composer.json found — skipping dependency install"
- fi
-
- - name: PHP syntax check
- run: |
- ERRORS=0
- for DIR in src/ htdocs/; do
- if [ -d "$DIR" ]; then
- FOUND=1
- while IFS= read -r -d '' FILE; do
- OUTPUT=$(php -l "$FILE" 2>&1)
- if echo "$OUTPUT" | grep -q "Parse error"; then
- echo "::error file=${FILE}::${OUTPUT}"
- ERRORS=$((ERRORS + 1))
- fi
- done < <(find "$DIR" -name "*.php" -print0)
- fi
- done
- echo "### PHP Syntax Check" >> $GITHUB_STEP_SUMMARY
- if [ "${ERRORS}" -gt 0 ]; then
- echo "**${ERRORS} syntax error(s) found.**" >> $GITHUB_STEP_SUMMARY
- exit 1
- else
- echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
- fi
-
- - name: XML manifest validation
- run: |
- echo "### XML Manifest Validation" >> $GITHUB_STEP_SUMMARY
- ERRORS=0
-
- # Find the extension manifest (XML with /dev/null; then
- MANIFEST="$XML_FILE"
- break
- fi
- done
-
- if [ -z "$MANIFEST" ]; then
- echo "No Joomla extension manifest found (XML file with \`> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- else
- echo "Manifest found: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
-
- # Validate well-formed XML
- php -r "
- \$xml = @simplexml_load_file('$MANIFEST');
- if (\$xml === false) {
- echo 'INVALID';
- exit(1);
- }
- echo 'VALID';
- " > /tmp/xml_result 2>&1
- XML_RESULT=$(cat /tmp/xml_result)
- if [ "$XML_RESULT" != "VALID" ]; then
- echo "Manifest is not well-formed XML." >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- else
- echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY
- fi
-
- # Check required tags: name, version, author, namespace (Joomla 5+)
- for TAG in name version author namespace; do
- if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then
- echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- else
- echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
- fi
- done
- fi
-
- if [ "${ERRORS}" -gt 0 ]; then
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "**${ERRORS} manifest issue(s) found.**" >> $GITHUB_STEP_SUMMARY
- exit 1
- else
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY
- fi
-
- - name: Check language files referenced in manifest
- run: |
- echo "### Language File Check" >> $GITHUB_STEP_SUMMARY
- ERRORS=0
-
- MANIFEST=""
- for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
- if grep -q "/dev/null; then
- MANIFEST="$XML_FILE"
- break
- fi
- done
-
- if [ -n "$MANIFEST" ]; then
- # Extract language file references from manifest
- LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
- if [ -z "$LANG_FILES" ]; then
- echo "No language file references found in manifest — skipping." >> $GITHUB_STEP_SUMMARY
- else
- while IFS= read -r LANG_FILE; do
- LANG_FILE=$(echo "$LANG_FILE" | xargs)
- if [ -z "$LANG_FILE" ]; then
- continue
- fi
- # Check in common locations
- FOUND=0
- for BASE in "." "src" "htdocs"; do
- if [ -f "${BASE}/${LANG_FILE}" ]; then
- FOUND=1
- break
- fi
- done
- if [ "$FOUND" -eq 0 ]; then
- echo "Missing language file: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- else
- echo "Language file present: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY
- fi
- done <<< "$LANG_FILES"
- fi
- else
- echo "No manifest found — skipping language check." >> $GITHUB_STEP_SUMMARY
- fi
-
- if [ "${ERRORS}" -gt 0 ]; then
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "**${ERRORS} missing language file(s).**" >> $GITHUB_STEP_SUMMARY
- exit 1
- else
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "**Language file check passed.**" >> $GITHUB_STEP_SUMMARY
- fi
-
- - name: Check index.html files in directories
- run: |
- echo "### Index.html Check" >> $GITHUB_STEP_SUMMARY
- MISSING=0
- CHECKED=0
-
- for DIR in src/ htdocs/; do
- if [ -d "$DIR" ]; then
- while IFS= read -r -d '' SUBDIR; do
- CHECKED=$((CHECKED + 1))
- if [ ! -f "${SUBDIR}/index.html" ]; then
- echo "Missing index.html in: \`${SUBDIR}\`" >> $GITHUB_STEP_SUMMARY
- MISSING=$((MISSING + 1))
- fi
- done < <(find "$DIR" -type d -print0)
- fi
- done
-
- if [ "${CHECKED}" -eq 0 ]; then
- echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY
- elif [ "${MISSING}" -gt 0 ]; then
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY
- exit 1
- else
- echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY
- fi
-
- release-readiness:
- name: Release Readiness Check
- runs-on: ubuntu-latest
- if: github.event_name == 'pull_request' && github.base_ref == 'main'
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
-
- - name: Validate release readiness
- run: |
- echo "## Release Readiness" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- ERRORS=0
-
- # Extract version from README.md
- README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1)
- if [ -z "$README_VERSION" ]; then
- echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- else
- echo "README version: \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY
- fi
-
- # Find the extension manifest
- MANIFEST=""
- for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
- if grep -q "/dev/null; then
- MANIFEST="$XML_FILE"
- break
- fi
- done
-
- if [ -z "$MANIFEST" ]; then
- echo "No Joomla extension manifest found." >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- else
- echo "Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
-
- # Check matches README VERSION
- MANIFEST_VERSION=$(grep -oP '\K[^<]+' "$MANIFEST" | head -1)
- if [ -z "$MANIFEST_VERSION" ]; then
- echo "No \`\` tag in manifest." >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- elif [ -n "$README_VERSION" ] && [ "$MANIFEST_VERSION" != "$README_VERSION" ]; then
- echo "Manifest version \`${MANIFEST_VERSION}\` does not match README \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- else
- echo "Manifest version: \`${MANIFEST_VERSION}\`" >> $GITHUB_STEP_SUMMARY
- fi
-
- # Check extension type, element, client attributes
- EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1)
- if [ -z "$EXT_TYPE" ]; then
- echo "Missing \`type\` attribute on \`\` tag." >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- else
- echo "Extension type: \`${EXT_TYPE}\`" >> $GITHUB_STEP_SUMMARY
- fi
-
- # Element check (component/module/plugin name)
- HAS_ELEMENT=$(grep -cP '<(element|name)>' "$MANIFEST" 2>/dev/null || echo "0")
- if [ "$HAS_ELEMENT" -eq 0 ]; then
- echo "Missing \`\` or \`\` in manifest." >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- fi
-
- # Client attribute for site/admin modules and plugins
- if echo "$EXT_TYPE" | grep -qP "^(module|plugin)$"; then
- HAS_CLIENT=$(grep -cP ']*\bclient=' "$MANIFEST" 2>/dev/null || echo "0")
- if [ "$HAS_CLIENT" -eq 0 ]; then
- echo "Missing \`client\` attribute for ${EXT_TYPE} extension." >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- fi
- fi
- fi
-
- # Check updates.xml exists
- if [ -f "updates.xml" ] || [ -f "updates.xml" ]; then
- echo "Update XML present." >> $GITHUB_STEP_SUMMARY
- else
- echo "No updates.xml found." >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- fi
-
- # Check CHANGELOG.md exists
- if [ -f "CHANGELOG.md" ]; then
- echo "CHANGELOG.md present." >> $GITHUB_STEP_SUMMARY
- else
- echo "No CHANGELOG.md found." >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS + 1))
- fi
-
- echo "" >> $GITHUB_STEP_SUMMARY
- if [ $ERRORS -gt 0 ]; then
- echo "**${ERRORS} issue(s) must be resolved before release.**" >> $GITHUB_STEP_SUMMARY
- exit 1
- else
- echo "**Extension is ready for release.**" >> $GITHUB_STEP_SUMMARY
- fi
-
- test:
- name: Tests (PHP ${{ matrix.php }})
- runs-on: ubuntu-latest
- needs: lint-and-validate
-
- strategy:
- fail-fast: false
- matrix:
- php: ['8.2', '8.3']
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
-
- - name: Setup PHP ${{ matrix.php }}
- run: |
- php -v && composer --version
-
- - name: Install dependencies
- env:
- COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
- run: |
- if [ -f "composer.json" ]; then
- composer install \
- --no-interaction \
- --prefer-dist \
- --optimize-autoloader
- else
- echo "No composer.json found — skipping dependency install"
- fi
-
- - name: Run tests
- run: |
- echo "### Test Results (PHP ${{ matrix.php }})" >> $GITHUB_STEP_SUMMARY
- if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
- vendor/bin/phpunit --testdox 2>&1 | tee /tmp/test-output.log
- EXIT=${PIPESTATUS[0]}
- if [ $EXIT -eq 0 ]; then
- echo "All tests passed." >> $GITHUB_STEP_SUMMARY
- else
- echo "Test failures detected — see log." >> $GITHUB_STEP_SUMMARY
- echo '```' >> $GITHUB_STEP_SUMMARY
- cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY
- echo '```' >> $GITHUB_STEP_SUMMARY
- fi
- exit $EXIT
- else
- echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY
- fi
-
- static-analysis:
- name: PHPStan Analysis
- runs-on: ubuntu-latest
- needs: lint-and-validate
- continue-on-error: true
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
-
- - name: Setup PHP
- run: php -v && composer --version
-
- - name: Install dependencies
- env:
- COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
- run: |
- if [ -f "composer.json" ]; then
- composer install --no-interaction --prefer-dist --optimize-autoloader
- fi
-
- - name: Install PHPStan
- run: |
- if ! command -v vendor/bin/phpstan &> /dev/null; then
- composer require --dev phpstan/phpstan --no-interaction 2>/dev/null || \
- composer global require phpstan/phpstan --no-interaction
- fi
-
- - name: Run PHPStan
- run: |
- echo "### PHPStan Static Analysis" >> $GITHUB_STEP_SUMMARY
- PHPSTAN="vendor/bin/phpstan"
- if [ ! -f "$PHPSTAN" ]; then
- PHPSTAN=$(composer global config bin-dir --absolute 2>/dev/null)/phpstan
- fi
-
- # Determine source directory
- SRC_DIR=""
- for DIR in src/ htdocs/ lib/; do
- if [ -d "$DIR" ]; then
- SRC_DIR="$DIR"
- break
- fi
- done
-
- if [ -z "$SRC_DIR" ]; then
- echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY
- exit 0
- fi
-
- # Use repo phpstan.neon if present, otherwise use baseline config
- ARGS="analyse ${SRC_DIR} --memory-limit=512M --no-progress --error-format=table"
- if [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ]; then
- echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY
- else
- ARGS="$ARGS --level=3"
- echo "No phpstan.neon found — using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY
- fi
-
- $PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt
- EXIT=${PIPESTATUS[0]}
-
- if [ $EXIT -eq 0 ]; then
- echo "**No errors found.**" >> $GITHUB_STEP_SUMMARY
- else
- ERRORS=$(grep -c "ERROR" /tmp/phpstan-output.txt 2>/dev/null || echo "some")
- echo "**${ERRORS} error(s) found.** Review output above." >> $GITHUB_STEP_SUMMARY
- echo '```' >> $GITHUB_STEP_SUMMARY
- tail -30 /tmp/phpstan-output.txt >> $GITHUB_STEP_SUMMARY
- echo '```' >> $GITHUB_STEP_SUMMARY
- fi
- exit $EXIT
-
- pre-release:
- name: Build RC Pre-Release
- runs-on: ubuntu-latest
- needs: [lint-and-validate, test]
- if: github.event_name == 'pull_request'
-
- steps:
- - name: Trigger pre-release build
- env:
- GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
- REPO: ${{ github.repository }}
- BRANCH: ${{ github.head_ref }}
- run: |
- curl -s -X POST "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
- echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
- echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
diff --git a/.mokogitea/workflows/cleanup.yml b/.mokogitea/workflows/cleanup.yml
deleted file mode 100644
index 29ca4d4..0000000
--- a/.mokogitea/workflows/cleanup.yml
+++ /dev/null
@@ -1,87 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Maintenance
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
-# PATH: /.gitea/workflows/cleanup.yml
-# VERSION: 01.00.00
-# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
-
-name: "Universal: Repository Cleanup"
-
-on:
- schedule:
- - cron: '0 3 * * 0' # Weekly on Sunday at 03:00 UTC
- workflow_dispatch:
-
-permissions:
- contents: write
-
-env:
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
-
-jobs:
- cleanup:
- name: Clean Merged Branches
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
- token: ${{ secrets.MOKOGITEA_TOKEN }}
-
- - name: Delete merged branches
- env:
- GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
- run: |
- echo "=== Merged Branch Cleanup ==="
- API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
-
- # List branches via API
- BRANCHES=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
- "${API}/branches?limit=50" | jq -r '.[].name')
-
- DELETED=0
- for BRANCH in $BRANCHES; do
- # Skip protected branches
- case "$BRANCH" in
- main|master|develop|release/*|hotfix/*) continue ;;
- esac
-
- # Check if branch is merged into main
- if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
- echo " Deleting merged branch: ${BRANCH}"
- curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
- "${API}/branches/${BRANCH}" 2>/dev/null || true
- DELETED=$((DELETED + 1))
- fi
- done
-
- echo "Deleted ${DELETED} merged branch(es)"
-
- - name: Clean old workflow runs
- env:
- GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
- run: |
- echo "=== Workflow Run Cleanup ==="
- API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
- CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
-
- # Get old completed runs
- RUNS=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
- "${API}/actions/runs?status=completed&limit=50" | \
- jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
-
- DELETED=0
- for RUN_ID in $RUNS; do
- curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
- "${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
- DELETED=$((DELETED + 1))
- done
-
- echo "Deleted ${DELETED} old workflow run(s)"
diff --git a/.mokogitea/workflows/gitleaks.yml b/.mokogitea/workflows/gitleaks.yml
deleted file mode 100644
index e0fdd1d..0000000
--- a/.mokogitea/workflows/gitleaks.yml
+++ /dev/null
@@ -1,96 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Security
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
-# PATH: /templates/workflows/gitleaks.yml.template
-# VERSION: 01.00.00
-# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
-#
-# +========================================================================+
-# | SECRET SCANNING |
-# +========================================================================+
-# | |
-# | Scans commits for leaked secrets using Gitleaks. |
-# | |
-# | - PR scan: only new commits in the PR |
-# | - Scheduled: full repo scan weekly |
-# | - Alerts via ntfy on findings |
-# | |
-# +========================================================================+
-
-name: "Universal: Secret Scanning"
-
-on:
- pull_request:
- branches:
- - main
- - 'dev/**'
- schedule:
- - cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
- workflow_dispatch:
-
-permissions:
- contents: read
-
-env:
- NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
- NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
-
-jobs:
- gitleaks:
- name: Gitleaks Secret Scan
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Install Gitleaks
- run: |
- GITLEAKS_VERSION="8.21.2"
- curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
- | tar -xz -C /usr/local/bin gitleaks
- gitleaks version
-
- - name: Scan for secrets
- id: scan
- run: |
- echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY
- ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json"
-
- if [ "${{ github.event_name }}" = "pull_request" ]; then
- # Scan only PR commits
- ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
- echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY
- else
- echo "Full repository scan" >> $GITHUB_STEP_SUMMARY
- fi
-
- if gitleaks detect $ARGS 2>&1; then
- echo "result=clean" >> "$GITHUB_OUTPUT"
- echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY
- else
- echo "result=found" >> "$GITHUB_OUTPUT"
- FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown")
- echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY
- exit 1
- fi
-
- - name: Notify on findings
- if: failure() && steps.scan.outputs.result == 'found'
- run: |
- REPO="${{ github.event.repository.name }}"
- curl -sS \
- -H "Title: ${REPO} — secrets detected in code" \
- -H "Tags: rotating_light,key" \
- -H "Priority: urgent" \
- -d "Gitleaks found potential secrets. Review and rotate credentials immediately." \
- "${NTFY_URL}/${NTFY_TOPIC}" || true
diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml
deleted file mode 100644
index ab17b80..0000000
--- a/.mokogitea/workflows/issue-branch.yml
+++ /dev/null
@@ -1,73 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Automation
-# VERSION: 02.26.18
-# 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
diff --git a/.mokogitea/workflows/notify.yml b/.mokogitea/workflows/notify.yml
deleted file mode 100644
index cde4541..0000000
--- a/.mokogitea/workflows/notify.yml
+++ /dev/null
@@ -1,70 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Notifications
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
-# PATH: /.gitea/workflows/notify.yml
-# VERSION: 01.00.00
-# BRIEF: Push notifications via ntfy on release success or workflow failure
-
-name: "Universal: Notifications"
-
-on:
- workflow_run:
- workflows:
- - "Joomla Build & Release"
- - "Joomla Extension CI"
- - "Deploy"
- types:
- - completed
-
-permissions:
- contents: read
-
-env:
- NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
- NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }}
-
-jobs:
- notify:
- name: Send Notification
- runs-on: ubuntu-latest
- if: >-
- github.event.workflow_run.conclusion == 'success' ||
- github.event.workflow_run.conclusion == 'failure'
-
- steps:
- - name: Notify on success (releases only)
- if: >-
- github.event.workflow_run.conclusion == 'success' &&
- contains(github.event.workflow_run.name, 'Release')
- run: |
- REPO="${{ github.event.repository.name }}"
- WORKFLOW="${{ github.event.workflow_run.name }}"
- URL="${{ github.event.workflow_run.html_url }}"
-
- curl -sS \
- -H "Title: ${REPO} released" \
- -H "Tags: white_check_mark,package" \
- -H "Priority: default" \
- -H "Click: ${URL}" \
- -d "${WORKFLOW} completed successfully." \
- "${NTFY_URL}/${NTFY_TOPIC}"
-
- - name: Notify on failure
- if: github.event.workflow_run.conclusion == 'failure'
- run: |
- REPO="${{ github.event.repository.name }}"
- WORKFLOW="${{ github.event.workflow_run.name }}"
- URL="${{ github.event.workflow_run.html_url }}"
-
- curl -sS \
- -H "Title: ${REPO} workflow failed" \
- -H "Tags: x,warning" \
- -H "Priority: high" \
- -H "Click: ${URL}" \
- -d "${WORKFLOW} failed. Check the run for details." \
- "${NTFY_URL}/${NTFY_TOPIC}"
diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml
deleted file mode 100644
index ce64a27..0000000
--- a/.mokogitea/workflows/pr-check.yml
+++ /dev/null
@@ -1,236 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.CI
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
-# PATH: /templates/workflows/universal/pr-check.yml.template
-# VERSION: 05.00.00
-# BRIEF: PR gate — branch policy + code validation before merge
-
-name: "Universal: PR Check"
-
-on:
- pull_request:
- types: [opened, synchronize, reopened, edited]
-
-permissions:
- contents: read
- pull-requests: write
-
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
-
-jobs:
- # ── Branch Policy ──────────────────────────────────────────────────────
- branch-policy:
- name: Branch Policy
- runs-on: ubuntu-latest
- steps:
- - name: Check branch merge target
- run: |
- HEAD="${{ github.head_ref }}"
- BASE="${{ github.base_ref }}"
-
- echo "PR: ${HEAD} → ${BASE}"
-
- ALLOWED=true
- REASON=""
-
- case "$HEAD" in
- feature/*|feat/*)
- if [ "$BASE" != "dev" ]; then
- ALLOWED=false
- REASON="Feature branches must target 'dev', not '${BASE}'"
- fi
- ;;
- fix/*|bugfix/*)
- if [ "$BASE" != "dev" ]; then
- ALLOWED=false
- REASON="Fix branches must target 'dev', not '${BASE}'"
- fi
- ;;
- patch/*)
- if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
- ALLOWED=false
- REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
- fi
- ;;
- hotfix/*)
- if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
- ALLOWED=false
- REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
- fi
- ;;
- rc)
- if [ "$BASE" != "main" ]; then
- ALLOWED=false
- REASON="RC branch can only merge into 'main', not '${BASE}'"
- fi
- ;;
- dev)
- if [ "$BASE" != "main" ]; then
- ALLOWED=false
- REASON="Dev branch can only merge into 'main', not '${BASE}'"
- fi
- ;;
- esac
-
- if [ "$ALLOWED" = false ]; then
- echo "::error::${REASON}"
- echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "${REASON}" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
- echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
- exit 1
- fi
-
- echo "Branch policy: OK (${HEAD} → ${BASE})"
- echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
-
- # ── Code Validation ────────────────────────────────────────────────────
- validate:
- name: Validate PR
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Detect platform
- id: platform
- run: |
- # Read platform from XML manifest ( tag) or plain text fallback
- PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
- [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
- [ -z "$PLATFORM" ] && PLATFORM="generic"
- echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
-
- - name: Setup PHP
- if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
- run: |
- if ! command -v php &> /dev/null; then
- sudo apt-get update -qq
- sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
- fi
-
- - name: PHP syntax check
- if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
- run: |
- ERRORS=0
- while IFS= read -r -d '' file; do
- if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
- ERRORS=$((ERRORS + 1))
- fi
- done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
- echo "PHP lint: ${ERRORS} error(s)"
- [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
-
- - name: Validate platform manifest
- run: |
- PLATFORM="${{ steps.platform.outputs.platform }}"
- case "$PLATFORM" in
- joomla)
- MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1)
- if [ -z "$MANIFEST" ]; then
- echo "::warning::No Joomla manifest found (WaaS site)"
- exit 0
- fi
- echo "Manifest: ${MANIFEST}"
- if command -v php &> /dev/null; then
- php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
- fi
- for ELEMENT in name version description; do
- grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
- done
- echo "Joomla manifest valid"
- ;;
- dolibarr)
- MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
- if [ -z "$MOD_FILE" ]; then
- echo "::error::No mod*.class.php found"
- exit 1
- fi
- echo "Dolibarr module: ${MOD_FILE}"
- ;;
- *)
- echo "Generic platform — no manifest validation"
- ;;
- esac
-
- - name: Check update stream format
- run: |
- PLATFORM="${{ steps.platform.outputs.platform }}"
- case "$PLATFORM" in
- joomla)
- if [ -f "updates.xml" ]; then
- if command -v php &> /dev/null; then
- php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
- fi
- echo "updates.xml valid"
- fi
- ;;
- dolibarr)
- [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
- ;;
- esac
-
- - name: Check changelog has unreleased entry
- run: |
- if [ ! -f "CHANGELOG.md" ]; then
- echo "::warning::No CHANGELOG.md found"
- exit 0
- fi
- # Check for content under [Unreleased] section
- if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
- echo "::error::CHANGELOG.md missing [Unreleased] section"
- exit 1
- fi
- # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
- UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
- if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
- echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
- echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
- echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
- echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
- exit 1
- fi
- echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
-
- - name: Verify package source
- run: |
- SOURCE_DIR="src"
- [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
- if [ ! -d "$SOURCE_DIR" ]; then
- echo "::warning::No src/ or htdocs/ directory"
- exit 0
- fi
- FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
- echo "Source: ${FILE_COUNT} files"
- [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
-
- # ── Pre-Release RC Build ─────────────────────────────────────────────────
- pre-release:
- name: Build RC Package
- runs-on: ubuntu-latest
- needs: [branch-policy, validate]
-
- steps:
- - name: Trigger RC pre-release
- env:
- GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
- REPO: ${{ github.repository }}
- BRANCH: ${{ github.head_ref }}
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- run: |
- curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
- echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
- echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml
deleted file mode 100644
index 162b08f..0000000
--- a/.mokogitea/workflows/pre-release.yml
+++ /dev/null
@@ -1,233 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Release
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
-# PATH: /templates/workflows/universal/pre-release.yml.template
-# VERSION: 05.01.00
-# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
-
-name: "Universal: Pre-Release"
-
-on:
- pull_request:
- types: [closed]
- branches:
- - dev
- workflow_dispatch:
- inputs:
- stability:
- description: 'Pre-release channel'
- required: true
- type: choice
- options:
- - development
- - alpha
- - beta
- - release-candidate
-
-permissions:
- contents: write
-
-env:
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
- GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
-
-jobs:
- build:
- name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
- runs-on: release
- if: >-
- github.event_name == 'workflow_dispatch' ||
- (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
- token: ${{ secrets.MOKOGITEA_TOKEN }}
-
- - name: Setup moko-platform tools
- env:
- MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
- MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
- run: |
- if ! command -v composer &> /dev/null; then
- sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
- fi
- # Always fetch latest CLI tools — never use stale cache from previous runs
- rm -rf /tmp/moko-platform-api
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
- /tmp/moko-platform-api
- cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
- echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
-
- - name: Detect platform
- id: platform
- run: |
- php ${MOKO_CLI}/manifest_read.php --path . --github-output
-
- - name: Resolve metadata and bump version
- id: meta
- run: |
- STABILITY="${{ inputs.stability || 'development' }}"
-
- case "$STABILITY" in
- development) SUFFIX="-dev"; TAG="development" ;;
- alpha) SUFFIX="-alpha"; TAG="alpha" ;;
- beta) SUFFIX="-beta"; TAG="beta" ;;
- release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
- esac
-
- # Read current version (bump already handled by push workflow)
- VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
- [ -z "$VERSION" ] && VERSION="00.00.01"
-
- # Strip any existing suffix from version before applying stability
- VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
-
- php ${MOKO_CLI}/version_set_platform.php \
- --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
-
- # Verify version consistency across all files
- php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
-
- # Update VERSION variable with suffix
- if [ -n "$SUFFIX" ]; then
- VERSION="${VERSION}${SUFFIX}"
- fi
-
- # Commit version bump
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- git add -A
- git diff --cached --quiet || {
- git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
- git push origin HEAD 2>&1
- }
-
- # Auto-detect element via manifest_element.php
- php ${MOKO_CLI}/manifest_element.php \
- --path . --version "$VERSION" --stability "$STABILITY" \
- --repo "${GITEA_REPO}" --github-output
-
- # Read back element outputs
- EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
- ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
- [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
- [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
-
- echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
- echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
- echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
- echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
- echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
-
- echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
-
- - name: Create release
- id: release
- run: |
- TAG="${{ steps.meta.outputs.tag }}"
- VERSION="${{ steps.meta.outputs.version }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php ${MOKO_CLI}/release_create.php \
- --path . --version "$VERSION" --tag "$TAG" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
- --repo "${GITEA_REPO}" --branch dev --prerelease
-
- - name: Build package and upload
- id: package
- run: |
- VERSION="${{ steps.meta.outputs.version }}"
- TAG="${{ steps.meta.outputs.tag }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php ${MOKO_CLI}/release_package.php \
- --path . --version "$VERSION" --tag "$TAG" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
- --repo "${GITEA_REPO}" --output /tmp || true
-
- - name: Update updates.xml
- if: steps.platform.outputs.platform == 'joomla'
- run: |
- VERSION="${{ steps.meta.outputs.version }}"
- STABILITY="${{ steps.meta.outputs.stability }}"
- SHA256="${{ steps.package.outputs.sha256_zip }}"
-
- if [ ! -f "updates.xml" ]; then
- echo "No updates.xml -- skipping"
- exit 0
- fi
-
- SHA_FLAG=""
- [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
-
- php ${MOKO_CLI}/updates_xml_build.php \
- --path . --version "${VERSION}" --stability "${STABILITY}" \
- --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
- ${SHA_FLAG}
-
- # Commit and push
- if ! git diff --quiet updates.xml 2>/dev/null; then
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git add updates.xml
- git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
- git push origin HEAD 2>&1 || echo "WARNING: push failed"
- fi
-
- - name: "Sync updates.xml to all branches"
- if: steps.platform.outputs.platform == 'joomla'
- run: |
- CURRENT_BRANCH="${{ github.ref_name }}"
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
-
- for BRANCH in main dev; do
- [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
- echo "Syncing updates.xml -> ${BRANCH}"
- git fetch origin "${BRANCH}" 2>/dev/null || continue
- git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue
- git checkout "${CURRENT_BRANCH}" -- updates.xml
- if ! git diff --quiet updates.xml 2>/dev/null; then
- git add updates.xml
- git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
- git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
- fi
- git checkout "${CURRENT_BRANCH}" 2>/dev/null
- done
-
- - name: "Delete lesser pre-release channels (cascade)"
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
-
- php ${MOKO_CLI}/release_cascade.php \
- --stability "${{ steps.meta.outputs.stability }}" \
- --token "${TOKEN}" \
- --api-base "${API_BASE}"
-
- - name: Summary
- if: always()
- run: |
- VERSION="${{ steps.meta.outputs.version }}"
- STABILITY="${{ steps.meta.outputs.stability }}"
- ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
- SHA256="${{ steps.package.outputs.sha256_zip }}"
- echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
- echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
- echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
- echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml
deleted file mode 100644
index be52e37..0000000
--- a/.mokogitea/workflows/repo-health.yml
+++ /dev/null
@@ -1,769 +0,0 @@
-# ============================================================================
-# Copyright (C) 2025 Moko Consulting
-#
-# This file is part of a Moko Consulting project.
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Validation
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
-# PATH: /templates/workflows/joomla/repo_health.yml.template
-# VERSION: 04.06.00
-# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
-# ============================================================================
-
-name: "Generic: Repo Health"
-
-defaults:
- run:
- shell: bash
-
-on:
- workflow_dispatch:
- inputs:
- profile:
- description: 'Validation profile: all, release, scripts, or repo'
- required: true
- default: all
- type: choice
- options:
- - all
- - release
- - scripts
- - repo
- pull_request:
- push:
-
-permissions:
- contents: read
-
-env:
- # Release policy - Repository Variables Only
- RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX
- RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX
-
- # Scripts governance policy
- SCRIPTS_REQUIRED_DIRS:
- SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
-
- # Repo health policy
- REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/
- REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
- REPO_DISALLOWED_DIRS:
- REPO_DISALLOWED_FILES: TODO.md,todo.md
-
- # Extended checks toggles
- EXTENDED_CHECKS: "true"
-
- # File / directory variables
- DOCS_INDEX: docs/docs-index.md
- SCRIPT_DIR: scripts
- WORKFLOWS_DIR: .mokogitea/workflows
- SHELLCHECK_PATTERN: '*.sh'
- SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
-
-jobs:
- access_check:
- name: Access control
- runs-on: ubuntu-latest
- timeout-minutes: 10
- permissions:
- contents: read
-
- outputs:
- allowed: ${{ steps.perm.outputs.allowed }}
- permission: ${{ steps.perm.outputs.permission }}
-
- steps:
- - name: Check actor permission (admin only)
- id: perm
- env:
- TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
- REPO: ${{ github.repository }}
- ACTOR: ${{ github.actor }}
- run: |
- set -euo pipefail
- ALLOWED=false
- PERMISSION=unknown
- METHOD=""
-
- # Hardcoded authorized users — always allowed
- case "$ACTOR" in
- jmiller|gitea-actions[bot])
- ALLOWED=true
- PERMISSION=admin
- METHOD="hardcoded allowlist"
- ;;
- *)
- # Detect platform and check permissions via API
- API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}"
- RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \
- "${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}')
- PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown")
- if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then
- ALLOWED=true
- fi
- METHOD="collaborator API"
- ;;
- esac
-
- echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT"
- echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT"
-
- {
- echo "## Access Authorization"
- echo ""
- echo "| Field | Value |"
- echo "|-------|-------|"
- echo "| **Actor** | \`${ACTOR}\` |"
- echo "| **Repository** | \`${REPO}\` |"
- echo "| **Permission** | \`${PERMISSION}\` |"
- echo "| **Method** | ${METHOD} |"
- echo "| **Authorized** | ${ALLOWED} |"
- echo ""
- if [ "$ALLOWED" = "true" ]; then
- echo "${ACTOR} authorized (${METHOD})"
- else
- echo "${ACTOR} is NOT authorized. Requires admin or maintain role."
- fi
- } >> "${GITHUB_STEP_SUMMARY}"
-
- - name: Deny execution when not permitted
- if: ${{ steps.perm.outputs.allowed != 'true' }}
- run: |
- set -euo pipefail
- printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
- exit 1
-
- release_config:
- name: Release configuration
- needs: access_check
- if: ${{ needs.access_check.outputs.allowed == 'true' }}
- runs-on: ubuntu-latest
- timeout-minutes: 20
- permissions:
- contents: read
-
- steps:
- - name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- fetch-depth: 0
-
- - name: Guardrails release vars
- env:
- PROFILE_RAW: ${{ github.event.inputs.profile }}
- RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }}
- DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
- run: |
- set -euo pipefail
-
- profile="${PROFILE_RAW:-all}"
- case "${profile}" in
- all|release|scripts|repo) ;;
- *)
- printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
- exit 1
- ;;
- esac
-
- if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then
- {
- printf '%s\n' '### Release configuration (Repository Variables)'
- printf '%s\n' "Profile: ${profile}"
- printf '%s\n' 'Status: SKIPPED'
- printf '%s\n' 'Reason: profile excludes release validation'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- exit 0
- fi
-
- IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}"
- IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}"
-
- missing=()
- missing_optional=()
-
- for k in "${required[@]}"; do
- v="${!k:-}"
- [ -z "${v}" ] && missing+=("${k}")
- done
-
- for k in "${optional[@]}"; do
- v="${!k:-}"
- [ -z "${v}" ] && missing_optional+=("${k}")
- done
-
- {
- printf '%s\n' '### Release configuration (Repository Variables)'
- printf '%s\n' "Profile: ${profile}"
- printf '%s\n' '| Variable | Status |'
- printf '%s\n' '|---|---|'
- printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |"
- printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |"
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
-
- if [ "${#missing_optional[@]}" -gt 0 ]; then
- {
- printf '%s\n' '### Missing optional repository variables'
- for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
-
- if [ "${#missing[@]}" -gt 0 ]; then
- {
- printf '%s\n' '### Missing required repository variables'
- for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done
- printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.'
- } >> "${GITHUB_STEP_SUMMARY}"
- exit 1
- fi
-
- {
- printf '%s\n' '### Repository variables validation result'
- printf '%s\n' 'Status: OK'
- printf '%s\n' 'All required repository variables present.'
- printf '%s\n' ''
- printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
-
- scripts_governance:
- name: Scripts governance
- needs: access_check
- if: ${{ needs.access_check.outputs.allowed == 'true' }}
- runs-on: ubuntu-latest
- timeout-minutes: 15
- permissions:
- contents: read
-
- steps:
- - name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- fetch-depth: 0
-
- - name: Scripts folder checks
- env:
- PROFILE_RAW: ${{ github.event.inputs.profile }}
- run: |
- set -euo pipefail
-
- profile="${PROFILE_RAW:-all}"
- case "${profile}" in
- all|release|scripts|repo) ;;
- *)
- printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
- exit 1
- ;;
- esac
-
- if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then
- {
- printf '%s\n' '### Scripts governance'
- printf '%s\n' "Profile: ${profile}"
- printf '%s\n' 'Status: SKIPPED'
- printf '%s\n' 'Reason: profile excludes scripts governance'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- exit 0
- fi
-
- if [ ! -d "${SCRIPT_DIR}" ]; then
- {
- printf '%s\n' '### Scripts governance'
- printf '%s\n' 'Status: OK (advisory)'
- printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- exit 0
- fi
-
- if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi
- IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
-
- missing_dirs=()
- unapproved_dirs=()
-
- for d in "${required_dirs[@]}"; do
- req="${d%/}"
- [ ! -d "${req}" ] && missing_dirs+=("${req}/")
- done
-
- while IFS= read -r d; do
- allowed=false
- for a in "${allowed_dirs[@]}"; do
- a_norm="${a%/}"
- [ "${d%/}" = "${a_norm}" ] && allowed=true
- done
- [ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/")
- done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##')
-
- {
- printf '%s\n' '### Scripts governance'
- printf '%s\n' "Profile: ${profile}"
- printf '%s\n' '| Area | Status | Notes |'
- printf '%s\n' '|---|---|---|'
-
- if [ "${#missing_dirs[@]}" -gt 0 ]; then
- printf '%s\n' '| Required directories | Warning | Missing required subfolders |'
- else
- printf '%s\n' '| Required directories | OK | All required subfolders present |'
- fi
-
- if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
- printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |'
- else
- printf '%s\n' '| Directory policy | OK | No unapproved directories |'
- fi
-
- printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |'
- printf '\n'
-
- if [ "${#missing_dirs[@]}" -gt 0 ]; then
- printf '%s\n' 'Missing required script directories:'
- for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done
- printf '\n'
- else
- printf '%s\n' 'Missing required script directories: none.'
- printf '\n'
- fi
-
- if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
- printf '%s\n' 'Unapproved script directories detected:'
- for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done
- printf '\n'
- else
- printf '%s\n' 'Unapproved script directories detected: none.'
- printf '\n'
- fi
-
- printf '%s\n' 'Scripts governance completed in advisory mode.'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
-
- repo_health:
- name: Repository health
- needs: access_check
- if: ${{ needs.access_check.outputs.allowed == 'true' }}
- runs-on: ubuntu-latest
- timeout-minutes: 20
- permissions:
- contents: read
-
- steps:
- - name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- fetch-depth: 0
-
- - name: Repository health checks
- env:
- PROFILE_RAW: ${{ github.event.inputs.profile }}
- run: |
- set -euo pipefail
-
- profile="${PROFILE_RAW:-all}"
- case "${profile}" in
- all|release|scripts|repo) ;;
- *)
- printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
- exit 1
- ;;
- esac
-
- if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then
- {
- printf '%s\n' '### Repository health'
- printf '%s\n' "Profile: ${profile}"
- printf '%s\n' 'Status: SKIPPED'
- printf '%s\n' 'Reason: profile excludes repository health'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- exit 0
- fi
-
- IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
- IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
- if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi
- IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}"
-
- missing_required=()
- missing_optional=()
-
- # Source directory: src/ or htdocs/ (either is valid for extension repos)
- SOURCE_DIR=""
- if [ -d "src" ]; then
- SOURCE_DIR="src"
- elif [ -d "htdocs" ]; then
- SOURCE_DIR="htdocs"
- elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
- # Platform/tooling repos don't need src/
- SOURCE_DIR=""
- else
- missing_required+=("src/ or htdocs/ (source directory required)")
- fi
-
- for item in "${required_artifacts[@]}"; do
- if printf '%s' "${item}" | grep -q '/$'; then
- d="${item%/}"
- [ ! -d "${d}" ] && missing_required+=("${item}")
- else
- [ ! -f "${item}" ] && missing_required+=("${item}")
- fi
- done
-
- for f in "${optional_files[@]}"; do
- if printf '%s' "${f}" | grep -q '/$'; then
- d="${f%/}"
- [ ! -d "${d}" ] && missing_optional+=("${f}")
- else
- [ ! -f "${f}" ] && missing_optional+=("${f}")
- fi
- done
-
- for d in "${disallowed_dirs[@]}"; do
- d_norm="${d%/}"
- [ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)")
- done
-
- for f in "${disallowed_files[@]}"; do
- [ -f "${f}" ] && missing_required+=("${f} (disallowed)")
- done
-
- git fetch origin --prune
-
- dev_paths=()
- dev_branches=()
-
- while IFS= read -r b; do
- name="${b#origin/}"
- if [ "${name}" = 'dev' ]; then
- dev_branches+=("${name}")
- else
- dev_paths+=("${name}")
- fi
- done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
-
- if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then
- missing_required+=("dev or dev/* branch")
- fi
-
- content_warnings=()
-
- if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then
- content_warnings+=("CHANGELOG.md missing '# Changelog' header")
- fi
-
- if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then
- content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)")
- fi
-
- if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then
- content_warnings+=("LICENSE does not look like a GPL text")
- fi
-
- if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then
- content_warnings+=("README.md missing expected brand keyword")
- fi
-
- export PROFILE_RAW="${profile}"
- export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")"
- export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
- export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
-
- report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}")
-
- {
- printf '%s\n' '### Repository health'
- printf '%s\n' "Profile: ${profile}"
- printf '%s\n' '| Metric | Value |'
- printf '%s\n' '|---|---|'
- printf '%s\n' "| Missing required | ${#missing_required[@]} |"
- printf '%s\n' "| Missing optional | ${#missing_optional[@]} |"
- printf '%s\n' "| Content warnings | ${#content_warnings[@]} |"
- printf '\n'
-
- printf '%s\n' '### Guardrails report (JSON)'
- printf '%s\n' '```json'
- printf '%s\n' "${report_json}"
- printf '%s\n' '```'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
-
- if [ "${#missing_required[@]}" -gt 0 ]; then
- {
- printf '%s\n' '### Missing required repo artifacts'
- for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done
- printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- exit 1
- fi
-
- if [ "${#missing_optional[@]}" -gt 0 ]; then
- {
- printf '%s\n' '### Missing optional repo artifacts'
- for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
-
- if [ "${#content_warnings[@]}" -gt 0 ]; then
- {
- printf '%s\n' '### Repo content warnings'
- for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
-
- # -- Joomla-specific checks --
- joomla_findings=()
-
- MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '/dev/null | head -1 || true)"
- if [ -z "${MANIFEST}" ]; then
- joomla_findings+=("Joomla XML manifest not found (no *.xml with tag)")
- else
- if ! grep -qP '' "${MANIFEST}"; then
- joomla_findings+=("XML manifest: tag missing")
- fi
- if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then
- joomla_findings+=("XML manifest: type attribute missing or invalid")
- fi
- if ! grep -qP '' "${MANIFEST}"; then
- joomla_findings+=("XML manifest: tag missing")
- fi
- if ! grep -qP '' "${MANIFEST}"; then
- joomla_findings+=("XML manifest: tag missing")
- fi
- if ! grep -qP ' missing (required for Joomla 5+)")
- fi
- fi
-
- INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)"
- if [ "${INI_COUNT}" -eq 0 ]; then
- joomla_findings+=("No .ini language files found")
- fi
-
- if [ ! -f 'updates.xml' ]; then
- joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
- fi
-
- if [ -n "${SOURCE_DIR}" ]; then
- INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
- for dir in "${INDEX_DIRS[@]}"; do
- if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
- joomla_findings+=("${dir}/index.html missing (directory listing protection)")
- fi
- done
- fi
-
- if [ "${#joomla_findings[@]}" -gt 0 ]; then
- {
- printf '%s\n' '### Joomla extension checks'
- printf '%s\n' '| Check | Status |'
- printf '%s\n' '|---|---|'
- for f in "${joomla_findings[@]}"; do
- printf '%s\n' "| ${f} | Warning |"
- done
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- else
- {
- printf '%s\n' '### Joomla extension checks'
- printf '%s\n' 'All Joomla-specific checks passed.'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
-
- extended_enabled="${EXTENDED_CHECKS:-true}"
- extended_findings=()
-
- if [ "${extended_enabled}" = 'true' ]; then
- if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then
- :
- else
- extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)")
- fi
-
- if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then
- bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)"
- if [ -n "${bad_refs}" ]; then
- extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt")
- {
- printf '%s\n' '### Workflow pinning advisory'
- printf '%s\n' 'Found uses: entries pinned to main/master:'
- printf '%s\n' '```'
- printf '%s\n' "${bad_refs}"
- printf '%s\n' '```'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
- fi
-
- if [ -f "${DOCS_INDEX}" ]; then
- missing_links=""
- while IFS= read -r docline; do
- for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do
- case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac
- linkpath="${link%%#*}"
- linkpath="${linkpath%%\?*}"
- [ -z "$linkpath" ] && continue
- if [ "${linkpath:0:1}" = "/" ]; then
- testpath="${linkpath#/}"
- else
- testpath="$(dirname "${DOCS_INDEX}")/${linkpath}"
- fi
- [ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} "
- done
- done < "${DOCS_INDEX}"
- if [ -n "${missing_links}" ]; then
- extended_findings+=("docs/docs-index.md contains broken relative links")
- {
- printf '%s\n' '### Docs index link integrity'
- printf '%s\n' 'Broken relative links:'
- for bl in ${missing_links}; do
- printf '%s\n' "- ${bl}"
- done
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
- fi
-
- if [ -d "${SCRIPT_DIR}" ]; then
- if ! command -v shellcheck >/dev/null 2>&1; then
- sudo apt-get update -qq
- sudo apt-get install -y shellcheck >/dev/null
- fi
-
- sc_out=''
- while IFS= read -r shf; do
- [ -z "${shf}" ] && continue
- out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)"
- if [ -n "${out_one}" ]; then
- sc_out="${sc_out}${out_one}\n"
- fi
- done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort)
-
- if [ -n "${sc_out}" ]; then
- extended_findings+=("ShellCheck warnings detected (advisory)")
- sc_head="$(printf '%s' "${sc_out}" | head -n 200)"
- {
- printf '%s\n' '### ShellCheck (advisory)'
- printf '%s\n' '```'
- printf '%s\n' "${sc_head}"
- printf '%s\n' '```'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
- fi
-
- spdx_missing=()
- IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}"
- spdx_args=()
- for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done
-
- while IFS= read -r f; do
- [ -z "${f}" ] && continue
- if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then
- spdx_missing+=("${f}")
- fi
- done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true)
-
- if [ "${#spdx_missing[@]}" -gt 0 ]; then
- extended_findings+=("SPDX header missing in some tracked files (advisory)")
- {
- printf '%s\n' '### SPDX header advisory'
- printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):'
- for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
-
- stale_cutoff_days=180
- stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)"
- if [ -n "${stale_branches}" ]; then
- extended_findings+=("Stale remote branches detected (advisory)")
- {
- printf '%s\n' '### Git hygiene advisory'
- printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):"
- while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}"
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
- fi
-
- {
- printf '%s\n' '### Guardrails coverage matrix'
- printf '%s\n' '| Domain | Status | Notes |'
- printf '%s\n' '|---|---|---|'
- printf '%s\n' '| Access control | OK | Admin-only execution gate |'
- printf '%s\n' '| Release variables | OK | Repository variables validation |'
- printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
- printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
- printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
- if [ "${extended_enabled}" = 'true' ]; then
- if [ "${#extended_findings[@]}" -gt 0 ]; then
- printf '%s\n' '| Extended checks | Warning | See extended findings below |'
- else
- printf '%s\n' '| Extended checks | OK | No findings |'
- fi
- else
- printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |'
- fi
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
-
- if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then
- {
- printf '%s\n' '### Extended findings (advisory)'
- for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
-
- printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
-
-
- site-health:
- name: Site Health
- runs-on: ubuntu-latest
- if: github.event_name == 'workflow_dispatch'
- steps:
- - uses: actions/checkout@v4
-
- - name: Setup PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: '8.3'
-
- - name: Uptime check
- if: env.URLS != ''
- run: |
- echo "$URLS" > /tmp/urls.txt
- php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down"
- rm -f /tmp/urls.txt
- env:
- URLS: ${{ vars.MONITORED_URLS }}
-
- - name: SSL certificate check
- if: env.DOMAINS != ''
- run: |
- echo "$DOMAINS" > /tmp/domains.txt
- php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon"
- rm -f /tmp/domains.txt
- env:
- DOMAINS: ${{ vars.MONITORED_DOMAINS }}
-
- - name: Summary
- if: always()
- run: |
- echo "### Site Health" >> $GITHUB_STEP_SUMMARY
- echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY
-
diff --git a/.mokogitea/workflows/security-audit.yml b/.mokogitea/workflows/security-audit.yml
deleted file mode 100644
index 714d407..0000000
--- a/.mokogitea/workflows/security-audit.yml
+++ /dev/null
@@ -1,98 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Security
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
-# PATH: /.gitea/workflows/security-audit.yml
-# VERSION: 01.00.00
-# BRIEF: Dependency vulnerability scanning for composer and npm packages
-
-name: "Universal: Security Audit"
-
-on:
- schedule:
- - cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC
- pull_request:
- branches:
- - main
- paths:
- - 'composer.json'
- - 'composer.lock'
- - 'package.json'
- - 'package-lock.json'
- workflow_dispatch:
-
-permissions:
- contents: read
-
-env:
- NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
- NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
-
-jobs:
- audit:
- name: Dependency Audit
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Composer audit
- if: hashFiles('composer.lock') != ''
- run: |
- echo "=== Composer Security Audit ==="
- if ! command -v composer &> /dev/null; then
- sudo apt-get update -qq
- sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1
- fi
- composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt
- RESULT=$?
- if [ $RESULT -ne 0 ]; then
- echo "::warning::Composer vulnerabilities found"
- echo "composer_vulnerable=true" >> "$GITHUB_ENV"
- else
- echo "No known vulnerabilities in composer dependencies"
- fi
-
- - name: NPM audit
- if: hashFiles('package-lock.json') != ''
- run: |
- echo "=== NPM Security Audit ==="
- npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true
- if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then
- echo "No known vulnerabilities in npm dependencies"
- else
- echo "::warning::NPM vulnerabilities found"
- echo "npm_vulnerable=true" >> "$GITHUB_ENV"
- fi
-
- - name: Notify on vulnerabilities
- if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true'
- run: |
- REPO="${{ github.event.repository.name }}"
- curl -sS \
- -H "Title: ${REPO} has vulnerable dependencies" \
- -H "Tags: lock,warning" \
- -H "Priority: high" \
- -d "Security audit found vulnerabilities. Review dependency updates." \
- "${NTFY_URL}/${NTFY_TOPIC}" || true
-
-
- - name: Joomla version audit
- if: always()
- run: |
- if [ -f "monitoring/joomla-version-audit.php" ] && [ -n "$JOOMLA_SITES" ]; then
- echo "$JOOMLA_SITES" > /tmp/sites.json
- php monitoring/joomla-version-audit.php --sites /tmp/sites.json || true
- echo "### Joomla Version Audit" >> $GITHUB_STEP_SUMMARY
- rm -f /tmp/sites.json
- else
- echo "Joomla audit skipped (no script or JOOMLA_SITES_JSON not configured)"
- fi
- env:
- JOOMLA_SITES: ${{ vars.JOOMLA_SITES_JSON }}
-
diff --git a/.mokogitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml
deleted file mode 100644
index 339d3f5..0000000
--- a/.mokogitea/workflows/update-server.yml
+++ /dev/null
@@ -1,312 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Universal
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
-# PATH: /templates/workflows/update-server.yml
-# VERSION: 05.00.00
-# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
-#
-# Thin wrapper around moko-platform CLI tools.
-# Builds packages, updates updates.xml, and optionally deploys via SFTP.
-#
-# Joomla filters update entries by the user's "Minimum Stability" setting.
-
-name: "Update Server"
-
-on:
- push:
- branches:
- - 'dev'
- - 'dev/**'
- - 'alpha/**'
- - 'beta/**'
- - 'rc/**'
- paths:
- - 'src/**'
- - 'htdocs/**'
- pull_request:
- types: [closed]
- branches:
- - 'dev'
- - 'dev/**'
- - 'alpha/**'
- - 'beta/**'
- - 'rc/**'
- paths:
- - 'src/**'
- - 'htdocs/**'
- workflow_dispatch:
- inputs:
- stability:
- description: 'Stability tag'
- required: true
- default: 'development'
- type: choice
- options:
- - development
- - alpha
- - beta
- - rc
- - stable
-
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
- GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
-
-permissions:
- contents: write
-
-jobs:
- update-xml:
- name: Update Server
- runs-on: release
- if: >-
- github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- token: ${{ secrets.MOKOGITEA_TOKEN }}
- fetch-depth: 0
-
- - name: Setup moko-platform tools
- env:
- MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
- MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
- COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
- run: |
- if ! command -v composer &> /dev/null; then
- sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
- fi
- # Always fetch latest CLI tools — never use stale cache from previous runs
- rm -rf /tmp/moko-platform
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
- /tmp/moko-platform 2>/dev/null || true
- if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
- cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
- fi
- echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
-
- - name: Detect platform
- id: platform
- run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
-
- - name: Resolve stability and bump version
- id: meta
- run: |
- BRANCH="${{ github.ref_name }}"
-
- # Configure git for bot pushes
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
-
- # Auto-bump patch version
- php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
-
- VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0")
-
- # Strip any existing suffix before applying stability
- VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
-
- # Determine stability from branch or manual input
- if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
- STABILITY="${{ inputs.stability }}"
- elif [[ "$BRANCH" == rc/* ]]; then
- STABILITY="rc"
- elif [[ "$BRANCH" == beta/* ]]; then
- STABILITY="beta"
- elif [[ "$BRANCH" == alpha/* ]]; then
- STABILITY="alpha"
- else
- STABILITY="development"
- fi
-
- # Version suffix per stability stream
- case "$STABILITY" in
- development) SUFFIX="-dev"; TAG="development" ;;
- alpha) SUFFIX="-alpha"; TAG="alpha" ;;
- beta) SUFFIX="-beta"; TAG="beta" ;;
- rc) SUFFIX="-rc"; TAG="release-candidate" ;;
- *) SUFFIX=""; TAG="stable" ;;
- esac
-
- # Propagate version with stability suffix to all manifest files
- php ${MOKO_CLI}/version_set_platform.php \
- --path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
- php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
-
- # Re-read version (now includes suffix from version_set_platform)
- if [ -n "$SUFFIX" ]; then
- VERSION="${VERSION}${SUFFIX}"
- fi
-
- echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
- echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
- echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
- echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT"
-
- # Commit version bump if changed
- git add -A
- git diff --cached --quiet || {
- git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
- --author="gitea-actions[bot] "
- git push
- }
-
- - name: Create release and upload package
- id: package
- run: |
- VERSION="${{ steps.meta.outputs.version }}"
- TAG="${{ steps.meta.outputs.tag }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
-
- # Create or update Gitea release
- php ${MOKO_CLI}/release_create.php \
- --path . --version "$VERSION" --tag "$TAG" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
- --repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
-
- # Build package and upload
- php ${MOKO_CLI}/release_package.php \
- --path . --version "$VERSION" --tag "$TAG" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
- --repo "${GITEA_REPO}" --output /tmp || true
-
- - name: Update updates.xml
- if: steps.platform.outputs.platform == 'joomla'
- run: |
- VERSION="${{ steps.meta.outputs.version }}"
- STABILITY="${{ steps.meta.outputs.stability }}"
- SHA256="${{ steps.package.outputs.sha256_zip }}"
-
- if [ ! -f "updates.xml" ]; then
- echo "No updates.xml — skipping"
- exit 0
- fi
-
- SHA_FLAG=""
- [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
-
- php ${MOKO_CLI}/updates_xml_build.php \
- --path . --version "${VERSION}" --stability "${STABILITY}" \
- --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
- ${SHA_FLAG}
-
- # Commit and push updates.xml
- git add updates.xml
- git diff --cached --quiet || {
- git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
- git push
- }
-
- - name: Sync updates.xml to main
- if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
-
- FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
- "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
-
- if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
- python3 -c "
- import base64, json, urllib.request, sys
- with open('updates.xml', 'rb') as f:
- content = base64.b64encode(f.read()).decode()
- payload = json.dumps({
- 'content': content,
- 'sha': '${FILE_SHA}',
- 'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
- 'branch': 'main'
- }).encode()
- req = urllib.request.Request(
- '${API_BASE}/contents/updates.xml',
- data=payload, method='PUT',
- headers={
- 'Authorization': 'token ${GITEA_TOKEN}',
- 'Content-Type': 'application/json'
- })
- try:
- urllib.request.urlopen(req)
- print('updates.xml synced to main')
- except Exception as e:
- print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
- "
- fi
-
- - name: SFTP deploy to dev server
- if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
- env:
- DEV_HOST: ${{ vars.DEV_FTP_HOST }}
- DEV_PATH: ${{ vars.DEV_FTP_PATH }}
- DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
- DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
- DEV_PORT: ${{ vars.DEV_FTP_PORT }}
- DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
- DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
- run: |
- # Permission check: admin or maintain role required
- ACTOR="${{ github.actor }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
-
- PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
- "${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
- python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
- case "$PERMISSION" in
- admin|maintain|write) ;;
- *)
- echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
- exit 0
- ;;
- esac
-
- [ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
-
- SOURCE_DIR="src"
- [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
- [ ! -d "$SOURCE_DIR" ] && exit 0
-
- PORT="${DEV_PORT:-22}"
- REMOTE="${DEV_PATH%/}"
- [ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
-
- printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
- "$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
- if [ -n "$DEV_KEY" ]; then
- echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
- printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
- else
- printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
- fi
-
- PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
- if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
- php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
- elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
- php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
- fi
- rm -f /tmp/deploy_key /tmp/sftp-config.json
- echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
-
- - name: Summary
- if: always()
- run: |
- VERSION="${{ steps.meta.outputs.version }}"
- STABILITY="${{ steps.meta.outputs.stability }}"
- DISPLAY="${{ steps.meta.outputs.display_version }}"
- echo "## Update Server" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
- echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
- echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 325cfe5..112766b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,15 +14,21 @@
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
PATH: ./CHANGELOG.md
- VERSION: 02.26.18
+ VERSION: 02.28.00
BRIEF: Version history using `Keep a Changelog`
-->
# Changelog
-## [Unreleased]
+## [02.28.00] - 2026-05-31
### Added
+- `plg_task_mokowaassync` — Joomla Scheduled Task plugin for automatic content sync to remote sites
+- Community Builder tables added to demo reset safe table list
- API endpoint `POST /api/index.php/v1/mokowaas/install` — install extensions from a remote ZIP URL
+
- Demo Mode with configurable warning banner on frontend when enabled
+
+### Fixed
+- Demo banner countdown now shows weeks/days/months for longer intervals instead of raw hours
- `DemoResetService` — baseline snapshot and restore for DB tables + media files
- API endpoints `POST /?mokowaas=reset` and `POST /?mokowaas=snapshot` (query-string)
- REST endpoints `POST /api/v1/mokowaas/reset` and `GET/POST /api/v1/mokowaas/snapshot`
diff --git a/src/packages/com_mokowaas/mokowaas.xml b/src/packages/com_mokowaas/mokowaas.xml
index 4deeacf..9dee8a7 100644
--- a/src/packages/com_mokowaas/mokowaas.xml
+++ b/src/packages/com_mokowaas/mokowaas.xml
@@ -7,7 +7,7 @@
GPL-3.0-or-later
hello@mokoconsulting.tech
https://mokoconsulting.tech
- 02.26.18-dev
+ 02.28.00
Minimal API-only component for MokoWaaS. Provides REST endpoints for site health, cache, updates, and backups.
Moko\Component\MokoWaaS\Api
diff --git a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php
index 74bcc31..14848b8 100644
--- a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php
+++ b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php
@@ -1064,10 +1064,30 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
var now = Date.now();
var diff = Math.max(0, Math.floor((resetAt - now) / 1000));
if (diff <= 0) { cdSpan.textContent = ' — Reset imminent'; return; }
- var h = Math.floor(diff / 3600);
- var m = Math.floor((diff % 3600) / 60);
- var s = diff % 60;
- cdSpan.textContent = ' — Resets in ' + h + 'h ' + m + 'm ' + s + 's';
+ var parts = [];
+ var d = Math.floor(diff / 86400);
+ if (d >= 30) {
+ var mo = Math.floor(d / 30);
+ parts.push(mo + (mo === 1 ? ' month' : ' months'));
+ d = d % 30;
+ }
+ if (d >= 7) {
+ var w = Math.floor(d / 7);
+ parts.push(w + (w === 1 ? ' week' : ' weeks'));
+ d = d % 7;
+ }
+ if (d > 0) { parts.push(d + (d === 1 ? ' day' : ' days')); }
+ var rem = diff % 86400;
+ if (parts.length === 0) {
+ var h = Math.floor(rem / 3600);
+ var m = Math.floor((rem % 3600) / 60);
+ var s = rem % 60;
+ parts.push(h + 'h ' + m + 'm ' + s + 's');
+ } else if (parts.length <= 2) {
+ var h = Math.floor(rem / 3600);
+ if (h > 0) { parts.push(h + 'h'); }
+ }
+ cdSpan.textContent = ' — Resets in ' + parts.join(' ');
};
tick();
setInterval(tick, 1000);
diff --git a/src/packages/plg_system_mokowaas/Service/DemoResetService.php b/src/packages/plg_system_mokowaas/Service/DemoResetService.php
index 52d841e..fee3322 100644
--- a/src/packages/plg_system_mokowaas/Service/DemoResetService.php
+++ b/src/packages/plg_system_mokowaas/Service/DemoResetService.php
@@ -71,6 +71,16 @@ class DemoResetService
'#__banners',
'#__banner_clients',
'#__banner_tracks',
+
+ // Community Builder
+ '#__comprofiler',
+ '#__comprofiler_fields',
+ '#__comprofiler_field_values',
+ '#__comprofiler_tabs',
+ '#__comprofiler_members',
+ '#__comprofiler_lists',
+ '#__comprofiler_plugin',
+ '#__comprofiler_userreports',
];
/**
diff --git a/src/packages/plg_system_mokowaas/mokowaas.xml b/src/packages/plg_system_mokowaas/mokowaas.xml
index 310d3e4..033e686 100644
--- a/src/packages/plg_system_mokowaas/mokowaas.xml
+++ b/src/packages/plg_system_mokowaas/mokowaas.xml
@@ -30,7 +30,7 @@
GNU General Public License version 3 or later; see LICENSE.md
hello@mokoconsulting.tech
https://mokoconsulting.tech
- 02.26.18-dev
+ 02.28.00
This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform.
Moko\Plugin\System\MokoWaaS
script.php
diff --git a/src/packages/plg_task_mokowaasdemo/mokowaasdemo.xml b/src/packages/plg_task_mokowaasdemo/mokowaasdemo.xml
index 2792994..879bd8e 100644
--- a/src/packages/plg_task_mokowaasdemo/mokowaasdemo.xml
+++ b/src/packages/plg_task_mokowaasdemo/mokowaasdemo.xml
@@ -12,7 +12,7 @@
GNU General Public License version 3 or later; see LICENSE
hello@mokoconsulting.tech
https://mokoconsulting.tech
- 02.26.18-dev
+ 02.28.00
PLG_TASK_MOKOWAASDEMO_DESC
Moko\Plugin\Task\MokoWaaSDemo
diff --git a/src/packages/plg_task_mokowaassync/forms/sync_params.xml b/src/packages/plg_task_mokowaassync/forms/sync_params.xml
new file mode 100644
index 0000000..881e09d
--- /dev/null
+++ b/src/packages/plg_task_mokowaassync/forms/sync_params.xml
@@ -0,0 +1,8 @@
+
+
diff --git a/src/packages/plg_task_mokowaassync/language/en-GB/plg_task_mokowaassync.ini b/src/packages/plg_task_mokowaassync/language/en-GB/plg_task_mokowaassync.ini
new file mode 100644
index 0000000..049f286
--- /dev/null
+++ b/src/packages/plg_task_mokowaassync/language/en-GB/plg_task_mokowaassync.ini
@@ -0,0 +1,8 @@
+; MokoWaaS Content Sync Task Plugin
+; Copyright (C) 2026 Moko Consulting
+; SPDX-License-Identifier: GPL-3.0-or-later
+
+PLG_TASK_MOKOWAASSYNC="Task - MokoWaaS Content Sync"
+PLG_TASK_MOKOWAASSYNC_DESC="Scheduled task to push content (articles, categories, menus, modules) to remote MokoWaaS sites."
+PLG_TASK_MOKOWAASSYNC_SYNC_TITLE="MokoWaaS Content Sync"
+PLG_TASK_MOKOWAASSYNC_SYNC_DESC="Push site content to all configured sync targets. Targets are configured in the MokoWaaS system plugin settings."
diff --git a/src/packages/plg_task_mokowaassync/language/en-GB/plg_task_mokowaassync.sys.ini b/src/packages/plg_task_mokowaassync/language/en-GB/plg_task_mokowaassync.sys.ini
new file mode 100644
index 0000000..1676934
--- /dev/null
+++ b/src/packages/plg_task_mokowaassync/language/en-GB/plg_task_mokowaassync.sys.ini
@@ -0,0 +1,6 @@
+; MokoWaaS Content Sync Task Plugin (sys)
+; Copyright (C) 2026 Moko Consulting
+; SPDX-License-Identifier: GPL-3.0-or-later
+
+PLG_TASK_MOKOWAASSYNC="Task - MokoWaaS Content Sync"
+PLG_TASK_MOKOWAASSYNC_DESC="Scheduled task to push content (articles, categories, menus, modules) to remote MokoWaaS sites."
diff --git a/src/packages/plg_task_mokowaassync/mokowaassync.xml b/src/packages/plg_task_mokowaassync/mokowaassync.xml
new file mode 100644
index 0000000..a08b874
--- /dev/null
+++ b/src/packages/plg_task_mokowaassync/mokowaassync.xml
@@ -0,0 +1,31 @@
+
+
+
+ Task - MokoWaaS Content Sync
+ mokowaassync
+ Moko Consulting
+ 2026-05-30
+ Copyright (C) 2026 Moko Consulting. All rights reserved.
+ GNU General Public License version 3 or later; see LICENSE
+ hello@mokoconsulting.tech
+ https://mokoconsulting.tech
+ 02.28.00
+ PLG_TASK_MOKOWAASSYNC_DESC
+ Moko\Plugin\Task\MokoWaaSSync
+
+
+ mokowaassync.xml
+ src
+ services
+ forms
+ language
+
+
+
+ en-GB/plg_task_mokowaassync.ini
+ en-GB/plg_task_mokowaassync.sys.ini
+
+
diff --git a/src/packages/plg_task_mokowaassync/services/provider.php b/src/packages/plg_task_mokowaassync/services/provider.php
new file mode 100644
index 0000000..7940e6f
--- /dev/null
+++ b/src/packages/plg_task_mokowaassync/services/provider.php
@@ -0,0 +1,37 @@
+set(
+ PluginInterface::class,
+ function (Container $container) {
+ $dispatcher = $container->get(DispatcherInterface::class);
+ $plugin = new ContentSync(
+ $dispatcher,
+ (array) PluginHelper::getPlugin('task', 'mokowaassync')
+ );
+ $plugin->setApplication(Factory::getApplication());
+
+ return $plugin;
+ }
+ );
+ }
+};
diff --git a/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php
new file mode 100644
index 0000000..82015fe
--- /dev/null
+++ b/src/packages/plg_task_mokowaassync/src/Extension/ContentSync.php
@@ -0,0 +1,164 @@
+ [
+ 'langConstPrefix' => 'PLG_TASK_MOKOWAASSYNC_SYNC',
+ 'method' => 'syncContent',
+ 'form' => 'sync_params',
+ ],
+ ];
+
+ public static function getSubscribedEvents(): array
+ {
+ return [
+ 'onTaskOptionsList' => 'advertiseRoutines',
+ 'onExecuteTask' => 'standardRoutineHandler',
+ 'onContentPrepareForm' => 'enhanceTaskItemForm',
+ ];
+ }
+
+ /**
+ * Push content to all configured sync targets.
+ *
+ * Reads sync_targets from the MokoWaaS system plugin params, then
+ * delegates to ContentSyncService. Task-level overrides (if any)
+ * are merged on top.
+ *
+ * @param ExecuteTaskEvent $event The task event
+ *
+ * @return int Status::OK or Status::KNOCKOUT
+ *
+ * @since 02.27.00
+ */
+ private function syncContent(ExecuteTaskEvent $event): int
+ {
+ $serviceFile = JPATH_PLUGINS . '/system/mokowaas/Service/ContentSyncService.php';
+
+ if (!file_exists($serviceFile))
+ {
+ $this->logTask('ContentSyncService.php not found — is plg_system_mokowaas installed?');
+
+ return Status::KNOCKOUT;
+ }
+
+ require_once $serviceFile;
+
+ // Read sync targets from the system plugin params
+ $targets = $this->getSyncTargets();
+
+ if (empty($targets))
+ {
+ $this->logTask('No sync targets configured in MokoWaaS system plugin');
+
+ return Status::OK;
+ }
+
+ try
+ {
+ $service = new \Moko\Plugin\System\MokoWaaS\Service\ContentSyncService();
+ $result = $service->syncAllTargets($targets);
+
+ $targetResults = $result['targets'] ?? [];
+ $okCount = 0;
+ $errCount = 0;
+
+ foreach ($targetResults as $tr)
+ {
+ if (($tr['status'] ?? '') === 'ok')
+ {
+ $okCount++;
+ }
+ else
+ {
+ $errCount++;
+ $this->logTask('Sync failed for ' . ($tr['target'] ?? 'unknown') . ': ' . ($tr['message'] ?? ''));
+ }
+ }
+
+ $this->logTask(sprintf('Content sync completed — %d ok, %d failed of %d target(s)', $okCount, $errCount, count($targetResults)));
+
+ return $errCount > 0 && $okCount === 0 ? Status::KNOCKOUT : Status::OK;
+ }
+ catch (\Throwable $e)
+ {
+ $this->logTask('Content sync failed: ' . $e->getMessage());
+
+ return Status::KNOCKOUT;
+ }
+ }
+
+ /**
+ * Read sync targets from the MokoWaaS system plugin configuration.
+ *
+ * @return array Array of ['url' => ..., 'token' => ..., 'label' => ...]
+ *
+ * @since 02.27.00
+ */
+ private function getSyncTargets(): array
+ {
+ try
+ {
+ $db = Factory::getDbo();
+ $query = $db->getQuery(true)
+ ->select($db->quoteName('params'))
+ ->from($db->quoteName('#__extensions'))
+ ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
+ ->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
+ ->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas'));
+
+ $db->setQuery($query);
+ $raw = $db->loadResult();
+
+ if (empty($raw))
+ {
+ return [];
+ }
+
+ $params = json_decode($raw, true) ?: [];
+ $targets = $params['sync_targets'] ?? [];
+
+ if (is_string($targets))
+ {
+ $targets = json_decode($targets, true) ?: [];
+ }
+
+ return is_array($targets) ? $targets : [];
+ }
+ catch (\Throwable $e)
+ {
+ $this->logTask('Failed to read sync targets: ' . $e->getMessage());
+
+ return [];
+ }
+ }
+}
diff --git a/src/packages/plg_webservices_mokowaas/mokowaas.xml b/src/packages/plg_webservices_mokowaas/mokowaas.xml
index 4ce8d72..1e59ec6 100644
--- a/src/packages/plg_webservices_mokowaas/mokowaas.xml
+++ b/src/packages/plg_webservices_mokowaas/mokowaas.xml
@@ -7,7 +7,7 @@
GPL-3.0-or-later
hello@mokoconsulting.tech
https://mokoconsulting.tech
- 02.26.18-dev
+ 02.28.00
Joomla Web Services API routes for MokoWaaS site management — health checks, cache, updates, backups, and site info.
Moko\Plugin\WebServices\MokoWaaS
diff --git a/src/packages/plg_webservices_perfectpublisher/perfectpublisher.xml b/src/packages/plg_webservices_perfectpublisher/perfectpublisher.xml
index 25c472d..d37c9e4 100644
--- a/src/packages/plg_webservices_perfectpublisher/perfectpublisher.xml
+++ b/src/packages/plg_webservices_perfectpublisher/perfectpublisher.xml
@@ -7,7 +7,7 @@
GPL-3.0-or-later
hello@mokoconsulting.tech
https://mokoconsulting.tech
- 02.26.18-dev
+ 02.28.00
Joomla Web Services API routes for Perfect Publisher (com_autotweet) — channels, posts, requests, rules, and feeds.
Moko\Plugin\WebServices\PerfectPublisher
diff --git a/src/pkg_mokowaas.xml b/src/pkg_mokowaas.xml
index 1aa3acb..876e2ed 100644
--- a/src/pkg_mokowaas.xml
+++ b/src/pkg_mokowaas.xml
@@ -2,7 +2,7 @@
Package - MokoWaaS
mokowaas
- 02.26.18-dev
+ 02.28.00
2026-05-23
Moko Consulting
hello@mokoconsulting.tech
@@ -18,6 +18,7 @@
plg_webservices_mokowaas.zip
plg_webservices_perfectpublisher.zip
plg_task_mokowaasdemo.zip
+ plg_task_mokowaassync.zip
diff --git a/src/script.php b/src/script.php
index aab2566..7923b24 100644
--- a/src/script.php
+++ b/src/script.php
@@ -40,6 +40,7 @@ class Pkg_MokowaasInstallerScript
$this->enablePlugin('system', 'mokowaas');
$this->enablePlugin('webservices', 'mokowaas');
$this->enablePlugin('task', 'mokowaasdemo');
+ $this->enablePlugin('task', 'mokowaassync');
// Mark MokoWaaS extensions as protected (prevents disable/uninstall at framework level)
$this->protectExtensions();
@@ -196,6 +197,7 @@ class Pkg_MokowaasInstallerScript
$db->quote('mokowaas'),
$db->quote('com_mokowaas'),
$db->quote('mokowaasdemo'),
+ $db->quote('mokowaassync'),
$db->quote('perfectpublisher'),
];
diff --git a/updates.xml b/updates.xml
index 55fb93f..68c9b59 100644
--- a/updates.xml
+++ b/updates.xml
@@ -1,99 +1,22 @@
-
- Package - MokoWaaS
- Package - MokoWaaS development build.
- pkg_mokowaas
- package
- site
- 02.26.18-dev
- 2026-05-31
- https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/development
-
- https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/pkg_mokowaas-02.26.18-dev.zip
-
- d337997fedcb5b4b10286a41b1779869bd01dc5fe198389fedc27bc27d159489
- dev
- https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/CHANGELOG.md
- Moko Consulting
- https://mokoconsulting.tech
-
-
-
- Package - MokoWaaS
- Package - MokoWaaS alpha build.
- pkg_mokowaas
- package
- site
- 02.26.00-alpha
- 2026-05-30
- https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/alpha
-
- https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/alpha/pkg_mokowaas-02.26.00-alpha.zip
-
- e5b47e71c97e67cb70f14c9c6c559ce1fbc841a60c7d0c75ee00ec150f3d88b7
- alpha
- https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/CHANGELOG.md
- Moko Consulting
- https://mokoconsulting.tech
-
-
-
- Package - MokoWaaS
- Package - MokoWaaS beta build.
- pkg_mokowaas
- package
- site
- 02.26.00-beta
- 2026-05-30
- https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/beta
-
- https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/beta/pkg_mokowaas-02.26.00-beta.zip
-
- e5b47e71c97e67cb70f14c9c6c559ce1fbc841a60c7d0c75ee00ec150f3d88b7
- beta
- https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/CHANGELOG.md
- Moko Consulting
- https://mokoconsulting.tech
-
-
-
- Package - MokoWaaS
- Package - MokoWaaS rc build.
- pkg_mokowaas
- package
- site
- 02.27.00-rc
- 2026-05-31
- https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/release-candidate
-
- https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/release-candidate/pkg_mokowaas-02.27.00-rc.zip
-
- 03e2855af7795fe6b6525e50d739f11707b89b26a6cd58a9c7d039814664cc70
- rc
- https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/CHANGELOG.md
- Moko Consulting
- https://mokoconsulting.tech
-
-
Package - MokoWaaS
Package - MokoWaaS stable build.
pkg_mokowaas
package
site
- 02.27.00
+ 02.28.00
2026-05-31
https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/stable
- https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.27.00.zip
+ https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/pkg_mokowaas-02.28.00.zip
- 88bb39655e84f469c62c40cef5a429daee99ca1dc1cc4504c33f6cd227641477
stable
https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/CHANGELOG.md
Moko Consulting
--
2.52.0
From 27d4409213f544eeb0768f3ee3c8b4a710227dd0 Mon Sep 17 00:00:00 2001
From: "gitea-actions[bot]"
Date: Sun, 31 May 2026 03:49:04 +0000
Subject: [PATCH 04/10] chore(ci): remove auto-release.yml for update server
migration [skip ci]
---
.mokogitea/workflows/auto-release.yml | 270 --------------------------
1 file changed, 270 deletions(-)
delete mode 100644 .mokogitea/workflows/auto-release.yml
diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml
deleted file mode 100644
index 1227ff8..0000000
--- a/.mokogitea/workflows/auto-release.yml
+++ /dev/null
@@ -1,270 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Release
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
-# PATH: /templates/workflows/universal/auto-release.yml.template
-# VERSION: 05.00.00
-# BRIEF: Universal build & release � detects platform from manifest.xml
-#
-# +========================================================================+
-# | UNIVERSAL BUILD & RELEASE PIPELINE |
-# +========================================================================+
-# | |
-# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
-# | |
-# | Platform-specific: |
-# | joomla: XML manifest, updates.xml, type-prefixed packages |
-# | dolibarr: mod*.class.php, update.txt, dev version reset |
-# | generic: README-only, no update stream |
-# | |
-# +========================================================================+
-
-name: "Universal: Build & Release"
-
-on:
- pull_request:
- types: [opened, closed]
- branches:
- - main
- workflow_dispatch:
- inputs:
- action:
- description: 'Action to perform'
- required: false
- type: choice
- default: release
- options:
- - release
- - promote-rc
-
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
- GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
-
-permissions:
- contents: write
-
-jobs:
- # ── PR Opened → Rename branch to RC and build RC release ─────────────────────
- promote-rc:
- name: Promote to RC
- runs-on: release
- if: >-
- (github.event.action == 'opened' && github.event.pull_request.merged != true) ||
- (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- token: ${{ secrets.MOKOGITEA_TOKEN }}
- fetch-depth: 1
-
- - name: Setup moko-platform tools
- env:
- MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
- MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
- run: |
- if ! command -v composer &> /dev/null; then
- sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
- fi
- # Always fetch latest CLI tools — never use stale cache from previous runs
- rm -rf /tmp/moko-platform-api
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
- /tmp/moko-platform-api
- cd /tmp/moko-platform-api
- composer install --no-dev --no-interaction --quiet
-
- - name: Rename branch to rc
- run: |
- php /tmp/moko-platform-api/cli/branch_rename.php \
- --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" \
- --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
- --pr "${{ github.event.pull_request.number }}"
-
- - name: Checkout rc and configure git
- run: |
- git fetch origin rc
- git checkout rc
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
-
- - name: Publish RC release
- run: |
- php /tmp/moko-platform-api/cli/release_publish.php \
- --path . --stability rc --bump minor --branch rc \
- --token "${{ secrets.MOKOGITEA_TOKEN }}"
-
- - name: Summary
- if: always()
- run: |
- echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
- echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY
-
- # ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
- release:
- name: Build & Release Pipeline
- runs-on: release
- if: >-
- github.event.pull_request.merged == true ||
- (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- token: ${{ secrets.MOKOGITEA_TOKEN }}
- fetch-depth: 0
-
- - name: Configure git for bot pushes
- run: |
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
-
- - name: Setup moko-platform tools
- env:
- MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
- MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
- COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
- run: |
- # Ensure PHP + Composer are available
- if ! command -v composer &> /dev/null; then
- sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
- fi
- # Always fetch latest CLI tools — never use stale cache from previous runs
- rm -rf /tmp/moko-platform-api
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
- /tmp/moko-platform-api
- cd /tmp/moko-platform-api
- composer install --no-dev --no-interaction --quiet
-
-
- - name: "Publish stable release"
- run: |
- php /tmp/moko-platform-api/cli/release_publish.php \
- --path . --stability stable --bump minor --branch main \
- --token "${{ secrets.MOKOGITEA_TOKEN }}"
-
- # -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- - name: "Step 9: Mirror release to GitHub"
- if: >-
- steps.version.outputs.skip != 'true' &&
- secrets.GH_MIRROR_TOKEN != ''
- continue-on-error: true
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/release_mirror.php \
- --version "$VERSION" --tag "$RELEASE_TAG" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
- --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
- --branch main 2>&1 || true
- echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
-
- # -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- - name: "Step 10: Push main to GitHub mirror"
- if: >-
- steps.version.outputs.skip != 'true' &&
- secrets.GH_MIRROR_TOKEN != ''
- continue-on-error: true
- run: |
- GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
- GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
- GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
- git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
- git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
- git fetch origin main --depth=1
- git push github origin/main:refs/heads/main --force 2>/dev/null \
- && echo "main branch pushed to GitHub mirror" \
- || echo "WARNING: GitHub mirror push failed"
-
- - name: "Step 11: Delete rc branch and recreate dev from main"
- if: steps.version.outputs.skip != 'true'
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
-
- # Delete rc branch (ephemeral — created by promote-rc)
- curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
- "${API_BASE}/branches/rc" 2>/dev/null \
- && echo "Deleted rc branch" || echo "rc branch not found"
-
- # Delete dev branch
- curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
- "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
-
- # Recreate dev from main (now includes version bump + changelog promotion)
- curl -sf -X POST -H "Authorization: token ${TOKEN}" \
- -H "Content-Type: application/json" \
- "${API_BASE}/branches" \
- -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
-
- echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
-
- - name: "Step 12: Create version branch from main"
- if: steps.version.outputs.skip != 'true'
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- BRANCH_NAME="version/${VERSION}"
- MAIN_SHA=$(git rev-parse HEAD)
-
- # Delete old version branch if it exists (same version re-release)
- curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
-
- # Create version/XX.YY.ZZ from main
- curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
-
- echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
-
-
-
- # -- Dolibarr post-release: Reset dev version -----------------------------
- - name: "Post-release: Reset dev version"
- if: steps.version.outputs.skip != 'true'
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/version_reset_dev.php \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
- --branch dev --path . 2>&1 || true
-
- # -- Summary --------------------------------------------------------------
- - name: Pipeline Summary
- if: always()
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- PLATFORM="${{ steps.platform.outputs.platform }}"
- if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
- echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
- echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
- elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
- echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
- else
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
- echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
- echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
- fi
--
2.52.0
From 75799d8b2f62e507eb52b88f57823a0f34e8d19b Mon Sep 17 00:00:00 2001
From: "gitea-actions[bot]"
Date: Sun, 31 May 2026 03:49:07 +0000
Subject: [PATCH 05/10] chore(ci): remove pre-release.yml for update server
migration [skip ci]
---
.mokogitea/workflows/pre-release.yml | 233 ---------------------------
1 file changed, 233 deletions(-)
delete mode 100644 .mokogitea/workflows/pre-release.yml
diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml
deleted file mode 100644
index 162b08f..0000000
--- a/.mokogitea/workflows/pre-release.yml
+++ /dev/null
@@ -1,233 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Release
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
-# PATH: /templates/workflows/universal/pre-release.yml.template
-# VERSION: 05.01.00
-# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
-
-name: "Universal: Pre-Release"
-
-on:
- pull_request:
- types: [closed]
- branches:
- - dev
- workflow_dispatch:
- inputs:
- stability:
- description: 'Pre-release channel'
- required: true
- type: choice
- options:
- - development
- - alpha
- - beta
- - release-candidate
-
-permissions:
- contents: write
-
-env:
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
- GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
-
-jobs:
- build:
- name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
- runs-on: release
- if: >-
- github.event_name == 'workflow_dispatch' ||
- (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
- token: ${{ secrets.MOKOGITEA_TOKEN }}
-
- - name: Setup moko-platform tools
- env:
- MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
- MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
- run: |
- if ! command -v composer &> /dev/null; then
- sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
- fi
- # Always fetch latest CLI tools — never use stale cache from previous runs
- rm -rf /tmp/moko-platform-api
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
- /tmp/moko-platform-api
- cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
- echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
-
- - name: Detect platform
- id: platform
- run: |
- php ${MOKO_CLI}/manifest_read.php --path . --github-output
-
- - name: Resolve metadata and bump version
- id: meta
- run: |
- STABILITY="${{ inputs.stability || 'development' }}"
-
- case "$STABILITY" in
- development) SUFFIX="-dev"; TAG="development" ;;
- alpha) SUFFIX="-alpha"; TAG="alpha" ;;
- beta) SUFFIX="-beta"; TAG="beta" ;;
- release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
- esac
-
- # Read current version (bump already handled by push workflow)
- VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
- [ -z "$VERSION" ] && VERSION="00.00.01"
-
- # Strip any existing suffix from version before applying stability
- VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
-
- php ${MOKO_CLI}/version_set_platform.php \
- --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
-
- # Verify version consistency across all files
- php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
-
- # Update VERSION variable with suffix
- if [ -n "$SUFFIX" ]; then
- VERSION="${VERSION}${SUFFIX}"
- fi
-
- # Commit version bump
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- git add -A
- git diff --cached --quiet || {
- git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
- git push origin HEAD 2>&1
- }
-
- # Auto-detect element via manifest_element.php
- php ${MOKO_CLI}/manifest_element.php \
- --path . --version "$VERSION" --stability "$STABILITY" \
- --repo "${GITEA_REPO}" --github-output
-
- # Read back element outputs
- EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
- ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
- [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
- [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
-
- echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
- echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
- echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
- echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
- echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
-
- echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
-
- - name: Create release
- id: release
- run: |
- TAG="${{ steps.meta.outputs.tag }}"
- VERSION="${{ steps.meta.outputs.version }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php ${MOKO_CLI}/release_create.php \
- --path . --version "$VERSION" --tag "$TAG" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
- --repo "${GITEA_REPO}" --branch dev --prerelease
-
- - name: Build package and upload
- id: package
- run: |
- VERSION="${{ steps.meta.outputs.version }}"
- TAG="${{ steps.meta.outputs.tag }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php ${MOKO_CLI}/release_package.php \
- --path . --version "$VERSION" --tag "$TAG" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
- --repo "${GITEA_REPO}" --output /tmp || true
-
- - name: Update updates.xml
- if: steps.platform.outputs.platform == 'joomla'
- run: |
- VERSION="${{ steps.meta.outputs.version }}"
- STABILITY="${{ steps.meta.outputs.stability }}"
- SHA256="${{ steps.package.outputs.sha256_zip }}"
-
- if [ ! -f "updates.xml" ]; then
- echo "No updates.xml -- skipping"
- exit 0
- fi
-
- SHA_FLAG=""
- [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
-
- php ${MOKO_CLI}/updates_xml_build.php \
- --path . --version "${VERSION}" --stability "${STABILITY}" \
- --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
- ${SHA_FLAG}
-
- # Commit and push
- if ! git diff --quiet updates.xml 2>/dev/null; then
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git add updates.xml
- git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
- git push origin HEAD 2>&1 || echo "WARNING: push failed"
- fi
-
- - name: "Sync updates.xml to all branches"
- if: steps.platform.outputs.platform == 'joomla'
- run: |
- CURRENT_BRANCH="${{ github.ref_name }}"
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
-
- for BRANCH in main dev; do
- [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
- echo "Syncing updates.xml -> ${BRANCH}"
- git fetch origin "${BRANCH}" 2>/dev/null || continue
- git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue
- git checkout "${CURRENT_BRANCH}" -- updates.xml
- if ! git diff --quiet updates.xml 2>/dev/null; then
- git add updates.xml
- git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
- git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
- fi
- git checkout "${CURRENT_BRANCH}" 2>/dev/null
- done
-
- - name: "Delete lesser pre-release channels (cascade)"
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
-
- php ${MOKO_CLI}/release_cascade.php \
- --stability "${{ steps.meta.outputs.stability }}" \
- --token "${TOKEN}" \
- --api-base "${API_BASE}"
-
- - name: Summary
- if: always()
- run: |
- VERSION="${{ steps.meta.outputs.version }}"
- STABILITY="${{ steps.meta.outputs.stability }}"
- ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
- SHA256="${{ steps.package.outputs.sha256_zip }}"
- echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
- echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
- echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
- echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
--
2.52.0
From 4ec971ec9b627e9a7722ba8a1230355bf66d8b39 Mon Sep 17 00:00:00 2001
From: "gitea-actions[bot]"
Date: Sun, 31 May 2026 03:49:10 +0000
Subject: [PATCH 06/10] chore(ci): remove auto-bump.yml for update server
migration [skip ci]
---
.mokogitea/workflows/auto-bump.yml | 66 ------------------------------
1 file changed, 66 deletions(-)
delete mode 100644 .mokogitea/workflows/auto-bump.yml
diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml
deleted file mode 100644
index fb9dc82..0000000
--- a/.mokogitea/workflows/auto-bump.yml
+++ /dev/null
@@ -1,66 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Release
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/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
- - rc
- - 'feature/**'
- - 'patch/**'
-
-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: |
- php ${MOKO_CLI}/version_auto_bump.php \
- --path . --branch "${GITHUB_REF_NAME}" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" \
- --repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
--
2.52.0
From 53b5f3bc88c2f9f44d0034f5959b507d8a4cde9d Mon Sep 17 00:00:00 2001
From: "gitea-actions[bot]"
Date: Sun, 31 May 2026 03:49:13 +0000
Subject: [PATCH 07/10] chore(ci): remove cascade-dev.yml for update server
migration [skip ci]
---
.mokogitea/workflows/cascade-dev.yml | 10 ----------
1 file changed, 10 deletions(-)
delete mode 100644 .mokogitea/workflows/cascade-dev.yml
diff --git a/.mokogitea/workflows/cascade-dev.yml b/.mokogitea/workflows/cascade-dev.yml
deleted file mode 100644
index 5f7c1d7..0000000
--- a/.mokogitea/workflows/cascade-dev.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-# DISABLED — auto-release Step 11 recreates dev from main after every release.
-# Cascade-dev is redundant and causes version conflicts when both main and dev
-# have different version numbers in templateDetails.xml / manifest.xml.
-name: "Cascade Main → Dev (DISABLED)"
-on: workflow_dispatch
-jobs:
- noop:
- runs-on: ubuntu-latest
- steps:
- - run: echo "Cascade disabled — auto-release handles dev recreation"
--
2.52.0
From b2b0bc9f94fd52649f28984525d71b615823f0ab Mon Sep 17 00:00:00 2001
From: "gitea-actions[bot]"
Date: Sun, 31 May 2026 03:49:16 +0000
Subject: [PATCH 08/10] chore(ci): remove update-server.yml for update server
migration [skip ci]
---
.mokogitea/workflows/update-server.yml | 312 -------------------------
1 file changed, 312 deletions(-)
delete mode 100644 .mokogitea/workflows/update-server.yml
diff --git a/.mokogitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml
deleted file mode 100644
index 339d3f5..0000000
--- a/.mokogitea/workflows/update-server.yml
+++ /dev/null
@@ -1,312 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Universal
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
-# PATH: /templates/workflows/update-server.yml
-# VERSION: 05.00.00
-# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
-#
-# Thin wrapper around moko-platform CLI tools.
-# Builds packages, updates updates.xml, and optionally deploys via SFTP.
-#
-# Joomla filters update entries by the user's "Minimum Stability" setting.
-
-name: "Update Server"
-
-on:
- push:
- branches:
- - 'dev'
- - 'dev/**'
- - 'alpha/**'
- - 'beta/**'
- - 'rc/**'
- paths:
- - 'src/**'
- - 'htdocs/**'
- pull_request:
- types: [closed]
- branches:
- - 'dev'
- - 'dev/**'
- - 'alpha/**'
- - 'beta/**'
- - 'rc/**'
- paths:
- - 'src/**'
- - 'htdocs/**'
- workflow_dispatch:
- inputs:
- stability:
- description: 'Stability tag'
- required: true
- default: 'development'
- type: choice
- options:
- - development
- - alpha
- - beta
- - rc
- - stable
-
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
- GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
-
-permissions:
- contents: write
-
-jobs:
- update-xml:
- name: Update Server
- runs-on: release
- if: >-
- github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- token: ${{ secrets.MOKOGITEA_TOKEN }}
- fetch-depth: 0
-
- - name: Setup moko-platform tools
- env:
- MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
- MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
- COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
- run: |
- if ! command -v composer &> /dev/null; then
- sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
- fi
- # Always fetch latest CLI tools — never use stale cache from previous runs
- rm -rf /tmp/moko-platform
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
- /tmp/moko-platform 2>/dev/null || true
- if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
- cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
- fi
- echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
-
- - name: Detect platform
- id: platform
- run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
-
- - name: Resolve stability and bump version
- id: meta
- run: |
- BRANCH="${{ github.ref_name }}"
-
- # Configure git for bot pushes
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
-
- # Auto-bump patch version
- php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
-
- VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0")
-
- # Strip any existing suffix before applying stability
- VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
-
- # Determine stability from branch or manual input
- if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
- STABILITY="${{ inputs.stability }}"
- elif [[ "$BRANCH" == rc/* ]]; then
- STABILITY="rc"
- elif [[ "$BRANCH" == beta/* ]]; then
- STABILITY="beta"
- elif [[ "$BRANCH" == alpha/* ]]; then
- STABILITY="alpha"
- else
- STABILITY="development"
- fi
-
- # Version suffix per stability stream
- case "$STABILITY" in
- development) SUFFIX="-dev"; TAG="development" ;;
- alpha) SUFFIX="-alpha"; TAG="alpha" ;;
- beta) SUFFIX="-beta"; TAG="beta" ;;
- rc) SUFFIX="-rc"; TAG="release-candidate" ;;
- *) SUFFIX=""; TAG="stable" ;;
- esac
-
- # Propagate version with stability suffix to all manifest files
- php ${MOKO_CLI}/version_set_platform.php \
- --path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
- php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
-
- # Re-read version (now includes suffix from version_set_platform)
- if [ -n "$SUFFIX" ]; then
- VERSION="${VERSION}${SUFFIX}"
- fi
-
- echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
- echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
- echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
- echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT"
-
- # Commit version bump if changed
- git add -A
- git diff --cached --quiet || {
- git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
- --author="gitea-actions[bot] "
- git push
- }
-
- - name: Create release and upload package
- id: package
- run: |
- VERSION="${{ steps.meta.outputs.version }}"
- TAG="${{ steps.meta.outputs.tag }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
-
- # Create or update Gitea release
- php ${MOKO_CLI}/release_create.php \
- --path . --version "$VERSION" --tag "$TAG" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
- --repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
-
- # Build package and upload
- php ${MOKO_CLI}/release_package.php \
- --path . --version "$VERSION" --tag "$TAG" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
- --repo "${GITEA_REPO}" --output /tmp || true
-
- - name: Update updates.xml
- if: steps.platform.outputs.platform == 'joomla'
- run: |
- VERSION="${{ steps.meta.outputs.version }}"
- STABILITY="${{ steps.meta.outputs.stability }}"
- SHA256="${{ steps.package.outputs.sha256_zip }}"
-
- if [ ! -f "updates.xml" ]; then
- echo "No updates.xml — skipping"
- exit 0
- fi
-
- SHA_FLAG=""
- [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
-
- php ${MOKO_CLI}/updates_xml_build.php \
- --path . --version "${VERSION}" --stability "${STABILITY}" \
- --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
- ${SHA_FLAG}
-
- # Commit and push updates.xml
- git add updates.xml
- git diff --cached --quiet || {
- git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
- git push
- }
-
- - name: Sync updates.xml to main
- if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
-
- FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
- "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
-
- if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
- python3 -c "
- import base64, json, urllib.request, sys
- with open('updates.xml', 'rb') as f:
- content = base64.b64encode(f.read()).decode()
- payload = json.dumps({
- 'content': content,
- 'sha': '${FILE_SHA}',
- 'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
- 'branch': 'main'
- }).encode()
- req = urllib.request.Request(
- '${API_BASE}/contents/updates.xml',
- data=payload, method='PUT',
- headers={
- 'Authorization': 'token ${GITEA_TOKEN}',
- 'Content-Type': 'application/json'
- })
- try:
- urllib.request.urlopen(req)
- print('updates.xml synced to main')
- except Exception as e:
- print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
- "
- fi
-
- - name: SFTP deploy to dev server
- if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
- env:
- DEV_HOST: ${{ vars.DEV_FTP_HOST }}
- DEV_PATH: ${{ vars.DEV_FTP_PATH }}
- DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
- DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
- DEV_PORT: ${{ vars.DEV_FTP_PORT }}
- DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
- DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
- run: |
- # Permission check: admin or maintain role required
- ACTOR="${{ github.actor }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
-
- PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
- "${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
- python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
- case "$PERMISSION" in
- admin|maintain|write) ;;
- *)
- echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
- exit 0
- ;;
- esac
-
- [ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
-
- SOURCE_DIR="src"
- [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
- [ ! -d "$SOURCE_DIR" ] && exit 0
-
- PORT="${DEV_PORT:-22}"
- REMOTE="${DEV_PATH%/}"
- [ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
-
- printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
- "$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
- if [ -n "$DEV_KEY" ]; then
- echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
- printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
- else
- printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
- fi
-
- PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
- if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
- php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
- elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
- php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
- fi
- rm -f /tmp/deploy_key /tmp/sftp-config.json
- echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
-
- - name: Summary
- if: always()
- run: |
- VERSION="${{ steps.meta.outputs.version }}"
- STABILITY="${{ steps.meta.outputs.stability }}"
- DISPLAY="${{ steps.meta.outputs.display_version }}"
- echo "## Update Server" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
- echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
- echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
--
2.52.0
From 053f503af5c415cddd4a0b401fddfd68bb1e9975 Mon Sep 17 00:00:00 2001
From: Jonathan Miller
Date: Sun, 31 May 2026 07:28:41 -0500
Subject: [PATCH 09/10] feat: separate extension update rights from installer
restrictions
Add `allow_extension_updates` param (default: Yes) so tenants can
update extensions even when the installer is restricted. The update
and updatesites views are now permitted independently of the install
and manage views.
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context)
---
CHANGELOG.md | 1 +
.../Extension/MokoWaaS.php | 20 ++++++++++++++++---
.../language/en-GB/plg_system_mokowaas.ini | 2 ++
.../language/en-US/plg_system_mokowaas.ini | 2 ++
src/packages/plg_system_mokowaas/mokowaas.xml | 8 ++++++++
5 files changed, 30 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3486a27..67ea69d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@
# Changelog
## [02.28.00] - 2026-05-31
### Added
+- `allow_extension_updates` param — separate update rights from installer restrictions; tenants can update extensions by default even when the installer is restricted
- `plg_task_mokowaassync` — Joomla Scheduled Task plugin for automatic content sync to remote sites
- Community Builder tables added to demo reset safe table list
- API endpoint `POST /api/index.php/v1/mokowaas/install` — install extensions from a remote ZIP URL
diff --git a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php
index d146d19..427e0bd 100644
--- a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php
+++ b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php
@@ -4338,7 +4338,20 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
if ($this->params->get('restrict_installer', 1))
{
- $blocked[] = ['option' => 'com_installer'];
+ // Allow the update view by default so tenants can update extensions
+ $allowUpdates = (int) $this->params->get('allow_extension_updates', 1);
+
+ if ($allowUpdates && $option === 'com_installer'
+ && \in_array($view, ['update', 'updatesites'], true))
+ {
+ // Do not block — update views are permitted
+ }
+ elseif ($option === 'com_installer')
+ {
+ $this->blockAccess('Access restricted.');
+
+ return;
+ }
}
if ($this->params->get('hide_sysinfo', 1))
@@ -4445,8 +4458,9 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
explode("\n", $this->params->get('hidden_menu_items', ''))
));
- // Auto-hide components that are restricted
- if ($this->params->get('restrict_installer', 1))
+ // Auto-hide components that are restricted (keep visible when updates are allowed)
+ if ($this->params->get('restrict_installer', 1)
+ && !$this->params->get('allow_extension_updates', 1))
{
$hidden[] = 'com_installer';
}
diff --git a/src/packages/plg_system_mokowaas/language/en-GB/plg_system_mokowaas.ini b/src/packages/plg_system_mokowaas/language/en-GB/plg_system_mokowaas.ini
index e9fc2f3..5f0f536 100644
--- a/src/packages/plg_system_mokowaas/language/en-GB/plg_system_mokowaas.ini
+++ b/src/packages/plg_system_mokowaas/language/en-GB/plg_system_mokowaas.ini
@@ -88,6 +88,8 @@ PLG_SYSTEM_MOKOWAAS_FIELDSET_TENANT_DESC="Restrict admin features for non-master
PLG_SYSTEM_MOKOWAAS_RESTRICT_INSTALLER_LABEL="Restrict Extension Installer"
PLG_SYSTEM_MOKOWAAS_RESTRICT_INSTALLER_DESC="Block non-master users from installing or removing extensions."
+PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_LABEL="Allow Extension Updates"
+PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_DESC="When the installer is restricted, still allow non-master users to update extensions."
PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_LABEL="Hide System Information"
PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_DESC="Block non-master users from viewing PHP, database, and server information."
PLG_SYSTEM_MOKOWAAS_RESTRICT_CONFIG_LABEL="Restrict Global Configuration"
diff --git a/src/packages/plg_system_mokowaas/language/en-US/plg_system_mokowaas.ini b/src/packages/plg_system_mokowaas/language/en-US/plg_system_mokowaas.ini
index e9fc2f3..5f0f536 100644
--- a/src/packages/plg_system_mokowaas/language/en-US/plg_system_mokowaas.ini
+++ b/src/packages/plg_system_mokowaas/language/en-US/plg_system_mokowaas.ini
@@ -88,6 +88,8 @@ PLG_SYSTEM_MOKOWAAS_FIELDSET_TENANT_DESC="Restrict admin features for non-master
PLG_SYSTEM_MOKOWAAS_RESTRICT_INSTALLER_LABEL="Restrict Extension Installer"
PLG_SYSTEM_MOKOWAAS_RESTRICT_INSTALLER_DESC="Block non-master users from installing or removing extensions."
+PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_LABEL="Allow Extension Updates"
+PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_DESC="When the installer is restricted, still allow non-master users to update extensions."
PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_LABEL="Hide System Information"
PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_DESC="Block non-master users from viewing PHP, database, and server information."
PLG_SYSTEM_MOKOWAAS_RESTRICT_CONFIG_LABEL="Restrict Global Configuration"
diff --git a/src/packages/plg_system_mokowaas/mokowaas.xml b/src/packages/plg_system_mokowaas/mokowaas.xml
index b2fe9f0..4ffde72 100644
--- a/src/packages/plg_system_mokowaas/mokowaas.xml
+++ b/src/packages/plg_system_mokowaas/mokowaas.xml
@@ -233,6 +233,14 @@
+
+
+
+
Date: Sun, 31 May 2026 07:38:40 -0500
Subject: [PATCH 10/10] =?UTF-8?q?chore:=20bump=20version=2002.28.00=20?=
=?UTF-8?q?=E2=86=92=2002.29.00?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.mokogitea/manifest.xml | 2 +-
CHANGELOG.md | 4 ++--
CODE_OF_CONDUCT.md | 2 +-
GOVERNANCE.md | 2 +-
LICENSE.md | 2 +-
README.md | 2 +-
SECURITY.md | 2 +-
docs/guides/build-guide.md | 4 ++--
docs/guides/configuration-guide.md | 4 ++--
docs/guides/installation-guide.md | 4 ++--
docs/guides/operations-guide.md | 4 ++--
docs/guides/rollback-and-recovery-guide.md | 4 ++--
docs/guides/testing-guide.md | 4 ++--
docs/guides/troubleshooting-guide.md | 4 ++--
docs/guides/upgrade-and-versioning-guide.md | 4 ++--
docs/index.md | 4 ++--
docs/plugin-basic.md | 4 ++--
docs/update-server.md | 2 +-
src/packages/com_mokowaas/mokowaas.xml | 4 ++--
src/packages/plg_system_mokowaas/Extension/MokoWaaS.php | 2 +-
src/packages/plg_system_mokowaas/Field/AllowedIpsField.php | 2 +-
.../plg_system_mokowaas/Field/CopyableTokenField.php | 2 +-
src/packages/plg_system_mokowaas/Field/CurrentIpField.php | 2 +-
.../plg_system_mokowaas/Field/DemoTaskInfoField.php | 2 +-
src/packages/plg_system_mokowaas/Field/NextResetField.php | 2 +-
.../plg_system_mokowaas/Field/SnapshotTablesField.php | 2 +-
.../plg_system_mokowaas/Service/ContentSyncReceiver.php | 2 +-
.../plg_system_mokowaas/Service/ContentSyncService.php | 2 +-
.../plg_system_mokowaas/Service/DemoResetService.php | 2 +-
src/packages/plg_system_mokowaas/mokowaas.xml | 6 +++---
src/packages/plg_system_mokowaas/script.php | 2 +-
src/packages/plg_system_mokowaas/services/provider.php | 2 +-
src/packages/plg_task_mokowaasdemo/mokowaasdemo.xml | 4 ++--
src/packages/plg_task_mokowaassync/mokowaassync.xml | 2 +-
src/packages/plg_webservices_mokowaas/mokowaas.xml | 4 ++--
.../plg_webservices_perfectpublisher/perfectpublisher.xml | 4 ++--
.../plg_webservices_perfectpublisher/services/provider.php | 2 +-
.../src/Extension/PerfectPublisherApi.php | 2 +-
src/pkg_mokowaas.xml | 4 ++--
updates.xml | 2 +-
40 files changed, 58 insertions(+), 58 deletions(-)
diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml
index 6ffb4cd..491d4b2 100644
--- a/.mokogitea/manifest.xml
+++ b/.mokogitea/manifest.xml
@@ -9,7 +9,7 @@
Package - MokoWaaS
MokoConsulting
White-label identity, security hardening, and tenant restriction layer for WaaS-managed Joomla environments
- 02.27.00
+ 02.29.00
GNU General Public License v3
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 67ea69d..65ff94e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,12 +14,12 @@
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
PATH: ./CHANGELOG.md
- VERSION: 02.28.00
+ VERSION: 02.29.00
BRIEF: Version history using `Keep a Changelog`
-->
# Changelog
-## [02.28.00] - 2026-05-31
+## [02.29.00] - 2026-05-31
### Added
- `allow_extension_updates` param — separate update rights from installer restrictions; tenants can update extensions by default even when the installer is restricted
- `plg_task_mokowaassync` — Joomla Scheduled Task plugin for automatic content sync to remote sites
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 57cd744..117d1fa 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -14,7 +14,7 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
- VERSION: 02.27.00
+ VERSION: 02.29.00
PATH: ./CODE_OF_CONDUCT.md
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
-->
diff --git a/GOVERNANCE.md b/GOVERNANCE.md
index e878bc8..d56d018 100644
--- a/GOVERNANCE.md
+++ b/GOVERNANCE.md
@@ -19,7 +19,7 @@
DEFGROUP: mokoconsulting-tech.MokoWaaSBrand
INGROUP: MokoStandards.Governance
REPO: https://github.com/mokoconsulting-tech/MokoWaaSBrand
- VERSION: 02.27.00
+ VERSION: 02.29.00
PATH: /GOVERNANCE.md
BRIEF: Project governance rules, roles, and decision process for MokoWaaSBrand
-->
diff --git a/LICENSE.md b/LICENSE.md
index bbefeb1..2e30030 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -15,7 +15,7 @@
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
PATH: ./LICENSE.md
- VERSION: 02.27.00
+ VERSION: 02.29.00
BRIEF: Project license (GPL-3.0-or-later)
-->
GNU GENERAL PUBLIC LICENSE
diff --git a/README.md b/README.md
index cc2330b..1b81d93 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
- VERSION: 02.27.00
+ VERSION: 02.29.00
PATH: /README.md
BRIEF: MokoWaaS platform plugin for Joomla
-->
diff --git a/SECURITY.md b/SECURITY.md
index e1f2438..ea2af4b 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME]
INGROUP: [PROJECT_NAME].Documentation
REPO: [REPOSITORY_URL]
PATH: /SECURITY.md
-VERSION: 02.27.00
+VERSION: 02.29.00
BRIEF: Security vulnerability reporting and handling policy
-->
diff --git a/docs/guides/build-guide.md b/docs/guides/build-guide.md
index 248b64d..c90190a 100644
--- a/docs/guides/build-guide.md
+++ b/docs/guides/build-guide.md
@@ -11,13 +11,13 @@
INGROUP: MokoWaaS.Build
REPO: https://github.com/mokoconsulting-tech/mokowaas
FILE: build-guide.md
- VERSION: 02.27.00
+ VERSION: 02.29.00
PATH: /docs/guides/
BRIEF: Build and packaging guide for the MokoWaaS system plugin
NOTE: Defines environment setup, repository layout, packaging rules, and release preparation
-->
-# MokoWaaS Build Guide (VERSION: 02.27.00)
+# MokoWaaS Build Guide (VERSION: 02.29.00)
## 1. Purpose
diff --git a/docs/guides/configuration-guide.md b/docs/guides/configuration-guide.md
index 503bcae..e7eb735 100644
--- a/docs/guides/configuration-guide.md
+++ b/docs/guides/configuration-guide.md
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
- VERSION: 02.27.00
+ VERSION: 02.29.00
PATH: /docs/guides/configuration-guide.md
BRIEF: Configuration guide for the MokoWaaS system plugin
NOTE: Defines plugin parameters, expected behaviors, and recommended defaults
-->
-# MokoWaaS Configuration Guide (VERSION: 02.27.00)
+# MokoWaaS Configuration Guide (VERSION: 02.29.00)
## 1. Objective
diff --git a/docs/guides/installation-guide.md b/docs/guides/installation-guide.md
index bc5687b..94bced2 100644
--- a/docs/guides/installation-guide.md
+++ b/docs/guides/installation-guide.md
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
- VERSION: 02.27.00
+ VERSION: 02.29.00
PATH: /docs/guides/installation-guide.md
BRIEF: Installation guide for the MokoWaaS system plugin
NOTE: First document in the guide set
-->
-# MokoWaaS Installation Guide (VERSION: 02.27.00)
+# MokoWaaS Installation Guide (VERSION: 02.29.00)
## Introduction
diff --git a/docs/guides/operations-guide.md b/docs/guides/operations-guide.md
index 97b8413..d340432 100644
--- a/docs/guides/operations-guide.md
+++ b/docs/guides/operations-guide.md
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
- VERSION: 02.27.00
+ VERSION: 02.29.00
PATH: /docs/guides/operations-guide.md
BRIEF: Operational guide for administering and managing the MokoWaaS system plugin
NOTE: Defines lifecycle, responsibilities, and operational behaviors
-->
-# MokoWaaS Operations Guide (VERSION: 02.27.00)
+# MokoWaaS Operations Guide (VERSION: 02.29.00)
## Introduction
diff --git a/docs/guides/rollback-and-recovery-guide.md b/docs/guides/rollback-and-recovery-guide.md
index deaebae..199e6d3 100644
--- a/docs/guides/rollback-and-recovery-guide.md
+++ b/docs/guides/rollback-and-recovery-guide.md
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
- VERSION: 02.27.00
+ VERSION: 02.29.00
PATH: /docs/guides/rollback-and-recovery-guide.md
BRIEF: Rollback and recovery guide for restoring stable operation after plugin related incidents
NOTE: Completes the core guide set for WaaS plugin governance
-->
-# MokoWaaS Rollback and Recovery Guide (VERSION: 02.27.00)
+# MokoWaaS Rollback and Recovery Guide (VERSION: 02.29.00)
## Introduction
diff --git a/docs/guides/testing-guide.md b/docs/guides/testing-guide.md
index 55f3eeb..b8cae31 100644
--- a/docs/guides/testing-guide.md
+++ b/docs/guides/testing-guide.md
@@ -7,13 +7,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
- VERSION: 02.27.00
+ VERSION: 02.29.00
PATH: /docs/guides/testing-guide.md
BRIEF: Testing guide for MokoWaaS v02.01.08
NOTE: Covers manual test procedures for language overrides, install/uninstall, and configuration
-->
-# MokoWaaS Testing Guide (VERSION: 02.27.00)
+# MokoWaaS Testing Guide (VERSION: 02.29.00)
## 1. Prerequisites
diff --git a/docs/guides/troubleshooting-guide.md b/docs/guides/troubleshooting-guide.md
index d377cde..192b2b0 100644
--- a/docs/guides/troubleshooting-guide.md
+++ b/docs/guides/troubleshooting-guide.md
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
- VERSION: 02.27.00
+ VERSION: 02.29.00
PATH: /docs/guides/troubleshooting-guide.md
BRIEF: Troubleshooting guide for diagnosing and resolving issues related to the MokoWaaS plugin
NOTE: Designed for administrators and WaaS operations teams
-->
-# MokoWaaS Troubleshooting Guide (VERSION: 02.27.00)
+# MokoWaaS Troubleshooting Guide (VERSION: 02.29.00)
## Introduction
diff --git a/docs/guides/upgrade-and-versioning-guide.md b/docs/guides/upgrade-and-versioning-guide.md
index f39d786..8cbc2de 100644
--- a/docs/guides/upgrade-and-versioning-guide.md
+++ b/docs/guides/upgrade-and-versioning-guide.md
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
- VERSION: 02.27.00
+ VERSION: 02.29.00
PATH: /docs/guides/upgrade-and-versioning-guide.md
BRIEF: Guide for updating, versioning, and maintaining the MokoWaaS plugin
NOTE: Defines release flow, version rules, and upgrade validation
-->
-# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.27.00)
+# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.29.00)
## Introduction
diff --git a/docs/index.md b/docs/index.md
index f5480db..c3f0c0c 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
- VERSION: 02.27.00
+ VERSION: 02.29.00
PATH: /docs/index.md
BRIEF: Master index of all documentation for the MokoWaaS plugin
NOTE: Automatically maintained index for all guide canvases
-->
-# MokoWaaS Documentation Index (VERSION: 02.27.00)
+# MokoWaaS Documentation Index (VERSION: 02.29.00)
## Introduction
diff --git a/docs/plugin-basic.md b/docs/plugin-basic.md
index 9365850..eb5bae9 100644
--- a/docs/plugin-basic.md
+++ b/docs/plugin-basic.md
@@ -11,12 +11,12 @@
INGROUP: MokoWaaS
REPO: https://github.com/mokoconsulting-tech/mokowaas
PATH: /docs/plugin-basic.md
- VERSION: 02.27.00
+ VERSION: 02.29.00
BRIEF: Baseline documentation for the MokoWaaS system plugin
NOTE: Foundational reference for internal and external stakeholders
-->
-# MokoWaaS Plugin Overview (VERSION: 02.27.00)
+# MokoWaaS Plugin Overview (VERSION: 02.29.00)
## Introduction
diff --git a/docs/update-server.md b/docs/update-server.md
index a4c6be4..dffefe7 100644
--- a/docs/update-server.md
+++ b/docs/update-server.md
@@ -10,7 +10,7 @@ DEFGROUP: MokoWaaS.Documentation
INGROUP: MokoStandards.Templates
REPO: https://github.com/mokoconsulting-tech/MokoWaaS
PATH: /docs/update-server.md
-VERSION: 02.27.00
+VERSION: 02.29.00
BRIEF: How this extension's Joomla update server file (update.xml) is managed
-->
diff --git a/src/packages/com_mokowaas/mokowaas.xml b/src/packages/com_mokowaas/mokowaas.xml
index d57d401..b3fb239 100644
--- a/src/packages/com_mokowaas/mokowaas.xml
+++ b/src/packages/com_mokowaas/mokowaas.xml
@@ -7,8 +7,8 @@
GPL-3.0-or-later
hello@mokoconsulting.tech
https://mokoconsulting.tech
- 02.28.00
- 02.28.00
+ 02.29.00
+ 02.29.00
Minimal API-only component for MokoWaaS. Provides REST endpoints for site health, cache, updates, and backups.
Moko\Component\MokoWaaS\Api
diff --git a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php
index 427e0bd..e38bf75 100644
--- a/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php
+++ b/src/packages/plg_system_mokowaas/Extension/MokoWaaS.php
@@ -22,7 +22,7 @@
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://github.com/mokoconsulting-tech/mokowaas
- * VERSION: 02.27.00
+ * VERSION: 02.29.00
* PATH: /src/Extension/MokoWaaS.php
* NOTE: Handles Joomla system events for rebranding functionality
*/
diff --git a/src/packages/plg_system_mokowaas/Field/AllowedIpsField.php b/src/packages/plg_system_mokowaas/Field/AllowedIpsField.php
index 9cdeb0d..650ab8a 100644
--- a/src/packages/plg_system_mokowaas/Field/AllowedIpsField.php
+++ b/src/packages/plg_system_mokowaas/Field/AllowedIpsField.php
@@ -7,7 +7,7 @@
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
- * VERSION: 02.27.00
+ * VERSION: 02.29.00
* PATH: /src/Field/AllowedIpsField.php
* BRIEF: Custom form field that displays the current IP whitelist
*/
diff --git a/src/packages/plg_system_mokowaas/Field/CopyableTokenField.php b/src/packages/plg_system_mokowaas/Field/CopyableTokenField.php
index 6bd9bdb..1b3ee1d 100644
--- a/src/packages/plg_system_mokowaas/Field/CopyableTokenField.php
+++ b/src/packages/plg_system_mokowaas/Field/CopyableTokenField.php
@@ -8,7 +8,7 @@
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
- * VERSION: 02.27.00
+ * VERSION: 02.29.00
* PATH: /src/Field/CopyableTokenField.php
* BRIEF: Read-only token field with a copy-to-clipboard button
*/
diff --git a/src/packages/plg_system_mokowaas/Field/CurrentIpField.php b/src/packages/plg_system_mokowaas/Field/CurrentIpField.php
index f8ab64d..52af6db 100644
--- a/src/packages/plg_system_mokowaas/Field/CurrentIpField.php
+++ b/src/packages/plg_system_mokowaas/Field/CurrentIpField.php
@@ -7,7 +7,7 @@
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
- * VERSION: 02.27.00
+ * VERSION: 02.29.00
* PATH: /src/Field/CurrentIpField.php
* BRIEF: Read-only field that displays the current user's IP address
*/
diff --git a/src/packages/plg_system_mokowaas/Field/DemoTaskInfoField.php b/src/packages/plg_system_mokowaas/Field/DemoTaskInfoField.php
index 7898a3a..7162dc2 100644
--- a/src/packages/plg_system_mokowaas/Field/DemoTaskInfoField.php
+++ b/src/packages/plg_system_mokowaas/Field/DemoTaskInfoField.php
@@ -8,7 +8,7 @@
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
- * VERSION: 02.27.00
+ * VERSION: 02.29.00
* PATH: /src/Field/DemoTaskInfoField.php
* BRIEF: Read-only field showing scheduled task info with link to manage it
*/
diff --git a/src/packages/plg_system_mokowaas/Field/NextResetField.php b/src/packages/plg_system_mokowaas/Field/NextResetField.php
index 2eef389..5e82d9a 100644
--- a/src/packages/plg_system_mokowaas/Field/NextResetField.php
+++ b/src/packages/plg_system_mokowaas/Field/NextResetField.php
@@ -8,7 +8,7 @@
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
- * VERSION: 02.27.00
+ * VERSION: 02.29.00
* PATH: /src/Field/NextResetField.php
* BRIEF: Read-only field showing next reset time from Joomla scheduled task
*/
diff --git a/src/packages/plg_system_mokowaas/Field/SnapshotTablesField.php b/src/packages/plg_system_mokowaas/Field/SnapshotTablesField.php
index 29b1a59..cf003e5 100644
--- a/src/packages/plg_system_mokowaas/Field/SnapshotTablesField.php
+++ b/src/packages/plg_system_mokowaas/Field/SnapshotTablesField.php
@@ -8,7 +8,7 @@
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
- * VERSION: 02.27.00
+ * VERSION: 02.29.00
* PATH: /src/Field/SnapshotTablesField.php
* BRIEF: Multi-select list field that loads DB tables with sensible defaults
*/
diff --git a/src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php b/src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php
index 45f6547..e12f332 100644
--- a/src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php
+++ b/src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php
@@ -10,7 +10,7 @@
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php
- * VERSION: 02.27.00
+ * VERSION: 02.29.00
* BRIEF: Receiver-side content sync — applies incoming payload to local DB
*/
diff --git a/src/packages/plg_system_mokowaas/Service/ContentSyncService.php b/src/packages/plg_system_mokowaas/Service/ContentSyncService.php
index 6e7e93c..88abad8 100644
--- a/src/packages/plg_system_mokowaas/Service/ContentSyncService.php
+++ b/src/packages/plg_system_mokowaas/Service/ContentSyncService.php
@@ -10,7 +10,7 @@
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_system_mokowaas/Service/ContentSyncService.php
- * VERSION: 02.27.00
+ * VERSION: 02.29.00
* BRIEF: Sender-side content sync — builds payload and pushes to remote sites
*/
diff --git a/src/packages/plg_system_mokowaas/Service/DemoResetService.php b/src/packages/plg_system_mokowaas/Service/DemoResetService.php
index c146d16..0fe5ddd 100644
--- a/src/packages/plg_system_mokowaas/Service/DemoResetService.php
+++ b/src/packages/plg_system_mokowaas/Service/DemoResetService.php
@@ -10,7 +10,7 @@
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_system_mokowaas/Service/DemoResetService.php
- * VERSION: 02.27.00
+ * VERSION: 02.29.00
* BRIEF: Content-only snapshot/restore for demo site reset
*/
diff --git a/src/packages/plg_system_mokowaas/mokowaas.xml b/src/packages/plg_system_mokowaas/mokowaas.xml
index 4ffde72..2126e50 100644
--- a/src/packages/plg_system_mokowaas/mokowaas.xml
+++ b/src/packages/plg_system_mokowaas/mokowaas.xml
@@ -16,7 +16,7 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS
REPO: https://github.com/mokoconsulting-tech/mokowaas
- VERSION: 02.01.11
+ VERSION: 02.29.00
PATH: /src/mokowaas.xml
BRIEF: Plugin manifest for MokoWaaS system plugin
NOTE: Defines installation metadata, files, and configuration for Joomla
@@ -30,8 +30,8 @@
GNU General Public License version 3 or later; see LICENSE.md
hello@mokoconsulting.tech
https://mokoconsulting.tech
- 02.28.00
- 02.28.00
+ 02.29.00
+ 02.29.00
This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform.
Moko\Plugin\System\MokoWaaS
script.php
diff --git a/src/packages/plg_system_mokowaas/script.php b/src/packages/plg_system_mokowaas/script.php
index 484d734..21fd61b 100644
--- a/src/packages/plg_system_mokowaas/script.php
+++ b/src/packages/plg_system_mokowaas/script.php
@@ -22,7 +22,7 @@
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://github.com/mokoconsulting-tech/mokowaas
- * VERSION: 02.27.00
+ * VERSION: 02.29.00
* PATH: /src/script.php
* BRIEF: Installation script for MokoWaaS plugin
* NOTE: Handles installation, update, and uninstallation tasks including language override deployment
diff --git a/src/packages/plg_system_mokowaas/services/provider.php b/src/packages/plg_system_mokowaas/services/provider.php
index fc4e9fc..8deb617 100644
--- a/src/packages/plg_system_mokowaas/services/provider.php
+++ b/src/packages/plg_system_mokowaas/services/provider.php
@@ -22,7 +22,7 @@
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://github.com/mokoconsulting-tech/mokowaas
- * VERSION: 02.27.00
+ * VERSION: 02.29.00
* PATH: /src/services/provider.php
* BRIEF: Service provider for dependency injection in Joomla 5.x
* NOTE: Registers the plugin with Joomla's DI container
diff --git a/src/packages/plg_task_mokowaasdemo/mokowaasdemo.xml b/src/packages/plg_task_mokowaasdemo/mokowaasdemo.xml
index f3bf44e..19c3b44 100644
--- a/src/packages/plg_task_mokowaasdemo/mokowaasdemo.xml
+++ b/src/packages/plg_task_mokowaasdemo/mokowaasdemo.xml
@@ -12,8 +12,8 @@
GNU General Public License version 3 or later; see LICENSE
hello@mokoconsulting.tech
https://mokoconsulting.tech
- 02.28.00
- 02.28.00
+ 02.29.00
+ 02.29.00
PLG_TASK_MOKOWAASDEMO_DESC
Moko\Plugin\Task\MokoWaaSDemo
diff --git a/src/packages/plg_task_mokowaassync/mokowaassync.xml b/src/packages/plg_task_mokowaassync/mokowaassync.xml
index a08b874..84590ca 100644
--- a/src/packages/plg_task_mokowaassync/mokowaassync.xml
+++ b/src/packages/plg_task_mokowaassync/mokowaassync.xml
@@ -12,7 +12,7 @@
GNU General Public License version 3 or later; see LICENSE
hello@mokoconsulting.tech
https://mokoconsulting.tech
- 02.28.00
+ 02.29.00
PLG_TASK_MOKOWAASSYNC_DESC
Moko\Plugin\Task\MokoWaaSSync
diff --git a/src/packages/plg_webservices_mokowaas/mokowaas.xml b/src/packages/plg_webservices_mokowaas/mokowaas.xml
index 04e024b..46fb967 100644
--- a/src/packages/plg_webservices_mokowaas/mokowaas.xml
+++ b/src/packages/plg_webservices_mokowaas/mokowaas.xml
@@ -7,8 +7,8 @@
GPL-3.0-or-later
hello@mokoconsulting.tech
https://mokoconsulting.tech
- 02.28.00
- 02.28.00
+ 02.29.00
+ 02.29.00
Joomla Web Services API routes for MokoWaaS site management — health checks, cache, updates, backups, and site info.
Moko\Plugin\WebServices\MokoWaaS
diff --git a/src/packages/plg_webservices_perfectpublisher/perfectpublisher.xml b/src/packages/plg_webservices_perfectpublisher/perfectpublisher.xml
index 10cc67c..00dd52d 100644
--- a/src/packages/plg_webservices_perfectpublisher/perfectpublisher.xml
+++ b/src/packages/plg_webservices_perfectpublisher/perfectpublisher.xml
@@ -7,8 +7,8 @@
GPL-3.0-or-later
hello@mokoconsulting.tech
https://mokoconsulting.tech
- 02.28.00
- 02.28.00
+ 02.29.00
+ 02.29.00
Joomla Web Services API routes for Perfect Publisher (com_autotweet) — channels, posts, requests, rules, and feeds.
Moko\Plugin\WebServices\PerfectPublisher
diff --git a/src/packages/plg_webservices_perfectpublisher/services/provider.php b/src/packages/plg_webservices_perfectpublisher/services/provider.php
index 16d5291..6232612 100644
--- a/src/packages/plg_webservices_perfectpublisher/services/provider.php
+++ b/src/packages/plg_webservices_perfectpublisher/services/provider.php
@@ -8,7 +8,7 @@
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_webservices_perfectpublisher/services/provider.php
- * VERSION: 02.27.00
+ * VERSION: 02.29.00
* BRIEF: DI service provider for Perfect Publisher Web Services plugin
*/
diff --git a/src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php b/src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php
index 8b8e2d1..da97dc0 100644
--- a/src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php
+++ b/src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php
@@ -8,7 +8,7 @@
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php
- * VERSION: 02.27.00
+ * VERSION: 02.29.00
* BRIEF: Web Services API plugin for Perfect Publisher (com_autotweet)
*/
diff --git a/src/pkg_mokowaas.xml b/src/pkg_mokowaas.xml
index 1df236f..c7bcf1d 100644
--- a/src/pkg_mokowaas.xml
+++ b/src/pkg_mokowaas.xml
@@ -2,8 +2,8 @@
Package - MokoWaaS
mokowaas
- 02.28.00
- 02.28.00
+ 02.29.00
+ 02.29.00
2026-05-23
Moko Consulting
hello@mokoconsulting.tech
diff --git a/updates.xml b/updates.xml
index 68c9b59..66a19ca 100644
--- a/updates.xml
+++ b/updates.xml
@@ -1,7 +1,7 @@
--
2.52.0