Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 17c36e73d0 | |||
| f602c54890 | |||
| 883f11cbf3 | |||
| eab88e8c8b | |||
| b402fa6fc4 | |||
| a3b4aa2015 | |||
| bb8f4dfa71 | |||
| d01b1e1675 | |||
| 8a4597552c | |||
| 4621eb5e3a | |||
| 3b85985512 | |||
| 0c12ccd506 | |||
| 5bad7a306d | |||
| 50068d2894 | |||
| c1f412d5c7 | |||
| e2e80f0f00 | |||
| 5095df4a18 | |||
| 2a36270a44 | |||
| c2804a4674 | |||
| 9c0887f69a | |||
| 753ef94d78 | |||
| aca86af9ec | |||
| a5d89cdd14 | |||
| 969f7a09e2 | |||
| e599140f89 | |||
| d86dd19709 | |||
| 70158b81ae | |||
| 7b29e79aff | |||
| 4ef0e4fe84 | |||
| 55f459c25b | |||
| 559351332c | |||
| 140033dfe7 | |||
| 617a408d0f | |||
| b29fb85ccf | |||
| 8731306d7a | |||
| 80f72ccf93 | |||
| e60560f3c4 | |||
| 53c996bb57 | |||
| 3b00949e94 | |||
| e8fc9a892c | |||
| dca6a8b283 | |||
| 047560e2eb | |||
| 9a4b921323 | |||
| 96b8fefbb1 | |||
| 72165dccff | |||
| 3a17f3c0e4 | |||
| 0d41fe9644 | |||
| fd6647f7ae | |||
| e391676fa8 | |||
| e6fbd1391b | |||
| 5905908d62 | |||
| b378d19c29 | |||
| 635b896879 |
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: moko-platform.Maintenance
|
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
|
||||||
# PATH: /templates/workflows/cascade-dev.yml.template
|
|
||||||
# VERSION: 02.00.00
|
|
||||||
# BRIEF: Forward-merge main → all open branches after every push to main
|
|
||||||
#
|
|
||||||
# +========================================================================+
|
|
||||||
# | 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
|
|
||||||
@@ -43,9 +43,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Clone MokoStandards
|
- name: Clone MokoStandards
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
|
||||||
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --branch main --quiet \
|
git clone --depth 1 --branch main --quiet \
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
env:
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
|
||||||
run: |
|
run: |
|
||||||
if [ -f "composer.json" ]; then
|
if [ -f "composer.json" ]; then
|
||||||
composer install \
|
composer install \
|
||||||
@@ -346,7 +346,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
env:
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
|
||||||
run: |
|
run: |
|
||||||
if [ -f "composer.json" ]; then
|
if [ -f "composer.json" ]; then
|
||||||
composer install \
|
composer install \
|
||||||
@@ -391,7 +391,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
env:
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
|
||||||
run: |
|
run: |
|
||||||
if [ -f "composer.json" ]; then
|
if [ -f "composer.json" ]; then
|
||||||
composer install --no-interaction --prefer-dist --optimize-autoloader
|
composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||||
@@ -458,10 +458,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Trigger pre-release build
|
- name: Trigger pre-release build
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
BRANCH: ${{ github.head_ref }}
|
BRANCH: ${{ github.head_ref }}
|
||||||
run: |
|
run: |
|
||||||
curl -s -X POST "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
curl -s -X POST "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
||||||
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -42,10 +42,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup MokoStandards tools
|
- name: Setup MokoStandards tools
|
||||||
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' }}
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
|
||||||
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" \
|
||||||
|
|||||||
+277
-214
@@ -1,214 +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: moko-platform.CI
|
# INGROUP: moko-platform.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
# 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)
|
||||||
# Read platform from XML manifest (<platform> tag) or plain text fallback
|
if [ -n "$CONFLICTS" ]; then
|
||||||
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
|
echo "::error::Merge conflict markers found in source files"
|
||||||
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
|
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
|
||||||
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
- name: Setup PHP
|
exit 1
|
||||||
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
fi
|
||||||
run: |
|
echo "No conflict markers found"
|
||||||
if ! command -v php &> /dev/null; then
|
|
||||||
sudo apt-get update -qq
|
- name: Detect platform
|
||||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
id: platform
|
||||||
fi
|
run: |
|
||||||
|
# Read platform from XML manifest (<platform> tag) or plain text fallback
|
||||||
- name: PHP syntax check
|
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
|
||||||
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
|
||||||
run: |
|
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
||||||
ERRORS=0
|
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||||
while IFS= read -r -d '' file; do
|
|
||||||
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
- name: Setup PHP
|
||||||
ERRORS=$((ERRORS + 1))
|
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
||||||
fi
|
run: |
|
||||||
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
if ! command -v php &> /dev/null; then
|
||||||
echo "PHP lint: ${ERRORS} error(s)"
|
sudo apt-get update -qq
|
||||||
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
|
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
||||||
|
fi
|
||||||
- name: Validate platform manifest
|
|
||||||
run: |
|
- name: PHP syntax check
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
||||||
case "$PLATFORM" in
|
run: |
|
||||||
joomla)
|
ERRORS=0
|
||||||
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
while IFS= read -r -d '' file; do
|
||||||
if [ -z "$MANIFEST" ]; then
|
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
||||||
echo "::warning::No Joomla manifest found (WaaS site)"
|
ERRORS=$((ERRORS + 1))
|
||||||
exit 0
|
fi
|
||||||
fi
|
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
||||||
echo "Manifest: ${MANIFEST}"
|
echo "PHP lint: ${ERRORS} error(s)"
|
||||||
if command -v php &> /dev/null; then
|
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
|
||||||
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
|
- name: Validate platform manifest
|
||||||
for ELEMENT in name version description; do
|
run: |
|
||||||
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
done
|
case "$PLATFORM" in
|
||||||
echo "Joomla manifest valid"
|
joomla)
|
||||||
;;
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
dolibarr)
|
if [ -z "$MANIFEST" ]; then
|
||||||
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
|
echo "::warning::No Joomla manifest found (WaaS site)"
|
||||||
if [ -z "$MOD_FILE" ]; then
|
exit 0
|
||||||
echo "::error::No mod*.class.php found"
|
fi
|
||||||
exit 1
|
echo "Manifest: ${MANIFEST}"
|
||||||
fi
|
if command -v php &> /dev/null; then
|
||||||
echo "Dolibarr module: ${MOD_FILE}"
|
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
|
||||||
;;
|
fi
|
||||||
*)
|
for ELEMENT in name version description; do
|
||||||
echo "Generic platform — no manifest validation"
|
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
|
||||||
;;
|
done
|
||||||
esac
|
echo "Joomla manifest valid"
|
||||||
|
;;
|
||||||
- name: Check update stream format
|
dolibarr)
|
||||||
run: |
|
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
if [ -z "$MOD_FILE" ]; then
|
||||||
case "$PLATFORM" in
|
echo "::error::No mod*.class.php found"
|
||||||
joomla)
|
exit 1
|
||||||
if [ -f "updates.xml" ]; then
|
fi
|
||||||
if command -v php &> /dev/null; then
|
echo "Dolibarr module: ${MOD_FILE}"
|
||||||
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
|
;;
|
||||||
fi
|
*)
|
||||||
echo "updates.xml valid"
|
echo "Generic platform — no manifest validation"
|
||||||
fi
|
;;
|
||||||
;;
|
esac
|
||||||
dolibarr)
|
|
||||||
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
|
- name: Check update stream format
|
||||||
;;
|
run: |
|
||||||
esac
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
|
case "$PLATFORM" in
|
||||||
- name: Verify package source
|
joomla)
|
||||||
run: |
|
if [ -f "updates.xml" ]; then
|
||||||
SOURCE_DIR="src"
|
if command -v php &> /dev/null; then
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
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; }
|
||||||
if [ ! -d "$SOURCE_DIR" ]; then
|
fi
|
||||||
echo "::warning::No src/ or htdocs/ directory"
|
echo "updates.xml valid"
|
||||||
exit 0
|
fi
|
||||||
fi
|
;;
|
||||||
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
|
dolibarr)
|
||||||
echo "Source: ${FILE_COUNT} files"
|
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
|
||||||
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
|
;;
|
||||||
|
esac
|
||||||
# ── Pre-Release RC Build ─────────────────────────────────────────────────
|
|
||||||
pre-release:
|
- name: Check changelog has unreleased entry
|
||||||
name: Build RC Package
|
run: |
|
||||||
runs-on: ubuntu-latest
|
if [ ! -f "CHANGELOG.md" ]; then
|
||||||
needs: [branch-policy, validate]
|
echo "::warning::No CHANGELOG.md found"
|
||||||
|
exit 0
|
||||||
steps:
|
fi
|
||||||
- name: Trigger RC pre-release
|
# Check for content under [Unreleased] section
|
||||||
env:
|
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
echo "::error::CHANGELOG.md missing [Unreleased] section"
|
||||||
REPO: ${{ github.repository }}
|
exit 1
|
||||||
BRANCH: ${{ github.head_ref }}
|
fi
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
# Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
|
||||||
run: |
|
UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
|
||||||
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
|
||||||
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
|
||||||
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
|
||||||
|
|
||||||
|
- name: Verify package source
|
||||||
|
run: |
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
if [ ! -d "$SOURCE_DIR" ]; then
|
||||||
|
echo "::warning::No src/ or htdocs/ directory"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
|
||||||
|
echo "Source: ${FILE_COUNT} files"
|
||||||
|
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
|
||||||
|
|
||||||
|
# ── Pre-Release RC Build ─────────────────────────────────────────────────
|
||||||
|
pre-release:
|
||||||
|
name: Build RC Package
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [branch-policy, validate]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Trigger RC pre-release
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
BRANCH: ${{ github.head_ref }}
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
run: |
|
||||||
|
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
||||||
|
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# ── 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."
|
||||||
|
|||||||
@@ -1,246 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: moko-platform.Release
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
||||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
|
||||||
# VERSION: 05.00.00
|
|
||||||
# BRIEF: Manual pre-release — builds dev/alpha/beta/rc packages from any branch
|
|
||||||
|
|
||||||
name: "Universal: Pre-Release"
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
stability:
|
|
||||||
description: 'Pre-release channel'
|
|
||||||
required: true
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- development
|
|
||||||
- alpha
|
|
||||||
- beta
|
|
||||||
- release-candidate
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
env:
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
|
||||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: "Build Pre-Release (${{ inputs.stability }})"
|
|
||||||
runs-on: release
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
token: ${{ secrets.GA_TOKEN }}
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
run: |
|
|
||||||
if ! command -v php &> /dev/null; then
|
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
|
||||||
env:
|
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
|
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
|
||||||
run: |
|
|
||||||
git clone --depth 1 --branch main --quiet "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" /tmp/moko-platform-api
|
|
||||||
|
|
||||||
- name: Detect platform
|
|
||||||
id: platform
|
|
||||||
run: |
|
|
||||||
php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output
|
|
||||||
|
|
||||||
- name: Resolve metadata
|
|
||||||
id: meta
|
|
||||||
run: |
|
|
||||||
STABILITY="${{ inputs.stability }}"
|
|
||||||
MOKO_API="/tmp/moko-platform-api/cli"
|
|
||||||
|
|
||||||
case "$STABILITY" in
|
|
||||||
development) SUFFIX="-dev"; TAG="development" ;;
|
|
||||||
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
|
||||||
beta) SUFFIX="-beta"; TAG="beta" ;;
|
|
||||||
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Bump patch version
|
|
||||||
BUMP_OUTPUT=$(php ${MOKO_API}/version_bump.php --path .)
|
|
||||||
VERSION=$(echo "$BUMP_OUTPUT" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true)
|
|
||||||
[ -z "$VERSION" ] && VERSION=$(php ${MOKO_API}/version_read.php --path .)
|
|
||||||
echo "Version: ${VERSION}"
|
|
||||||
|
|
||||||
# Update platform-specific manifest
|
|
||||||
php ${MOKO_API}/version_set_platform.php --path . --version "${VERSION}"
|
|
||||||
|
|
||||||
# Commit version bump
|
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "gitea-actions[bot]"
|
|
||||||
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
|
||||||
git add -A
|
|
||||||
git diff --cached --quiet || {
|
|
||||||
git commit -m "chore(version): bump to ${VERSION} [skip ci]"
|
|
||||||
git push origin HEAD 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Detect element from Joomla/Dolibarr manifest
|
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
|
||||||
EXT_ELEMENT=$(php ${MOKO_API}/manifest_read.php --path . --field name 2>/dev/null | tr -d ' ' | tr '[:upper:]' '[:lower:]' || true)
|
|
||||||
# For Joomla, prefer <element> tag
|
|
||||||
if [ "$PLATFORM" = "joomla" ]; then
|
|
||||||
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
|
|
||||||
if [ -n "$MANIFEST" ]; then
|
|
||||||
ELEM=$(grep -oP "<element>\K[^<]+" "$MANIFEST" 2>/dev/null | head -1)
|
|
||||||
[ -n "$ELEM" ] && EXT_ELEMENT="$ELEM"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
|
||||||
|
|
||||||
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
|
|
||||||
|
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
|
||||||
|
|
||||||
- name: Build package
|
|
||||||
id: zip
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
SUFFIX="${{ steps.meta.outputs.suffix }}"
|
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
|
||||||
|
|
||||||
if [ "$PLATFORM" = "joomla" ]; then
|
|
||||||
php /tmp/moko-platform-api/cli/joomla_build.php --path . --version "${VERSION}" --suffix "${SUFFIX}" --output build --github-output
|
|
||||||
else
|
|
||||||
# Generic build: zip src/ directory
|
|
||||||
SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && { echo "::error::No src/ or htdocs/"; exit 1; }
|
|
||||||
EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}"
|
|
||||||
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
|
|
||||||
mkdir -p build
|
|
||||||
cd "$SOURCE_DIR" && zip -r "../build/${ZIP_NAME}" . && cd ..
|
|
||||||
SHA256=$(sha256sum "build/${ZIP_NAME}" | cut -d' ' -f1)
|
|
||||||
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "zip_path=build/${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Create or replace Gitea release
|
|
||||||
id: release
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
|
||||||
SHA256="${{ steps.zip.outputs.sha256 }}"
|
|
||||||
ZIP_NAME="${{ steps.zip.outputs.zip_name }}"
|
|
||||||
EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}"
|
|
||||||
TOKEN="${{ secrets.GA_TOKEN }}"
|
|
||||||
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
BRANCH=$(git branch --show-current)
|
|
||||||
|
|
||||||
BODY="## ${VERSION} ($(date +%Y-%m-%d))
|
|
||||||
**Channel:** ${STABILITY}
|
|
||||||
**SHA-256:** \`${SHA256}\`"
|
|
||||||
|
|
||||||
# Delete existing release
|
|
||||||
EXISTING_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API}/releases/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null)
|
|
||||||
if [ -n "$EXISTING_ID" ]; then
|
|
||||||
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API}/releases/${EXISTING_ID}" 2>/dev/null || true
|
|
||||||
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API}/tags/${TAG}" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create release
|
|
||||||
RELEASE_ID=$(curl -sS -X POST -H "Authorization: token ${TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API}/releases" \
|
|
||||||
-d "$(jq -n \
|
|
||||||
--arg tag "$TAG" \
|
|
||||||
--arg target "$BRANCH" \
|
|
||||||
--arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \
|
|
||||||
--arg body "$BODY" \
|
|
||||||
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: true}'
|
|
||||||
)" | jq -r '.id')
|
|
||||||
|
|
||||||
echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# Upload ZIP
|
|
||||||
curl -sS -X POST -H "Authorization: token ${TOKEN}" \
|
|
||||||
-H "Content-Type: application/octet-stream" \
|
|
||||||
"${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
|
|
||||||
--data-binary "@${{ steps.zip.outputs.zip_path }}"
|
|
||||||
|
|
||||||
echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})"
|
|
||||||
|
|
||||||
- name: "Update updates.xml"
|
|
||||||
if: steps.platform.outputs.platform == 'joomla'
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
|
||||||
SHA256="${{ steps.zip.outputs.sha256 }}"
|
|
||||||
php /tmp/moko-platform-api/cli/updates_xml_build.php --path . --version "$VERSION" --stability "$STABILITY" --sha "$SHA256" --gitea-url "$GITEA_URL" --org "$GITEA_ORG" --repo "$GITEA_REPO"
|
|
||||||
if ! git diff --quiet updates.xml 2>/dev/null; then
|
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "gitea-actions[bot]"
|
|
||||||
git add updates.xml
|
|
||||||
git commit -m "chore: update $STABILITY channel $VERSION [skip ci]"
|
|
||||||
git push origin HEAD 2>&1 || echo "WARNING: push failed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: "Sync updates.xml to all branches"
|
|
||||||
if: steps.platform.outputs.platform == 'joomla'
|
|
||||||
run: |
|
|
||||||
php /tmp/moko-platform-api/cli/updates_xml_sync.php --path . --current "${{ github.ref_name }}" --branches main,dev --version "${{ steps.meta.outputs.version }}" --token "${{ secrets.GA_TOKEN }}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" --gitea-url "${GITEA_URL}"
|
|
||||||
|
|
||||||
- name: "Delete lesser pre-release channels (cascade)"
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
TOKEN="${{ secrets.GA_TOKEN }}"
|
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
|
||||||
|
|
||||||
# Cascade: rc → beta,alpha,dev | beta → alpha,dev | alpha → dev | dev → nothing
|
|
||||||
case "$STABILITY" in
|
|
||||||
release-candidate) TAGS_TO_DELETE="beta alpha development" ;;
|
|
||||||
beta) TAGS_TO_DELETE="alpha development" ;;
|
|
||||||
alpha) TAGS_TO_DELETE="development" ;;
|
|
||||||
*) TAGS_TO_DELETE="" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
[ -z "$TAGS_TO_DELETE" ] && exit 0
|
|
||||||
|
|
||||||
for TAG in $TAGS_TO_DELETE; do
|
|
||||||
RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \
|
|
||||||
python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then
|
|
||||||
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true
|
|
||||||
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API_BASE}/tags/${TAG}" 2>/dev/null || true
|
|
||||||
echo "Deleted: ${TAG} (id: ${RELEASE_ID})"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
|
||||||
+5
-49
@@ -1,55 +1,11 @@
|
|||||||
<!--
|
|
||||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
|
|
||||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoDPCalendarAPI
|
|
||||||
VERSION: 01.00.00
|
|
||||||
PATH: ./CHANGELOG.md
|
|
||||||
-->
|
|
||||||
|
|
||||||
# CHANGELOG - MokoDPCalendarAPI
|
|
||||||
|
|
||||||
## [Unreleased] — 2026-05-23
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
- Removed deploy-manual.yml workflow — switching to Joomla update server method for extension distribution
|
|
||||||
- Removed deploy variables and secrets (RS_FTP_PATH_SUFFIX, DEV_FTP_KEY)
|
|
||||||
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
## [Unreleased]
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
||||||
|
|
||||||
## [03.01.00] — 2026-05-09
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Bookings API: list, get via `/v1/dpcalendar/bookings`
|
|
||||||
- Tickets API: list, get via `/v1/dpcalendar/tickets`
|
|
||||||
- ICS/iCal export: single event and full calendar export
|
|
||||||
- Recurring event occurrence expansion with RRULE support
|
|
||||||
- Bulk event creation via `POST /v1/dpcalendar/events/bulk`
|
|
||||||
- Calendar-level iCal export via `/v1/dpcalendar/calendars/{id}/ical`
|
|
||||||
- CORS support with preflight handling
|
|
||||||
- ETag caching for conditional GET requests
|
|
||||||
- Location expansion on events via `?expand=locations`
|
|
||||||
- Advanced filtering: date ranges, featured, access level, language
|
|
||||||
- Sorting and field selection on all list endpoints
|
|
||||||
- Search across event titles and descriptions
|
|
||||||
- Pagination with total count metadata
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Version aligned across manifest and source files
|
- Migrated all workflow and template paths from `.github/` to `.mokogitea/`
|
||||||
- Production-ready error handling with proper HTTP status codes
|
- Template source paths updated: `templates/gitea/` to `templates/mokogitea/`
|
||||||
|
- HCL definition files removed -- Template repos are now the canonical source
|
||||||
### Fixed
|
|
||||||
- Version header consistency between manifest.xml and PHP source
|
|
||||||
|
|
||||||
## [01.00.00] — 2026-04-26
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Initial release of the DPCalendar Web Services plugin
|
- `branch-cleanup.yml`: auto-delete merged feature branches after PR merge
|
||||||
- Events API: list, get, create, update, delete via `/v1/dpcalendar/events`
|
|
||||||
- Calendars API: list, get via `/v1/dpcalendar/calendars`
|
|
||||||
- Locations API: list, get, create, update, delete via `/v1/dpcalendar/locations`
|
|
||||||
- JSON:API views with configurable field lists for items and collections
|
|
||||||
- Joomla 5/6 namespace-based plugin architecture
|
|
||||||
- MokoStandards-compliant file headers and structure
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -24,3 +24,4 @@
|
|||||||
<server type="extension" name="MokoDPCalendarAPI Updates">https://git.mokoconsulting.tech/MokoConsulting/MokoDPCalendarAPI/raw/branch/main/updates.xml</server>
|
<server type="extension" name="MokoDPCalendarAPI Updates">https://git.mokoconsulting.tech/MokoConsulting/MokoDPCalendarAPI/raw/branch/main/updates.xml</server>
|
||||||
</updateservers>
|
</updateservers>
|
||||||
</extension>
|
</extension>
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -19,7 +19,7 @@
|
|||||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoDPCalendarAPI/releases/download/stable/-03.02.00.zip</downloadurl>
|
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoDPCalendarAPI/releases/download/stable/-03.02.00.zip</downloadurl>
|
||||||
</downloads>
|
</downloads>
|
||||||
<sha256>652b87468d39eb4a513b6b0aae3a563abf7662f0ee3b837b724ee3b6e19a08fb</sha256>
|
<sha256>652b87468d39eb4a513b6b0aae3a563abf7662f0ee3b837b724ee3b6e19a08fb</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>
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoDPCalendarAPI/releases/download/stable/-03.02.00.zip</downloadurl>
|
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoDPCalendarAPI/releases/download/stable/-03.02.00.zip</downloadurl>
|
||||||
</downloads>
|
</downloads>
|
||||||
<sha256>652b87468d39eb4a513b6b0aae3a563abf7662f0ee3b837b724ee3b6e19a08fb</sha256>
|
<sha256>652b87468d39eb4a513b6b0aae3a563abf7662f0ee3b837b724ee3b6e19a08fb</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>
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoDPCalendarAPI/releases/download/stable/-03.02.00.zip</downloadurl>
|
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoDPCalendarAPI/releases/download/stable/-03.02.00.zip</downloadurl>
|
||||||
</downloads>
|
</downloads>
|
||||||
<sha256>652b87468d39eb4a513b6b0aae3a563abf7662f0ee3b837b724ee3b6e19a08fb</sha256>
|
<sha256>652b87468d39eb4a513b6b0aae3a563abf7662f0ee3b837b724ee3b6e19a08fb</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>
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoDPCalendarAPI/releases/download/stable/-03.02.00.zip</downloadurl>
|
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoDPCalendarAPI/releases/download/stable/-03.02.00.zip</downloadurl>
|
||||||
</downloads>
|
</downloads>
|
||||||
<sha256>652b87468d39eb4a513b6b0aae3a563abf7662f0ee3b837b724ee3b6e19a08fb</sha256>
|
<sha256>652b87468d39eb4a513b6b0aae3a563abf7662f0ee3b837b724ee3b6e19a08fb</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>
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoDPCalendarAPI/releases/download/stable/-03.02.00.zip</downloadurl>
|
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoDPCalendarAPI/releases/download/stable/-03.02.00.zip</downloadurl>
|
||||||
</downloads>
|
</downloads>
|
||||||
<sha256>652b87468d39eb4a513b6b0aae3a563abf7662f0ee3b837b724ee3b6e19a08fb</sha256>
|
<sha256>652b87468d39eb4a513b6b0aae3a563abf7662f0ee3b837b724ee3b6e19a08fb</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