Compare commits
145 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 80be62026f | |||
| 9d5d60fadf | |||
| a0a9b4c204 | |||
| c6ecee1917 | |||
| b3aa838da6 | |||
| 4c62eac923 | |||
| cb64d7371d | |||
| 312ba8072f | |||
| 5b378b564b | |||
| d6a1e77453 | |||
| 2062575736 | |||
| 06d9499b39 | |||
| 39177bf78b | |||
| 97cc0acdf3 | |||
| fcacd01af9 | |||
| bab1187da3 | |||
| 940caa3d2d | |||
| b61a1eff6d | |||
| a49fe2add8 | |||
| eec803edac | |||
| cf5400a047 | |||
| 436a3b1029 | |||
| 9b07bfa167 | |||
| f7a0c3672d | |||
| d2139df514 | |||
| 159b71b1bd | |||
| e1b0fafd70 | |||
| 64e0aec932 | |||
| 52b1f33f3e | |||
| 9e55617f0f | |||
| 7228bb9744 | |||
| 20922e7a1c | |||
| 4579cf684b | |||
| f193d62652 | |||
| 7b0aa534ed | |||
| 416725c212 | |||
| a4e6bdd2af | |||
| b6b212e7d2 | |||
| a984b03e07 | |||
| 6f92653ed7 | |||
| 53da1d7457 | |||
| 6ec75f2b5d | |||
| 07a4841dec | |||
| fff64be76d | |||
| f01d7ba140 | |||
| 0b691be220 | |||
| d9c8752081 | |||
| 9373459301 | |||
| d3494e1f36 | |||
| 978e1774aa | |||
| bbf9ef3310 | |||
| 010a467f5c | |||
| 8cb83668e1 | |||
| 01e6778664 | |||
| 2b5734207c | |||
| 33979440b4 | |||
| 983e56fc16 | |||
| 54b27702e9 | |||
| 67ee1d8282 | |||
| 2c86454989 | |||
| 0856361ebc | |||
| 802bab7777 | |||
| f4ed92c0c3 | |||
| 0ee3b181a5 | |||
| 3226a75086 | |||
| d4ac99c7d9 | |||
| e65ff7e422 | |||
| 998e2f0622 | |||
| e0a7348e2c | |||
| a47d229cbb | |||
| 693ab27fe7 | |||
| 93cf3ff81a | |||
| 694947586f | |||
| f2c1ce0764 | |||
| 20d085f91c | |||
| fc359ec5a5 | |||
| 63e76c27fd | |||
| 87853baf75 | |||
| f35ccb197a | |||
| 3e19ca7408 | |||
| 65d156e15b | |||
| d38766d0b8 | |||
| c3bb1a1295 | |||
| d6f0e1fe0c | |||
| a1ba923155 | |||
| 60b2ee2436 | |||
| 3efcba855d | |||
| 2614e6d930 | |||
| 71bf7c78b7 | |||
| 3a29a94920 | |||
| abdc861dfb | |||
| d42f23dae9 | |||
| 63613aa48c | |||
| f33e0f2701 | |||
| fae7614c29 | |||
| 5b4d5e4d42 | |||
| 0e7a022478 | |||
| da96e87969 | |||
| af3e53b734 | |||
| d9b14431e6 | |||
| e8d9755a2d | |||
| ea347ea41b | |||
| 6e28f9c89a | |||
| 785bfca673 | |||
| eef5d54774 | |||
| 1d99daf4a5 | |||
| 81eaf0d4e3 | |||
| fb376ae368 | |||
| 5bd15387c1 | |||
| 6594bb6239 | |||
| f2930c401f | |||
| f2f91f2f2c | |||
| 649b5c1f23 | |||
| 2923918e0b | |||
| 45e57510f4 | |||
| f9aa1b91ce | |||
| 92491d5559 | |||
| d9db104dd7 | |||
| 660d65aee4 | |||
| 22d8965be1 | |||
| 4d104b8dc7 | |||
| 7b4f03d1f2 | |||
| d201fa0384 | |||
| a1a722c267 | |||
| 739b9f974a | |||
| f65afd370c | |||
| 92609bbdf3 | |||
| 9bae409466 | |||
| ccd9e3c0e4 | |||
| cd780ce8a3 | |||
| 687fdb9ed7 | |||
| 20b56b9b00 | |||
| afdd7b4fe8 | |||
| e65ef8a140 | |||
| 7800ca140f | |||
| f269dfd441 | |||
| 7af5c44406 | |||
| 1d6854b4be | |||
| c4b1983e9e | |||
| ad3d46180b | |||
| 5dfcf926be | |||
| 50ee63ef80 | |||
| 6371c7240c | |||
| 2810caa810 | |||
| 4efce6b8ef |
@@ -1,126 +0,0 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# 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
|
||||
@@ -1,383 +0,0 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||
# VERSION: 05.00.00
|
||||
# BRIEF: Manual pre-release — builds dev/alpha/beta/rc packages from any branch
|
||||
|
||||
name: "Universal: 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: Detect platform
|
||||
id: platform
|
||||
run: |
|
||||
# Read platform from .gitea/manifest.xml
|
||||
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .gitea/manifest.xml 2>/dev/null | head -1)
|
||||
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./.gitea/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
|
||||
echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
|
||||
echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- 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 platform-specific manifest
|
||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||
MANIFEST="${{ steps.platform.outputs.manifest }}"
|
||||
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
|
||||
case "$PLATFORM" in
|
||||
joomla)
|
||||
if [ -n "$MANIFEST" ]; then
|
||||
MANIFEST_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
|
||||
sed -i "s|<version>${MANIFEST_VER}</version>|<version>${VERSION}</version>|" "$MANIFEST"
|
||||
sed -i "s|<creationDate>[^<]*</creationDate>|<creationDate>${TODAY}</creationDate>|" "$MANIFEST"
|
||||
fi
|
||||
;;
|
||||
dolibarr)
|
||||
if [ -n "$MOD_FILE" ]; then
|
||||
sed -i "s/\$this->version = '[^']*'/\$this->version = '${VERSION}'/" "$MOD_FILE"
|
||||
fi
|
||||
;;
|
||||
*) ;;
|
||||
esac
|
||||
|
||||
# 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 (platform-aware)
|
||||
case "$PLATFORM" in
|
||||
joomla)
|
||||
MANIFEST="${{ steps.platform.outputs.manifest }}"
|
||||
EXT_ELEMENT=""
|
||||
if [ -n "$MANIFEST" ]; then
|
||||
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/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
|
||||
;;
|
||||
dolibarr)
|
||||
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
|
||||
if [ -n "$MOD_FILE" ]; then
|
||||
MOD_BASENAME=$(basename "$MOD_FILE" .class.php)
|
||||
EXT_ELEMENT=$(echo "$MOD_BASENAME" | sed 's/^mod//' | tr '[:upper:]' '[:lower:]')
|
||||
else
|
||||
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||
;;
|
||||
esac
|
||||
|
||||
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
|
||||
if: steps.platform.outputs.platform == 'joomla'
|
||||
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"(<update>(?:(?!</update>).)*?<tag>" + re.escape(xml_tag) + r"</tag>.*?</update>)"
|
||||
match = re.search(pattern, content, re.DOTALL)
|
||||
if match:
|
||||
block = match.group(1)
|
||||
updated = re.sub(r"<version>[^<]*</version>", f"<version>{version}</version>", block)
|
||||
updated = re.sub(r"<creationDate>[^<]*</creationDate>", f"<creationDate>{date}</creationDate>", updated)
|
||||
if "<sha256>" in updated:
|
||||
updated = re.sub(r"<sha256>[^<]*</sha256>", f"<sha256>{sha256}</sha256>", updated)
|
||||
else:
|
||||
updated = updated.replace("</downloads>", f"</downloads>\n <sha256>{sha256}</sha256>")
|
||||
updated = re.sub(r"(<downloadurl[^>]*>)[^<]*(</downloadurl>)", 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 <tag>{xml_tag}</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"
|
||||
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]"
|
||||
|
||||
# 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
|
||||
@@ -1,20 +0,0 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: MokoStandards.Templates.Config
|
||||
# INGROUP: MokoStandards.Templates
|
||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||
# PATH: /templates/configs/moko-standards.yml
|
||||
# VERSION: 04.01.00
|
||||
# BRIEF: Governance attachment template — synced to .moko-standards in every governed repository
|
||||
# NOTE: Tokens replaced at sync time: mokoconsulting-tech, MokoJoomTOS, waas-component, 04.00.04
|
||||
#
|
||||
# This file is managed automatically by MokoStandards bulk sync.
|
||||
# Do not edit manually — changes will be overwritten on the next sync.
|
||||
# To update governance settings, open a PR in MokoStandards instead:
|
||||
# https://github.com/mokoconsulting-tech/MokoStandards
|
||||
|
||||
standards_source: "https://github.com/mokoconsulting-tech/MokoStandards"
|
||||
standards_version: "04.00.04"
|
||||
platform: "waas-component"
|
||||
governed_repo: "mokoconsulting-tech/MokoJoomTOS"
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
name: WaaS Client Site Issue
|
||||
about: Report an issue with a WaaS client site (branding, deployment, media sync)
|
||||
title: '[WAAS] '
|
||||
labels: 'waas, client-site'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Site Issue Type
|
||||
- [ ] Branding / CSS not applying
|
||||
- [ ] Deployment failure
|
||||
- [ ] Media sync issue
|
||||
- [ ] Template override not working
|
||||
- [ ] Module positioning issue
|
||||
- [ ] Mobile / responsive layout
|
||||
- [ ] Performance issue
|
||||
|
||||
## Client Site
|
||||
- **Client Org**: [e.g., ClarksvilleFurs]
|
||||
- **Repo**: [e.g., client-waas-clarksvillefurs]
|
||||
- **Environment**: [Dev / Production]
|
||||
- **Site URL**: [dev or production URL — omit if private]
|
||||
|
||||
## Issue Description
|
||||
Describe the issue clearly.
|
||||
|
||||
## Steps to Reproduce
|
||||
1. Visit [page URL]
|
||||
2. Look at [element]
|
||||
3. See error
|
||||
|
||||
## Expected Behavior
|
||||
What the site should look like or how it should behave.
|
||||
|
||||
## Actual Behavior
|
||||
What is happening instead.
|
||||
|
||||
## Screenshots
|
||||
Attach screenshots showing the issue (desktop and mobile if relevant).
|
||||
|
||||
## Deployment Status
|
||||
- **Last deploy**: [date or "unknown"]
|
||||
- **Deploy workflow**: [succeeded / failed / not run]
|
||||
- **Branch**: [dev / main]
|
||||
|
||||
## Media Sync
|
||||
- [ ] Images missing after sync
|
||||
- [ ] Sync direction: [dev-to-prod / prod-to-dev / bidirectional]
|
||||
- [ ] Last sync: [date]
|
||||
|
||||
## Template Details
|
||||
- **Joomla Version**: [e.g., 5.x]
|
||||
- **Template Name**: [e.g., clienttemplate]
|
||||
- **MokoWaaS Plugin**: [Active / Inactive]
|
||||
- **MokoOnyx Admin**: [Active / Inactive]
|
||||
|
||||
## CSS Custom Properties
|
||||
If branding issue, list the relevant CSS variables:
|
||||
```css
|
||||
:root {
|
||||
--client-primary: #...;
|
||||
--client-secondary: #...;
|
||||
}
|
||||
```
|
||||
|
||||
## Browser / Device
|
||||
- **Browser**: [e.g., Chrome 120, Safari 17]
|
||||
- **Device**: [Desktop / Tablet / Mobile]
|
||||
- **Screen Width**: [e.g., 1920px, 768px, 375px]
|
||||
|
||||
## Checklist
|
||||
- [ ] I have cleared Joomla cache
|
||||
- [ ] I have hard-refreshed the browser (Ctrl+Shift+R)
|
||||
- [ ] I have checked the deploy workflow completed
|
||||
- [ ] I have verified the change is on the correct branch
|
||||
- [ ] No credentials or PII are included in this issue
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
MokoStandards Repository Manifest
|
||||
Auto-generated by cleanup script.
|
||||
Moko Platform Repository Manifest
|
||||
See: https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home
|
||||
-->
|
||||
<moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/1.0" schema-version="1.0">
|
||||
@@ -4,8 +4,8 @@
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Release
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||
# 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
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
token: ${{ secrets.GA_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup moko-platform
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
@@ -69,132 +69,47 @@ jobs:
|
||||
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 ]; then
|
||||
cd /tmp/mokostandards-api
|
||||
composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||
fi
|
||||
echo "MokoStandards tools: $([ -d /tmp/mokostandards-api ] && echo 'available' || echo 'unavailable (using fallback)')"
|
||||
"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
|
||||
|
||||
|
||||
# -- PLATFORM DETECTION ---------------------------------------------------
|
||||
- name: Detect platform
|
||||
id: platform
|
||||
run: |
|
||||
# 1. Try explicit platform from manifest (.mokogitea takes precedence)
|
||||
if [ -f ".mokogitea/manifest.xml" ]; then
|
||||
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
|
||||
elif [ -f ".gitea/manifest.xml" ]; then
|
||||
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .gitea/manifest.xml 2>/dev/null | head -1)
|
||||
fi
|
||||
|
||||
# 2. Auto-sense from file structure if no manifest
|
||||
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||
CB_MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<cbplugin' {} \; 2>/dev/null | head -1)
|
||||
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
|
||||
|
||||
if [ -z "$PLATFORM" ]; then
|
||||
if [ -n "$CB_MANIFEST" ]; then
|
||||
PLATFORM="joomla"
|
||||
MANIFEST="$CB_MANIFEST"
|
||||
elif [ -n "$MANIFEST" ]; then
|
||||
PLATFORM="joomla"
|
||||
elif [ -n "$MOD_FILE" ]; then
|
||||
PLATFORM="dolibarr"
|
||||
else
|
||||
PLATFORM="generic"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||
echo "Platform detected: ${PLATFORM}"
|
||||
php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output
|
||||
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
|
||||
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1 || true)
|
||||
echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
|
||||
echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Detect update file methodology
|
||||
if [ "$PLATFORM" = "joomla" ]; then
|
||||
echo "update_method=updates.xml" >> "$GITHUB_OUTPUT"
|
||||
echo "Update method: Joomla updates.xml"
|
||||
elif [ "$PLATFORM" = "dolibarr" ]; then
|
||||
echo "update_method=update.txt" >> "$GITHUB_OUTPUT"
|
||||
echo "Update method: Dolibarr update.txt"
|
||||
else
|
||||
echo "update_method=none" >> "$GITHUB_OUTPUT"
|
||||
echo "Update method: none (generic)"
|
||||
fi
|
||||
|
||||
# -- STEP 1: Read version -----------------------------------------------
|
||||
- name: "Step 1: Read version from README.md"
|
||||
- name: "Step 1: Read version"
|
||||
id: version
|
||||
run: |
|
||||
# Try MokoStandards PHP tool first, fall back to direct parsing
|
||||
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || true)
|
||||
VERSION=$(php /tmp/moko-platform-api/cli/version_read.php --path .)
|
||||
if [ -z "$VERSION" ]; then
|
||||
# Fallback: read from README table format "| **Version** | XX.YY.ZZ |"
|
||||
VERSION=$(sed -n 's/.*\*\*Version\*\*[[:space:]]*|[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
|
||||
fi
|
||||
if [ -z "$VERSION" ]; then
|
||||
# Fallback: read VERSION: XX.YY.ZZ pattern from any file
|
||||
VERSION=$(grep -rh "VERSION:[[:space:]]*[0-9]" README.md CHANGELOG.md 2>/dev/null | sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' | head -1)
|
||||
fi
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "No VERSION in README.md — skipping release"
|
||||
echo "::error::No VERSION in README.md"
|
||||
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: Promote CHANGELOG [Unreleased] to current version -----------
|
||||
- name: "Step 1b: Promote CHANGELOG for release"
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
id: bump
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
TODAY=$(date +%Y-%m-%d)
|
||||
|
||||
# Promote [Unreleased] section in CHANGELOG.md to release 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 changelog promotion if changed
|
||||
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(release): promote CHANGELOG ${VERSION} [skip ci]"
|
||||
git push origin HEAD:main 2>&1
|
||||
}
|
||||
|
||||
# Pass through version (no bump — release uses version as-is from dev)
|
||||
MAJOR=$(echo "$VERSION" | cut -d. -f1)
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "major=${{ steps.version.outputs.major }}" >> "$GITHUB_OUTPUT"
|
||||
echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT"
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: "Step 1b: Bump version"
|
||||
id: bump
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
run: |
|
||||
MOKO_API="/tmp/moko-platform-api/cli"
|
||||
BUMP=$(php ${MOKO_API}/version_bump.php --path . --minor)
|
||||
VERSION=$(echo "$BUMP" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true)
|
||||
[ -z "$VERSION" ] && VERSION=$(php ${MOKO_API}/version_read.php --path .)
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "Bumped to: ${VERSION}"
|
||||
|
||||
- name: Check if already released
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
@@ -339,199 +254,27 @@ jobs:
|
||||
steps.check.outputs.already_released != 'true'
|
||||
run: |
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
if [ -f /tmp/mokostandards-api/cli/version_set_platform.php ]; then
|
||||
php /tmp/mokostandards-api/cli/version_set_platform.php \
|
||||
--path . --version "$VERSION" --branch main
|
||||
else
|
||||
# Fallback: update version in templateDetails.xml directly
|
||||
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||
if [ -n "$MANIFEST" ]; then
|
||||
sed -i "s|<version>[^<]*</version>|<version>${VERSION}</version>|" "$MANIFEST"
|
||||
sed -i "s|<creationDate>[^<]*</creationDate>|<creationDate>$(date +%Y-%m-%d)</creationDate>|" "$MANIFEST"
|
||||
fi
|
||||
# Update README version table
|
||||
sed -i "s/|\s*\*\*Version\*\*\s*|[^|]*/| **Version** | ${VERSION} /" README.md 2>/dev/null || true
|
||||
fi
|
||||
php /tmp/moko-platform-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'
|
||||
if: steps.version.outputs.skip != '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
|
||||
php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true
|
||||
|
||||
# -- STEP 5: Write updates.xml (Joomla update server) ---------------------
|
||||
- name: "Step 5: Write update stream"
|
||||
id: updates
|
||||
if: >-
|
||||
steps.version.outputs.skip != 'true' &&
|
||||
steps.check.outputs.already_released != 'true'
|
||||
steps.platform.outputs.platform == 'joomla'
|
||||
run: |
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
REPO="${{ github.repository }}"
|
||||
php /tmp/moko-platform-api/cli/updates_xml_build.php \
|
||||
--path . --version "${VERSION}" --stability stable \
|
||||
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
||||
--github-output
|
||||
|
||||
# -- Parse extension metadata from XML manifest ----------------
|
||||
# Check for Joomla <extension> or Community Builder <cbplugin>
|
||||
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||
CB_MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<cbplugin' {} \; 2>/dev/null | head -1)
|
||||
IS_CB="false"
|
||||
|
||||
if [ -z "$MANIFEST" ] && [ -z "$CB_MANIFEST" ]; then
|
||||
echo "Warning: No Joomla/CB XML manifest found — skipping updates.xml" >> $GITHUB_STEP_SUMMARY
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# CB plugin takes precedence if no standard manifest
|
||||
if [ -z "$MANIFEST" ] && [ -n "$CB_MANIFEST" ]; then
|
||||
MANIFEST="$CB_MANIFEST"
|
||||
IS_CB="true"
|
||||
fi
|
||||
|
||||
# Extract fields using sed (portable — no grep -P)
|
||||
EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
|
||||
if [ "$IS_CB" = "true" ]; then
|
||||
EXT_TYPE="cb_plugin"
|
||||
EXT_ELEMENT=$(sed -n 's/.*<cbplugin[^>]*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||
EXT_CLIENT=""
|
||||
EXT_FOLDER="cb"
|
||||
else
|
||||
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
|
||||
EXT_CLIENT=$(sed -n 's/.*<extension[^>]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
||||
fi
|
||||
TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1)
|
||||
PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
|
||||
|
||||
# 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 <client>site</client>
|
||||
CLIENT_TAG=""
|
||||
if [ -n "$EXT_CLIENT" ]; then
|
||||
CLIENT_TAG="<client>${EXT_CLIENT}</client>"
|
||||
elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then
|
||||
CLIENT_TAG="<client>site</client>"
|
||||
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="<folder>${EXT_FOLDER}</folder>"
|
||||
fi
|
||||
|
||||
# Build targetplatform (fallback to Joomla 5 if not in manifest)
|
||||
if [ -z "$TARGET_PLATFORM" ]; then
|
||||
TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" %s>' "/")
|
||||
fi
|
||||
|
||||
# Build php_minimum tag
|
||||
PHP_TAG=""
|
||||
if [ -n "$PHP_MINIMUM" ]; then
|
||||
PHP_TAG="<php_minimum>${PHP_MINIMUM}</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_" ;;
|
||||
cb_plugin) TYPE_PREFIX="cb_" ;;
|
||||
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' ' <update>'
|
||||
printf '%s\n' " <name>${EXT_NAME}</name>"
|
||||
printf '%s\n' " <description>${EXT_NAME} update</description>"
|
||||
printf '%s\n' " <element>${EXT_ELEMENT}</element>"
|
||||
printf '%s\n' " <type>${EXT_TYPE}</type>"
|
||||
printf '%s\n' " <version>${VERSION}</version>"
|
||||
[ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}"
|
||||
[ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}"
|
||||
printf '%s\n' " <tags><tag>${TAG_NAME}</tag></tags>"
|
||||
printf '%s\n' " <infourl title=\"${EXT_NAME}\">${INFO_URL}</infourl>"
|
||||
printf '%s\n' ' <downloads>'
|
||||
printf '%s\n' " <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>"
|
||||
printf '%s\n' ' </downloads>'
|
||||
printf '%s\n' " ${TARGET_PLATFORM}"
|
||||
[ -n "$PHP_TAG" ] && printf '%s\n' " ${PHP_TAG}"
|
||||
printf '%s\n' ' <maintainer>Moko Consulting</maintainer>'
|
||||
printf '%s\n' ' <maintainerurl>https://mokoconsulting.tech</maintainerurl>'
|
||||
printf '%s\n' ' </update>'
|
||||
}
|
||||
|
||||
# -- Write updates.xml with cascading channels
|
||||
# Stable release updates ALL channels (development, alpha, beta, rc, stable)
|
||||
{
|
||||
printf '%s\n' "<?xml version='1.0' encoding='UTF-8'?>"
|
||||
printf '%s\n' "<!-- Copyright (C) $(date +%Y) Moko Consulting <hello@mokoconsulting.tech>"
|
||||
printf '%s\n' " SPDX-License-Identifier: GPL-3.0-or-later"
|
||||
printf '%s\n' " VERSION: ${VERSION}"
|
||||
printf '%s\n' " -->"
|
||||
printf '%s\n' ""
|
||||
printf '%s\n' '<updates>'
|
||||
build_entry "development"
|
||||
build_entry "alpha"
|
||||
build_entry "beta"
|
||||
build_entry "rc"
|
||||
build_entry "stable"
|
||||
printf '%s\n' '</updates>'
|
||||
} > 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' &&
|
||||
@@ -592,7 +335,7 @@ jobs:
|
||||
fi
|
||||
[ -z "$EXT_NAME" ] && EXT_NAME="${GITEA_REPO}"
|
||||
|
||||
NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true)
|
||||
NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null)
|
||||
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
||||
|
||||
# Build release name: "Pretty Name VERSION (type_element-VERSION)"
|
||||
@@ -604,7 +347,6 @@ jobs:
|
||||
template) TYPE_PREFIX="tpl_" ;;
|
||||
library) TYPE_PREFIX="lib_" ;;
|
||||
package) TYPE_PREFIX="pkg_" ;;
|
||||
cb_plugin) TYPE_PREFIX="cb_" ;;
|
||||
esac
|
||||
RELEASE_NAME="${EXT_NAME} ${VERSION} (${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION})"
|
||||
|
||||
@@ -675,7 +417,6 @@ jobs:
|
||||
template) TYPE_PREFIX="tpl_" ;;
|
||||
library) TYPE_PREFIX="lib_" ;;
|
||||
package) TYPE_PREFIX="pkg_" ;;
|
||||
cb_plugin) TYPE_PREFIX="cb_" ;;
|
||||
esac
|
||||
ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
|
||||
TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
|
||||
@@ -683,19 +424,18 @@ jobs:
|
||||
# -- 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; }
|
||||
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/"; exit 0; }
|
||||
|
||||
EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
|
||||
# ZIP package (type-aware via moko-platform PHP API)
|
||||
php /tmp/moko-platform-api/cli/joomla_build.php --path . --version "${VERSION}" --output /tmp
|
||||
# Match the expected ZIP_NAME for upload
|
||||
BUILT_ZIP=$(ls /tmp/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip 2>/dev/null | head -1 || true)
|
||||
if [ -n "$BUILT_ZIP" ] && [ "$BUILT_ZIP" != "/tmp/${ZIP_NAME}" ]; then
|
||||
mv "$BUILT_ZIP" "/tmp/${ZIP_NAME}"
|
||||
fi
|
||||
|
||||
# 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*' .
|
||||
# tar.gz package (flat source archive)
|
||||
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" --exclude='.ftpignore' --exclude='sftp-config*' --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
|
||||
|
||||
ZIP_SIZE=$(stat -c%s "/tmp/${ZIP_NAME}" 2>/dev/null || stat -f%z "/tmp/${ZIP_NAME}" 2>/dev/null || echo "unknown")
|
||||
TAR_SIZE=$(stat -c%s "/tmp/${TAR_NAME}" 2>/dev/null || stat -f%z "/tmp/${TAR_NAME}" 2>/dev/null || echo "unknown")
|
||||
@@ -838,7 +578,6 @@ jobs:
|
||||
template) TYPE_PREFIX="tpl_" ;;
|
||||
library) TYPE_PREFIX="lib_" ;;
|
||||
package) TYPE_PREFIX="pkg_" ;;
|
||||
cb_plugin) TYPE_PREFIX="cb_" ;;
|
||||
esac
|
||||
ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
|
||||
TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
|
||||
@@ -886,105 +625,95 @@ jobs:
|
||||
" 2>/dev/null && echo "Release body updated with changelog + SHA" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
# -- (GitHub mirror removed — Gitea is the sole release platform) ----------
|
||||
# -- 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/moko-platform-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"
|
||||
continue-on-error: true
|
||||
run: |
|
||||
php /tmp/moko-platform-api/cli/release_cascade.php \
|
||||
--stability stable \
|
||||
--token "${{ secrets.GA_TOKEN }}" \
|
||||
--org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
||||
--gitea-url "${GITEA_URL}" 2>/dev/null || true
|
||||
|
||||
- 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 }}"
|
||||
|
||||
# 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 and bump to next minor -------------------------
|
||||
- name: "Step 11: Reset dev and bump to next minor"
|
||||
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 }}"
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||
|
||||
# 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
|
||||
# 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"
|
||||
|
||||
# Calculate next minor version for dev
|
||||
MAJOR=$((10#$(echo "$VERSION" | cut -d. -f1)))
|
||||
MINOR=$((10#$(echo "$VERSION" | cut -d. -f2)))
|
||||
MINOR=$((MINOR + 1))
|
||||
if [ $MINOR -gt 99 ]; then
|
||||
MINOR=0
|
||||
MAJOR=$((MAJOR + 1))
|
||||
fi
|
||||
NEXT=$(printf "%02d.%02d.00" $MAJOR $MINOR)
|
||||
|
||||
# Bump version on dev via API (README + manifest)
|
||||
# Update README.md on dev
|
||||
README_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/README.md?ref=dev" 2>/dev/null || true)
|
||||
README_SHA=$(echo "$README_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
|
||||
README_CONTENT=$(echo "$README_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true)
|
||||
if [ -n "$README_SHA" ] && [ -n "$README_CONTENT" ]; then
|
||||
UPDATED=$(echo "$README_CONTENT" | sed "s/${VERSION}/${NEXT}/g")
|
||||
ENCODED=$(echo "$UPDATED" | base64 -w0)
|
||||
curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" \
|
||||
"${API_BASE}/contents/README.md" \
|
||||
-d "$(python3 -c "import json; print(json.dumps({'content':'${ENCODED}','sha':'${README_SHA}','message':'chore(version): bump ${VERSION} → ${NEXT} (dev) [skip ci]','branch':'dev'}))")" > /dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Update manifest on dev (Joomla or Dolibarr)
|
||||
case "$PLATFORM" in
|
||||
joomla)
|
||||
MANIFEST_PATH="${{ steps.platform.outputs.manifest }}"
|
||||
[ -n "$MANIFEST_PATH" ] && MANIFEST_PATH=$(echo "$MANIFEST_PATH" | sed 's|^\./||')
|
||||
if [ -n "$MANIFEST_PATH" ]; then
|
||||
ENCODED_PATH=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${MANIFEST_PATH}'))")
|
||||
MF_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/${ENCODED_PATH}?ref=dev" 2>/dev/null || true)
|
||||
MF_SHA=$(echo "$MF_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
|
||||
MF_CONTENT=$(echo "$MF_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true)
|
||||
if [ -n "$MF_SHA" ] && [ -n "$MF_CONTENT" ]; then
|
||||
UPDATED=$(echo "$MF_CONTENT" | sed "s|<version>${VERSION}</version>|<version>${NEXT}</version>|")
|
||||
ENCODED=$(echo "$UPDATED" | base64 -w0)
|
||||
curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" \
|
||||
"${API_BASE}/contents/${ENCODED_PATH}" \
|
||||
-d "$(python3 -c "import json; print(json.dumps({'content':'${ENCODED}','sha':'${MF_SHA}','message':'chore(version): bump ${VERSION} → ${NEXT} (dev) [skip ci]','branch':'dev'}))")" > /dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
dolibarr)
|
||||
# Dolibarr handled by separate step below
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Dev branch bumped to ${NEXT}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
|
||||
# -- Dolibarr post-release: Reset dev version -----------------------------
|
||||
@@ -4,8 +4,8 @@
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Maintenance
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||
# INGROUP: moko-platform.Maintenance
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||
# PATH: /templates/workflows/cascade-dev.yml.template
|
||||
# VERSION: 02.00.00
|
||||
# BRIEF: Forward-merge main → all open branches after every push to main
|
||||
@@ -4,8 +4,8 @@
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Maintenance
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||
# 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
|
||||
@@ -4,8 +4,8 @@
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Security
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||
# 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
|
||||
@@ -4,8 +4,8 @@
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Notifications
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||
# 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
|
||||
@@ -18,7 +18,6 @@ on:
|
||||
- "Joomla Build & Release"
|
||||
- "Joomla Extension CI"
|
||||
- "Deploy"
|
||||
- "Cascade Main → Dev"
|
||||
types:
|
||||
- completed
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.CI
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||
# 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
|
||||
@@ -108,8 +108,9 @@ jobs:
|
||||
- name: Detect platform
|
||||
id: platform
|
||||
run: |
|
||||
# Parse manifest for platform detection
|
||||
PLATFORM=$(php /tmp/mokostandards-api/cli/manifest_read.php --path . --field platform 2>/dev/null)
|
||||
# Read platform from XML manifest (<platform> tag) or plain text fallback
|
||||
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
|
||||
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
|
||||
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -193,32 +194,3 @@ jobs:
|
||||
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; }
|
||||
|
||||
# ── Changelog Gate ────────────────────────────────────────────────────
|
||||
changelog:
|
||||
name: Changelog Updated
|
||||
runs-on: ubuntu-latest
|
||||
if: github.base_ref == 'main'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check CHANGELOG.md was updated
|
||||
run: |
|
||||
BASE="${{ github.event.pull_request.base.sha }}"
|
||||
HEAD="${{ github.event.pull_request.head.sha }}"
|
||||
|
||||
if git diff --name-only "$BASE" "$HEAD" | grep -q "^CHANGELOG.md$"; then
|
||||
echo "CHANGELOG.md updated"
|
||||
else
|
||||
# Allow [skip changelog] in PR title or body
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
PR_BODY="${{ github.event.pull_request.body }}"
|
||||
if echo "$PR_TITLE $PR_BODY" | grep -qi "\[skip changelog\]"; then
|
||||
echo "::warning::Changelog skip requested via [skip changelog]"
|
||||
exit 0
|
||||
fi
|
||||
echo "::error::CHANGELOG.md must be updated before merging to main. Add [skip changelog] to the PR title to bypass."
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,246 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||
# VERSION: 05.00.00
|
||||
# BRIEF: Manual pre-release — builds dev/alpha/beta/rc packages from any branch
|
||||
|
||||
name: "Universal: 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 php-curl >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
run: |
|
||||
git clone --depth 1 --branch main --quiet "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" /tmp/moko-platform-api
|
||||
|
||||
- name: Detect platform
|
||||
id: platform
|
||||
run: |
|
||||
php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output
|
||||
|
||||
- name: Resolve metadata
|
||||
id: meta
|
||||
run: |
|
||||
STABILITY="${{ inputs.stability }}"
|
||||
MOKO_API="/tmp/moko-platform-api/cli"
|
||||
|
||||
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
|
||||
|
||||
# Bump patch version
|
||||
BUMP_OUTPUT=$(php ${MOKO_API}/version_bump.php --path .)
|
||||
VERSION=$(echo "$BUMP_OUTPUT" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true)
|
||||
[ -z "$VERSION" ] && VERSION=$(php ${MOKO_API}/version_read.php --path .)
|
||||
echo "Version: ${VERSION}"
|
||||
|
||||
# Update platform-specific manifest
|
||||
php ${MOKO_API}/version_set_platform.php --path . --version "${VERSION}"
|
||||
|
||||
# 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 to ${VERSION} [skip ci]"
|
||||
git push origin HEAD 2>&1
|
||||
}
|
||||
|
||||
# Detect element from Joomla/Dolibarr manifest
|
||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||
EXT_ELEMENT=$(php ${MOKO_API}/manifest_read.php --path . --field name 2>/dev/null | tr -d ' ' | tr '[:upper:]' '[:lower:]' || true)
|
||||
# For Joomla, prefer <element> tag
|
||||
if [ "$PLATFORM" = "joomla" ]; then
|
||||
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
|
||||
if [ -n "$MANIFEST" ]; then
|
||||
ELEM=$(grep -oP "<element>\K[^<]+" "$MANIFEST" 2>/dev/null | head -1)
|
||||
[ -n "$ELEM" ] && EXT_ELEMENT="$ELEM"
|
||||
fi
|
||||
fi
|
||||
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||
|
||||
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
|
||||
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
||||
|
||||
- name: Build package
|
||||
id: zip
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
SUFFIX="${{ steps.meta.outputs.suffix }}"
|
||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||
|
||||
if [ "$PLATFORM" = "joomla" ]; then
|
||||
php /tmp/moko-platform-api/cli/joomla_build.php --path . --version "${VERSION}" --suffix "${SUFFIX}" --output build --github-output
|
||||
else
|
||||
# Generic build: zip src/ directory
|
||||
SOURCE_DIR="src"
|
||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||
[ ! -d "$SOURCE_DIR" ] && { echo "::error::No src/ or htdocs/"; exit 1; }
|
||||
EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}"
|
||||
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
|
||||
mkdir -p build
|
||||
cd "$SOURCE_DIR" && zip -r "../build/${ZIP_NAME}" . && cd ..
|
||||
SHA256=$(sha256sum "build/${ZIP_NAME}" | cut -d' ' -f1)
|
||||
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||
echo "zip_path=build/${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||
echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Create or replace Gitea release
|
||||
id: release
|
||||
continue-on-error: true
|
||||
run: |
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||
SHA256="${{ steps.zip.outputs.sha256 }}"
|
||||
ZIP_NAME="${{ steps.zip.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 "@${{ steps.zip.outputs.zip_path }}"
|
||||
|
||||
echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})"
|
||||
|
||||
- name: "Update updates.xml"
|
||||
if: steps.platform.outputs.platform == 'joomla'
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||
SHA256="${{ steps.zip.outputs.sha256 }}"
|
||||
php /tmp/moko-platform-api/cli/updates_xml_build.php --path . --version "$VERSION" --stability "$STABILITY" --sha "$SHA256" --gitea-url "$GITEA_URL" --org "$GITEA_ORG" --repo "$GITEA_REPO"
|
||||
if ! 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: |
|
||||
php /tmp/moko-platform-api/cli/updates_xml_sync.php --path . --current "${{ github.ref_name }}" --branches main,dev --version "${{ steps.meta.outputs.version }}" --token "${{ secrets.GA_TOKEN }}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" --gitea-url "${GITEA_URL}"
|
||||
|
||||
- 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
|
||||
@@ -7,18 +7,14 @@
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Validation
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||
# 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: "Joomla: Repo Health"
|
||||
|
||||
concurrency:
|
||||
group: repo-health-${{ github.repository }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
name: "Generic: Repo Health"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -288,7 +284,7 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"
|
||||
if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi
|
||||
IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
|
||||
|
||||
missing_dirs=()
|
||||
@@ -392,23 +388,27 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Source directory: src/ or htdocs/ (either is valid)
|
||||
IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
|
||||
IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
|
||||
if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi
|
||||
IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}"
|
||||
|
||||
missing_required=()
|
||||
missing_optional=()
|
||||
|
||||
# Source directory: src/ or htdocs/ (either is valid for extension repos)
|
||||
SOURCE_DIR=""
|
||||
if [ -d "src" ]; then
|
||||
SOURCE_DIR="src"
|
||||
elif [ -d "htdocs" ]; then
|
||||
SOURCE_DIR="htdocs"
|
||||
elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
|
||||
# Platform/tooling repos don't need src/
|
||||
SOURCE_DIR=""
|
||||
else
|
||||
missing_required+=("src/ or htdocs/ (source directory required)")
|
||||
fi
|
||||
|
||||
IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
|
||||
IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
|
||||
IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"
|
||||
IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}"
|
||||
|
||||
missing_required=()
|
||||
missing_optional=()
|
||||
|
||||
for item in "${required_artifacts[@]}"; do
|
||||
if printf '%s' "${item}" | grep -q '/$'; then
|
||||
d="${item%/}"
|
||||
@@ -450,12 +450,8 @@ jobs:
|
||||
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/<version>)")
|
||||
if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then
|
||||
missing_required+=("dev or dev/* branch")
|
||||
fi
|
||||
|
||||
content_warnings=()
|
||||
@@ -481,26 +477,7 @@ jobs:
|
||||
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
|
||||
)"
|
||||
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'
|
||||
@@ -578,12 +555,14 @@ jobs:
|
||||
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 [ -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
|
||||
{
|
||||
@@ -629,43 +608,29 @@ jobs:
|
||||
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
|
||||
)"
|
||||
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:'
|
||||
while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}"
|
||||
for bl in ${missing_links}; do
|
||||
printf '%s\n' "- ${bl}"
|
||||
done
|
||||
printf '\n'
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
@@ -764,3 +729,41 @@ jobs:
|
||||
fi
|
||||
|
||||
printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
|
||||
site-health:
|
||||
name: Site Health
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
|
||||
- name: Uptime check
|
||||
if: env.URLS != ''
|
||||
run: |
|
||||
echo "$URLS" > /tmp/urls.txt
|
||||
php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down"
|
||||
rm -f /tmp/urls.txt
|
||||
env:
|
||||
URLS: ${{ vars.MONITORED_URLS }}
|
||||
|
||||
- name: SSL certificate check
|
||||
if: env.DOMAINS != ''
|
||||
run: |
|
||||
echo "$DOMAINS" > /tmp/domains.txt
|
||||
php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon"
|
||||
rm -f /tmp/domains.txt
|
||||
env:
|
||||
DOMAINS: ${{ vars.MONITORED_DOMAINS }}
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "### Site Health" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Security
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||
# 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
|
||||
@@ -80,3 +80,19 @@ 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 }}
|
||||
|
||||
+65
-113
@@ -1,10 +1,4 @@
|
||||
<!--
|
||||
## [Unreleased]
|
||||
|
||||
## [04.01.00] --- 2026-05-16
|
||||
|
||||
## [04.00.00] --- 2026-05-16
|
||||
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
|
||||
This file is part of a Moko Consulting project.
|
||||
@@ -27,8 +21,8 @@
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: MokoJoomTOS
|
||||
INGROUP: plg_system_mokojoomtos
|
||||
REPO: https://github.com/mokoconsulting-tech/MokoJoomTOS
|
||||
VERSION: 04.00.00
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS
|
||||
VERSION: 04.02.01
|
||||
PATH: ./CHANGELOG.md
|
||||
BRIEF: Version history and release notes
|
||||
-->
|
||||
@@ -40,7 +34,52 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [04.00.00] --- 2026-05-16
|
||||
## [Unreleased]
|
||||
|
||||
## [04.02.01] - 2026-05-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- enablePlugin() now called unconditionally in postflight() (#89)
|
||||
- Replaced Table::addIncludePath() with bootComponent()->getMVCFactory() for Joomla 5 (#90)
|
||||
- Added non-SEF URL fallback via Itemid matching (#91)
|
||||
- enablePlugin() now fires on upgrade path (#92)
|
||||
- Removed $_GET superglobal mutation (#93)
|
||||
- Added params, metadata, attribs defaults to article creation (#94)
|
||||
- Applied urldecode() to URI path before slug comparison (#95)
|
||||
- Cast Registry return to array before iterating slugs (#96)
|
||||
- Fixed MenuslugField separator `disable` to `disabled` property (#99)
|
||||
- Hardcode description in XML manifest (language variables don't resolve during install)
|
||||
- Synced VERSION header in manifest to 04.02.01 (#105)
|
||||
- Added SEF_WARNING language key to site-side .ini files (#106)
|
||||
- Fixed updates.xml version mismatch and Joomla 4.x targetplatform (#107)
|
||||
- Changed catch(Exception) to catch(Throwable) in script.php and Extension class (#108, #113)
|
||||
- Fixed dev channel targetplatform to include Joomla 4.x (#111)
|
||||
- Fixed misleading article duplicate check comment (#98)
|
||||
|
||||
### Added
|
||||
|
||||
- SEF disabled warning in MenuslugField dropdown (#97)
|
||||
- Include Children toggle for offline-accessible menu items (defaults to Yes)
|
||||
- Auto-select default menu slugs (terms-of-service, privacy-policy) on fresh install
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed deploy-manual.yml workflow — switching to Joomla update server method for extension distribution
|
||||
|
||||
### Changed
|
||||
|
||||
- Stripped legacy mokojoomtos.php to minimal stub (#101)
|
||||
- Converted script.php indentation from spaces to tabs (#102)
|
||||
- Renamed installer class to PlgSystemMokojoomtosInstallerScript (#103)
|
||||
- Promoted CHANGELOG [Unreleased] to versioned section (#112)
|
||||
|
||||
## [04.01.00] - 2026-05-16
|
||||
|
||||
### Fixed
|
||||
|
||||
- Use literal display name in manifest `<name>` tag so Joomla stores "System - Moko Terms of Service" directly in the DB
|
||||
- Updated plugin help text to describe multi-select workflow
|
||||
|
||||
## [04.00.00] - 2026-05-16
|
||||
|
||||
@@ -54,7 +93,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Match against full menu route path instead of alias only (fixes nested routes like `/legal/terms-of-service`)
|
||||
- Plugin pretty name updated to Joomla convention: "System - Moko Terms of Service"
|
||||
- Renamed `.mokogitea/` to `.gitea/` for standard Gitea compatibility
|
||||
- MenuslugField now stores and displays full route paths (e.g., `legal/terms-of-service`)
|
||||
|
||||
### Removed
|
||||
@@ -76,119 +114,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Changed
|
||||
|
||||
- Added Gitea update server URL to plugin manifest (`updates.xml` on `main`)
|
||||
- Removed obsolete `src/plugins/` legacy directory
|
||||
- Fixed template chrome loading issue by changing event hook from onAfterInitialise to onAfterRoute
|
||||
- Component-only view now properly applied when site is offline
|
||||
|
||||
### Removed
|
||||
|
||||
- Legacy duplicate manifest at `src/plugins/system/mokojoomtos/mokojoomtos.xml`
|
||||
|
||||
## [03.09.00] - 2026-02-28
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated version number to 03.09.00 across all files
|
||||
- Fixed template chrome loading issue by changing event hook from onAfterInitialise to onAfterRoute
|
||||
|
||||
### Fixed
|
||||
|
||||
- Template chrome (header, footer, modules) no longer loads when accessing TOS page in offline mode
|
||||
- Component-only view now properly applied when site is offline
|
||||
|
||||
## [1.0.0] - 2026-01-16
|
||||
|
||||
### Added
|
||||
|
||||
- Initial template repository structure
|
||||
- Comprehensive Makefile with 30+ build targets
|
||||
- Complete README.md with usage documentation
|
||||
- CONTRIBUTING.md with contribution guidelines
|
||||
- SECURITY.md with security policy and best practices
|
||||
- CHANGELOG.md for version tracking
|
||||
- LICENSE file (GPL-3.0-or-later)
|
||||
- EditorConfig for consistent code formatting
|
||||
- Documentation structure in `docs/` directory
|
||||
- Placeholders for component directories (`admin/`, `site/`, `media/`)
|
||||
- MokoStandards compliance:
|
||||
- File header standards with copyright and SPDX identifiers
|
||||
- Joomla coding standards configuration
|
||||
- PHPStan static analysis setup
|
||||
- Dependency security auditing
|
||||
- Makefile targets for:
|
||||
- Dependency management (install-deps, update-deps)
|
||||
- Code validation (lint, phpcs, phpstan)
|
||||
- Testing (test, test-coverage)
|
||||
- Building (build, build-assets)
|
||||
- Development (dev-install, watch-assets)
|
||||
- Release management (release, bump-version)
|
||||
- Utility commands (clean, version, help)
|
||||
- Documentation index files with MokoStandards metadata
|
||||
|
||||
### Documentation
|
||||
|
||||
- Comprehensive README with:
|
||||
- Quick start guide
|
||||
- Prerequisites and installation
|
||||
- Usage examples
|
||||
- Project structure overview
|
||||
- Makefile command reference
|
||||
- Standards compliance information
|
||||
- Detailed CONTRIBUTING guide with:
|
||||
- Commit message conventions
|
||||
- DCO sign-off requirements
|
||||
- Development setup instructions
|
||||
- Code review process
|
||||
- Security policy with:
|
||||
- Vulnerability reporting procedures
|
||||
- Security best practices
|
||||
- Code examples for secure development
|
||||
- Template structure for Joomla 4.x and 5.x compatibility
|
||||
|
||||
## Version Guidelines
|
||||
|
||||
### Version Numbering
|
||||
|
||||
This template follows [Semantic Versioning](https://semver.org/):
|
||||
|
||||
- **MAJOR** version for incompatible changes (e.g., 2.0.0)
|
||||
- **MINOR** version for new features (e.g., 1.1.0)
|
||||
- **PATCH** version for bug fixes (e.g., 1.0.1)
|
||||
|
||||
### Change Categories
|
||||
|
||||
- **Added**: New features
|
||||
- **Changed**: Changes to existing functionality
|
||||
- **Deprecated**: Soon-to-be removed features
|
||||
- **Removed**: Removed features
|
||||
- **Fixed**: Bug fixes
|
||||
- **Security**: Security fixes
|
||||
|
||||
## Release Notes
|
||||
|
||||
### [1.0.0] Release Notes
|
||||
|
||||
This is the initial release of the MokoJoomTOS plugin. It provides a production-ready Joomla system plugin for offline TOS access with:
|
||||
|
||||
- **Complete build system** via comprehensive Makefile
|
||||
- **MokoStandards compliance** out of the box
|
||||
- **Developer-friendly** workflow with automation
|
||||
- **Security-focused** with built-in best practices
|
||||
- **Well-documented** with clear usage instructions
|
||||
|
||||
This template is ready for use in creating new Joomla components that follow organizational coding standards and best practices.
|
||||
|
||||
### Migration Guide
|
||||
|
||||
**For New Projects**: Simply use this template to create your new component repository.
|
||||
|
||||
**For Existing Projects**: Review the Makefile, documentation structure, and standards compliance files to gradually adopt features that benefit your project.
|
||||
- Initial plugin release
|
||||
- Offline mode bypass for configured menu slug
|
||||
- Auto-provisioning installer (creates article, menu type, menu item)
|
||||
- Component-only rendering (`tmpl=component`)
|
||||
- Language files for en-GB and en-US
|
||||
- MokoStandards compliance
|
||||
|
||||
## Links
|
||||
|
||||
- [Repository](https://github.com/mokoconsulting-tech/MokoJoomTOS)
|
||||
- [Issues](https://github.com/mokoconsulting-tech/MokoJoomTOS/issues)
|
||||
- [Pull Requests](https://github.com/mokoconsulting-tech/MokoJoomTOS/pulls)
|
||||
- [MokoStandards](https://github.com/mokoconsulting-tech/MokoCodingDefaults)
|
||||
- [Repository](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS)
|
||||
- [Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/issues)
|
||||
- [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases)
|
||||
|
||||
[Unreleased]: https://github.com/mokoconsulting-tech/MokoJoomTOS/compare/v03.09.00...HEAD
|
||||
[03.09.00]: https://github.com/mokoconsulting-tech/MokoJoomTOS/releases/tag/v03.09.00
|
||||
[1.0.0]: https://github.com/mokoconsulting-tech/MokoJoomTOS/releases/tag/v1.0.0
|
||||
[Unreleased]: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/compare/stable...dev
|
||||
[04.02.01]: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable
|
||||
[04.01.00]: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/compare/v04.01.00...stable
|
||||
[04.00.00]: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/compare/v03.09.00...stable
|
||||
[03.09.00]: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/v03.09.00
|
||||
[1.0.0]: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/v1.0.0
|
||||
|
||||
@@ -6,7 +6,7 @@ MokoJoomTOS is a lightweight Joomla 4.x/5.x system plugin that allows Terms of S
|
||||
|
||||
```
|
||||
/
|
||||
├── .github/ # GitHub workflows, issue templates, copilot-instructions.md
|
||||
├── .mokogitea/ # GitHub workflows, issue templates, copilot-instructions.md
|
||||
├── docs/ # Detailed documentation (currently minimal with index.md)
|
||||
├── scripts/ # Build and utility scripts (validate/, package scripts)
|
||||
├── src/ # Plugin source code at root level (NOT nested under plugins/)
|
||||
@@ -469,6 +469,6 @@ This repository follows minimal documentation structure with essential docs in r
|
||||
2. **SECURITY.md** - Security policy and vulnerability reporting procedures
|
||||
3. **CODE_OF_CONDUCT.md** - Community standards and behavior expectations
|
||||
4. **CHANGELOG.md** - Version history following Keep a Changelog format
|
||||
5. **.github/copilot-instructions.md** - Comprehensive guidance for GitHub Copilot (includes all Joomla patterns)
|
||||
5. **.mokogitea/copilot-instructions.md** - Comprehensive guidance for GitHub Copilot (includes all Joomla patterns)
|
||||
|
||||
Currently no `docs/policy/` directory exists - all policy is in root-level markdown files.
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
A Joomla system plugin that keeps your Terms of Service, Privacy Policy, or any legal page accessible to visitors -- even when the site is in offline (maintenance) mode.
|
||||
|
||||
    
|
||||
    
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Author** | [Moko Consulting](https://mokoconsulting.tech) |
|
||||
| **License** | GPL-3.0-or-later |
|
||||
| **Platform** | [Gitea](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS) |
|
||||
| **Version** | 04.01.00 |
|
||||
| **Version** | 04.02.00 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -11,10 +11,16 @@ PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings"
|
||||
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Offline-Accessible Menu Items"
|
||||
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Select one or more menu items that should remain accessible when the site is in offline mode. Hold Ctrl/Cmd to select multiple items."
|
||||
|
||||
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL="Include Child Menu Items"
|
||||
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC="When enabled, child menu items under the selected items will also be accessible during offline mode. For example, selecting 'legal' will also allow access to 'legal/terms-of-service' and 'legal/privacy-policy'."
|
||||
|
||||
; Help
|
||||
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
|
||||
PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="<strong>Step 1:</strong> Create articles for your legal pages (Terms of Service, Privacy Policy, etc.).<br/><strong>Step 2:</strong> Create menu items pointing to those articles.<br/><strong>Step 3:</strong> Select the menu items above (hold Ctrl/Cmd to select multiple).<br/><strong>Step 4:</strong> When your site goes offline, visitors can still access the selected pages.<br/><br/><em>Tip:</em> The dropdown shows the full URL path for each menu item (e.g., /legal/terms-of-service)."
|
||||
|
||||
; Warnings
|
||||
PLG_SYSTEM_MOKOJOOMTOS_FIELD_SEF_WARNING="⚠ SEF URLs are disabled — path matching requires SEF. Itemid fallback is active."
|
||||
|
||||
; Errors
|
||||
PLG_SYSTEM_MOKOJOOMTOS_ERROR_LOADING_MENU_ITEMS="Error loading menu items: %s"
|
||||
|
||||
|
||||
@@ -11,10 +11,16 @@ PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings"
|
||||
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Offline-Accessible Menu Items"
|
||||
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Select one or more menu items that should remain accessible when the site is in offline mode. Hold Ctrl/Cmd to select multiple items."
|
||||
|
||||
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL="Include Child Menu Items"
|
||||
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC="When enabled, child menu items under the selected items will also be accessible during offline mode. For example, selecting 'legal' will also allow access to 'legal/terms-of-service' and 'legal/privacy-policy'."
|
||||
|
||||
; Help
|
||||
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
|
||||
PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="<strong>Step 1:</strong> Create articles for your legal pages (Terms of Service, Privacy Policy, etc.).<br/><strong>Step 2:</strong> Create menu items pointing to those articles.<br/><strong>Step 3:</strong> Select the menu items above (hold Ctrl/Cmd to select multiple).<br/><strong>Step 4:</strong> When your site goes offline, visitors can still access the selected pages.<br/><br/><em>Tip:</em> The dropdown shows the full URL path for each menu item (e.g., /legal/terms-of-service)."
|
||||
|
||||
; Warnings
|
||||
PLG_SYSTEM_MOKOJOOMTOS_FIELD_SEF_WARNING="⚠ SEF URLs are disabled — path matching requires SEF. Itemid fallback is active."
|
||||
|
||||
; Errors
|
||||
PLG_SYSTEM_MOKOJOOMTOS_ERROR_LOADING_MENU_ITEMS="Error loading menu items: %s"
|
||||
|
||||
|
||||
@@ -11,6 +11,12 @@ PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings"
|
||||
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Offline-Accessible Menu Items"
|
||||
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Select one or more menu items that should remain accessible when the site is in offline mode. Hold Ctrl/Cmd to select multiple items."
|
||||
|
||||
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL="Include Child Menu Items"
|
||||
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC="When enabled, child menu items under the selected items will also be accessible during offline mode. For example, selecting 'legal' will also allow access to 'legal/terms-of-service' and 'legal/privacy-policy'."
|
||||
|
||||
; Warnings
|
||||
PLG_SYSTEM_MOKOJOOMTOS_FIELD_SEF_WARNING="⚠ SEF URLs are disabled — path matching requires SEF. Itemid fallback is active."
|
||||
|
||||
; Help
|
||||
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
|
||||
PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="<strong>Step 1:</strong> Create articles for your legal pages (Terms of Service, Privacy Policy, etc.).<br/><strong>Step 2:</strong> Create menu items pointing to those articles.<br/><strong>Step 3:</strong> Select the menu items above (hold Ctrl/Cmd to select multiple).<br/><strong>Step 4:</strong> When your site goes offline, visitors can still access the selected pages.<br/><br/><em>Tip:</em> The dropdown shows the full URL path for each menu item (e.g., /legal/terms-of-service)."
|
||||
|
||||
@@ -11,6 +11,12 @@ PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings"
|
||||
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Offline-Accessible Menu Items"
|
||||
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Select one or more menu items that should remain accessible when the site is in offline mode. Hold Ctrl/Cmd to select multiple items."
|
||||
|
||||
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL="Include Child Menu Items"
|
||||
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC="When enabled, child menu items under the selected items will also be accessible during offline mode. For example, selecting 'legal' will also allow access to 'legal/terms-of-service' and 'legal/privacy-policy'."
|
||||
|
||||
; Warnings
|
||||
PLG_SYSTEM_MOKOJOOMTOS_FIELD_SEF_WARNING="⚠ SEF URLs are disabled — path matching requires SEF. Itemid fallback is active."
|
||||
|
||||
; Help
|
||||
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
|
||||
PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="<strong>Step 1:</strong> Create articles for your legal pages (Terms of Service, Privacy Policy, etc.).<br/><strong>Step 2:</strong> Create menu items pointing to those articles.<br/><strong>Step 3:</strong> Select the menu items above (hold Ctrl/Cmd to select multiple).<br/><strong>Step 4:</strong> When your site goes offline, visitors can still access the selected pages.<br/><br/><em>Tip:</em> The dropdown shows the full URL path for each menu item (e.g., /legal/terms-of-service)."
|
||||
|
||||
+4
-94
@@ -9,106 +9,16 @@
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Plugin\CMSPlugin;
|
||||
use Joomla\CMS\Uri\Uri;
|
||||
|
||||
/**
|
||||
* MokoJoomTOS Offline Mode Bypass Plugin (Legacy)
|
||||
* MokoJoomTOS Legacy Entry Point
|
||||
*
|
||||
* Allows configured menu items to remain accessible when the site
|
||||
* is in offline mode.
|
||||
* This file is required by Joomla's plugin loader (<filename plugin="mokojoomtos">)
|
||||
* but is NOT executed under Joomla 5's DI container. The actual plugin logic lives
|
||||
* in src/Extension/MokoJoomTOS.php, bootstrapped via services/provider.php.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class PlgSystemMokojoomtos extends CMSPlugin
|
||||
{
|
||||
/**
|
||||
* Load the language file on instantiation.
|
||||
*
|
||||
* @var boolean
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected $autoloadLanguage = true;
|
||||
|
||||
/**
|
||||
* Application object
|
||||
*
|
||||
* @var \Joomla\CMS\Application\CMSApplication
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* After route event handler
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 04.00.00
|
||||
*/
|
||||
public function onAfterRoute()
|
||||
{
|
||||
// Only process for site application
|
||||
if (!$this->app->isClient('site'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the global configuration
|
||||
$config = $this->app->getConfig();
|
||||
|
||||
// Only proceed if site is offline
|
||||
if (!$config->get('offline'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the configured slugs (stored as array for multi-select)
|
||||
$slugs = $this->params->get('tos_slug', []);
|
||||
|
||||
// Handle legacy single-value string format
|
||||
if (is_string($slugs))
|
||||
{
|
||||
$slugs = array_filter([trim($slugs)]);
|
||||
}
|
||||
|
||||
if (empty($slugs))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current URI path
|
||||
$uri = Uri::getInstance();
|
||||
$path = trim($uri->getPath(), '/');
|
||||
|
||||
// Remove the base path if present
|
||||
$base = trim(Uri::base(true), '/');
|
||||
if (!empty($base) && strpos($path, $base) === 0)
|
||||
{
|
||||
$path = trim(substr($path, strlen($base)), '/');
|
||||
}
|
||||
|
||||
// Check if the path matches any configured slug
|
||||
foreach ($slugs as $slug)
|
||||
{
|
||||
$slug = trim($slug);
|
||||
if (empty($slug))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($path === $slug || strpos($path, $slug . '/') === 0)
|
||||
{
|
||||
// Temporarily disable offline mode for this request
|
||||
$config->set('offline', 0);
|
||||
|
||||
// Set component-only view (no template chrome)
|
||||
$input = $this->app->input;
|
||||
$input->set('tmpl', 'component');
|
||||
|
||||
// Also set in GET superglobal to ensure recognition
|
||||
$_GET['tmpl'] = 'component';
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-3
@@ -25,7 +25,7 @@
|
||||
DEFGROUP: MokoJoomTOS
|
||||
INGROUP: plg_system_mokojoomtos
|
||||
PATH: src/mokojoomtos.xml
|
||||
VERSION: 04.00.00
|
||||
VERSION: 04.02.01
|
||||
BRIEF: Plugin manifest XML file for MokoJoomTOS system plugin
|
||||
=========================================================================
|
||||
-->
|
||||
@@ -37,8 +37,8 @@
|
||||
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>00.00.01</version>
|
||||
<description>PLG_SYSTEM_MOKOJOOMTOS_XML_DESCRIPTION</description>
|
||||
<version>04.03.00</version>
|
||||
<description>Allows Terms of Service to be accessible via menu slug when site is offline</description>
|
||||
|
||||
<namespace path="src">Joomla\Plugin\System\MokoJoomTOS</namespace>
|
||||
|
||||
@@ -71,6 +71,18 @@
|
||||
description="PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC"
|
||||
multiple="true"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="include_children"
|
||||
type="radio"
|
||||
label="PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL"
|
||||
description="PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC"
|
||||
default="1"
|
||||
class="btn-group btn-group-yesno"
|
||||
>
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
|
||||
<field
|
||||
name="help_spacer"
|
||||
|
||||
+440
-364
@@ -13,394 +13,470 @@ use Joomla\CMS\Installer\InstallerAdapter;
|
||||
use Joomla\CMS\Installer\InstallerScript;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Log\Log;
|
||||
use Joomla\CMS\Table\Table;
|
||||
|
||||
/**
|
||||
* Installation script for MokoJoomTOS Offline Plugin
|
||||
* Installation script for MokoJoomTOS Plugin
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class PlgSystemMokojoomtosOfflineInstallerScript extends InstallerScript
|
||||
class PlgSystemMokojoomtosInstallerScript extends InstallerScript
|
||||
{
|
||||
/**
|
||||
* Minimum Joomla version required to install the plugin
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected $minimumJoomla = '4.0.0';
|
||||
/**
|
||||
* Minimum Joomla version required to install the plugin
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected $minimumJoomla = '4.0.0';
|
||||
|
||||
/**
|
||||
* Minimum PHP version required to install the plugin
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected $minimumPhp = '7.4.0';
|
||||
/**
|
||||
* Minimum PHP version required to install the plugin
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected $minimumPhp = '7.4.0';
|
||||
|
||||
/**
|
||||
* Extension type (used by parent class)
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected $extension = 'plg_system_mokojoomtos';
|
||||
/**
|
||||
* Extension type (used by parent class)
|
||||
*
|
||||
* @var string
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected $extension = 'plg_system_mokojoomtos';
|
||||
|
||||
/**
|
||||
* Function called before plugin installation/update/uninstall
|
||||
*
|
||||
* @param string $type Installation type (install, update, discover_install)
|
||||
* @param InstallerAdapter $parent Parent installer adapter
|
||||
*
|
||||
* @return boolean True on success
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function preflight($type, $parent)
|
||||
{
|
||||
// Check minimum requirements
|
||||
if (!parent::preflight($type, $parent)) {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Function called before plugin installation/update/uninstall
|
||||
*
|
||||
* @param string $type Installation type (install, update, discover_install)
|
||||
* @param InstallerAdapter $parent Parent installer adapter
|
||||
*
|
||||
* @return boolean True on success
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function preflight($type, $parent)
|
||||
{
|
||||
if (!parent::preflight($type, $parent)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called after plugin installation
|
||||
*
|
||||
* @param InstallerAdapter $parent Parent installer adapter
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function install($parent)
|
||||
{
|
||||
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_INSTALL_SUCCESS') . '</p>';
|
||||
}
|
||||
/**
|
||||
* Function called after plugin installation
|
||||
*
|
||||
* @param InstallerAdapter $parent Parent installer adapter
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function install($parent)
|
||||
{
|
||||
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_INSTALL_SUCCESS') . '</p>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called after plugin update
|
||||
*
|
||||
* @param InstallerAdapter $parent Parent installer adapter
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function update($parent)
|
||||
{
|
||||
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UPDATE_SUCCESS') . '</p>';
|
||||
}
|
||||
/**
|
||||
* Function called after plugin update
|
||||
*
|
||||
* @param InstallerAdapter $parent Parent installer adapter
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function update($parent)
|
||||
{
|
||||
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UPDATE_SUCCESS') . '</p>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called after plugin uninstallation
|
||||
*
|
||||
* @param InstallerAdapter $parent Parent installer adapter
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function uninstall($parent)
|
||||
{
|
||||
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UNINSTALL_SUCCESS') . '</p>';
|
||||
}
|
||||
/**
|
||||
* Function called after plugin uninstallation
|
||||
*
|
||||
* @param InstallerAdapter $parent Parent installer adapter
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function uninstall($parent)
|
||||
{
|
||||
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UNINSTALL_SUCCESS') . '</p>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called after extension installation/update/discover_install
|
||||
*
|
||||
* @param string $type Installation type (install, update, discover_install)
|
||||
* @param InstallerAdapter $parent Parent installer adapter
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function postflight($type, $parent)
|
||||
{
|
||||
if ($type === 'install' || $type === 'discover_install') {
|
||||
// Create Terms of Service article and menu item
|
||||
$this->createTermsOfServiceSetup();
|
||||
|
||||
echo '<div class="alert alert-success">';
|
||||
echo '<h4>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_TITLE') . '</h4>';
|
||||
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_DESC') . '</p>';
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Function called after extension installation/update/discover_install
|
||||
*
|
||||
* Fixes #89: enablePlugin() is now called unconditionally for both
|
||||
* install and upgrade paths.
|
||||
* Fixes #92: enablePlugin() is called on upgrade to re-enable if disabled.
|
||||
*
|
||||
* @param string $type Installation type (install, update, discover_install)
|
||||
* @param InstallerAdapter $parent Parent installer adapter
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function postflight($type, $parent)
|
||||
{
|
||||
// Always enable the plugin on install or upgrade
|
||||
$this->enablePlugin();
|
||||
|
||||
/**
|
||||
* Create Terms of Service article and menu item
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function createTermsOfServiceSetup()
|
||||
{
|
||||
try {
|
||||
$db = Factory::getDbo();
|
||||
|
||||
// Check if Terms of Service article already exists
|
||||
$query = $db->getQuery(true)
|
||||
->select('id')
|
||||
->from($db->quoteName('#__content'))
|
||||
->where($db->quoteName('alias') . ' = ' . $db->quote('terms-of-service'));
|
||||
$db->setQuery($query);
|
||||
$articleId = $db->loadResult();
|
||||
|
||||
// Create article if it doesn't exist
|
||||
if (!$articleId) {
|
||||
$articleId = $this->createTermsArticle();
|
||||
}
|
||||
|
||||
if ($articleId) {
|
||||
// Check if menu item already exists
|
||||
$query = $db->getQuery(true)
|
||||
->select('id')
|
||||
->from($db->quoteName('#__menu'))
|
||||
->where($db->quoteName('alias') . ' = ' . $db->quote('terms-of-service'))
|
||||
->where($db->quoteName('published') . ' >= 0');
|
||||
$db->setQuery($query);
|
||||
$menuId = $db->loadResult();
|
||||
|
||||
// Create menu item if it doesn't exist
|
||||
if (!$menuId) {
|
||||
$this->createTermsMenuItem($articleId);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::add('Error creating Terms of Service setup: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||
}
|
||||
}
|
||||
if ($type === 'install' || $type === 'discover_install') {
|
||||
$this->createTermsOfServiceSetup();
|
||||
$this->setDefaultSlugs();
|
||||
|
||||
/**
|
||||
* Create Terms of Service article
|
||||
*
|
||||
* @return int|null Article ID or null on failure
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function createTermsArticle()
|
||||
{
|
||||
try {
|
||||
$db = Factory::getDbo();
|
||||
echo '<div class="alert alert-success">';
|
||||
echo '<h4>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_TITLE') . '</h4>';
|
||||
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_DESC') . '</p>';
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_content/tables');
|
||||
$table = Table::getInstance('Content', 'Joomla\\Component\\Content\\Administrator\\Table\\');
|
||||
/**
|
||||
* Create Terms of Service article and menu item
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function createTermsOfServiceSetup()
|
||||
{
|
||||
try {
|
||||
$db = Factory::getDbo();
|
||||
|
||||
if (!$table) {
|
||||
Log::add('Failed to get Content table instance', Log::WARNING, 'jerror');
|
||||
return null;
|
||||
}
|
||||
// Check if Terms of Service article already exists (by alias, any category)
|
||||
$query = $db->getQuery(true)
|
||||
->select('id')
|
||||
->from($db->quoteName('#__content'))
|
||||
->where($db->quoteName('alias') . ' = ' . $db->quote('terms-of-service'))
|
||||
->where($db->quoteName('state') . ' >= 0');
|
||||
$db->setQuery($query);
|
||||
$articleId = $db->loadResult();
|
||||
|
||||
// Get Uncategorised category ID dynamically
|
||||
$query = $db->getQuery(true)
|
||||
->select('id')
|
||||
->from($db->quoteName('#__categories'))
|
||||
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'))
|
||||
->where($db->quoteName('alias') . ' = ' . $db->quote('uncategorised'))
|
||||
->where($db->quoteName('published') . ' = 1');
|
||||
$db->setQuery($query);
|
||||
$catId = (int) $db->loadResult();
|
||||
if (!$articleId) {
|
||||
$articleId = $this->createTermsArticle();
|
||||
}
|
||||
|
||||
if (!$catId) {
|
||||
Log::add('Could not find Uncategorised category for com_content', Log::WARNING, 'jerror');
|
||||
return null;
|
||||
}
|
||||
if ($articleId) {
|
||||
$query = $db->getQuery(true)
|
||||
->select('id')
|
||||
->from($db->quoteName('#__menu'))
|
||||
->where($db->quoteName('alias') . ' = ' . $db->quote('terms-of-service'))
|
||||
->where($db->quoteName('published') . ' >= 0');
|
||||
$db->setQuery($query);
|
||||
$menuId = $db->loadResult();
|
||||
|
||||
// Get current user ID for article ownership
|
||||
$createdBy = Factory::getApplication()->getIdentity()->id ?: 0;
|
||||
if (!$menuId) {
|
||||
$this->createTermsMenuItem($articleId);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::add('Error creating Terms of Service setup: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||
}
|
||||
}
|
||||
|
||||
$data = [
|
||||
'title' => 'Terms of Service',
|
||||
'alias' => 'terms-of-service',
|
||||
'introtext' => '<h2>Terms of Service</h2><p>Welcome to our Terms of Service page.</p><p>This page will remain accessible even when the site is in offline/maintenance mode.</p>',
|
||||
'fulltext' => '',
|
||||
'state' => 1,
|
||||
'catid' => $catId,
|
||||
'created' => Factory::getDate()->toSql(),
|
||||
'created_by' => $createdBy,
|
||||
'language' => '*',
|
||||
'access' => 1, // Public
|
||||
];
|
||||
|
||||
// Bind data to table object first
|
||||
if (!$table->bind($data)) {
|
||||
Log::add('Failed to bind data to Content table: ' . $table->getError(), Log::WARNING, 'jerror');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check data validity
|
||||
if (!$table->check()) {
|
||||
Log::add('Content table check failed: ' . $table->getError(), Log::WARNING, 'jerror');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Save the table
|
||||
if (!$table->store()) {
|
||||
Log::add('Failed to store Content table: ' . $table->getError(), Log::WARNING, 'jerror');
|
||||
return null;
|
||||
}
|
||||
|
||||
echo '<p class="alert alert-info">✓ Created Terms of Service article</p>';
|
||||
return $table->id;
|
||||
} catch (\Exception $e) {
|
||||
Log::add('Error creating Terms of Service article: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Create Terms of Service article using Joomla 5 MVCFactory
|
||||
*
|
||||
* Fixes #90: Uses bootComponent()->getMVCFactory() instead of
|
||||
* the removed Table::addIncludePath() / Table::getInstance().
|
||||
* Fixes #94: Includes params, metadata, and attribs defaults.
|
||||
*
|
||||
* @return int|null Article ID or null on failure
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function createTermsArticle()
|
||||
{
|
||||
try {
|
||||
$db = Factory::getDbo();
|
||||
$app = Factory::getApplication();
|
||||
|
||||
/**
|
||||
* Create Terms of Service menu item
|
||||
*
|
||||
* @param int $articleId The article ID to link to
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function createTermsMenuItem($articleId)
|
||||
{
|
||||
try {
|
||||
$db = Factory::getDbo();
|
||||
|
||||
// Check if "Legal" menu type exists
|
||||
$query = $db->getQuery(true)
|
||||
->select('id')
|
||||
->from($db->quoteName('#__menu_types'))
|
||||
->where($db->quoteName('menutype') . ' = ' . $db->quote('legal'));
|
||||
$db->setQuery($query);
|
||||
$legalMenuExists = $db->loadResult();
|
||||
|
||||
// Create "Legal" menu type if it doesn't exist
|
||||
if (!$legalMenuExists) {
|
||||
$this->createLegalMenuType();
|
||||
}
|
||||
|
||||
// Get com_content component ID dynamically
|
||||
$query = $db->getQuery(true)
|
||||
->select('extension_id')
|
||||
->from($db->quoteName('#__extensions'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_content'));
|
||||
$db->setQuery($query);
|
||||
$componentId = (int) $db->loadResult();
|
||||
// Get content table via MVCFactory (Joomla 4/5 compatible)
|
||||
$table = $app->bootComponent('com_content')
|
||||
->getMVCFactory()
|
||||
->createTable('Article', 'Administrator');
|
||||
|
||||
if (!$componentId) {
|
||||
Log::add('Could not determine com_content component ID', Log::WARNING, 'jerror');
|
||||
return;
|
||||
}
|
||||
|
||||
Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_menus/tables');
|
||||
$table = Table::getInstance('Menu', 'Joomla\\Component\\Menus\\Administrator\\Table\\');
|
||||
|
||||
if (!$table) {
|
||||
Log::add('Failed to get Menu table instance', Log::WARNING, 'jerror');
|
||||
return;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'menutype' => 'legal',
|
||||
'title' => 'Terms of Service',
|
||||
'alias' => 'terms-of-service',
|
||||
'link' => 'index.php?option=com_content&view=article&id=' . $articleId,
|
||||
'type' => 'component',
|
||||
'published' => 1,
|
||||
'parent_id' => 1,
|
||||
'component_id' => $componentId,
|
||||
'level' => 1,
|
||||
'language' => '*',
|
||||
'access' => 1, // Public
|
||||
'params' => '{"show_title":"1","link_titles":"0","show_intro":"","info_block_position":"","show_category":"0","link_category":"0","show_parent_category":"0","link_parent_category":"0","show_author":"0","link_author":"0","show_create_date":"0","show_modify_date":"0","show_publish_date":"0","show_item_navigation":"0","show_icons":"0","show_print_icon":"0","show_email_icon":"0","show_hits":"0","show_noauth":"0","urls_position":"","menu-anchor_title":"","menu-anchor_css":"","menu_image":"","menu_text":1,"page_title":"","show_page_heading":0,"page_heading":"","pageclass_sfx":"","menu-meta_description":"","menu-meta_keywords":"","robots":"","secure":0}',
|
||||
];
|
||||
|
||||
// Set the location in the menu tree
|
||||
$table->setLocation($data['parent_id'], 'last-child');
|
||||
|
||||
// Bind data to table object
|
||||
if (!$table->bind($data)) {
|
||||
Log::add('Failed to bind data to Menu table: ' . $table->getError(), Log::WARNING, 'jerror');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check data validity
|
||||
if (!$table->check()) {
|
||||
Log::add('Menu table check failed: ' . $table->getError(), Log::WARNING, 'jerror');
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the menu item
|
||||
if (!$table->store()) {
|
||||
Log::add('Failed to store Menu table: ' . $table->getError(), Log::WARNING, 'jerror');
|
||||
return;
|
||||
}
|
||||
|
||||
echo '<p class="alert alert-info">✓ Created Terms of Service menu item in Legal menu with slug: terms-of-service</p>';
|
||||
|
||||
// Enable the plugin
|
||||
$this->enablePlugin();
|
||||
} catch (\Exception $e) {
|
||||
Log::add('Error creating Terms of Service menu item: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||
}
|
||||
}
|
||||
if (!$table) {
|
||||
Log::add('Failed to get Content table instance', Log::WARNING, 'jerror');
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Legal menu type
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function createLegalMenuType()
|
||||
{
|
||||
try {
|
||||
$db = Factory::getDbo();
|
||||
|
||||
// Insert the Legal menu type
|
||||
$query = $db->getQuery(true)
|
||||
->insert($db->quoteName('#__menu_types'))
|
||||
->columns($db->quoteName(['menutype', 'title', 'description']))
|
||||
->values(
|
||||
$db->quote('legal') . ', ' .
|
||||
$db->quote('Legal') . ', ' .
|
||||
$db->quote('Legal documents and policies menu')
|
||||
);
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
|
||||
echo '<p class="alert alert-info">✓ Created Legal menu type</p>';
|
||||
} catch (\Exception $e) {
|
||||
Log::add('Error creating Legal menu type: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||
}
|
||||
}
|
||||
// Get Uncategorised category ID dynamically
|
||||
$query = $db->getQuery(true)
|
||||
->select('id')
|
||||
->from($db->quoteName('#__categories'))
|
||||
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'))
|
||||
->where($db->quoteName('alias') . ' = ' . $db->quote('uncategorised'))
|
||||
->where($db->quoteName('published') . ' = 1');
|
||||
$db->setQuery($query);
|
||||
$catId = (int) $db->loadResult();
|
||||
|
||||
/**
|
||||
* Enable the plugin after installation
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function enablePlugin()
|
||||
{
|
||||
try {
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__extensions'))
|
||||
->set($db->quoteName('enabled') . ' = 1')
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoomtos'));
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
|
||||
echo '<p class="alert alert-success">✓ Plugin enabled automatically</p>';
|
||||
} catch (\Exception $e) {
|
||||
Log::add('Error enabling plugin: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||
}
|
||||
}
|
||||
if (!$catId) {
|
||||
Log::add('Could not find Uncategorised category for com_content', Log::WARNING, 'jerror');
|
||||
return null;
|
||||
}
|
||||
|
||||
$createdBy = $app->getIdentity()->id ?: 0;
|
||||
|
||||
$data = [
|
||||
'title' => 'Terms of Service',
|
||||
'alias' => 'terms-of-service',
|
||||
'introtext' => '<h2>Terms of Service</h2><p>Welcome to our Terms of Service page.</p><p>This page will remain accessible even when the site is in offline/maintenance mode.</p>',
|
||||
'fulltext' => '',
|
||||
'state' => 1,
|
||||
'catid' => $catId,
|
||||
'created' => Factory::getDate()->toSql(),
|
||||
'created_by' => $createdBy,
|
||||
'language' => '*',
|
||||
'access' => 1,
|
||||
'params' => '{}',
|
||||
'metadata' => '{"robots":"","author":"","rights":""}',
|
||||
'attribs' => '{}',
|
||||
];
|
||||
|
||||
if (!$table->bind($data)) {
|
||||
Log::add('Failed to bind data to Content table: ' . $table->getError(), Log::WARNING, 'jerror');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$table->check()) {
|
||||
Log::add('Content table check failed: ' . $table->getError(), Log::WARNING, 'jerror');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$table->store()) {
|
||||
Log::add('Failed to store Content table: ' . $table->getError(), Log::WARNING, 'jerror');
|
||||
return null;
|
||||
}
|
||||
|
||||
echo '<p class="alert alert-info">Created Terms of Service article</p>';
|
||||
return $table->id;
|
||||
} catch (\Throwable $e) {
|
||||
Log::add('Error creating Terms of Service article: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Terms of Service menu item using Joomla 5 MVCFactory
|
||||
*
|
||||
* Fixes #90: Uses bootComponent()->getMVCFactory() instead of
|
||||
* the removed Table::addIncludePath() / Table::getInstance().
|
||||
*
|
||||
* @param int $articleId The article ID to link to
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function createTermsMenuItem($articleId)
|
||||
{
|
||||
try {
|
||||
$db = Factory::getDbo();
|
||||
$app = Factory::getApplication();
|
||||
|
||||
// Check if "Legal" menu type exists
|
||||
$query = $db->getQuery(true)
|
||||
->select('id')
|
||||
->from($db->quoteName('#__menu_types'))
|
||||
->where($db->quoteName('menutype') . ' = ' . $db->quote('legal'));
|
||||
$db->setQuery($query);
|
||||
$legalMenuExists = $db->loadResult();
|
||||
|
||||
if (!$legalMenuExists) {
|
||||
$this->createLegalMenuType();
|
||||
}
|
||||
|
||||
// Get com_content component ID dynamically
|
||||
$query = $db->getQuery(true)
|
||||
->select('extension_id')
|
||||
->from($db->quoteName('#__extensions'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_content'));
|
||||
$db->setQuery($query);
|
||||
$componentId = (int) $db->loadResult();
|
||||
|
||||
if (!$componentId) {
|
||||
Log::add('Could not determine com_content component ID', Log::WARNING, 'jerror');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get menu table via MVCFactory (Joomla 4/5 compatible)
|
||||
$table = $app->bootComponent('com_menus')
|
||||
->getMVCFactory()
|
||||
->createTable('Menu', 'Administrator');
|
||||
|
||||
if (!$table) {
|
||||
Log::add('Failed to get Menu table instance', Log::WARNING, 'jerror');
|
||||
return;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'menutype' => 'legal',
|
||||
'title' => 'Terms of Service',
|
||||
'alias' => 'terms-of-service',
|
||||
'link' => 'index.php?option=com_content&view=article&id=' . $articleId,
|
||||
'type' => 'component',
|
||||
'published' => 1,
|
||||
'parent_id' => 1,
|
||||
'component_id' => $componentId,
|
||||
'level' => 1,
|
||||
'language' => '*',
|
||||
'access' => 1,
|
||||
'params' => '{"show_title":"1","link_titles":"0","show_intro":"","info_block_position":"","show_category":"0","link_category":"0","show_parent_category":"0","link_parent_category":"0","show_author":"0","link_author":"0","show_create_date":"0","show_modify_date":"0","show_publish_date":"0","show_item_navigation":"0","show_icons":"0","show_print_icon":"0","show_email_icon":"0","show_hits":"0","show_noauth":"0","urls_position":"","menu-anchor_title":"","menu-anchor_css":"","menu_image":"","menu_text":1,"page_title":"","show_page_heading":0,"page_heading":"","pageclass_sfx":"","menu-meta_description":"","menu-meta_keywords":"","robots":"","secure":0}',
|
||||
];
|
||||
|
||||
$table->setLocation($data['parent_id'], 'last-child');
|
||||
|
||||
if (!$table->bind($data)) {
|
||||
Log::add('Failed to bind data to Menu table: ' . $table->getError(), Log::WARNING, 'jerror');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$table->check()) {
|
||||
Log::add('Menu table check failed: ' . $table->getError(), Log::WARNING, 'jerror');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$table->store()) {
|
||||
Log::add('Failed to store Menu table: ' . $table->getError(), Log::WARNING, 'jerror');
|
||||
return;
|
||||
}
|
||||
|
||||
echo '<p class="alert alert-info">Created Terms of Service menu item in Legal menu</p>';
|
||||
} catch (\Throwable $e) {
|
||||
Log::add('Error creating Terms of Service menu item: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Legal menu type
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function createLegalMenuType()
|
||||
{
|
||||
try {
|
||||
$db = Factory::getDbo();
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->insert($db->quoteName('#__menu_types'))
|
||||
->columns($db->quoteName(['menutype', 'title', 'description']))
|
||||
->values(
|
||||
$db->quote('legal') . ', ' .
|
||||
$db->quote('Legal') . ', ' .
|
||||
$db->quote('Legal documents and policies menu')
|
||||
);
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
|
||||
echo '<p class="alert alert-info">Created Legal menu type</p>';
|
||||
} catch (\Throwable $e) {
|
||||
// Duplicate key is expected if race condition — safe to ignore
|
||||
Log::add('Error creating Legal menu type: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-select default menu slugs (terms-of-service, privacy-policy)
|
||||
*
|
||||
* Looks up menu items matching common legal page aliases and sets
|
||||
* them as the default tos_slug parameter so the plugin works
|
||||
* immediately after install with zero configuration.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.2.1
|
||||
*/
|
||||
private function setDefaultSlugs()
|
||||
{
|
||||
try {
|
||||
$db = Factory::getDbo();
|
||||
$defaultAliases = ['terms-of-service', 'privacy-policy'];
|
||||
$slugs = [];
|
||||
|
||||
foreach ($defaultAliases as $alias) {
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('path'))
|
||||
->from($db->quoteName('#__menu'))
|
||||
->where($db->quoteName('alias') . ' = ' . $db->quote($alias))
|
||||
->where($db->quoteName('published') . ' = 1')
|
||||
->where($db->quoteName('client_id') . ' = 0');
|
||||
$db->setQuery($query);
|
||||
$path = $db->loadResult();
|
||||
|
||||
if ($path) {
|
||||
$slugs[] = trim($path, '/');
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($slugs)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load current plugin params
|
||||
$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('mokojoomtos'));
|
||||
$db->setQuery($query);
|
||||
$paramsJson = $db->loadResult();
|
||||
|
||||
$params = json_decode($paramsJson ?: '{}', true) ?: [];
|
||||
|
||||
// Only set defaults if no slugs are already configured
|
||||
$existing = $params['tos_slug'] ?? [];
|
||||
|
||||
if (!empty($existing)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params['tos_slug'] = $slugs;
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__extensions'))
|
||||
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($params)))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoomtos'));
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
} catch (\Throwable $e) {
|
||||
Log::add('Error setting default slugs: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the plugin after installation
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function enablePlugin()
|
||||
{
|
||||
try {
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__extensions'))
|
||||
->set($db->quoteName('enabled') . ' = 1')
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoomtos'));
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
} catch (\Throwable $e) {
|
||||
Log::add('Error enabling plugin: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace Joomla\Plugin\System\MokoJoomTOS\Extension;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Plugin\CMSPlugin;
|
||||
use Joomla\CMS\Uri\Uri;
|
||||
use Joomla\Event\SubscriberInterface;
|
||||
@@ -53,23 +54,21 @@ final class MokoJoomTOS extends CMSPlugin implements SubscriberInterface
|
||||
* the site is in offline mode. If both conditions are met, temporarily
|
||||
* disables offline mode and sets component-only view for this request.
|
||||
*
|
||||
* This event fires after routing but before template selection, making it
|
||||
* the correct place to set tmpl=component to prevent template chrome loading.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function onAfterRoute()
|
||||
{
|
||||
$app = $this->getApplication();
|
||||
|
||||
// Only process for site application
|
||||
if (!$this->getApplication()->isClient('site'))
|
||||
if (!$app->isClient('site'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the global configuration
|
||||
$config = $this->getApplication()->getConfig();
|
||||
$config = $app->getConfig();
|
||||
|
||||
// Only proceed if site is offline
|
||||
if (!$config->get('offline'))
|
||||
@@ -77,7 +76,7 @@ final class MokoJoomTOS extends CMSPlugin implements SubscriberInterface
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the configured slugs (stored as array for multi-select)
|
||||
// Get the configured slugs — cast to array to handle stdClass from Registry (#96)
|
||||
$slugs = $this->params->get('tos_slug', []);
|
||||
|
||||
// Handle legacy single-value string format
|
||||
@@ -85,46 +84,161 @@ final class MokoJoomTOS extends CMSPlugin implements SubscriberInterface
|
||||
{
|
||||
$slugs = array_filter([trim($slugs)]);
|
||||
}
|
||||
else
|
||||
{
|
||||
$slugs = (array) $slugs;
|
||||
}
|
||||
|
||||
if (empty($slugs))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current URI path
|
||||
$uri = Uri::getInstance();
|
||||
$path = trim($uri->getPath(), '/');
|
||||
$includeChildren = (int) $this->params->get('include_children', 1);
|
||||
|
||||
// Remove the base path if present
|
||||
// Try SEF path matching first, then fall back to Itemid matching (#91)
|
||||
if ($this->matchByPath($slugs, $config, $app, $includeChildren))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$this->matchByItemId($slugs, $config, $app, $includeChildren);
|
||||
}
|
||||
|
||||
/**
|
||||
* Match the current request path against configured slugs (SEF mode)
|
||||
*
|
||||
* @param array $slugs Configured slug values
|
||||
* @param object $config Joomla configuration object
|
||||
* @param object $app Application instance
|
||||
* @param integer $includeChildren Whether to include child menu items
|
||||
*
|
||||
* @return boolean True if a match was found and offline mode was bypassed
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
private function matchByPath(array $slugs, $config, $app, int $includeChildren = 1): bool
|
||||
{
|
||||
$uri = Uri::getInstance();
|
||||
$path = urldecode(trim($uri->getPath(), '/'));
|
||||
|
||||
// Remove the base path if present (subdirectory installs)
|
||||
$base = trim(Uri::base(true), '/');
|
||||
if (!empty($base) && strpos($path, $base) === 0)
|
||||
{
|
||||
$path = trim(substr($path, strlen($base)), '/');
|
||||
}
|
||||
|
||||
// Check if the path matches any configured slug
|
||||
// Skip if path is empty or just index.php (non-SEF)
|
||||
if (empty($path) || $path === 'index.php')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($slugs as $slug)
|
||||
{
|
||||
$slug = trim($slug);
|
||||
$slug = trim((string) $slug);
|
||||
if (empty($slug))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($path === $slug || strpos($path, $slug . '/') === 0)
|
||||
$isMatch = ($path === $slug)
|
||||
|| ($includeChildren && strpos($path, $slug . '/') === 0);
|
||||
|
||||
if ($isMatch)
|
||||
{
|
||||
// Temporarily disable offline mode for this request
|
||||
$config->set('offline', 0);
|
||||
|
||||
// Set component-only view (no template chrome)
|
||||
$input = $this->getApplication()->input;
|
||||
$input->set('tmpl', 'component');
|
||||
|
||||
// Also set in GET superglobal to ensure recognition
|
||||
$_GET['tmpl'] = 'component';
|
||||
|
||||
return;
|
||||
$this->bypassOffline($config, $app);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match the current request Itemid against menu items for configured slugs (non-SEF fallback)
|
||||
*
|
||||
* When SEF URLs are disabled, the path is just index.php so we match by
|
||||
* checking if the requested Itemid belongs to a menu item whose path
|
||||
* matches a configured slug.
|
||||
*
|
||||
* @param array $slugs Configured slug values
|
||||
* @param object $config Joomla configuration object
|
||||
* @param object $app Application instance
|
||||
* @param integer $includeChildren Whether to include child menu items
|
||||
*
|
||||
* @return boolean True if a match was found and offline mode was bypassed
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
private function matchByItemId(array $slugs, $config, $app, int $includeChildren = 1): bool
|
||||
{
|
||||
$itemId = (int) $app->input->getInt('Itemid', 0);
|
||||
|
||||
if (!$itemId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('path'))
|
||||
->from($db->quoteName('#__menu'))
|
||||
->where($db->quoteName('id') . ' = ' . $itemId)
|
||||
->where($db->quoteName('published') . ' = 1')
|
||||
->where($db->quoteName('client_id') . ' = 0');
|
||||
$db->setQuery($query);
|
||||
$menuPath = $db->loadResult();
|
||||
|
||||
if (!$menuPath)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$menuPath = trim($menuPath, '/');
|
||||
|
||||
foreach ($slugs as $slug)
|
||||
{
|
||||
$slug = trim((string) $slug);
|
||||
if (empty($slug))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$isMatch = ($menuPath === $slug)
|
||||
|| ($includeChildren && strpos($menuPath, $slug . '/') === 0);
|
||||
|
||||
if ($isMatch)
|
||||
{
|
||||
$this->bypassOffline($config, $app);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
// Silently fail — do not bypass offline mode on error
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bypass offline mode and set component-only view for this request
|
||||
*
|
||||
* @param object $config Joomla configuration object
|
||||
* @param object $app Application instance
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
private function bypassOffline($config, $app): void
|
||||
{
|
||||
$config->set('offline', 0);
|
||||
$app->input->set('tmpl', 'component');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,24 @@ class MenuslugField extends ListField
|
||||
{
|
||||
$options = parent::getOptions();
|
||||
|
||||
// Warn if SEF URLs are disabled (#97)
|
||||
try
|
||||
{
|
||||
$sef = Factory::getApplication()->get('sef', true);
|
||||
if (!$sef)
|
||||
{
|
||||
$options[] = (object) [
|
||||
'value' => '',
|
||||
'text' => Text::_('PLG_SYSTEM_MOKOJOOMTOS_FIELD_SEF_WARNING'),
|
||||
'disabled' => true
|
||||
];
|
||||
}
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
// Ignore — field still works without the warning
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
@@ -70,7 +88,7 @@ class MenuslugField extends ListField
|
||||
$options[] = (object) [
|
||||
'value' => '',
|
||||
'text' => '──────────────',
|
||||
'disable' => true
|
||||
'disabled' => true
|
||||
];
|
||||
}
|
||||
$lastMenuType = $item->menutype;
|
||||
|
||||
+26
-31
@@ -1,96 +1,91 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
VERSION: 04.00.00
|
||||
VERSION: 04.03.00
|
||||
-->
|
||||
|
||||
<updates>
|
||||
<update>
|
||||
<name>plg_system_mokojoomtos</name>
|
||||
<description>plg_system_mokojoomtos update</description>
|
||||
<name>System - Moko Terms of Service</name>
|
||||
<description>System - Moko Terms of Service update</description>
|
||||
<element>mokojoomtos</element>
|
||||
<type>plugin</type>
|
||||
<version>04.00.00</version>
|
||||
<version>04.03.00-dev</version>
|
||||
<client>site</client>
|
||||
<folder>system</folder>
|
||||
<tags><tag>development</tag></tags>
|
||||
<infourl title="plg_system_mokojoomtos">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable</infourl>
|
||||
<infourl title="System - Moko Terms of Service">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/development</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/stable/plg_system_mokojoomtos-04.00.00.zip</downloadurl>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/development/plg_system_mokojoomtos-04.03.00-dev.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>6d8c6a03d6dc0a784a4442c9c46e1a79e8f65b3da7091c5857958653c5b6efe3</sha256>
|
||||
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
</update>
|
||||
<update>
|
||||
<name>plg_system_mokojoomtos</name>
|
||||
<description>plg_system_mokojoomtos update</description>
|
||||
<name>System - Moko Terms of Service</name>
|
||||
<description>System - Moko Terms of Service update</description>
|
||||
<element>mokojoomtos</element>
|
||||
<type>plugin</type>
|
||||
<version>04.00.00</version>
|
||||
<version>04.03.00-alpha</version>
|
||||
<client>site</client>
|
||||
<folder>system</folder>
|
||||
<tags><tag>alpha</tag></tags>
|
||||
<infourl title="plg_system_mokojoomtos">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable</infourl>
|
||||
<infourl title="System - Moko Terms of Service">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/alpha</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/stable/plg_system_mokojoomtos-04.00.00.zip</downloadurl>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/alpha/plg_system_mokojoomtos-04.03.00-alpha.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>6d8c6a03d6dc0a784a4442c9c46e1a79e8f65b3da7091c5857958653c5b6efe3</sha256>
|
||||
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
</update>
|
||||
<update>
|
||||
<name>plg_system_mokojoomtos</name>
|
||||
<description>plg_system_mokojoomtos update</description>
|
||||
<name>System - Moko Terms of Service</name>
|
||||
<description>System - Moko Terms of Service update</description>
|
||||
<element>mokojoomtos</element>
|
||||
<type>plugin</type>
|
||||
<version>04.00.00</version>
|
||||
<version>04.03.00-beta</version>
|
||||
<client>site</client>
|
||||
<folder>system</folder>
|
||||
<tags><tag>beta</tag></tags>
|
||||
<infourl title="plg_system_mokojoomtos">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable</infourl>
|
||||
<infourl title="System - Moko Terms of Service">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/beta</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/stable/plg_system_mokojoomtos-04.00.00.zip</downloadurl>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/beta/plg_system_mokojoomtos-04.03.00-beta.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>6d8c6a03d6dc0a784a4442c9c46e1a79e8f65b3da7091c5857958653c5b6efe3</sha256>
|
||||
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
</update>
|
||||
<update>
|
||||
<name>plg_system_mokojoomtos</name>
|
||||
<description>plg_system_mokojoomtos update</description>
|
||||
<name>System - Moko Terms of Service</name>
|
||||
<description>System - Moko Terms of Service update</description>
|
||||
<element>mokojoomtos</element>
|
||||
<type>plugin</type>
|
||||
<version>04.00.00</version>
|
||||
<version>04.03.00-rc</version>
|
||||
<client>site</client>
|
||||
<folder>system</folder>
|
||||
<tags><tag>rc</tag></tags>
|
||||
<infourl title="plg_system_mokojoomtos">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable</infourl>
|
||||
<infourl title="System - Moko Terms of Service">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/rc</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/stable/plg_system_mokojoomtos-04.00.00.zip</downloadurl>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/rc/plg_system_mokojoomtos-04.03.00-rc.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>6d8c6a03d6dc0a784a4442c9c46e1a79e8f65b3da7091c5857958653c5b6efe3</sha256>
|
||||
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
</update>
|
||||
<update>
|
||||
<name>plg_system_mokojoomtos</name>
|
||||
<description>plg_system_mokojoomtos update</description>
|
||||
<name>System - Moko Terms of Service</name>
|
||||
<description>System - Moko Terms of Service update</description>
|
||||
<element>mokojoomtos</element>
|
||||
<type>plugin</type>
|
||||
<version>04.00.00</version>
|
||||
<version>04.03.00</version>
|
||||
<client>site</client>
|
||||
<folder>system</folder>
|
||||
<tags><tag>stable</tag></tags>
|
||||
<infourl title="plg_system_mokojoomtos">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable</infourl>
|
||||
<infourl title="System - Moko Terms of Service">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/stable/plg_system_mokojoomtos-04.00.00.zip</downloadurl>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/stable/plg_system_mokojoomtos-04.03.00.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>6d8c6a03d6dc0a784a4442c9c46e1a79e8f65b3da7091c5857958653c5b6efe3</sha256>
|
||||
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
|
||||
Reference in New Issue
Block a user