Compare commits
208 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 114ac66404 | |||
| b08c5fa2d2 | |||
| 5bd2ffc5bf | |||
| 342cdf99f3 | |||
| b3e9fe6241 | |||
| 210638929d | |||
| e3ddeb0294 | |||
| 130921d99a | |||
| 3d7e7898f2 | |||
| 539f034519 | |||
| 22e8e90d25 | |||
| c8aa8d1b89 | |||
| 2a8d43a576 | |||
| 4c3404d432 | |||
| 56e563d5dd | |||
| ecebb1b989 | |||
| d955c530c2 | |||
| b07bd2967d | |||
| 16dd7cd774 | |||
| 5653c90935 | |||
| fa1e24fae9 | |||
| 4d60ca129a | |||
| d028db4bdd | |||
| b3b77a71dd | |||
| 9417d27356 | |||
| 15c4f506f7 | |||
| b39a8ca137 | |||
| 448f7a1b7f | |||
| 93c737010a | |||
| eaf60fad91 | |||
| 0dfa409df5 | |||
| 2a150b66f7 | |||
| eeddb211e8 | |||
| c545e1d65c | |||
| 429a1aa819 | |||
| 9b112fb708 | |||
| 8339ff550a | |||
| e50b2f3bff | |||
| e6d523a1a2 | |||
| f1a59547ae | |||
| ee1a537740 | |||
| 895ab7620f | |||
| efdc966d5b | |||
| 4629bc3352 | |||
| adf2b772a1 | |||
| 1a80686df1 | |||
| 71885a775e | |||
| f7a1f75b68 | |||
| 7e02993dac | |||
| 9c73567946 | |||
| 801c47baec | |||
| 06efd6654e | |||
| d969d42d79 | |||
| 8876a24dec | |||
| 377609ec76 | |||
| 858f4a1138 | |||
| f44723ed08 | |||
| fe19eac25d | |||
| 9d5d60fadf | |||
| 4a48dc3150 | |||
| 27856f7cc8 | |||
| a0a9b4c204 | |||
| 4dfac0a81e | |||
| dc2d49f18d | |||
| c6ecee1917 | |||
| b3aa838da6 | |||
| 458043d2e6 | |||
| 12592dc390 | |||
| 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 |
File diff suppressed because it is too large
Load Diff
@@ -1,213 +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.Maintenance
|
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
|
||||||
# PATH: /templates/workflows/cascade-dev.yml.template
|
|
||||||
# VERSION: 02.00.00
|
|
||||||
# BRIEF: Forward-merge main → all open branches after every push to main
|
|
||||||
#
|
|
||||||
# +========================================================================+
|
|
||||||
# | CASCADE MAIN → ALL BRANCHES |
|
|
||||||
# +========================================================================+
|
|
||||||
# | |
|
|
||||||
# | Triggers on every push to main (PR merges, bot commits, etc.) |
|
|
||||||
# | |
|
|
||||||
# | 1. List all branches matching: dev, rc/*, beta/*, alpha/* |
|
|
||||||
# | 2. For each: create PR (main → branch), auto-merge if clean |
|
|
||||||
# | 3. On conflict: leave PR open for manual resolution |
|
|
||||||
# | |
|
|
||||||
# +========================================================================+
|
|
||||||
|
|
||||||
name: "Universal: Cascade Main → Dev"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
|
||||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
cascade:
|
|
||||||
name: Cascade main → branches
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: >-
|
|
||||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
|
||||||
!contains(github.event.head_commit.message, '[skip cascade]')
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Discover target branches
|
|
||||||
id: branches
|
|
||||||
env:
|
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
|
|
||||||
# Fetch all branches (paginated)
|
|
||||||
PAGE=1
|
|
||||||
ALL_BRANCHES=""
|
|
||||||
while true; do
|
|
||||||
BATCH=$(curl -sS \
|
|
||||||
-H "Authorization: token ${GA_TOKEN}" \
|
|
||||||
"${API}/branches?page=${PAGE}&limit=50" \
|
|
||||||
| jq -r '.[].name // empty')
|
|
||||||
[ -z "$BATCH" ] && break
|
|
||||||
ALL_BRANCHES="$ALL_BRANCHES $BATCH"
|
|
||||||
PAGE=$((PAGE + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
# Filter to cascade targets: dev, dev/*, rc/*, beta/*, alpha/*
|
|
||||||
TARGETS=""
|
|
||||||
for BRANCH in $ALL_BRANCHES; do
|
|
||||||
case "$BRANCH" in
|
|
||||||
dev|dev/*|rc/*|beta/*|alpha/*)
|
|
||||||
TARGETS="$TARGETS $BRANCH"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
TARGETS=$(echo "$TARGETS" | xargs) # trim whitespace
|
|
||||||
|
|
||||||
if [ -z "$TARGETS" ]; then
|
|
||||||
echo "targets=" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "ℹ️ No cascade target branches found"
|
|
||||||
else
|
|
||||||
echo "targets=$TARGETS" >> "$GITHUB_OUTPUT"
|
|
||||||
COUNT=$(echo "$TARGETS" | wc -w)
|
|
||||||
echo "📋 Found ${COUNT} target branch(es): ${TARGETS}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Cascade to all target branches
|
|
||||||
if: steps.branches.outputs.targets != ''
|
|
||||||
env:
|
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
SHORT_SHA="${GITHUB_SHA:0:7}"
|
|
||||||
TARGETS="${{ steps.branches.outputs.targets }}"
|
|
||||||
|
|
||||||
SUCCESS=0
|
|
||||||
CONFLICTS=0
|
|
||||||
SKIPPED=0
|
|
||||||
FAILED=0
|
|
||||||
|
|
||||||
for BRANCH in $TARGETS; do
|
|
||||||
echo ""
|
|
||||||
echo "═══ main → ${BRANCH} ═══"
|
|
||||||
|
|
||||||
# Check if branch is already up to date
|
|
||||||
ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g')
|
|
||||||
RESPONSE=$(curl -sS \
|
|
||||||
-H "Authorization: token ${GA_TOKEN}" \
|
|
||||||
"${API}/compare/${ENCODED_BRANCH}...main")
|
|
||||||
|
|
||||||
AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0')
|
|
||||||
|
|
||||||
if [ "$AHEAD" -eq 0 ]; then
|
|
||||||
echo " ✅ Already up to date"
|
|
||||||
SKIPPED=$((SKIPPED + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo " ℹ️ main is ${AHEAD} commit(s) ahead"
|
|
||||||
|
|
||||||
# Check for existing cascade PR
|
|
||||||
EXISTING=$(curl -sS \
|
|
||||||
-H "Authorization: token ${GA_TOKEN}" \
|
|
||||||
"${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1")
|
|
||||||
|
|
||||||
EXISTING_COUNT=$(echo "$EXISTING" | jq 'length')
|
|
||||||
PR_NUMBER=""
|
|
||||||
|
|
||||||
if [ "$EXISTING_COUNT" -gt 0 ]; then
|
|
||||||
PR_NUMBER=$(echo "$EXISTING" | jq -r '.[0].number')
|
|
||||||
echo " ℹ️ Reusing existing PR #${PR_NUMBER}"
|
|
||||||
else
|
|
||||||
# Create cascade PR
|
|
||||||
PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
|
||||||
-X POST \
|
|
||||||
-H "Authorization: token ${GA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{
|
|
||||||
\"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\",
|
|
||||||
\"body\": \"## Automatic cascade\\n\\nForward-merging \`main\` (${SHORT_SHA}) into \`${BRANCH}\`.\\n\\nIf conflicts exist, resolve manually and merge.\\n\\n> Auto-created by **Cascade Main → Dev**.\",
|
|
||||||
\"head\": \"main\",
|
|
||||||
\"base\": \"${BRANCH}\"
|
|
||||||
}" \
|
|
||||||
"${API}/pulls")
|
|
||||||
|
|
||||||
HTTP_CODE=$(echo "$PR_RESPONSE" | tail -1)
|
|
||||||
BODY=$(echo "$PR_RESPONSE" | sed '$d')
|
|
||||||
PR_NUMBER=$(echo "$BODY" | jq -r '.number // empty')
|
|
||||||
|
|
||||||
if [ "$HTTP_CODE" != "201" ] || [ -z "$PR_NUMBER" ]; then
|
|
||||||
MSG=$(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1)
|
|
||||||
echo " ❌ Failed to create PR (HTTP ${HTTP_CODE}): ${MSG}"
|
|
||||||
FAILED=$((FAILED + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo " ✅ Created PR #${PR_NUMBER}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Try auto-merge
|
|
||||||
PR_DATA=$(curl -sS \
|
|
||||||
-H "Authorization: token ${GA_TOKEN}" \
|
|
||||||
"${API}/pulls/${PR_NUMBER}")
|
|
||||||
|
|
||||||
MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false')
|
|
||||||
|
|
||||||
if [ "$MERGEABLE" != "true" ]; then
|
|
||||||
echo " ⚠️ Conflicts — PR #${PR_NUMBER} left open"
|
|
||||||
CONFLICTS=$((CONFLICTS + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
|
||||||
-X POST \
|
|
||||||
-H "Authorization: token ${GA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{
|
|
||||||
\"Do\": \"merge\",
|
|
||||||
\"merge_message_field\": \"chore: cascade main → ${BRANCH} [skip ci]\",
|
|
||||||
\"delete_branch_after_merge\": false
|
|
||||||
}" \
|
|
||||||
"${API}/pulls/${PR_NUMBER}/merge")
|
|
||||||
|
|
||||||
MERGE_HTTP=$(echo "$MERGE_RESPONSE" | tail -1)
|
|
||||||
|
|
||||||
if [ "$MERGE_HTTP" = "200" ] || [ "$MERGE_HTTP" = "204" ]; then
|
|
||||||
echo " ✅ Merged — ${BRANCH} is in sync"
|
|
||||||
SUCCESS=$((SUCCESS + 1))
|
|
||||||
else
|
|
||||||
MERGE_BODY=$(echo "$MERGE_RESPONSE" | sed '$d')
|
|
||||||
echo " ⚠️ Merge failed (HTTP ${MERGE_HTTP}) — PR #${PR_NUMBER} left open"
|
|
||||||
CONFLICTS=$((CONFLICTS + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Summary
|
|
||||||
echo ""
|
|
||||||
echo "════════════════════════════════════════"
|
|
||||||
echo " ✅ Merged: ${SUCCESS}"
|
|
||||||
echo " ⚠️ Conflicts: ${CONFLICTS}"
|
|
||||||
echo " ⏭️ Up to date: ${SKIPPED}"
|
|
||||||
echo " ❌ Failed: ${FAILED}"
|
|
||||||
echo "════════════════════════════════════════"
|
|
||||||
|
|
||||||
if [ "$FAILED" -gt 0 ]; then
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -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,464 +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.Joomla
|
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
|
||||||
# PATH: /templates/workflows/joomla/update-server.yml.template
|
|
||||||
# VERSION: 04.06.00
|
|
||||||
# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries
|
|
||||||
#
|
|
||||||
# Writes updates.xml with multiple <update> entries:
|
|
||||||
# - <tag>stable</tag> on push to main (from auto-release)
|
|
||||||
# - <tag>rc</tag> on push to rc/**
|
|
||||||
# - <tag>development</tag> on push to dev or dev/**
|
|
||||||
#
|
|
||||||
# Joomla filters by user's "Minimum Stability" setting.
|
|
||||||
|
|
||||||
name: "Joomla: Update Server"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'dev'
|
|
||||||
- 'dev/**'
|
|
||||||
- 'alpha/**'
|
|
||||||
- 'beta/**'
|
|
||||||
- 'rc/**'
|
|
||||||
paths:
|
|
||||||
- 'src/**'
|
|
||||||
- 'htdocs/**'
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
branches:
|
|
||||||
- 'dev'
|
|
||||||
- 'dev/**'
|
|
||||||
- 'alpha/**'
|
|
||||||
- 'beta/**'
|
|
||||||
- 'rc/**'
|
|
||||||
paths:
|
|
||||||
- 'src/**'
|
|
||||||
- 'htdocs/**'
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
stability:
|
|
||||||
description: 'Stability tag'
|
|
||||||
required: true
|
|
||||||
default: 'development'
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- development
|
|
||||||
- alpha
|
|
||||||
- beta
|
|
||||||
- rc
|
|
||||||
- stable
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
|
||||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-xml:
|
|
||||||
name: Update updates.xml
|
|
||||||
runs-on: release
|
|
||||||
if: >-
|
|
||||||
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GA_TOKEN }}
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup MokoStandards tools
|
|
||||||
env:
|
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
|
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
|
||||||
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}'
|
|
||||||
run: |
|
|
||||||
if ! command -v composer &> /dev/null; then
|
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
git clone --depth 1 --branch main --quiet \
|
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
|
||||||
/tmp/mokostandards-api 2>/dev/null || true
|
|
||||||
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
|
|
||||||
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Generate updates.xml entry
|
|
||||||
id: update
|
|
||||||
run: |
|
|
||||||
BRANCH="${{ github.ref_name }}"
|
|
||||||
REPO="${{ github.repository }}"
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0")
|
|
||||||
|
|
||||||
# Auto-bump patch on all branches (dev, alpha, beta, rc)
|
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "gitea-actions[bot]"
|
|
||||||
BUMPED=$(php /tmp/mokostandards-api/cli/version_bump.php --path . 2>/dev/null || true)
|
|
||||||
if [ -n "$BUMPED" ]; then
|
|
||||||
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION")
|
|
||||||
git add -A
|
|
||||||
git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
|
|
||||||
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" 2>/dev/null || true
|
|
||||||
git push 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Determine stability from branch or input
|
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
||||||
STABILITY="${{ inputs.stability }}"
|
|
||||||
elif [[ "$BRANCH" == rc/* ]]; then
|
|
||||||
STABILITY="rc"
|
|
||||||
elif [[ "$BRANCH" == beta/* ]]; then
|
|
||||||
STABILITY="beta"
|
|
||||||
elif [[ "$BRANCH" == alpha/* ]]; then
|
|
||||||
STABILITY="alpha"
|
|
||||||
elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then
|
|
||||||
STABILITY="development"
|
|
||||||
else
|
|
||||||
STABILITY="stable"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# Parse manifest (portable — no grep -P)
|
|
||||||
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
|
||||||
if [ -z "$MANIFEST" ]; then
|
|
||||||
echo "No Joomla manifest found — skipping"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract fields using sed (works on all runners)
|
|
||||||
EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_CLIENT=$(sed -n 's/.*<extension[^>]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
EXT_VERSION=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
|
|
||||||
|
|
||||||
# Fallbacks
|
|
||||||
[ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
|
|
||||||
[ -z "$EXT_TYPE" ] && EXT_TYPE="component"
|
|
||||||
|
|
||||||
# Derive element if not in manifest: try XML filename, then repo name
|
|
||||||
if [ -z "$EXT_ELEMENT" ]; then
|
|
||||||
EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
|
|
||||||
case "$EXT_ELEMENT" in
|
|
||||||
templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use manifest version if README version is empty
|
|
||||||
[ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION"
|
|
||||||
|
|
||||||
[ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" %s>' "/")
|
|
||||||
|
|
||||||
CLIENT_TAG=""
|
|
||||||
[ -n "$EXT_CLIENT" ] && CLIENT_TAG="<client>${EXT_CLIENT}</client>"
|
|
||||||
[ -z "$CLIENT_TAG" ] && ([ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]) && CLIENT_TAG="<client>site</client>"
|
|
||||||
|
|
||||||
FOLDER_TAG=""
|
|
||||||
[ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
|
|
||||||
|
|
||||||
PHP_TAG=""
|
|
||||||
[ -n "$PHP_MINIMUM" ] && PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>"
|
|
||||||
|
|
||||||
# Version suffix for non-stable
|
|
||||||
DISPLAY_VERSION="$VERSION"
|
|
||||||
case "$STABILITY" in
|
|
||||||
development) DISPLAY_VERSION="${VERSION}-dev" ;;
|
|
||||||
alpha) DISPLAY_VERSION="${VERSION}-alpha" ;;
|
|
||||||
beta) DISPLAY_VERSION="${VERSION}-beta" ;;
|
|
||||||
rc) DISPLAY_VERSION="${VERSION}-rc" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
|
|
||||||
|
|
||||||
# Each stability level has its own release tag
|
|
||||||
case "$STABILITY" in
|
|
||||||
development) RELEASE_TAG="development" ;;
|
|
||||||
alpha) RELEASE_TAG="alpha" ;;
|
|
||||||
beta) RELEASE_TAG="beta" ;;
|
|
||||||
rc) RELEASE_TAG="release-candidate" ;;
|
|
||||||
*) RELEASE_TAG="v${MAJOR}" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip"
|
|
||||||
DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}"
|
|
||||||
INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
|
|
||||||
# -- Build install packages (ZIP + tar.gz) --------------------
|
|
||||||
SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
||||||
if [ -d "$SOURCE_DIR" ]; then
|
|
||||||
EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
|
|
||||||
TAR_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz"
|
|
||||||
|
|
||||||
cd "$SOURCE_DIR"
|
|
||||||
zip -r "/tmp/${PACKAGE_NAME}" . -x $EXCLUDES
|
|
||||||
cd ..
|
|
||||||
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \
|
|
||||||
--exclude='.ftpignore' --exclude='sftp-config*' \
|
|
||||||
--exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
|
|
||||||
|
|
||||||
SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1)
|
|
||||||
|
|
||||||
# Ensure release exists on Gitea
|
|
||||||
RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
|
|
||||||
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -z "$RELEASE_ID" ]; then
|
|
||||||
# Create release
|
|
||||||
RELEASE_JSON=$(curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API_BASE}/releases" \
|
|
||||||
-d "$(python3 -c "import json; print(json.dumps({
|
|
||||||
'tag_name': '${RELEASE_TAG}',
|
|
||||||
'name': '${RELEASE_TAG} (${DISPLAY_VERSION})',
|
|
||||||
'body': '${STABILITY} release',
|
|
||||||
'prerelease': True,
|
|
||||||
'target_commitish': 'main'
|
|
||||||
}))")" 2>/dev/null || true)
|
|
||||||
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$RELEASE_ID" ]; then
|
|
||||||
# Delete existing assets with same name before uploading
|
|
||||||
ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]")
|
|
||||||
for ASSET_FILE in "$PACKAGE_NAME" "$TAR_NAME"; do
|
|
||||||
ASSET_ID=$(echo "$ASSETS" | python3 -c "
|
|
||||||
import sys,json
|
|
||||||
assets = json.load(sys.stdin)
|
|
||||||
for a in assets:
|
|
||||||
if a['name'] == '${ASSET_FILE}':
|
|
||||||
print(a['id']); break
|
|
||||||
" 2>/dev/null || true)
|
|
||||||
if [ -n "$ASSET_ID" ]; then
|
|
||||||
curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Upload both formats
|
|
||||||
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
-H "Content-Type: application/octet-stream" \
|
|
||||||
--data-binary @"/tmp/${PACKAGE_NAME}" \
|
|
||||||
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${PACKAGE_NAME}" > /dev/null 2>&1 || true
|
|
||||||
|
|
||||||
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
-H "Content-Type: application/octet-stream" \
|
|
||||||
--data-binary @"/tmp/${TAR_NAME}" \
|
|
||||||
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
SHA256=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# -- Build the new entry (canonical format matching release.yml) --
|
|
||||||
NEW_ENTRY=""
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <update>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <name>${EXT_NAME}</name>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <description>${EXT_NAME} ${STABILITY} build.</description>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <element>${EXT_ELEMENT}</element>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <type>${EXT_TYPE}</type>\n"
|
|
||||||
[ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n"
|
|
||||||
[ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <version>${VERSION}</version>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <creationDate>$(date +%Y-%m-%d)</creationDate>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <infourl title='${EXT_NAME}'>https://git.mokoconsulting.tech/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${RELEASE_TAG}</infourl>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <downloads>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <downloadurl type='full' format='zip'>${DOWNLOAD_URL}</downloadurl>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} </downloads>\n"
|
|
||||||
[ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} <sha256>${SHA256}</sha256>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <tags><tag>${STABILITY}</tag></tags>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <maintainer>Moko Consulting</maintainer>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <maintainerurl>https://mokoconsulting.tech</maintainerurl>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} <targetplatform name='joomla' version='(5|6).*'/>\n"
|
|
||||||
[ -n "$PHP_MINIMUM" ] && NEW_ENTRY="${NEW_ENTRY} <php_minimum>${PHP_MINIMUM}</php_minimum>\n"
|
|
||||||
NEW_ENTRY="${NEW_ENTRY} </update>"
|
|
||||||
|
|
||||||
# -- Write new entry to temp file --------------------------------
|
|
||||||
printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml
|
|
||||||
|
|
||||||
# -- Merge into updates.xml ----------------------------------------
|
|
||||||
# Cascade: stable→all | rc→rc+lower | beta→beta+lower | alpha→alpha+dev | dev→dev
|
|
||||||
CASCADE_MAP="stable:development,alpha,beta,rc,stable rc:development,alpha,beta,rc beta:development,alpha,beta alpha:development,alpha development:development"
|
|
||||||
TARGETS=""
|
|
||||||
for entry in $CASCADE_MAP; do
|
|
||||||
key="${entry%%:*}"
|
|
||||||
vals="${entry#*:}"
|
|
||||||
if [ "$key" = "${STABILITY}" ]; then
|
|
||||||
TARGETS="$vals"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
[ -z "$TARGETS" ] && TARGETS="${STABILITY}"
|
|
||||||
|
|
||||||
echo "Cascade: ${STABILITY} → ${TARGETS}"
|
|
||||||
|
|
||||||
# Create updates.xml if missing
|
|
||||||
if [ ! -f "updates.xml" ]; then
|
|
||||||
printf '%s\n' "<?xml version='1.0' encoding='UTF-8'?>" > updates.xml
|
|
||||||
printf '%s\n' "<!-- Copyright (C) $(date +%Y) Moko Consulting -->" >> updates.xml
|
|
||||||
printf '%s\n' "<updates>" >> updates.xml
|
|
||||||
printf '%s\n' "</updates>" >> updates.xml
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update existing blocks or create missing ones
|
|
||||||
export PY_TARGETS="$TARGETS" PY_VERSION="$VERSION" PY_DATE="$(date +%Y-%m-%d)"
|
|
||||||
python3 << 'PYEOF'
|
|
||||||
import re, os
|
|
||||||
|
|
||||||
targets = os.environ["PY_TARGETS"].split(",")
|
|
||||||
version = os.environ["PY_VERSION"]
|
|
||||||
date = os.environ["PY_DATE"]
|
|
||||||
|
|
||||||
with open("updates.xml") as f:
|
|
||||||
content = f.read()
|
|
||||||
with open("/tmp/new_entry.xml") as f:
|
|
||||||
new_entry_template = f.read()
|
|
||||||
|
|
||||||
for tag in targets:
|
|
||||||
tag = tag.strip()
|
|
||||||
# Build entry with this tag's name
|
|
||||||
new_entry = re.sub(r"<tag>[^<]*</tag>", f"<tag>{tag}</tag>", new_entry_template)
|
|
||||||
|
|
||||||
# Try to find existing block (handles both single-line and multi-line <tags>)
|
|
||||||
block_pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(tag) + r"</tag>.*?</update>)"
|
|
||||||
match = re.search(block_pattern, content, re.DOTALL)
|
|
||||||
|
|
||||||
if match:
|
|
||||||
# Update in place — replace entire block
|
|
||||||
content = content.replace(match.group(1), new_entry.strip())
|
|
||||||
print(f" UPDATED: <tag>{tag}</tag> → {version}")
|
|
||||||
else:
|
|
||||||
# Create — insert before </updates>
|
|
||||||
content = content.replace("</updates>", "\n" + new_entry.strip() + "\n\n</updates>")
|
|
||||||
print(f" CREATED: <tag>{tag}</tag> → {version}")
|
|
||||||
|
|
||||||
# Clean up excessive blank lines
|
|
||||||
content = re.sub(r"\n{3,}", "\n\n", content)
|
|
||||||
|
|
||||||
with open("updates.xml", "w") as f:
|
|
||||||
f.write(content)
|
|
||||||
PYEOF
|
|
||||||
|
|
||||||
# Commit
|
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "gitea-actions[bot]"
|
|
||||||
git add updates.xml
|
|
||||||
git diff --cached --quiet || {
|
|
||||||
git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \
|
|
||||||
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
|
||||||
git push
|
|
||||||
}
|
|
||||||
|
|
||||||
# -- Sync updates.xml to main (for non-main branches) ----------------------
|
|
||||||
- name: Sync updates.xml to main
|
|
||||||
if: github.ref_name != 'main'
|
|
||||||
run: |
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
GA_TOKEN="${{ secrets.GA_TOKEN }}"
|
|
||||||
|
|
||||||
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
|
|
||||||
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
|
|
||||||
CONTENT=$(base64 -w0 updates.xml)
|
|
||||||
curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API_BASE}/contents/updates.xml" \
|
|
||||||
-d "$(python3 -c "import json; print(json.dumps({
|
|
||||||
'content': '${CONTENT}',
|
|
||||||
'sha': '${FILE_SHA}',
|
|
||||||
'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]',
|
|
||||||
'branch': 'main'
|
|
||||||
}))")" > /dev/null 2>&1 \
|
|
||||||
&& echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \
|
|
||||||
|| echo "WARNING: failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "WARNING: could not get updates.xml SHA from main" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: SFTP deploy to dev server
|
|
||||||
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
|
|
||||||
env:
|
|
||||||
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
|
|
||||||
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
|
|
||||||
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
|
||||||
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
|
|
||||||
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
|
|
||||||
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
|
|
||||||
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
|
||||||
run: |
|
|
||||||
# -- Permission check: admin or maintain role required --------
|
|
||||||
ACTOR="${{ github.actor }}"
|
|
||||||
REPO="${{ github.repository }}"
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
|
|
||||||
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
|
|
||||||
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
|
|
||||||
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
|
|
||||||
case "$PERMISSION" in
|
|
||||||
admin|maintain|write) ;;
|
|
||||||
*)
|
|
||||||
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
|
|
||||||
|
|
||||||
SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && exit 0
|
|
||||||
|
|
||||||
PORT="${DEV_PORT:-22}"
|
|
||||||
REMOTE="${DEV_PATH%/}"
|
|
||||||
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
|
|
||||||
|
|
||||||
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
|
||||||
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
|
|
||||||
if [ -n "$DEV_KEY" ]; then
|
|
||||||
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
|
|
||||||
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
|
||||||
else
|
|
||||||
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
|
|
||||||
fi
|
|
||||||
|
|
||||||
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
|
|
||||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
|
|
||||||
php /tmp/mokostandards-api/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
|
||||||
elif [ -f "/tmp/mokostandards-api/deploy/deploy-sftp.php" ]; then
|
|
||||||
php /tmp/mokostandards-api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
|
||||||
fi
|
|
||||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
|
||||||
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
@@ -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,21 +1,21 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!--
|
<!--
|
||||||
MokoStandards Repository Manifest
|
Moko Platform Repository Manifest
|
||||||
Auto-generated by cleanup script.
|
|
||||||
See: https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home
|
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">
|
<moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/1.0" schema-version="1.0">
|
||||||
<identity>
|
<identity>
|
||||||
<name>MokoJoomTOS</name>
|
<name>MokoJoomTOS</name>
|
||||||
|
<display-name>Plugin - MokoJoomTOS</display-name>
|
||||||
<org>MokoConsulting</org>
|
<org>MokoConsulting</org>
|
||||||
<description>Joomla system plugin to keep legal pages accessible during offline mode</description>
|
<description>Joomla system plugin to keep legal pages accessible during offline mode</description>
|
||||||
|
<version>04.02.01</version>
|
||||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||||
</identity>
|
</identity>
|
||||||
<governance>
|
<governance>
|
||||||
<platform>joomla</platform>
|
<platform>joomla</platform>
|
||||||
<standards-version>05.00.00</standards-version>
|
<standards-version>05.00.00</standards-version>
|
||||||
<standards-source>https://git.mokoconsulting.tech/MokoConsulting/moko-platform</standards-source>
|
<standards-source>https://git.mokoconsulting.tech/MokoConsulting/moko-platform</standards-source>
|
||||||
<last-synced>2026-05-16T17:30:00+00:00</last-synced>
|
|
||||||
</governance>
|
</governance>
|
||||||
<build>
|
<build>
|
||||||
<language>PHP</language>
|
<language>PHP</language>
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
# 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-tech/moko-platform
|
||||||
|
# PATH: /templates/workflows/universal/auto-release.yml.template
|
||||||
|
# VERSION: 05.00.00
|
||||||
|
# BRIEF: Universal build & release � detects platform from manifest.xml
|
||||||
|
#
|
||||||
|
# +========================================================================+
|
||||||
|
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
||||||
|
# +========================================================================+
|
||||||
|
# | |
|
||||||
|
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||||
|
# | |
|
||||||
|
# | Platform-specific: |
|
||||||
|
# | joomla: XML manifest, updates.xml, type-prefixed packages |
|
||||||
|
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||||
|
# | generic: README-only, no update stream |
|
||||||
|
# | |
|
||||||
|
# +========================================================================+
|
||||||
|
|
||||||
|
name: "Universal: Build & Release"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, closed]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
action:
|
||||||
|
description: 'Action to perform'
|
||||||
|
required: false
|
||||||
|
type: choice
|
||||||
|
default: release
|
||||||
|
options:
|
||||||
|
- release
|
||||||
|
- promote-rc
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
|
||||||
|
promote-rc:
|
||||||
|
name: Promote to RC
|
||||||
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Setup moko-platform tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
|
run: |
|
||||||
|
if ! command -v composer &> /dev/null; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||||
|
rm -rf /tmp/moko-platform-api
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
|
/tmp/moko-platform-api
|
||||||
|
cd /tmp/moko-platform-api
|
||||||
|
composer install --no-dev --no-interaction --quiet
|
||||||
|
|
||||||
|
- name: Rename branch to rc
|
||||||
|
run: |
|
||||||
|
php /tmp/moko-platform-api/cli/branch_rename.php \
|
||||||
|
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||||
|
--pr "${{ github.event.pull_request.number }}"
|
||||||
|
|
||||||
|
- name: Checkout rc and configure git
|
||||||
|
run: |
|
||||||
|
git fetch origin rc
|
||||||
|
git checkout rc
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
|
||||||
|
- name: Publish RC release
|
||||||
|
run: |
|
||||||
|
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||||
|
--path . --stability rc --bump minor --branch rc \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||||
|
release:
|
||||||
|
name: Build & Release Pipeline
|
||||||
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
github.event.pull_request.merged == true ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure git for bot pushes
|
||||||
|
run: |
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
|
||||||
|
- name: Check for merge conflict markers
|
||||||
|
run: |
|
||||||
|
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||||
|
if [ -n "$CONFLICTS" ]; then
|
||||||
|
echo "::error::Merge conflict markers found — aborting release"
|
||||||
|
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "No conflict markers found"
|
||||||
|
|
||||||
|
- name: Setup moko-platform tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||||
|
run: |
|
||||||
|
# Ensure PHP + Composer are available
|
||||||
|
if ! command -v composer &> /dev/null; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||||
|
rm -rf /tmp/moko-platform-api
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
|
/tmp/moko-platform-api
|
||||||
|
cd /tmp/moko-platform-api
|
||||||
|
composer install --no-dev --no-interaction --quiet
|
||||||
|
|
||||||
|
|
||||||
|
- name: "Publish stable release"
|
||||||
|
run: |
|
||||||
|
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||||
|
--path . --stability stable --bump minor --branch main \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||||
|
- name: "Step 9: Mirror release to GitHub"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
secrets.GH_MIRROR_TOKEN != ''
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php /tmp/moko-platform-api/cli/release_mirror.php \
|
||||||
|
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
||||||
|
--branch main 2>&1 || true
|
||||||
|
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
|
||||||
|
- name: "Step 10: Push main to GitHub mirror"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
secrets.GH_MIRROR_TOKEN != ''
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
|
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
|
||||||
|
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
|
||||||
|
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
|
||||||
|
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
|
||||||
|
git fetch origin main --depth=1
|
||||||
|
git push github origin/main:refs/heads/main --force 2>/dev/null \
|
||||||
|
&& echo "main branch pushed to GitHub mirror" \
|
||||||
|
|| echo "WARNING: GitHub mirror push failed"
|
||||||
|
|
||||||
|
- name: "Step 11: Delete rc branch and recreate dev from main"
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
# Delete rc branch (ephemeral — created by promote-rc)
|
||||||
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/branches/rc" 2>/dev/null \
|
||||||
|
&& echo "Deleted rc branch" || echo "rc branch not found"
|
||||||
|
|
||||||
|
# Delete dev branch
|
||||||
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
|
||||||
|
|
||||||
|
# Recreate dev from main (now includes version bump + changelog promotion)
|
||||||
|
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API_BASE}/branches" \
|
||||||
|
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
|
||||||
|
|
||||||
|
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
- name: "Step 12: Create version branch from main"
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
BRANCH_NAME="version/${VERSION}"
|
||||||
|
MAIN_SHA=$(git rev-parse HEAD)
|
||||||
|
|
||||||
|
# Delete old version branch if it exists (same version re-release)
|
||||||
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
|
||||||
|
|
||||||
|
# Create version/XX.YY.ZZ from main
|
||||||
|
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
|
||||||
|
|
||||||
|
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# -- Dolibarr post-release: Reset dev version -----------------------------
|
||||||
|
- name: "Post-release: Reset dev version"
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php /tmp/moko-platform-api/cli/version_reset_dev.php \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||||
|
--branch dev --path . 2>&1 || true
|
||||||
|
|
||||||
|
# -- Summary --------------------------------------------------------------
|
||||||
|
- name: Pipeline Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
|
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
|
||||||
|
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
|
||||||
|
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
@@ -43,9 +43,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Clone MokoStandards
|
- name: Clone MokoStandards
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
|
||||||
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --branch main --quiet \
|
git clone --depth 1 --branch main --quiet \
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
env:
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
|
||||||
run: |
|
run: |
|
||||||
if [ -f "composer.json" ]; then
|
if [ -f "composer.json" ]; then
|
||||||
composer install \
|
composer install \
|
||||||
@@ -346,7 +346,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
env:
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
|
||||||
run: |
|
run: |
|
||||||
if [ -f "composer.json" ]; then
|
if [ -f "composer.json" ]; then
|
||||||
composer install \
|
composer install \
|
||||||
@@ -391,7 +391,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
env:
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
|
||||||
run: |
|
run: |
|
||||||
if [ -f "composer.json" ]; then
|
if [ -f "composer.json" ]; then
|
||||||
composer install --no-interaction --prefer-dist --optimize-autoloader
|
composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||||
@@ -448,3 +448,20 @@ jobs:
|
|||||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
fi
|
||||||
exit $EXIT
|
exit $EXIT
|
||||||
|
|
||||||
|
pre-release:
|
||||||
|
name: Build RC Pre-Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [lint-and-validate, test]
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Trigger pre-release build
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
BRANCH: ${{ github.head_ref }}
|
||||||
|
run: |
|
||||||
|
curl -s -X POST "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
||||||
|
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Maintenance
|
# INGROUP: moko-platform.Maintenance
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /.gitea/workflows/cleanup.yml
|
# PATH: /.gitea/workflows/cleanup.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
||||||
@@ -33,17 +33,17 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.GA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
|
||||||
- name: Delete merged branches
|
- name: Delete merged branches
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "=== Merged Branch Cleanup ==="
|
echo "=== Merged Branch Cleanup ==="
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
|
||||||
# List branches via API
|
# List branches via API
|
||||||
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
BRANCHES=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
"${API}/branches?limit=50" | jq -r '.[].name')
|
"${API}/branches?limit=50" | jq -r '.[].name')
|
||||||
|
|
||||||
DELETED=0
|
DELETED=0
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
# Check if branch is merged into main
|
# Check if branch is merged into main
|
||||||
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
||||||
echo " Deleting merged branch: ${BRANCH}"
|
echo " Deleting merged branch: ${BRANCH}"
|
||||||
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
||||||
DELETED=$((DELETED + 1))
|
DELETED=$((DELETED + 1))
|
||||||
fi
|
fi
|
||||||
@@ -66,20 +66,20 @@ jobs:
|
|||||||
|
|
||||||
- name: Clean old workflow runs
|
- name: Clean old workflow runs
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "=== Workflow Run Cleanup ==="
|
echo "=== Workflow Run Cleanup ==="
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
|
||||||
# Get old completed runs
|
# Get old completed runs
|
||||||
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
RUNS=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
"${API}/actions/runs?status=completed&limit=50" | \
|
"${API}/actions/runs?status=completed&limit=50" | \
|
||||||
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
||||||
|
|
||||||
DELETED=0
|
DELETED=0
|
||||||
for RUN_ID in $RUNS; do
|
for RUN_ID in $RUNS; do
|
||||||
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
||||||
DELETED=$((DELETED + 1))
|
DELETED=$((DELETED + 1))
|
||||||
done
|
done
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Security
|
# INGROUP: moko-platform.Security
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
# PATH: /templates/workflows/gitleaks.yml.template
|
# PATH: /templates/workflows/gitleaks.yml.template
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Notifications
|
# INGROUP: moko-platform.Notifications
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /.gitea/workflows/notify.yml
|
# PATH: /.gitea/workflows/notify.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||||
@@ -18,7 +18,6 @@ on:
|
|||||||
- "Joomla Build & Release"
|
- "Joomla Build & Release"
|
||||||
- "Joomla Extension CI"
|
- "Joomla Extension CI"
|
||||||
- "Deploy"
|
- "Deploy"
|
||||||
- "Cascade Main → Dev"
|
|
||||||
types:
|
types:
|
||||||
- completed
|
- completed
|
||||||
|
|
||||||
@@ -1,224 +1,277 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.CI
|
# INGROUP: moko-platform.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
# PATH: /templates/workflows/universal/pr-check.yml.template
|
# PATH: /templates/workflows/universal/pr-check.yml.template
|
||||||
# VERSION: 05.00.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: PR gate — branch policy + code validation before merge
|
# BRIEF: PR gate — branch policy + code validation before merge
|
||||||
|
|
||||||
name: "Universal: PR Check"
|
name: "Universal: PR Check"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, reopened, edited]
|
types: [opened, synchronize, reopened, edited]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ── Branch Policy ──────────────────────────────────────────────────────
|
# ── Branch Policy ──────────────────────────────────────────────────────
|
||||||
branch-policy:
|
branch-policy:
|
||||||
name: Branch Policy
|
name: Branch Policy
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check branch merge target
|
- name: Check branch merge target
|
||||||
run: |
|
run: |
|
||||||
HEAD="${{ github.head_ref }}"
|
HEAD="${{ github.head_ref }}"
|
||||||
BASE="${{ github.base_ref }}"
|
BASE="${{ github.base_ref }}"
|
||||||
|
|
||||||
echo "PR: ${HEAD} → ${BASE}"
|
echo "PR: ${HEAD} → ${BASE}"
|
||||||
|
|
||||||
ALLOWED=true
|
ALLOWED=true
|
||||||
REASON=""
|
REASON=""
|
||||||
|
|
||||||
case "$HEAD" in
|
case "$HEAD" in
|
||||||
feature/*|feat/*)
|
feature/*|feat/*)
|
||||||
if [ "$BASE" != "dev" ]; then
|
if [ "$BASE" != "dev" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Feature branches must target 'dev', not '${BASE}'"
|
REASON="Feature branches must target 'dev', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
fix/*|bugfix/*)
|
fix/*|bugfix/*)
|
||||||
if [ "$BASE" != "dev" ]; then
|
if [ "$BASE" != "dev" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Fix branches must target 'dev', not '${BASE}'"
|
REASON="Fix branches must target 'dev', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
hotfix/*)
|
patch/*)
|
||||||
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
|
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
|
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
alpha/*|beta/*)
|
hotfix/*)
|
||||||
if [ "$BASE" != "dev" ]; then
|
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Pre-release branches must target 'dev', not '${BASE}'"
|
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
rc/*)
|
rc)
|
||||||
if [ "$BASE" != "main" ]; then
|
if [ "$BASE" != "main" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Release candidate branches must target 'main', not '${BASE}'"
|
REASON="RC branch can only merge into 'main', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
dev)
|
dev)
|
||||||
if [ "$BASE" != "main" ]; then
|
if [ "$BASE" != "main" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Dev branch can only merge into 'main', not '${BASE}'"
|
REASON="Dev branch can only merge into 'main', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [ "$ALLOWED" = false ]; then
|
if [ "$ALLOWED" = false ]; then
|
||||||
echo "::error::${REASON}"
|
echo "::error::${REASON}"
|
||||||
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
|
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
|
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
|
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
||||||
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# ── Code Validation ────────────────────────────────────────────────────
|
# ── Code Validation ────────────────────────────────────────────────────
|
||||||
validate:
|
validate:
|
||||||
name: Validate PR
|
name: Validate PR
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Detect platform
|
- name: Check for merge conflict markers
|
||||||
id: platform
|
run: |
|
||||||
run: |
|
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||||
# Parse manifest for platform detection
|
if [ -n "$CONFLICTS" ]; then
|
||||||
PLATFORM=$(php /tmp/mokostandards-api/cli/manifest_read.php --path . --field platform 2>/dev/null)
|
echo "::error::Merge conflict markers found in source files"
|
||||||
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||||
- name: Setup PHP
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
exit 1
|
||||||
run: |
|
fi
|
||||||
if ! command -v php &> /dev/null; then
|
echo "No conflict markers found"
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
- name: Detect platform
|
||||||
fi
|
id: platform
|
||||||
|
run: |
|
||||||
- name: PHP syntax check
|
# Read platform from XML manifest (<platform> tag) or plain text fallback
|
||||||
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
|
||||||
run: |
|
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
|
||||||
ERRORS=0
|
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
||||||
while IFS= read -r -d '' file; do
|
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||||
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
|
||||||
ERRORS=$((ERRORS + 1))
|
- name: Setup PHP
|
||||||
fi
|
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
||||||
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
run: |
|
||||||
echo "PHP lint: ${ERRORS} error(s)"
|
if ! command -v php &> /dev/null; then
|
||||||
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
||||||
- name: Validate platform manifest
|
fi
|
||||||
run: |
|
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
- name: PHP syntax check
|
||||||
case "$PLATFORM" in
|
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
||||||
joomla)
|
run: |
|
||||||
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
ERRORS=0
|
||||||
if [ -z "$MANIFEST" ]; then
|
while IFS= read -r -d '' file; do
|
||||||
echo "::warning::No Joomla manifest found (WaaS site)"
|
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
||||||
exit 0
|
ERRORS=$((ERRORS + 1))
|
||||||
fi
|
fi
|
||||||
echo "Manifest: ${MANIFEST}"
|
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
||||||
if command -v php &> /dev/null; then
|
echo "PHP lint: ${ERRORS} error(s)"
|
||||||
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
|
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
|
||||||
fi
|
|
||||||
for ELEMENT in name version description; do
|
- name: Validate platform manifest
|
||||||
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
|
run: |
|
||||||
done
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
echo "Joomla manifest valid"
|
case "$PLATFORM" in
|
||||||
;;
|
joomla)
|
||||||
dolibarr)
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -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)
|
if [ -z "$MANIFEST" ]; then
|
||||||
if [ -z "$MOD_FILE" ]; then
|
echo "::warning::No Joomla manifest found (WaaS site)"
|
||||||
echo "::error::No mod*.class.php found"
|
exit 0
|
||||||
exit 1
|
fi
|
||||||
fi
|
echo "Manifest: ${MANIFEST}"
|
||||||
echo "Dolibarr module: ${MOD_FILE}"
|
if command -v php &> /dev/null; then
|
||||||
;;
|
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
|
||||||
*)
|
fi
|
||||||
echo "Generic platform — no manifest validation"
|
for ELEMENT in name version description; do
|
||||||
;;
|
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
|
||||||
esac
|
done
|
||||||
|
echo "Joomla manifest valid"
|
||||||
- name: Check update stream format
|
;;
|
||||||
run: |
|
dolibarr)
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
|
||||||
case "$PLATFORM" in
|
if [ -z "$MOD_FILE" ]; then
|
||||||
joomla)
|
echo "::error::No mod*.class.php found"
|
||||||
if [ -f "updates.xml" ]; then
|
exit 1
|
||||||
if command -v php &> /dev/null; then
|
fi
|
||||||
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
|
echo "Dolibarr module: ${MOD_FILE}"
|
||||||
fi
|
;;
|
||||||
echo "updates.xml valid"
|
*)
|
||||||
fi
|
echo "Generic platform — no manifest validation"
|
||||||
;;
|
;;
|
||||||
dolibarr)
|
esac
|
||||||
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
|
|
||||||
;;
|
- name: Check update stream format
|
||||||
esac
|
run: |
|
||||||
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
- name: Verify package source
|
case "$PLATFORM" in
|
||||||
run: |
|
joomla)
|
||||||
SOURCE_DIR="src"
|
if [ -f "updates.xml" ]; then
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
if command -v php &> /dev/null; then
|
||||||
if [ ! -d "$SOURCE_DIR" ]; then
|
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
|
||||||
echo "::warning::No src/ or htdocs/ directory"
|
fi
|
||||||
exit 0
|
echo "updates.xml valid"
|
||||||
fi
|
fi
|
||||||
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
|
;;
|
||||||
echo "Source: ${FILE_COUNT} files"
|
dolibarr)
|
||||||
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
|
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
|
||||||
|
;;
|
||||||
# ── Changelog Gate ────────────────────────────────────────────────────
|
esac
|
||||||
changelog:
|
|
||||||
name: Changelog Updated
|
- name: Check changelog has unreleased entry
|
||||||
runs-on: ubuntu-latest
|
run: |
|
||||||
if: github.base_ref == 'main'
|
if [ ! -f "CHANGELOG.md" ]; then
|
||||||
steps:
|
echo "::warning::No CHANGELOG.md found"
|
||||||
- uses: actions/checkout@v4
|
exit 0
|
||||||
with:
|
fi
|
||||||
fetch-depth: 0
|
# Check for content under [Unreleased] section
|
||||||
|
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
|
||||||
- name: Check CHANGELOG.md was updated
|
echo "::error::CHANGELOG.md missing [Unreleased] section"
|
||||||
run: |
|
exit 1
|
||||||
BASE="${{ github.event.pull_request.base.sha }}"
|
fi
|
||||||
HEAD="${{ github.event.pull_request.head.sha }}"
|
# Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
|
||||||
|
UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
|
||||||
if git diff --name-only "$BASE" "$HEAD" | grep -q "^CHANGELOG.md$"; then
|
if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
|
||||||
echo "CHANGELOG.md updated"
|
echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
|
||||||
else
|
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
|
||||||
# Allow [skip changelog] in PR title or body
|
echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
|
||||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
|
||||||
PR_BODY="${{ github.event.pull_request.body }}"
|
exit 1
|
||||||
if echo "$PR_TITLE $PR_BODY" | grep -qi "\[skip changelog\]"; then
|
fi
|
||||||
echo "::warning::Changelog skip requested via [skip changelog]"
|
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
|
||||||
exit 0
|
|
||||||
fi
|
- name: Verify package source
|
||||||
echo "::error::CHANGELOG.md must be updated before merging to main. Add [skip changelog] to the PR title to bypass."
|
run: |
|
||||||
exit 1
|
SOURCE_DIR="src"
|
||||||
fi
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
if [ ! -d "$SOURCE_DIR" ]; then
|
||||||
|
echo "::warning::No src/ or htdocs/ directory"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
|
||||||
|
echo "Source: ${FILE_COUNT} files"
|
||||||
|
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
|
||||||
|
|
||||||
|
# ── Pre-Release RC Build ─────────────────────────────────────────────────
|
||||||
|
pre-release:
|
||||||
|
name: Build RC Package
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [branch-policy, validate]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Trigger RC pre-release
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
BRANCH: ${{ github.head_ref }}
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
run: |
|
||||||
|
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
||||||
|
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# ── Issue Reporter ──────────────────────────────────────────────────────
|
||||||
|
report-issues:
|
||||||
|
name: Report Issues
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [branch-policy, validate]
|
||||||
|
if: >-
|
||||||
|
always() &&
|
||||||
|
needs.validate.result == 'failure'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
sparse-checkout: automation/ci-issue-reporter.sh
|
||||||
|
sparse-checkout-cone-mode: false
|
||||||
|
|
||||||
|
- name: "File issue for PR validation failure"
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
run: |
|
||||||
|
chmod +x automation/ci-issue-reporter.sh
|
||||||
|
./automation/ci-issue-reporter.sh \
|
||||||
|
--gate "PR Validation" \
|
||||||
|
--workflow "PR Check" \
|
||||||
|
--severity error \
|
||||||
|
--details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoStandards.Security
|
# INGROUP: moko-platform.Security
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /.gitea/workflows/security-audit.yml
|
# PATH: /.gitea/workflows/security-audit.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
||||||
@@ -80,3 +80,19 @@ jobs:
|
|||||||
-H "Priority: high" \
|
-H "Priority: high" \
|
||||||
-d "Security audit found vulnerabilities. Review dependency updates." \
|
-d "Security audit found vulnerabilities. Review dependency updates." \
|
||||||
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
||||||
|
|
||||||
|
|
||||||
|
- name: Joomla version audit
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [ -f "monitoring/joomla-version-audit.php" ] && [ -n "$JOOMLA_SITES" ]; then
|
||||||
|
echo "$JOOMLA_SITES" > /tmp/sites.json
|
||||||
|
php monitoring/joomla-version-audit.php --sites /tmp/sites.json || true
|
||||||
|
echo "### Joomla Version Audit" >> $GITHUB_STEP_SUMMARY
|
||||||
|
rm -f /tmp/sites.json
|
||||||
|
else
|
||||||
|
echo "Joomla audit skipped (no script or JOOMLA_SITES_JSON not configured)"
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
JOOMLA_SITES: ${{ vars.JOOMLA_SITES_JSON }}
|
||||||
|
|
||||||
+7
-190
@@ -1,194 +1,11 @@
|
|||||||
<!--
|
|
||||||
|
|
||||||
## [Unreleased]
|
## [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.
|
### Changed
|
||||||
|
- Migrated all workflow and template paths from `.github/` to `.mokogitea/`
|
||||||
SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
|
- Template source paths updated: `templates/gitea/` to `templates/mokogitea/`
|
||||||
|
- HCL definition files removed -- Template repos are now the canonical source
|
||||||
This program is free software; you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation; either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
(./LICENSE).
|
|
||||||
|
|
||||||
# FILE INFORMATION
|
|
||||||
DEFGROUP: MokoJoomTOS
|
|
||||||
INGROUP: plg_system_mokojoomtos
|
|
||||||
REPO: https://github.com/mokoconsulting-tech/MokoJoomTOS
|
|
||||||
VERSION: 04.00.00
|
|
||||||
PATH: ./CHANGELOG.md
|
|
||||||
BRIEF: Version history and release notes
|
|
||||||
-->
|
|
||||||
|
|
||||||
# Changelog
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
## [04.00.00] - 2026-05-16
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- `branch-cleanup.yml`: auto-delete merged feature branches after PR merge
|
||||||
- Multi-select support: configure multiple menu items to remain accessible during offline mode
|
|
||||||
- `services/provider.php` for Joomla 5 dependency injection container registration
|
|
||||||
- Gitea-hosted update server URL in plugin manifest
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- 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
|
|
||||||
|
|
||||||
- GitHub update server references (fully migrated to Gitea)
|
|
||||||
- Legacy `src/plugins/` directory
|
|
||||||
|
|
||||||
## [03.09.00] - 2026-05-16
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Installation script now resolves Uncategorised category ID dynamically (no longer assumes ID 2)
|
|
||||||
- Installation script now resolves `com_content` component ID dynamically (removed hardcoded fallback 22)
|
|
||||||
- All `Exception` catches qualified with backslash for namespace safety
|
|
||||||
- Added missing `PLG_SYSTEM_MOKOJOOMTOS_ERROR_LOADING_MENU_ITEMS` language key to all locale files
|
|
||||||
- Standardized help text URL across all locale files
|
|
||||||
- Articles created during install are now owned by the installing admin user (not user ID 0)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Added Gitea update server URL to plugin manifest (`updates.xml` on `main`)
|
|
||||||
- Removed obsolete `src/plugins/` legacy directory
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
## 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)
|
|
||||||
|
|
||||||
[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
|
|
||||||
|
|||||||
@@ -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)
|
├── docs/ # Detailed documentation (currently minimal with index.md)
|
||||||
├── scripts/ # Build and utility scripts (validate/, package scripts)
|
├── scripts/ # Build and utility scripts (validate/, package scripts)
|
||||||
├── src/ # Plugin source code at root level (NOT nested under plugins/)
|
├── 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
|
2. **SECURITY.md** - Security policy and vulnerability reporting procedures
|
||||||
3. **CODE_OF_CONDUCT.md** - Community standards and behavior expectations
|
3. **CODE_OF_CONDUCT.md** - Community standards and behavior expectations
|
||||||
4. **CHANGELOG.md** - Version history following Keep a Changelog format
|
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.
|
Currently no `docs/policy/` directory exists - all policy is in root-level markdown files.
|
||||||
|
|||||||
+141
-108
@@ -1,128 +1,161 @@
|
|||||||
<!--
|
# Contributing to Moko Consulting Projects
|
||||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
|
|
||||||
This file is part of a Moko Consulting project.
|
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
|
||||||
|
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
## Branching Workflow
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License (./LICENSE).
|
|
||||||
|
|
||||||
# FILE INFORMATION
|
|
||||||
DEFGROUP: {{DEFGROUP}}
|
|
||||||
INGROUP: Project.Documentation
|
|
||||||
REPO: https://github.com/mokoconsulting-tech/MokoJoomTOS
|
|
||||||
VERSION: 04.04.00
|
|
||||||
PATH: ./CONTRIBUTING.md
|
|
||||||
BRIEF: How to contribute; branch strategy, commit conventions, PR workflow, and release pipeline
|
|
||||||
-->
|
|
||||||
|
|
||||||
# Contributing
|
|
||||||
|
|
||||||
Thank you for your interest in contributing to **MokoJoomTOS**!
|
|
||||||
|
|
||||||
This repository is governed by **[MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards)** — the authoritative source of coding standards, workflows, and policies for all Moko Consulting repositories.
|
|
||||||
|
|
||||||
## Branch Strategy
|
|
||||||
|
|
||||||
| Branch | Purpose | Deploys To |
|
|
||||||
|--------|---------|------------|
|
|
||||||
| `main` | Bleeding edge — all development merges here | CI only |
|
|
||||||
| `dev/XX.YY.ZZ` | Feature development | Dev server (version: "development") |
|
|
||||||
| `version/XX.YY` | Stable frozen snapshot | Demo + RS servers |
|
|
||||||
|
|
||||||
### Development Workflow
|
|
||||||
|
|
||||||
```
|
```
|
||||||
1. Create branch: git checkout -b dev/XX.YY.ZZ/my-feature
|
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
|
||||||
2. Develop + test (dev server auto-deploys on push)
|
|
||||||
3. Open PR → main (squash merge only)
|
|
||||||
4. Auto-release (version branch + tag + GitHub Release created automatically)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Branch Naming
|
### Step by step
|
||||||
|
|
||||||
| Prefix | Use |
|
1. **Create a feature branch** from `dev`:
|
||||||
|--------|-----|
|
```bash
|
||||||
| `dev/XX.YY.ZZ` | Feature development (e.g., `dev/02.00.00/add-extrafields`) |
|
git checkout dev && git pull
|
||||||
| `version/XX.YY` | Stable release (auto-created, never manually pushed) |
|
git checkout -b feature/my-change
|
||||||
| `chore/` | Automated sync branches (managed by MokoStandards) |
|
```
|
||||||
|
|
||||||
> **Never use** `feature/`, `hotfix/`, or `release/` prefixes — they are not part of the MokoStandards branch strategy.
|
2. **Work and commit** on your feature branch. Push to origin.
|
||||||
|
|
||||||
## Commit Conventions
|
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
|
||||||
|
|
||||||
Use [conventional commits](https://www.conventionalcommits.org/):
|
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
|
||||||
|
- This automatically renames the source branch to `rc` (release candidate)
|
||||||
|
- An RC pre-release is built and uploaded
|
||||||
|
|
||||||
|
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
|
||||||
|
- Rename `dev` to `alpha` for early testing → alpha pre-release is built
|
||||||
|
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
|
||||||
|
- When the draft PR is created, the branch is renamed to `rc`
|
||||||
|
|
||||||
|
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
|
||||||
|
|
||||||
|
7. **Merging to main** triggers the stable release pipeline:
|
||||||
|
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
|
||||||
|
- Stability suffix stripped (clean version)
|
||||||
|
- Gitea release created with ZIP/tar.gz packages
|
||||||
|
- `updates.xml` updated (Joomla extensions)
|
||||||
|
- `dev` branch recreated from `main`
|
||||||
|
|
||||||
|
### Branch summary
|
||||||
|
|
||||||
|
| Branch | Purpose | Created by |
|
||||||
|
|--------|---------|-----------|
|
||||||
|
| `feature/*` | New features and fixes | Developer |
|
||||||
|
| `dev` | Integration branch | Auto-recreated after release |
|
||||||
|
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
|
||||||
|
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
|
||||||
|
| `rc` | Release candidate | Auto-renamed on draft PR to main |
|
||||||
|
| `main` | Stable releases | Protected, merge only |
|
||||||
|
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
|
||||||
|
|
||||||
|
### Protected branches
|
||||||
|
|
||||||
|
| Branch | Direct push | Merge via |
|
||||||
|
|--------|------------|-----------|
|
||||||
|
| `main` | Blocked (CI bot whitelisted) | PR merge only |
|
||||||
|
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
|
||||||
|
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
|
||||||
|
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
|
||||||
|
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
|
||||||
|
| `feature/*` | Open | N/A (source branch) |
|
||||||
|
|
||||||
|
## Version Policy
|
||||||
|
|
||||||
|
### Format
|
||||||
|
|
||||||
|
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
|
||||||
|
|
||||||
|
- **XX** — Major version (breaking changes)
|
||||||
|
- **YY** — Minor version (new features, bumped on release to main)
|
||||||
|
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
|
||||||
|
|
||||||
|
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
|
||||||
|
|
||||||
|
### Stability suffixes
|
||||||
|
|
||||||
|
Each branch appends a suffix to indicate stability:
|
||||||
|
|
||||||
|
| Branch | Suffix | Example |
|
||||||
|
|--------|--------|---------|
|
||||||
|
| `main` | (none) | `02.09.00` |
|
||||||
|
| `dev` | `-dev` | `02.09.01-dev` |
|
||||||
|
| `feature/*` | `-dev` | `02.09.01-dev` |
|
||||||
|
| `alpha` | `-alpha` | `02.09.01-alpha` |
|
||||||
|
| `beta` | `-beta` | `02.09.01-beta` |
|
||||||
|
| `rc` | `-rc` | `02.09.01-rc` |
|
||||||
|
|
||||||
|
### Auto version bump
|
||||||
|
|
||||||
|
On every push to `dev`, `feature/*`, or `patch/*`:
|
||||||
|
|
||||||
|
1. Patch version incremented
|
||||||
|
2. Stability suffix `-dev` applied
|
||||||
|
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
|
||||||
|
4. Commit created with `[skip ci]` to avoid loops
|
||||||
|
|
||||||
|
### Release version flow
|
||||||
|
|
||||||
|
Version bumps happen at specific release events:
|
||||||
|
|
||||||
|
| Event | Bump | Example |
|
||||||
|
|-------|------|---------|
|
||||||
|
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
|
||||||
|
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
|
||||||
|
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
|
||||||
|
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
|
||||||
|
|
||||||
|
### Release stream copies
|
||||||
|
|
||||||
|
When a higher-stability release is published, copies are created for all lesser streams with the same base version:
|
||||||
|
|
||||||
|
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
|
||||||
|
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
|
||||||
|
|
||||||
|
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
|
||||||
|
|
||||||
|
### Version files
|
||||||
|
|
||||||
|
The version tools update all files containing version stamps:
|
||||||
|
|
||||||
|
- `.mokogitea/manifest.xml` (canonical source)
|
||||||
|
- Joomla XML manifests (`<version>` tag)
|
||||||
|
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
|
||||||
|
- `package.json`, `pyproject.toml`
|
||||||
|
- Any text file with a `VERSION: XX.YY.ZZ` label
|
||||||
|
|
||||||
|
Files synced from other repos (with a `# REPO:` header) are not touched.
|
||||||
|
|
||||||
|
## Code Standards
|
||||||
|
|
||||||
|
- **PHP**: PSR-12, tabs for indentation
|
||||||
|
- **Copyright**: all files must include the Moko Consulting copyright header
|
||||||
|
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
|
||||||
|
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
|
||||||
|
|
||||||
|
## Commit Messages
|
||||||
|
|
||||||
|
Use conventional commit format:
|
||||||
|
|
||||||
```
|
```
|
||||||
feat(scope): add new extrafield for invoice tracking
|
type(scope): short description
|
||||||
fix(sql): correct column type in llx_mytable
|
|
||||||
docs(readme): update installation instructions
|
Optional body with context.
|
||||||
chore(deps): bump enterprise library to 04.02.30
|
|
||||||
|
Authored-by: Moko Consulting
|
||||||
```
|
```
|
||||||
|
|
||||||
**Valid types:** `feat` | `fix` | `docs` | `chore` | `ci` | `refactor` | `style` | `test` | `perf` | `revert` | `build`
|
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
|
||||||
|
|
||||||
## Pull Request Workflow
|
Special flags in commit messages:
|
||||||
|
- `[skip ci]` — skip all CI workflows
|
||||||
|
- `[skip bump]` — skip auto version bump only
|
||||||
|
|
||||||
1. **Branch** from `main` using `dev/XX.YY.ZZ/description` format
|
## Reporting Issues
|
||||||
2. **Bump** the patch version in `README.md` before opening the PR
|
|
||||||
3. **Title** must be a valid conventional commit subject line
|
|
||||||
4. **Target** `main` — squash merge only (merge commits are disabled)
|
|
||||||
5. **CI checks** must pass before merge
|
|
||||||
|
|
||||||
### What Happens on Merge
|
Use the repository's issue tracker with the appropriate template.
|
||||||
|
|
||||||
When your PR is merged to `main`, these workflows run automatically:
|
|
||||||
|
|
||||||
1. **sync-version-on-merge** — auto-bumps patch version, propagates to all file headers
|
|
||||||
2. **auto-release** — creates `version/XX.YY` branch, git tag, and GitHub Release
|
|
||||||
3. **deploy-demo / deploy-rs** — deploys to demo and RS servers (if `src/**` changed)
|
|
||||||
|
|
||||||
## Coding Standards
|
|
||||||
|
|
||||||
All contributions must follow [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards):
|
|
||||||
|
|
||||||
| Standard | Reference |
|
|
||||||
|----------|-----------|
|
|
||||||
| Coding Style | [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) |
|
|
||||||
| File Headers | [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) |
|
|
||||||
| Branching | [branch-release-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branch-release-strategy.md) |
|
|
||||||
| Merge Strategy | [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) |
|
|
||||||
| Scripting | [scripting-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/scripting-standards.md) |
|
|
||||||
| Build & Release | [build-release.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/workflows/build-release.md) |
|
|
||||||
|
|
||||||
## PR Checklist
|
|
||||||
|
|
||||||
- [ ] Branch named `dev/XX.YY.ZZ/description`
|
|
||||||
- [ ] Patch version bumped in `README.md`
|
|
||||||
- [ ] Conventional commit format for PR title
|
|
||||||
- [ ] All new files have FILE INFORMATION headers
|
|
||||||
- [ ] `declare(strict_types=1)` in all PHP files
|
|
||||||
- [ ] PHPDoc on all public methods
|
|
||||||
- [ ] Tests pass
|
|
||||||
- [ ] CHANGELOG.md updated
|
|
||||||
- [ ] No secrets, tokens, or credentials committed
|
|
||||||
|
|
||||||
## Custom Workflows
|
|
||||||
|
|
||||||
Place repo-specific workflows in `.github/workflows/custom/` — they are **never overwritten or deleted** by MokoStandards sync:
|
|
||||||
|
|
||||||
```
|
|
||||||
.github/workflows/
|
|
||||||
├── deploy-dev.yml ← Synced from MokoStandards
|
|
||||||
├── auto-release.yml ← Synced from MokoStandards
|
|
||||||
└── custom/ ← Your custom workflows (safe)
|
|
||||||
└── my-custom-ci.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
By contributing, you agree that your contributions will be licensed under the [GPL-3.0-or-later](LICENSE) license.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*This file is synced from [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). Do not edit directly — changes will be overwritten on the next sync.*
|
*Moko Consulting <hello@mokoconsulting.tech>*
|
||||||
|
|||||||
@@ -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.
|
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 |
|
| Field | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Author** | [Moko Consulting](https://mokoconsulting.tech) |
|
| **Author** | [Moko Consulting](https://mokoconsulting.tech) |
|
||||||
| **License** | GPL-3.0-or-later |
|
| **License** | GPL-3.0-or-later |
|
||||||
| **Platform** | [Gitea](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS) |
|
| **Platform** | [Gitea](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS) |
|
||||||
| **Version** | 04.01.00 |
|
| **Version** | 04.02.00 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================================
|
||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Automation.CI
|
||||||
|
# INGROUP: moko-platform.Automation
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /automation/ci-issue-reporter.sh
|
||||||
|
# VERSION: 09.23.00
|
||||||
|
# BRIEF: Creates or updates a Gitea issue when a CI gate fails.
|
||||||
|
# Deduplicates by searching open issues with the "ci-auto" label
|
||||||
|
# whose title matches the gate. If a matching issue exists, a comment
|
||||||
|
# is appended instead of opening a duplicate.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── Defaults ────────────────────────────────────────────────────────────────
|
||||||
|
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
|
||||||
|
GITEA_TOKEN="${GITEA_TOKEN:-}"
|
||||||
|
REPO="${GITHUB_REPOSITORY:-}"
|
||||||
|
RUN_URL="${GITHUB_SERVER_URL:-${GITEA_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}"
|
||||||
|
LABEL_NAME="ci-auto"
|
||||||
|
LABEL_COLOR="#e11d48"
|
||||||
|
|
||||||
|
GATE=""
|
||||||
|
DETAILS=""
|
||||||
|
SEVERITY="error"
|
||||||
|
WORKFLOW=""
|
||||||
|
|
||||||
|
# ── Parse arguments ─────────────────────────────────────────────────────────
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: ci-issue-reporter.sh --gate NAME --details TEXT [OPTIONS]
|
||||||
|
|
||||||
|
Required:
|
||||||
|
--gate CI gate name (e.g. "Code Quality", "Self-Health")
|
||||||
|
--details Human-readable failure description
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
--severity "error" (default) or "warning"
|
||||||
|
--workflow Workflow name for the issue title
|
||||||
|
--repo owner/repo (default: \$GITHUB_REPOSITORY)
|
||||||
|
--run-url URL to the CI run (auto-detected from env)
|
||||||
|
--token Gitea API token (default: \$GITEA_TOKEN)
|
||||||
|
--url Gitea base URL (default: \$GITEA_URL)
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--gate) GATE="$2"; shift 2 ;;
|
||||||
|
--details) DETAILS="$2"; shift 2 ;;
|
||||||
|
--severity) SEVERITY="$2"; shift 2 ;;
|
||||||
|
--workflow) WORKFLOW="$2"; shift 2 ;;
|
||||||
|
--repo) REPO="$2"; shift 2 ;;
|
||||||
|
--run-url) RUN_URL="$2"; shift 2 ;;
|
||||||
|
--token) GITEA_TOKEN="$2"; shift 2 ;;
|
||||||
|
--url) GITEA_URL="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage ;;
|
||||||
|
*) echo "Unknown option: $1"; usage ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -z "$GATE" ]] && { echo "ERROR: --gate is required"; usage; }
|
||||||
|
[[ -z "$DETAILS" ]] && { echo "ERROR: --details is required"; usage; }
|
||||||
|
[[ -z "$GITEA_TOKEN" ]] && { echo "ERROR: GITEA_TOKEN not set"; exit 1; }
|
||||||
|
[[ -z "$REPO" ]] && { echo "ERROR: GITHUB_REPOSITORY not set"; exit 1; }
|
||||||
|
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${REPO}"
|
||||||
|
|
||||||
|
# ── Build title ─────────────────────────────────────────────────────────────
|
||||||
|
if [[ -n "$WORKFLOW" ]]; then
|
||||||
|
TITLE="[CI] ${WORKFLOW}: ${GATE} failed"
|
||||||
|
else
|
||||||
|
TITLE="[CI] ${GATE} failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Ensure label exists ─────────────────────────────────────────────────────
|
||||||
|
ensure_label() {
|
||||||
|
local exists
|
||||||
|
exists=$(curl -sf -o /dev/null -w '%{http_code}' \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/labels" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [[ "$exists" == "200" ]]; then
|
||||||
|
# Check if label already exists
|
||||||
|
local found
|
||||||
|
found=$(curl -sf \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/labels" 2>/dev/null \
|
||||||
|
| grep -o "\"name\":\"${LABEL_NAME}\"" || true)
|
||||||
|
|
||||||
|
if [[ -z "$found" ]]; then
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/labels" \
|
||||||
|
-d "{\"name\":\"${LABEL_NAME}\",\"color\":\"${LABEL_COLOR}\",\"description\":\"Auto-created by CI issue reporter\"}" \
|
||||||
|
> /dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Search for existing open issue ──────────────────────────────────────────
|
||||||
|
find_existing_issue() {
|
||||||
|
# URL-encode the gate name for the query
|
||||||
|
local query
|
||||||
|
query=$(printf '%s' "[CI] ${GATE}" | sed 's/ /%20/g; s/\[/%5B/g; s/\]/%5D/g')
|
||||||
|
|
||||||
|
local response
|
||||||
|
response=$(curl -sf \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/issues?type=issues&state=open&labels=${LABEL_NAME}&q=${query}&limit=5" \
|
||||||
|
2>/dev/null || echo "[]")
|
||||||
|
|
||||||
|
# Extract the first matching issue number
|
||||||
|
echo "$response" \
|
||||||
|
| grep -oP '"number":\s*\K[0-9]+' \
|
||||||
|
| head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Build issue body ────────────────────────────────────────────────────────
|
||||||
|
build_body() {
|
||||||
|
local severity_badge
|
||||||
|
if [[ "$SEVERITY" == "error" ]]; then
|
||||||
|
severity_badge="**Severity:** Error"
|
||||||
|
else
|
||||||
|
severity_badge="**Severity:** Warning"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<BODY
|
||||||
|
## CI Gate Failure: ${GATE}
|
||||||
|
|
||||||
|
${severity_badge}
|
||||||
|
**Workflow:** ${WORKFLOW:-unknown}
|
||||||
|
**Branch:** ${GITHUB_REF_NAME:-unknown}
|
||||||
|
**Commit:** \`${GITHUB_SHA:0:8}\`
|
||||||
|
**Run:** [View CI run](${RUN_URL})
|
||||||
|
|
||||||
|
### Details
|
||||||
|
|
||||||
|
${DETAILS}
|
||||||
|
|
||||||
|
### Resolution
|
||||||
|
|
||||||
|
Fix the issue described above and push a new commit. This issue will be closed automatically when the gate passes, or can be closed manually.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Auto-created by [ci-issue-reporter](${GITEA_URL}/${REPO}/src/branch/main/automation/ci-issue-reporter.sh)*
|
||||||
|
BODY
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Build comment body (for existing issues) ────────────────────────────────
|
||||||
|
build_comment() {
|
||||||
|
cat <<COMMENT
|
||||||
|
### CI failure recurrence
|
||||||
|
|
||||||
|
**Branch:** ${GITHUB_REF_NAME:-unknown}
|
||||||
|
**Commit:** \`${GITHUB_SHA:0:8}\`
|
||||||
|
**Run:** [View CI run](${RUN_URL})
|
||||||
|
|
||||||
|
${DETAILS}
|
||||||
|
COMMENT
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Main ────────────────────────────────────────────────────────────────────
|
||||||
|
ensure_label
|
||||||
|
|
||||||
|
EXISTING=$(find_existing_issue)
|
||||||
|
|
||||||
|
if [[ -n "$EXISTING" ]]; then
|
||||||
|
# Append comment to existing issue
|
||||||
|
COMMENT_BODY=$(build_comment)
|
||||||
|
COMMENT_JSON=$(printf '%s' "$COMMENT_BODY" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
print(json.dumps({'body': sys.stdin.read()}))" 2>/dev/null)
|
||||||
|
|
||||||
|
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/issues/${EXISTING}/comments" \
|
||||||
|
-d "${COMMENT_JSON}" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [[ "$HTTP" == "201" ]]; then
|
||||||
|
echo "Commented on existing issue #${EXISTING}"
|
||||||
|
else
|
||||||
|
echo "WARNING: Failed to comment on issue #${EXISTING} (HTTP ${HTTP})"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Create new issue
|
||||||
|
ISSUE_BODY=$(build_body)
|
||||||
|
ISSUE_JSON=$(python3 -c "
|
||||||
|
import sys, json
|
||||||
|
body = sys.stdin.read()
|
||||||
|
print(json.dumps({
|
||||||
|
'title': sys.argv[1],
|
||||||
|
'body': body,
|
||||||
|
'labels': []
|
||||||
|
}))" "$TITLE" <<< "$ISSUE_BODY" 2>/dev/null)
|
||||||
|
|
||||||
|
# Create the issue
|
||||||
|
RESPONSE=$(curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/issues" \
|
||||||
|
-d "${ISSUE_JSON}" 2>/dev/null || echo "{}")
|
||||||
|
|
||||||
|
ISSUE_NUM=$(echo "$RESPONSE" | grep -oP '"number":\s*\K[0-9]+' | head -1)
|
||||||
|
|
||||||
|
if [[ -n "$ISSUE_NUM" ]]; then
|
||||||
|
# Apply label (separate call — more reliable across Gitea versions)
|
||||||
|
LABEL_ID=$(curl -sf \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/labels" 2>/dev/null \
|
||||||
|
| grep -oP "\"id\":\s*\K[0-9]+(?=[^}]*\"name\":\s*\"${LABEL_NAME}\")" \
|
||||||
|
| head -1 || true)
|
||||||
|
|
||||||
|
if [[ -n "$LABEL_ID" ]]; then
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/issues/${ISSUE_NUM}/labels" \
|
||||||
|
-d "{\"labels\":[${LABEL_ID}]}" \
|
||||||
|
> /dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Created issue #${ISSUE_NUM}: ${TITLE}"
|
||||||
|
else
|
||||||
|
echo "WARNING: Failed to create issue"
|
||||||
|
echo "Response: ${RESPONSE}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
@@ -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_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_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
|
; Help
|
||||||
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
|
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)."
|
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
|
; Errors
|
||||||
PLG_SYSTEM_MOKOJOOMTOS_ERROR_LOADING_MENU_ITEMS="Error loading menu items: %s"
|
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_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_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
|
; Help
|
||||||
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
|
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)."
|
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
|
; Errors
|
||||||
PLG_SYSTEM_MOKOJOOMTOS_ERROR_LOADING_MENU_ITEMS="Error loading menu items: %s"
|
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_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_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
|
; Help
|
||||||
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
|
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)."
|
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_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_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
|
; Help
|
||||||
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
|
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)."
|
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;
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
use Joomla\CMS\Plugin\CMSPlugin;
|
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
|
* This file is required by Joomla's plugin loader (<filename plugin="mokojoomtos">)
|
||||||
* is in offline mode.
|
* 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
|
* @since 1.0.0
|
||||||
*/
|
*/
|
||||||
class PlgSystemMokojoomtos extends CMSPlugin
|
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
|
DEFGROUP: MokoJoomTOS
|
||||||
INGROUP: plg_system_mokojoomtos
|
INGROUP: plg_system_mokojoomtos
|
||||||
PATH: src/mokojoomtos.xml
|
PATH: src/mokojoomtos.xml
|
||||||
VERSION: 04.00.00
|
VERSION: 04.02.01
|
||||||
BRIEF: Plugin manifest XML file for MokoJoomTOS system plugin
|
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>
|
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
<version>00.00.01</version>
|
<version>04.02.01</version>
|
||||||
<description>PLG_SYSTEM_MOKOJOOMTOS_XML_DESCRIPTION</description>
|
<description>Allows Terms of Service to be accessible via menu slug when site is offline</description>
|
||||||
|
|
||||||
<namespace path="src">Joomla\Plugin\System\MokoJoomTOS</namespace>
|
<namespace path="src">Joomla\Plugin\System\MokoJoomTOS</namespace>
|
||||||
|
|
||||||
@@ -71,6 +71,18 @@
|
|||||||
description="PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC"
|
description="PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC"
|
||||||
multiple="true"
|
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
|
<field
|
||||||
name="help_spacer"
|
name="help_spacer"
|
||||||
|
|||||||
+440
-364
@@ -13,394 +13,470 @@ use Joomla\CMS\Installer\InstallerAdapter;
|
|||||||
use Joomla\CMS\Installer\InstallerScript;
|
use Joomla\CMS\Installer\InstallerScript;
|
||||||
use Joomla\CMS\Language\Text;
|
use Joomla\CMS\Language\Text;
|
||||||
use Joomla\CMS\Log\Log;
|
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
|
* @since 1.0.0
|
||||||
*/
|
*/
|
||||||
class PlgSystemMokojoomtosOfflineInstallerScript extends InstallerScript
|
class PlgSystemMokojoomtosInstallerScript extends InstallerScript
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Minimum Joomla version required to install the plugin
|
* Minimum Joomla version required to install the plugin
|
||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
*/
|
*/
|
||||||
protected $minimumJoomla = '4.0.0';
|
protected $minimumJoomla = '4.0.0';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Minimum PHP version required to install the plugin
|
* Minimum PHP version required to install the plugin
|
||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
*/
|
*/
|
||||||
protected $minimumPhp = '7.4.0';
|
protected $minimumPhp = '7.4.0';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extension type (used by parent class)
|
* Extension type (used by parent class)
|
||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
*/
|
*/
|
||||||
protected $extension = 'plg_system_mokojoomtos';
|
protected $extension = 'plg_system_mokojoomtos';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function called before plugin installation/update/uninstall
|
* Function called before plugin installation/update/uninstall
|
||||||
*
|
*
|
||||||
* @param string $type Installation type (install, update, discover_install)
|
* @param string $type Installation type (install, update, discover_install)
|
||||||
* @param InstallerAdapter $parent Parent installer adapter
|
* @param InstallerAdapter $parent Parent installer adapter
|
||||||
*
|
*
|
||||||
* @return boolean True on success
|
* @return boolean True on success
|
||||||
*
|
*
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
*/
|
*/
|
||||||
public function preflight($type, $parent)
|
public function preflight($type, $parent)
|
||||||
{
|
{
|
||||||
// Check minimum requirements
|
if (!parent::preflight($type, $parent)) {
|
||||||
if (!parent::preflight($type, $parent)) {
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function called after plugin installation
|
* Function called after plugin installation
|
||||||
*
|
*
|
||||||
* @param InstallerAdapter $parent Parent installer adapter
|
* @param InstallerAdapter $parent Parent installer adapter
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*
|
*
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
*/
|
*/
|
||||||
public function install($parent)
|
public function install($parent)
|
||||||
{
|
{
|
||||||
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_INSTALL_SUCCESS') . '</p>';
|
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_INSTALL_SUCCESS') . '</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function called after plugin update
|
* Function called after plugin update
|
||||||
*
|
*
|
||||||
* @param InstallerAdapter $parent Parent installer adapter
|
* @param InstallerAdapter $parent Parent installer adapter
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*
|
*
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
*/
|
*/
|
||||||
public function update($parent)
|
public function update($parent)
|
||||||
{
|
{
|
||||||
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UPDATE_SUCCESS') . '</p>';
|
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UPDATE_SUCCESS') . '</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function called after plugin uninstallation
|
* Function called after plugin uninstallation
|
||||||
*
|
*
|
||||||
* @param InstallerAdapter $parent Parent installer adapter
|
* @param InstallerAdapter $parent Parent installer adapter
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*
|
*
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
*/
|
*/
|
||||||
public function uninstall($parent)
|
public function uninstall($parent)
|
||||||
{
|
{
|
||||||
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UNINSTALL_SUCCESS') . '</p>';
|
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UNINSTALL_SUCCESS') . '</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function called after extension installation/update/discover_install
|
* Function called after extension installation/update/discover_install
|
||||||
*
|
*
|
||||||
* @param string $type Installation type (install, update, discover_install)
|
* Fixes #89: enablePlugin() is now called unconditionally for both
|
||||||
* @param InstallerAdapter $parent Parent installer adapter
|
* install and upgrade paths.
|
||||||
*
|
* Fixes #92: enablePlugin() is called on upgrade to re-enable if disabled.
|
||||||
* @return void
|
*
|
||||||
*
|
* @param string $type Installation type (install, update, discover_install)
|
||||||
* @since 1.0.0
|
* @param InstallerAdapter $parent Parent installer adapter
|
||||||
*/
|
*
|
||||||
public function postflight($type, $parent)
|
* @return void
|
||||||
{
|
*
|
||||||
if ($type === 'install' || $type === 'discover_install') {
|
* @since 1.0.0
|
||||||
// Create Terms of Service article and menu item
|
*/
|
||||||
$this->createTermsOfServiceSetup();
|
public function postflight($type, $parent)
|
||||||
|
{
|
||||||
echo '<div class="alert alert-success">';
|
// Always enable the plugin on install or upgrade
|
||||||
echo '<h4>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_TITLE') . '</h4>';
|
$this->enablePlugin();
|
||||||
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_DESC') . '</p>';
|
|
||||||
echo '</div>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if ($type === 'install' || $type === 'discover_install') {
|
||||||
* Create Terms of Service article and menu item
|
$this->createTermsOfServiceSetup();
|
||||||
*
|
$this->setDefaultSlugs();
|
||||||
* @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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
echo '<div class="alert alert-success">';
|
||||||
* Create Terms of Service article
|
echo '<h4>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_TITLE') . '</h4>';
|
||||||
*
|
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_DESC') . '</p>';
|
||||||
* @return int|null Article ID or null on failure
|
echo '</div>';
|
||||||
*
|
}
|
||||||
* @since 1.0.0
|
}
|
||||||
*/
|
|
||||||
private function createTermsArticle()
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
|
|
||||||
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) {
|
// Check if Terms of Service article already exists (by alias, any category)
|
||||||
Log::add('Failed to get Content table instance', Log::WARNING, 'jerror');
|
$query = $db->getQuery(true)
|
||||||
return null;
|
->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
|
if (!$articleId) {
|
||||||
$query = $db->getQuery(true)
|
$articleId = $this->createTermsArticle();
|
||||||
->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 (!$catId) {
|
if ($articleId) {
|
||||||
Log::add('Could not find Uncategorised category for com_content', Log::WARNING, 'jerror');
|
$query = $db->getQuery(true)
|
||||||
return null;
|
->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
|
if (!$menuId) {
|
||||||
$createdBy = Factory::getApplication()->getIdentity()->id ?: 0;
|
$this->createTermsMenuItem($articleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::add('Error creating Terms of Service setup: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$data = [
|
/**
|
||||||
'title' => 'Terms of Service',
|
* Create Terms of Service article using Joomla 5 MVCFactory
|
||||||
'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>',
|
* Fixes #90: Uses bootComponent()->getMVCFactory() instead of
|
||||||
'fulltext' => '',
|
* the removed Table::addIncludePath() / Table::getInstance().
|
||||||
'state' => 1,
|
* Fixes #94: Includes params, metadata, and attribs defaults.
|
||||||
'catid' => $catId,
|
*
|
||||||
'created' => Factory::getDate()->toSql(),
|
* @return int|null Article ID or null on failure
|
||||||
'created_by' => $createdBy,
|
*
|
||||||
'language' => '*',
|
* @since 1.0.0
|
||||||
'access' => 1, // Public
|
*/
|
||||||
];
|
private function createTermsArticle()
|
||||||
|
{
|
||||||
// Bind data to table object first
|
try {
|
||||||
if (!$table->bind($data)) {
|
$db = Factory::getDbo();
|
||||||
Log::add('Failed to bind data to Content table: ' . $table->getError(), Log::WARNING, 'jerror');
|
$app = Factory::getApplication();
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Get content table via MVCFactory (Joomla 4/5 compatible)
|
||||||
* Create Terms of Service menu item
|
$table = $app->bootComponent('com_content')
|
||||||
*
|
->getMVCFactory()
|
||||||
* @param int $articleId The article ID to link to
|
->createTable('Article', 'Administrator');
|
||||||
*
|
|
||||||
* @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();
|
|
||||||
|
|
||||||
if (!$componentId) {
|
if (!$table) {
|
||||||
Log::add('Could not determine com_content component ID', Log::WARNING, 'jerror');
|
Log::add('Failed to get Content table instance', Log::WARNING, 'jerror');
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Get Uncategorised category ID dynamically
|
||||||
* Create Legal menu type
|
$query = $db->getQuery(true)
|
||||||
*
|
->select('id')
|
||||||
* @return void
|
->from($db->quoteName('#__categories'))
|
||||||
*
|
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'))
|
||||||
* @since 1.0.0
|
->where($db->quoteName('alias') . ' = ' . $db->quote('uncategorised'))
|
||||||
*/
|
->where($db->quoteName('published') . ' = 1');
|
||||||
private function createLegalMenuType()
|
$db->setQuery($query);
|
||||||
{
|
$catId = (int) $db->loadResult();
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (!$catId) {
|
||||||
* Enable the plugin after installation
|
Log::add('Could not find Uncategorised category for com_content', Log::WARNING, 'jerror');
|
||||||
*
|
return null;
|
||||||
* @return void
|
}
|
||||||
*
|
|
||||||
* @since 1.0.0
|
$createdBy = $app->getIdentity()->id ?: 0;
|
||||||
*/
|
|
||||||
private function enablePlugin()
|
$data = [
|
||||||
{
|
'title' => 'Terms of Service',
|
||||||
try {
|
'alias' => 'terms-of-service',
|
||||||
$db = Factory::getDbo();
|
'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>',
|
||||||
$query = $db->getQuery(true)
|
'fulltext' => '',
|
||||||
->update($db->quoteName('#__extensions'))
|
'state' => 1,
|
||||||
->set($db->quoteName('enabled') . ' = 1')
|
'catid' => $catId,
|
||||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
'created' => Factory::getDate()->toSql(),
|
||||||
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
|
'created_by' => $createdBy,
|
||||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoomtos'));
|
'language' => '*',
|
||||||
$db->setQuery($query);
|
'access' => 1,
|
||||||
$db->execute();
|
'params' => '{}',
|
||||||
|
'metadata' => '{"robots":"","author":"","rights":""}',
|
||||||
echo '<p class="alert alert-success">✓ Plugin enabled automatically</p>';
|
'attribs' => '{}',
|
||||||
} catch (\Exception $e) {
|
];
|
||||||
Log::add('Error enabling plugin: ' . $e->getMessage(), Log::WARNING, 'jerror');
|
|
||||||
}
|
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;
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Factory;
|
||||||
use Joomla\CMS\Plugin\CMSPlugin;
|
use Joomla\CMS\Plugin\CMSPlugin;
|
||||||
use Joomla\CMS\Uri\Uri;
|
use Joomla\CMS\Uri\Uri;
|
||||||
use Joomla\Event\SubscriberInterface;
|
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
|
* the site is in offline mode. If both conditions are met, temporarily
|
||||||
* disables offline mode and sets component-only view for this request.
|
* 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
|
* @return void
|
||||||
*
|
*
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
*/
|
*/
|
||||||
public function onAfterRoute()
|
public function onAfterRoute()
|
||||||
{
|
{
|
||||||
|
$app = $this->getApplication();
|
||||||
|
|
||||||
// Only process for site application
|
// Only process for site application
|
||||||
if (!$this->getApplication()->isClient('site'))
|
if (!$app->isClient('site'))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the global configuration
|
$config = $app->getConfig();
|
||||||
$config = $this->getApplication()->getConfig();
|
|
||||||
|
|
||||||
// Only proceed if site is offline
|
// Only proceed if site is offline
|
||||||
if (!$config->get('offline'))
|
if (!$config->get('offline'))
|
||||||
@@ -77,7 +76,7 @@ final class MokoJoomTOS extends CMSPlugin implements SubscriberInterface
|
|||||||
return;
|
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', []);
|
$slugs = $this->params->get('tos_slug', []);
|
||||||
|
|
||||||
// Handle legacy single-value string format
|
// Handle legacy single-value string format
|
||||||
@@ -85,46 +84,161 @@ final class MokoJoomTOS extends CMSPlugin implements SubscriberInterface
|
|||||||
{
|
{
|
||||||
$slugs = array_filter([trim($slugs)]);
|
$slugs = array_filter([trim($slugs)]);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$slugs = (array) $slugs;
|
||||||
|
}
|
||||||
|
|
||||||
if (empty($slugs))
|
if (empty($slugs))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the current URI path
|
$includeChildren = (int) $this->params->get('include_children', 1);
|
||||||
$uri = Uri::getInstance();
|
|
||||||
$path = trim($uri->getPath(), '/');
|
|
||||||
|
|
||||||
// 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), '/');
|
$base = trim(Uri::base(true), '/');
|
||||||
if (!empty($base) && strpos($path, $base) === 0)
|
if (!empty($base) && strpos($path, $base) === 0)
|
||||||
{
|
{
|
||||||
$path = trim(substr($path, strlen($base)), '/');
|
$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)
|
foreach ($slugs as $slug)
|
||||||
{
|
{
|
||||||
$slug = trim($slug);
|
$slug = trim((string) $slug);
|
||||||
if (empty($slug))
|
if (empty($slug))
|
||||||
{
|
{
|
||||||
continue;
|
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
|
$this->bypassOffline($config, $app);
|
||||||
$config->set('offline', 0);
|
return true;
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
$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
|
try
|
||||||
{
|
{
|
||||||
$db = Factory::getDbo();
|
$db = Factory::getDbo();
|
||||||
@@ -70,7 +88,7 @@ class MenuslugField extends ListField
|
|||||||
$options[] = (object) [
|
$options[] = (object) [
|
||||||
'value' => '',
|
'value' => '',
|
||||||
'text' => '──────────────',
|
'text' => '──────────────',
|
||||||
'disable' => true
|
'disabled' => true
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
$lastMenuType = $item->menutype;
|
$lastMenuType = $item->menutype;
|
||||||
|
|||||||
+36
-36
@@ -1,97 +1,97 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
VERSION: 04.00.00
|
VERSION: 04.03.00
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<updates>
|
<updates>
|
||||||
<update>
|
<update>
|
||||||
<name>plg_system_mokojoomtos</name>
|
<name>System - Moko Terms of Service</name>
|
||||||
<description>plg_system_mokojoomtos update</description>
|
<description>System - Moko Terms of Service update</description>
|
||||||
<element>mokojoomtos</element>
|
<element>mokojoomtos</element>
|
||||||
<type>plugin</type>
|
<type>plugin</type>
|
||||||
<version>04.00.00</version>
|
<version>04.03.00-dev</version>
|
||||||
<client>site</client>
|
<client>site</client>
|
||||||
<folder>system</folder>
|
<folder>system</folder>
|
||||||
<tags><tag>development</tag></tags>
|
<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>
|
<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/v04/plg_system_mokojoomtos-04.03.00.zip</downloadurl>
|
||||||
</downloads>
|
</downloads>
|
||||||
<sha256>6d8c6a03d6dc0a784a4442c9c46e1a79e8f65b3da7091c5857958653c5b6efe3</sha256>
|
<sha256>7668df3f07eef38c8172dcde129a29078736f0a511b51446ec1cc51bc2bd2e4c</sha256>
|
||||||
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
|
<targetplatform name="joomla" version="(5|6)\..*" />
|
||||||
<maintainer>Moko Consulting</maintainer>
|
<maintainer>Moko Consulting</maintainer>
|
||||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||||
</update>
|
</update>
|
||||||
<update>
|
<update>
|
||||||
<name>plg_system_mokojoomtos</name>
|
<name>System - Moko Terms of Service</name>
|
||||||
<description>plg_system_mokojoomtos update</description>
|
<description>System - Moko Terms of Service update</description>
|
||||||
<element>mokojoomtos</element>
|
<element>mokojoomtos</element>
|
||||||
<type>plugin</type>
|
<type>plugin</type>
|
||||||
<version>04.00.00</version>
|
<version>04.03.00-alpha</version>
|
||||||
<client>site</client>
|
<client>site</client>
|
||||||
<folder>system</folder>
|
<folder>system</folder>
|
||||||
<tags><tag>alpha</tag></tags>
|
<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>
|
<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/v04/plg_system_mokojoomtos-04.03.00.zip</downloadurl>
|
||||||
</downloads>
|
</downloads>
|
||||||
<sha256>6d8c6a03d6dc0a784a4442c9c46e1a79e8f65b3da7091c5857958653c5b6efe3</sha256>
|
<sha256>7668df3f07eef38c8172dcde129a29078736f0a511b51446ec1cc51bc2bd2e4c</sha256>
|
||||||
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
|
<targetplatform name="joomla" version="(5|6)\..*" />
|
||||||
<maintainer>Moko Consulting</maintainer>
|
<maintainer>Moko Consulting</maintainer>
|
||||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||||
</update>
|
</update>
|
||||||
<update>
|
<update>
|
||||||
<name>plg_system_mokojoomtos</name>
|
<name>System - Moko Terms of Service</name>
|
||||||
<description>plg_system_mokojoomtos update</description>
|
<description>System - Moko Terms of Service update</description>
|
||||||
<element>mokojoomtos</element>
|
<element>mokojoomtos</element>
|
||||||
<type>plugin</type>
|
<type>plugin</type>
|
||||||
<version>04.00.00</version>
|
<version>04.03.00-beta</version>
|
||||||
<client>site</client>
|
<client>site</client>
|
||||||
<folder>system</folder>
|
<folder>system</folder>
|
||||||
<tags><tag>beta</tag></tags>
|
<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>
|
<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/v04/plg_system_mokojoomtos-04.03.00.zip</downloadurl>
|
||||||
</downloads>
|
</downloads>
|
||||||
<sha256>6d8c6a03d6dc0a784a4442c9c46e1a79e8f65b3da7091c5857958653c5b6efe3</sha256>
|
<sha256>7668df3f07eef38c8172dcde129a29078736f0a511b51446ec1cc51bc2bd2e4c</sha256>
|
||||||
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
|
<targetplatform name="joomla" version="(5|6)\..*" />
|
||||||
<maintainer>Moko Consulting</maintainer>
|
<maintainer>Moko Consulting</maintainer>
|
||||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||||
</update>
|
</update>
|
||||||
<update>
|
<update>
|
||||||
<name>plg_system_mokojoomtos</name>
|
<name>System - Moko Terms of Service</name>
|
||||||
<description>plg_system_mokojoomtos update</description>
|
<description>System - Moko Terms of Service update</description>
|
||||||
<element>mokojoomtos</element>
|
<element>mokojoomtos</element>
|
||||||
<type>plugin</type>
|
<type>plugin</type>
|
||||||
<version>04.00.00</version>
|
<version>04.03.00-rc</version>
|
||||||
<client>site</client>
|
<client>site</client>
|
||||||
<folder>system</folder>
|
<folder>system</folder>
|
||||||
<tags><tag>rc</tag></tags>
|
<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>
|
<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/v04/plg_system_mokojoomtos-04.03.00.zip</downloadurl>
|
||||||
</downloads>
|
</downloads>
|
||||||
<sha256>6d8c6a03d6dc0a784a4442c9c46e1a79e8f65b3da7091c5857958653c5b6efe3</sha256>
|
<sha256>7668df3f07eef38c8172dcde129a29078736f0a511b51446ec1cc51bc2bd2e4c</sha256>
|
||||||
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
|
<targetplatform name="joomla" version="(5|6)\..*" />
|
||||||
<maintainer>Moko Consulting</maintainer>
|
<maintainer>Moko Consulting</maintainer>
|
||||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||||
</update>
|
</update>
|
||||||
<update>
|
<update>
|
||||||
<name>plg_system_mokojoomtos</name>
|
<name>System - Moko Terms of Service</name>
|
||||||
<description>plg_system_mokojoomtos update</description>
|
<description>System - Moko Terms of Service update</description>
|
||||||
<element>mokojoomtos</element>
|
<element>mokojoomtos</element>
|
||||||
<type>plugin</type>
|
<type>plugin</type>
|
||||||
<version>04.00.00</version>
|
<version>04.03.00</version>
|
||||||
<client>site</client>
|
<client>site</client>
|
||||||
<folder>system</folder>
|
<folder>system</folder>
|
||||||
<tags><tag>stable</tag></tags>
|
<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>
|
<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/v04/plg_system_mokojoomtos-04.03.00.zip</downloadurl>
|
||||||
</downloads>
|
</downloads>
|
||||||
<sha256>6d8c6a03d6dc0a784a4442c9c46e1a79e8f65b3da7091c5857958653c5b6efe3</sha256>
|
<sha256>7668df3f07eef38c8172dcde129a29078736f0a511b51446ec1cc51bc2bd2e4c</sha256>
|
||||||
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
|
<targetplatform name="joomla" version="(5|6)\..*" />
|
||||||
<maintainer>Moko Consulting</maintainer>
|
<maintainer>Moko Consulting</maintainer>
|
||||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||||
</update>
|
</update>
|
||||||
|
|||||||
Reference in New Issue
Block a user