diff --git a/.mokogitea/manifest.xml b/.mokogitea/manifest.xml
index 6305d8e..016f7ff 100644
--- a/.mokogitea/manifest.xml
+++ b/.mokogitea/manifest.xml
@@ -10,7 +10,7 @@
Package - MokoJoomHero
MokoConsulting
A Joomla Module designed to provide a random image from a folder with content on top as a Hero.
- 01.19.00
+ 01.20.00
GNU General Public License v3
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
new file mode 100644
index 0000000..fb9dc82
--- /dev/null
+++ b/.mokogitea/workflows/auto-bump.yml
@@ -0,0 +1,66 @@
+# 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
index 44a2d64..bec445b 100644
--- a/.mokogitea/workflows/auto-release.yml
+++ b/.mokogitea/workflows/auto-release.yml
@@ -1,285 +1,341 @@
-# 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 }}" \
- --skip-update-stream
-
- - name: Summary
- if: always()
- run: |
- echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
- echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY
-
- # ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
- release:
- 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: Check for merge conflict markers
- run: |
- CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
- if [ -n "$CONFLICTS" ]; then
- echo "::error::Merge conflict markers found — aborting release"
- echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
- echo '```' >> $GITHUB_STEP_SUMMARY
- echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
- echo '```' >> $GITHUB_STEP_SUMMARY
- exit 1
- fi
- echo "No conflict markers found"
-
- - name: 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 }}" \
- --skip-update-stream
-
- # -- 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
+# 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, 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 [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
+ echo Using pre-installed /opt/moko-platform
+ echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
+ else
+ echo Falling back to fresh clone
+ if ! command -v composer > /dev/null 2>&1; 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
+ rm -rf /tmp/moko-platform-api
+ CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
+ git clone --depth 1 --branch main --quiet $CLONE_URL /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: Rename branch to rc
+ run: |
+ php ${MOKO_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 ${MOKO_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 release built" >> $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: Check for merge conflict markers
+ run: |
+ CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
+ if [ -n "$CONFLICTS" ]; then
+ echo "::error::Merge conflict markers found — aborting release"
+ echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
+ echo '```' >> $GITHUB_STEP_SUMMARY
+ echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
+ echo '```' >> $GITHUB_STEP_SUMMARY
+ exit 1
+ fi
+ echo "No conflict markers found"
+
+ - name: 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: |
+ if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
+ echo Using pre-installed /opt/moko-platform
+ echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
+ else
+ echo Falling back to fresh clone
+ if ! command -v composer > /dev/null 2>&1; 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
+ rm -rf /tmp/moko-platform-api
+ CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
+ git clone --depth 1 --branch main --quiet $CLONE_URL /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: "Determine version bump level"
+ id: bump
+ run: |
+ # Fix/patch branches: version was already bumped by pre-release, just strip suffix
+ # Feature/dev branches: bump minor for the new stable release
+ HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}"
+ case "$HEAD_REF" in
+ fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;;
+ *) BUMP="minor" ;;
+ esac
+ echo "level=${BUMP}" >> "$GITHUB_OUTPUT"
+ echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})"
+
+ - name: "Publish stable release"
+ run: |
+ BUMP_FLAG=""
+ if [ "${{ steps.bump.outputs.level }}" != "none" ]; then
+ BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}"
+ fi
+ php ${MOKO_CLI}/release_publish.php \
+ --path . --stability stable ${BUMP_FLAG} --branch main \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}"
+
+ - name: Update release notes from CHANGELOG.md
+ run: |
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+
+ # Extract [Unreleased] section from changelog
+ if [ -f "CHANGELOG.md" ]; then
+ NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
+ [ -z "$NOTES" ] && NOTES="Stable release"
+ else
+ NOTES="Stable release"
+ fi
+
+ # Update release body via API
+ RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
+ "${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
+
+ if [ -n "$RELEASE_ID" ]; then
+ python3 -c "
+ import json, urllib.request
+ body = open('/dev/stdin').read()
+ payload = json.dumps({'body': body}).encode()
+ req = urllib.request.Request(
+ '${API_BASE}/releases/${RELEASE_ID}',
+ data=payload, method='PATCH',
+ headers={
+ 'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
+ 'Content-Type': 'application/json'
+ })
+ urllib.request.urlopen(req)
+ " <<< "$NOTES"
+ echo "Release notes updated from CHANGELOG.md"
+ fi
+
+ # -- 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 ${MOKO_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 ${MOKO_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/cascade-dev.yml b/.mokogitea/workflows/cascade-dev.yml
new file mode 100644
index 0000000..5f7c1d7
--- /dev/null
+++ b/.mokogitea/workflows/cascade-dev.yml
@@ -0,0 +1,10 @@
+# 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-generic.yml b/.mokogitea/workflows/ci-generic.yml
new file mode 100644
index 0000000..87fd059
--- /dev/null
+++ b/.mokogitea/workflows/ci-generic.yml
@@ -0,0 +1,204 @@
+# 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/Template-Generic
+# PATH: /.gitea/workflows/ci-generic.yml
+# VERSION: 01.00.00
+# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js)
+
+name: "Generic: Project CI"
+
+on:
+ push:
+ branches:
+ - main
+ - dev
+ - dev/**
+ - rc/**
+ - version/**
+ pull_request:
+ branches:
+ - main
+ - dev
+ - dev/**
+ - rc/**
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+
+jobs:
+ # ── Lint & Validate ───────────────────────────────────────────────────
+ lint:
+ name: Lint & Validate
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Detect toolchain
+ id: detect
+ run: |
+ HAS_PHP=false
+ HAS_NODE=false
+ [ -f "composer.json" ] && HAS_PHP=true
+ [ -f "package.json" ] && HAS_NODE=true
+ echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
+ echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
+ echo "Toolchain: PHP=$HAS_PHP Node=$HAS_NODE"
+
+ - name: Setup PHP
+ if: steps.detect.outputs.has_php == 'true'
+ 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
+ php -v
+
+ - name: Setup Node.js
+ if: steps.detect.outputs.has_node == 'true'
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - name: Install PHP dependencies
+ if: steps.detect.outputs.has_php == 'true'
+ run: |
+ if [ -f "composer.json" ]; then
+ composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
+ fi
+
+ - name: Install Node.js dependencies
+ if: steps.detect.outputs.has_node == 'true'
+ run: |
+ if [ -f "package.json" ]; then
+ npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true
+ fi
+
+ - name: PHP syntax check
+ if: steps.detect.outputs.has_php == 'true'
+ run: |
+ ERRORS=0
+ while IFS= read -r -d '' file; do
+ if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
+ echo "::error file=${file}::PHP syntax error"
+ ERRORS=$((ERRORS + 1))
+ fi
+ done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" -print0)
+
+ echo "## PHP Lint" >> $GITHUB_STEP_SUMMARY
+ if [ "$ERRORS" -eq 0 ]; then
+ echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
+ else
+ echo "${ERRORS} file(s) with syntax errors." >> $GITHUB_STEP_SUMMARY
+ exit 1
+ fi
+
+ - name: TypeScript/JavaScript lint
+ if: steps.detect.outputs.has_node == 'true'
+ run: |
+ if [ -f "node_modules/.bin/eslint" ]; then
+ npx eslint src/ --quiet 2>&1 || { echo "::error::ESLint errors found"; exit 1; }
+ echo "## ESLint" >> $GITHUB_STEP_SUMMARY
+ echo "All files passed ESLint." >> $GITHUB_STEP_SUMMARY
+ elif [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f "eslint.config.js" ]; then
+ echo "::warning::ESLint config found but eslint not installed"
+ else
+ echo "No ESLint configured — skipping"
+ fi
+
+ - name: TypeScript compile check
+ if: steps.detect.outputs.has_node == 'true'
+ run: |
+ if [ -f "tsconfig.json" ] && [ -f "node_modules/.bin/tsc" ]; then
+ npx tsc --noEmit 2>&1 || { echo "::error::TypeScript compilation errors"; exit 1; }
+ echo "## TypeScript" >> $GITHUB_STEP_SUMMARY
+ echo "TypeScript compilation passed." >> $GITHUB_STEP_SUMMARY
+ fi
+
+ - name: PHPStan static analysis
+ if: steps.detect.outputs.has_php == 'true'
+ run: |
+ if [ -f "phpstan.neon" ] && [ -f "vendor/bin/phpstan" ]; then
+ vendor/bin/phpstan analyse --no-progress 2>&1 || { echo "::warning::PHPStan found issues"; }
+ fi
+
+ # ── Tests ─────────────────────────────────────────────────────────────
+ test:
+ name: Tests
+ runs-on: ubuntu-latest
+ needs: lint
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Detect toolchain
+ id: detect
+ run: |
+ HAS_PHP=false
+ HAS_NODE=false
+ [ -f "composer.json" ] && HAS_PHP=true
+ [ -f "package.json" ] && HAS_NODE=true
+ echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
+ echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
+
+ - name: Setup PHP
+ if: steps.detect.outputs.has_php == 'true'
+ 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: Setup Node.js
+ if: steps.detect.outputs.has_node == 'true'
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - name: Install dependencies
+ run: |
+ [ -f "composer.json" ] && composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
+ [ -f "package.json" ] && { npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true; }
+
+ - name: Run PHP tests
+ if: steps.detect.outputs.has_php == 'true'
+ run: |
+ if [ -f "vendor/bin/phpunit" ]; then
+ vendor/bin/phpunit --testdox 2>&1
+ echo "## PHPUnit" >> $GITHUB_STEP_SUMMARY
+ echo "Tests passed." >> $GITHUB_STEP_SUMMARY
+ elif [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
+ echo "::warning::PHPUnit config found but phpunit not installed"
+ else
+ echo "No PHPUnit configured — skipping"
+ fi
+
+ - name: Run Node.js tests
+ if: steps.detect.outputs.has_node == 'true'
+ run: |
+ if jq -e '.scripts.test' package.json > /dev/null 2>&1; then
+ npm test 2>&1
+ echo "## Node.js Tests" >> $GITHUB_STEP_SUMMARY
+ echo "Tests passed." >> $GITHUB_STEP_SUMMARY
+ else
+ echo "No test script in package.json — skipping"
+ fi
+
+ - name: Build check
+ run: |
+ if [ -f "Makefile" ]; then
+ make build 2>&1 || echo "::warning::Build failed or not configured"
+ elif [ -f "package.json" ] && jq -e '.scripts.build' package.json > /dev/null 2>&1; then
+ npm run build 2>&1 || echo "::warning::Build failed"
+ fi
diff --git a/.mokogitea/workflows/ci-joomla.yml b/.mokogitea/workflows/ci-joomla.yml
index de2d9eb..0c6f5ea 100644
--- a/.mokogitea/workflows/ci-joomla.yml
+++ b/.mokogitea/workflows/ci-joomla.yml
@@ -35,25 +35,32 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ 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 php-zip php-curl composer >/dev/null 2>&1
+ fi
php -v && composer --version
- - name: Clone MokoStandards
+ - name: Setup moko-platform tools
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' }}
+ MOKO_CLONE_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
+ if [ -d "/tmp/moko-platform" ] || [ -d "/opt/moko-platform" ]; then
+ echo "moko-platform already available on runner — skipping clone"
+ else
+ git clone --depth 1 --branch main --quiet \
+ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
+ /tmp/moko-platform 2>/dev/null || echo "moko-platform clone skipped — continuing without it"
+ fi
- name: Install dependencies
env:
- COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
+ COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install \
@@ -124,8 +131,8 @@ jobs:
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
+ # Check required tags: name, version, author
+ for TAG in name version author; do
if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then
echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
@@ -133,6 +140,19 @@ jobs:
echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
fi
done
+
+ # Namespace is required for components/plugins but not packages
+ EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1)
+ if [ "$EXT_TYPE" != "package" ]; then
+ if ! grep -q "/dev/null; then
+ echo "Missing required tag: \`\` (required for Joomla 5+ ${EXT_TYPE} extensions)" >> $GITHUB_STEP_SUMMARY
+ ERRORS=$((ERRORS + 1))
+ else
+ echo "Found required tag: \`\`" >> $GITHUB_STEP_SUMMARY
+ fi
+ else
+ echo "Package extension — \`\` not required." >> $GITHUB_STEP_SUMMARY
+ fi
fi
if [ "${ERRORS}" -gt 0 ]; then
@@ -232,7 +252,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ uses: actions/checkout@v4
- name: Validate release readiness
run: |
@@ -338,15 +358,19 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ uses: actions/checkout@v4
- name: Setup PHP ${{ matrix.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
php -v && composer --version
- name: Install dependencies
env:
- COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
+ COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install \
@@ -384,14 +408,19 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ uses: actions/checkout@v4
- name: Setup PHP
- run: php -v && composer --version
+ 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
+ php -v && composer --version
- name: Install dependencies
env:
- COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
+ COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install --no-interaction --prefer-dist --optimize-autoloader
@@ -458,10 +487,14 @@ jobs:
steps:
- name: Trigger pre-release build
env:
- GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
+ GA_TOKEN: ${{ secrets.GA_TOKEN }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }}
run: |
- curl -s -X POST "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
+ curl -s -X POST \
+ "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" \
+ -H "Authorization: token ${GA_TOKEN}" \
+ -H "Content-Type: application/json" \
+ -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
diff --git a/.mokogitea/workflows/cleanup.yml b/.mokogitea/workflows/cleanup.yml
index 29ca4d4..3a81856 100644
--- a/.mokogitea/workflows/cleanup.yml
+++ b/.mokogitea/workflows/cleanup.yml
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Maintenance
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
+# INGROUP: MokoStandards.Maintenance
+# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/cleanup.yml
# VERSION: 01.00.00
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
@@ -33,17 +33,17 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
- token: ${{ secrets.MOKOGITEA_TOKEN }}
+ token: ${{ secrets.GA_TOKEN }}
- name: Delete merged branches
env:
- GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
+ GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
echo "=== Merged Branch Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
# List branches via API
- BRANCHES=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
+ BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
"${API}/branches?limit=50" | jq -r '.[].name')
DELETED=0
@@ -56,7 +56,7 @@ jobs:
# Check if branch is merged into main
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
echo " Deleting merged branch: ${BRANCH}"
- curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
+ curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
"${API}/branches/${BRANCH}" 2>/dev/null || true
DELETED=$((DELETED + 1))
fi
@@ -66,20 +66,20 @@ jobs:
- name: Clean old workflow runs
env:
- GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
+ GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
echo "=== Workflow Run Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
# Get old completed runs
- RUNS=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
+ RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
"${API}/actions/runs?status=completed&limit=50" | \
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
DELETED=0
for RUN_ID in $RUNS; do
- curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
+ curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
DELETED=$((DELETED + 1))
done
diff --git a/.mokogitea/workflows/deploy-manual.yml b/.mokogitea/workflows/deploy-manual.yml
new file mode 100644
index 0000000..bb133ed
--- /dev/null
+++ b/.mokogitea/workflows/deploy-manual.yml
@@ -0,0 +1,126 @@
+# 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: "Universal: 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/workflows/gitleaks.yml b/.mokogitea/workflows/gitleaks.yml
index e0fdd1d..0c07612 100644
--- a/.mokogitea/workflows/gitleaks.yml
+++ b/.mokogitea/workflows/gitleaks.yml
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Security
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
+# INGROUP: MokoStandards.Security
+# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/gitleaks.yml.template
# VERSION: 01.00.00
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
diff --git a/.mokogitea/workflows/issue-branch.yml b/.mokogitea/workflows/issue-branch.yml
index fc49b37..c2b02a6 100644
--- a/.mokogitea/workflows/issue-branch.yml
+++ b/.mokogitea/workflows/issue-branch.yml
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation
-# VERSION: 01.19.00
+# VERSION: 01.00.00
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
diff --git a/.mokogitea/workflows/notify.yml b/.mokogitea/workflows/notify.yml
index cde4541..51dfcb5 100644
--- a/.mokogitea/workflows/notify.yml
+++ b/.mokogitea/workflows/notify.yml
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Notifications
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
+# INGROUP: MokoStandards.Notifications
+# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/notify.yml
# VERSION: 01.00.00
# BRIEF: Push notifications via ntfy on release success or workflow failure
diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml
new file mode 100644
index 0000000..bc53b7f
--- /dev/null
+++ b/.mokogitea/workflows/pre-release.yml
@@ -0,0 +1,11 @@
+# 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: Auto pre-release on push to dev/alpha/beta/rc branches
\ No newline at end of file
diff --git a/.mokogitea/workflows/security-audit.yml b/.mokogitea/workflows/security-audit.yml
index 714d407..789325a 100644
--- a/.mokogitea/workflows/security-audit.yml
+++ b/.mokogitea/workflows/security-audit.yml
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Security
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
+# INGROUP: MokoStandards.Security
+# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/security-audit.yml
# VERSION: 01.00.00
# BRIEF: Dependency vulnerability scanning for composer and npm packages
@@ -80,19 +80,3 @@ jobs:
-H "Priority: high" \
-d "Security audit found vulnerabilities. Review dependency updates." \
"${NTFY_URL}/${NTFY_TOPIC}" || true
-
-
- - name: Joomla version audit
- if: always()
- run: |
- if [ -f "monitoring/joomla-version-audit.php" ] && [ -n "$JOOMLA_SITES" ]; then
- echo "$JOOMLA_SITES" > /tmp/sites.json
- php monitoring/joomla-version-audit.php --sites /tmp/sites.json || true
- echo "### Joomla Version Audit" >> $GITHUB_STEP_SUMMARY
- rm -f /tmp/sites.json
- else
- echo "Joomla audit skipped (no script or JOOMLA_SITES_JSON not configured)"
- fi
- env:
- JOOMLA_SITES: ${{ vars.JOOMLA_SITES_JSON }}
-
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2a0ff35..85606a7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,8 @@
# Changelog
## [Unreleased]
+## [01.20.00] --- 2026-06-04
+
## [01.19.00] --- 2026-06-04
@@ -23,10 +25,3 @@
### Fixed
- Add missing `` section to module manifest
-
-## [01.13] - 2026-06-04
-
-### Changed
-- Restructure from package extension back to standalone site module
-- Remove system plugin (`plg_system_mokojoomhero`) and package wrapper
-- Simplify build target for module ZIP
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 8247207..f4a9f02 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -14,7 +14,7 @@
DEFGROUP:
INGROUP: Project.Documentation
REPO:
- VERSION: 01.19.00
+ VERSION: 01.20.00
PATH: ./CODE_OF_CONDUCT.md
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
-->
diff --git a/SECURITY.md b/SECURITY.md
index 8d23d0d..354b7b5 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: 01.19.00
+VERSION: 01.20.00
BRIEF: Security vulnerability reporting and handling policy
-->
diff --git a/src/language/en-GB/mod_mokojoomhero.ini b/src/language/en-GB/mod_mokojoomhero.ini
index 8c8a268..0550515 100644
--- a/src/language/en-GB/mod_mokojoomhero.ini
+++ b/src/language/en-GB/mod_mokojoomhero.ini
@@ -6,7 +6,7 @@
; INGROUP: MokoJoomHero.Module
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
; PATH: /src/language/en-GB/mod_mokojoomhero.ini
-; VERSION: 01.19.00
+; VERSION: 01.20.00
; BRIEF: Language strings for MokoJoomHero module (frontend + admin form fields)
MOD_MOKOJOOMHERO_NO_CONTENT="Add content to this module to display it over the hero image."
diff --git a/src/language/en-GB/mod_mokojoomhero.sys.ini b/src/language/en-GB/mod_mokojoomhero.sys.ini
index dde2175..edda0b1 100644
--- a/src/language/en-GB/mod_mokojoomhero.sys.ini
+++ b/src/language/en-GB/mod_mokojoomhero.sys.ini
@@ -6,7 +6,7 @@
; INGROUP: MokoJoomHero.Module
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
; PATH: /src/language/en-GB/mod_mokojoomhero.sys.ini
-; VERSION: 01.19.00
+; VERSION: 01.20.00
; BRIEF: System language strings — used in admin Extension Manager and Module Manager
MOD_MOKOJOOMHERO="Module - MokoJoomHero"
diff --git a/src/language/en-US/mod_mokojoomhero.ini b/src/language/en-US/mod_mokojoomhero.ini
index 3f7b473..ad72e48 100644
--- a/src/language/en-US/mod_mokojoomhero.ini
+++ b/src/language/en-US/mod_mokojoomhero.ini
@@ -6,7 +6,7 @@
; INGROUP: MokoJoomHero.Module
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
; PATH: /src/language/en-US/mod_mokojoomhero.ini
-; VERSION: 01.19.00
+; VERSION: 01.20.00
; BRIEF: Language strings for MokoJoomHero module (en-US, frontend + admin form fields)
MOD_MOKOJOOMHERO_NO_CONTENT="Add content to this module to display it over the hero image."
diff --git a/src/language/en-US/mod_mokojoomhero.sys.ini b/src/language/en-US/mod_mokojoomhero.sys.ini
index 1b45035..b4d815b 100644
--- a/src/language/en-US/mod_mokojoomhero.sys.ini
+++ b/src/language/en-US/mod_mokojoomhero.sys.ini
@@ -6,7 +6,7 @@
; INGROUP: MokoJoomHero.Module
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
; PATH: /src/language/en-US/mod_mokojoomhero.sys.ini
-; VERSION: 01.19.00
+; VERSION: 01.20.00
; BRIEF: System language strings — used in admin Extension Manager and Module Manager (en-US)
MOD_MOKOJOOMHERO="Module - MokoJoomHero"
diff --git a/src/media/css/mod_mokojoomhero.css b/src/media/css/mod_mokojoomhero.css
index 1cd4e98..4648117 100644
--- a/src/media/css/mod_mokojoomhero.css
+++ b/src/media/css/mod_mokojoomhero.css
@@ -7,7 +7,7 @@
* INGROUP: MokoJoomHero.Module
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
* PATH: /src/media/css/mod_mokojoomhero.css
- * VERSION: 01.19.00
+ * VERSION: 01.20.00
* BRIEF: Hero module stylesheet — slideshow, video, colour/gradient, overlay, card, mute toggle, responsive
*/
diff --git a/src/media/js/mod_mokojoomhero.js b/src/media/js/mod_mokojoomhero.js
index f6a88f6..644bf71 100644
--- a/src/media/js/mod_mokojoomhero.js
+++ b/src/media/js/mod_mokojoomhero.js
@@ -8,7 +8,7 @@
* INGROUP: MokoJoomHero.Module
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
* PATH: /src/media/js/mod_mokojoomhero.js
- * VERSION: 01.19.00
+ * VERSION: 01.20.00
* BRIEF: Hero module JavaScript — slideshow crossfade, video viewport control, mute toggle
*/
diff --git a/src/mod_mokojoomhero.xml b/src/mod_mokojoomhero.xml
index 035e518..53e9160 100644
--- a/src/mod_mokojoomhero.xml
+++ b/src/mod_mokojoomhero.xml
@@ -22,7 +22,7 @@
https://mokoconsulting.tech
Copyright (C) 2026 Moko Consulting. All rights reserved.
GPL-3.0-or-later
- 01.19.00
+ 01.20.00
Random hero image slideshow, video backgrounds, solid colour/gradient, parallax, content animations, A/B testing, scheduling, and overlay with card support. Free and open source. By Moko Consulting.
script.php