From 42bf60072c9524ce281797aaa23e0fb851c73429 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Tue, 26 May 2026 04:47:48 +0000
Subject: [PATCH 01/40] sync: update-server.yml with updates.xml integrity
check [skip ci]
---
.mokogitea/workflows/update-server.yml | 175 +++++++++++++++++++++++++
1 file changed, 175 insertions(+)
diff --git a/.mokogitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml
index d7087f6..510ad8e 100644
--- a/.mokogitea/workflows/update-server.yml
+++ b/.mokogitea/workflows/update-server.yml
@@ -455,6 +455,181 @@ jobs:
rm -f /tmp/deploy_key /tmp/sftp-config.json
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
+ - name: Validate updates.xml integrity
+ run: |
+ ERRORS=0
+
+ if [ ! -f "updates.xml" ]; then
+ echo "::error::updates.xml not found"
+ exit 1
+ fi
+
+ # Well-formed XML
+ if ! python3 -c "import xml.etree.ElementTree as ET; ET.parse('updates.xml')" 2>/dev/null; then
+ echo "::error::updates.xml is not valid XML"
+ ERRORS=$((ERRORS+1))
+ fi
+
+ python3 << 'PYEOF'
+ import xml.etree.ElementTree as ET, sys, re, os
+
+ tree = ET.parse("updates.xml")
+ root = tree.getroot()
+ updates = root.findall("update")
+ errors = 0
+ warnings = 0
+ seen_tags = set()
+
+ # All 5 channels MUST be present
+ REQUIRED_CHANNELS = {"stable", "rc", "beta", "alpha", "dev"}
+ VALID_TAGS = REQUIRED_CHANNELS | {"development"} # accept legacy alias
+ REPO = os.environ.get("GITEA_REPO", "")
+ ORG = os.environ.get("GITEA_ORG", "MokoConsulting")
+ REPO_BASE = f"https://git.mokoconsulting.tech/{ORG}/"
+
+ # Gitea release tag names per channel (Moko standard)
+ RELEASE_TAG_MAP = {
+ "stable": "stable",
+ "rc": "release-candidate",
+ "beta": "beta",
+ "alpha": "alpha",
+ "dev": "development",
+ "development": "development",
+ }
+
+ # Joomla update XML required fields per
+ # https://docs.joomla.org/Deploying_an_Update_Server
+ REQUIRED_FIELDS = ["name", "element", "type", "version", "infourl"]
+
+ for i, u in enumerate(updates):
+ tag_el = u.find("tags/tag")
+ tag = tag_el.text.strip() if tag_el is not None and tag_el.text else None
+ label = f"Entry {i+1} ({tag or '?'})"
+
+ # -- Required Joomla fields --
+ for field in REQUIRED_FIELDS:
+ el = u.find(field)
+ if el is None or not (el.text or "").strip():
+ print(f"::error::{label}: missing required <{field}>")
+ errors += 1
+
+ # -- --
+ dl = u.find("downloads/downloadurl")
+ if dl is None or not (dl.text or "").strip():
+ print(f"::error::{label}: missing ")
+ errors += 1
+ else:
+ dl_url = dl.text.strip()
+ # Must point to org repo
+ if REPO_BASE not in dl_url:
+ print(f"::error::{label}: download URL not under {REPO_BASE}: {dl_url}")
+ errors += 1
+ # Must end in .zip
+ if not dl_url.endswith(".zip"):
+ print(f"::error::{label}: download URL must end in .zip: {dl_url}")
+ errors += 1
+ # Must use correct Gitea release tag in path
+ if tag and tag in RELEASE_TAG_MAP:
+ expected_tag = RELEASE_TAG_MAP[tag]
+ if f"/download/{expected_tag}/" not in dl_url:
+ print(f"::error::{label}: download URL should contain /download/{expected_tag}/ but got: {dl_url}")
+ errors += 1
+
+ # -- (required for Joomla to match update) --
+ client = u.find("client")
+ if client is None or not (client.text or "").strip():
+ print(f"::error::{label}: missing (required for Joomla update matching)")
+ errors += 1
+
+ # -- --
+ tp = u.find("targetplatform")
+ if tp is None:
+ print(f"::error::{label}: missing ")
+ errors += 1
+ else:
+ tp_name = tp.get("name", "")
+ tp_ver = tp.get("version", "")
+ if tp_name != "joomla":
+ print(f"::error::{label}: targetplatform name should be 'joomla', got '{tp_name}'")
+ errors += 1
+ if not tp_ver:
+ print(f"::error::{label}: targetplatform missing version regex")
+ errors += 1
+ elif "5" not in tp_ver or "6" not in tp_ver:
+ print(f"::warning::{label}: targetplatform version may not cover Joomla 5+6: {tp_ver}")
+ warnings += 1
+
+ # -- must be valid Joomla type --
+ type_el = u.find("type")
+ if type_el is not None and type_el.text:
+ valid_types = {"component", "module", "plugin", "template", "library", "package", "file"}
+ if type_el.text.strip() not in valid_types:
+ print(f"::error::{label}: invalid type '{type_el.text}' (expected: {valid_types})")
+ errors += 1
+
+ # -- format (XX.YY.ZZ with optional suffix) --
+ ver_el = u.find("version")
+ if ver_el is not None and ver_el.text:
+ if not re.match(r"^\d{2}\.\d{2}\.\d{2}(-\w+)?$", ver_el.text.strip()):
+ print(f"::warning::{label}: version '{ver_el.text}' does not match XX.YY.ZZ format")
+ warnings += 1
+
+ # -- and --
+ for field in ["maintainer", "maintainerurl"]:
+ el = u.find(field)
+ if el is None or not (el.text or "").strip():
+ print(f"::warning::{label}: missing <{field}>")
+ warnings += 1
+
+ # -- Valid stability tag --
+ if tag is None:
+ print(f"::error::{label}: missing ")
+ errors += 1
+ elif tag not in VALID_TAGS:
+ print(f"::error::{label}: invalid tag '{tag}' (expected: {VALID_TAGS})")
+ errors += 1
+
+ # -- Duplicate tag check --
+ norm_tag = "dev" if tag == "development" else tag
+ if norm_tag in seen_tags:
+ print(f"::error::{label}: duplicate channel '{tag}'")
+ errors += 1
+ if norm_tag:
+ seen_tags.add(norm_tag)
+
+ # -- All 5 channels must exist --
+ missing = REQUIRED_CHANNELS - seen_tags
+ if missing:
+ print(f"::error::Missing required update channels: {', '.join(sorted(missing))}")
+ errors += 1
+
+ # -- Version ordering: higher stability must not exceed dev version --
+ channel_versions = {}
+ for u in updates:
+ tag_el = u.find("tags/tag")
+ ver_el = u.find("version")
+ if tag_el is not None and ver_el is not None and tag_el.text and ver_el.text:
+ norm = "dev" if tag_el.text.strip() == "development" else tag_el.text.strip()
+ # Strip suffix for comparison (01.00.18-dev -> 01.00.18)
+ base_ver = re.sub(r"-\w+$", "", ver_el.text.strip())
+ channel_versions[norm] = base_ver
+
+ # Cascade check: dev >= alpha >= beta >= rc >= stable
+ ORDER = ["dev", "alpha", "beta", "rc", "stable"]
+ for j in range(1, len(ORDER)):
+ current = ORDER[j]
+ previous = ORDER[j - 1]
+ if current in channel_versions and previous in channel_versions:
+ if channel_versions[current] > channel_versions[previous]:
+ print(f"::error::{current} version ({channel_versions[current]}) is ahead of {previous} ({channel_versions[previous]})")
+ errors += 1
+
+ # -- Summary --
+ print(f"\nupdates.xml validation: {len(updates)} entries, {errors} error(s), {warnings} warning(s)")
+ if errors > 0:
+ sys.exit(1)
+ PYEOF
+
- name: Summary
if: always()
run: |
--
2.52.0
From 8a22ece4bb517de9aabe87e0b695c11a9de46c1d Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Tue, 26 May 2026 17:35:20 +0000
Subject: [PATCH 02/40] chore(ci): update pre-release.yml from moko-platform
[skip ci]
---
.mokogitea/workflows/pre-release.yml | 763 ++++++++++++++-------------
1 file changed, 388 insertions(+), 375 deletions(-)
diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml
index 44d3de6..b90d2c8 100644
--- a/.mokogitea/workflows/pre-release.yml
+++ b/.mokogitea/workflows/pre-release.yml
@@ -1,375 +1,388 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Release
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
-# PATH: /templates/workflows/universal/pre-release.yml.template
-# VERSION: 05.01.00
-# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
-
-name: "Universal: Pre-Release"
-
-on:
- 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 tools
- run: |
- # Update moko-platform CLI tools if available; install PHP if missing
- if command -v moko-platform-update &> /dev/null; then
- moko-platform-update
- elif [ -d "/opt/moko-platform" ]; then
- cd /opt/moko-platform && git pull origin main --quiet 2>/dev/null || true
- else
- 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
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
- /tmp/moko-platform-api
- fi
- # Set MOKO_CLI to whichever path exists
- if [ -d "/opt/moko-platform/cli" ]; then
- echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
- else
- echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
- fi
-
- - name: Detect platform
- id: platform
- run: |
- PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1 | tr -d '[:space:]')
- [ -z "$PLATFORM" ] && PLATFORM="generic"
- echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
- MANIFEST=$(find ./src -maxdepth 1 -name "pkg_*.xml" -exec grep -l '/dev/null | head -1)
- [ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "*/packages/*" -exec grep -l '/dev/null | head -1)
- [ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/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 and bump version
- 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
-
- # Patch bump via CLI tool
- php ${MOKO_CLI}/version_bump.php --path .
- VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
- [ -z "$VERSION" ] && VERSION="00.00.01"
- TODAY=$(date +%Y-%m-%d)
-
- # Update platform-specific manifest
- PLATFORM="${{ steps.platform.outputs.platform }}"
- MANIFEST="${{ steps.platform.outputs.manifest }}"
- MOD_FILE="${{ steps.platform.outputs.mod_file }}"
-
- php ${MOKO_CLI}/version_set_platform.php \
- --path . --version "$VERSION" --branch "${{ github.ref_name }}" 2>/dev/null || true
-
- # 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): pre-release bump to ${VERSION} [skip ci]"
- git push origin HEAD 2>&1
- }
-
- # Auto-detect element (platform-aware)
- EXT_ELEMENT=""
- case "$PLATFORM" in
- joomla)
- if [ -n "$MANIFEST" ]; then
- EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
- if [ -z "$EXT_ELEMENT" ]; then
- EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
- case "$EXT_ELEMENT" in
- templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
- esac
- fi
- else
- EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
- fi
- ;;
- dolibarr)
- 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
-
- MANIFEST="${{ steps.meta.outputs.manifest }}"
- EXT_TYPE=""
- if [ -n "$MANIFEST" ]; then
- EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- fi
-
- EXCLUDES="sftp-config* .ftpignore *.ppk *.pem *.key .env* *.local .build-trigger"
-
- mkdir -p build/package
-
- if [ "$EXT_TYPE" = "package" ] && [ -d "${SOURCE_DIR}/packages" ]; then
- echo "=== Building Joomla PACKAGE (multi-extension) ==="
- for ext_dir in "${SOURCE_DIR}"/packages/*/; do
- [ ! -d "$ext_dir" ] && continue
- EXT_NAME=$(basename "$ext_dir")
- echo " Packaging sub-extension: ${EXT_NAME}"
- cd "$ext_dir"
- zip -r "../../build/package/${EXT_NAME}.zip" . -x $EXCLUDES
- cd "$OLDPWD"
- done
- for f in "${SOURCE_DIR}"/*.xml "${SOURCE_DIR}"/*.php; do
- [ -f "$f" ] && cp "$f" build/package/
- done
- else
- echo "=== Building standard extension ==="
- rsync -a \
- --exclude='sftp-config*' \
- --exclude='.ftpignore' \
- --exclude='*.ppk' \
- --exclude='*.pem' \
- --exclude='*.key' \
- --exclude='.env*' \
- --exclude='*.local' \
- --exclude='.build-trigger' \
- "${SOURCE_DIR}/" build/package/
- fi
-
- - 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 }}"
-
- if [ ! -f "updates.xml" ]; then
- echo "No updates.xml -- skipping"
- exit 0
- fi
-
- # Map stability to XML tag name
- case "$STABILITY" in
- development) XML_TAG="development" ;;
- alpha) XML_TAG="alpha" ;;
- beta) XML_TAG="beta" ;;
- release-candidate) XML_TAG="rc" ;;
- *) XML_TAG="$STABILITY" ;;
- esac
-
- DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${TAG}/${ZIP_NAME}"
-
- # Use PHP to update the channel in updates.xml
- php -r '
- $xml_tag = $argv[1];
- $version = $argv[2];
- $sha256 = $argv[3];
- $url = $argv[4];
- $date = date("Y-m-d");
-
- $content = file_get_contents("updates.xml");
- $pattern = "/((?:(?!<\/update>).)*?" . preg_quote($xml_tag) . "<\/tag>.*?<\/update>)/s";
-
- $content = preg_replace_callback($pattern, function($m) use ($version, $sha256, $url, $date) {
- $block = $m[0];
- $block = preg_replace("/[^<]*<\/version>/", "{$version}", $block);
- if (strpos($block, "") !== false) {
- $block = preg_replace("/[^<]*<\/sha256>/", "{$sha256}", $block);
- } else {
- $block = str_replace("", "\n {$sha256}", $block);
- }
- $block = preg_replace("/(]*>)[^<]*(<\/downloadurl>)/", "\${1}{$url}\${2}", $block);
- return $block;
- }, $content);
-
- file_put_contents("updates.xml", $content);
- echo "Updated {$xml_tag} channel: version={$version}\n";
- ' "$XML_TAG" "$VERSION" "$SHA256" "$DOWNLOAD_URL"
-
- # Commit and push
- if ! git diff --quiet updates.xml 2>/dev/null; then
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git add updates.xml
- git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
- git push origin HEAD 2>&1 || echo "WARNING: push failed"
- fi
-
- - name: "Sync updates.xml to all branches"
- if: steps.platform.outputs.platform == 'joomla'
- run: |
- CURRENT_BRANCH="${{ github.ref_name }}"
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
-
- for BRANCH in main dev; do
- [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
- echo "Syncing updates.xml -> ${BRANCH}"
- git fetch origin "${BRANCH}" 2>/dev/null || continue
- git checkout "origin/${BRANCH}" -- . 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 }}"
-
- php ${MOKO_CLI}/release_cascade.php \
- --stability "${{ steps.meta.outputs.stability }}" \
- --token "${TOKEN}" \
- --api-base "${API_BASE}"
-
- - name: Summary
- if: always()
- run: |
- VERSION="${{ steps.meta.outputs.version }}"
- STABILITY="${{ steps.meta.outputs.stability }}"
- ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
- SHA256="${{ steps.zip.outputs.sha256 }}"
- echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
- echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
- echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
- echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
+# Copyright (C) 2026 Moko Consulting
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# FILE INFORMATION
+# DEFGROUP: Gitea.Workflow
+# INGROUP: moko-platform.Release
+# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
+# PATH: /templates/workflows/universal/pre-release.yml.template
+# VERSION: 05.01.00
+# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
+
+name: "Universal: Pre-Release"
+
+on:
+ pull_request:
+ types: [closed]
+ branches:
+ - dev
+ paths:
+ - 'src/**'
+ - 'htdocs/**'
+ workflow_dispatch:
+ inputs:
+ stability:
+ description: 'Pre-release channel'
+ required: true
+ type: choice
+ options:
+ - development
+ - alpha
+ - beta
+ - release-candidate
+
+permissions:
+ contents: write
+
+env:
+ GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
+ GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
+ GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
+
+jobs:
+ build:
+ name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
+ runs-on: release
+ if: >-
+ github.event_name == 'workflow_dispatch' ||
+ (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ token: ${{ secrets.GA_TOKEN }}
+
+ - name: Setup tools
+ run: |
+ # Update moko-platform CLI tools if available; install PHP if missing
+ if command -v moko-platform-update &> /dev/null; then
+ moko-platform-update
+ elif [ -d "/opt/moko-platform" ]; then
+ cd /opt/moko-platform && git pull origin main --quiet 2>/dev/null || true
+ else
+ 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
+ git clone --depth 1 --branch main --quiet \
+ "https://x-access-token:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
+ /tmp/moko-platform-api
+ fi
+ # Set MOKO_CLI to whichever path exists
+ if [ -d "/opt/moko-platform/cli" ]; then
+ echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
+ else
+ echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
+ fi
+
+ - name: Detect platform
+ id: platform
+ run: |
+ PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1 | tr -d '[:space:]')
+ [ -z "$PLATFORM" ] && PLATFORM="generic"
+ echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
+ MANIFEST=$(find ./src -maxdepth 1 -name "pkg_*.xml" -exec grep -l '/dev/null | head -1)
+ [ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "*/packages/*" -exec grep -l '/dev/null | head -1)
+ [ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/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 and bump version
+ id: meta
+ run: |
+ STABILITY="${{ inputs.stability || 'development' }}"
+
+ case "$STABILITY" in
+ development) SUFFIX="-dev"; TAG="development" ;;
+ alpha) SUFFIX="-alpha"; TAG="alpha" ;;
+ beta) SUFFIX="-beta"; TAG="beta" ;;
+ release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
+ esac
+
+ # Patch bump via CLI tool
+ php ${MOKO_CLI}/version_bump.php --path .
+ VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
+ [ -z "$VERSION" ] && VERSION="00.00.01"
+ TODAY=$(date +%Y-%m-%d)
+
+ # Update platform-specific manifest
+ PLATFORM="${{ steps.platform.outputs.platform }}"
+ MANIFEST="${{ steps.platform.outputs.manifest }}"
+ MOD_FILE="${{ steps.platform.outputs.mod_file }}"
+
+ php ${MOKO_CLI}/version_set_platform.php \
+ --path . --version "$VERSION" --branch "${{ github.ref_name }}" 2>/dev/null || true
+
+ # Verify version consistency across all files
+ php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
+
+ # 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): pre-release bump to ${VERSION} [skip ci]"
+ git push origin HEAD 2>&1
+ }
+
+ # Auto-detect element (platform-aware)
+ EXT_ELEMENT=""
+ case "$PLATFORM" in
+ joomla)
+ if [ -n "$MANIFEST" ]; then
+ EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
+ if [ -z "$EXT_ELEMENT" ]; then
+ EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
+ case "$EXT_ELEMENT" in
+ templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
+ esac
+ fi
+ else
+ EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
+ fi
+ ;;
+ dolibarr)
+ 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
+
+ MANIFEST="${{ steps.meta.outputs.manifest }}"
+ EXT_TYPE=""
+ if [ -n "$MANIFEST" ]; then
+ EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
+ fi
+
+ EXCLUDES="sftp-config* .ftpignore *.ppk *.pem *.key .env* *.local .build-trigger"
+
+ mkdir -p build/package
+
+ if [ "$EXT_TYPE" = "package" ] && [ -d "${SOURCE_DIR}/packages" ]; then
+ echo "=== Building Joomla PACKAGE (multi-extension) ==="
+ for ext_dir in "${SOURCE_DIR}"/packages/*/; do
+ [ ! -d "$ext_dir" ] && continue
+ EXT_NAME=$(basename "$ext_dir")
+ echo " Packaging sub-extension: ${EXT_NAME}"
+ cd "$ext_dir"
+ zip -r "../../build/package/${EXT_NAME}.zip" . -x $EXCLUDES
+ cd "$OLDPWD"
+ done
+ for f in "${SOURCE_DIR}"/*.xml "${SOURCE_DIR}"/*.php; do
+ [ -f "$f" ] && cp "$f" build/package/
+ done
+ else
+ echo "=== Building standard extension ==="
+ rsync -a \
+ --exclude='sftp-config*' \
+ --exclude='.ftpignore' \
+ --exclude='*.ppk' \
+ --exclude='*.pem' \
+ --exclude='*.key' \
+ --exclude='.env*' \
+ --exclude='*.local' \
+ --exclude='.build-trigger' \
+ "${SOURCE_DIR}/" build/package/
+ fi
+
+ - 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 }}"
+
+ if [ ! -f "updates.xml" ]; then
+ echo "No updates.xml -- skipping"
+ exit 0
+ fi
+
+ # Map stability to XML tag name
+ case "$STABILITY" in
+ development) XML_TAG="development" ;;
+ alpha) XML_TAG="alpha" ;;
+ beta) XML_TAG="beta" ;;
+ release-candidate) XML_TAG="rc" ;;
+ *) XML_TAG="$STABILITY" ;;
+ esac
+
+ DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${TAG}/${ZIP_NAME}"
+
+ # Use PHP to update the channel in updates.xml
+ php -r '
+ $xml_tag = $argv[1];
+ $version = $argv[2];
+ $sha256 = $argv[3];
+ $url = $argv[4];
+ $date = date("Y-m-d");
+
+ $content = file_get_contents("updates.xml");
+ $pattern = "/((?:(?!<\/update>).)*?" . preg_quote($xml_tag) . "<\/tag>.*?<\/update>)/s";
+
+ $content = preg_replace_callback($pattern, function($m) use ($version, $sha256, $url, $date) {
+ $block = $m[0];
+ $block = preg_replace("/[^<]*<\/version>/", "{$version}", $block);
+ if (strpos($block, "") !== false) {
+ $block = preg_replace("/[^<]*<\/sha256>/", "{$sha256}", $block);
+ } else {
+ $block = str_replace("", "\n {$sha256}", $block);
+ }
+ $block = preg_replace("/(]*>)[^<]*(<\/downloadurl>)/", "\${1}{$url}\${2}", $block);
+ return $block;
+ }, $content);
+
+ file_put_contents("updates.xml", $content);
+ echo "Updated {$xml_tag} channel: version={$version}\n";
+ ' "$XML_TAG" "$VERSION" "$SHA256" "$DOWNLOAD_URL"
+
+ # Commit and push
+ if ! git diff --quiet updates.xml 2>/dev/null; then
+ git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
+ git config --local user.name "gitea-actions[bot]"
+ git add updates.xml
+ git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
+ git push origin HEAD 2>&1 || echo "WARNING: push failed"
+ fi
+
+ - name: "Sync updates.xml to all branches"
+ if: steps.platform.outputs.platform == 'joomla'
+ run: |
+ CURRENT_BRANCH="${{ github.ref_name }}"
+ git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
+ git config --local user.name "gitea-actions[bot]"
+
+ for BRANCH in main dev; do
+ [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
+ echo "Syncing updates.xml -> ${BRANCH}"
+ git fetch origin "${BRANCH}" 2>/dev/null || continue
+ git checkout "origin/${BRANCH}" -- . 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 }}"
+
+ php ${MOKO_CLI}/release_cascade.php \
+ --stability "${{ steps.meta.outputs.stability }}" \
+ --token "${TOKEN}" \
+ --api-base "${API_BASE}"
+
+ - name: Summary
+ if: always()
+ run: |
+ VERSION="${{ steps.meta.outputs.version }}"
+ STABILITY="${{ steps.meta.outputs.stability }}"
+ ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
+ SHA256="${{ steps.zip.outputs.sha256 }}"
+ echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
+ echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
+ echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
+ echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
--
2.52.0
From 8e91f85c78983be375302de3fbe286d1f7e5209a Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Tue, 26 May 2026 17:36:28 +0000
Subject: [PATCH 03/40] chore(ci): update auto-release.yml from moko-platform
[skip ci]
---
.mokogitea/workflows/auto-release.yml | 1521 ++++++++++++-------------
1 file changed, 760 insertions(+), 761 deletions(-)
diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml
index 2201100..9eb98c8 100644
--- a/.mokogitea/workflows/auto-release.yml
+++ b/.mokogitea/workflows/auto-release.yml
@@ -1,761 +1,760 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Release
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
-# PATH: /templates/workflows/universal/auto-release.yml.template
-# VERSION: 05.00.00
-# BRIEF: Universal build & release � detects platform from manifest.xml
-#
-# +========================================================================+
-# | UNIVERSAL BUILD & RELEASE PIPELINE |
-# +========================================================================+
-# | |
-# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
-# | |
-# | Platform-specific: |
-# | joomla: XML manifest, updates.xml, type-prefixed packages |
-# | dolibarr: mod*.class.php, update.txt, dev version reset |
-# | generic: README-only, no update stream |
-# | |
-# +========================================================================+
-
-name: "Universal: Build & Release"
-
-on:
- push:
- branches:
- - main
- paths:
- - 'src/**'
- - 'htdocs/**'
- workflow_dispatch:
-
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
- GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
-
-permissions:
- contents: write
-
-jobs:
- release:
- name: Build & Release Pipeline
- runs-on: release
- if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- token: ${{ secrets.GA_TOKEN }}
- fetch-depth: 0
-
- - name: Setup moko-platform tools
- env:
- MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
- MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
- COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}'
- run: |
- # Ensure PHP + Composer are available
- if ! command -v composer &> /dev/null; then
- sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
- fi
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
- /tmp/moko-platform-api
- cd /tmp/moko-platform-api
- composer install --no-dev --no-interaction --quiet
-
-
- # -- PLATFORM DETECTION ---------------------------------------------------
- - name: Detect platform
- id: platform
- run: |
- php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output
- MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1 || true)
- MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1 || true)
- echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
- echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT"
-
- - name: "Step 1: Read version"
- id: version
- run: |
- VERSION=$(php /tmp/moko-platform-api/cli/version_read.php --path .)
- if [ -z "$VERSION" ]; then
- echo "::error::No VERSION in README.md"
- echo "skip=true" >> "$GITHUB_OUTPUT"
- exit 0
- fi
- MAJOR=$(echo "$VERSION" | cut -d. -f1)
- echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT"
- echo "skip=false" >> "$GITHUB_OUTPUT"
- echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT"
-
- - name: "Step 1b: Bump version"
- id: bump
- if: steps.version.outputs.skip != 'true'
- run: |
- MOKO_API="/tmp/moko-platform-api/cli"
- BUMP=$(php ${MOKO_API}/version_bump.php --path . --minor)
- VERSION=$(echo "$BUMP" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true)
- [ -z "$VERSION" ] && VERSION=$(php ${MOKO_API}/version_read.php --path .)
- echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- echo "Bumped to: ${VERSION}"
-
- - name: Check if already released
- if: steps.version.outputs.skip != 'true'
- id: check
- run: |
- TAG="${{ steps.version.outputs.release_tag }}"
- BRANCH="${{ steps.version.outputs.branch }}"
-
- TAG_EXISTS=false
- BRANCH_EXISTS=false
-
- git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true
- git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true
-
- echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT"
- echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT"
-
- # Tag and branch may persist across patch releases — never skip
- echo "already_released=false" >> "$GITHUB_OUTPUT"
-
- # -- SANITY CHECKS -------------------------------------------------------
- - name: "Sanity: Pre-release validation"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.check.outputs.already_released != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- ERRORS=0
-
- PLATFORM="${{ steps.platform.outputs.platform }}"
- MANIFEST="${{ steps.platform.outputs.manifest }}"
- MOD_FILE="${{ steps.platform.outputs.mod_file }}"
- echo "## Pre-Release Sanity Checks (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
-
- # -- Version drift check (must pass before release) --------
- README_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
- if [ "$README_VER" != "$VERSION" ]; then
- echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS+1))
- else
- echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
- fi
-
- # Check CHANGELOG version matches
- CL_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' CHANGELOG.md 2>/dev/null | head -1)
- if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then
- echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS+1))
- fi
-
- # Check composer.json version if present
- if [ -f "composer.json" ]; then
- COMP_VER=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' composer.json 2>/dev/null | head -1)
- if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then
- echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS+1))
- fi
- fi
-
- # Common checks
- if [ ! -f "LICENSE" ]; then
- echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS+1))
- else
- echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY
- fi
-
- if [ ! -d "src" ] && [ ! -d "htdocs" ]; then
- echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY
- else
- echo "- Source directory present" >> $GITHUB_STEP_SUMMARY
- fi
-
- # -- Platform-specific checks --------
- case "$PLATFORM" in
- joomla)
- if [ -n "$MANIFEST" ]; then
- XML_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
- if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then
- echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS+1))
- else
- echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
- fi
- TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null)
- echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY
- else
- echo "- No Joomla XML manifest (WaaS site)" >> $GITHUB_STEP_SUMMARY
- fi ;;
- dolibarr)
- if [ -n "$MOD_FILE" ]; then
- MOD_VER=$(sed -n "s/.*\\\$this->version = '\([^']*\)'.*/\1/p" "$MOD_FILE" 2>/dev/null | head -1)
- if [ -n "$MOD_VER" ] && [ "$MOD_VER" != "$VERSION" ]; then
- echo "- Module drift: \`${MOD_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS+1))
- else
- echo "- Module version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
- fi
- else
- echo "- No mod*.class.php found" >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS+1))
- fi
- if [ ! -f "update.txt" ]; then
- echo "- Missing update.txt" >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS+1))
- fi ;;
- *) echo "- Generic platform � no manifest checks" >> $GITHUB_STEP_SUMMARY ;;
- esac
-
- echo "" >> $GITHUB_STEP_SUMMARY
- if [ "$ERRORS" -gt 0 ]; then
- echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY
- else
- echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY
- fi
-
- # -- STEP 2: Create or update version/XX.YY archive branch ---------------
- # Always runs — every version change on main archives to version/XX.YY
- - name: "Step 2: Version archive branch"
- if: steps.check.outputs.already_released != 'true'
- run: |
- BRANCH="${{ steps.version.outputs.branch }}"
- IS_MINOR="${{ steps.version.outputs.is_minor }}"
- PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}')
-
- # Check if branch exists
- if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
- git push origin HEAD:"$BRANCH" --force
- echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY
- else
- git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH"
- git push origin "$BRANCH" --force
- echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
- fi
-
- # -- STEP 3: Set platform version ----------------------------------------
- - name: "Step 3: Set platform version"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.check.outputs.already_released != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- php /tmp/moko-platform-api/cli/version_set_platform.php \
- --path . --version "$VERSION" --branch main
-
- # -- STEP 4: Update version badges ----------------------------------------
- - name: "Step 4: Update version badges"
- if: steps.version.outputs.skip != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true
-
- - name: "Step 5: Write update stream"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.platform.outputs.platform == 'joomla'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- php /tmp/moko-platform-api/cli/updates_xml_build.php \
- --path . --version "${VERSION}" --stability stable \
- --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
- --github-output
-
- - name: Commit release changes
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.check.outputs.already_released != 'true'
- run: |
- if git diff --quiet && git diff --cached --quiet; then
- echo "No changes to commit"
- exit 0
- fi
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- # Set push URL with token for branch-protected repos
- git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- git add -A
- git commit -m "chore(release): build ${VERSION} [skip ci]" \
- --author="gitea-actions[bot] "
- git push -u origin HEAD
-
- # -- STEP 6: Create tag ---------------------------------------------------
- - name: "Step 6: Create git tag"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.check.outputs.tag_exists != 'true' &&
- steps.version.outputs.is_minor == 'true'
- run: |
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- # Only create the major release tag if it doesn't exist yet
- if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then
- git tag "$RELEASE_TAG"
- git push origin "$RELEASE_TAG"
- echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
- else
- echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY
- fi
- echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
-
- # -- STEP 7: Create or update Gitea Release --------------------------------
- - name: "Step 7: Gitea Release"
- if: >-
- steps.version.outputs.skip != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- BRANCH="${{ steps.version.outputs.branch }}"
- MAJOR="${{ steps.version.outputs.major }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
-
- # Reuse metadata from Step 5 (single source of truth)
- EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
- EXT_NAME="${{ steps.updates.outputs.ext_name }}"
- EXT_TYPE="${{ steps.updates.outputs.ext_type }}"
- EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}"
-
- # Fallbacks if Step 5 was skipped
- if [ -z "$EXT_ELEMENT" ]; then
- EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
- fi
- [ -z "$EXT_NAME" ] && EXT_NAME="${GITEA_REPO}"
-
- NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null)
- [ -z "$NOTES" ] && NOTES="Release ${VERSION}"
-
- # Build release name: "Pretty Name VERSION (type_element-VERSION)"
- TYPE_PREFIX=""
- case "${EXT_TYPE}" in
- plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
- module) TYPE_PREFIX="mod_" ;;
- component) TYPE_PREFIX="com_" ;;
- template) TYPE_PREFIX="tpl_" ;;
- library) TYPE_PREFIX="lib_" ;;
- package) TYPE_PREFIX="pkg_" ;;
- esac
- RELEASE_NAME="${EXT_NAME} ${VERSION} (${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION})"
-
- # Delete existing release if present (overwrite, not append)
- EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
- EXISTING_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true)
-
- if [ -n "$EXISTING_ID" ]; then
- curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- "${API_BASE}/releases/${EXISTING_ID}" 2>/dev/null || true
- curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- "${API_BASE}/tags/${RELEASE_TAG}" 2>/dev/null || true
- echo "Deleted previous stable release (id: ${EXISTING_ID})"
- fi
-
- # Create fresh release
- curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- -H "Content-Type: application/json" \
- "${API_BASE}/releases" \
- -d "$(python3 -c "import json; print(json.dumps({
- 'tag_name': '${RELEASE_TAG}',
- 'name': '${RELEASE_NAME}',
- 'body': '''## ${VERSION} ($(date +%Y-%m-%d))\n${NOTES}''',
- 'target_commitish': '${BRANCH}'
- }))")"
- echo "Release created: ${RELEASE_NAME}" >> $GITHUB_STEP_SUMMARY
-
- # -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------
- - name: "Step 8: Build package and update checksum"
- if: >-
- steps.version.outputs.skip != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- REPO="${{ github.repository }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
-
- # All ZIPs upload to the major release tag (vXX)
- RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
- RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
- if [ -z "$RELEASE_ID" ]; then
- echo "No release ${RELEASE_TAG} found — skipping ZIP upload"
- exit 0
- fi
-
- # Find extension element name from manifest
- MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1 || true)
- [ -z "$MANIFEST" ] && exit 0
-
- # Reuse element from Step 5, with same fallback chain
- EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
- if [ -z "$EXT_ELEMENT" ]; then
- EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
- [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
- [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
- [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
- fi
- # ZIP name: type_folder_element-VERSION (e.g. plg_system_mokojgdpc-01.01.00.zip)
- EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- TYPE_PREFIX=""
- case "${EXT_TYPE}" in
- plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
- module) TYPE_PREFIX="mod_" ;;
- component) TYPE_PREFIX="com_" ;;
- template) TYPE_PREFIX="tpl_" ;;
- library) TYPE_PREFIX="lib_" ;;
- package) TYPE_PREFIX="pkg_" ;;
- esac
- ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
- TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
-
- # -- Build install packages from src/ ----------------------------
- SOURCE_DIR="src"
- [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
- [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/"; exit 0; }
-
- # ZIP package (type-aware via moko-platform PHP API)
- php /tmp/moko-platform-api/cli/joomla_build.php --path . --version "${VERSION}" --output /tmp
- # Match the expected ZIP_NAME for upload
- BUILT_ZIP=$(ls /tmp/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip 2>/dev/null | head -1 || true)
- if [ -n "$BUILT_ZIP" ] && [ "$BUILT_ZIP" != "/tmp/${ZIP_NAME}" ]; then
- mv "$BUILT_ZIP" "/tmp/${ZIP_NAME}"
- fi
-
- # tar.gz package (flat source archive)
- tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" --exclude='.ftpignore' --exclude='sftp-config*' --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
-
- ZIP_SIZE=$(stat -c%s "/tmp/${ZIP_NAME}" 2>/dev/null || stat -f%z "/tmp/${ZIP_NAME}" 2>/dev/null || echo "unknown")
- TAR_SIZE=$(stat -c%s "/tmp/${TAR_NAME}" 2>/dev/null || stat -f%z "/tmp/${TAR_NAME}" 2>/dev/null || echo "unknown")
-
- # -- Calculate SHA-256 for both ----------------------------------
- SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1)
- SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1)
-
- # -- Delete existing assets with same name before uploading ------
- ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- "${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]")
- for ASSET_NAME in "$ZIP_NAME" "$TAR_NAME"; do
- ASSET_ID=$(echo "$ASSETS" | python3 -c "
- import sys,json
- assets = json.load(sys.stdin)
- for a in assets:
- if a['name'] == '${ASSET_NAME}':
- print(a['id']); break
- " 2>/dev/null || true)
- if [ -n "$ASSET_ID" ]; then
- curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- "${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true
- fi
- done
-
- # -- Upload both to release tag ----------------------------------
- curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- -H "Content-Type: application/octet-stream" \
- --data-binary @"/tmp/${ZIP_NAME}" \
- "${API_BASE}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" > /dev/null 2>&1 || true
-
- curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- -H "Content-Type: application/octet-stream" \
- --data-binary @"/tmp/${TAR_NAME}" \
- "${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true
-
- # -- Update updates.xml with both download formats ---------------
- if [ -f "updates.xml" ]; then
- ZIP_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}"
- TAR_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${TAR_NAME}"
-
- # Use Python to update only the stable entry's downloads + sha256
- export PY_ZIP_URL="$ZIP_URL" PY_TAR_URL="$TAR_URL" PY_SHA="$SHA256_ZIP"
- python3 << 'PYEOF'
- import re, os
-
- with open("updates.xml") as f:
- content = f.read()
-
- zip_url = os.environ["PY_ZIP_URL"]
- tar_url = os.environ["PY_TAR_URL"]
- sha = os.environ["PY_SHA"]
-
- # Find the stable update block and replace its downloads + sha256
- def replace_stable(m):
- block = m.group(0)
- # Replace downloads block
- new_downloads = (
- " \n"
- f" {zip_url}\n"
- " "
- )
- block = re.sub(r' .*?', new_downloads, block, flags=re.DOTALL)
- # Add or replace sha256
- if '' in block:
- block = re.sub(r' .*?', f' {sha}', block)
- else:
- block = block.replace('', f'\n {sha}')
- return block
-
- content = re.sub(
- r' .*?stable.*?',
- replace_stable,
- content,
- flags=re.DOTALL
- )
-
- with open("updates.xml", "w") as f:
- f.write(content)
- PYEOF
-
- CURRENT_BRANCH="${{ github.ref_name }}"
- git add updates.xml
- git commit -m "chore(release): ZIP + tar.gz for ${VERSION} [skip ci]" \
- --author="gitea-actions[bot] " || true
- git push || true
-
- # Sync updates.xml to main via direct API (always runs — may be on version/XX branch)
- GA_TOKEN="${{ secrets.GA_TOKEN }}"
- API="${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}"
-
- FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
- "${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty')
-
- if [ -n "$FILE_SHA" ]; then
- CONTENT=$(base64 -w0 updates.xml)
- curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \
- -H "Content-Type: application/json" \
- "${API}/contents/updates.xml" \
- -d "$(jq -n \
- --arg content "$CONTENT" \
- --arg sha "$FILE_SHA" \
- --arg msg "chore: sync updates.xml ${VERSION} [skip ci]" \
- --arg branch "main" \
- '{content: $content, sha: $sha, message: $msg, branch: $branch}'
- )" > /dev/null 2>&1 \
- && echo "updates.xml synced to main via API" \
- || echo "WARNING: failed to sync updates.xml to main"
- else
- echo "WARNING: could not get updates.xml SHA from main"
- fi
- fi
-
- echo "### Packages" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "| Package | Size | SHA-256 |" >> $GITHUB_STEP_SUMMARY
- echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY
- echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY
- echo "| Download | [${ZIP_NAME}](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}) |" >> $GITHUB_STEP_SUMMARY
-
- # -- STEP 8b: Update release description with changelog + SHA ----------------
- - name: "Step 8b: Update release body with changelog and SHA"
- if: steps.version.outputs.skip != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
- EXT_TYPE="${{ steps.updates.outputs.ext_type }}"
- EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}"
-
- # Build TYPE_PREFIX to match Step 8's ZIP naming
- TYPE_PREFIX=""
- case "${EXT_TYPE}" in
- plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
- module) TYPE_PREFIX="mod_" ;;
- component) TYPE_PREFIX="com_" ;;
- template) TYPE_PREFIX="tpl_" ;;
- library) TYPE_PREFIX="lib_" ;;
- package) TYPE_PREFIX="pkg_" ;;
- esac
- ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
- TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
-
- # Get SHA from the built files
- SHA256_ZIP=""
- [ -f "/tmp/${ZIP_NAME}" ] && SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1)
- SHA256_TAR=""
- [ -f "/tmp/${TAR_NAME}" ] && SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1)
-
- # Extract latest changelog entry (strip the ## header to avoid duplicate)
- CHANGELOG=""
- if [ -f "CHANGELOG.md" ]; then
- CHANGELOG=$(sed -n "/^## \[*${VERSION}/,/^## \[*[0-9]/p" CHANGELOG.md | sed '$d' | sed '1d')
- [ -z "$CHANGELOG" ] && CHANGELOG=$(sed -n '/^## /,/^## /p' CHANGELOG.md | sed '$d' | sed '1d' | head -30)
- fi
-
- # Build release body (single header, no duplicate from changelog)
- BODY="## ${VERSION} ($(date +%Y-%m-%d))\n\n"
- if [ -n "$CHANGELOG" ]; then
- BODY="${BODY}${CHANGELOG}\n\n"
- fi
- BODY="${BODY}---\n\n### Checksums\n\n"
- BODY="${BODY}| File | SHA-256 |\n|------|--------|\n"
- [ -n "$SHA256_ZIP" ] && BODY="${BODY}| \`${ZIP_NAME}\` | \`${SHA256_ZIP}\` |\n"
- [ -n "$SHA256_TAR" ] && BODY="${BODY}| \`${TAR_NAME}\` | \`${SHA256_TAR}\` |\n"
-
- # Get release ID and update body
- RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null | \
- python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
-
- if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then
- python3 -c "
- import json, urllib.request
- body = '''$(printf '%b' "$BODY")'''
- data = json.dumps({'body': body}).encode()
- req = urllib.request.Request(
- '${API_BASE}/releases/${RELEASE_ID}',
- data=data,
- headers={'Authorization': 'token ${{ secrets.GA_TOKEN }}', 'Content-Type': 'application/json'},
- method='PATCH'
- )
- urllib.request.urlopen(req)
- " 2>/dev/null && echo "Release body updated with changelog + SHA" >> $GITHUB_STEP_SUMMARY
- fi
-
- # -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- - name: "Step 9: Mirror release to GitHub"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.version.outputs.stability == 'stable' &&
- secrets.GH_TOKEN != ''
- continue-on-error: true
- env:
- GH_TOKEN: ${{ secrets.GH_TOKEN }}
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- MAJOR="${{ steps.version.outputs.major }}"
- BRANCH="${{ steps.version.outputs.branch }}"
- GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
-
- NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true)
- [ -z "$NOTES" ] && NOTES="Release ${VERSION}"
- echo "$NOTES" > /tmp/release_notes.md
-
- EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".tag_name // empty" || true)
-
- if [ -z "$EXISTING" ]; then
- gh release create "$RELEASE_TAG" \
- --repo "$GH_REPO" \
- --title "v${MAJOR} (latest: ${VERSION})" \
- --notes-file /tmp/release_notes.md \
- --target "$BRANCH" || true
- else
- gh release edit "$RELEASE_TAG" \
- --repo "$GH_REPO" \
- --title "v${MAJOR} (latest: ${VERSION})" || true
- fi
-
- # Upload assets to GitHub mirror
- for PKG in /tmp/${EXT_ELEMENT:-pkg}-${VERSION}.*; do
- if [ -f "$PKG" ]; then
- _RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".id // empty")
- [ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true
- fi
- done
- echo "GitHub mirror updated: ${GH_REPO} ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
-
- # -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- - name: "Step 10: Push main to GitHub mirror"
- if: >-
- steps.version.outputs.skip != 'true' &&
- secrets.GH_TOKEN != ''
- continue-on-error: true
- run: |
- GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
- GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
- GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
- git remote add github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
- git remote set-url github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
- git fetch origin main --depth=1
- git push github origin/main:refs/heads/main --force 2>/dev/null \
- && echo "main branch pushed to GitHub mirror" \
- || echo "WARNING: GitHub mirror push failed"
-
- # -- Clean up lesser pre-releases (cascade) ---------------------------------
- # stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev
- - name: "Delete lesser pre-release channels"
- continue-on-error: true
- run: |
- php /tmp/moko-platform-api/cli/release_cascade.php \
- --stability stable \
- --token "${{ secrets.GA_TOKEN }}" \
- --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
- --gitea-url "${GITEA_URL}" 2>/dev/null || true
-
- - name: "Step 11: Delete and recreate dev branch from main"
- if: steps.version.outputs.skip != 'true'
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- TOKEN="${{ secrets.GA_TOKEN }}"
-
- # Delete dev branch
- curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
- "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
-
- # Recreate dev from main (now includes version bump + changelog promotion)
- curl -sf -X POST -H "Authorization: token ${TOKEN}" \
- -H "Content-Type: application/json" \
- "${API_BASE}/branches" \
- -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
-
- echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY
-
-
- # -- Dolibarr post-release: Reset dev version -----------------------------
- - name: "Dolibarr: Reset dev version"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.platform.outputs.platform == 'dolibarr' &&
- steps.platform.outputs.mod_file != ''
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- TOKEN="${{ secrets.GA_TOKEN }}"
- MOD_FILE="${{ steps.platform.outputs.mod_file }}"
- ENCODED_PATH=$(echo "$MOD_FILE" | sed 's|^\./||' | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))")
- FILE_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/${ENCODED_PATH}?ref=dev" 2>/dev/null || true)
- FILE_SHA=$(echo "$FILE_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
- FILE_CONTENT=$(echo "$FILE_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true)
- if [ -n "$FILE_SHA" ] && [ -n "$FILE_CONTENT" ]; then
- UPDATED=$(echo "$FILE_CONTENT" | sed "s/\$this->version = '[^']*'/\$this->version = 'development'/")
- ENCODED=$(echo "$UPDATED" | base64 -w0)
- curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/contents/${ENCODED_PATH}" \
- -d "$(jq -n --arg content \"$ENCODED\" --arg sha \"$FILE_SHA\" --arg msg \"chore(version): reset dev version [skip ci]\" --arg branch \"dev\" '{content:$content,sha:$sha,message:$msg,branch:$branch}')" > /dev/null 2>&1 || true
- fi
-
- # -- Summary --------------------------------------------------------------
- - name: Pipeline Summary
- if: always()
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- PLATFORM="${{ steps.platform.outputs.platform }}"
- if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
- echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
- echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
- elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
- echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
- else
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
- echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
- echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
- fi
+# Copyright (C) 2026 Moko Consulting
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# FILE INFORMATION
+# DEFGROUP: Gitea.Workflow
+# INGROUP: moko-platform.Release
+# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
+# PATH: /templates/workflows/universal/auto-release.yml.template
+# VERSION: 05.00.00
+# BRIEF: Universal build & release � detects platform from manifest.xml
+#
+# +========================================================================+
+# | UNIVERSAL BUILD & RELEASE PIPELINE |
+# +========================================================================+
+# | |
+# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
+# | |
+# | Platform-specific: |
+# | joomla: XML manifest, 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
+ paths:
+ - 'src/**'
+ - 'htdocs/**'
+ workflow_dispatch:
+
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+ GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
+ GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
+ GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
+
+permissions:
+ contents: write
+
+jobs:
+ # ── Draft PR → Promote highest pre-release to RC ─────────────────────────────
+ promote-rc:
+ name: Promote Pre-Release to RC
+ runs-on: release
+ if: >-
+ github.event.action == 'opened' &&
+ github.event.pull_request.draft == true
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ token: ${{ secrets.GA_TOKEN }}
+ fetch-depth: 1
+
+ - name: Setup moko-platform tools
+ env:
+ MOKO_CLONE_TOKEN: ${{ secrets.GA_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
+ 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: Promote to release-candidate
+ run: |
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ php /tmp/moko-platform-api/cli/release_promote.php \
+ --from auto --to release-candidate \
+ --token "${{ secrets.GA_TOKEN }}" \
+ --api-base "${API_BASE}" \
+ --branch "${{ github.event.pull_request.head.ref }}"
+
+ - name: Cascade lesser channels
+ continue-on-error: true
+ run: |
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ php /tmp/moko-platform-api/cli/release_cascade.php \
+ --stability release-candidate \
+ --token "${{ secrets.GA_TOKEN }}" \
+ --api-base "${API_BASE}"
+
+ - name: Summary
+ if: always()
+ run: |
+ echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
+ echo "Draft PR opened — promoted highest pre-release to RC" >> $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'
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ token: ${{ secrets.GA_TOKEN }}
+ fetch-depth: 0
+
+ - name: Setup moko-platform tools
+ env:
+ MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
+ MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
+ COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}'
+ run: |
+ # Ensure PHP + Composer are available
+ if ! command -v composer &> /dev/null; then
+ sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
+ fi
+ git clone --depth 1 --branch main --quiet \
+ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
+ /tmp/moko-platform-api
+ cd /tmp/moko-platform-api
+ composer install --no-dev --no-interaction --quiet
+
+
+ # -- PLATFORM DETECTION ---------------------------------------------------
+ - name: Detect platform
+ id: platform
+ run: |
+ php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output
+ MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1 || true)
+ MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1 || true)
+ echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
+ echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT"
+
+ - name: "Step 1: Read version"
+ id: version
+ run: |
+ VERSION=$(php /tmp/moko-platform-api/cli/version_read.php --path .)
+ if [ -z "$VERSION" ]; then
+ echo "::error::No VERSION in README.md"
+ echo "skip=true" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+ MAJOR=$(echo "$VERSION" | cut -d. -f1)
+ echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
+ echo "release_tag=stable" >> "$GITHUB_OUTPUT"
+ echo "skip=false" >> "$GITHUB_OUTPUT"
+ echo "branch=main" >> "$GITHUB_OUTPUT"
+
+ # -- CHECK FOR RC PROMOTION ------------------------------------------------
+ - name: "Check for RC release"
+ id: rc
+ if: steps.version.outputs.skip != 'true'
+ run: |
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ RC_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
+ "${API_BASE}/releases/tags/release-candidate" 2>/dev/null || echo "{}")
+ RC_ID=$(echo "$RC_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true)
+
+ if [ -n "$RC_ID" ] && [ "$RC_ID" != "None" ] && [ "$RC_ID" != "" ]; then
+ echo "promote=true" >> "$GITHUB_OUTPUT"
+ echo "release_id=${RC_ID}" >> "$GITHUB_OUTPUT"
+ echo "::notice::RC release found (id: ${RC_ID}) — will promote to stable"
+ else
+ echo "promote=false" >> "$GITHUB_OUTPUT"
+ echo "::notice::No RC release — full build pipeline"
+ fi
+
+ - name: "Step 1b: Bump version"
+ id: bump
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.rc.outputs.promote != 'true'
+ run: |
+ MOKO_API="/tmp/moko-platform-api/cli"
+ BUMP=$(php ${MOKO_API}/version_bump.php --path . --minor)
+ VERSION=$(echo "$BUMP" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true)
+ [ -z "$VERSION" ] && VERSION=$(php ${MOKO_API}/version_read.php --path .)
+ echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
+ echo "Bumped to: ${VERSION}"
+
+ - name: Check if already released
+ if: steps.version.outputs.skip != 'true'
+ id: check
+ run: |
+ TAG="${{ steps.version.outputs.release_tag }}"
+ BRANCH="${{ steps.version.outputs.branch }}"
+
+ TAG_EXISTS=false
+ BRANCH_EXISTS=false
+
+ git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true
+ git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true
+
+ echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT"
+ echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT"
+
+ # Tag and branch may persist across patch releases — never skip
+ echo "already_released=false" >> "$GITHUB_OUTPUT"
+
+ # -- SANITY CHECKS -------------------------------------------------------
+ - name: "Sanity: Pre-release validation"
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.check.outputs.already_released != 'true'
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ ERRORS=0
+
+ PLATFORM="${{ steps.platform.outputs.platform }}"
+ MANIFEST="${{ steps.platform.outputs.manifest }}"
+ MOD_FILE="${{ steps.platform.outputs.mod_file }}"
+ echo "## Pre-Release Sanity Checks (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+
+ # -- Version drift check (must pass before release) --------
+ README_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
+ if [ "$README_VER" != "$VERSION" ]; then
+ echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
+ ERRORS=$((ERRORS+1))
+ else
+ echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
+ fi
+
+ # Check CHANGELOG version matches
+ CL_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' CHANGELOG.md 2>/dev/null | head -1)
+ if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then
+ echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
+ ERRORS=$((ERRORS+1))
+ fi
+
+ # Check composer.json version if present
+ if [ -f "composer.json" ]; then
+ COMP_VER=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' composer.json 2>/dev/null | head -1)
+ if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then
+ echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
+ ERRORS=$((ERRORS+1))
+ fi
+ fi
+
+ # Common checks
+ if [ ! -f "LICENSE" ]; then
+ echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY
+ ERRORS=$((ERRORS+1))
+ else
+ echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY
+ fi
+
+ if [ ! -d "src" ] && [ ! -d "htdocs" ]; then
+ echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY
+ else
+ echo "- Source directory present" >> $GITHUB_STEP_SUMMARY
+ fi
+
+ # -- Platform-specific checks --------
+ case "$PLATFORM" in
+ joomla)
+ if [ -n "$MANIFEST" ]; then
+ XML_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
+ if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then
+ echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
+ ERRORS=$((ERRORS+1))
+ else
+ echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
+ fi
+ TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null)
+ echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY
+ else
+ echo "- No Joomla XML manifest (WaaS site)" >> $GITHUB_STEP_SUMMARY
+ fi ;;
+ dolibarr)
+ if [ -n "$MOD_FILE" ]; then
+ MOD_VER=$(sed -n "s/.*\\\$this->version = '\([^']*\)'.*/\1/p" "$MOD_FILE" 2>/dev/null | head -1)
+ if [ -n "$MOD_VER" ] && [ "$MOD_VER" != "$VERSION" ]; then
+ echo "- Module drift: \`${MOD_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
+ ERRORS=$((ERRORS+1))
+ else
+ echo "- Module version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
+ fi
+ else
+ echo "- No mod*.class.php found" >> $GITHUB_STEP_SUMMARY
+ ERRORS=$((ERRORS+1))
+ fi
+ if [ ! -f "update.txt" ]; then
+ echo "- Missing update.txt" >> $GITHUB_STEP_SUMMARY
+ ERRORS=$((ERRORS+1))
+ fi ;;
+ *) echo "- Generic platform � no manifest checks" >> $GITHUB_STEP_SUMMARY ;;
+ esac
+
+ echo "" >> $GITHUB_STEP_SUMMARY
+ if [ "$ERRORS" -gt 0 ]; then
+ echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY
+ else
+ echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY
+ fi
+
+ # -- STEP 2: Create or update version/XX.YY archive branch ---------------
+ # Always runs — every version change on main archives to version/XX.YY
+ - name: "Step 2: Version archive branch"
+ if: steps.check.outputs.already_released != 'true'
+ run: |
+ BRANCH="${{ steps.version.outputs.branch }}"
+ IS_MINOR="${{ steps.version.outputs.is_minor }}"
+ PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}')
+
+ # Check if branch exists
+ if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
+ git push origin HEAD:"$BRANCH" --force
+ echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY
+ else
+ git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH"
+ git push origin "$BRANCH" --force
+ echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
+ fi
+
+ # -- STEP 3: Set platform version ----------------------------------------
+ - name: "Step 3: Set platform version"
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.check.outputs.already_released != 'true'
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ php /tmp/moko-platform-api/cli/version_set_platform.php \
+ --path . --version "$VERSION" --branch main
+
+ # -- STEP 4: Update version badges ----------------------------------------
+ - name: "Step 4: Update version badges"
+ if: steps.version.outputs.skip != 'true'
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true
+ php /tmp/moko-platform-api/cli/version_check.php --path . --fix 2>/dev/null || true
+
+ - name: "Step 5: Write update stream"
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.platform.outputs.platform == 'joomla'
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+
+ # Fetch latest updates.xml from main so preserve logic has all channels
+ GA_TOKEN="${{ secrets.GA_TOKEN }}"
+ API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
+ curl -sf -H "Authorization: token ${GA_TOKEN}" \
+ "${API}/contents/updates.xml?ref=main" 2>/dev/null | \
+ python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin)['content']).decode())" \
+ > updates.xml 2>/dev/null || true
+
+ php /tmp/moko-platform-api/cli/updates_xml_build.php \
+ --path . --version "${VERSION}" --stability stable \
+ --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
+ --github-output
+
+ - name: Commit release changes
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.check.outputs.already_released != 'true'
+ run: |
+ if git diff --quiet && git diff --cached --quiet; then
+ echo "No changes to commit"
+ exit 0
+ fi
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
+ git config --local user.name "gitea-actions[bot]"
+ # Set push URL with token for branch-protected repos
+ git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
+ git add -A
+ git commit -m "chore(release): build ${VERSION} [skip ci]" \
+ --author="gitea-actions[bot] "
+ git push -u origin HEAD
+
+ # -- STEP 6: Create tag ---------------------------------------------------
+ - name: "Step 6: Create git tag"
+ if: >-
+ steps.version.outputs.skip != 'true'
+ run: |
+ RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
+ # Only create the major release tag if it doesn't exist yet
+ if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then
+ git tag "$RELEASE_TAG"
+ git push origin "$RELEASE_TAG"
+ echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
+ else
+ echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY
+ fi
+ echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
+
+ # -- STEP 7a: Promote RC to stable (skip build) ----------------------------
+ - name: "Step 7a: Promote RC to stable"
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.rc.outputs.promote == 'true'
+ run: |
+ VERSION="${{ steps.version.outputs.version }}"
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ php /tmp/moko-platform-api/cli/release_promote.php \
+ --from release-candidate --to stable \
+ --token "${{ secrets.GA_TOKEN }}" \
+ --api-base "${API_BASE}" \
+ --path . --branch main
+ echo "Promoted RC → stable (${VERSION})" >> $GITHUB_STEP_SUMMARY
+
+ # -- STEP 7b: Create or update Gitea Release (full build path) -------------
+ - name: "Step 7b: Gitea Release"
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.rc.outputs.promote != 'true'
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
+ BRANCH="${{ steps.version.outputs.branch }}"
+ MAJOR="${{ steps.version.outputs.major }}"
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+
+ # Reuse metadata from Step 5 (single source of truth)
+ EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
+ EXT_NAME="${{ steps.updates.outputs.ext_name }}"
+ EXT_TYPE="${{ steps.updates.outputs.ext_type }}"
+ EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}"
+
+ # Fallbacks if Step 5 was skipped
+ if [ -z "$EXT_ELEMENT" ]; then
+ EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
+ fi
+ [ -z "$EXT_NAME" ] && EXT_NAME="${GITEA_REPO}"
+
+ NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null)
+ [ -z "$NOTES" ] && NOTES="Release ${VERSION}"
+
+ # Build release name: "Pretty Name VERSION (type_element-VERSION)"
+ # Strip existing type prefix to prevent duplication
+ EXT_ELEMENT=$(echo "$EXT_ELEMENT" | sed -E 's/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)//')
+ TYPE_PREFIX=""
+ case "${EXT_TYPE}" in
+ plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
+ module) TYPE_PREFIX="mod_" ;;
+ component) TYPE_PREFIX="com_" ;;
+ template) TYPE_PREFIX="tpl_" ;;
+ library) TYPE_PREFIX="lib_" ;;
+ package) TYPE_PREFIX="pkg_" ;;
+ esac
+ RELEASE_NAME="${EXT_NAME} ${VERSION} (${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION})"
+
+ # Delete existing release if present (overwrite, not append)
+ EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
+ "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
+ EXISTING_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true)
+
+ if [ -n "$EXISTING_ID" ]; then
+ curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
+ "${API_BASE}/releases/${EXISTING_ID}" 2>/dev/null || true
+ curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
+ "${API_BASE}/tags/${RELEASE_TAG}" 2>/dev/null || true
+ echo "Deleted previous stable release (id: ${EXISTING_ID})"
+ fi
+
+ # Create fresh release
+ curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
+ -H "Content-Type: application/json" \
+ "${API_BASE}/releases" \
+ -d "$(python3 -c "import json; print(json.dumps({
+ 'tag_name': '${RELEASE_TAG}',
+ 'name': '${RELEASE_NAME}',
+ 'body': '''## ${VERSION} ($(date +%Y-%m-%d))\n${NOTES}''',
+ 'target_commitish': '${BRANCH}'
+ }))")"
+ echo "Release created: ${RELEASE_NAME}" >> $GITHUB_STEP_SUMMARY
+
+ # -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------
+ - name: "Step 8: Build package and update checksum"
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.rc.outputs.promote != 'true'
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
+ REPO="${{ github.repository }}"
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+
+ # All ZIPs upload to the major release tag (vXX)
+ RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
+ "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
+ RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
+ if [ -z "$RELEASE_ID" ]; then
+ echo "No release ${RELEASE_TAG} found — skipping ZIP upload"
+ exit 0
+ fi
+
+ # Find extension element name from manifest
+ MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1 || true)
+ [ -z "$MANIFEST" ] && exit 0
+
+ # Reuse element from Step 5, with same fallback chain
+ EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
+ if [ -z "$EXT_ELEMENT" ]; then
+ EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
+ [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
+ [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
+ [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
+ fi
+ # ZIP name: type_folder_element-VERSION (e.g. plg_system_mokojgdpc-01.01.00.zip)
+ EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
+ EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
+ # For packages, prefer over filename-derived element
+ if [ "$EXT_TYPE" = "package" ]; then
+ PKG_NAME=$(sed -n 's/.*\([^<]*\)<\/packagename>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
+ [ -n "$PKG_NAME" ] && EXT_ELEMENT="$PKG_NAME"
+ fi
+ # Strip existing type prefix to prevent duplication (e.g. pkg_mokowaas → mokowaas)
+ EXT_ELEMENT=$(echo "$EXT_ELEMENT" | sed -E 's/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)//')
+ TYPE_PREFIX=""
+ case "${EXT_TYPE}" in
+ plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
+ module) TYPE_PREFIX="mod_" ;;
+ component) TYPE_PREFIX="com_" ;;
+ template) TYPE_PREFIX="tpl_" ;;
+ library) TYPE_PREFIX="lib_" ;;
+ package) TYPE_PREFIX="pkg_" ;;
+ esac
+ ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
+ TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
+
+ # -- Build install packages from src/ ----------------------------
+ SOURCE_DIR="src"
+ [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
+ [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/"; exit 0; }
+
+ # ZIP package (type-aware via moko-platform PHP API)
+ php /tmp/moko-platform-api/cli/joomla_build.php --path . --version "${VERSION}" --output /tmp
+ # Match the expected ZIP_NAME for upload
+ BUILT_ZIP=$(ls /tmp/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip 2>/dev/null | head -1 || true)
+ if [ -n "$BUILT_ZIP" ] && [ "$BUILT_ZIP" != "/tmp/${ZIP_NAME}" ]; then
+ mv "$BUILT_ZIP" "/tmp/${ZIP_NAME}"
+ fi
+
+ # tar.gz package (flat source archive)
+ tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" --exclude='.ftpignore' --exclude='sftp-config*' --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
+
+ ZIP_SIZE=$(stat -c%s "/tmp/${ZIP_NAME}" 2>/dev/null || stat -f%z "/tmp/${ZIP_NAME}" 2>/dev/null || echo "unknown")
+ TAR_SIZE=$(stat -c%s "/tmp/${TAR_NAME}" 2>/dev/null || stat -f%z "/tmp/${TAR_NAME}" 2>/dev/null || echo "unknown")
+
+ # -- Calculate SHA-256 for both ----------------------------------
+ SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1)
+ SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1)
+
+ # -- Get existing assets for cleanup --------------------------------
+ ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
+ "${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]")
+
+ # -- Create per-file .sha256 checksum files -------------------------
+ echo "${SHA256_ZIP} ${ZIP_NAME}" > "/tmp/${ZIP_NAME}.sha256"
+ echo "${SHA256_TAR} ${TAR_NAME}" > "/tmp/${TAR_NAME}.sha256"
+
+ # -- Upload packages + checksums to release tag --------------------
+ for ASSET in "${ZIP_NAME}" "${TAR_NAME}" "${ZIP_NAME}.sha256" "${TAR_NAME}.sha256"; do
+ [ ! -f "/tmp/${ASSET}" ] && continue
+ # Delete existing asset with same name
+ ASSET_ID=$(echo "$ASSETS" | python3 -c "
+ import sys,json
+ assets = json.load(sys.stdin)
+ for a in assets:
+ if a['name'] == '${ASSET}':
+ print(a['id']); break
+ " 2>/dev/null || true)
+ [ -n "$ASSET_ID" ] && curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
+ "${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true
+ # Upload
+ curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
+ -H "Content-Type: application/octet-stream" \
+ --data-binary @"/tmp/${ASSET}" \
+ "${API_BASE}/releases/${RELEASE_ID}/assets?name=${ASSET}" > /dev/null 2>&1 || true
+ done
+
+ # updates.xml already handled by Step 5 (updates_xml_build.php with preserve logic)
+
+ echo "### Packages" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "| Package | Size | SHA-256 |" >> $GITHUB_STEP_SUMMARY
+ echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY
+ echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY
+ echo "| Download | [${ZIP_NAME}](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}) |" >> $GITHUB_STEP_SUMMARY
+
+ # -- STEP 8b: Update release description with changelog ----------------------
+ - name: "Step 8b: Update release body"
+ if: steps.version.outputs.skip != 'true'
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
+ MOKO_CLI="/tmp/moko-platform-api/cli"
+
+ php ${MOKO_CLI}/release_body_update.php \
+ --path . --version "${VERSION}" --tag "${RELEASE_TAG}" \
+ --token "${{ secrets.GA_TOKEN }}" \
+ --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
+ 2>/dev/null || {
+ # Fallback: simple body update if CLI not available
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
+ "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null | \
+ python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
+ if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then
+ BODY="## ${VERSION} ($(date +%Y-%m-%d))\n\nChecksum files attached as \`*.sha256\` assets."
+ curl -sf -X PATCH -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
+ -H "Content-Type: application/json" \
+ "${API_BASE}/releases/${RELEASE_ID}" \
+ -d "{\"body\":\"${BODY}\"}" > /dev/null 2>&1
+ fi
+ }
+ echo "Release body updated" >> $GITHUB_STEP_SUMMARY
+
+ # -- STEP 9: Mirror to GitHub (stable only) --------------------------------
+ - name: "Step 9: Mirror release to GitHub"
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.version.outputs.stability == 'stable' &&
+ secrets.GH_TOKEN != ''
+ continue-on-error: true
+ env:
+ GH_TOKEN: ${{ secrets.GH_TOKEN }}
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
+ MAJOR="${{ steps.version.outputs.major }}"
+ BRANCH="${{ steps.version.outputs.branch }}"
+ GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
+
+ NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true)
+ [ -z "$NOTES" ] && NOTES="Release ${VERSION}"
+ echo "$NOTES" > /tmp/release_notes.md
+
+ EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".tag_name // empty" || true)
+
+ if [ -z "$EXISTING" ]; then
+ gh release create "$RELEASE_TAG" \
+ --repo "$GH_REPO" \
+ --title "v${MAJOR} (latest: ${VERSION})" \
+ --notes-file /tmp/release_notes.md \
+ --target "$BRANCH" || true
+ else
+ gh release edit "$RELEASE_TAG" \
+ --repo "$GH_REPO" \
+ --title "v${MAJOR} (latest: ${VERSION})" || true
+ fi
+
+ # Upload assets to GitHub mirror
+ for PKG in /tmp/${EXT_ELEMENT:-pkg}-${VERSION}.*; do
+ if [ -f "$PKG" ]; then
+ _RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".id // empty")
+ [ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true
+ fi
+ done
+ echo "GitHub mirror updated: ${GH_REPO} ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
+
+ # -- STEP 10: Sync main branch to GitHub mirror ----------------------------
+ - name: "Step 10: Push main to GitHub mirror"
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ secrets.GH_TOKEN != ''
+ continue-on-error: true
+ run: |
+ GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
+ GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
+ GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
+ git remote add github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
+ git remote set-url github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
+ git fetch origin main --depth=1
+ git push github origin/main:refs/heads/main --force 2>/dev/null \
+ && echo "main branch pushed to GitHub mirror" \
+ || echo "WARNING: GitHub mirror push failed"
+
+ # -- Clean up lesser pre-releases (cascade) ---------------------------------
+ # stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev
+ - name: "Delete lesser pre-release channels"
+ continue-on-error: true
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ php /tmp/moko-platform-api/cli/release_cascade.php \
+ --stability stable \
+ --version "${VERSION}" \
+ --token "${{ secrets.GA_TOKEN }}" \
+ --api-base "${API_BASE}" 2>/dev/null || true
+
+ - name: "Step 11: Delete and recreate dev branch from main"
+ if: steps.version.outputs.skip != 'true'
+ continue-on-error: true
+ run: |
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ TOKEN="${{ secrets.GA_TOKEN }}"
+
+ # Delete dev branch
+ curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
+ "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
+
+ # Recreate dev from main (now includes version bump + changelog promotion)
+ curl -sf -X POST -H "Authorization: token ${TOKEN}" \
+ -H "Content-Type: application/json" \
+ "${API_BASE}/branches" \
+ -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
+
+ echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY
+
+
+ # -- Dolibarr post-release: Reset dev version -----------------------------
+ - name: "Dolibarr: Reset dev version"
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.platform.outputs.platform == 'dolibarr' &&
+ steps.platform.outputs.mod_file != ''
+ continue-on-error: true
+ run: |
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ TOKEN="${{ secrets.GA_TOKEN }}"
+ MOD_FILE="${{ steps.platform.outputs.mod_file }}"
+ ENCODED_PATH=$(echo "$MOD_FILE" | sed 's|^\./||' | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))")
+ FILE_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/${ENCODED_PATH}?ref=dev" 2>/dev/null || true)
+ FILE_SHA=$(echo "$FILE_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
+ FILE_CONTENT=$(echo "$FILE_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true)
+ if [ -n "$FILE_SHA" ] && [ -n "$FILE_CONTENT" ]; then
+ UPDATED=$(echo "$FILE_CONTENT" | sed "s/\$this->version = '[^']*'/\$this->version = 'development'/")
+ ENCODED=$(echo "$UPDATED" | base64 -w0)
+ curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/contents/${ENCODED_PATH}" \
+ -d "$(jq -n --arg content \"$ENCODED\" --arg sha \"$FILE_SHA\" --arg msg \"chore(version): reset dev version [skip ci]\" --arg branch \"dev\" '{content:$content,sha:$sha,message:$msg,branch:$branch}')" > /dev/null 2>&1 || true
+ fi
+
+ # -- Summary --------------------------------------------------------------
+ - name: Pipeline Summary
+ if: always()
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ PLATFORM="${{ steps.platform.outputs.platform }}"
+ if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
+ echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
+ echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
+ elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
+ echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
+ else
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
+ echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
+ echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
+ fi
--
2.52.0
From dca819dfefa3e27a2264c9fe4099cc0075a2d2d9 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Tue, 26 May 2026 19:36:35 +0000
Subject: [PATCH 04/40] chore(ci): update auto-release.yml from moko-platform
[skip ci]
---
.mokogitea/workflows/auto-release.yml | 3 ---
1 file changed, 3 deletions(-)
diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml
index 9eb98c8..eca25d8 100644
--- a/.mokogitea/workflows/auto-release.yml
+++ b/.mokogitea/workflows/auto-release.yml
@@ -30,9 +30,6 @@ on:
types: [opened, closed]
branches:
- main
- paths:
- - 'src/**'
- - 'htdocs/**'
workflow_dispatch:
env:
--
2.52.0
From 1efada2d37ec2db2e9e23c75d517971501e614d7 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Tue, 26 May 2026 19:36:36 +0000
Subject: [PATCH 05/40] chore(ci): update pre-release.yml from moko-platform
[skip ci]
---
.mokogitea/workflows/pre-release.yml | 3 ---
1 file changed, 3 deletions(-)
diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml
index b90d2c8..698251d 100644
--- a/.mokogitea/workflows/pre-release.yml
+++ b/.mokogitea/workflows/pre-release.yml
@@ -17,9 +17,6 @@ on:
types: [closed]
branches:
- dev
- paths:
- - 'src/**'
- - 'htdocs/**'
workflow_dispatch:
inputs:
stability:
--
2.52.0
From cff2ced161cd06271de78d9b681f233796214240 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Tue, 26 May 2026 19:54:13 +0000
Subject: [PATCH 06/40] fix(ci): use release_package.php for Joomla package
builds [skip ci]
---
.mokogitea/workflows/update-server.yml | 1257 +++++++++++-------------
1 file changed, 597 insertions(+), 660 deletions(-)
diff --git a/.mokogitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml
index c77cdaa..fd6407f 100644
--- a/.mokogitea/workflows/update-server.yml
+++ b/.mokogitea/workflows/update-server.yml
@@ -1,660 +1,597 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: MokoStandards.Universal
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
-# PATH: /templates/workflows/update-server.yml
-# VERSION: 04.07.00
-# BRIEF: Update server XML feed with stable/rc/beta/alpha/dev entries (universal)
-#
-# Writes updates.xml with multiple entries:
-# - stable on push to main (from auto-release)
-# - rc on push to rc/**
-# - development on push to dev or dev/**
-#
-# Joomla filters by user's "Minimum Stability" setting.
-
-name: "Update 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@v4
- with:
- token: ${{ secrets.GA_TOKEN }}
- fetch-depth: 0
-
- - name: Setup moko-platform 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
- if [ -d "/tmp/moko-platform" ]; then
- echo "moko-platform already available — skipping clone"
- else
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
- /tmp/moko-platform 2>/dev/null || true
- fi
- if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
- cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
- fi
-
- - 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/moko-platform/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/moko-platform/cli/version_bump.php --path . 2>/dev/null || true)
- if [ -n "$BUMPED" ]; then
- VERSION=$(php /tmp/moko-platform/cli/version_read.php --path . 2>/dev/null || echo "$VERSION")
- git add -A
- git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
- --author="gitea-actions[bot] " 2>/dev/null || true
- git push 2>/dev/null || true
- fi
-
- # Determine stability from branch or input
- if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
- STABILITY="${{ inputs.stability }}"
- elif [[ "$BRANCH" == rc/* ]]; then
- STABILITY="rc"
- elif [[ "$BRANCH" == beta/* ]]; then
- STABILITY="beta"
- elif [[ "$BRANCH" == alpha/* ]]; then
- STABILITY="alpha"
- elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then
- STABILITY="development"
- else
- STABILITY="stable"
- fi
-
- echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
-
- # Parse manifest (portable — no grep -P)
- MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '/dev/null | head -1)
- if [ -z "$MANIFEST" ]; then
- echo "No Joomla manifest found — skipping"
- exit 0
- fi
-
- # Extract fields using sed (works on all runners)
- EXT_NAME=$(sed -n 's/.*\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
- EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
- EXT_CLIENT=$(sed -n 's/.*]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- EXT_VERSION=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
- TARGET_PLATFORM=$(sed -n 's/.*\(\).*/\1/p' "$MANIFEST" | head -1)
- PHP_MINIMUM=$(sed -n 's/.*\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
-
- # Fallbacks
- [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
- [ -z "$EXT_TYPE" ] && EXT_TYPE="component"
-
- # Derive element if not in manifest: try XML filename, then repo name
- if [ -z "$EXT_ELEMENT" ]; then
- EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
- case "$EXT_ELEMENT" in
- templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
- esac
- fi
-
- # Use manifest version if README version is empty
- [ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION"
-
- [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '' "/")
-
- # Joomla requires on ALL extension types for update matching
- if [ -n "$EXT_CLIENT" ]; then
- CLIENT_TAG="${EXT_CLIENT}"
- else
- CLIENT_TAG="site"
- fi
-
- FOLDER_TAG=""
- [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="${EXT_FOLDER}"
-
- PHP_TAG=""
- [ -n "$PHP_MINIMUM" ] && PHP_TAG="${PHP_MINIMUM}"
-
- # Version suffix for non-stable
- DISPLAY_VERSION="$VERSION"
- case "$STABILITY" in
- development) DISPLAY_VERSION="${VERSION}-dev" ;;
- alpha) DISPLAY_VERSION="${VERSION}-alpha" ;;
- beta) DISPLAY_VERSION="${VERSION}-beta" ;;
- rc) DISPLAY_VERSION="${VERSION}-rc" ;;
- esac
-
- MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
-
- # Each stability level has its own release tag
- case "$STABILITY" in
- development) RELEASE_TAG="development" ;;
- alpha) RELEASE_TAG="alpha" ;;
- beta) RELEASE_TAG="beta" ;;
- rc) RELEASE_TAG="release-candidate" ;;
- *) RELEASE_TAG="v${MAJOR}" ;;
- esac
-
- PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip"
- DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}"
- INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}"
-
- # -- Build install packages (ZIP + tar.gz) --------------------
- SOURCE_DIR="src"
- [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
- if [ -d "$SOURCE_DIR" ]; then
- EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
- TAR_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz"
-
- cd "$SOURCE_DIR"
- zip -r "/tmp/${PACKAGE_NAME}" . -x $EXCLUDES
- cd ..
- tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \
- --exclude='.ftpignore' --exclude='sftp-config*' \
- --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
-
- SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1)
-
- # Ensure release exists on Gitea
- RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
- RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
-
- if [ -z "$RELEASE_ID" ]; then
- # Create release
- RELEASE_JSON=$(curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- -H "Content-Type: application/json" \
- "${API_BASE}/releases" \
- -d "$(python3 -c "import json; print(json.dumps({
- 'tag_name': '${RELEASE_TAG}',
- 'name': '${RELEASE_TAG} (${DISPLAY_VERSION})',
- 'body': '${STABILITY} release',
- 'prerelease': True,
- 'target_commitish': 'main'
- }))")" 2>/dev/null || true)
- RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
- fi
-
- if [ -n "$RELEASE_ID" ]; then
- # Delete existing assets with same name before uploading
- ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- "${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]")
- for ASSET_FILE in "$PACKAGE_NAME" "$TAR_NAME"; do
- ASSET_ID=$(echo "$ASSETS" | python3 -c "
- import sys,json
- assets = json.load(sys.stdin)
- for a in assets:
- if a['name'] == '${ASSET_FILE}':
- print(a['id']); break
- " 2>/dev/null || true)
- if [ -n "$ASSET_ID" ]; then
- curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- "${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true
- fi
- done
-
- # Upload both formats
- curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- -H "Content-Type: application/octet-stream" \
- --data-binary @"/tmp/${PACKAGE_NAME}" \
- "${API_BASE}/releases/${RELEASE_ID}/assets?name=${PACKAGE_NAME}" > /dev/null 2>&1 || true
-
- curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- -H "Content-Type: application/octet-stream" \
- --data-binary @"/tmp/${TAR_NAME}" \
- "${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true
- fi
-
- echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY
- else
- SHA256=""
- fi
-
- # -- Build the new entry (canonical format matching release.yml) --
- NEW_ENTRY=""
- NEW_ENTRY="${NEW_ENTRY} \n"
- NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME}\n"
- NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME} ${STABILITY} build.\n"
- NEW_ENTRY="${NEW_ENTRY} ${EXT_ELEMENT}\n"
- NEW_ENTRY="${NEW_ENTRY} ${EXT_TYPE}\n"
- [ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n"
- [ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n"
- NEW_ENTRY="${NEW_ENTRY} ${VERSION}\n"
- NEW_ENTRY="${NEW_ENTRY} $(date +%Y-%m-%d)\n"
- NEW_ENTRY="${NEW_ENTRY} https://git.mokoconsulting.tech/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${RELEASE_TAG}\n"
- NEW_ENTRY="${NEW_ENTRY} \n"
- NEW_ENTRY="${NEW_ENTRY} ${DOWNLOAD_URL}\n"
- NEW_ENTRY="${NEW_ENTRY} \n"
- [ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} ${SHA256}\n"
- NEW_ENTRY="${NEW_ENTRY} ${STABILITY}\n"
- NEW_ENTRY="${NEW_ENTRY} Moko Consulting\n"
- NEW_ENTRY="${NEW_ENTRY} https://mokoconsulting.tech\n"
- NEW_ENTRY="${NEW_ENTRY} \n"
- [ -n "$PHP_MINIMUM" ] && NEW_ENTRY="${NEW_ENTRY} ${PHP_MINIMUM}\n"
- NEW_ENTRY="${NEW_ENTRY} "
-
- # -- Write new entry to temp file --------------------------------
- printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml
-
- # -- Merge into updates.xml ----------------------------------------
- # Cascade: stable→all | rc→rc+lower | beta→beta+lower | alpha→alpha+dev | dev→dev
- CASCADE_MAP="stable:development,alpha,beta,rc,stable rc:development,alpha,beta,rc beta:development,alpha,beta alpha:development,alpha development:development"
- TARGETS=""
- for entry in $CASCADE_MAP; do
- key="${entry%%:*}"
- vals="${entry#*:}"
- if [ "$key" = "${STABILITY}" ]; then
- TARGETS="$vals"
- break
- fi
- done
- [ -z "$TARGETS" ] && TARGETS="${STABILITY}"
-
- echo "Cascade: ${STABILITY} → ${TARGETS}"
-
- # Create updates.xml if missing
- if [ ! -f "updates.xml" ]; then
- printf '%s\n' "" > updates.xml
- printf '%s\n' "" >> updates.xml
- printf '%s\n' "" >> updates.xml
- printf '%s\n' "" >> updates.xml
- fi
-
- # Update existing blocks or create missing ones
- export PY_TARGETS="$TARGETS" PY_VERSION="$VERSION" PY_DATE="$(date +%Y-%m-%d)"
- python3 << 'PYEOF'
- import re, os
-
- targets = os.environ["PY_TARGETS"].split(",")
- version = os.environ["PY_VERSION"]
- date = os.environ["PY_DATE"]
-
- with open("updates.xml") as f:
- content = f.read()
- with open("/tmp/new_entry.xml") as f:
- new_entry_template = f.read()
-
- for tag in targets:
- tag = tag.strip()
- # Build entry with this tag's name
- new_entry = re.sub(r"[^<]*", f"{tag}", new_entry_template)
-
- # Try to find existing block (handles both single-line and multi-line )
- block_pattern = r"((?:(?!).)*?" + re.escape(tag) + r".*?)"
- match = re.search(block_pattern, content, re.DOTALL)
-
- if match:
- # Update in place — replace entire block
- content = content.replace(match.group(1), new_entry.strip())
- print(f" UPDATED: {tag} → {version}")
- else:
- # Create — insert before
- content = content.replace("", "\n" + new_entry.strip() + "\n\n")
- print(f" CREATED: {tag} → {version}")
-
- # Clean up excessive blank lines
- content = re.sub(r"\n{3,}", "\n\n", content)
-
- with open("updates.xml", "w") as f:
- f.write(content)
- PYEOF
-
- # Commit
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git add updates.xml
- git diff --cached --quiet || {
- git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \
- --author="gitea-actions[bot] "
- git push
- }
-
- # -- Sync updates.xml to main (for non-main branches) ----------------------
- - name: Sync updates.xml to main
- if: github.ref_name != 'main'
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- GA_TOKEN="${{ secrets.GA_TOKEN }}"
-
- FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
- "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
-
- if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
- python3 -c "
- import base64, json, urllib.request, sys
- with open('updates.xml', 'rb') as f:
- content = base64.b64encode(f.read()).decode()
- payload = json.dumps({
- 'content': content,
- 'sha': '${FILE_SHA}',
- 'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]',
- 'branch': 'main'
- }).encode()
- req = urllib.request.Request(
- '${API_BASE}/contents/updates.xml',
- data=payload, method='PUT',
- headers={
- 'Authorization': 'token ${GA_TOKEN}',
- 'Content-Type': 'application/json'
- })
- try:
- urllib.request.urlopen(req)
- print('updates.xml synced to main')
- except Exception as e:
- print(f'ERROR: failed to sync updates.xml to main: {e}', file=sys.stderr)
- sys.exit(1)
- " \
- && echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \
- || echo "::error::failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY
- else
- echo "::error::could not get updates.xml SHA from main — file may not exist on main yet" >> $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/moko-platform/cli/platform_detect.php --path . 2>/dev/null || true)
- if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/moko-platform/deploy/deploy-joomla.php" ]; then
- php /tmp/moko-platform/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
- elif [ -f "/tmp/moko-platform/deploy/deploy-sftp.php" ]; then
- php /tmp/moko-platform/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: Validate updates.xml integrity
- run: |
- ERRORS=0
-
- if [ ! -f "updates.xml" ]; then
- echo "::error::updates.xml not found"
- exit 1
- fi
-
- # Well-formed XML
- if ! python3 -c "import xml.etree.ElementTree as ET; ET.parse('updates.xml')" 2>/dev/null; then
- echo "::error::updates.xml is not valid XML"
- ERRORS=$((ERRORS+1))
- fi
-
- python3 << 'PYEOF'
- import xml.etree.ElementTree as ET, sys, re, os
-
- tree = ET.parse("updates.xml")
- root = tree.getroot()
- updates = root.findall("update")
- errors = 0
- warnings = 0
- seen_tags = set()
-
- # All 5 channels MUST be present
- REQUIRED_CHANNELS = {"stable", "rc", "beta", "alpha", "dev"}
- VALID_TAGS = REQUIRED_CHANNELS | {"development"} # accept legacy alias
- REPO = os.environ.get("GITEA_REPO", "")
- ORG = os.environ.get("GITEA_ORG", "MokoConsulting")
- REPO_BASE = f"https://git.mokoconsulting.tech/{ORG}/"
-
- # Gitea release tag names per channel (Moko standard)
- RELEASE_TAG_MAP = {
- "stable": "stable",
- "rc": "release-candidate",
- "beta": "beta",
- "alpha": "alpha",
- "dev": "development",
- "development": "development",
- }
-
- # Joomla update XML required fields per
- # https://docs.joomla.org/Deploying_an_Update_Server
- REQUIRED_FIELDS = ["name", "element", "type", "version", "infourl"]
-
- for i, u in enumerate(updates):
- tag_el = u.find("tags/tag")
- tag = tag_el.text.strip() if tag_el is not None and tag_el.text else None
- label = f"Entry {i+1} ({tag or '?'})"
-
- # -- Required Joomla fields --
- for field in REQUIRED_FIELDS:
- el = u.find(field)
- if el is None or not (el.text or "").strip():
- print(f"::error::{label}: missing required <{field}>")
- errors += 1
-
- # -- --
- dl = u.find("downloads/downloadurl")
- if dl is None or not (dl.text or "").strip():
- print(f"::error::{label}: missing ")
- errors += 1
- else:
- dl_url = dl.text.strip()
- # Must point to org repo
- if REPO_BASE not in dl_url:
- print(f"::error::{label}: download URL not under {REPO_BASE}: {dl_url}")
- errors += 1
- # Must end in .zip
- if not dl_url.endswith(".zip"):
- print(f"::error::{label}: download URL must end in .zip: {dl_url}")
- errors += 1
- # Must use correct Gitea release tag in path
- if tag and tag in RELEASE_TAG_MAP:
- expected_tag = RELEASE_TAG_MAP[tag]
- if f"/download/{expected_tag}/" not in dl_url:
- print(f"::error::{label}: download URL should contain /download/{expected_tag}/ but got: {dl_url}")
- errors += 1
-
- # -- (required for Joomla to match update) --
- client = u.find("client")
- if client is None or not (client.text or "").strip():
- print(f"::error::{label}: missing (required for Joomla update matching)")
- errors += 1
-
- # -- --
- tp = u.find("targetplatform")
- if tp is None:
- print(f"::error::{label}: missing ")
- errors += 1
- else:
- tp_name = tp.get("name", "")
- tp_ver = tp.get("version", "")
- if tp_name != "joomla":
- print(f"::error::{label}: targetplatform name should be 'joomla', got '{tp_name}'")
- errors += 1
- if not tp_ver:
- print(f"::error::{label}: targetplatform missing version regex")
- errors += 1
- elif "5" not in tp_ver or "6" not in tp_ver:
- print(f"::warning::{label}: targetplatform version may not cover Joomla 5+6: {tp_ver}")
- warnings += 1
-
- # -- must be valid Joomla type --
- type_el = u.find("type")
- if type_el is not None and type_el.text:
- valid_types = {"component", "module", "plugin", "template", "library", "package", "file"}
- if type_el.text.strip() not in valid_types:
- print(f"::error::{label}: invalid type '{type_el.text}' (expected: {valid_types})")
- errors += 1
-
- # -- format (XX.YY.ZZ with optional suffix) --
- ver_el = u.find("version")
- if ver_el is not None and ver_el.text:
- if not re.match(r"^\d{2}\.\d{2}\.\d{2}(-\w+)?$", ver_el.text.strip()):
- print(f"::warning::{label}: version '{ver_el.text}' does not match XX.YY.ZZ format")
- warnings += 1
-
- # -- and --
- for field in ["maintainer", "maintainerurl"]:
- el = u.find(field)
- if el is None or not (el.text or "").strip():
- print(f"::warning::{label}: missing <{field}>")
- warnings += 1
-
- # -- Valid stability tag --
- if tag is None:
- print(f"::error::{label}: missing ")
- errors += 1
- elif tag not in VALID_TAGS:
- print(f"::error::{label}: invalid tag '{tag}' (expected: {VALID_TAGS})")
- errors += 1
-
- # -- Duplicate tag check --
- norm_tag = "dev" if tag == "development" else tag
- if norm_tag in seen_tags:
- print(f"::error::{label}: duplicate channel '{tag}'")
- errors += 1
- if norm_tag:
- seen_tags.add(norm_tag)
-
- # -- All 5 channels must exist --
- missing = REQUIRED_CHANNELS - seen_tags
- if missing:
- print(f"::error::Missing required update channels: {', '.join(sorted(missing))}")
- errors += 1
-
- # -- Version ordering: higher stability must not exceed dev version --
- channel_versions = {}
- for u in updates:
- tag_el = u.find("tags/tag")
- ver_el = u.find("version")
- if tag_el is not None and ver_el is not None and tag_el.text and ver_el.text:
- norm = "dev" if tag_el.text.strip() == "development" else tag_el.text.strip()
- # Strip suffix for comparison (01.00.18-dev -> 01.00.18)
- base_ver = re.sub(r"-\w+$", "", ver_el.text.strip())
- channel_versions[norm] = base_ver
-
- # Cascade check: dev >= alpha >= beta >= rc >= stable
- ORDER = ["dev", "alpha", "beta", "rc", "stable"]
- for j in range(1, len(ORDER)):
- current = ORDER[j]
- previous = ORDER[j - 1]
- if current in channel_versions and previous in channel_versions:
- if channel_versions[current] > channel_versions[previous]:
- print(f"::error::{current} version ({channel_versions[current]}) is ahead of {previous} ({channel_versions[previous]})")
- errors += 1
-
- # -- Summary --
- print(f"\nupdates.xml validation: {len(updates)} entries, {errors} error(s), {warnings} warning(s)")
- if errors > 0:
- sys.exit(1)
- PYEOF
-
- - 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
+# Copyright (C) 2026 Moko Consulting
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# FILE INFORMATION
+# DEFGROUP: Gitea.Workflow
+# INGROUP: MokoStandards.Universal
+# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
+# PATH: /templates/workflows/update-server.yml
+# VERSION: 04.07.00
+# BRIEF: Update server XML feed with stable/rc/beta/alpha/dev entries (universal)
+#
+# Writes updates.xml with multiple entries:
+# - stable on push to main (from auto-release)
+# - rc on push to rc/**
+# - development on push to dev or dev/**
+#
+# Joomla filters by user's "Minimum Stability" setting.
+
+name: "Update 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@v4
+ with:
+ token: ${{ secrets.GA_TOKEN }}
+ fetch-depth: 0
+
+ - name: Setup moko-platform 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
+ if [ -d "/tmp/moko-platform" ]; then
+ echo "moko-platform already available — skipping clone"
+ else
+ git clone --depth 1 --branch main --quiet \
+ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
+ /tmp/moko-platform 2>/dev/null || true
+ fi
+ if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
+ cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
+ fi
+
+ - 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/moko-platform/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/moko-platform/cli/version_bump.php --path . 2>/dev/null || true)
+ if [ -n "$BUMPED" ]; then
+ VERSION=$(php /tmp/moko-platform/cli/version_read.php --path . 2>/dev/null || echo "$VERSION")
+ git add -A
+ git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
+ --author="gitea-actions[bot] " 2>/dev/null || true
+ git push 2>/dev/null || true
+ fi
+
+ # Determine stability from branch or input
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
+ STABILITY="${{ inputs.stability }}"
+ elif [[ "$BRANCH" == rc/* ]]; then
+ STABILITY="rc"
+ elif [[ "$BRANCH" == beta/* ]]; then
+ STABILITY="beta"
+ elif [[ "$BRANCH" == alpha/* ]]; then
+ STABILITY="alpha"
+ elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then
+ STABILITY="development"
+ else
+ STABILITY="stable"
+ fi
+
+ echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
+
+ # Parse manifest (portable — no grep -P)
+ MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '/dev/null | head -1)
+ if [ -z "$MANIFEST" ]; then
+ echo "No Joomla manifest found — skipping"
+ exit 0
+ fi
+
+ # Extract fields using sed (works on all runners)
+ EXT_NAME=$(sed -n 's/.*\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
+ EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
+ EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
+ EXT_CLIENT=$(sed -n 's/.*]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
+ EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
+ EXT_VERSION=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
+ TARGET_PLATFORM=$(sed -n 's/.*\(\).*/\1/p' "$MANIFEST" | head -1)
+ PHP_MINIMUM=$(sed -n 's/.*\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
+
+ # Fallbacks
+ [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
+ [ -z "$EXT_TYPE" ] && EXT_TYPE="component"
+
+ # Derive element if not in manifest: try XML filename, then repo name
+ if [ -z "$EXT_ELEMENT" ]; then
+ EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
+ case "$EXT_ELEMENT" in
+ templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
+ esac
+ fi
+
+ # Use manifest version if README version is empty
+ [ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION"
+
+ [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '' "/")
+
+ # Joomla requires on ALL extension types for update matching
+ if [ -n "$EXT_CLIENT" ]; then
+ CLIENT_TAG="${EXT_CLIENT}"
+ else
+ CLIENT_TAG="site"
+ fi
+
+ FOLDER_TAG=""
+ [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="${EXT_FOLDER}"
+
+ PHP_TAG=""
+ [ -n "$PHP_MINIMUM" ] && PHP_TAG="${PHP_MINIMUM}"
+
+ # Version suffix for non-stable
+ DISPLAY_VERSION="$VERSION"
+ case "$STABILITY" in
+ development) DISPLAY_VERSION="${VERSION}-dev" ;;
+ alpha) DISPLAY_VERSION="${VERSION}-alpha" ;;
+ beta) DISPLAY_VERSION="${VERSION}-beta" ;;
+ rc) DISPLAY_VERSION="${VERSION}-rc" ;;
+ esac
+
+ MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
+
+ # Each stability level has its own release tag
+ case "$STABILITY" in
+ development) RELEASE_TAG="development" ;;
+ alpha) RELEASE_TAG="alpha" ;;
+ beta) RELEASE_TAG="beta" ;;
+ rc) RELEASE_TAG="release-candidate" ;;
+ *) RELEASE_TAG="v${MAJOR}" ;;
+ esac
+
+ PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip"
+ DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}"
+ INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}"
+
+ # -- Build install packages and upload to release --------------------
+ php /tmp/moko-platform/cli/release_package.php \
+ --path . --version "${DISPLAY_VERSION}" --tag "${RELEASE_TAG}" \
+ --token "${{ secrets.GA_TOKEN }}" --api-base "${API_BASE}" \
+ --repo "${GITEA_REPO}" --output /tmp 2>&1 || true
+
+ echo "Package built and uploaded for ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
+
+ # -- Build the new entry (canonical format matching release.yml) --
+ NEW_ENTRY=""
+ NEW_ENTRY="${NEW_ENTRY} \n"
+ NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME}\n"
+ NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME} ${STABILITY} build.\n"
+ NEW_ENTRY="${NEW_ENTRY} ${EXT_ELEMENT}\n"
+ NEW_ENTRY="${NEW_ENTRY} ${EXT_TYPE}\n"
+ [ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n"
+ [ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n"
+ NEW_ENTRY="${NEW_ENTRY} ${VERSION}\n"
+ NEW_ENTRY="${NEW_ENTRY} $(date +%Y-%m-%d)\n"
+ NEW_ENTRY="${NEW_ENTRY} https://git.mokoconsulting.tech/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${RELEASE_TAG}\n"
+ NEW_ENTRY="${NEW_ENTRY} \n"
+ NEW_ENTRY="${NEW_ENTRY} ${DOWNLOAD_URL}\n"
+ NEW_ENTRY="${NEW_ENTRY} \n"
+ [ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} ${SHA256}\n"
+ NEW_ENTRY="${NEW_ENTRY} ${STABILITY}\n"
+ NEW_ENTRY="${NEW_ENTRY} Moko Consulting\n"
+ NEW_ENTRY="${NEW_ENTRY} https://mokoconsulting.tech\n"
+ NEW_ENTRY="${NEW_ENTRY} \n"
+ [ -n "$PHP_MINIMUM" ] && NEW_ENTRY="${NEW_ENTRY} ${PHP_MINIMUM}\n"
+ NEW_ENTRY="${NEW_ENTRY} "
+
+ # -- Write new entry to temp file --------------------------------
+ printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml
+
+ # -- Merge into updates.xml ----------------------------------------
+ # Cascade: stable→all | rc→rc+lower | beta→beta+lower | alpha→alpha+dev | dev→dev
+ CASCADE_MAP="stable:development,alpha,beta,rc,stable rc:development,alpha,beta,rc beta:development,alpha,beta alpha:development,alpha development:development"
+ TARGETS=""
+ for entry in $CASCADE_MAP; do
+ key="${entry%%:*}"
+ vals="${entry#*:}"
+ if [ "$key" = "${STABILITY}" ]; then
+ TARGETS="$vals"
+ break
+ fi
+ done
+ [ -z "$TARGETS" ] && TARGETS="${STABILITY}"
+
+ echo "Cascade: ${STABILITY} → ${TARGETS}"
+
+ # Create updates.xml if missing
+ if [ ! -f "updates.xml" ]; then
+ printf '%s\n' "" > updates.xml
+ printf '%s\n' "" >> updates.xml
+ printf '%s\n' "" >> updates.xml
+ printf '%s\n' "" >> updates.xml
+ fi
+
+ # Update existing blocks or create missing ones
+ export PY_TARGETS="$TARGETS" PY_VERSION="$VERSION" PY_DATE="$(date +%Y-%m-%d)"
+ python3 << 'PYEOF'
+ import re, os
+
+ targets = os.environ["PY_TARGETS"].split(",")
+ version = os.environ["PY_VERSION"]
+ date = os.environ["PY_DATE"]
+
+ with open("updates.xml") as f:
+ content = f.read()
+ with open("/tmp/new_entry.xml") as f:
+ new_entry_template = f.read()
+
+ for tag in targets:
+ tag = tag.strip()
+ # Build entry with this tag's name
+ new_entry = re.sub(r"[^<]*", f"{tag}", new_entry_template)
+
+ # Try to find existing block (handles both single-line and multi-line )
+ block_pattern = r"((?:(?!).)*?" + re.escape(tag) + r".*?)"
+ match = re.search(block_pattern, content, re.DOTALL)
+
+ if match:
+ # Update in place — replace entire block
+ content = content.replace(match.group(1), new_entry.strip())
+ print(f" UPDATED: {tag} → {version}")
+ else:
+ # Create — insert before
+ content = content.replace("", "\n" + new_entry.strip() + "\n\n")
+ print(f" CREATED: {tag} → {version}")
+
+ # Clean up excessive blank lines
+ content = re.sub(r"\n{3,}", "\n\n", content)
+
+ with open("updates.xml", "w") as f:
+ f.write(content)
+ PYEOF
+
+ # Commit
+ git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
+ git config --local user.name "gitea-actions[bot]"
+ git add updates.xml
+ git diff --cached --quiet || {
+ git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \
+ --author="gitea-actions[bot] "
+ git push
+ }
+
+ # -- Sync updates.xml to main (for non-main branches) ----------------------
+ - name: Sync updates.xml to main
+ if: github.ref_name != 'main'
+ run: |
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ GA_TOKEN="${{ secrets.GA_TOKEN }}"
+
+ FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
+ "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
+
+ if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
+ python3 -c "
+ import base64, json, urllib.request, sys
+ with open('updates.xml', 'rb') as f:
+ content = base64.b64encode(f.read()).decode()
+ payload = json.dumps({
+ 'content': content,
+ 'sha': '${FILE_SHA}',
+ 'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]',
+ 'branch': 'main'
+ }).encode()
+ req = urllib.request.Request(
+ '${API_BASE}/contents/updates.xml',
+ data=payload, method='PUT',
+ headers={
+ 'Authorization': 'token ${GA_TOKEN}',
+ 'Content-Type': 'application/json'
+ })
+ try:
+ urllib.request.urlopen(req)
+ print('updates.xml synced to main')
+ except Exception as e:
+ print(f'ERROR: failed to sync updates.xml to main: {e}', file=sys.stderr)
+ sys.exit(1)
+ " \
+ && echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \
+ || echo "::error::failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY
+ else
+ echo "::error::could not get updates.xml SHA from main — file may not exist on main yet" >> $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/moko-platform/cli/platform_detect.php --path . 2>/dev/null || true)
+ if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/moko-platform/deploy/deploy-joomla.php" ]; then
+ php /tmp/moko-platform/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
+ elif [ -f "/tmp/moko-platform/deploy/deploy-sftp.php" ]; then
+ php /tmp/moko-platform/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: Validate updates.xml integrity
+ run: |
+ ERRORS=0
+
+ if [ ! -f "updates.xml" ]; then
+ echo "::error::updates.xml not found"
+ exit 1
+ fi
+
+ # Well-formed XML
+ if ! python3 -c "import xml.etree.ElementTree as ET; ET.parse('updates.xml')" 2>/dev/null; then
+ echo "::error::updates.xml is not valid XML"
+ ERRORS=$((ERRORS+1))
+ fi
+
+ python3 << 'PYEOF'
+ import xml.etree.ElementTree as ET, sys, re, os
+
+ tree = ET.parse("updates.xml")
+ root = tree.getroot()
+ updates = root.findall("update")
+ errors = 0
+ warnings = 0
+ seen_tags = set()
+
+ # All 5 channels MUST be present
+ REQUIRED_CHANNELS = {"stable", "rc", "beta", "alpha", "dev"}
+ VALID_TAGS = REQUIRED_CHANNELS | {"development"} # accept legacy alias
+ REPO = os.environ.get("GITEA_REPO", "")
+ ORG = os.environ.get("GITEA_ORG", "MokoConsulting")
+ REPO_BASE = f"https://git.mokoconsulting.tech/{ORG}/"
+
+ # Gitea release tag names per channel (Moko standard)
+ RELEASE_TAG_MAP = {
+ "stable": "stable",
+ "rc": "release-candidate",
+ "beta": "beta",
+ "alpha": "alpha",
+ "dev": "development",
+ "development": "development",
+ }
+
+ # Joomla update XML required fields per
+ # https://docs.joomla.org/Deploying_an_Update_Server
+ REQUIRED_FIELDS = ["name", "element", "type", "version", "infourl"]
+
+ for i, u in enumerate(updates):
+ tag_el = u.find("tags/tag")
+ tag = tag_el.text.strip() if tag_el is not None and tag_el.text else None
+ label = f"Entry {i+1} ({tag or '?'})"
+
+ # -- Required Joomla fields --
+ for field in REQUIRED_FIELDS:
+ el = u.find(field)
+ if el is None or not (el.text or "").strip():
+ print(f"::error::{label}: missing required <{field}>")
+ errors += 1
+
+ # -- --
+ dl = u.find("downloads/downloadurl")
+ if dl is None or not (dl.text or "").strip():
+ print(f"::error::{label}: missing ")
+ errors += 1
+ else:
+ dl_url = dl.text.strip()
+ # Must point to org repo
+ if REPO_BASE not in dl_url:
+ print(f"::error::{label}: download URL not under {REPO_BASE}: {dl_url}")
+ errors += 1
+ # Must end in .zip
+ if not dl_url.endswith(".zip"):
+ print(f"::error::{label}: download URL must end in .zip: {dl_url}")
+ errors += 1
+ # Must use correct Gitea release tag in path
+ if tag and tag in RELEASE_TAG_MAP:
+ expected_tag = RELEASE_TAG_MAP[tag]
+ if f"/download/{expected_tag}/" not in dl_url:
+ print(f"::error::{label}: download URL should contain /download/{expected_tag}/ but got: {dl_url}")
+ errors += 1
+
+ # -- (required for Joomla to match update) --
+ client = u.find("client")
+ if client is None or not (client.text or "").strip():
+ print(f"::error::{label}: missing (required for Joomla update matching)")
+ errors += 1
+
+ # -- --
+ tp = u.find("targetplatform")
+ if tp is None:
+ print(f"::error::{label}: missing ")
+ errors += 1
+ else:
+ tp_name = tp.get("name", "")
+ tp_ver = tp.get("version", "")
+ if tp_name != "joomla":
+ print(f"::error::{label}: targetplatform name should be 'joomla', got '{tp_name}'")
+ errors += 1
+ if not tp_ver:
+ print(f"::error::{label}: targetplatform missing version regex")
+ errors += 1
+ elif "5" not in tp_ver or "6" not in tp_ver:
+ print(f"::warning::{label}: targetplatform version may not cover Joomla 5+6: {tp_ver}")
+ warnings += 1
+
+ # -- must be valid Joomla type --
+ type_el = u.find("type")
+ if type_el is not None and type_el.text:
+ valid_types = {"component", "module", "plugin", "template", "library", "package", "file"}
+ if type_el.text.strip() not in valid_types:
+ print(f"::error::{label}: invalid type '{type_el.text}' (expected: {valid_types})")
+ errors += 1
+
+ # -- format (XX.YY.ZZ with optional suffix) --
+ ver_el = u.find("version")
+ if ver_el is not None and ver_el.text:
+ if not re.match(r"^\d{2}\.\d{2}\.\d{2}(-\w+)?$", ver_el.text.strip()):
+ print(f"::warning::{label}: version '{ver_el.text}' does not match XX.YY.ZZ format")
+ warnings += 1
+
+ # -- and --
+ for field in ["maintainer", "maintainerurl"]:
+ el = u.find(field)
+ if el is None or not (el.text or "").strip():
+ print(f"::warning::{label}: missing <{field}>")
+ warnings += 1
+
+ # -- Valid stability tag --
+ if tag is None:
+ print(f"::error::{label}: missing ")
+ errors += 1
+ elif tag not in VALID_TAGS:
+ print(f"::error::{label}: invalid tag '{tag}' (expected: {VALID_TAGS})")
+ errors += 1
+
+ # -- Duplicate tag check --
+ norm_tag = "dev" if tag == "development" else tag
+ if norm_tag in seen_tags:
+ print(f"::error::{label}: duplicate channel '{tag}'")
+ errors += 1
+ if norm_tag:
+ seen_tags.add(norm_tag)
+
+ # -- All 5 channels must exist --
+ missing = REQUIRED_CHANNELS - seen_tags
+ if missing:
+ print(f"::error::Missing required update channels: {', '.join(sorted(missing))}")
+ errors += 1
+
+ # -- Version ordering: higher stability must not exceed dev version --
+ channel_versions = {}
+ for u in updates:
+ tag_el = u.find("tags/tag")
+ ver_el = u.find("version")
+ if tag_el is not None and ver_el is not None and tag_el.text and ver_el.text:
+ norm = "dev" if tag_el.text.strip() == "development" else tag_el.text.strip()
+ # Strip suffix for comparison (01.00.18-dev -> 01.00.18)
+ base_ver = re.sub(r"-\w+$", "", ver_el.text.strip())
+ channel_versions[norm] = base_ver
+
+ # Cascade check: dev >= alpha >= beta >= rc >= stable
+ ORDER = ["dev", "alpha", "beta", "rc", "stable"]
+ for j in range(1, len(ORDER)):
+ current = ORDER[j]
+ previous = ORDER[j - 1]
+ if current in channel_versions and previous in channel_versions:
+ if channel_versions[current] > channel_versions[previous]:
+ print(f"::error::{current} version ({channel_versions[current]}) is ahead of {previous} ({channel_versions[previous]})")
+ errors += 1
+
+ # -- Summary --
+ print(f"\nupdates.xml validation: {len(updates)} entries, {errors} error(s), {warnings} warning(s)")
+ if errors > 0:
+ sys.exit(1)
+ PYEOF
+
+ - 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
--
2.52.0
From 5d6c1e6c1abd25ca07fd74091a2323c8186e2d29 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Tue, 26 May 2026 22:12:10 +0000
Subject: [PATCH 07/40] chore(ci): add auto-bump.yml from moko-platform [skip
ci]
---
.mokogitea/workflows/auto-bump.yml | 82 ++++++++++++++++++++++++++++++
1 file changed, 82 insertions(+)
create mode 100644 .mokogitea/workflows/auto-bump.yml
diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml
new file mode 100644
index 0000000..ef0aedc
--- /dev/null
+++ b/.mokogitea/workflows/auto-bump.yml
@@ -0,0 +1,82 @@
+# Copyright (C) 2026 Moko Consulting
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# FILE INFORMATION
+# DEFGROUP: Gitea.Workflow
+# INGROUP: moko-platform.Release
+# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
+# PATH: /.mokogitea/workflows/auto-bump.yml
+# VERSION: 09.02.00
+# BRIEF: Auto patch-bump version on every push to dev
+
+name: "Universal: Auto Version Bump"
+
+on:
+ push:
+ branches:
+ - dev
+
+env:
+ GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
+
+permissions:
+ contents: write
+
+jobs:
+ bump:
+ name: Patch Bump
+ runs-on: release
+ if: >-
+ !contains(github.event.head_commit.message, '[skip ci]') &&
+ !contains(github.event.head_commit.message, '[skip bump]')
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ token: ${{ secrets.GA_TOKEN }}
+ fetch-depth: 1
+
+ - name: Setup moko-platform tools
+ run: |
+ if ! command -v composer &> /dev/null; then
+ sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
+ fi
+ if [ -d "/opt/moko-platform/cli" ]; then
+ echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
+ else
+ git clone --depth 1 --branch main --quiet \
+ "https://x-access-token:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
+ /tmp/moko-platform-api
+ cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
+ echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
+ fi
+
+ - name: Patch bump version
+ run: |
+ BUMP=$(php ${MOKO_CLI}/version_bump.php --path . 2>&1) || true
+ echo "$BUMP"
+
+ VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) || true
+ [ -z "$VERSION" ] && { echo "No version found — skipping"; exit 0; }
+
+ # Propagate to platform manifests
+ php ${MOKO_CLI}/version_set_platform.php \
+ --path . --version "$VERSION" --branch dev 2>/dev/null || true
+ php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
+
+ # Commit if anything changed
+ if git diff --quiet && git diff --cached --quiet; then
+ echo "No version changes to commit"
+ exit 0
+ fi
+
+ 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 commit -m "chore(version): patch bump to ${VERSION} [skip ci]" \
+ --author="gitea-actions[bot] "
+ git push origin dev
+ echo "Bumped to ${VERSION}" >> $GITHUB_STEP_SUMMARY
--
2.52.0
From 2e6a20fb6a2e51849280a2e10335fda209587291 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Tue, 26 May 2026 22:13:22 +0000
Subject: [PATCH 08/40] chore(ci): update pre-release.yml from moko-platform
[skip ci]
---
.mokogitea/workflows/pre-release.yml | 104 +++++----------------------
1 file changed, 16 insertions(+), 88 deletions(-)
diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml
index 698251d..83443c7 100644
--- a/.mokogitea/workflows/pre-release.yml
+++ b/.mokogitea/workflows/pre-release.yml
@@ -78,15 +78,7 @@ jobs:
- name: Detect platform
id: platform
run: |
- PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1 | tr -d '[:space:]')
- [ -z "$PLATFORM" ] && PLATFORM="generic"
- echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
- MANIFEST=$(find ./src -maxdepth 1 -name "pkg_*.xml" -exec grep -l '/dev/null | head -1)
- [ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "*/packages/*" -exec grep -l '/dev/null | head -1)
- [ -z "$MANIFEST" ] && MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/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"
+ php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve metadata and bump version
id: meta
@@ -100,16 +92,9 @@ jobs:
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac
- # Patch bump via CLI tool
- php ${MOKO_CLI}/version_bump.php --path .
+ # Read current version (bump already handled by push workflow)
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
[ -z "$VERSION" ] && VERSION="00.00.01"
- TODAY=$(date +%Y-%m-%d)
-
- # Update platform-specific manifest
- PLATFORM="${{ steps.platform.outputs.platform }}"
- MANIFEST="${{ steps.platform.outputs.manifest }}"
- MOD_FILE="${{ steps.platform.outputs.mod_file }}"
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch "${{ github.ref_name }}" 2>/dev/null || true
@@ -127,36 +112,16 @@ jobs:
git push origin HEAD 2>&1
}
- # Auto-detect element (platform-aware)
- EXT_ELEMENT=""
- case "$PLATFORM" in
- joomla)
- if [ -n "$MANIFEST" ]; then
- EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
- if [ -z "$EXT_ELEMENT" ]; then
- EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
- case "$EXT_ELEMENT" in
- templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
- esac
- fi
- else
- EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
- fi
- ;;
- dolibarr)
- 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
+ # Auto-detect element via manifest_element.php
+ php ${MOKO_CLI}/manifest_element.php \
+ --path . --version "$VERSION" --stability "$STABILITY" \
+ --repo "${GITEA_REPO}" --github-output
- ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
+ # Read back element outputs
+ EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
+ ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
+ [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
+ [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
@@ -278,54 +243,17 @@ jobs:
- 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 }}"
+ STABILITY="${{ steps.meta.outputs.stability }}"
if [ ! -f "updates.xml" ]; then
echo "No updates.xml -- skipping"
exit 0
fi
- # Map stability to XML tag name
- case "$STABILITY" in
- development) XML_TAG="development" ;;
- alpha) XML_TAG="alpha" ;;
- beta) XML_TAG="beta" ;;
- release-candidate) XML_TAG="rc" ;;
- *) XML_TAG="$STABILITY" ;;
- esac
-
- DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${TAG}/${ZIP_NAME}"
-
- # Use PHP to update the channel in updates.xml
- php -r '
- $xml_tag = $argv[1];
- $version = $argv[2];
- $sha256 = $argv[3];
- $url = $argv[4];
- $date = date("Y-m-d");
-
- $content = file_get_contents("updates.xml");
- $pattern = "/((?:(?!<\/update>).)*?" . preg_quote($xml_tag) . "<\/tag>.*?<\/update>)/s";
-
- $content = preg_replace_callback($pattern, function($m) use ($version, $sha256, $url, $date) {
- $block = $m[0];
- $block = preg_replace("/[^<]*<\/version>/", "{$version}", $block);
- if (strpos($block, "") !== false) {
- $block = preg_replace("/[^<]*<\/sha256>/", "{$sha256}", $block);
- } else {
- $block = str_replace("", "\n {$sha256}", $block);
- }
- $block = preg_replace("/(]*>)[^<]*(<\/downloadurl>)/", "\${1}{$url}\${2}", $block);
- return $block;
- }, $content);
-
- file_put_contents("updates.xml", $content);
- echo "Updated {$xml_tag} channel: version={$version}\n";
- ' "$XML_TAG" "$VERSION" "$SHA256" "$DOWNLOAD_URL"
+ php ${MOKO_CLI}/updates_xml_build.php \
+ --path . --version "${VERSION}" --stability "${STABILITY}" \
+ --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}"
# Commit and push
if ! git diff --quiet updates.xml 2>/dev/null; then
@@ -347,7 +275,7 @@ jobs:
[ "$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 "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue
git checkout "${CURRENT_BRANCH}" -- updates.xml
if ! git diff --quiet updates.xml 2>/dev/null; then
git add updates.xml
--
2.52.0
From 604ff5d11ef764c87051c49bac5cc85f60147c88 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Tue, 26 May 2026 22:23:59 +0000
Subject: [PATCH 09/40] chore(ci): update auto-release.yml from moko-platform
[skip ci]
---
.mokogitea/workflows/auto-release.yml | 380 ++++----------------------
1 file changed, 51 insertions(+), 329 deletions(-)
diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml
index eca25d8..d665ce7 100644
--- a/.mokogitea/workflows/auto-release.yml
+++ b/.mokogitea/workflows/auto-release.yml
@@ -31,6 +31,15 @@ on:
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
@@ -47,8 +56,8 @@ jobs:
name: Promote Pre-Release to RC
runs-on: release
if: >-
- github.event.action == 'opened' &&
- github.event.pull_request.draft == true
+ (github.event.action == 'opened' && github.event.pull_request.draft == true) ||
+ (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
steps:
- name: Checkout repository
@@ -100,7 +109,8 @@ jobs:
name: Build & Release Pipeline
runs-on: release
if: >-
- github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
+ github.event.pull_request.merged == true ||
+ (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
steps:
- name: Checkout repository
@@ -170,18 +180,7 @@ jobs:
echo "::notice::No RC release — full build pipeline"
fi
- - name: "Step 1b: Bump version"
- id: bump
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.rc.outputs.promote != 'true'
- run: |
- MOKO_API="/tmp/moko-platform-api/cli"
- BUMP=$(php ${MOKO_API}/version_bump.php --path . --minor)
- VERSION=$(echo "$BUMP" | grep -oP '\d{2}\.\d{2}\.\d{2}$' || true)
- [ -z "$VERSION" ] && VERSION=$(php ${MOKO_API}/version_read.php --path .)
- echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- echo "Bumped to: ${VERSION}"
+ # Version bump handled by auto-bump.yml (minor on main, patch on dev)
- name: Check if already released
if: steps.version.outputs.skip != 'true'
@@ -208,96 +207,9 @@ jobs:
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- ERRORS=0
-
- PLATFORM="${{ steps.platform.outputs.platform }}"
- MANIFEST="${{ steps.platform.outputs.manifest }}"
- MOD_FILE="${{ steps.platform.outputs.mod_file }}"
- echo "## Pre-Release Sanity Checks (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
-
- # -- Version drift check (must pass before release) --------
- README_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
- if [ "$README_VER" != "$VERSION" ]; then
- echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS+1))
- else
- echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
- fi
-
- # Check CHANGELOG version matches
- CL_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' CHANGELOG.md 2>/dev/null | head -1)
- if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then
- echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS+1))
- fi
-
- # Check composer.json version if present
- if [ -f "composer.json" ]; then
- COMP_VER=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' composer.json 2>/dev/null | head -1)
- if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then
- echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS+1))
- fi
- fi
-
- # Common checks
- if [ ! -f "LICENSE" ]; then
- echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS+1))
- else
- echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY
- fi
-
- if [ ! -d "src" ] && [ ! -d "htdocs" ]; then
- echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY
- else
- echo "- Source directory present" >> $GITHUB_STEP_SUMMARY
- fi
-
- # -- Platform-specific checks --------
- case "$PLATFORM" in
- joomla)
- if [ -n "$MANIFEST" ]; then
- XML_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
- if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then
- echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS+1))
- else
- echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
- fi
- TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null)
- echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY
- else
- echo "- No Joomla XML manifest (WaaS site)" >> $GITHUB_STEP_SUMMARY
- fi ;;
- dolibarr)
- if [ -n "$MOD_FILE" ]; then
- MOD_VER=$(sed -n "s/.*\\\$this->version = '\([^']*\)'.*/\1/p" "$MOD_FILE" 2>/dev/null | head -1)
- if [ -n "$MOD_VER" ] && [ "$MOD_VER" != "$VERSION" ]; then
- echo "- Module drift: \`${MOD_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS+1))
- else
- echo "- Module version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
- fi
- else
- echo "- No mod*.class.php found" >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS+1))
- fi
- if [ ! -f "update.txt" ]; then
- echo "- Missing update.txt" >> $GITHUB_STEP_SUMMARY
- ERRORS=$((ERRORS+1))
- fi ;;
- *) echo "- Generic platform � no manifest checks" >> $GITHUB_STEP_SUMMARY ;;
- esac
-
- echo "" >> $GITHUB_STEP_SUMMARY
- if [ "$ERRORS" -gt 0 ]; then
- echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY
- else
- echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY
- fi
+ VERSION="${{ steps.version.outputs.version }}"
+ php /tmp/moko-platform-api/cli/release_validate.php \
+ --path . --version "$VERSION" --output-summary --github-output || true
# -- STEP 2: Create or update version/XX.YY archive branch ---------------
# Always runs — every version change on main archives to version/XX.YY
@@ -306,7 +218,7 @@ jobs:
run: |
BRANCH="${{ steps.version.outputs.branch }}"
IS_MINOR="${{ steps.version.outputs.is_minor }}"
- PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ PATCH="${{ steps.version.outputs.version }}"
PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}')
# Check if branch exists
@@ -325,7 +237,7 @@ jobs:
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ VERSION="${{ steps.version.outputs.version }}"
php /tmp/moko-platform-api/cli/version_set_platform.php \
--path . --version "$VERSION" --branch main
@@ -333,7 +245,7 @@ jobs:
- name: "Step 4: Update version badges"
if: steps.version.outputs.skip != 'true'
run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ VERSION="${{ steps.version.outputs.version }}"
php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true
php /tmp/moko-platform-api/cli/version_check.php --path . --fix 2>/dev/null || true
@@ -342,7 +254,7 @@ jobs:
steps.version.outputs.skip != 'true' &&
steps.platform.outputs.platform == 'joomla'
run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ VERSION="${{ steps.version.outputs.version }}"
# Fetch latest updates.xml from main so preserve logic has all channels
GA_TOKEN="${{ secrets.GA_TOKEN }}"
@@ -366,7 +278,7 @@ jobs:
echo "No changes to commit"
exit 0
fi
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ VERSION="${{ steps.version.outputs.version }}"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
# Set push URL with token for branch-protected repos
@@ -413,187 +325,34 @@ jobs:
steps.version.outputs.skip != 'true' &&
steps.rc.outputs.promote != 'true'
run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ VERSION="${{ steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- BRANCH="${{ steps.version.outputs.branch }}"
- MAJOR="${{ steps.version.outputs.major }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ php /tmp/moko-platform-api/cli/release_create.php \
+ --path . --version "$VERSION" --tag "$RELEASE_TAG" \
+ --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \
+ --repo "${GITEA_REPO}" --branch main
+ echo "Release created: ${VERSION}" >> $GITHUB_STEP_SUMMARY
- # Reuse metadata from Step 5 (single source of truth)
- EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
- EXT_NAME="${{ steps.updates.outputs.ext_name }}"
- EXT_TYPE="${{ steps.updates.outputs.ext_type }}"
- EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}"
-
- # Fallbacks if Step 5 was skipped
- if [ -z "$EXT_ELEMENT" ]; then
- EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
- fi
- [ -z "$EXT_NAME" ] && EXT_NAME="${GITEA_REPO}"
-
- NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null)
- [ -z "$NOTES" ] && NOTES="Release ${VERSION}"
-
- # Build release name: "Pretty Name VERSION (type_element-VERSION)"
- # Strip existing type prefix to prevent duplication
- EXT_ELEMENT=$(echo "$EXT_ELEMENT" | sed -E 's/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)//')
- TYPE_PREFIX=""
- case "${EXT_TYPE}" in
- plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
- module) TYPE_PREFIX="mod_" ;;
- component) TYPE_PREFIX="com_" ;;
- template) TYPE_PREFIX="tpl_" ;;
- library) TYPE_PREFIX="lib_" ;;
- package) TYPE_PREFIX="pkg_" ;;
- esac
- RELEASE_NAME="${EXT_NAME} ${VERSION} (${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION})"
-
- # Delete existing release if present (overwrite, not append)
- EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
- EXISTING_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true)
-
- if [ -n "$EXISTING_ID" ]; then
- curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- "${API_BASE}/releases/${EXISTING_ID}" 2>/dev/null || true
- curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- "${API_BASE}/tags/${RELEASE_TAG}" 2>/dev/null || true
- echo "Deleted previous stable release (id: ${EXISTING_ID})"
- fi
-
- # Create fresh release
- curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- -H "Content-Type: application/json" \
- "${API_BASE}/releases" \
- -d "$(python3 -c "import json; print(json.dumps({
- 'tag_name': '${RELEASE_TAG}',
- 'name': '${RELEASE_NAME}',
- 'body': '''## ${VERSION} ($(date +%Y-%m-%d))\n${NOTES}''',
- 'target_commitish': '${BRANCH}'
- }))")"
- echo "Release created: ${RELEASE_NAME}" >> $GITHUB_STEP_SUMMARY
-
- # -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------
- - name: "Step 8: Build package and update checksum"
+ # -- STEP 8: Build packages and upload to release ----------------------------
+ - name: "Step 8: Build package and upload"
if: >-
steps.version.outputs.skip != 'true' &&
steps.rc.outputs.promote != 'true'
run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ VERSION="${{ steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- REPO="${{ github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
-
- # All ZIPs upload to the major release tag (vXX)
- RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
- RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
- if [ -z "$RELEASE_ID" ]; then
- echo "No release ${RELEASE_TAG} found — skipping ZIP upload"
- exit 0
- fi
-
- # Find extension element name from manifest
- MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1 || true)
- [ -z "$MANIFEST" ] && exit 0
-
- # Reuse element from Step 5, with same fallback chain
- EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}"
- if [ -z "$EXT_ELEMENT" ]; then
- EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
- [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
- [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
- [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
- fi
- # ZIP name: type_folder_element-VERSION (e.g. plg_system_mokojgdpc-01.01.00.zip)
- EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- # For packages, prefer over filename-derived element
- if [ "$EXT_TYPE" = "package" ]; then
- PKG_NAME=$(sed -n 's/.*\([^<]*\)<\/packagename>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
- [ -n "$PKG_NAME" ] && EXT_ELEMENT="$PKG_NAME"
- fi
- # Strip existing type prefix to prevent duplication (e.g. pkg_mokowaas → mokowaas)
- EXT_ELEMENT=$(echo "$EXT_ELEMENT" | sed -E 's/^(pkg_|com_|mod_|plg_[a-z]+_|tpl_|lib_)//')
- TYPE_PREFIX=""
- case "${EXT_TYPE}" in
- plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;;
- module) TYPE_PREFIX="mod_" ;;
- component) TYPE_PREFIX="com_" ;;
- template) TYPE_PREFIX="tpl_" ;;
- library) TYPE_PREFIX="lib_" ;;
- package) TYPE_PREFIX="pkg_" ;;
- esac
- ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
- TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
-
- # -- Build install packages from src/ ----------------------------
- SOURCE_DIR="src"
- [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
- [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/"; exit 0; }
-
- # ZIP package (type-aware via moko-platform PHP API)
- php /tmp/moko-platform-api/cli/joomla_build.php --path . --version "${VERSION}" --output /tmp
- # Match the expected ZIP_NAME for upload
- BUILT_ZIP=$(ls /tmp/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip 2>/dev/null | head -1 || true)
- if [ -n "$BUILT_ZIP" ] && [ "$BUILT_ZIP" != "/tmp/${ZIP_NAME}" ]; then
- mv "$BUILT_ZIP" "/tmp/${ZIP_NAME}"
- fi
-
- # tar.gz package (flat source archive)
- tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" --exclude='.ftpignore' --exclude='sftp-config*' --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
-
- ZIP_SIZE=$(stat -c%s "/tmp/${ZIP_NAME}" 2>/dev/null || stat -f%z "/tmp/${ZIP_NAME}" 2>/dev/null || echo "unknown")
- TAR_SIZE=$(stat -c%s "/tmp/${TAR_NAME}" 2>/dev/null || stat -f%z "/tmp/${TAR_NAME}" 2>/dev/null || echo "unknown")
-
- # -- Calculate SHA-256 for both ----------------------------------
- SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1)
- SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1)
-
- # -- Get existing assets for cleanup --------------------------------
- ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- "${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]")
-
- # -- Create per-file .sha256 checksum files -------------------------
- echo "${SHA256_ZIP} ${ZIP_NAME}" > "/tmp/${ZIP_NAME}.sha256"
- echo "${SHA256_TAR} ${TAR_NAME}" > "/tmp/${TAR_NAME}.sha256"
-
- # -- Upload packages + checksums to release tag --------------------
- for ASSET in "${ZIP_NAME}" "${TAR_NAME}" "${ZIP_NAME}.sha256" "${TAR_NAME}.sha256"; do
- [ ! -f "/tmp/${ASSET}" ] && continue
- # Delete existing asset with same name
- ASSET_ID=$(echo "$ASSETS" | python3 -c "
- import sys,json
- assets = json.load(sys.stdin)
- for a in assets:
- if a['name'] == '${ASSET}':
- print(a['id']); break
- " 2>/dev/null || true)
- [ -n "$ASSET_ID" ] && curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- "${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true
- # Upload
- curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- -H "Content-Type: application/octet-stream" \
- --data-binary @"/tmp/${ASSET}" \
- "${API_BASE}/releases/${RELEASE_ID}/assets?name=${ASSET}" > /dev/null 2>&1 || true
- done
-
- # updates.xml already handled by Step 5 (updates_xml_build.php with preserve logic)
-
- echo "### Packages" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "| Package | Size | SHA-256 |" >> $GITHUB_STEP_SUMMARY
- echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY
- echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY
- echo "| Download | [${ZIP_NAME}](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}) |" >> $GITHUB_STEP_SUMMARY
+ php /tmp/moko-platform-api/cli/release_package.php \
+ --path . --version "$VERSION" --tag "$RELEASE_TAG" \
+ --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \
+ --repo "${GITEA_REPO}" --output /tmp || true
# -- STEP 8b: Update release description with changelog ----------------------
- name: "Step 8b: Update release body"
if: steps.version.outputs.skip != 'true'
run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ VERSION="${{ steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
MOKO_CLI="/tmp/moko-platform-api/cli"
@@ -621,44 +380,19 @@ jobs:
- name: "Step 9: Mirror release to GitHub"
if: >-
steps.version.outputs.skip != 'true' &&
- steps.version.outputs.stability == 'stable' &&
secrets.GH_TOKEN != ''
continue-on-error: true
- env:
- GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ VERSION="${{ steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- MAJOR="${{ steps.version.outputs.major }}"
- BRANCH="${{ steps.version.outputs.branch }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
-
- NOTES=$(php /tmp/moko-platform-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true)
- [ -z "$NOTES" ] && NOTES="Release ${VERSION}"
- echo "$NOTES" > /tmp/release_notes.md
-
- EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".tag_name // empty" || true)
-
- if [ -z "$EXISTING" ]; then
- gh release create "$RELEASE_TAG" \
- --repo "$GH_REPO" \
- --title "v${MAJOR} (latest: ${VERSION})" \
- --notes-file /tmp/release_notes.md \
- --target "$BRANCH" || true
- else
- gh release edit "$RELEASE_TAG" \
- --repo "$GH_REPO" \
- --title "v${MAJOR} (latest: ${VERSION})" || true
- fi
-
- # Upload assets to GitHub mirror
- for PKG in /tmp/${EXT_ELEMENT:-pkg}-${VERSION}.*; do
- if [ -f "$PKG" ]; then
- _RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".id // empty")
- [ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true
- fi
- done
- echo "GitHub mirror updated: ${GH_REPO} ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
+ 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.GA_TOKEN }}" --api-base "$API_BASE" \
+ --gh-token "${{ secrets.GH_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"
@@ -682,7 +416,7 @@ jobs:
- name: "Delete lesser pre-release channels"
continue-on-error: true
run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ VERSION="${{ steps.version.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/release_cascade.php \
--stability stable \
@@ -711,32 +445,20 @@ jobs:
# -- Dolibarr post-release: Reset dev version -----------------------------
- - name: "Dolibarr: Reset dev version"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.platform.outputs.platform == 'dolibarr' &&
- steps.platform.outputs.mod_file != ''
+ - 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}"
- TOKEN="${{ secrets.GA_TOKEN }}"
- MOD_FILE="${{ steps.platform.outputs.mod_file }}"
- ENCODED_PATH=$(echo "$MOD_FILE" | sed 's|^\./||' | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))")
- FILE_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/${ENCODED_PATH}?ref=dev" 2>/dev/null || true)
- FILE_SHA=$(echo "$FILE_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
- FILE_CONTENT=$(echo "$FILE_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true)
- if [ -n "$FILE_SHA" ] && [ -n "$FILE_CONTENT" ]; then
- UPDATED=$(echo "$FILE_CONTENT" | sed "s/\$this->version = '[^']*'/\$this->version = 'development'/")
- ENCODED=$(echo "$UPDATED" | base64 -w0)
- curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/contents/${ENCODED_PATH}" \
- -d "$(jq -n --arg content \"$ENCODED\" --arg sha \"$FILE_SHA\" --arg msg \"chore(version): reset dev version [skip ci]\" --arg branch \"dev\" '{content:$content,sha:$sha,message:$msg,branch:$branch}')" > /dev/null 2>&1 || true
- fi
+ php /tmp/moko-platform-api/cli/version_reset_dev.php \
+ --token "${{ secrets.GA_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 }}"
+ VERSION="${{ steps.version.outputs.version }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
--
2.52.0
From 582ec0a2db0ace6914a73d3d80790b368c9caff0 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Tue, 26 May 2026 22:25:15 +0000
Subject: [PATCH 10/40] chore(ci): update auto-bump.yml from moko-platform
[skip ci]
---
.mokogitea/workflows/auto-bump.yml | 24 ++++++++++++++++++------
1 file changed, 18 insertions(+), 6 deletions(-)
diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml
index ef0aedc..d1dedf1 100644
--- a/.mokogitea/workflows/auto-bump.yml
+++ b/.mokogitea/workflows/auto-bump.yml
@@ -16,6 +16,7 @@ on:
push:
branches:
- dev
+ - main
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
@@ -53,9 +54,20 @@ jobs:
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
fi
- - name: Patch bump version
+ - name: Bump version
run: |
- BUMP=$(php ${MOKO_CLI}/version_bump.php --path . 2>&1) || true
+ BRANCH="${{ github.ref_name }}"
+
+ # main = minor bump, dev = patch bump
+ if [ "$BRANCH" = "main" ]; then
+ BUMP_TYPE="--minor"
+ BUMP_LABEL="minor"
+ else
+ BUMP_TYPE=""
+ BUMP_LABEL="patch"
+ fi
+
+ BUMP=$(php ${MOKO_CLI}/version_bump.php --path . $BUMP_TYPE 2>&1) || true
echo "$BUMP"
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) || true
@@ -63,7 +75,7 @@ jobs:
# Propagate to platform manifests
php ${MOKO_CLI}/version_set_platform.php \
- --path . --version "$VERSION" --branch dev 2>/dev/null || true
+ --path . --version "$VERSION" --branch "$BRANCH" 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Commit if anything changed
@@ -76,7 +88,7 @@ jobs:
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 commit -m "chore(version): patch bump to ${VERSION} [skip ci]" \
+ git commit -m "chore(version): ${BUMP_LABEL} bump to ${VERSION} [skip ci]" \
--author="gitea-actions[bot] "
- git push origin dev
- echo "Bumped to ${VERSION}" >> $GITHUB_STEP_SUMMARY
+ git push origin "$BRANCH"
+ echo "Bumped to ${VERSION} (${BUMP_LABEL})" >> $GITHUB_STEP_SUMMARY
--
2.52.0
From e567d151ca9c4435ee3f0033344a92d351554c5c Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Tue, 26 May 2026 22:35:38 +0000
Subject: [PATCH 11/40] chore(ci): update auto-release.yml from moko-platform
[skip ci]
---
.mokogitea/workflows/auto-release.yml | 55 ++++++++++++++++++---------
1 file changed, 36 insertions(+), 19 deletions(-)
diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml
index d665ce7..e555c14 100644
--- a/.mokogitea/workflows/auto-release.yml
+++ b/.mokogitea/workflows/auto-release.yml
@@ -249,25 +249,7 @@ jobs:
php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true
php /tmp/moko-platform-api/cli/version_check.php --path . --fix 2>/dev/null || true
- - name: "Step 5: Write update stream"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.platform.outputs.platform == 'joomla'
- run: |
- VERSION="${{ steps.version.outputs.version }}"
-
- # Fetch latest updates.xml from main so preserve logic has all channels
- GA_TOKEN="${{ secrets.GA_TOKEN }}"
- API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
- curl -sf -H "Authorization: token ${GA_TOKEN}" \
- "${API}/contents/updates.xml?ref=main" 2>/dev/null | \
- python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin)['content']).decode())" \
- > updates.xml 2>/dev/null || true
-
- php /tmp/moko-platform-api/cli/updates_xml_build.php \
- --path . --version "${VERSION}" --stability stable \
- --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
- --github-output
+ # Step 5 (updates.xml) moved after Step 8 to include SHA-256 checksum
- name: Commit release changes
if: >-
@@ -336,6 +318,7 @@ jobs:
# -- STEP 8: Build packages and upload to release ----------------------------
- name: "Step 8: Build package and upload"
+ id: package
if: >-
steps.version.outputs.skip != 'true' &&
steps.rc.outputs.promote != 'true'
@@ -348,6 +331,40 @@ jobs:
--token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true
+ # -- STEP 5: Write update stream (after build so SHA-256 is available) -----
+ - name: "Step 5: Write update stream"
+ if: steps.version.outputs.skip != 'true'
+ run: |
+ VERSION="${{ steps.version.outputs.version }}"
+ SHA256="${{ steps.package.outputs.sha256_zip }}"
+
+ # Fetch latest updates.xml from main so preserve logic has all channels
+ GA_TOKEN="${{ secrets.GA_TOKEN }}"
+ API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
+ curl -sf -H "Authorization: token ${GA_TOKEN}" \
+ "${API}/contents/updates.xml?ref=main" 2>/dev/null | \
+ python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin)['content']).decode())" \
+ > updates.xml 2>/dev/null || true
+
+ SHA_FLAG=""
+ [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
+
+ php /tmp/moko-platform-api/cli/updates_xml_build.php \
+ --path . --version "${VERSION}" --stability stable \
+ --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
+ ${SHA_FLAG} --github-output
+
+ # Commit updates.xml if changed
+ 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 remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
+ git add updates.xml
+ git commit -m "chore: update stable channel ${VERSION} [skip ci]" \
+ --author="gitea-actions[bot] "
+ git push origin HEAD 2>&1 || true
+ fi
+
# -- STEP 8b: Update release description with changelog ----------------------
- name: "Step 8b: Update release body"
if: steps.version.outputs.skip != 'true'
--
2.52.0
From 1a8998702f9cf875bc27d1fef4b0d85762cfc4ec Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Tue, 26 May 2026 22:37:01 +0000
Subject: [PATCH 12/40] chore(ci): update pre-release.yml from moko-platform
[skip ci]
---
.mokogitea/workflows/pre-release.yml | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml
index 83443c7..a84e469 100644
--- a/.mokogitea/workflows/pre-release.yml
+++ b/.mokogitea/workflows/pre-release.yml
@@ -162,9 +162,18 @@ jobs:
zip -r "../../build/package/${EXT_NAME}.zip" . -x $EXCLUDES
cd "$OLDPWD"
done
+ # Copy top-level files (manifest XML, script PHP, etc.)
for f in "${SOURCE_DIR}"/*.xml "${SOURCE_DIR}"/*.php; do
[ -f "$f" ] && cp "$f" build/package/
done
+ # Copy top-level directories (language/, media/, etc.) — exclude packages/
+ for d in "${SOURCE_DIR}"/*/; do
+ [ ! -d "$d" ] && continue
+ DIRNAME=$(basename "$d")
+ [ "$DIRNAME" = "packages" ] && continue
+ cp -r "$d" "build/package/${DIRNAME}"
+ echo " Included dir: ${DIRNAME}/"
+ done
else
echo "=== Building standard extension ==="
rsync -a \
@@ -245,15 +254,20 @@ jobs:
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
+ SHA256="${{ steps.zip.outputs.sha256 }}"
if [ ! -f "updates.xml" ]; then
echo "No updates.xml -- skipping"
exit 0
fi
+ SHA_FLAG=""
+ [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
+
php ${MOKO_CLI}/updates_xml_build.php \
--path . --version "${VERSION}" --stability "${STABILITY}" \
- --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}"
+ --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
+ ${SHA_FLAG}
# Commit and push
if ! git diff --quiet updates.xml 2>/dev/null; then
--
2.52.0
From f8617e298cae633a2fd2224108bac57d6db0bb71 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Tue, 26 May 2026 22:48:32 +0000
Subject: [PATCH 13/40] chore(ci): update auto-release.yml from moko-platform
[skip ci]
---
.mokogitea/workflows/auto-release.yml | 60 +++++++++++++--------------
1 file changed, 28 insertions(+), 32 deletions(-)
diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml
index e555c14..80908e4 100644
--- a/.mokogitea/workflows/auto-release.yml
+++ b/.mokogitea/workflows/auto-release.yml
@@ -87,7 +87,7 @@ jobs:
--from auto --to release-candidate \
--token "${{ secrets.GA_TOKEN }}" \
--api-base "${API_BASE}" \
- --branch "${{ github.event.pull_request.head.ref }}"
+ --branch "${{ github.event.pull_request.head.ref || 'dev' }}"
- name: Cascade lesser channels
continue-on-error: true
@@ -180,7 +180,17 @@ jobs:
echo "::notice::No RC release — full build pipeline"
fi
- # Version bump handled by auto-bump.yml (minor on main, patch on dev)
+ - name: "Step 1b: Minor bump version"
+ id: bump
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.rc.outputs.promote != 'true'
+ run: |
+ MOKO_API="/tmp/moko-platform-api/cli"
+ php ${MOKO_API}/version_bump.php --path . --minor 2>&1 || true
+ VERSION=$(php ${MOKO_API}/version_read.php --path .)
+ echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
+ echo "Bumped to: ${VERSION}"
- name: Check if already released
if: steps.version.outputs.skip != 'true'
@@ -207,7 +217,7 @@ jobs:
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
- VERSION="${{ steps.version.outputs.version }}"
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
php /tmp/moko-platform-api/cli/release_validate.php \
--path . --version "$VERSION" --output-summary --github-output || true
@@ -218,7 +228,7 @@ jobs:
run: |
BRANCH="${{ steps.version.outputs.branch }}"
IS_MINOR="${{ steps.version.outputs.is_minor }}"
- PATCH="${{ steps.version.outputs.version }}"
+ PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}')
# Check if branch exists
@@ -237,7 +247,7 @@ jobs:
steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: |
- VERSION="${{ steps.version.outputs.version }}"
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
php /tmp/moko-platform-api/cli/version_set_platform.php \
--path . --version "$VERSION" --branch main
@@ -245,7 +255,7 @@ jobs:
- name: "Step 4: Update version badges"
if: steps.version.outputs.skip != 'true'
run: |
- VERSION="${{ steps.version.outputs.version }}"
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true
php /tmp/moko-platform-api/cli/version_check.php --path . --fix 2>/dev/null || true
@@ -260,7 +270,7 @@ jobs:
echo "No changes to commit"
exit 0
fi
- VERSION="${{ steps.version.outputs.version }}"
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
# Set push URL with token for branch-protected repos
@@ -292,7 +302,7 @@ jobs:
steps.version.outputs.skip != 'true' &&
steps.rc.outputs.promote == 'true'
run: |
- VERSION="${{ steps.version.outputs.version }}"
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/release_promote.php \
--from release-candidate --to stable \
@@ -307,7 +317,7 @@ jobs:
steps.version.outputs.skip != 'true' &&
steps.rc.outputs.promote != 'true'
run: |
- VERSION="${{ steps.version.outputs.version }}"
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/release_create.php \
@@ -323,7 +333,7 @@ jobs:
steps.version.outputs.skip != 'true' &&
steps.rc.outputs.promote != 'true'
run: |
- VERSION="${{ steps.version.outputs.version }}"
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/release_package.php \
@@ -335,7 +345,7 @@ jobs:
- name: "Step 5: Write update stream"
if: steps.version.outputs.skip != 'true'
run: |
- VERSION="${{ steps.version.outputs.version }}"
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
# Fetch latest updates.xml from main so preserve logic has all channels
@@ -368,29 +378,15 @@ jobs:
# -- STEP 8b: Update release description with changelog ----------------------
- name: "Step 8b: Update release body"
if: steps.version.outputs.skip != 'true'
+ continue-on-error: true
run: |
- VERSION="${{ steps.version.outputs.version }}"
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- MOKO_CLI="/tmp/moko-platform-api/cli"
-
- php ${MOKO_CLI}/release_body_update.php \
+ php /tmp/moko-platform-api/cli/release_body_update.php \
--path . --version "${VERSION}" --tag "${RELEASE_TAG}" \
--token "${{ secrets.GA_TOKEN }}" \
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
- 2>/dev/null || {
- # Fallback: simple body update if CLI not available
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null | \
- python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
- if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then
- BODY="## ${VERSION} ($(date +%Y-%m-%d))\n\nChecksum files attached as \`*.sha256\` assets."
- curl -sf -X PATCH -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- -H "Content-Type: application/json" \
- "${API_BASE}/releases/${RELEASE_ID}" \
- -d "{\"body\":\"${BODY}\"}" > /dev/null 2>&1
- fi
- }
+ 2>&1 || true
echo "Release body updated" >> $GITHUB_STEP_SUMMARY
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
@@ -400,7 +396,7 @@ jobs:
secrets.GH_TOKEN != ''
continue-on-error: true
run: |
- VERSION="${{ steps.version.outputs.version }}"
+ 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}"
@@ -433,7 +429,7 @@ jobs:
- name: "Delete lesser pre-release channels"
continue-on-error: true
run: |
- VERSION="${{ steps.version.outputs.version }}"
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/release_cascade.php \
--stability stable \
@@ -475,7 +471,7 @@ jobs:
- name: Pipeline Summary
if: always()
run: |
- VERSION="${{ steps.version.outputs.version }}"
+ 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
--
2.52.0
From f5df49833b8f3d996e0dee950a1c0b7d3ac38d87 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Tue, 26 May 2026 22:49:45 +0000
Subject: [PATCH 14/40] chore(ci): update auto-bump.yml from moko-platform
[skip ci]
---
.mokogitea/workflows/auto-bump.yml | 30 ++++++++++--------------------
1 file changed, 10 insertions(+), 20 deletions(-)
diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml
index d1dedf1..10a7e51 100644
--- a/.mokogitea/workflows/auto-bump.yml
+++ b/.mokogitea/workflows/auto-bump.yml
@@ -8,7 +8,7 @@
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.mokogitea/workflows/auto-bump.yml
# VERSION: 09.02.00
-# BRIEF: Auto patch-bump version on every push to dev
+# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
name: "Universal: Auto Version Bump"
@@ -16,9 +16,9 @@ on:
push:
branches:
- dev
- - main
env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
permissions:
@@ -26,11 +26,12 @@ permissions:
jobs:
bump:
- name: Patch Bump
+ name: Version Bump
runs-on: release
if: >-
!contains(github.event.head_commit.message, '[skip ci]') &&
- !contains(github.event.head_commit.message, '[skip bump]')
+ !contains(github.event.head_commit.message, '[skip bump]') &&
+ !startsWith(github.event.head_commit.message, 'Merge pull request')
steps:
- name: Checkout
@@ -56,18 +57,7 @@ jobs:
- name: Bump version
run: |
- BRANCH="${{ github.ref_name }}"
-
- # main = minor bump, dev = patch bump
- if [ "$BRANCH" = "main" ]; then
- BUMP_TYPE="--minor"
- BUMP_LABEL="minor"
- else
- BUMP_TYPE=""
- BUMP_LABEL="patch"
- fi
-
- BUMP=$(php ${MOKO_CLI}/version_bump.php --path . $BUMP_TYPE 2>&1) || true
+ BUMP=$(php ${MOKO_CLI}/version_bump.php --path . 2>&1) || true
echo "$BUMP"
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) || true
@@ -75,7 +65,7 @@ jobs:
# Propagate to platform manifests
php ${MOKO_CLI}/version_set_platform.php \
- --path . --version "$VERSION" --branch "$BRANCH" 2>/dev/null || true
+ --path . --version "$VERSION" --branch dev 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Commit if anything changed
@@ -88,7 +78,7 @@ jobs:
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 commit -m "chore(version): ${BUMP_LABEL} bump to ${VERSION} [skip ci]" \
+ git commit -m "chore(version): patch bump to ${VERSION} [skip ci]" \
--author="gitea-actions[bot] "
- git push origin "$BRANCH"
- echo "Bumped to ${VERSION} (${BUMP_LABEL})" >> $GITHUB_STEP_SUMMARY
+ git push origin dev
+ echo "Bumped to ${VERSION}" >> $GITHUB_STEP_SUMMARY
--
2.52.0
From 878ac0d6a701ed85974a6d5866690672d495bfb5 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Tue, 26 May 2026 22:50:57 +0000
Subject: [PATCH 15/40] chore(ci): update pre-release.yml from moko-platform
[skip ci]
---
.mokogitea/workflows/pre-release.yml | 162 +++++----------------------
1 file changed, 29 insertions(+), 133 deletions(-)
diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml
index a84e469..f0b1d88 100644
--- a/.mokogitea/workflows/pre-release.yml
+++ b/.mokogitea/workflows/pre-release.yml
@@ -52,28 +52,19 @@ jobs:
fetch-depth: 0
token: ${{ secrets.GA_TOKEN }}
- - name: Setup tools
+ - name: Setup moko-platform tools
+ env:
+ MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
+ MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
- # Update moko-platform CLI tools if available; install PHP if missing
- if command -v moko-platform-update &> /dev/null; then
- moko-platform-update
- elif [ -d "/opt/moko-platform" ]; then
- cd /opt/moko-platform && git pull origin main --quiet 2>/dev/null || true
- else
- 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
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
- /tmp/moko-platform-api
- fi
- # Set MOKO_CLI to whichever path exists
- if [ -d "/opt/moko-platform/cli" ]; then
- echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
- else
- echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
+ 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}/moko-platform.git" \
+ /tmp/moko-platform-api
+ cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
+ echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
- name: Detect platform
id: platform
@@ -129,132 +120,37 @@ jobs:
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
-
- MANIFEST="${{ steps.meta.outputs.manifest }}"
- EXT_TYPE=""
- if [ -n "$MANIFEST" ]; then
- EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- fi
-
- EXCLUDES="sftp-config* .ftpignore *.ppk *.pem *.key .env* *.local .build-trigger"
-
- mkdir -p build/package
-
- if [ "$EXT_TYPE" = "package" ] && [ -d "${SOURCE_DIR}/packages" ]; then
- echo "=== Building Joomla PACKAGE (multi-extension) ==="
- for ext_dir in "${SOURCE_DIR}"/packages/*/; do
- [ ! -d "$ext_dir" ] && continue
- EXT_NAME=$(basename "$ext_dir")
- echo " Packaging sub-extension: ${EXT_NAME}"
- cd "$ext_dir"
- zip -r "../../build/package/${EXT_NAME}.zip" . -x $EXCLUDES
- cd "$OLDPWD"
- done
- # Copy top-level files (manifest XML, script PHP, etc.)
- for f in "${SOURCE_DIR}"/*.xml "${SOURCE_DIR}"/*.php; do
- [ -f "$f" ] && cp "$f" build/package/
- done
- # Copy top-level directories (language/, media/, etc.) — exclude packages/
- for d in "${SOURCE_DIR}"/*/; do
- [ ! -d "$d" ] && continue
- DIRNAME=$(basename "$d")
- [ "$DIRNAME" = "packages" ] && continue
- cp -r "$d" "build/package/${DIRNAME}"
- echo " Included dir: ${DIRNAME}/"
- done
- else
- echo "=== Building standard extension ==="
- rsync -a \
- --exclude='sftp-config*' \
- --exclude='.ftpignore' \
- --exclude='*.ppk' \
- --exclude='*.pem' \
- --exclude='*.key' \
- --exclude='.env*' \
- --exclude='*.local' \
- --exclude='.build-trigger' \
- "${SOURCE_DIR}/" build/package/
- fi
-
- - 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
+ - name: Create 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)
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ php ${MOKO_CLI}/release_create.php \
+ --path . --version "$VERSION" --tag "$TAG" \
+ --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \
+ --repo "${GITEA_REPO}" --branch dev --prerelease
- 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: Build package and upload
+ id: package
+ run: |
+ VERSION="${{ steps.meta.outputs.version }}"
+ TAG="${{ steps.meta.outputs.tag }}"
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ php ${MOKO_CLI}/release_package.php \
+ --path . --version "$VERSION" --tag "$TAG" \
+ --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \
+ --repo "${GITEA_REPO}" --output /tmp || true
- name: Update updates.xml
if: steps.platform.outputs.platform == 'joomla'
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
- SHA256="${{ steps.zip.outputs.sha256 }}"
+ SHA256="${{ steps.package.outputs.sha256_zip }}"
if [ ! -f "updates.xml" ]; then
echo "No updates.xml -- skipping"
@@ -316,7 +212,7 @@ jobs:
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
- SHA256="${{ steps.zip.outputs.sha256 }}"
+ SHA256="${{ steps.package.outputs.sha256_zip }}"
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
--
2.52.0
From ead3765383d6d170deb4ff8f73a7970ef3ee4147 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Thu, 28 May 2026 20:02:22 +0000
Subject: [PATCH 16/40] chore: sync .mokogitea/workflows/auto-release.yml from
moko-platform [skip ci]
---
.mokogitea/workflows/auto-release.yml | 1020 +++++++++++++------------
1 file changed, 528 insertions(+), 492 deletions(-)
diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml
index 80908e4..a05d0f4 100644
--- a/.mokogitea/workflows/auto-release.yml
+++ b/.mokogitea/workflows/auto-release.yml
@@ -1,492 +1,528 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Release
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
-# PATH: /templates/workflows/universal/auto-release.yml.template
-# VERSION: 05.00.00
-# BRIEF: Universal build & release � detects platform from manifest.xml
-#
-# +========================================================================+
-# | UNIVERSAL BUILD & RELEASE PIPELINE |
-# +========================================================================+
-# | |
-# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
-# | |
-# | Platform-specific: |
-# | joomla: XML manifest, updates.xml, type-prefixed packages |
-# | dolibarr: mod*.class.php, update.txt, dev version reset |
-# | generic: README-only, no update stream |
-# | |
-# +========================================================================+
-
-name: "Universal: Build & Release"
-
-on:
- pull_request:
- types: [opened, closed]
- branches:
- - main
- workflow_dispatch:
- inputs:
- action:
- description: 'Action to perform'
- required: false
- type: choice
- default: release
- options:
- - release
- - promote-rc
-
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
- GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
-
-permissions:
- contents: write
-
-jobs:
- # ── Draft PR → Promote highest pre-release to RC ─────────────────────────────
- promote-rc:
- name: Promote Pre-Release to RC
- runs-on: release
- if: >-
- (github.event.action == 'opened' && github.event.pull_request.draft == true) ||
- (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- token: ${{ secrets.GA_TOKEN }}
- fetch-depth: 1
-
- - name: Setup moko-platform tools
- env:
- MOKO_CLONE_TOKEN: ${{ secrets.GA_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
- 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: Promote to release-candidate
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/release_promote.php \
- --from auto --to release-candidate \
- --token "${{ secrets.GA_TOKEN }}" \
- --api-base "${API_BASE}" \
- --branch "${{ github.event.pull_request.head.ref || 'dev' }}"
-
- - name: Cascade lesser channels
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/release_cascade.php \
- --stability release-candidate \
- --token "${{ secrets.GA_TOKEN }}" \
- --api-base "${API_BASE}"
-
- - name: Summary
- if: always()
- run: |
- echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
- echo "Draft PR opened — promoted highest pre-release to RC" >> $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.GA_TOKEN }}
- fetch-depth: 0
-
- - name: Setup moko-platform tools
- env:
- MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
- MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
- COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}'
- run: |
- # Ensure PHP + Composer are available
- if ! command -v composer &> /dev/null; then
- sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
- fi
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
- /tmp/moko-platform-api
- cd /tmp/moko-platform-api
- composer install --no-dev --no-interaction --quiet
-
-
- # -- PLATFORM DETECTION ---------------------------------------------------
- - name: Detect platform
- id: platform
- run: |
- php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output
- MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1 || true)
- MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1 || true)
- echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
- echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT"
-
- - name: "Step 1: Read version"
- id: version
- run: |
- VERSION=$(php /tmp/moko-platform-api/cli/version_read.php --path .)
- if [ -z "$VERSION" ]; then
- echo "::error::No VERSION in README.md"
- echo "skip=true" >> "$GITHUB_OUTPUT"
- exit 0
- fi
- MAJOR=$(echo "$VERSION" | cut -d. -f1)
- echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- echo "release_tag=stable" >> "$GITHUB_OUTPUT"
- echo "skip=false" >> "$GITHUB_OUTPUT"
- echo "branch=main" >> "$GITHUB_OUTPUT"
-
- # -- CHECK FOR RC PROMOTION ------------------------------------------------
- - name: "Check for RC release"
- id: rc
- if: steps.version.outputs.skip != 'true'
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- RC_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
- "${API_BASE}/releases/tags/release-candidate" 2>/dev/null || echo "{}")
- RC_ID=$(echo "$RC_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true)
-
- if [ -n "$RC_ID" ] && [ "$RC_ID" != "None" ] && [ "$RC_ID" != "" ]; then
- echo "promote=true" >> "$GITHUB_OUTPUT"
- echo "release_id=${RC_ID}" >> "$GITHUB_OUTPUT"
- echo "::notice::RC release found (id: ${RC_ID}) — will promote to stable"
- else
- echo "promote=false" >> "$GITHUB_OUTPUT"
- echo "::notice::No RC release — full build pipeline"
- fi
-
- - name: "Step 1b: Minor bump version"
- id: bump
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.rc.outputs.promote != 'true'
- run: |
- MOKO_API="/tmp/moko-platform-api/cli"
- php ${MOKO_API}/version_bump.php --path . --minor 2>&1 || true
- VERSION=$(php ${MOKO_API}/version_read.php --path .)
- echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- echo "Bumped to: ${VERSION}"
-
- - name: Check if already released
- if: steps.version.outputs.skip != 'true'
- id: check
- run: |
- TAG="${{ steps.version.outputs.release_tag }}"
- BRANCH="${{ steps.version.outputs.branch }}"
-
- TAG_EXISTS=false
- BRANCH_EXISTS=false
-
- git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true
- git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true
-
- echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT"
- echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT"
-
- # Tag and branch may persist across patch releases — never skip
- echo "already_released=false" >> "$GITHUB_OUTPUT"
-
- # -- SANITY CHECKS -------------------------------------------------------
- - name: "Sanity: Pre-release validation"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.check.outputs.already_released != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- php /tmp/moko-platform-api/cli/release_validate.php \
- --path . --version "$VERSION" --output-summary --github-output || true
-
- # -- STEP 2: Create or update version/XX.YY archive branch ---------------
- # Always runs — every version change on main archives to version/XX.YY
- - name: "Step 2: Version archive branch"
- if: steps.check.outputs.already_released != 'true'
- run: |
- BRANCH="${{ steps.version.outputs.branch }}"
- IS_MINOR="${{ steps.version.outputs.is_minor }}"
- PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}')
-
- # Check if branch exists
- if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
- git push origin HEAD:"$BRANCH" --force
- echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY
- else
- git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH"
- git push origin "$BRANCH" --force
- echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
- fi
-
- # -- STEP 3: Set platform version ----------------------------------------
- - name: "Step 3: Set platform version"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.check.outputs.already_released != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- php /tmp/moko-platform-api/cli/version_set_platform.php \
- --path . --version "$VERSION" --branch main
-
- # -- STEP 4: Update version badges ----------------------------------------
- - name: "Step 4: Update version badges"
- if: steps.version.outputs.skip != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true
- php /tmp/moko-platform-api/cli/version_check.php --path . --fix 2>/dev/null || true
-
- # Step 5 (updates.xml) moved after Step 8 to include SHA-256 checksum
-
- - name: Commit release changes
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.check.outputs.already_released != 'true'
- run: |
- if git diff --quiet && git diff --cached --quiet; then
- echo "No changes to commit"
- exit 0
- fi
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- # Set push URL with token for branch-protected repos
- git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- git add -A
- git commit -m "chore(release): build ${VERSION} [skip ci]" \
- --author="gitea-actions[bot] "
- git push -u origin HEAD
-
- # -- STEP 6: Create tag ---------------------------------------------------
- - name: "Step 6: Create git tag"
- if: >-
- steps.version.outputs.skip != 'true'
- run: |
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- # Only create the major release tag if it doesn't exist yet
- if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then
- git tag "$RELEASE_TAG"
- git push origin "$RELEASE_TAG"
- echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
- else
- echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY
- fi
- echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
-
- # -- STEP 7a: Promote RC to stable (skip build) ----------------------------
- - name: "Step 7a: Promote RC to stable"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.rc.outputs.promote == 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/release_promote.php \
- --from release-candidate --to stable \
- --token "${{ secrets.GA_TOKEN }}" \
- --api-base "${API_BASE}" \
- --path . --branch main
- echo "Promoted RC → stable (${VERSION})" >> $GITHUB_STEP_SUMMARY
-
- # -- STEP 7b: Create or update Gitea Release (full build path) -------------
- - name: "Step 7b: Gitea Release"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.rc.outputs.promote != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/release_create.php \
- --path . --version "$VERSION" --tag "$RELEASE_TAG" \
- --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \
- --repo "${GITEA_REPO}" --branch main
- echo "Release created: ${VERSION}" >> $GITHUB_STEP_SUMMARY
-
- # -- STEP 8: Build packages and upload to release ----------------------------
- - name: "Step 8: Build package and upload"
- id: package
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.rc.outputs.promote != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/release_package.php \
- --path . --version "$VERSION" --tag "$RELEASE_TAG" \
- --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \
- --repo "${GITEA_REPO}" --output /tmp || true
-
- # -- STEP 5: Write update stream (after build so SHA-256 is available) -----
- - name: "Step 5: Write update stream"
- if: steps.version.outputs.skip != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- SHA256="${{ steps.package.outputs.sha256_zip }}"
-
- # Fetch latest updates.xml from main so preserve logic has all channels
- GA_TOKEN="${{ secrets.GA_TOKEN }}"
- API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
- curl -sf -H "Authorization: token ${GA_TOKEN}" \
- "${API}/contents/updates.xml?ref=main" 2>/dev/null | \
- python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin)['content']).decode())" \
- > updates.xml 2>/dev/null || true
-
- SHA_FLAG=""
- [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
-
- php /tmp/moko-platform-api/cli/updates_xml_build.php \
- --path . --version "${VERSION}" --stability stable \
- --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
- ${SHA_FLAG} --github-output
-
- # Commit updates.xml if changed
- 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 remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- git add updates.xml
- git commit -m "chore: update stable channel ${VERSION} [skip ci]" \
- --author="gitea-actions[bot] "
- git push origin HEAD 2>&1 || true
- fi
-
- # -- STEP 8b: Update release description with changelog ----------------------
- - name: "Step 8b: Update release body"
- if: steps.version.outputs.skip != 'true'
- continue-on-error: true
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- php /tmp/moko-platform-api/cli/release_body_update.php \
- --path . --version "${VERSION}" --tag "${RELEASE_TAG}" \
- --token "${{ secrets.GA_TOKEN }}" \
- --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
- 2>&1 || true
- echo "Release body updated" >> $GITHUB_STEP_SUMMARY
-
- # -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- - name: "Step 9: Mirror release to GitHub"
- if: >-
- steps.version.outputs.skip != 'true' &&
- secrets.GH_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.GA_TOKEN }}" --api-base "$API_BASE" \
- --gh-token "${{ secrets.GH_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_TOKEN != ''
- continue-on-error: true
- run: |
- GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
- GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
- GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
- git remote add github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
- git remote set-url github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
- git fetch origin main --depth=1
- git push github origin/main:refs/heads/main --force 2>/dev/null \
- && echo "main branch pushed to GitHub mirror" \
- || echo "WARNING: GitHub mirror push failed"
-
- # -- Clean up lesser pre-releases (cascade) ---------------------------------
- # stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev
- - name: "Delete lesser pre-release channels"
- continue-on-error: true
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/release_cascade.php \
- --stability stable \
- --version "${VERSION}" \
- --token "${{ secrets.GA_TOKEN }}" \
- --api-base "${API_BASE}" 2>/dev/null || true
-
- - name: "Step 11: Delete and recreate dev branch from main"
- if: steps.version.outputs.skip != 'true'
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- TOKEN="${{ secrets.GA_TOKEN }}"
-
- # Delete dev branch
- curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
- "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
-
- # Recreate dev from main (now includes version bump + changelog promotion)
- curl -sf -X POST -H "Authorization: token ${TOKEN}" \
- -H "Content-Type: application/json" \
- "${API_BASE}/branches" \
- -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
-
- echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY
-
-
- # -- 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.GA_TOKEN }}" --api-base "${API_BASE}" \
- --branch dev --path . 2>&1 || true
-
- # -- Summary --------------------------------------------------------------
- - name: Pipeline Summary
- if: always()
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- PLATFORM="${{ steps.platform.outputs.platform }}"
- if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
- echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
- echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
- elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
- echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
- else
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
- echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
- echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
- fi
+# Copyright (C) 2026 Moko Consulting
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# FILE INFORMATION
+# DEFGROUP: Gitea.Workflow
+# INGROUP: moko-platform.Release
+# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
+# PATH: /templates/workflows/universal/auto-release.yml.template
+# VERSION: 05.00.00
+# BRIEF: Universal build & release � detects platform from manifest.xml
+#
+# +========================================================================+
+# | UNIVERSAL BUILD & RELEASE PIPELINE |
+# +========================================================================+
+# | |
+# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
+# | |
+# | Platform-specific: |
+# | joomla: XML manifest, 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:
+ # ── Draft PR → Promote highest pre-release to RC ─────────────────────────────
+ promote-rc:
+ name: Promote Pre-Release to RC
+ runs-on: release
+ if: >-
+ (github.event.action == 'opened' && github.event.pull_request.draft == 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: Promote to release-candidate
+ run: |
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ php /tmp/moko-platform-api/cli/release_promote.php \
+ --from auto --to release-candidate \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" \
+ --api-base "${API_BASE}" \
+ --branch "${{ github.event.pull_request.head.ref || 'dev' }}"
+
+ - name: Cascade lesser channels
+ continue-on-error: true
+ run: |
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ php /tmp/moko-platform-api/cli/release_cascade.php \
+ --stability release-candidate \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" \
+ --api-base "${API_BASE}"
+
+ - name: Summary
+ if: always()
+ run: |
+ echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
+ echo "Draft PR opened — promoted highest pre-release to RC" >> $GITHUB_STEP_SUMMARY
+
+ # ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
+ release:
+ name: Build & Release Pipeline
+ runs-on: release
+ if: >-
+ github.event.pull_request.merged == true ||
+ (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ token: ${{ secrets.MOKOGITEA_TOKEN }}
+ fetch-depth: 0
+
+ - name: Configure git for bot pushes
+ run: |
+ git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
+ git config --local user.name "gitea-actions[bot]"
+ git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
+
+ - name: Setup moko-platform tools
+ env:
+ MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
+ MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
+ COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
+ run: |
+ # Ensure PHP + Composer are available
+ if ! command -v composer &> /dev/null; then
+ sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
+ fi
+ # Always fetch latest CLI tools — never use stale cache from previous runs
+ rm -rf /tmp/moko-platform-api
+ git clone --depth 1 --branch main --quiet \
+ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
+ /tmp/moko-platform-api
+ cd /tmp/moko-platform-api
+ composer install --no-dev --no-interaction --quiet
+
+
+ # -- PLATFORM DETECTION ---------------------------------------------------
+ - name: Detect platform
+ id: platform
+ run: |
+ php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output
+ MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1 || true)
+ MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1 || true)
+ echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
+ echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT"
+
+ - name: "Step 1: Read version"
+ id: version
+ run: |
+ VERSION=$(php /tmp/moko-platform-api/cli/version_read.php --path .)
+ if [ -z "$VERSION" ]; then
+ echo "::error::No VERSION in README.md"
+ echo "skip=true" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+ # Strip any pre-release suffix merged from dev (e.g. 01.02.20-dev → 01.02.20)
+ VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
+ MAJOR=$(echo "$VERSION" | cut -d. -f1)
+ echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
+ echo "release_tag=stable" >> "$GITHUB_OUTPUT"
+ echo "skip=false" >> "$GITHUB_OUTPUT"
+ echo "branch=main" >> "$GITHUB_OUTPUT"
+
+ # -- CHECK FOR RC PROMOTION ------------------------------------------------
+ - name: "Check for RC release"
+ id: rc
+ if: steps.version.outputs.skip != 'true'
+ run: |
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ RC_JSON=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
+ "${API_BASE}/releases/tags/release-candidate" 2>/dev/null || echo "{}")
+ RC_ID=$(echo "$RC_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true)
+
+ if [ -n "$RC_ID" ] && [ "$RC_ID" != "None" ] && [ "$RC_ID" != "" ]; then
+ echo "promote=true" >> "$GITHUB_OUTPUT"
+ echo "release_id=${RC_ID}" >> "$GITHUB_OUTPUT"
+ echo "::notice::RC release found (id: ${RC_ID}) — will promote to stable"
+ else
+ echo "promote=false" >> "$GITHUB_OUTPUT"
+ echo "::notice::No RC release — full build pipeline"
+ fi
+
+ - name: "Step 1b: Minor bump version"
+ id: bump
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.rc.outputs.promote != 'true'
+ run: |
+ MOKO_API="/tmp/moko-platform-api/cli"
+ php ${MOKO_API}/version_bump.php --path . --minor 2>&1 || true
+ VERSION=$(php ${MOKO_API}/version_read.php --path .)
+ echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
+ echo "Bumped to: ${VERSION}"
+
+ - name: Check if already released
+ if: steps.version.outputs.skip != 'true'
+ id: check
+ run: |
+ TAG="${{ steps.version.outputs.release_tag }}"
+ BRANCH="${{ steps.version.outputs.branch }}"
+
+ TAG_EXISTS=false
+ BRANCH_EXISTS=false
+
+ git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true
+ git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true
+
+ echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT"
+ echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT"
+
+ # Tag and branch may persist across patch releases — never skip
+ echo "already_released=false" >> "$GITHUB_OUTPUT"
+
+ # -- SANITY CHECKS -------------------------------------------------------
+ - name: "Sanity: Pre-release validation"
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.check.outputs.already_released != 'true'
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ php /tmp/moko-platform-api/cli/release_validate.php \
+ --path . --version "$VERSION" --output-summary --github-output || true
+
+ # -- STEP 2: Create or update version/XX.YY archive branch ---------------
+ # Always runs — every version change on main archives to version/XX.YY
+ - name: "Step 2: Version archive branch"
+ if: steps.check.outputs.already_released != 'true'
+ run: |
+ BRANCH="${{ steps.version.outputs.branch }}"
+ IS_MINOR="${{ steps.version.outputs.is_minor }}"
+ PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}')
+
+ # Check if branch exists
+ if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
+ git push origin HEAD:"$BRANCH" --force
+ echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY
+ else
+ git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH"
+ git push origin "$BRANCH" --force
+ echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
+ fi
+
+ # -- STEP 3: Set platform version ----------------------------------------
+ - name: "Step 3: Set platform version"
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.check.outputs.already_released != 'true'
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ php /tmp/moko-platform-api/cli/version_set_platform.php \
+ --path . --version "$VERSION" --branch main
+
+ # -- STEP 4: Update version badges ----------------------------------------
+ - name: "Step 4: Update version badges"
+ if: steps.version.outputs.skip != 'true'
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true
+ php /tmp/moko-platform-api/cli/version_check.php --path . --fix 2>/dev/null || true
+
+ # Step 5 (updates.xml) moved after Step 8 to include SHA-256 checksum
+
+ - name: "Step 4b: Promote and prune CHANGELOG"
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.check.outputs.already_released != 'true'
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ MOKO_API="/tmp/moko-platform-api/cli"
+ if [ -f "CHANGELOG.md" ]; then
+ php ${MOKO_API}/changelog_promote.php --path . --version "$VERSION" 2>&1 || true
+ php ${MOKO_API}/changelog_prune.php --path . --keep 5 2>&1 || true
+ fi
+
+ - name: Commit release changes
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.check.outputs.already_released != 'true'
+ run: |
+ if git diff --quiet && git diff --cached --quiet; then
+ echo "No changes to commit"
+ exit 0
+ fi
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ git add -A
+ git commit -m "chore(release): build ${VERSION} [skip ci]" \
+ --author="gitea-actions[bot] "
+ git push -u origin HEAD
+
+ # -- STEP 6: Create tag ---------------------------------------------------
+ - name: "Step 6: Create git tag"
+ if: >-
+ steps.version.outputs.skip != 'true'
+ run: |
+ RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
+ # Only create the major release tag if it doesn't exist yet
+ if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then
+ git tag "$RELEASE_TAG"
+ git push origin "$RELEASE_TAG"
+ echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
+ else
+ echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY
+ fi
+ echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
+
+ # -- STEP 7a: Promote RC to stable (skip build) ----------------------------
+ - name: "Step 7a: Promote RC to stable"
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.rc.outputs.promote == 'true'
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ php /tmp/moko-platform-api/cli/release_promote.php \
+ --from release-candidate --to stable \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" \
+ --api-base "${API_BASE}" \
+ --path . --branch main
+ echo "Promoted RC → stable (${VERSION})" >> $GITHUB_STEP_SUMMARY
+
+ # -- STEP 7b: Create or update Gitea Release (full build path) -------------
+ - name: "Step 7b: Gitea Release"
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.rc.outputs.promote != 'true'
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ php /tmp/moko-platform-api/cli/release_create.php \
+ --path . --version "$VERSION" --tag "$RELEASE_TAG" \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
+ --repo "${GITEA_REPO}" --branch main
+ echo "Release created: ${VERSION}" >> $GITHUB_STEP_SUMMARY
+
+ # -- STEP 8: Build packages and upload to release ----------------------------
+ - name: "Step 8: Build package and upload"
+ id: package
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.rc.outputs.promote != 'true'
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ php /tmp/moko-platform-api/cli/release_package.php \
+ --path . --version "$VERSION" --tag "$RELEASE_TAG" \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
+ --repo "${GITEA_REPO}" --output /tmp || true
+
+ # -- STEP 5: Write update stream (after build so SHA-256 is available) -----
+ - name: "Step 5: Write update stream"
+ if: steps.version.outputs.skip != 'true'
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ SHA256="${{ steps.package.outputs.sha256_zip }}"
+
+ # Fetch latest updates.xml from main so preserve logic has all channels
+ GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
+ API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
+ curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
+ "${API}/contents/updates.xml?ref=main" 2>/dev/null | \
+ python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin)['content']).decode())" \
+ > updates.xml 2>/dev/null || true
+
+ SHA_FLAG=""
+ [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
+
+ php /tmp/moko-platform-api/cli/updates_xml_build.php \
+ --path . --version "${VERSION}" --stability stable \
+ --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
+ ${SHA_FLAG} --github-output
+
+ # Commit updates.xml if changed
+ if ! git diff --quiet updates.xml 2>/dev/null; then
+ git add updates.xml
+ git commit -m "chore: update stable channel ${VERSION} [skip ci]" \
+ --author="gitea-actions[bot] "
+ git push origin HEAD 2>&1 || true
+ fi
+
+ # -- STEP 8b: Update release description with changelog ----------------------
+ - name: "Step 8b: Update release body"
+ if: steps.version.outputs.skip != 'true'
+ continue-on-error: true
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
+ php /tmp/moko-platform-api/cli/release_body_update.php \
+ --path . --version "${VERSION}" --tag "${RELEASE_TAG}" \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" \
+ --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
+ 2>&1 || true
+ echo "Release body updated" >> $GITHUB_STEP_SUMMARY
+
+ # -- 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"
+
+ # -- Clean up lesser pre-releases (cascade) ---------------------------------
+ # stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev
+ - name: "Delete lesser pre-release channels"
+ continue-on-error: true
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ php /tmp/moko-platform-api/cli/release_cascade.php \
+ --stability stable \
+ --version "${VERSION}" \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" \
+ --api-base "${API_BASE}" 2>/dev/null || true
+
+ - name: "Step 11: Delete and recreate dev branch from main"
+ if: steps.version.outputs.skip != 'true'
+ continue-on-error: true
+ run: |
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
+
+ # Delete dev branch
+ curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
+ "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
+
+ # Recreate dev from main (now includes version bump + changelog promotion)
+ curl -sf -X POST -H "Authorization: token ${TOKEN}" \
+ -H "Content-Type: application/json" \
+ "${API_BASE}/branches" \
+ -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
+
+ echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY
+
+ - name: "Step 12: Create version branch from main"
+ if: steps.version.outputs.skip != 'true'
+ continue-on-error: true
+ run: |
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ BRANCH_NAME="version/${VERSION}"
+ MAIN_SHA=$(git rev-parse HEAD)
+
+ # Delete old version branch if it exists (same version re-release)
+ curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
+
+ # Create version/XX.YY.ZZ from main
+ curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
+
+ echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
+
+
+
+ # -- Dolibarr post-release: Reset dev version -----------------------------
+ - name: "Post-release: Reset dev version"
+ if: steps.version.outputs.skip != 'true'
+ continue-on-error: true
+ run: |
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ php /tmp/moko-platform-api/cli/version_reset_dev.php \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
+ --branch dev --path . 2>&1 || true
+
+ # -- Summary --------------------------------------------------------------
+ - name: Pipeline Summary
+ if: always()
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ PLATFORM="${{ steps.platform.outputs.platform }}"
+ if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
+ echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
+ echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
+ elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
+ echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
+ else
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
+ echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
+ echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
+ fi
--
2.52.0
From 13327ac707354c2c6e2dc247495f9988ae361c3f Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Thu, 28 May 2026 20:05:42 +0000
Subject: [PATCH 17/40] chore: sync .mokogitea/workflows/update-server.yml from
moko-platform [skip ci]
---
.mokogitea/workflows/update-server.yml | 906 +++++++++----------------
1 file changed, 309 insertions(+), 597 deletions(-)
diff --git a/.mokogitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml
index fd6407f..0e0a8e5 100644
--- a/.mokogitea/workflows/update-server.yml
+++ b/.mokogitea/workflows/update-server.yml
@@ -1,597 +1,309 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: MokoStandards.Universal
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
-# PATH: /templates/workflows/update-server.yml
-# VERSION: 04.07.00
-# BRIEF: Update server XML feed with stable/rc/beta/alpha/dev entries (universal)
-#
-# Writes updates.xml with multiple entries:
-# - stable on push to main (from auto-release)
-# - rc on push to rc/**
-# - development on push to dev or dev/**
-#
-# Joomla filters by user's "Minimum Stability" setting.
-
-name: "Update 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@v4
- with:
- token: ${{ secrets.GA_TOKEN }}
- fetch-depth: 0
-
- - name: Setup moko-platform 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
- if [ -d "/tmp/moko-platform" ]; then
- echo "moko-platform already available — skipping clone"
- else
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
- /tmp/moko-platform 2>/dev/null || true
- fi
- if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
- cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
- fi
-
- - 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/moko-platform/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/moko-platform/cli/version_bump.php --path . 2>/dev/null || true)
- if [ -n "$BUMPED" ]; then
- VERSION=$(php /tmp/moko-platform/cli/version_read.php --path . 2>/dev/null || echo "$VERSION")
- git add -A
- git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
- --author="gitea-actions[bot] " 2>/dev/null || true
- git push 2>/dev/null || true
- fi
-
- # Determine stability from branch or input
- if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
- STABILITY="${{ inputs.stability }}"
- elif [[ "$BRANCH" == rc/* ]]; then
- STABILITY="rc"
- elif [[ "$BRANCH" == beta/* ]]; then
- STABILITY="beta"
- elif [[ "$BRANCH" == alpha/* ]]; then
- STABILITY="alpha"
- elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then
- STABILITY="development"
- else
- STABILITY="stable"
- fi
-
- echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
-
- # Parse manifest (portable — no grep -P)
- MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '/dev/null | head -1)
- if [ -z "$MANIFEST" ]; then
- echo "No Joomla manifest found — skipping"
- exit 0
- fi
-
- # Extract fields using sed (works on all runners)
- EXT_NAME=$(sed -n 's/.*\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
- EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
- EXT_CLIENT=$(sed -n 's/.*]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
- EXT_VERSION=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
- TARGET_PLATFORM=$(sed -n 's/.*\(\).*/\1/p' "$MANIFEST" | head -1)
- PHP_MINIMUM=$(sed -n 's/.*\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
-
- # Fallbacks
- [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
- [ -z "$EXT_TYPE" ] && EXT_TYPE="component"
-
- # Derive element if not in manifest: try XML filename, then repo name
- if [ -z "$EXT_ELEMENT" ]; then
- EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
- case "$EXT_ELEMENT" in
- templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
- esac
- fi
-
- # Use manifest version if README version is empty
- [ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION"
-
- [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '' "/")
-
- # Joomla requires on ALL extension types for update matching
- if [ -n "$EXT_CLIENT" ]; then
- CLIENT_TAG="${EXT_CLIENT}"
- else
- CLIENT_TAG="site"
- fi
-
- FOLDER_TAG=""
- [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="${EXT_FOLDER}"
-
- PHP_TAG=""
- [ -n "$PHP_MINIMUM" ] && PHP_TAG="${PHP_MINIMUM}"
-
- # Version suffix for non-stable
- DISPLAY_VERSION="$VERSION"
- case "$STABILITY" in
- development) DISPLAY_VERSION="${VERSION}-dev" ;;
- alpha) DISPLAY_VERSION="${VERSION}-alpha" ;;
- beta) DISPLAY_VERSION="${VERSION}-beta" ;;
- rc) DISPLAY_VERSION="${VERSION}-rc" ;;
- esac
-
- MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
-
- # Each stability level has its own release tag
- case "$STABILITY" in
- development) RELEASE_TAG="development" ;;
- alpha) RELEASE_TAG="alpha" ;;
- beta) RELEASE_TAG="beta" ;;
- rc) RELEASE_TAG="release-candidate" ;;
- *) RELEASE_TAG="v${MAJOR}" ;;
- esac
-
- PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip"
- DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}"
- INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}"
-
- # -- Build install packages and upload to release --------------------
- php /tmp/moko-platform/cli/release_package.php \
- --path . --version "${DISPLAY_VERSION}" --tag "${RELEASE_TAG}" \
- --token "${{ secrets.GA_TOKEN }}" --api-base "${API_BASE}" \
- --repo "${GITEA_REPO}" --output /tmp 2>&1 || true
-
- echo "Package built and uploaded for ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
-
- # -- Build the new entry (canonical format matching release.yml) --
- NEW_ENTRY=""
- NEW_ENTRY="${NEW_ENTRY} \n"
- NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME}\n"
- NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME} ${STABILITY} build.\n"
- NEW_ENTRY="${NEW_ENTRY} ${EXT_ELEMENT}\n"
- NEW_ENTRY="${NEW_ENTRY} ${EXT_TYPE}\n"
- [ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n"
- [ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n"
- NEW_ENTRY="${NEW_ENTRY} ${VERSION}\n"
- NEW_ENTRY="${NEW_ENTRY} $(date +%Y-%m-%d)\n"
- NEW_ENTRY="${NEW_ENTRY} https://git.mokoconsulting.tech/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${RELEASE_TAG}\n"
- NEW_ENTRY="${NEW_ENTRY} \n"
- NEW_ENTRY="${NEW_ENTRY} ${DOWNLOAD_URL}\n"
- NEW_ENTRY="${NEW_ENTRY} \n"
- [ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} ${SHA256}\n"
- NEW_ENTRY="${NEW_ENTRY} ${STABILITY}\n"
- NEW_ENTRY="${NEW_ENTRY} Moko Consulting\n"
- NEW_ENTRY="${NEW_ENTRY} https://mokoconsulting.tech\n"
- NEW_ENTRY="${NEW_ENTRY} \n"
- [ -n "$PHP_MINIMUM" ] && NEW_ENTRY="${NEW_ENTRY} ${PHP_MINIMUM}\n"
- NEW_ENTRY="${NEW_ENTRY} "
-
- # -- Write new entry to temp file --------------------------------
- printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml
-
- # -- Merge into updates.xml ----------------------------------------
- # Cascade: stable→all | rc→rc+lower | beta→beta+lower | alpha→alpha+dev | dev→dev
- CASCADE_MAP="stable:development,alpha,beta,rc,stable rc:development,alpha,beta,rc beta:development,alpha,beta alpha:development,alpha development:development"
- TARGETS=""
- for entry in $CASCADE_MAP; do
- key="${entry%%:*}"
- vals="${entry#*:}"
- if [ "$key" = "${STABILITY}" ]; then
- TARGETS="$vals"
- break
- fi
- done
- [ -z "$TARGETS" ] && TARGETS="${STABILITY}"
-
- echo "Cascade: ${STABILITY} → ${TARGETS}"
-
- # Create updates.xml if missing
- if [ ! -f "updates.xml" ]; then
- printf '%s\n' "" > updates.xml
- printf '%s\n' "" >> updates.xml
- printf '%s\n' "" >> updates.xml
- printf '%s\n' "" >> updates.xml
- fi
-
- # Update existing blocks or create missing ones
- export PY_TARGETS="$TARGETS" PY_VERSION="$VERSION" PY_DATE="$(date +%Y-%m-%d)"
- python3 << 'PYEOF'
- import re, os
-
- targets = os.environ["PY_TARGETS"].split(",")
- version = os.environ["PY_VERSION"]
- date = os.environ["PY_DATE"]
-
- with open("updates.xml") as f:
- content = f.read()
- with open("/tmp/new_entry.xml") as f:
- new_entry_template = f.read()
-
- for tag in targets:
- tag = tag.strip()
- # Build entry with this tag's name
- new_entry = re.sub(r"[^<]*", f"{tag}", new_entry_template)
-
- # Try to find existing block (handles both single-line and multi-line )
- block_pattern = r"((?:(?!).)*?" + re.escape(tag) + r".*?)"
- match = re.search(block_pattern, content, re.DOTALL)
-
- if match:
- # Update in place — replace entire block
- content = content.replace(match.group(1), new_entry.strip())
- print(f" UPDATED: {tag} → {version}")
- else:
- # Create — insert before
- content = content.replace("", "\n" + new_entry.strip() + "\n\n")
- print(f" CREATED: {tag} → {version}")
-
- # Clean up excessive blank lines
- content = re.sub(r"\n{3,}", "\n\n", content)
-
- with open("updates.xml", "w") as f:
- f.write(content)
- PYEOF
-
- # Commit
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git add updates.xml
- git diff --cached --quiet || {
- git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \
- --author="gitea-actions[bot] "
- git push
- }
-
- # -- Sync updates.xml to main (for non-main branches) ----------------------
- - name: Sync updates.xml to main
- if: github.ref_name != 'main'
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- GA_TOKEN="${{ secrets.GA_TOKEN }}"
-
- FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
- "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
-
- if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
- python3 -c "
- import base64, json, urllib.request, sys
- with open('updates.xml', 'rb') as f:
- content = base64.b64encode(f.read()).decode()
- payload = json.dumps({
- 'content': content,
- 'sha': '${FILE_SHA}',
- 'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]',
- 'branch': 'main'
- }).encode()
- req = urllib.request.Request(
- '${API_BASE}/contents/updates.xml',
- data=payload, method='PUT',
- headers={
- 'Authorization': 'token ${GA_TOKEN}',
- 'Content-Type': 'application/json'
- })
- try:
- urllib.request.urlopen(req)
- print('updates.xml synced to main')
- except Exception as e:
- print(f'ERROR: failed to sync updates.xml to main: {e}', file=sys.stderr)
- sys.exit(1)
- " \
- && echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \
- || echo "::error::failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY
- else
- echo "::error::could not get updates.xml SHA from main — file may not exist on main yet" >> $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/moko-platform/cli/platform_detect.php --path . 2>/dev/null || true)
- if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/moko-platform/deploy/deploy-joomla.php" ]; then
- php /tmp/moko-platform/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
- elif [ -f "/tmp/moko-platform/deploy/deploy-sftp.php" ]; then
- php /tmp/moko-platform/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: Validate updates.xml integrity
- run: |
- ERRORS=0
-
- if [ ! -f "updates.xml" ]; then
- echo "::error::updates.xml not found"
- exit 1
- fi
-
- # Well-formed XML
- if ! python3 -c "import xml.etree.ElementTree as ET; ET.parse('updates.xml')" 2>/dev/null; then
- echo "::error::updates.xml is not valid XML"
- ERRORS=$((ERRORS+1))
- fi
-
- python3 << 'PYEOF'
- import xml.etree.ElementTree as ET, sys, re, os
-
- tree = ET.parse("updates.xml")
- root = tree.getroot()
- updates = root.findall("update")
- errors = 0
- warnings = 0
- seen_tags = set()
-
- # All 5 channels MUST be present
- REQUIRED_CHANNELS = {"stable", "rc", "beta", "alpha", "dev"}
- VALID_TAGS = REQUIRED_CHANNELS | {"development"} # accept legacy alias
- REPO = os.environ.get("GITEA_REPO", "")
- ORG = os.environ.get("GITEA_ORG", "MokoConsulting")
- REPO_BASE = f"https://git.mokoconsulting.tech/{ORG}/"
-
- # Gitea release tag names per channel (Moko standard)
- RELEASE_TAG_MAP = {
- "stable": "stable",
- "rc": "release-candidate",
- "beta": "beta",
- "alpha": "alpha",
- "dev": "development",
- "development": "development",
- }
-
- # Joomla update XML required fields per
- # https://docs.joomla.org/Deploying_an_Update_Server
- REQUIRED_FIELDS = ["name", "element", "type", "version", "infourl"]
-
- for i, u in enumerate(updates):
- tag_el = u.find("tags/tag")
- tag = tag_el.text.strip() if tag_el is not None and tag_el.text else None
- label = f"Entry {i+1} ({tag or '?'})"
-
- # -- Required Joomla fields --
- for field in REQUIRED_FIELDS:
- el = u.find(field)
- if el is None or not (el.text or "").strip():
- print(f"::error::{label}: missing required <{field}>")
- errors += 1
-
- # -- --
- dl = u.find("downloads/downloadurl")
- if dl is None or not (dl.text or "").strip():
- print(f"::error::{label}: missing ")
- errors += 1
- else:
- dl_url = dl.text.strip()
- # Must point to org repo
- if REPO_BASE not in dl_url:
- print(f"::error::{label}: download URL not under {REPO_BASE}: {dl_url}")
- errors += 1
- # Must end in .zip
- if not dl_url.endswith(".zip"):
- print(f"::error::{label}: download URL must end in .zip: {dl_url}")
- errors += 1
- # Must use correct Gitea release tag in path
- if tag and tag in RELEASE_TAG_MAP:
- expected_tag = RELEASE_TAG_MAP[tag]
- if f"/download/{expected_tag}/" not in dl_url:
- print(f"::error::{label}: download URL should contain /download/{expected_tag}/ but got: {dl_url}")
- errors += 1
-
- # -- (required for Joomla to match update) --
- client = u.find("client")
- if client is None or not (client.text or "").strip():
- print(f"::error::{label}: missing (required for Joomla update matching)")
- errors += 1
-
- # -- --
- tp = u.find("targetplatform")
- if tp is None:
- print(f"::error::{label}: missing ")
- errors += 1
- else:
- tp_name = tp.get("name", "")
- tp_ver = tp.get("version", "")
- if tp_name != "joomla":
- print(f"::error::{label}: targetplatform name should be 'joomla', got '{tp_name}'")
- errors += 1
- if not tp_ver:
- print(f"::error::{label}: targetplatform missing version regex")
- errors += 1
- elif "5" not in tp_ver or "6" not in tp_ver:
- print(f"::warning::{label}: targetplatform version may not cover Joomla 5+6: {tp_ver}")
- warnings += 1
-
- # -- must be valid Joomla type --
- type_el = u.find("type")
- if type_el is not None and type_el.text:
- valid_types = {"component", "module", "plugin", "template", "library", "package", "file"}
- if type_el.text.strip() not in valid_types:
- print(f"::error::{label}: invalid type '{type_el.text}' (expected: {valid_types})")
- errors += 1
-
- # -- format (XX.YY.ZZ with optional suffix) --
- ver_el = u.find("version")
- if ver_el is not None and ver_el.text:
- if not re.match(r"^\d{2}\.\d{2}\.\d{2}(-\w+)?$", ver_el.text.strip()):
- print(f"::warning::{label}: version '{ver_el.text}' does not match XX.YY.ZZ format")
- warnings += 1
-
- # -- and --
- for field in ["maintainer", "maintainerurl"]:
- el = u.find(field)
- if el is None or not (el.text or "").strip():
- print(f"::warning::{label}: missing <{field}>")
- warnings += 1
-
- # -- Valid stability tag --
- if tag is None:
- print(f"::error::{label}: missing ")
- errors += 1
- elif tag not in VALID_TAGS:
- print(f"::error::{label}: invalid tag '{tag}' (expected: {VALID_TAGS})")
- errors += 1
-
- # -- Duplicate tag check --
- norm_tag = "dev" if tag == "development" else tag
- if norm_tag in seen_tags:
- print(f"::error::{label}: duplicate channel '{tag}'")
- errors += 1
- if norm_tag:
- seen_tags.add(norm_tag)
-
- # -- All 5 channels must exist --
- missing = REQUIRED_CHANNELS - seen_tags
- if missing:
- print(f"::error::Missing required update channels: {', '.join(sorted(missing))}")
- errors += 1
-
- # -- Version ordering: higher stability must not exceed dev version --
- channel_versions = {}
- for u in updates:
- tag_el = u.find("tags/tag")
- ver_el = u.find("version")
- if tag_el is not None and ver_el is not None and tag_el.text and ver_el.text:
- norm = "dev" if tag_el.text.strip() == "development" else tag_el.text.strip()
- # Strip suffix for comparison (01.00.18-dev -> 01.00.18)
- base_ver = re.sub(r"-\w+$", "", ver_el.text.strip())
- channel_versions[norm] = base_ver
-
- # Cascade check: dev >= alpha >= beta >= rc >= stable
- ORDER = ["dev", "alpha", "beta", "rc", "stable"]
- for j in range(1, len(ORDER)):
- current = ORDER[j]
- previous = ORDER[j - 1]
- if current in channel_versions and previous in channel_versions:
- if channel_versions[current] > channel_versions[previous]:
- print(f"::error::{current} version ({channel_versions[current]}) is ahead of {previous} ({channel_versions[previous]})")
- errors += 1
-
- # -- Summary --
- print(f"\nupdates.xml validation: {len(updates)} entries, {errors} error(s), {warnings} warning(s)")
- if errors > 0:
- sys.exit(1)
- PYEOF
-
- - 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
+# Copyright (C) 2026 Moko Consulting
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# FILE INFORMATION
+# DEFGROUP: Gitea.Workflow
+# INGROUP: moko-platform.Universal
+# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
+# PATH: /templates/workflows/update-server.yml
+# VERSION: 05.00.00
+# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
+#
+# Thin wrapper around moko-platform CLI tools.
+# Builds packages, updates updates.xml, and optionally deploys via SFTP.
+#
+# Joomla filters update entries by the user's "Minimum Stability" setting.
+
+name: "Update Server"
+
+on:
+ push:
+ branches:
+ - 'dev'
+ - 'dev/**'
+ - 'alpha/**'
+ - 'beta/**'
+ - 'rc/**'
+ paths:
+ - 'src/**'
+ - 'htdocs/**'
+ pull_request:
+ types: [closed]
+ branches:
+ - 'dev'
+ - 'dev/**'
+ - 'alpha/**'
+ - 'beta/**'
+ - 'rc/**'
+ paths:
+ - 'src/**'
+ - 'htdocs/**'
+ workflow_dispatch:
+ inputs:
+ stability:
+ description: 'Stability tag'
+ required: true
+ default: 'development'
+ type: choice
+ options:
+ - development
+ - alpha
+ - beta
+ - rc
+ - stable
+
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+ GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
+ GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
+ GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
+
+permissions:
+ contents: write
+
+jobs:
+ update-xml:
+ name: Update Server
+ runs-on: release
+ if: >-
+ github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ token: ${{ secrets.MOKOGITEA_TOKEN }}
+ fetch-depth: 0
+
+ - name: Setup moko-platform tools
+ env:
+ MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
+ MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
+ COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
+ run: |
+ if ! command -v composer &> /dev/null; then
+ sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
+ fi
+ # Always fetch latest CLI tools — never use stale cache from previous runs
+ rm -rf /tmp/moko-platform
+ git clone --depth 1 --branch main --quiet \
+ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
+ /tmp/moko-platform 2>/dev/null || true
+ if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
+ cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
+ fi
+ echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
+
+ - name: Detect platform
+ id: platform
+ run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
+
+ - name: Resolve stability and bump version
+ id: meta
+ run: |
+ BRANCH="${{ github.ref_name }}"
+
+ # Configure git for bot pushes
+ git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
+ git config --local user.name "gitea-actions[bot]"
+ git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
+
+ # Auto-bump patch version
+ php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
+
+ VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0")
+
+ # Determine stability from branch or manual input
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
+ STABILITY="${{ inputs.stability }}"
+ elif [[ "$BRANCH" == rc/* ]]; then
+ STABILITY="rc"
+ elif [[ "$BRANCH" == beta/* ]]; then
+ STABILITY="beta"
+ elif [[ "$BRANCH" == alpha/* ]]; then
+ STABILITY="alpha"
+ else
+ STABILITY="development"
+ fi
+
+ # Version suffix per stability stream
+ case "$STABILITY" in
+ development) SUFFIX="-dev"; TAG="development" ;;
+ alpha) SUFFIX="-alpha"; TAG="alpha" ;;
+ beta) SUFFIX="-beta"; TAG="beta" ;;
+ rc) SUFFIX="-rc"; TAG="release-candidate" ;;
+ *) SUFFIX=""; TAG="stable" ;;
+ esac
+
+ # Propagate version with stability suffix to all manifest files
+ php ${MOKO_CLI}/version_set_platform.php \
+ --path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
+ php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
+
+ # Re-read version (now includes suffix from version_set_platform)
+ if [ -n "$SUFFIX" ]; then
+ VERSION="${VERSION}${SUFFIX}"
+ fi
+
+ echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
+ echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
+ echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
+ echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
+ echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT"
+
+ # Commit version bump if changed
+ git add -A
+ git diff --cached --quiet || {
+ git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
+ --author="gitea-actions[bot] "
+ git push
+ }
+
+ - name: Create release and upload package
+ id: package
+ run: |
+ VERSION="${{ steps.meta.outputs.version }}"
+ TAG="${{ steps.meta.outputs.tag }}"
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+
+ # Create or update Gitea release
+ php ${MOKO_CLI}/release_create.php \
+ --path . --version "$VERSION" --tag "$TAG" \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
+ --repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
+
+ # Build package and upload
+ php ${MOKO_CLI}/release_package.php \
+ --path . --version "$VERSION" --tag "$TAG" \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
+ --repo "${GITEA_REPO}" --output /tmp || true
+
+ - name: Update updates.xml
+ if: steps.platform.outputs.platform == 'joomla'
+ run: |
+ VERSION="${{ steps.meta.outputs.version }}"
+ STABILITY="${{ steps.meta.outputs.stability }}"
+ SHA256="${{ steps.package.outputs.sha256_zip }}"
+
+ if [ ! -f "updates.xml" ]; then
+ echo "No updates.xml — skipping"
+ exit 0
+ fi
+
+ SHA_FLAG=""
+ [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
+
+ php ${MOKO_CLI}/updates_xml_build.php \
+ --path . --version "${VERSION}" --stability "${STABILITY}" \
+ --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
+ ${SHA_FLAG}
+
+ # Commit and push updates.xml
+ git add updates.xml
+ git diff --cached --quiet || {
+ git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
+ git push
+ }
+
+ - name: Sync updates.xml to main
+ if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
+ run: |
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
+
+ FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
+ "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
+
+ if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
+ python3 -c "
+ import base64, json, urllib.request, sys
+ with open('updates.xml', 'rb') as f:
+ content = base64.b64encode(f.read()).decode()
+ payload = json.dumps({
+ 'content': content,
+ 'sha': '${FILE_SHA}',
+ 'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
+ 'branch': 'main'
+ }).encode()
+ req = urllib.request.Request(
+ '${API_BASE}/contents/updates.xml',
+ data=payload, method='PUT',
+ headers={
+ 'Authorization': 'token ${GITEA_TOKEN}',
+ 'Content-Type': 'application/json'
+ })
+ try:
+ urllib.request.urlopen(req)
+ print('updates.xml synced to main')
+ except Exception as e:
+ print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
+ "
+ fi
+
+ - name: SFTP deploy to dev server
+ if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
+ env:
+ DEV_HOST: ${{ vars.DEV_FTP_HOST }}
+ DEV_PATH: ${{ vars.DEV_FTP_PATH }}
+ DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
+ DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
+ DEV_PORT: ${{ vars.DEV_FTP_PORT }}
+ DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
+ DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
+ run: |
+ # Permission check: admin or maintain role required
+ ACTOR="${{ github.actor }}"
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+
+ PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
+ "${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
+ python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
+ case "$PERMISSION" in
+ admin|maintain|write) ;;
+ *)
+ echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
+ exit 0
+ ;;
+ esac
+
+ [ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
+
+ SOURCE_DIR="src"
+ [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
+ [ ! -d "$SOURCE_DIR" ] && exit 0
+
+ PORT="${DEV_PORT:-22}"
+ REMOTE="${DEV_PATH%/}"
+ [ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
+
+ printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
+ "$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
+ if [ -n "$DEV_KEY" ]; then
+ echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
+ printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
+ else
+ printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
+ fi
+
+ PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
+ if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
+ php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
+ elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
+ php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
+ fi
+ rm -f /tmp/deploy_key /tmp/sftp-config.json
+ echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
+
+ - name: Summary
+ if: always()
+ run: |
+ VERSION="${{ steps.meta.outputs.version }}"
+ STABILITY="${{ steps.meta.outputs.stability }}"
+ DISPLAY="${{ steps.meta.outputs.display_version }}"
+ echo "## Update Server" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
+ echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
+ echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
--
2.52.0
From b34b10db54c73050d704bf89312d8d46b0ffd59f Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Thu, 28 May 2026 20:09:01 +0000
Subject: [PATCH 18/40] chore: sync .mokogitea/workflows/pre-release.yml from
moko-platform [skip ci]
---
.mokogitea/workflows/pre-release.yml | 456 ++++++++++++++-------------
1 file changed, 233 insertions(+), 223 deletions(-)
diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml
index f0b1d88..162b08f 100644
--- a/.mokogitea/workflows/pre-release.yml
+++ b/.mokogitea/workflows/pre-release.yml
@@ -1,223 +1,233 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Release
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
-# PATH: /templates/workflows/universal/pre-release.yml.template
-# VERSION: 05.01.00
-# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
-
-name: "Universal: Pre-Release"
-
-on:
- pull_request:
- types: [closed]
- branches:
- - dev
- workflow_dispatch:
- inputs:
- stability:
- description: 'Pre-release channel'
- required: true
- type: choice
- options:
- - development
- - alpha
- - beta
- - release-candidate
-
-permissions:
- contents: write
-
-env:
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
- GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
-
-jobs:
- build:
- name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
- runs-on: release
- if: >-
- github.event_name == 'workflow_dispatch' ||
- (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
- token: ${{ secrets.GA_TOKEN }}
-
- - name: Setup moko-platform tools
- env:
- MOKO_CLONE_TOKEN: ${{ secrets.GA_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
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
- /tmp/moko-platform-api
- cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
- echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
-
- - name: Detect platform
- id: platform
- run: |
- php ${MOKO_CLI}/manifest_read.php --path . --github-output
-
- - name: Resolve metadata and bump version
- id: meta
- run: |
- STABILITY="${{ inputs.stability || 'development' }}"
-
- case "$STABILITY" in
- development) SUFFIX="-dev"; TAG="development" ;;
- alpha) SUFFIX="-alpha"; TAG="alpha" ;;
- beta) SUFFIX="-beta"; TAG="beta" ;;
- release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
- esac
-
- # Read current version (bump already handled by push workflow)
- VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
- [ -z "$VERSION" ] && VERSION="00.00.01"
-
- php ${MOKO_CLI}/version_set_platform.php \
- --path . --version "$VERSION" --branch "${{ github.ref_name }}" 2>/dev/null || true
-
- # Verify version consistency across all files
- php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
-
- # 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): pre-release bump to ${VERSION} [skip ci]"
- git push origin HEAD 2>&1
- }
-
- # Auto-detect element via manifest_element.php
- php ${MOKO_CLI}/manifest_element.php \
- --path . --version "$VERSION" --stability "$STABILITY" \
- --repo "${GITEA_REPO}" --github-output
-
- # Read back element outputs
- EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
- ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
- [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
- [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}${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: Create release
- id: release
- run: |
- TAG="${{ steps.meta.outputs.tag }}"
- VERSION="${{ steps.meta.outputs.version }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php ${MOKO_CLI}/release_create.php \
- --path . --version "$VERSION" --tag "$TAG" \
- --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \
- --repo "${GITEA_REPO}" --branch dev --prerelease
-
- - name: Build package and upload
- id: package
- run: |
- VERSION="${{ steps.meta.outputs.version }}"
- TAG="${{ steps.meta.outputs.tag }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php ${MOKO_CLI}/release_package.php \
- --path . --version "$VERSION" --tag "$TAG" \
- --token "${{ secrets.GA_TOKEN }}" --api-base "$API_BASE" \
- --repo "${GITEA_REPO}" --output /tmp || true
-
- - name: Update updates.xml
- if: steps.platform.outputs.platform == 'joomla'
- run: |
- VERSION="${{ steps.meta.outputs.version }}"
- STABILITY="${{ steps.meta.outputs.stability }}"
- SHA256="${{ steps.package.outputs.sha256_zip }}"
-
- if [ ! -f "updates.xml" ]; then
- echo "No updates.xml -- skipping"
- exit 0
- fi
-
- SHA_FLAG=""
- [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
-
- php ${MOKO_CLI}/updates_xml_build.php \
- --path . --version "${VERSION}" --stability "${STABILITY}" \
- --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
- ${SHA_FLAG}
-
- # Commit and push
- if ! git diff --quiet updates.xml 2>/dev/null; then
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git add updates.xml
- git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
- git push origin HEAD 2>&1 || echo "WARNING: push failed"
- fi
-
- - name: "Sync updates.xml to all branches"
- if: steps.platform.outputs.platform == 'joomla'
- run: |
- CURRENT_BRANCH="${{ github.ref_name }}"
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
-
- for BRANCH in main dev; do
- [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
- echo "Syncing updates.xml -> ${BRANCH}"
- git fetch origin "${BRANCH}" 2>/dev/null || continue
- git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue
- git checkout "${CURRENT_BRANCH}" -- updates.xml
- if ! git diff --quiet updates.xml 2>/dev/null; then
- git add updates.xml
- git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
- git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
- fi
- git checkout "${CURRENT_BRANCH}" 2>/dev/null
- done
-
- - name: "Delete lesser pre-release channels (cascade)"
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- TOKEN="${{ secrets.GA_TOKEN }}"
-
- php ${MOKO_CLI}/release_cascade.php \
- --stability "${{ steps.meta.outputs.stability }}" \
- --token "${TOKEN}" \
- --api-base "${API_BASE}"
-
- - name: Summary
- if: always()
- run: |
- VERSION="${{ steps.meta.outputs.version }}"
- STABILITY="${{ steps.meta.outputs.stability }}"
- ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
- SHA256="${{ steps.package.outputs.sha256_zip }}"
- echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
- echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
- echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
- echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
+# Copyright (C) 2026 Moko Consulting
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# FILE INFORMATION
+# DEFGROUP: Gitea.Workflow
+# INGROUP: moko-platform.Release
+# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
+# PATH: /templates/workflows/universal/pre-release.yml.template
+# VERSION: 05.01.00
+# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
+
+name: "Universal: Pre-Release"
+
+on:
+ pull_request:
+ types: [closed]
+ branches:
+ - dev
+ workflow_dispatch:
+ inputs:
+ stability:
+ description: 'Pre-release channel'
+ required: true
+ type: choice
+ options:
+ - development
+ - alpha
+ - beta
+ - release-candidate
+
+permissions:
+ contents: write
+
+env:
+ GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
+ GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
+ GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
+
+jobs:
+ build:
+ name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
+ runs-on: release
+ if: >-
+ github.event_name == 'workflow_dispatch' ||
+ (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ token: ${{ secrets.MOKOGITEA_TOKEN }}
+
+ - name: Setup moko-platform tools
+ env:
+ MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
+ MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
+ run: |
+ if ! command -v composer &> /dev/null; then
+ sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
+ fi
+ # Always fetch latest CLI tools — never use stale cache from previous runs
+ rm -rf /tmp/moko-platform-api
+ git clone --depth 1 --branch main --quiet \
+ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
+ /tmp/moko-platform-api
+ cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
+ echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
+
+ - name: Detect platform
+ id: platform
+ run: |
+ php ${MOKO_CLI}/manifest_read.php --path . --github-output
+
+ - name: Resolve metadata and bump version
+ id: meta
+ run: |
+ STABILITY="${{ inputs.stability || 'development' }}"
+
+ case "$STABILITY" in
+ development) SUFFIX="-dev"; TAG="development" ;;
+ alpha) SUFFIX="-alpha"; TAG="alpha" ;;
+ beta) SUFFIX="-beta"; TAG="beta" ;;
+ release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
+ esac
+
+ # Read current version (bump already handled by push workflow)
+ VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
+ [ -z "$VERSION" ] && VERSION="00.00.01"
+
+ # Strip any existing suffix from version before applying stability
+ VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
+
+ php ${MOKO_CLI}/version_set_platform.php \
+ --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
+
+ # Verify version consistency across all files
+ php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
+
+ # Update VERSION variable with suffix
+ if [ -n "$SUFFIX" ]; then
+ VERSION="${VERSION}${SUFFIX}"
+ fi
+
+ # Commit version bump
+ git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
+ git config --local user.name "gitea-actions[bot]"
+ git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
+ git add -A
+ git diff --cached --quiet || {
+ git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
+ git push origin HEAD 2>&1
+ }
+
+ # Auto-detect element via manifest_element.php
+ php ${MOKO_CLI}/manifest_element.php \
+ --path . --version "$VERSION" --stability "$STABILITY" \
+ --repo "${GITEA_REPO}" --github-output
+
+ # Read back element outputs
+ EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
+ ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
+ [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
+ [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
+
+ echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
+ echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
+ echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
+ echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
+ echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
+ echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
+
+ echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
+
+ - name: Create release
+ id: release
+ run: |
+ TAG="${{ steps.meta.outputs.tag }}"
+ VERSION="${{ steps.meta.outputs.version }}"
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ php ${MOKO_CLI}/release_create.php \
+ --path . --version "$VERSION" --tag "$TAG" \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
+ --repo "${GITEA_REPO}" --branch dev --prerelease
+
+ - name: Build package and upload
+ id: package
+ run: |
+ VERSION="${{ steps.meta.outputs.version }}"
+ TAG="${{ steps.meta.outputs.tag }}"
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ php ${MOKO_CLI}/release_package.php \
+ --path . --version "$VERSION" --tag "$TAG" \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
+ --repo "${GITEA_REPO}" --output /tmp || true
+
+ - name: Update updates.xml
+ if: steps.platform.outputs.platform == 'joomla'
+ run: |
+ VERSION="${{ steps.meta.outputs.version }}"
+ STABILITY="${{ steps.meta.outputs.stability }}"
+ SHA256="${{ steps.package.outputs.sha256_zip }}"
+
+ if [ ! -f "updates.xml" ]; then
+ echo "No updates.xml -- skipping"
+ exit 0
+ fi
+
+ SHA_FLAG=""
+ [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
+
+ php ${MOKO_CLI}/updates_xml_build.php \
+ --path . --version "${VERSION}" --stability "${STABILITY}" \
+ --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
+ ${SHA_FLAG}
+
+ # Commit and push
+ if ! git diff --quiet updates.xml 2>/dev/null; then
+ git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
+ git config --local user.name "gitea-actions[bot]"
+ git add updates.xml
+ git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
+ git push origin HEAD 2>&1 || echo "WARNING: push failed"
+ fi
+
+ - name: "Sync updates.xml to all branches"
+ if: steps.platform.outputs.platform == 'joomla'
+ run: |
+ CURRENT_BRANCH="${{ github.ref_name }}"
+ git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
+ git config --local user.name "gitea-actions[bot]"
+
+ for BRANCH in main dev; do
+ [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
+ echo "Syncing updates.xml -> ${BRANCH}"
+ git fetch origin "${BRANCH}" 2>/dev/null || continue
+ git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue
+ git checkout "${CURRENT_BRANCH}" -- updates.xml
+ if ! git diff --quiet updates.xml 2>/dev/null; then
+ git add updates.xml
+ git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
+ git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
+ fi
+ git checkout "${CURRENT_BRANCH}" 2>/dev/null
+ done
+
+ - name: "Delete lesser pre-release channels (cascade)"
+ continue-on-error: true
+ run: |
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
+
+ php ${MOKO_CLI}/release_cascade.php \
+ --stability "${{ steps.meta.outputs.stability }}" \
+ --token "${TOKEN}" \
+ --api-base "${API_BASE}"
+
+ - name: Summary
+ if: always()
+ run: |
+ VERSION="${{ steps.meta.outputs.version }}"
+ STABILITY="${{ steps.meta.outputs.stability }}"
+ ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
+ SHA256="${{ steps.package.outputs.sha256_zip }}"
+ echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
+ echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
+ echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
+ echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
--
2.52.0
From dd019143fe6a331c00a73a1e2a9cdc9e64c7e9de Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Thu, 28 May 2026 20:27:59 +0000
Subject: [PATCH 19/40] chore: sync .mokogitea/workflows/auto-release.yml from
moko-platform [skip ci]
---
.mokogitea/workflows/auto-release.yml | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml
index a05d0f4..757bfb4 100644
--- a/.mokogitea/workflows/auto-release.yml
+++ b/.mokogitea/workflows/auto-release.yml
@@ -298,7 +298,8 @@ jobs:
git add -A
git commit -m "chore(release): build ${VERSION} [skip ci]" \
--author="gitea-actions[bot] "
- git push -u origin HEAD
+ # Detached HEAD on PR merge — push explicitly to main
+ git push origin HEAD:refs/heads/main
# -- STEP 6: Create tag ---------------------------------------------------
- name: "Step 6: Create git tag"
@@ -389,7 +390,7 @@ jobs:
git add updates.xml
git commit -m "chore: update stable channel ${VERSION} [skip ci]" \
--author="gitea-actions[bot] "
- git push origin HEAD 2>&1 || true
+ git push origin HEAD:refs/heads/main 2>&1 || true
fi
# -- STEP 8b: Update release description with changelog ----------------------
--
2.52.0
From cf4259ea5831e6a61f56d37f1f48d4eeb304875e Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Thu, 28 May 2026 20:46:04 +0000
Subject: [PATCH 20/40] chore: sync .mokogitea/workflows/auto-release.yml from
moko-platform [skip ci]
---
.mokogitea/workflows/auto-release.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml
index 757bfb4..72ce95a 100644
--- a/.mokogitea/workflows/auto-release.yml
+++ b/.mokogitea/workflows/auto-release.yml
@@ -201,6 +201,8 @@ jobs:
MOKO_API="/tmp/moko-platform-api/cli"
php ${MOKO_API}/version_bump.php --path . --minor 2>&1 || true
VERSION=$(php ${MOKO_API}/version_read.php --path .)
+ # Strip any pre-release suffix — stable releases have no suffix
+ VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Bumped to: ${VERSION}"
--
2.52.0
From 45f5806189194ca83fe13f22f59fb97fc7ba605a Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Thu, 28 May 2026 20:50:50 +0000
Subject: [PATCH 21/40] chore: sync .mokogitea/workflows/update-server.yml from
moko-platform [skip ci]
---
.mokogitea/workflows/update-server.yml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.mokogitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml
index 0e0a8e5..339d3f5 100644
--- a/.mokogitea/workflows/update-server.yml
+++ b/.mokogitea/workflows/update-server.yml
@@ -114,6 +114,9 @@ jobs:
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0")
+ # Strip any existing suffix before applying stability
+ VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
+
# Determine stability from branch or manual input
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
STABILITY="${{ inputs.stability }}"
--
2.52.0
From c8898af2f6cf6e5d9fee29ce433da1318947f4c4 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Fri, 29 May 2026 10:32:02 +0000
Subject: [PATCH 22/40] chore: sync .mokogitea/workflows/auto-bump.yml from
moko-platform [skip ci]
---
.mokogitea/workflows/auto-bump.yml | 37 ++++++++----------------------
1 file changed, 10 insertions(+), 27 deletions(-)
diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml
index 10a7e51..a397a9e 100644
--- a/.mokogitea/workflows/auto-bump.yml
+++ b/.mokogitea/workflows/auto-bump.yml
@@ -16,6 +16,10 @@ on:
push:
branches:
- dev
+ - alpha
+ - beta
+ - rc
+ - 'feature/**'
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
@@ -37,7 +41,7 @@ jobs:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
- token: ${{ secrets.GA_TOKEN }}
+ token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup moko-platform tools
@@ -49,7 +53,7 @@ jobs:
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
else
git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
+ "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
@@ -57,28 +61,7 @@ jobs:
- name: Bump version
run: |
- BUMP=$(php ${MOKO_CLI}/version_bump.php --path . 2>&1) || true
- echo "$BUMP"
-
- VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) || true
- [ -z "$VERSION" ] && { echo "No version found — skipping"; exit 0; }
-
- # Propagate to platform manifests
- php ${MOKO_CLI}/version_set_platform.php \
- --path . --version "$VERSION" --branch dev 2>/dev/null || true
- php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
-
- # Commit if anything changed
- if git diff --quiet && git diff --cached --quiet; then
- echo "No version changes to commit"
- exit 0
- fi
-
- 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 commit -m "chore(version): patch bump to ${VERSION} [skip ci]" \
- --author="gitea-actions[bot] "
- git push origin dev
- echo "Bumped to ${VERSION}" >> $GITHUB_STEP_SUMMARY
+ php ${MOKO_CLI}/version_auto_bump.php \
+ --path . --branch "${GITHUB_REF_NAME}" \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" \
+ --repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
--
2.52.0
From 94fc426d198bd8da1d63eb1e3382b0eeabc7869e Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Sat, 30 May 2026 01:16:34 +0000
Subject: [PATCH 23/40] chore: sync .mokogitea/workflows/auto-release.yml from
moko-platform [skip ci]
---
.mokogitea/workflows/auto-release.yml | 1110 +++++++++++++------------
1 file changed, 579 insertions(+), 531 deletions(-)
diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml
index 72ce95a..04ec817 100644
--- a/.mokogitea/workflows/auto-release.yml
+++ b/.mokogitea/workflows/auto-release.yml
@@ -1,531 +1,579 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Release
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
-# PATH: /templates/workflows/universal/auto-release.yml.template
-# VERSION: 05.00.00
-# BRIEF: Universal build & release � detects platform from manifest.xml
-#
-# +========================================================================+
-# | UNIVERSAL BUILD & RELEASE PIPELINE |
-# +========================================================================+
-# | |
-# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
-# | |
-# | Platform-specific: |
-# | joomla: XML manifest, updates.xml, type-prefixed packages |
-# | dolibarr: mod*.class.php, update.txt, dev version reset |
-# | generic: README-only, no update stream |
-# | |
-# +========================================================================+
-
-name: "Universal: Build & Release"
-
-on:
- pull_request:
- types: [opened, closed]
- branches:
- - main
- workflow_dispatch:
- inputs:
- action:
- description: 'Action to perform'
- required: false
- type: choice
- default: release
- options:
- - release
- - promote-rc
-
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
- GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
-
-permissions:
- contents: write
-
-jobs:
- # ── Draft PR → Promote highest pre-release to RC ─────────────────────────────
- promote-rc:
- name: Promote Pre-Release to RC
- runs-on: release
- if: >-
- (github.event.action == 'opened' && github.event.pull_request.draft == 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: Promote to release-candidate
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/release_promote.php \
- --from auto --to release-candidate \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" \
- --api-base "${API_BASE}" \
- --branch "${{ github.event.pull_request.head.ref || 'dev' }}"
-
- - name: Cascade lesser channels
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/release_cascade.php \
- --stability release-candidate \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" \
- --api-base "${API_BASE}"
-
- - name: Summary
- if: always()
- run: |
- echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
- echo "Draft PR opened — promoted highest pre-release to RC" >> $GITHUB_STEP_SUMMARY
-
- # ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
- release:
- name: Build & Release Pipeline
- runs-on: release
- if: >-
- github.event.pull_request.merged == true ||
- (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- token: ${{ secrets.MOKOGITEA_TOKEN }}
- fetch-depth: 0
-
- - name: Configure git for bot pushes
- run: |
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
-
- - name: Setup moko-platform tools
- env:
- MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
- MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
- COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
- run: |
- # Ensure PHP + Composer are available
- if ! command -v composer &> /dev/null; then
- sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
- fi
- # Always fetch latest CLI tools — never use stale cache from previous runs
- rm -rf /tmp/moko-platform-api
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
- /tmp/moko-platform-api
- cd /tmp/moko-platform-api
- composer install --no-dev --no-interaction --quiet
-
-
- # -- PLATFORM DETECTION ---------------------------------------------------
- - name: Detect platform
- id: platform
- run: |
- php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output
- MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1 || true)
- MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1 || true)
- echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
- echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT"
-
- - name: "Step 1: Read version"
- id: version
- run: |
- VERSION=$(php /tmp/moko-platform-api/cli/version_read.php --path .)
- if [ -z "$VERSION" ]; then
- echo "::error::No VERSION in README.md"
- echo "skip=true" >> "$GITHUB_OUTPUT"
- exit 0
- fi
- # Strip any pre-release suffix merged from dev (e.g. 01.02.20-dev → 01.02.20)
- VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
- MAJOR=$(echo "$VERSION" | cut -d. -f1)
- echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- echo "release_tag=stable" >> "$GITHUB_OUTPUT"
- echo "skip=false" >> "$GITHUB_OUTPUT"
- echo "branch=main" >> "$GITHUB_OUTPUT"
-
- # -- CHECK FOR RC PROMOTION ------------------------------------------------
- - name: "Check for RC release"
- id: rc
- if: steps.version.outputs.skip != 'true'
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- RC_JSON=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
- "${API_BASE}/releases/tags/release-candidate" 2>/dev/null || echo "{}")
- RC_ID=$(echo "$RC_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true)
-
- if [ -n "$RC_ID" ] && [ "$RC_ID" != "None" ] && [ "$RC_ID" != "" ]; then
- echo "promote=true" >> "$GITHUB_OUTPUT"
- echo "release_id=${RC_ID}" >> "$GITHUB_OUTPUT"
- echo "::notice::RC release found (id: ${RC_ID}) — will promote to stable"
- else
- echo "promote=false" >> "$GITHUB_OUTPUT"
- echo "::notice::No RC release — full build pipeline"
- fi
-
- - name: "Step 1b: Minor bump version"
- id: bump
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.rc.outputs.promote != 'true'
- run: |
- MOKO_API="/tmp/moko-platform-api/cli"
- php ${MOKO_API}/version_bump.php --path . --minor 2>&1 || true
- VERSION=$(php ${MOKO_API}/version_read.php --path .)
- # Strip any pre-release suffix — stable releases have no suffix
- VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
- echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- echo "Bumped to: ${VERSION}"
-
- - name: Check if already released
- if: steps.version.outputs.skip != 'true'
- id: check
- run: |
- TAG="${{ steps.version.outputs.release_tag }}"
- BRANCH="${{ steps.version.outputs.branch }}"
-
- TAG_EXISTS=false
- BRANCH_EXISTS=false
-
- git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true
- git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true
-
- echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT"
- echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT"
-
- # Tag and branch may persist across patch releases — never skip
- echo "already_released=false" >> "$GITHUB_OUTPUT"
-
- # -- SANITY CHECKS -------------------------------------------------------
- - name: "Sanity: Pre-release validation"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.check.outputs.already_released != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- php /tmp/moko-platform-api/cli/release_validate.php \
- --path . --version "$VERSION" --output-summary --github-output || true
-
- # -- STEP 2: Create or update version/XX.YY archive branch ---------------
- # Always runs — every version change on main archives to version/XX.YY
- - name: "Step 2: Version archive branch"
- if: steps.check.outputs.already_released != 'true'
- run: |
- BRANCH="${{ steps.version.outputs.branch }}"
- IS_MINOR="${{ steps.version.outputs.is_minor }}"
- PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}')
-
- # Check if branch exists
- if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
- git push origin HEAD:"$BRANCH" --force
- echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY
- else
- git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH"
- git push origin "$BRANCH" --force
- echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
- fi
-
- # -- STEP 3: Set platform version ----------------------------------------
- - name: "Step 3: Set platform version"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.check.outputs.already_released != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- php /tmp/moko-platform-api/cli/version_set_platform.php \
- --path . --version "$VERSION" --branch main
-
- # -- STEP 4: Update version badges ----------------------------------------
- - name: "Step 4: Update version badges"
- if: steps.version.outputs.skip != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true
- php /tmp/moko-platform-api/cli/version_check.php --path . --fix 2>/dev/null || true
-
- # Step 5 (updates.xml) moved after Step 8 to include SHA-256 checksum
-
- - name: "Step 4b: Promote and prune CHANGELOG"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.check.outputs.already_released != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- MOKO_API="/tmp/moko-platform-api/cli"
- if [ -f "CHANGELOG.md" ]; then
- php ${MOKO_API}/changelog_promote.php --path . --version "$VERSION" 2>&1 || true
- php ${MOKO_API}/changelog_prune.php --path . --keep 5 2>&1 || true
- fi
-
- - name: Commit release changes
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.check.outputs.already_released != 'true'
- run: |
- if git diff --quiet && git diff --cached --quiet; then
- echo "No changes to commit"
- exit 0
- fi
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- git add -A
- git commit -m "chore(release): build ${VERSION} [skip ci]" \
- --author="gitea-actions[bot] "
- # Detached HEAD on PR merge — push explicitly to main
- git push origin HEAD:refs/heads/main
-
- # -- STEP 6: Create tag ---------------------------------------------------
- - name: "Step 6: Create git tag"
- if: >-
- steps.version.outputs.skip != 'true'
- run: |
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- # Only create the major release tag if it doesn't exist yet
- if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then
- git tag "$RELEASE_TAG"
- git push origin "$RELEASE_TAG"
- echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
- else
- echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY
- fi
- echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
-
- # -- STEP 7a: Promote RC to stable (skip build) ----------------------------
- - name: "Step 7a: Promote RC to stable"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.rc.outputs.promote == 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/release_promote.php \
- --from release-candidate --to stable \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" \
- --api-base "${API_BASE}" \
- --path . --branch main
- echo "Promoted RC → stable (${VERSION})" >> $GITHUB_STEP_SUMMARY
-
- # -- STEP 7b: Create or update Gitea Release (full build path) -------------
- - name: "Step 7b: Gitea Release"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.rc.outputs.promote != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/release_create.php \
- --path . --version "$VERSION" --tag "$RELEASE_TAG" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
- --repo "${GITEA_REPO}" --branch main
- echo "Release created: ${VERSION}" >> $GITHUB_STEP_SUMMARY
-
- # -- STEP 8: Build packages and upload to release ----------------------------
- - name: "Step 8: Build package and upload"
- id: package
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.rc.outputs.promote != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/release_package.php \
- --path . --version "$VERSION" --tag "$RELEASE_TAG" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
- --repo "${GITEA_REPO}" --output /tmp || true
-
- # -- STEP 5: Write update stream (after build so SHA-256 is available) -----
- - name: "Step 5: Write update stream"
- if: steps.version.outputs.skip != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- SHA256="${{ steps.package.outputs.sha256_zip }}"
-
- # Fetch latest updates.xml from main so preserve logic has all channels
- GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
- API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
- curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
- "${API}/contents/updates.xml?ref=main" 2>/dev/null | \
- python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin)['content']).decode())" \
- > updates.xml 2>/dev/null || true
-
- SHA_FLAG=""
- [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
-
- php /tmp/moko-platform-api/cli/updates_xml_build.php \
- --path . --version "${VERSION}" --stability stable \
- --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
- ${SHA_FLAG} --github-output
-
- # Commit updates.xml if changed
- if ! git diff --quiet updates.xml 2>/dev/null; then
- git add updates.xml
- git commit -m "chore: update stable channel ${VERSION} [skip ci]" \
- --author="gitea-actions[bot] "
- git push origin HEAD:refs/heads/main 2>&1 || true
- fi
-
- # -- STEP 8b: Update release description with changelog ----------------------
- - name: "Step 8b: Update release body"
- if: steps.version.outputs.skip != 'true'
- continue-on-error: true
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- php /tmp/moko-platform-api/cli/release_body_update.php \
- --path . --version "${VERSION}" --tag "${RELEASE_TAG}" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" \
- --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
- 2>&1 || true
- echo "Release body updated" >> $GITHUB_STEP_SUMMARY
-
- # -- 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"
-
- # -- Clean up lesser pre-releases (cascade) ---------------------------------
- # stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev
- - name: "Delete lesser pre-release channels"
- continue-on-error: true
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/release_cascade.php \
- --stability stable \
- --version "${VERSION}" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" \
- --api-base "${API_BASE}" 2>/dev/null || true
-
- - name: "Step 11: Delete and recreate dev branch from main"
- if: steps.version.outputs.skip != 'true'
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
-
- # Delete dev branch
- curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
- "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
-
- # Recreate dev from main (now includes version bump + changelog promotion)
- curl -sf -X POST -H "Authorization: token ${TOKEN}" \
- -H "Content-Type: application/json" \
- "${API_BASE}/branches" \
- -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
-
- echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY
-
- - name: "Step 12: Create version branch from main"
- if: steps.version.outputs.skip != 'true'
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- BRANCH_NAME="version/${VERSION}"
- MAIN_SHA=$(git rev-parse HEAD)
-
- # Delete old version branch if it exists (same version re-release)
- curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
-
- # Create version/XX.YY.ZZ from main
- curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
-
- echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
-
-
-
- # -- Dolibarr post-release: Reset dev version -----------------------------
- - name: "Post-release: Reset dev version"
- if: steps.version.outputs.skip != 'true'
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/version_reset_dev.php \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
- --branch dev --path . 2>&1 || true
-
- # -- Summary --------------------------------------------------------------
- - name: Pipeline Summary
- if: always()
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- PLATFORM="${{ steps.platform.outputs.platform }}"
- if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
- echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
- echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
- elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
- echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
- else
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
- echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
- echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
- fi
+# Copyright (C) 2026 Moko Consulting
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# FILE INFORMATION
+# DEFGROUP: Gitea.Workflow
+# INGROUP: moko-platform.Release
+# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
+# PATH: /templates/workflows/universal/auto-release.yml.template
+# VERSION: 05.00.00
+# BRIEF: Universal build & release � detects platform from manifest.xml
+#
+# +========================================================================+
+# | UNIVERSAL BUILD & RELEASE PIPELINE |
+# +========================================================================+
+# | |
+# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
+# | |
+# | Platform-specific: |
+# | joomla: XML manifest, 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 source branch to rc
+ run: |
+ SOURCE_BRANCH="${{ github.event.pull_request.head.ref || 'dev' }}"
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ PR_NUM="${{ github.event.pull_request.number }}"
+ php /tmp/moko-platform-api/cli/branch_rename.php \
+ --from "$SOURCE_BRANCH" --to rc \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" \
+ --api-base "${API_BASE}" \
+ --pr "$PR_NUM"
+
+ - name: Set RC version on renamed branch
+ run: |
+ # Checkout the new rc branch
+ git fetch origin rc
+ git checkout rc
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ MOKO_CLI="/tmp/moko-platform-api/cli"
+
+ VERSION=$(php ${MOKO_CLI}/version_read.php --path .) || true
+ [ -z "$VERSION" ] && { echo "No version — skipping"; exit 0; }
+
+ php ${MOKO_CLI}/version_set_platform.php \
+ --path . --version "$VERSION" --branch rc --stability rc 2>/dev/null || true
+ php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
+
+ if ! git diff --quiet || ! git diff --cached --quiet; then
+ git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
+ git config --local user.name "gitea-actions[bot]"
+ git add -A
+ git commit -m "chore(version): set RC stability suffix [skip ci]" \
+ --author="gitea-actions[bot] "
+ git push origin rc
+ fi
+
+ - name: Build RC release
+ run: |
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ MOKO_CLI="/tmp/moko-platform-api/cli"
+ VERSION=$(php ${MOKO_CLI}/version_read.php --path .) || true
+
+ php ${MOKO_CLI}/release_create.php \
+ --path . --version "$VERSION" --tag "release-candidate" \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
+ --repo "${GITEA_REPO}" --branch rc 2>&1 || true
+
+ php ${MOKO_CLI}/release_package.php \
+ --path . --version "$VERSION" --tag "release-candidate" \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
+ --repo "${GITEA_REPO}" --output /tmp 2>&1 || true
+
+ - name: Cascade lesser channels
+ continue-on-error: true
+ run: |
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ php /tmp/moko-platform-api/cli/release_cascade.php \
+ --stability release-candidate \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" \
+ --api-base "${API_BASE}"
+
+ - name: Summary
+ if: always()
+ run: |
+ echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
+ echo "Draft PR opened — branch renamed to rc, RC release built" >> $GITHUB_STEP_SUMMARY
+
+ # ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
+ release:
+ name: Build & Release Pipeline
+ runs-on: release
+ if: >-
+ github.event.pull_request.merged == true ||
+ (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ token: ${{ secrets.MOKOGITEA_TOKEN }}
+ fetch-depth: 0
+
+ - name: Configure git for bot pushes
+ run: |
+ git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
+ git config --local user.name "gitea-actions[bot]"
+ git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
+
+ - name: 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
+
+
+ # -- PLATFORM DETECTION ---------------------------------------------------
+ - name: Detect platform
+ id: platform
+ run: |
+ php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output
+ MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1 || true)
+ MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1 || true)
+ echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
+ echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT"
+
+ - name: "Step 1: Read version"
+ id: version
+ run: |
+ VERSION=$(php /tmp/moko-platform-api/cli/version_read.php --path .)
+ if [ -z "$VERSION" ]; then
+ echo "::error::No VERSION in README.md"
+ echo "skip=true" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+ # version_set_platform strips suffixes internally when --stability stable
+ MAJOR=$(echo "$VERSION" | cut -d. -f1 | sed 's/-.*//')
+ echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
+ echo "release_tag=stable" >> "$GITHUB_OUTPUT"
+ echo "skip=false" >> "$GITHUB_OUTPUT"
+ echo "branch=main" >> "$GITHUB_OUTPUT"
+
+ # -- CHECK FOR RC PROMOTION ------------------------------------------------
+ - name: "Check for RC release"
+ id: rc
+ if: steps.version.outputs.skip != 'true'
+ run: |
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ RC_JSON=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
+ "${API_BASE}/releases/tags/release-candidate" 2>/dev/null || echo "{}")
+ RC_ID=$(echo "$RC_JSON" | php -r "\$d=json_decode(file_get_contents('php://stdin'),true); echo \$d['id'] ?? '';" 2>/dev/null || true)
+
+ if [ -n "$RC_ID" ] && [ "$RC_ID" != "None" ] && [ "$RC_ID" != "" ]; then
+ echo "promote=true" >> "$GITHUB_OUTPUT"
+ echo "release_id=${RC_ID}" >> "$GITHUB_OUTPUT"
+ echo "::notice::RC release found (id: ${RC_ID}) — will promote to stable"
+ else
+ echo "promote=false" >> "$GITHUB_OUTPUT"
+ echo "::notice::No RC release — full build pipeline"
+ fi
+
+ - name: "Step 1b: Minor bump version"
+ id: bump
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.rc.outputs.promote != 'true'
+ run: |
+ MOKO_API="/tmp/moko-platform-api/cli"
+ php ${MOKO_API}/version_bump.php --path . --minor 2>&1 || true
+ VERSION=$(php ${MOKO_API}/version_read.php --path .)
+ # version_set_platform handles suffix stripping — just pass clean base version
+ echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
+ echo "Bumped to: ${VERSION}"
+
+ - name: Check if already released
+ if: steps.version.outputs.skip != 'true'
+ id: check
+ run: |
+ TAG="${{ steps.version.outputs.release_tag }}"
+ BRANCH="${{ steps.version.outputs.branch }}"
+
+ TAG_EXISTS=false
+ BRANCH_EXISTS=false
+
+ git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true
+ git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true
+
+ echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT"
+ echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT"
+
+ # Tag and branch may persist across patch releases — never skip
+ echo "already_released=false" >> "$GITHUB_OUTPUT"
+
+ # -- SANITY CHECKS -------------------------------------------------------
+ - name: "Sanity: Pre-release validation"
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.check.outputs.already_released != 'true'
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ php /tmp/moko-platform-api/cli/release_validate.php \
+ --path . --version "$VERSION" --output-summary --github-output || true
+
+ # -- STEP 2: Create or update version/XX.YY archive branch ---------------
+ # Always runs — every version change on main archives to version/XX.YY
+ - name: "Step 2: Version archive branch"
+ if: steps.check.outputs.already_released != 'true'
+ run: |
+ BRANCH="${{ steps.version.outputs.branch }}"
+ IS_MINOR="${{ steps.version.outputs.is_minor }}"
+ PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}')
+
+ # Check if branch exists
+ if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
+ git push origin HEAD:"$BRANCH" --force
+ echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY
+ else
+ git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH"
+ git push origin "$BRANCH" --force
+ echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
+ fi
+
+ # -- STEP 3: Set platform version ----------------------------------------
+ - name: "Step 3: Set platform version"
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.check.outputs.already_released != 'true'
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ php /tmp/moko-platform-api/cli/version_set_platform.php \
+ --path . --version "$VERSION" --branch main
+
+ # -- STEP 4: Update version badges ----------------------------------------
+ - name: "Step 4: Update version badges"
+ if: steps.version.outputs.skip != 'true'
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true
+ php /tmp/moko-platform-api/cli/version_check.php --path . --fix 2>/dev/null || true
+
+ # Step 5 (updates.xml) moved after Step 8 to include SHA-256 checksum
+
+ - name: "Step 4b: Promote and prune CHANGELOG"
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.check.outputs.already_released != 'true'
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ MOKO_API="/tmp/moko-platform-api/cli"
+ if [ -f "CHANGELOG.md" ]; then
+ php ${MOKO_API}/changelog_promote.php --path . --version "$VERSION" 2>&1 || true
+ php ${MOKO_API}/changelog_prune.php --path . --keep 5 2>&1 || true
+ fi
+
+ - name: Commit release changes
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.check.outputs.already_released != 'true'
+ run: |
+ if git diff --quiet && git diff --cached --quiet; then
+ echo "No changes to commit"
+ exit 0
+ fi
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ git add -A
+ git commit -m "chore(release): build ${VERSION} [skip ci]" \
+ --author="gitea-actions[bot] "
+ # Detached HEAD on PR merge — push explicitly to main
+ git push origin HEAD:refs/heads/main
+
+ # -- STEP 6: Create tag ---------------------------------------------------
+ - name: "Step 6: Create git tag"
+ if: >-
+ steps.version.outputs.skip != 'true'
+ run: |
+ RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
+ # Only create the major release tag if it doesn't exist yet
+ if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then
+ git tag "$RELEASE_TAG"
+ git push origin "$RELEASE_TAG"
+ echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
+ else
+ echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY
+ fi
+ echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
+
+ # -- STEP 7a: Promote RC to stable (skip build) ----------------------------
+ - name: "Step 7a: Promote RC to stable"
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.rc.outputs.promote == 'true'
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ php /tmp/moko-platform-api/cli/release_promote.php \
+ --from release-candidate --to stable \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" \
+ --api-base "${API_BASE}" \
+ --path . --branch main
+ echo "Promoted RC → stable (${VERSION})" >> $GITHUB_STEP_SUMMARY
+
+ # -- STEP 7b: Create or update Gitea Release (full build path) -------------
+ - name: "Step 7b: Gitea Release"
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.rc.outputs.promote != 'true'
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ php /tmp/moko-platform-api/cli/release_create.php \
+ --path . --version "$VERSION" --tag "$RELEASE_TAG" \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
+ --repo "${GITEA_REPO}" --branch main
+ echo "Release created: ${VERSION}" >> $GITHUB_STEP_SUMMARY
+
+ # -- STEP 8: Build packages and upload to release ----------------------------
+ - name: "Step 8: Build package and upload"
+ id: package
+ if: >-
+ steps.version.outputs.skip != 'true' &&
+ steps.rc.outputs.promote != 'true'
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ php /tmp/moko-platform-api/cli/release_package.php \
+ --path . --version "$VERSION" --tag "$RELEASE_TAG" \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
+ --repo "${GITEA_REPO}" --output /tmp || true
+
+ # -- STEP 5: Write update stream (after build so SHA-256 is available) -----
+ - name: "Step 5: Write update stream"
+ if: steps.version.outputs.skip != 'true'
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ SHA256="${{ steps.package.outputs.sha256_zip }}"
+
+ # Fetch latest updates.xml from main so preserve logic has all channels
+ GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
+ API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
+ curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
+ "${API}/contents/updates.xml?ref=main" 2>/dev/null | \
+ php -r "\$d=json_decode(file_get_contents('php://stdin'),true); echo base64_decode(\$d['content'] ?? '');" \
+ > updates.xml 2>/dev/null || true
+
+ SHA_FLAG=""
+ [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
+
+ php /tmp/moko-platform-api/cli/updates_xml_build.php \
+ --path . --version "${VERSION}" --stability stable \
+ --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
+ ${SHA_FLAG} --github-output
+
+ # Commit updates.xml if changed
+ if ! git diff --quiet updates.xml 2>/dev/null; then
+ git add updates.xml
+ git commit -m "chore: update stable channel ${VERSION} [skip ci]" \
+ --author="gitea-actions[bot] "
+ git push origin HEAD:refs/heads/main 2>&1 || true
+ fi
+
+ # -- STEP 8b: Update release description with changelog ----------------------
+ - name: "Step 8b: Update release body"
+ if: steps.version.outputs.skip != 'true'
+ continue-on-error: true
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
+ php /tmp/moko-platform-api/cli/release_body_update.php \
+ --path . --version "${VERSION}" --tag "${RELEASE_TAG}" \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" \
+ --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
+ 2>&1 || true
+ echo "Release body updated" >> $GITHUB_STEP_SUMMARY
+
+ # -- 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"
+
+ # -- Clean up lesser pre-releases (cascade) ---------------------------------
+ # stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev
+ - name: "Delete lesser pre-release channels"
+ continue-on-error: true
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ php /tmp/moko-platform-api/cli/release_cascade.php \
+ --stability stable \
+ --version "${VERSION}" \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" \
+ --api-base "${API_BASE}" 2>/dev/null || true
+
+ - name: "Step 11: Clean up pre-release branches 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 ephemeral pre-release branches (rc, alpha, beta)
+ for EPHEMERAL in rc alpha beta; do
+ curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
+ "${API_BASE}/branches/${EPHEMERAL}" 2>/dev/null \
+ && echo "Deleted ${EPHEMERAL} branch" \
+ || echo "${EPHEMERAL} branch not found"
+ done
+
+ # Delete dev branch
+ curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
+ "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
+
+ # Recreate dev from main (now includes version bump + changelog promotion)
+ curl -sf -X POST -H "Authorization: token ${TOKEN}" \
+ -H "Content-Type: application/json" \
+ "${API_BASE}/branches" \
+ -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
+
+ echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
+
+ - name: "Step 12: Create version branch from main"
+ if: steps.version.outputs.skip != 'true'
+ continue-on-error: true
+ run: |
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ BRANCH_NAME="version/${VERSION}"
+ MAIN_SHA=$(git rev-parse HEAD)
+
+ # Delete old version branch if it exists (same version re-release)
+ curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
+
+ # Create version/XX.YY.ZZ from main
+ curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
+
+ echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
+
+
+
+ # -- Dolibarr post-release: Reset dev version -----------------------------
+ - name: "Post-release: Reset dev version"
+ if: steps.version.outputs.skip != 'true'
+ continue-on-error: true
+ run: |
+ API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
+ php /tmp/moko-platform-api/cli/version_reset_dev.php \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
+ --branch dev --path . 2>&1 || true
+
+ # -- Summary --------------------------------------------------------------
+ - name: Pipeline Summary
+ if: always()
+ run: |
+ VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
+ PLATFORM="${{ steps.platform.outputs.platform }}"
+ if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
+ echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
+ echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
+ elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
+ echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
+ else
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
+ echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
+ echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
+ echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
+ fi
--
2.52.0
From b9453f97454401e2277f450bbd6d9554739e40b7 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Sat, 30 May 2026 15:01:38 +0000
Subject: [PATCH 24/40] chore: sync .mokogitea/workflows/auto-bump.yml from
moko-platform [skip ci]
---
.mokogitea/workflows/auto-bump.yml | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml
index a397a9e..fb9dc82 100644
--- a/.mokogitea/workflows/auto-bump.yml
+++ b/.mokogitea/workflows/auto-bump.yml
@@ -16,10 +16,9 @@ on:
push:
branches:
- dev
- - alpha
- - beta
- rc
- 'feature/**'
+ - 'patch/**'
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
--
2.52.0
From 6711f1f0e6815b95bdb6fd6f36ad33c345e8205d Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Sat, 30 May 2026 15:04:05 +0000
Subject: [PATCH 25/40] chore: sync .mokogitea/workflows/auto-release.yml from
moko-platform [skip ci]
---
.mokogitea/workflows/auto-release.yml | 353 ++------------------------
1 file changed, 22 insertions(+), 331 deletions(-)
diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml
index 04ec817..1227ff8 100644
--- a/.mokogitea/workflows/auto-release.yml
+++ b/.mokogitea/workflows/auto-release.yml
@@ -82,71 +82,33 @@ jobs:
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
- - name: Rename source branch to rc
+ - name: Rename branch to rc
run: |
- SOURCE_BRANCH="${{ github.event.pull_request.head.ref || 'dev' }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- PR_NUM="${{ github.event.pull_request.number }}"
php /tmp/moko-platform-api/cli/branch_rename.php \
- --from "$SOURCE_BRANCH" --to rc \
+ --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
- --api-base "${API_BASE}" \
- --pr "$PR_NUM"
+ --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
+ --pr "${{ github.event.pull_request.number }}"
- - name: Set RC version on renamed branch
+ - name: Checkout rc and configure git
run: |
- # Checkout the new rc branch
git fetch origin rc
git checkout rc
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- MOKO_CLI="/tmp/moko-platform-api/cli"
+ 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"
- VERSION=$(php ${MOKO_CLI}/version_read.php --path .) || true
- [ -z "$VERSION" ] && { echo "No version — skipping"; exit 0; }
-
- php ${MOKO_CLI}/version_set_platform.php \
- --path . --version "$VERSION" --branch rc --stability rc 2>/dev/null || true
- php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
-
- if ! git diff --quiet || ! git diff --cached --quiet; then
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git add -A
- git commit -m "chore(version): set RC stability suffix [skip ci]" \
- --author="gitea-actions[bot] "
- git push origin rc
- fi
-
- - name: Build RC release
+ - name: Publish RC release
run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- MOKO_CLI="/tmp/moko-platform-api/cli"
- VERSION=$(php ${MOKO_CLI}/version_read.php --path .) || true
-
- php ${MOKO_CLI}/release_create.php \
- --path . --version "$VERSION" --tag "release-candidate" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
- --repo "${GITEA_REPO}" --branch rc 2>&1 || true
-
- php ${MOKO_CLI}/release_package.php \
- --path . --version "$VERSION" --tag "release-candidate" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
- --repo "${GITEA_REPO}" --output /tmp 2>&1 || true
-
- - name: Cascade lesser channels
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/release_cascade.php \
- --stability release-candidate \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" \
- --api-base "${API_BASE}"
+ 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 "Draft PR opened — branch renamed to rc, RC release built" >> $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:
@@ -188,266 +150,11 @@ jobs:
composer install --no-dev --no-interaction --quiet
- # -- PLATFORM DETECTION ---------------------------------------------------
- - name: Detect platform
- id: platform
+ - name: "Publish stable release"
run: |
- php /tmp/moko-platform-api/cli/manifest_read.php --path . --github-output
- MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1 || true)
- MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1 || true)
- echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
- echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT"
-
- - name: "Step 1: Read version"
- id: version
- run: |
- VERSION=$(php /tmp/moko-platform-api/cli/version_read.php --path .)
- if [ -z "$VERSION" ]; then
- echo "::error::No VERSION in README.md"
- echo "skip=true" >> "$GITHUB_OUTPUT"
- exit 0
- fi
- # version_set_platform strips suffixes internally when --stability stable
- MAJOR=$(echo "$VERSION" | cut -d. -f1 | sed 's/-.*//')
- echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- echo "release_tag=stable" >> "$GITHUB_OUTPUT"
- echo "skip=false" >> "$GITHUB_OUTPUT"
- echo "branch=main" >> "$GITHUB_OUTPUT"
-
- # -- CHECK FOR RC PROMOTION ------------------------------------------------
- - name: "Check for RC release"
- id: rc
- if: steps.version.outputs.skip != 'true'
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- RC_JSON=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
- "${API_BASE}/releases/tags/release-candidate" 2>/dev/null || echo "{}")
- RC_ID=$(echo "$RC_JSON" | php -r "\$d=json_decode(file_get_contents('php://stdin'),true); echo \$d['id'] ?? '';" 2>/dev/null || true)
-
- if [ -n "$RC_ID" ] && [ "$RC_ID" != "None" ] && [ "$RC_ID" != "" ]; then
- echo "promote=true" >> "$GITHUB_OUTPUT"
- echo "release_id=${RC_ID}" >> "$GITHUB_OUTPUT"
- echo "::notice::RC release found (id: ${RC_ID}) — will promote to stable"
- else
- echo "promote=false" >> "$GITHUB_OUTPUT"
- echo "::notice::No RC release — full build pipeline"
- fi
-
- - name: "Step 1b: Minor bump version"
- id: bump
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.rc.outputs.promote != 'true'
- run: |
- MOKO_API="/tmp/moko-platform-api/cli"
- php ${MOKO_API}/version_bump.php --path . --minor 2>&1 || true
- VERSION=$(php ${MOKO_API}/version_read.php --path .)
- # version_set_platform handles suffix stripping — just pass clean base version
- echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- echo "Bumped to: ${VERSION}"
-
- - name: Check if already released
- if: steps.version.outputs.skip != 'true'
- id: check
- run: |
- TAG="${{ steps.version.outputs.release_tag }}"
- BRANCH="${{ steps.version.outputs.branch }}"
-
- TAG_EXISTS=false
- BRANCH_EXISTS=false
-
- git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true
- git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true
-
- echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT"
- echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT"
-
- # Tag and branch may persist across patch releases — never skip
- echo "already_released=false" >> "$GITHUB_OUTPUT"
-
- # -- SANITY CHECKS -------------------------------------------------------
- - name: "Sanity: Pre-release validation"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.check.outputs.already_released != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- php /tmp/moko-platform-api/cli/release_validate.php \
- --path . --version "$VERSION" --output-summary --github-output || true
-
- # -- STEP 2: Create or update version/XX.YY archive branch ---------------
- # Always runs — every version change on main archives to version/XX.YY
- - name: "Step 2: Version archive branch"
- if: steps.check.outputs.already_released != 'true'
- run: |
- BRANCH="${{ steps.version.outputs.branch }}"
- IS_MINOR="${{ steps.version.outputs.is_minor }}"
- PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}')
-
- # Check if branch exists
- if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
- git push origin HEAD:"$BRANCH" --force
- echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY
- else
- git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH"
- git push origin "$BRANCH" --force
- echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
- fi
-
- # -- STEP 3: Set platform version ----------------------------------------
- - name: "Step 3: Set platform version"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.check.outputs.already_released != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- php /tmp/moko-platform-api/cli/version_set_platform.php \
- --path . --version "$VERSION" --branch main
-
- # -- STEP 4: Update version badges ----------------------------------------
- - name: "Step 4: Update version badges"
- if: steps.version.outputs.skip != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- php /tmp/moko-platform-api/cli/badge_update.php --path . --version "${VERSION}" 2>/dev/null || true
- php /tmp/moko-platform-api/cli/version_check.php --path . --fix 2>/dev/null || true
-
- # Step 5 (updates.xml) moved after Step 8 to include SHA-256 checksum
-
- - name: "Step 4b: Promote and prune CHANGELOG"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.check.outputs.already_released != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- MOKO_API="/tmp/moko-platform-api/cli"
- if [ -f "CHANGELOG.md" ]; then
- php ${MOKO_API}/changelog_promote.php --path . --version "$VERSION" 2>&1 || true
- php ${MOKO_API}/changelog_prune.php --path . --keep 5 2>&1 || true
- fi
-
- - name: Commit release changes
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.check.outputs.already_released != 'true'
- run: |
- if git diff --quiet && git diff --cached --quiet; then
- echo "No changes to commit"
- exit 0
- fi
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- git add -A
- git commit -m "chore(release): build ${VERSION} [skip ci]" \
- --author="gitea-actions[bot] "
- # Detached HEAD on PR merge — push explicitly to main
- git push origin HEAD:refs/heads/main
-
- # -- STEP 6: Create tag ---------------------------------------------------
- - name: "Step 6: Create git tag"
- if: >-
- steps.version.outputs.skip != 'true'
- run: |
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- # Only create the major release tag if it doesn't exist yet
- if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then
- git tag "$RELEASE_TAG"
- git push origin "$RELEASE_TAG"
- echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
- else
- echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY
- fi
- echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
-
- # -- STEP 7a: Promote RC to stable (skip build) ----------------------------
- - name: "Step 7a: Promote RC to stable"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.rc.outputs.promote == 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/release_promote.php \
- --from release-candidate --to stable \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" \
- --api-base "${API_BASE}" \
- --path . --branch main
- echo "Promoted RC → stable (${VERSION})" >> $GITHUB_STEP_SUMMARY
-
- # -- STEP 7b: Create or update Gitea Release (full build path) -------------
- - name: "Step 7b: Gitea Release"
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.rc.outputs.promote != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/release_create.php \
- --path . --version "$VERSION" --tag "$RELEASE_TAG" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
- --repo "${GITEA_REPO}" --branch main
- echo "Release created: ${VERSION}" >> $GITHUB_STEP_SUMMARY
-
- # -- STEP 8: Build packages and upload to release ----------------------------
- - name: "Step 8: Build package and upload"
- id: package
- if: >-
- steps.version.outputs.skip != 'true' &&
- steps.rc.outputs.promote != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/release_package.php \
- --path . --version "$VERSION" --tag "$RELEASE_TAG" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
- --repo "${GITEA_REPO}" --output /tmp || true
-
- # -- STEP 5: Write update stream (after build so SHA-256 is available) -----
- - name: "Step 5: Write update stream"
- if: steps.version.outputs.skip != 'true'
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- SHA256="${{ steps.package.outputs.sha256_zip }}"
-
- # Fetch latest updates.xml from main so preserve logic has all channels
- GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
- API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
- curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
- "${API}/contents/updates.xml?ref=main" 2>/dev/null | \
- php -r "\$d=json_decode(file_get_contents('php://stdin'),true); echo base64_decode(\$d['content'] ?? '');" \
- > updates.xml 2>/dev/null || true
-
- SHA_FLAG=""
- [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
-
- php /tmp/moko-platform-api/cli/updates_xml_build.php \
- --path . --version "${VERSION}" --stability stable \
- --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
- ${SHA_FLAG} --github-output
-
- # Commit updates.xml if changed
- if ! git diff --quiet updates.xml 2>/dev/null; then
- git add updates.xml
- git commit -m "chore: update stable channel ${VERSION} [skip ci]" \
- --author="gitea-actions[bot] "
- git push origin HEAD:refs/heads/main 2>&1 || true
- fi
-
- # -- STEP 8b: Update release description with changelog ----------------------
- - name: "Step 8b: Update release body"
- if: steps.version.outputs.skip != 'true'
- continue-on-error: true
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- php /tmp/moko-platform-api/cli/release_body_update.php \
- --path . --version "${VERSION}" --tag "${RELEASE_TAG}" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" \
- --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
- 2>&1 || true
- echo "Release body updated" >> $GITHUB_STEP_SUMMARY
+ 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"
@@ -484,33 +191,17 @@ jobs:
&& echo "main branch pushed to GitHub mirror" \
|| echo "WARNING: GitHub mirror push failed"
- # -- Clean up lesser pre-releases (cascade) ---------------------------------
- # stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev
- - name: "Delete lesser pre-release channels"
- continue-on-error: true
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/release_cascade.php \
- --stability stable \
- --version "${VERSION}" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" \
- --api-base "${API_BASE}" 2>/dev/null || true
-
- - name: "Step 11: Clean up pre-release branches and recreate dev from main"
+ - 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 ephemeral pre-release branches (rc, alpha, beta)
- for EPHEMERAL in rc alpha beta; do
- curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
- "${API_BASE}/branches/${EPHEMERAL}" 2>/dev/null \
- && echo "Deleted ${EPHEMERAL} branch" \
- || echo "${EPHEMERAL} branch not found"
- done
+ # 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}" \
--
2.52.0
From 6737569cc33aa70ddae872fffa22b4693ca5f893 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Sat, 30 May 2026 16:03:17 +0000
Subject: [PATCH 26/40] chore: sync .mokogitea/workflows/pr-check.yml from
moko-platform [skip ci]
---
.mokogitea/workflows/pr-check.yml | 54 +++++++++++++++++++++----------
1 file changed, 37 insertions(+), 17 deletions(-)
diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml
index 3e436cf..ce64a27 100644
--- a/.mokogitea/workflows/pr-check.yml
+++ b/.mokogitea/workflows/pr-check.yml
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
-# INGROUP: MokoStandards.CI
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
+# INGROUP: moko-platform.CI
+# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/pr-check.yml.template
# VERSION: 05.00.00
# BRIEF: PR gate — branch policy + code validation before merge
@@ -52,22 +52,22 @@ jobs:
REASON="Fix branches must target 'dev', not '${BASE}'"
fi
;;
+ patch/*)
+ if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
+ ALLOWED=false
+ REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
+ fi
+ ;;
hotfix/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
fi
;;
- alpha/*|beta/*)
- if [ "$BASE" != "dev" ]; then
- ALLOWED=false
- REASON="Pre-release branches must target 'dev', not '${BASE}'"
- fi
- ;;
- rc/*)
+ rc)
if [ "$BASE" != "main" ]; then
ALLOWED=false
- REASON="Release candidate branches must target 'main', not '${BASE}'"
+ REASON="RC branch can only merge into 'main', not '${BASE}'"
fi
;;
dev)
@@ -108,7 +108,9 @@ jobs:
- name: Detect platform
id: platform
run: |
- PLATFORM=$(cat .mokogitea/.moko-platform 2>/dev/null | tr -d '[:space:]')
+ # Read platform from XML manifest ( tag) or plain text fallback
+ PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
+ [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
[ -z "$PLATFORM" ] && PLATFORM="generic"
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
@@ -181,6 +183,28 @@ jobs:
;;
esac
+ - name: Check changelog has unreleased entry
+ run: |
+ if [ ! -f "CHANGELOG.md" ]; then
+ echo "::warning::No CHANGELOG.md found"
+ exit 0
+ fi
+ # Check for content under [Unreleased] section
+ if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
+ echo "::error::CHANGELOG.md missing [Unreleased] section"
+ exit 1
+ fi
+ # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
+ UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
+ if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
+ echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
+ echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
+ echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
+ echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
+ exit 1
+ fi
+ echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
+
- name: Verify package source
run: |
SOURCE_DIR="src"
@@ -202,15 +226,11 @@ jobs:
steps:
- name: Trigger RC pre-release
env:
- GA_TOKEN: ${{ secrets.GA_TOKEN }}
+ 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 ${GA_TOKEN}" \
- -H "Content-Type: application/json" \
- -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
+ 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
--
2.52.0
From 063b6a6dd957eaf868b2036ac665b0d550594f82 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Sun, 31 May 2026 01:45:28 +0000
Subject: [PATCH 27/40] chore: sync .mokogitea/workflows/cascade-dev.yml from
moko-platform [skip ci]
---
.mokogitea/workflows/cascade-dev.yml | 217 +--------------------------
1 file changed, 7 insertions(+), 210 deletions(-)
diff --git a/.mokogitea/workflows/cascade-dev.yml b/.mokogitea/workflows/cascade-dev.yml
index 4dbb135..5f7c1d7 100644
--- a/.mokogitea/workflows/cascade-dev.yml
+++ b/.mokogitea/workflows/cascade-dev.yml
@@ -1,213 +1,10 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: MokoStandards.Maintenance
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
-# PATH: /templates/workflows/cascade-dev.yml.template
-# VERSION: 02.00.00
-# BRIEF: Forward-merge main → all open branches after every push to main
-#
-# +========================================================================+
-# | CASCADE MAIN → ALL BRANCHES |
-# +========================================================================+
-# | |
-# | Triggers on every push to main (PR merges, bot commits, etc.) |
-# | |
-# | 1. List all branches matching: dev, rc/*, beta/*, alpha/* |
-# | 2. For each: create PR (main → branch), auto-merge if clean |
-# | 3. On conflict: leave PR open for manual resolution |
-# | |
-# +========================================================================+
-
-name: "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
-
+# DISABLED — auto-release Step 11 recreates dev from main after every release.
+# Cascade-dev is redundant and causes version conflicts when both main and dev
+# have different version numbers in templateDetails.xml / manifest.xml.
+name: "Cascade Main → Dev (DISABLED)"
+on: workflow_dispatch
jobs:
- cascade:
- name: Cascade main → branches
+ noop:
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
+ - run: echo "Cascade disabled — auto-release handles dev recreation"
--
2.52.0
From 146d386aa3ab9b66a375dd00f8830514a6bfc213 Mon Sep 17 00:00:00 2001
From: "gitea-actions[bot]"
Date: Sun, 31 May 2026 03:50:37 +0000
Subject: [PATCH 28/40] chore(ci): remove auto-release.yml for update server
migration [skip ci]
---
.mokogitea/workflows/auto-release.yml | 270 --------------------------
1 file changed, 270 deletions(-)
delete mode 100644 .mokogitea/workflows/auto-release.yml
diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml
deleted file mode 100644
index 1227ff8..0000000
--- a/.mokogitea/workflows/auto-release.yml
+++ /dev/null
@@ -1,270 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Release
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
-# PATH: /templates/workflows/universal/auto-release.yml.template
-# VERSION: 05.00.00
-# BRIEF: Universal build & release � detects platform from manifest.xml
-#
-# +========================================================================+
-# | UNIVERSAL BUILD & RELEASE PIPELINE |
-# +========================================================================+
-# | |
-# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
-# | |
-# | Platform-specific: |
-# | joomla: XML manifest, updates.xml, type-prefixed packages |
-# | dolibarr: mod*.class.php, update.txt, dev version reset |
-# | generic: README-only, no update stream |
-# | |
-# +========================================================================+
-
-name: "Universal: Build & Release"
-
-on:
- pull_request:
- types: [opened, closed]
- branches:
- - main
- workflow_dispatch:
- inputs:
- action:
- description: 'Action to perform'
- required: false
- type: choice
- default: release
- options:
- - release
- - promote-rc
-
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
- GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
-
-permissions:
- contents: write
-
-jobs:
- # ── PR Opened → Rename branch to RC and build RC release ─────────────────────
- promote-rc:
- name: Promote to RC
- runs-on: release
- if: >-
- (github.event.action == 'opened' && github.event.pull_request.merged != true) ||
- (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- token: ${{ secrets.MOKOGITEA_TOKEN }}
- fetch-depth: 1
-
- - name: Setup moko-platform tools
- env:
- MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
- MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
- run: |
- if ! command -v composer &> /dev/null; then
- sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
- fi
- # Always fetch latest CLI tools — never use stale cache from previous runs
- rm -rf /tmp/moko-platform-api
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
- /tmp/moko-platform-api
- cd /tmp/moko-platform-api
- composer install --no-dev --no-interaction --quiet
-
- - name: Rename branch to rc
- run: |
- php /tmp/moko-platform-api/cli/branch_rename.php \
- --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" \
- --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
- --pr "${{ github.event.pull_request.number }}"
-
- - name: Checkout rc and configure git
- run: |
- git fetch origin rc
- git checkout rc
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
-
- - name: Publish RC release
- run: |
- php /tmp/moko-platform-api/cli/release_publish.php \
- --path . --stability rc --bump minor --branch rc \
- --token "${{ secrets.MOKOGITEA_TOKEN }}"
-
- - name: Summary
- if: always()
- run: |
- echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
- echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY
-
- # ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
- release:
- name: Build & Release Pipeline
- runs-on: release
- if: >-
- github.event.pull_request.merged == true ||
- (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- token: ${{ secrets.MOKOGITEA_TOKEN }}
- fetch-depth: 0
-
- - name: Configure git for bot pushes
- run: |
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
-
- - name: Setup moko-platform tools
- env:
- MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
- MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
- COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
- run: |
- # Ensure PHP + Composer are available
- if ! command -v composer &> /dev/null; then
- sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
- fi
- # Always fetch latest CLI tools — never use stale cache from previous runs
- rm -rf /tmp/moko-platform-api
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
- /tmp/moko-platform-api
- cd /tmp/moko-platform-api
- composer install --no-dev --no-interaction --quiet
-
-
- - name: "Publish stable release"
- run: |
- php /tmp/moko-platform-api/cli/release_publish.php \
- --path . --stability stable --bump minor --branch main \
- --token "${{ secrets.MOKOGITEA_TOKEN }}"
-
- # -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- - name: "Step 9: Mirror release to GitHub"
- if: >-
- steps.version.outputs.skip != 'true' &&
- secrets.GH_MIRROR_TOKEN != ''
- continue-on-error: true
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
- GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/release_mirror.php \
- --version "$VERSION" --tag "$RELEASE_TAG" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
- --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
- --branch main 2>&1 || true
- echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
-
- # -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- - name: "Step 10: Push main to GitHub mirror"
- if: >-
- steps.version.outputs.skip != 'true' &&
- secrets.GH_MIRROR_TOKEN != ''
- continue-on-error: true
- run: |
- GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
- GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
- GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
- git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
- git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
- git fetch origin main --depth=1
- git push github origin/main:refs/heads/main --force 2>/dev/null \
- && echo "main branch pushed to GitHub mirror" \
- || echo "WARNING: GitHub mirror push failed"
-
- - name: "Step 11: Delete rc branch and recreate dev from main"
- if: steps.version.outputs.skip != 'true'
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
-
- # Delete rc branch (ephemeral — created by promote-rc)
- curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
- "${API_BASE}/branches/rc" 2>/dev/null \
- && echo "Deleted rc branch" || echo "rc branch not found"
-
- # Delete dev branch
- curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
- "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
-
- # Recreate dev from main (now includes version bump + changelog promotion)
- curl -sf -X POST -H "Authorization: token ${TOKEN}" \
- -H "Content-Type: application/json" \
- "${API_BASE}/branches" \
- -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
-
- echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
-
- - name: "Step 12: Create version branch from main"
- if: steps.version.outputs.skip != 'true'
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- BRANCH_NAME="version/${VERSION}"
- MAIN_SHA=$(git rev-parse HEAD)
-
- # Delete old version branch if it exists (same version re-release)
- curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
-
- # Create version/XX.YY.ZZ from main
- curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
-
- echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
-
-
-
- # -- Dolibarr post-release: Reset dev version -----------------------------
- - name: "Post-release: Reset dev version"
- if: steps.version.outputs.skip != 'true'
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php /tmp/moko-platform-api/cli/version_reset_dev.php \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
- --branch dev --path . 2>&1 || true
-
- # -- Summary --------------------------------------------------------------
- - name: Pipeline Summary
- if: always()
- run: |
- VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
- PLATFORM="${{ steps.platform.outputs.platform }}"
- if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
- echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
- echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
- elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
- echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
- else
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
- echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
- echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
- fi
--
2.52.0
From f1468eede7f6c1f3c3e0a9dc6bbc6e26b7156f85 Mon Sep 17 00:00:00 2001
From: "gitea-actions[bot]"
Date: Sun, 31 May 2026 03:50:39 +0000
Subject: [PATCH 29/40] chore(ci): remove pre-release.yml for update server
migration [skip ci]
---
.mokogitea/workflows/pre-release.yml | 233 ---------------------------
1 file changed, 233 deletions(-)
delete mode 100644 .mokogitea/workflows/pre-release.yml
diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml
deleted file mode 100644
index 162b08f..0000000
--- a/.mokogitea/workflows/pre-release.yml
+++ /dev/null
@@ -1,233 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Release
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
-# PATH: /templates/workflows/universal/pre-release.yml.template
-# VERSION: 05.01.00
-# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
-
-name: "Universal: Pre-Release"
-
-on:
- pull_request:
- types: [closed]
- branches:
- - dev
- workflow_dispatch:
- inputs:
- stability:
- description: 'Pre-release channel'
- required: true
- type: choice
- options:
- - development
- - alpha
- - beta
- - release-candidate
-
-permissions:
- contents: write
-
-env:
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
- GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
-
-jobs:
- build:
- name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
- runs-on: release
- if: >-
- github.event_name == 'workflow_dispatch' ||
- (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
- token: ${{ secrets.MOKOGITEA_TOKEN }}
-
- - name: Setup moko-platform tools
- env:
- MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
- MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
- run: |
- if ! command -v composer &> /dev/null; then
- sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
- fi
- # Always fetch latest CLI tools — never use stale cache from previous runs
- rm -rf /tmp/moko-platform-api
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
- /tmp/moko-platform-api
- cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
- echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
-
- - name: Detect platform
- id: platform
- run: |
- php ${MOKO_CLI}/manifest_read.php --path . --github-output
-
- - name: Resolve metadata and bump version
- id: meta
- run: |
- STABILITY="${{ inputs.stability || 'development' }}"
-
- case "$STABILITY" in
- development) SUFFIX="-dev"; TAG="development" ;;
- alpha) SUFFIX="-alpha"; TAG="alpha" ;;
- beta) SUFFIX="-beta"; TAG="beta" ;;
- release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
- esac
-
- # Read current version (bump already handled by push workflow)
- VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
- [ -z "$VERSION" ] && VERSION="00.00.01"
-
- # Strip any existing suffix from version before applying stability
- VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
-
- php ${MOKO_CLI}/version_set_platform.php \
- --path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
-
- # Verify version consistency across all files
- php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
-
- # Update VERSION variable with suffix
- if [ -n "$SUFFIX" ]; then
- VERSION="${VERSION}${SUFFIX}"
- fi
-
- # Commit version bump
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- git add -A
- git diff --cached --quiet || {
- git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
- git push origin HEAD 2>&1
- }
-
- # Auto-detect element via manifest_element.php
- php ${MOKO_CLI}/manifest_element.php \
- --path . --version "$VERSION" --stability "$STABILITY" \
- --repo "${GITEA_REPO}" --github-output
-
- # Read back element outputs
- EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
- ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
- [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
- [ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
-
- echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
- echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
- echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
- echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
- echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
-
- echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
-
- - name: Create release
- id: release
- run: |
- TAG="${{ steps.meta.outputs.tag }}"
- VERSION="${{ steps.meta.outputs.version }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php ${MOKO_CLI}/release_create.php \
- --path . --version "$VERSION" --tag "$TAG" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
- --repo "${GITEA_REPO}" --branch dev --prerelease
-
- - name: Build package and upload
- id: package
- run: |
- VERSION="${{ steps.meta.outputs.version }}"
- TAG="${{ steps.meta.outputs.tag }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- php ${MOKO_CLI}/release_package.php \
- --path . --version "$VERSION" --tag "$TAG" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
- --repo "${GITEA_REPO}" --output /tmp || true
-
- - name: Update updates.xml
- if: steps.platform.outputs.platform == 'joomla'
- run: |
- VERSION="${{ steps.meta.outputs.version }}"
- STABILITY="${{ steps.meta.outputs.stability }}"
- SHA256="${{ steps.package.outputs.sha256_zip }}"
-
- if [ ! -f "updates.xml" ]; then
- echo "No updates.xml -- skipping"
- exit 0
- fi
-
- SHA_FLAG=""
- [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
-
- php ${MOKO_CLI}/updates_xml_build.php \
- --path . --version "${VERSION}" --stability "${STABILITY}" \
- --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
- ${SHA_FLAG}
-
- # Commit and push
- if ! git diff --quiet updates.xml 2>/dev/null; then
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git add updates.xml
- git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
- git push origin HEAD 2>&1 || echo "WARNING: push failed"
- fi
-
- - name: "Sync updates.xml to all branches"
- if: steps.platform.outputs.platform == 'joomla'
- run: |
- CURRENT_BRANCH="${{ github.ref_name }}"
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
-
- for BRANCH in main dev; do
- [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
- echo "Syncing updates.xml -> ${BRANCH}"
- git fetch origin "${BRANCH}" 2>/dev/null || continue
- git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue
- git checkout "${CURRENT_BRANCH}" -- updates.xml
- if ! git diff --quiet updates.xml 2>/dev/null; then
- git add updates.xml
- git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
- git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
- fi
- git checkout "${CURRENT_BRANCH}" 2>/dev/null
- done
-
- - name: "Delete lesser pre-release channels (cascade)"
- continue-on-error: true
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
-
- php ${MOKO_CLI}/release_cascade.php \
- --stability "${{ steps.meta.outputs.stability }}" \
- --token "${TOKEN}" \
- --api-base "${API_BASE}"
-
- - name: Summary
- if: always()
- run: |
- VERSION="${{ steps.meta.outputs.version }}"
- STABILITY="${{ steps.meta.outputs.stability }}"
- ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
- SHA256="${{ steps.package.outputs.sha256_zip }}"
- echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
- echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
- echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
- echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
--
2.52.0
From bbb003825169d33911058c44481670fe55f8117b Mon Sep 17 00:00:00 2001
From: "gitea-actions[bot]"
Date: Sun, 31 May 2026 03:50:42 +0000
Subject: [PATCH 30/40] chore(ci): remove auto-bump.yml for update server
migration [skip ci]
---
.mokogitea/workflows/auto-bump.yml | 66 ------------------------------
1 file changed, 66 deletions(-)
delete mode 100644 .mokogitea/workflows/auto-bump.yml
diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml
deleted file mode 100644
index fb9dc82..0000000
--- a/.mokogitea/workflows/auto-bump.yml
+++ /dev/null
@@ -1,66 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Release
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
-# PATH: /.mokogitea/workflows/auto-bump.yml
-# VERSION: 09.02.00
-# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
-
-name: "Universal: Auto Version Bump"
-
-on:
- push:
- branches:
- - dev
- - rc
- - 'feature/**'
- - 'patch/**'
-
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
-
-permissions:
- contents: write
-
-jobs:
- bump:
- name: Version Bump
- runs-on: release
- if: >-
- !contains(github.event.head_commit.message, '[skip ci]') &&
- !contains(github.event.head_commit.message, '[skip bump]') &&
- !startsWith(github.event.head_commit.message, 'Merge pull request')
-
- steps:
- - name: Checkout
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- with:
- token: ${{ secrets.MOKOGITEA_TOKEN }}
- fetch-depth: 1
-
- - name: Setup moko-platform tools
- run: |
- if ! command -v composer &> /dev/null; then
- sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
- fi
- if [ -d "/opt/moko-platform/cli" ]; then
- echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
- else
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
- /tmp/moko-platform-api
- cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
- echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
- fi
-
- - name: Bump version
- run: |
- php ${MOKO_CLI}/version_auto_bump.php \
- --path . --branch "${GITHUB_REF_NAME}" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" \
- --repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
--
2.52.0
From 77de90d94cf6e08a36f1023b09fd4923a2746138 Mon Sep 17 00:00:00 2001
From: "gitea-actions[bot]"
Date: Sun, 31 May 2026 03:50:46 +0000
Subject: [PATCH 31/40] chore(ci): remove cascade-dev.yml for update server
migration [skip ci]
---
.mokogitea/workflows/cascade-dev.yml | 10 ----------
1 file changed, 10 deletions(-)
delete mode 100644 .mokogitea/workflows/cascade-dev.yml
diff --git a/.mokogitea/workflows/cascade-dev.yml b/.mokogitea/workflows/cascade-dev.yml
deleted file mode 100644
index 5f7c1d7..0000000
--- a/.mokogitea/workflows/cascade-dev.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-# DISABLED — auto-release Step 11 recreates dev from main after every release.
-# Cascade-dev is redundant and causes version conflicts when both main and dev
-# have different version numbers in templateDetails.xml / manifest.xml.
-name: "Cascade Main → Dev (DISABLED)"
-on: workflow_dispatch
-jobs:
- noop:
- runs-on: ubuntu-latest
- steps:
- - run: echo "Cascade disabled — auto-release handles dev recreation"
--
2.52.0
From 8280c5a182f9794fdf0af87c2c748949e062b00a Mon Sep 17 00:00:00 2001
From: "gitea-actions[bot]"
Date: Sun, 31 May 2026 03:50:51 +0000
Subject: [PATCH 32/40] chore(ci): remove update-server.yml for update server
migration [skip ci]
---
.mokogitea/workflows/update-server.yml | 312 -------------------------
1 file changed, 312 deletions(-)
delete mode 100644 .mokogitea/workflows/update-server.yml
diff --git a/.mokogitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml
deleted file mode 100644
index 339d3f5..0000000
--- a/.mokogitea/workflows/update-server.yml
+++ /dev/null
@@ -1,312 +0,0 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.Universal
-# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
-# PATH: /templates/workflows/update-server.yml
-# VERSION: 05.00.00
-# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
-#
-# Thin wrapper around moko-platform CLI tools.
-# Builds packages, updates updates.xml, and optionally deploys via SFTP.
-#
-# Joomla filters update entries by the user's "Minimum Stability" setting.
-
-name: "Update Server"
-
-on:
- push:
- branches:
- - 'dev'
- - 'dev/**'
- - 'alpha/**'
- - 'beta/**'
- - 'rc/**'
- paths:
- - 'src/**'
- - 'htdocs/**'
- pull_request:
- types: [closed]
- branches:
- - 'dev'
- - 'dev/**'
- - 'alpha/**'
- - 'beta/**'
- - 'rc/**'
- paths:
- - 'src/**'
- - 'htdocs/**'
- workflow_dispatch:
- inputs:
- stability:
- description: 'Stability tag'
- required: true
- default: 'development'
- type: choice
- options:
- - development
- - alpha
- - beta
- - rc
- - stable
-
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
- GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
-
-permissions:
- contents: write
-
-jobs:
- update-xml:
- name: Update Server
- runs-on: release
- if: >-
- github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- token: ${{ secrets.MOKOGITEA_TOKEN }}
- fetch-depth: 0
-
- - name: Setup moko-platform tools
- env:
- MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
- MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
- COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
- run: |
- if ! command -v composer &> /dev/null; then
- sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
- fi
- # Always fetch latest CLI tools — never use stale cache from previous runs
- rm -rf /tmp/moko-platform
- git clone --depth 1 --branch main --quiet \
- "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
- /tmp/moko-platform 2>/dev/null || true
- if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
- cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
- fi
- echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
-
- - name: Detect platform
- id: platform
- run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
-
- - name: Resolve stability and bump version
- id: meta
- run: |
- BRANCH="${{ github.ref_name }}"
-
- # Configure git for bot pushes
- git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
- git config --local user.name "gitea-actions[bot]"
- git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
-
- # Auto-bump patch version
- php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
-
- VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0")
-
- # Strip any existing suffix before applying stability
- VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
-
- # Determine stability from branch or manual input
- if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
- STABILITY="${{ inputs.stability }}"
- elif [[ "$BRANCH" == rc/* ]]; then
- STABILITY="rc"
- elif [[ "$BRANCH" == beta/* ]]; then
- STABILITY="beta"
- elif [[ "$BRANCH" == alpha/* ]]; then
- STABILITY="alpha"
- else
- STABILITY="development"
- fi
-
- # Version suffix per stability stream
- case "$STABILITY" in
- development) SUFFIX="-dev"; TAG="development" ;;
- alpha) SUFFIX="-alpha"; TAG="alpha" ;;
- beta) SUFFIX="-beta"; TAG="beta" ;;
- rc) SUFFIX="-rc"; TAG="release-candidate" ;;
- *) SUFFIX=""; TAG="stable" ;;
- esac
-
- # Propagate version with stability suffix to all manifest files
- php ${MOKO_CLI}/version_set_platform.php \
- --path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
- php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
-
- # Re-read version (now includes suffix from version_set_platform)
- if [ -n "$SUFFIX" ]; then
- VERSION="${VERSION}${SUFFIX}"
- fi
-
- echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
- echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
- echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
- echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT"
-
- # Commit version bump if changed
- git add -A
- git diff --cached --quiet || {
- git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
- --author="gitea-actions[bot] "
- git push
- }
-
- - name: Create release and upload package
- id: package
- run: |
- VERSION="${{ steps.meta.outputs.version }}"
- TAG="${{ steps.meta.outputs.tag }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
-
- # Create or update Gitea release
- php ${MOKO_CLI}/release_create.php \
- --path . --version "$VERSION" --tag "$TAG" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
- --repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
-
- # Build package and upload
- php ${MOKO_CLI}/release_package.php \
- --path . --version "$VERSION" --tag "$TAG" \
- --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
- --repo "${GITEA_REPO}" --output /tmp || true
-
- - name: Update updates.xml
- if: steps.platform.outputs.platform == 'joomla'
- run: |
- VERSION="${{ steps.meta.outputs.version }}"
- STABILITY="${{ steps.meta.outputs.stability }}"
- SHA256="${{ steps.package.outputs.sha256_zip }}"
-
- if [ ! -f "updates.xml" ]; then
- echo "No updates.xml — skipping"
- exit 0
- fi
-
- SHA_FLAG=""
- [ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
-
- php ${MOKO_CLI}/updates_xml_build.php \
- --path . --version "${VERSION}" --stability "${STABILITY}" \
- --gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
- ${SHA_FLAG}
-
- # Commit and push updates.xml
- git add updates.xml
- git diff --cached --quiet || {
- git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
- git push
- }
-
- - name: Sync updates.xml to main
- if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
- run: |
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
- GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
-
- FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
- "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
-
- if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
- python3 -c "
- import base64, json, urllib.request, sys
- with open('updates.xml', 'rb') as f:
- content = base64.b64encode(f.read()).decode()
- payload = json.dumps({
- 'content': content,
- 'sha': '${FILE_SHA}',
- 'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
- 'branch': 'main'
- }).encode()
- req = urllib.request.Request(
- '${API_BASE}/contents/updates.xml',
- data=payload, method='PUT',
- headers={
- 'Authorization': 'token ${GITEA_TOKEN}',
- 'Content-Type': 'application/json'
- })
- try:
- urllib.request.urlopen(req)
- print('updates.xml synced to main')
- except Exception as e:
- print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
- "
- fi
-
- - name: SFTP deploy to dev server
- if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
- env:
- DEV_HOST: ${{ vars.DEV_FTP_HOST }}
- DEV_PATH: ${{ vars.DEV_FTP_PATH }}
- DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
- DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
- DEV_PORT: ${{ vars.DEV_FTP_PORT }}
- DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
- DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
- run: |
- # Permission check: admin or maintain role required
- ACTOR="${{ github.actor }}"
- API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
-
- PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
- "${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
- python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
- case "$PERMISSION" in
- admin|maintain|write) ;;
- *)
- echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
- exit 0
- ;;
- esac
-
- [ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
-
- SOURCE_DIR="src"
- [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
- [ ! -d "$SOURCE_DIR" ] && exit 0
-
- PORT="${DEV_PORT:-22}"
- REMOTE="${DEV_PATH%/}"
- [ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
-
- printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
- "$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
- if [ -n "$DEV_KEY" ]; then
- echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
- printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
- else
- printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
- fi
-
- PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
- if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
- php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
- elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
- php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
- fi
- rm -f /tmp/deploy_key /tmp/sftp-config.json
- echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
-
- - name: Summary
- if: always()
- run: |
- VERSION="${{ steps.meta.outputs.version }}"
- STABILITY="${{ steps.meta.outputs.stability }}"
- DISPLAY="${{ steps.meta.outputs.display_version }}"
- echo "## Update Server" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
- echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
- echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
- echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
--
2.52.0
From caeffcbeaac7ae72e7cd4b1a418edb9c034a1c69 Mon Sep 17 00:00:00 2001
From: Moko Consulting
Date: Tue, 2 Jun 2026 20:37:00 +0000
Subject: [PATCH 33/40] chore(ci): add CI issue reporter for auto-filing gate
failures
---
.mokogitea/workflows/repo-health.yml | 1583 +++++++++++++-------------
1 file changed, 817 insertions(+), 766 deletions(-)
diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml
index 87bc666..b23d971 100644
--- a/.mokogitea/workflows/repo-health.yml
+++ b/.mokogitea/workflows/repo-health.yml
@@ -1,766 +1,817 @@
-# ============================================================================
-# Copyright (C) 2025 Moko Consulting
-#
-# This file is part of a Moko Consulting project.
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: MokoStandards.Validation
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
-# PATH: /templates/workflows/joomla/repo_health.yml.template
-# VERSION: 04.06.00
-# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
-# ============================================================================
-
-name: "Joomla: Repo Health"
-
-concurrency:
- group: repo-health-${{ github.repository }}-${{ github.ref }}
- cancel-in-progress: true
-
-defaults:
- run:
- shell: bash
-
-on:
- workflow_dispatch:
- inputs:
- profile:
- description: 'Validation profile: all, release, scripts, or repo'
- required: true
- default: all
- type: choice
- options:
- - all
- - release
- - scripts
- - repo
- pull_request:
- push:
-
-permissions:
- contents: read
-
-env:
- # Release policy - Repository Variables Only
- RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX
- RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX
-
- # Scripts governance policy
- SCRIPTS_REQUIRED_DIRS:
- SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
-
- # Repo health policy
- REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.gitea/workflows/
- REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
- REPO_DISALLOWED_DIRS:
- REPO_DISALLOWED_FILES: TODO.md,todo.md
-
- # Extended checks toggles
- EXTENDED_CHECKS: "true"
-
- # File / directory variables
- DOCS_INDEX: docs/docs-index.md
- SCRIPT_DIR: scripts
- WORKFLOWS_DIR: .gitea/workflows
- SHELLCHECK_PATTERN: '*.sh'
- SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
-
-jobs:
- access_check:
- name: Access control
- runs-on: ubuntu-latest
- timeout-minutes: 10
- permissions:
- contents: read
-
- outputs:
- allowed: ${{ steps.perm.outputs.allowed }}
- permission: ${{ steps.perm.outputs.permission }}
-
- steps:
- - name: Check actor permission (admin only)
- id: perm
- env:
- TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
- REPO: ${{ github.repository }}
- ACTOR: ${{ github.actor }}
- run: |
- set -euo pipefail
- ALLOWED=false
- PERMISSION=unknown
- METHOD=""
-
- # Hardcoded authorized users — always allowed
- case "$ACTOR" in
- jmiller|gitea-actions[bot])
- ALLOWED=true
- PERMISSION=admin
- METHOD="hardcoded allowlist"
- ;;
- *)
- # Detect platform and check permissions via API
- API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}"
- RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \
- "${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}')
- PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown")
- if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then
- ALLOWED=true
- fi
- METHOD="collaborator API"
- ;;
- esac
-
- echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT"
- echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT"
-
- {
- echo "## Access Authorization"
- echo ""
- echo "| Field | Value |"
- echo "|-------|-------|"
- echo "| **Actor** | \`${ACTOR}\` |"
- echo "| **Repository** | \`${REPO}\` |"
- echo "| **Permission** | \`${PERMISSION}\` |"
- echo "| **Method** | ${METHOD} |"
- echo "| **Authorized** | ${ALLOWED} |"
- echo ""
- if [ "$ALLOWED" = "true" ]; then
- echo "${ACTOR} authorized (${METHOD})"
- else
- echo "${ACTOR} is NOT authorized. Requires admin or maintain role."
- fi
- } >> "${GITHUB_STEP_SUMMARY}"
-
- - name: Deny execution when not permitted
- if: ${{ steps.perm.outputs.allowed != 'true' }}
- run: |
- set -euo pipefail
- printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
- exit 1
-
- release_config:
- name: Release configuration
- needs: access_check
- if: ${{ needs.access_check.outputs.allowed == 'true' }}
- runs-on: ubuntu-latest
- timeout-minutes: 20
- permissions:
- contents: read
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Guardrails release vars
- env:
- PROFILE_RAW: ${{ github.event.inputs.profile }}
- RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }}
- DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
- run: |
- set -euo pipefail
-
- profile="${PROFILE_RAW:-all}"
- case "${profile}" in
- all|release|scripts|repo) ;;
- *)
- printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
- exit 1
- ;;
- esac
-
- if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then
- {
- printf '%s\n' '### Release configuration (Repository Variables)'
- printf '%s\n' "Profile: ${profile}"
- printf '%s\n' 'Status: SKIPPED'
- printf '%s\n' 'Reason: profile excludes release validation'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- exit 0
- fi
-
- IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}"
- IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}"
-
- missing=()
- missing_optional=()
-
- for k in "${required[@]}"; do
- v="${!k:-}"
- [ -z "${v}" ] && missing+=("${k}")
- done
-
- for k in "${optional[@]}"; do
- v="${!k:-}"
- [ -z "${v}" ] && missing_optional+=("${k}")
- done
-
- {
- printf '%s\n' '### Release configuration (Repository Variables)'
- printf '%s\n' "Profile: ${profile}"
- printf '%s\n' '| Variable | Status |'
- printf '%s\n' '|---|---|'
- printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |"
- printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |"
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
-
- if [ "${#missing_optional[@]}" -gt 0 ]; then
- {
- printf '%s\n' '### Missing optional repository variables'
- for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
-
- if [ "${#missing[@]}" -gt 0 ]; then
- {
- printf '%s\n' '### Missing required repository variables'
- for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done
- printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.'
- } >> "${GITHUB_STEP_SUMMARY}"
- exit 1
- fi
-
- {
- printf '%s\n' '### Repository variables validation result'
- printf '%s\n' 'Status: OK'
- printf '%s\n' 'All required repository variables present.'
- printf '%s\n' ''
- printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
-
- scripts_governance:
- name: Scripts governance
- needs: access_check
- if: ${{ needs.access_check.outputs.allowed == 'true' }}
- runs-on: ubuntu-latest
- timeout-minutes: 15
- permissions:
- contents: read
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Scripts folder checks
- env:
- PROFILE_RAW: ${{ github.event.inputs.profile }}
- run: |
- set -euo pipefail
-
- profile="${PROFILE_RAW:-all}"
- case "${profile}" in
- all|release|scripts|repo) ;;
- *)
- printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
- exit 1
- ;;
- esac
-
- if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then
- {
- printf '%s\n' '### Scripts governance'
- printf '%s\n' "Profile: ${profile}"
- printf '%s\n' 'Status: SKIPPED'
- printf '%s\n' 'Reason: profile excludes scripts governance'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- exit 0
- fi
-
- if [ ! -d "${SCRIPT_DIR}" ]; then
- {
- printf '%s\n' '### Scripts governance'
- printf '%s\n' 'Status: OK (advisory)'
- printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- exit 0
- fi
-
- IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"
- IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
-
- missing_dirs=()
- unapproved_dirs=()
-
- for d in "${required_dirs[@]}"; do
- req="${d%/}"
- [ ! -d "${req}" ] && missing_dirs+=("${req}/")
- done
-
- while IFS= read -r d; do
- allowed=false
- for a in "${allowed_dirs[@]}"; do
- a_norm="${a%/}"
- [ "${d%/}" = "${a_norm}" ] && allowed=true
- done
- [ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/")
- done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##')
-
- {
- printf '%s\n' '### Scripts governance'
- printf '%s\n' "Profile: ${profile}"
- printf '%s\n' '| Area | Status | Notes |'
- printf '%s\n' '|---|---|---|'
-
- if [ "${#missing_dirs[@]}" -gt 0 ]; then
- printf '%s\n' '| Required directories | Warning | Missing required subfolders |'
- else
- printf '%s\n' '| Required directories | OK | All required subfolders present |'
- fi
-
- if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
- printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |'
- else
- printf '%s\n' '| Directory policy | OK | No unapproved directories |'
- fi
-
- printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |'
- printf '\n'
-
- if [ "${#missing_dirs[@]}" -gt 0 ]; then
- printf '%s\n' 'Missing required script directories:'
- for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done
- printf '\n'
- else
- printf '%s\n' 'Missing required script directories: none.'
- printf '\n'
- fi
-
- if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
- printf '%s\n' 'Unapproved script directories detected:'
- for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done
- printf '\n'
- else
- printf '%s\n' 'Unapproved script directories detected: none.'
- printf '\n'
- fi
-
- printf '%s\n' 'Scripts governance completed in advisory mode.'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
-
- repo_health:
- name: Repository health
- needs: access_check
- if: ${{ needs.access_check.outputs.allowed == 'true' }}
- runs-on: ubuntu-latest
- timeout-minutes: 20
- permissions:
- contents: read
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Repository health checks
- env:
- PROFILE_RAW: ${{ github.event.inputs.profile }}
- run: |
- set -euo pipefail
-
- profile="${PROFILE_RAW:-all}"
- case "${profile}" in
- all|release|scripts|repo) ;;
- *)
- printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
- exit 1
- ;;
- esac
-
- if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then
- {
- printf '%s\n' '### Repository health'
- printf '%s\n' "Profile: ${profile}"
- printf '%s\n' 'Status: SKIPPED'
- printf '%s\n' 'Reason: profile excludes repository health'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- exit 0
- fi
-
- # Source directory: src/ or htdocs/ (either is valid)
- if [ -d "src" ]; then
- SOURCE_DIR="src"
- elif [ -d "htdocs" ]; then
- SOURCE_DIR="htdocs"
- else
- missing_required+=("src/ or htdocs/ (source directory required)")
- fi
-
- IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
- IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
- IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"
- IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}"
-
- missing_required=()
- missing_optional=()
-
- for item in "${required_artifacts[@]}"; do
- if printf '%s' "${item}" | grep -q '/$'; then
- d="${item%/}"
- [ ! -d "${d}" ] && missing_required+=("${item}")
- else
- [ ! -f "${item}" ] && missing_required+=("${item}")
- fi
- done
-
- for f in "${optional_files[@]}"; do
- if printf '%s' "${f}" | grep -q '/$'; then
- d="${f%/}"
- [ ! -d "${d}" ] && missing_optional+=("${f}")
- else
- [ ! -f "${f}" ] && missing_optional+=("${f}")
- fi
- done
-
- for d in "${disallowed_dirs[@]}"; do
- d_norm="${d%/}"
- [ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)")
- done
-
- for f in "${disallowed_files[@]}"; do
- [ -f "${f}" ] && missing_required+=("${f} (disallowed)")
- done
-
- git fetch origin --prune
-
- dev_paths=()
- dev_branches=()
-
- while IFS= read -r b; do
- name="${b#origin/}"
- if [ "${name}" = 'dev' ]; then
- dev_branches+=("${name}")
- else
- dev_paths+=("${name}")
- fi
- done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
-
- if [ "${#dev_paths[@]}" -eq 0 ]; then
- missing_required+=("dev/* branch (e.g. dev/01.00.00)")
- fi
-
- if [ "${#dev_branches[@]}" -gt 0 ]; then
- missing_required+=("invalid branch dev (must be dev/)")
- fi
-
- content_warnings=()
-
- if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then
- content_warnings+=("CHANGELOG.md missing '# Changelog' header")
- fi
-
- if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then
- content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)")
- fi
-
- if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then
- content_warnings+=("LICENSE does not look like a GPL text")
- fi
-
- if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then
- content_warnings+=("README.md missing expected brand keyword")
- fi
-
- export PROFILE_RAW="${profile}"
- export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")"
- export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
- export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
-
- report_json="$(python3 - <<'PY'
- import json
- import os
-
- profile = os.environ.get('PROFILE_RAW') or 'all'
-
- missing_required = os.environ.get('MISSING_REQUIRED', '').splitlines() if os.environ.get('MISSING_REQUIRED') else []
- missing_optional = os.environ.get('MISSING_OPTIONAL', '').splitlines() if os.environ.get('MISSING_OPTIONAL') else []
- content_warnings = os.environ.get('CONTENT_WARNINGS', '').splitlines() if os.environ.get('CONTENT_WARNINGS') else []
-
- out = {
- 'profile': profile,
- 'missing_required': [x for x in missing_required if x],
- 'missing_optional': [x for x in missing_optional if x],
- 'content_warnings': [x for x in content_warnings if x],
- }
-
- print(json.dumps(out, indent=2))
- PY
- )"
-
- {
- printf '%s\n' '### Repository health'
- printf '%s\n' "Profile: ${profile}"
- printf '%s\n' '| Metric | Value |'
- printf '%s\n' '|---|---|'
- printf '%s\n' "| Missing required | ${#missing_required[@]} |"
- printf '%s\n' "| Missing optional | ${#missing_optional[@]} |"
- printf '%s\n' "| Content warnings | ${#content_warnings[@]} |"
- printf '\n'
-
- printf '%s\n' '### Guardrails report (JSON)'
- printf '%s\n' '```json'
- printf '%s\n' "${report_json}"
- printf '%s\n' '```'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
-
- if [ "${#missing_required[@]}" -gt 0 ]; then
- {
- printf '%s\n' '### Missing required repo artifacts'
- for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done
- printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- exit 1
- fi
-
- if [ "${#missing_optional[@]}" -gt 0 ]; then
- {
- printf '%s\n' '### Missing optional repo artifacts'
- for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
-
- if [ "${#content_warnings[@]}" -gt 0 ]; then
- {
- printf '%s\n' '### Repo content warnings'
- for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
-
- # -- Joomla-specific checks --
- joomla_findings=()
-
- MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '/dev/null | head -1 || true)"
- if [ -z "${MANIFEST}" ]; then
- joomla_findings+=("Joomla XML manifest not found (no *.xml with tag)")
- else
- if ! grep -qP '' "${MANIFEST}"; then
- joomla_findings+=("XML manifest: tag missing")
- fi
- if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then
- joomla_findings+=("XML manifest: type attribute missing or invalid")
- fi
- if ! grep -qP '' "${MANIFEST}"; then
- joomla_findings+=("XML manifest: tag missing")
- fi
- if ! grep -qP '' "${MANIFEST}"; then
- joomla_findings+=("XML manifest: tag missing")
- fi
- if ! grep -qP ' missing (required for Joomla 5+)")
- fi
- fi
-
- INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)"
- if [ "${INI_COUNT}" -eq 0 ]; then
- joomla_findings+=("No .ini language files found")
- fi
-
- if [ ! -f 'updates.xml' ]; then
- joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
- fi
-
- INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
- for dir in "${INDEX_DIRS[@]}"; do
- if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
- joomla_findings+=("${dir}/index.html missing (directory listing protection)")
- fi
- done
-
- if [ "${#joomla_findings[@]}" -gt 0 ]; then
- {
- printf '%s\n' '### Joomla extension checks'
- printf '%s\n' '| Check | Status |'
- printf '%s\n' '|---|---|'
- for f in "${joomla_findings[@]}"; do
- printf '%s\n' "| ${f} | Warning |"
- done
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- else
- {
- printf '%s\n' '### Joomla extension checks'
- printf '%s\n' 'All Joomla-specific checks passed.'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
-
- extended_enabled="${EXTENDED_CHECKS:-true}"
- extended_findings=()
-
- if [ "${extended_enabled}" = 'true' ]; then
- if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then
- :
- else
- extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)")
- fi
-
- if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then
- bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)"
- if [ -n "${bad_refs}" ]; then
- extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt")
- {
- printf '%s\n' '### Workflow pinning advisory'
- printf '%s\n' 'Found uses: entries pinned to main/master:'
- printf '%s\n' '```'
- printf '%s\n' "${bad_refs}"
- printf '%s\n' '```'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
- fi
-
- if [ -f "${DOCS_INDEX}" ]; then
- missing_links="$(python3 - <<'PY'
- import os
- import re
-
- idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md')
- base = os.getcwd()
-
- bad = []
- pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)')
-
- with open(idx, 'r', encoding='utf-8') as f:
- for line in f:
- for m in pat.findall(line):
- link = m.strip()
- if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'):
- continue
- if link.startswith('/'):
- rel = link.lstrip('/')
- else:
- rel = os.path.normpath(os.path.join(os.path.dirname(idx), link))
- rel = rel.split('#', 1)[0]
- rel = rel.split('?', 1)[0]
- if not rel:
- continue
- p = os.path.join(base, rel)
- if not os.path.exists(p):
- bad.append(rel)
-
- print('\n'.join(sorted(set(bad))))
- PY
- )"
- if [ -n "${missing_links}" ]; then
- extended_findings+=("docs/docs-index.md contains broken relative links")
- {
- printf '%s\n' '### Docs index link integrity'
- printf '%s\n' 'Broken relative links:'
- while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}"
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
- fi
-
- if [ -d "${SCRIPT_DIR}" ]; then
- if ! command -v shellcheck >/dev/null 2>&1; then
- sudo apt-get update -qq
- sudo apt-get install -y shellcheck >/dev/null
- fi
-
- sc_out=''
- while IFS= read -r shf; do
- [ -z "${shf}" ] && continue
- out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)"
- if [ -n "${out_one}" ]; then
- sc_out="${sc_out}${out_one}\n"
- fi
- done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort)
-
- if [ -n "${sc_out}" ]; then
- extended_findings+=("ShellCheck warnings detected (advisory)")
- sc_head="$(printf '%s' "${sc_out}" | head -n 200)"
- {
- printf '%s\n' '### ShellCheck (advisory)'
- printf '%s\n' '```'
- printf '%s\n' "${sc_head}"
- printf '%s\n' '```'
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
- fi
-
- spdx_missing=()
- IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}"
- spdx_args=()
- for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done
-
- while IFS= read -r f; do
- [ -z "${f}" ] && continue
- if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then
- spdx_missing+=("${f}")
- fi
- done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true)
-
- if [ "${#spdx_missing[@]}" -gt 0 ]; then
- extended_findings+=("SPDX header missing in some tracked files (advisory)")
- {
- printf '%s\n' '### SPDX header advisory'
- printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):'
- for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
-
- stale_cutoff_days=180
- stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)"
- if [ -n "${stale_branches}" ]; then
- extended_findings+=("Stale remote branches detected (advisory)")
- {
- printf '%s\n' '### Git hygiene advisory'
- printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):"
- while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}"
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
- fi
-
- {
- printf '%s\n' '### Guardrails coverage matrix'
- printf '%s\n' '| Domain | Status | Notes |'
- printf '%s\n' '|---|---|---|'
- printf '%s\n' '| Access control | OK | Admin-only execution gate |'
- printf '%s\n' '| Release variables | OK | Repository variables validation |'
- printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
- printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
- printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
- if [ "${extended_enabled}" = 'true' ]; then
- if [ "${#extended_findings[@]}" -gt 0 ]; then
- printf '%s\n' '| Extended checks | Warning | See extended findings below |'
- else
- printf '%s\n' '| Extended checks | OK | No findings |'
- fi
- else
- printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |'
- fi
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
-
- if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then
- {
- printf '%s\n' '### Extended findings (advisory)'
- for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done
- printf '\n'
- } >> "${GITHUB_STEP_SUMMARY}"
- fi
-
- printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
+# ============================================================================
+# Copyright (C) 2025 Moko Consulting
+#
+# This file is part of a Moko Consulting project.
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# FILE INFORMATION
+# DEFGROUP: Gitea.Workflow
+# INGROUP: moko-platform.Validation
+# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
+# PATH: /templates/workflows/joomla/repo_health.yml.template
+# VERSION: 09.23.00
+# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
+# ============================================================================
+
+name: "Generic: Repo Health"
+
+defaults:
+ run:
+ shell: bash
+
+on:
+ workflow_dispatch:
+ inputs:
+ profile:
+ description: 'Validation profile: all, release, scripts, or repo'
+ required: true
+ default: all
+ type: choice
+ options:
+ - all
+ - release
+ - scripts
+ - repo
+ pull_request:
+ push:
+
+permissions:
+ contents: read
+
+env:
+ # Release policy - Repository Variables Only
+ RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX
+ RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX
+
+ # Scripts governance policy
+ SCRIPTS_REQUIRED_DIRS:
+ SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
+
+ # Repo health policy
+ REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/
+ REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
+ REPO_DISALLOWED_DIRS:
+ REPO_DISALLOWED_FILES: TODO.md,todo.md
+
+ # Extended checks toggles
+ EXTENDED_CHECKS: "true"
+
+ # File / directory variables
+ DOCS_INDEX: docs/docs-index.md
+ SCRIPT_DIR: scripts
+ WORKFLOWS_DIR: .mokogitea/workflows
+ SHELLCHECK_PATTERN: '*.sh'
+ SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+
+jobs:
+ access_check:
+ name: Access control
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ permissions:
+ contents: read
+
+ outputs:
+ allowed: ${{ steps.perm.outputs.allowed }}
+ permission: ${{ steps.perm.outputs.permission }}
+
+ steps:
+ - name: Check actor permission (admin only)
+ id: perm
+ env:
+ TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
+ REPO: ${{ github.repository }}
+ ACTOR: ${{ github.actor }}
+ run: |
+ set -euo pipefail
+ ALLOWED=false
+ PERMISSION=unknown
+ METHOD=""
+
+ # Hardcoded authorized users — always allowed
+ case "$ACTOR" in
+ jmiller|gitea-actions[bot])
+ ALLOWED=true
+ PERMISSION=admin
+ METHOD="hardcoded allowlist"
+ ;;
+ *)
+ # Detect platform and check permissions via API
+ API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}"
+ RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \
+ "${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}')
+ PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown")
+ if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then
+ ALLOWED=true
+ fi
+ METHOD="collaborator API"
+ ;;
+ esac
+
+ echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT"
+ echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT"
+
+ {
+ echo "## Access Authorization"
+ echo ""
+ echo "| Field | Value |"
+ echo "|-------|-------|"
+ echo "| **Actor** | \`${ACTOR}\` |"
+ echo "| **Repository** | \`${REPO}\` |"
+ echo "| **Permission** | \`${PERMISSION}\` |"
+ echo "| **Method** | ${METHOD} |"
+ echo "| **Authorized** | ${ALLOWED} |"
+ echo ""
+ if [ "$ALLOWED" = "true" ]; then
+ echo "${ACTOR} authorized (${METHOD})"
+ else
+ echo "${ACTOR} is NOT authorized. Requires admin or maintain role."
+ fi
+ } >> "${GITHUB_STEP_SUMMARY}"
+
+ - name: Deny execution when not permitted
+ if: ${{ steps.perm.outputs.allowed != 'true' }}
+ run: |
+ set -euo pipefail
+ printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
+ exit 1
+
+ release_config:
+ name: Release configuration
+ needs: access_check
+ if: ${{ needs.access_check.outputs.allowed == 'true' }}
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ fetch-depth: 0
+
+ - name: Guardrails release vars
+ env:
+ PROFILE_RAW: ${{ github.event.inputs.profile }}
+ RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }}
+ DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
+ run: |
+ set -euo pipefail
+
+ profile="${PROFILE_RAW:-all}"
+ case "${profile}" in
+ all|release|scripts|repo) ;;
+ *)
+ printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
+ exit 1
+ ;;
+ esac
+
+ if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then
+ {
+ printf '%s\n' '### Release configuration (Repository Variables)'
+ printf '%s\n' "Profile: ${profile}"
+ printf '%s\n' 'Status: SKIPPED'
+ printf '%s\n' 'Reason: profile excludes release validation'
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ exit 0
+ fi
+
+ IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}"
+ IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}"
+
+ missing=()
+ missing_optional=()
+
+ for k in "${required[@]}"; do
+ v="${!k:-}"
+ [ -z "${v}" ] && missing+=("${k}")
+ done
+
+ for k in "${optional[@]}"; do
+ v="${!k:-}"
+ [ -z "${v}" ] && missing_optional+=("${k}")
+ done
+
+ {
+ printf '%s\n' '### Release configuration (Repository Variables)'
+ printf '%s\n' "Profile: ${profile}"
+ printf '%s\n' '| Variable | Status |'
+ printf '%s\n' '|---|---|'
+ printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |"
+ printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |"
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+
+ if [ "${#missing_optional[@]}" -gt 0 ]; then
+ {
+ printf '%s\n' '### Missing optional repository variables'
+ for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ fi
+
+ if [ "${#missing[@]}" -gt 0 ]; then
+ {
+ printf '%s\n' '### Missing required repository variables'
+ for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done
+ printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ exit 1
+ fi
+
+ {
+ printf '%s\n' '### Repository variables validation result'
+ printf '%s\n' 'Status: OK'
+ printf '%s\n' 'All required repository variables present.'
+ printf '%s\n' ''
+ printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.'
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+
+ scripts_governance:
+ name: Scripts governance
+ needs: access_check
+ if: ${{ needs.access_check.outputs.allowed == 'true' }}
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ fetch-depth: 0
+
+ - name: Scripts folder checks
+ env:
+ PROFILE_RAW: ${{ github.event.inputs.profile }}
+ run: |
+ set -euo pipefail
+
+ profile="${PROFILE_RAW:-all}"
+ case "${profile}" in
+ all|release|scripts|repo) ;;
+ *)
+ printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
+ exit 1
+ ;;
+ esac
+
+ if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then
+ {
+ printf '%s\n' '### Scripts governance'
+ printf '%s\n' "Profile: ${profile}"
+ printf '%s\n' 'Status: SKIPPED'
+ printf '%s\n' 'Reason: profile excludes scripts governance'
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ exit 0
+ fi
+
+ if [ ! -d "${SCRIPT_DIR}" ]; then
+ {
+ printf '%s\n' '### Scripts governance'
+ printf '%s\n' 'Status: OK (advisory)'
+ printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.'
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ exit 0
+ fi
+
+ if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi
+ IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
+
+ missing_dirs=()
+ unapproved_dirs=()
+
+ for d in "${required_dirs[@]}"; do
+ req="${d%/}"
+ [ ! -d "${req}" ] && missing_dirs+=("${req}/")
+ done
+
+ while IFS= read -r d; do
+ allowed=false
+ for a in "${allowed_dirs[@]}"; do
+ a_norm="${a%/}"
+ [ "${d%/}" = "${a_norm}" ] && allowed=true
+ done
+ [ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/")
+ done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##')
+
+ {
+ printf '%s\n' '### Scripts governance'
+ printf '%s\n' "Profile: ${profile}"
+ printf '%s\n' '| Area | Status | Notes |'
+ printf '%s\n' '|---|---|---|'
+
+ if [ "${#missing_dirs[@]}" -gt 0 ]; then
+ printf '%s\n' '| Required directories | Warning | Missing required subfolders |'
+ else
+ printf '%s\n' '| Required directories | OK | All required subfolders present |'
+ fi
+
+ if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
+ printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |'
+ else
+ printf '%s\n' '| Directory policy | OK | No unapproved directories |'
+ fi
+
+ printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |'
+ printf '\n'
+
+ if [ "${#missing_dirs[@]}" -gt 0 ]; then
+ printf '%s\n' 'Missing required script directories:'
+ for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done
+ printf '\n'
+ else
+ printf '%s\n' 'Missing required script directories: none.'
+ printf '\n'
+ fi
+
+ if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
+ printf '%s\n' 'Unapproved script directories detected:'
+ for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done
+ printf '\n'
+ else
+ printf '%s\n' 'Unapproved script directories detected: none.'
+ printf '\n'
+ fi
+
+ printf '%s\n' 'Scripts governance completed in advisory mode.'
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+
+ repo_health:
+ name: Repository health
+ needs: access_check
+ if: ${{ needs.access_check.outputs.allowed == 'true' }}
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ fetch-depth: 0
+
+ - name: Repository health checks
+ env:
+ PROFILE_RAW: ${{ github.event.inputs.profile }}
+ run: |
+ set -euo pipefail
+
+ profile="${PROFILE_RAW:-all}"
+ case "${profile}" in
+ all|release|scripts|repo) ;;
+ *)
+ printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
+ exit 1
+ ;;
+ esac
+
+ if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then
+ {
+ printf '%s\n' '### Repository health'
+ printf '%s\n' "Profile: ${profile}"
+ printf '%s\n' 'Status: SKIPPED'
+ printf '%s\n' 'Reason: profile excludes repository health'
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ exit 0
+ fi
+
+ IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
+ IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
+ if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi
+ IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}"
+
+ missing_required=()
+ missing_optional=()
+
+ # Source directory: src/ or htdocs/ (either is valid for extension repos)
+ SOURCE_DIR=""
+ if [ -d "src" ]; then
+ SOURCE_DIR="src"
+ elif [ -d "htdocs" ]; then
+ SOURCE_DIR="htdocs"
+ elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
+ # Platform/tooling repos don't need src/
+ SOURCE_DIR=""
+ else
+ missing_required+=("src/ or htdocs/ (source directory required)")
+ fi
+
+ for item in "${required_artifacts[@]}"; do
+ if printf '%s' "${item}" | grep -q '/$'; then
+ d="${item%/}"
+ [ ! -d "${d}" ] && missing_required+=("${item}")
+ else
+ [ ! -f "${item}" ] && missing_required+=("${item}")
+ fi
+ done
+
+ for f in "${optional_files[@]}"; do
+ if printf '%s' "${f}" | grep -q '/$'; then
+ d="${f%/}"
+ [ ! -d "${d}" ] && missing_optional+=("${f}")
+ else
+ [ ! -f "${f}" ] && missing_optional+=("${f}")
+ fi
+ done
+
+ for d in "${disallowed_dirs[@]}"; do
+ d_norm="${d%/}"
+ [ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)")
+ done
+
+ for f in "${disallowed_files[@]}"; do
+ [ -f "${f}" ] && missing_required+=("${f} (disallowed)")
+ done
+
+ git fetch origin --prune
+
+ dev_paths=()
+ dev_branches=()
+
+ while IFS= read -r b; do
+ name="${b#origin/}"
+ if [ "${name}" = 'dev' ]; then
+ dev_branches+=("${name}")
+ else
+ dev_paths+=("${name}")
+ fi
+ done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
+
+ if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then
+ missing_required+=("dev or dev/* branch")
+ fi
+
+ content_warnings=()
+
+ if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then
+ content_warnings+=("CHANGELOG.md missing '# Changelog' header")
+ fi
+
+ if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then
+ content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)")
+ fi
+
+ if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then
+ content_warnings+=("LICENSE does not look like a GPL text")
+ fi
+
+ if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then
+ content_warnings+=("README.md missing expected brand keyword")
+ fi
+
+ export PROFILE_RAW="${profile}"
+ export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")"
+ export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
+ export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
+
+ report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}")
+
+ {
+ printf '%s\n' '### Repository health'
+ printf '%s\n' "Profile: ${profile}"
+ printf '%s\n' '| Metric | Value |'
+ printf '%s\n' '|---|---|'
+ printf '%s\n' "| Missing required | ${#missing_required[@]} |"
+ printf '%s\n' "| Missing optional | ${#missing_optional[@]} |"
+ printf '%s\n' "| Content warnings | ${#content_warnings[@]} |"
+ printf '\n'
+
+ printf '%s\n' '### Guardrails report (JSON)'
+ printf '%s\n' '```json'
+ printf '%s\n' "${report_json}"
+ printf '%s\n' '```'
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+
+ if [ "${#missing_required[@]}" -gt 0 ]; then
+ {
+ printf '%s\n' '### Missing required repo artifacts'
+ for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done
+ printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.'
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ exit 1
+ fi
+
+ if [ "${#missing_optional[@]}" -gt 0 ]; then
+ {
+ printf '%s\n' '### Missing optional repo artifacts'
+ for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ fi
+
+ if [ "${#content_warnings[@]}" -gt 0 ]; then
+ {
+ printf '%s\n' '### Repo content warnings'
+ for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ fi
+
+ # -- Joomla-specific checks --
+ joomla_findings=()
+
+ MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '/dev/null | head -1 || true)"
+ if [ -z "${MANIFEST}" ]; then
+ joomla_findings+=("Joomla XML manifest not found (no *.xml with tag)")
+ else
+ if ! grep -qP '' "${MANIFEST}"; then
+ joomla_findings+=("XML manifest: tag missing")
+ fi
+ if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then
+ joomla_findings+=("XML manifest: type attribute missing or invalid")
+ fi
+ if ! grep -qP '' "${MANIFEST}"; then
+ joomla_findings+=("XML manifest: tag missing")
+ fi
+ if ! grep -qP '' "${MANIFEST}"; then
+ joomla_findings+=("XML manifest: tag missing")
+ fi
+ if ! grep -qP ' missing (required for Joomla 5+)")
+ fi
+ fi
+
+ INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)"
+ if [ "${INI_COUNT}" -eq 0 ]; then
+ joomla_findings+=("No .ini language files found")
+ fi
+
+ if [ ! -f 'updates.xml' ]; then
+ joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
+ fi
+
+ if [ -n "${SOURCE_DIR}" ]; then
+ INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
+ for dir in "${INDEX_DIRS[@]}"; do
+ if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
+ joomla_findings+=("${dir}/index.html missing (directory listing protection)")
+ fi
+ done
+ fi
+
+ if [ "${#joomla_findings[@]}" -gt 0 ]; then
+ {
+ printf '%s\n' '### Joomla extension checks'
+ printf '%s\n' '| Check | Status |'
+ printf '%s\n' '|---|---|'
+ for f in "${joomla_findings[@]}"; do
+ printf '%s\n' "| ${f} | Warning |"
+ done
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ else
+ {
+ printf '%s\n' '### Joomla extension checks'
+ printf '%s\n' 'All Joomla-specific checks passed.'
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ fi
+
+ extended_enabled="${EXTENDED_CHECKS:-true}"
+ extended_findings=()
+
+ if [ "${extended_enabled}" = 'true' ]; then
+ if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then
+ :
+ else
+ extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)")
+ fi
+
+ if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then
+ bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)"
+ if [ -n "${bad_refs}" ]; then
+ extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt")
+ {
+ printf '%s\n' '### Workflow pinning advisory'
+ printf '%s\n' 'Found uses: entries pinned to main/master:'
+ printf '%s\n' '```'
+ printf '%s\n' "${bad_refs}"
+ printf '%s\n' '```'
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ fi
+ fi
+
+ if [ -f "${DOCS_INDEX}" ]; then
+ missing_links=""
+ while IFS= read -r docline; do
+ for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do
+ case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac
+ linkpath="${link%%#*}"
+ linkpath="${linkpath%%\?*}"
+ [ -z "$linkpath" ] && continue
+ if [ "${linkpath:0:1}" = "/" ]; then
+ testpath="${linkpath#/}"
+ else
+ testpath="$(dirname "${DOCS_INDEX}")/${linkpath}"
+ fi
+ [ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} "
+ done
+ done < "${DOCS_INDEX}"
+ if [ -n "${missing_links}" ]; then
+ extended_findings+=("docs/docs-index.md contains broken relative links")
+ {
+ printf '%s\n' '### Docs index link integrity'
+ printf '%s\n' 'Broken relative links:'
+ for bl in ${missing_links}; do
+ printf '%s\n' "- ${bl}"
+ done
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ fi
+ fi
+
+ if [ -d "${SCRIPT_DIR}" ]; then
+ if ! command -v shellcheck >/dev/null 2>&1; then
+ sudo apt-get update -qq
+ sudo apt-get install -y shellcheck >/dev/null
+ fi
+
+ sc_out=''
+ while IFS= read -r shf; do
+ [ -z "${shf}" ] && continue
+ out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)"
+ if [ -n "${out_one}" ]; then
+ sc_out="${sc_out}${out_one}\n"
+ fi
+ done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort)
+
+ if [ -n "${sc_out}" ]; then
+ extended_findings+=("ShellCheck warnings detected (advisory)")
+ sc_head="$(printf '%s' "${sc_out}" | head -n 200)"
+ {
+ printf '%s\n' '### ShellCheck (advisory)'
+ printf '%s\n' '```'
+ printf '%s\n' "${sc_head}"
+ printf '%s\n' '```'
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ fi
+ fi
+
+ spdx_missing=()
+ IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}"
+ spdx_args=()
+ for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done
+
+ while IFS= read -r f; do
+ [ -z "${f}" ] && continue
+ if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then
+ spdx_missing+=("${f}")
+ fi
+ done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true)
+
+ if [ "${#spdx_missing[@]}" -gt 0 ]; then
+ extended_findings+=("SPDX header missing in some tracked files (advisory)")
+ {
+ printf '%s\n' '### SPDX header advisory'
+ printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):'
+ for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ fi
+
+ stale_cutoff_days=180
+ stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)"
+ if [ -n "${stale_branches}" ]; then
+ extended_findings+=("Stale remote branches detected (advisory)")
+ {
+ printf '%s\n' '### Git hygiene advisory'
+ printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):"
+ while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}"
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ fi
+ fi
+
+ {
+ printf '%s\n' '### Guardrails coverage matrix'
+ printf '%s\n' '| Domain | Status | Notes |'
+ printf '%s\n' '|---|---|---|'
+ printf '%s\n' '| Access control | OK | Admin-only execution gate |'
+ printf '%s\n' '| Release variables | OK | Repository variables validation |'
+ printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
+ printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
+ printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
+ if [ "${extended_enabled}" = 'true' ]; then
+ if [ "${#extended_findings[@]}" -gt 0 ]; then
+ printf '%s\n' '| Extended checks | Warning | See extended findings below |'
+ else
+ printf '%s\n' '| Extended checks | OK | No findings |'
+ fi
+ else
+ printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |'
+ fi
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+
+ if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then
+ {
+ printf '%s\n' '### Extended findings (advisory)'
+ for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done
+ printf '\n'
+ } >> "${GITHUB_STEP_SUMMARY}"
+ fi
+
+ printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
+
+
+ site-health:
+ name: Site Health
+ runs-on: ubuntu-latest
+ if: github.event_name == 'workflow_dispatch'
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.3'
+
+ - name: Uptime check
+ if: env.URLS != ''
+ run: |
+ echo "$URLS" > /tmp/urls.txt
+ php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down"
+ rm -f /tmp/urls.txt
+ env:
+ URLS: ${{ vars.MONITORED_URLS }}
+
+ - name: SSL certificate check
+ if: env.DOMAINS != ''
+ run: |
+ echo "$DOMAINS" > /tmp/domains.txt
+ php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon"
+ rm -f /tmp/domains.txt
+ env:
+ DOMAINS: ${{ vars.MONITORED_DOMAINS }}
+
+ - name: Summary
+ if: always()
+ run: |
+ echo "### Site Health" >> $GITHUB_STEP_SUMMARY
+ echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY
+
+ # ═══════════════════════════════════════════════════════════════════════
+ # Issue Reporter — file issues for failed gates
+ # ═══════════════════════════════════════════════════════════════════════
+ report-issues:
+ name: "Report Issues"
+ runs-on: ubuntu-latest
+ needs: [access_check, release_config, scripts_governance, repo_health]
+ if: >-
+ always() &&
+ (needs.release_config.result == 'failure' ||
+ needs.scripts_governance.result == 'failure' ||
+ needs.repo_health.result == 'failure')
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ sparse-checkout: automation/ci-issue-reporter.sh
+ sparse-checkout-cone-mode: false
+
+ - name: "File issues for failed gates"
+ env:
+ GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
+ GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
+ run: |
+ chmod +x automation/ci-issue-reporter.sh
+ REPORTER="./automation/ci-issue-reporter.sh"
+ WF="Repo Health"
+
+ report_gate() {
+ local gate="$1" result="$2" details="$3"
+ if [ "$result" = "failure" ]; then
+ "$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error
+ fi
+ }
+
+ report_gate "Release Configuration" \
+ "${{ needs.release_config.result }}" \
+ "Required repository variables are missing (RS_FTP_PATH_SUFFIX). Check repository settings."
+
+ report_gate "Scripts Governance" \
+ "${{ needs.scripts_governance.result }}" \
+ "Scripts directory policy violations detected. Review required and allowed directories."
+
+ report_gate "Repository Health" \
+ "${{ needs.repo_health.result }}" \
+ "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
+
--
2.52.0
From ef99d366686ce6a2f313043f5f67cdd0bfad89d4 Mon Sep 17 00:00:00 2001
From: Moko Consulting
Date: Tue, 2 Jun 2026 20:37:01 +0000
Subject: [PATCH 34/40] chore(ci): add CI issue reporter for auto-filing gate
failures
---
.mokogitea/workflows/pr-check.yml | 500 ++++++++++++++++--------------
1 file changed, 264 insertions(+), 236 deletions(-)
diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml
index ce64a27..e2c82ef 100644
--- a/.mokogitea/workflows/pr-check.yml
+++ b/.mokogitea/workflows/pr-check.yml
@@ -1,236 +1,264 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.CI
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
-# PATH: /templates/workflows/universal/pr-check.yml.template
-# VERSION: 05.00.00
-# BRIEF: PR gate — branch policy + code validation before merge
-
-name: "Universal: PR Check"
-
-on:
- pull_request:
- types: [opened, synchronize, reopened, edited]
-
-permissions:
- contents: read
- pull-requests: write
-
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
-
-jobs:
- # ── Branch Policy ──────────────────────────────────────────────────────
- branch-policy:
- name: Branch Policy
- runs-on: ubuntu-latest
- steps:
- - name: Check branch merge target
- run: |
- HEAD="${{ github.head_ref }}"
- BASE="${{ github.base_ref }}"
-
- echo "PR: ${HEAD} → ${BASE}"
-
- ALLOWED=true
- REASON=""
-
- case "$HEAD" in
- feature/*|feat/*)
- if [ "$BASE" != "dev" ]; then
- ALLOWED=false
- REASON="Feature branches must target 'dev', not '${BASE}'"
- fi
- ;;
- fix/*|bugfix/*)
- if [ "$BASE" != "dev" ]; then
- ALLOWED=false
- REASON="Fix branches must target 'dev', not '${BASE}'"
- fi
- ;;
- patch/*)
- if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
- ALLOWED=false
- REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
- fi
- ;;
- hotfix/*)
- if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
- ALLOWED=false
- REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
- fi
- ;;
- rc)
- if [ "$BASE" != "main" ]; then
- ALLOWED=false
- REASON="RC branch can only merge into 'main', not '${BASE}'"
- fi
- ;;
- dev)
- if [ "$BASE" != "main" ]; then
- ALLOWED=false
- REASON="Dev branch can only merge into 'main', not '${BASE}'"
- fi
- ;;
- esac
-
- if [ "$ALLOWED" = false ]; then
- echo "::error::${REASON}"
- echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "${REASON}" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
- echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
- exit 1
- fi
-
- echo "Branch policy: OK (${HEAD} → ${BASE})"
- echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
-
- # ── Code Validation ────────────────────────────────────────────────────
- validate:
- name: Validate PR
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Detect platform
- id: platform
- run: |
- # Read platform from XML manifest ( tag) or plain text fallback
- PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
- [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
- [ -z "$PLATFORM" ] && PLATFORM="generic"
- echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
-
- - name: Setup PHP
- if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
- run: |
- if ! command -v php &> /dev/null; then
- sudo apt-get update -qq
- sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
- fi
-
- - name: PHP syntax check
- if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
- run: |
- ERRORS=0
- while IFS= read -r -d '' file; do
- if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
- ERRORS=$((ERRORS + 1))
- fi
- done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
- echo "PHP lint: ${ERRORS} error(s)"
- [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
-
- - name: Validate platform manifest
- run: |
- PLATFORM="${{ steps.platform.outputs.platform }}"
- case "$PLATFORM" in
- joomla)
- MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1)
- if [ -z "$MANIFEST" ]; then
- echo "::warning::No Joomla manifest found (WaaS site)"
- exit 0
- fi
- echo "Manifest: ${MANIFEST}"
- if command -v php &> /dev/null; then
- php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
- fi
- for ELEMENT in name version description; do
- grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
- done
- echo "Joomla manifest valid"
- ;;
- dolibarr)
- MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
- if [ -z "$MOD_FILE" ]; then
- echo "::error::No mod*.class.php found"
- exit 1
- fi
- echo "Dolibarr module: ${MOD_FILE}"
- ;;
- *)
- echo "Generic platform — no manifest validation"
- ;;
- esac
-
- - name: Check update stream format
- run: |
- PLATFORM="${{ steps.platform.outputs.platform }}"
- case "$PLATFORM" in
- joomla)
- if [ -f "updates.xml" ]; then
- if command -v php &> /dev/null; then
- php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
- fi
- echo "updates.xml valid"
- fi
- ;;
- dolibarr)
- [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
- ;;
- esac
-
- - name: Check changelog has unreleased entry
- run: |
- if [ ! -f "CHANGELOG.md" ]; then
- echo "::warning::No CHANGELOG.md found"
- exit 0
- fi
- # Check for content under [Unreleased] section
- if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
- echo "::error::CHANGELOG.md missing [Unreleased] section"
- exit 1
- fi
- # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
- UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
- if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
- echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
- echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
- echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
- echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
- exit 1
- fi
- echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
-
- - name: Verify package source
- run: |
- SOURCE_DIR="src"
- [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
- if [ ! -d "$SOURCE_DIR" ]; then
- echo "::warning::No src/ or htdocs/ directory"
- exit 0
- fi
- FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
- echo "Source: ${FILE_COUNT} files"
- [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
-
- # ── Pre-Release RC Build ─────────────────────────────────────────────────
- pre-release:
- name: Build RC Package
- runs-on: ubuntu-latest
- needs: [branch-policy, validate]
-
- steps:
- - name: Trigger RC pre-release
- env:
- GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
- REPO: ${{ github.repository }}
- BRANCH: ${{ github.head_ref }}
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- run: |
- curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
- echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
- echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
+# Copyright (C) 2026 Moko Consulting
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# FILE INFORMATION
+# DEFGROUP: Gitea.Workflow
+# INGROUP: moko-platform.CI
+# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
+# PATH: /templates/workflows/universal/pr-check.yml.template
+# VERSION: 09.23.00
+# BRIEF: PR gate — branch policy + code validation before merge
+
+name: "Universal: PR Check"
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened, edited]
+
+permissions:
+ contents: read
+ pull-requests: write
+
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+
+jobs:
+ # ── Branch Policy ──────────────────────────────────────────────────────
+ branch-policy:
+ name: Branch Policy
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check branch merge target
+ run: |
+ HEAD="${{ github.head_ref }}"
+ BASE="${{ github.base_ref }}"
+
+ echo "PR: ${HEAD} → ${BASE}"
+
+ ALLOWED=true
+ REASON=""
+
+ case "$HEAD" in
+ feature/*|feat/*)
+ if [ "$BASE" != "dev" ]; then
+ ALLOWED=false
+ REASON="Feature branches must target 'dev', not '${BASE}'"
+ fi
+ ;;
+ fix/*|bugfix/*)
+ if [ "$BASE" != "dev" ]; then
+ ALLOWED=false
+ REASON="Fix branches must target 'dev', not '${BASE}'"
+ fi
+ ;;
+ patch/*)
+ if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
+ ALLOWED=false
+ REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
+ fi
+ ;;
+ hotfix/*)
+ if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
+ ALLOWED=false
+ REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
+ fi
+ ;;
+ rc)
+ if [ "$BASE" != "main" ]; then
+ ALLOWED=false
+ REASON="RC branch can only merge into 'main', not '${BASE}'"
+ fi
+ ;;
+ dev)
+ if [ "$BASE" != "main" ]; then
+ ALLOWED=false
+ REASON="Dev branch can only merge into 'main', not '${BASE}'"
+ fi
+ ;;
+ esac
+
+ if [ "$ALLOWED" = false ]; then
+ echo "::error::${REASON}"
+ echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "${REASON}" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
+ echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
+ echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
+ echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
+ echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
+ echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
+ exit 1
+ fi
+
+ echo "Branch policy: OK (${HEAD} → ${BASE})"
+ echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
+
+ # ── Code Validation ────────────────────────────────────────────────────
+ validate:
+ name: Validate PR
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Detect platform
+ id: platform
+ run: |
+ # Read platform from XML manifest ( tag) or plain text fallback
+ PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
+ [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
+ [ -z "$PLATFORM" ] && PLATFORM="generic"
+ echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
+
+ - name: Setup PHP
+ if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
+ run: |
+ if ! command -v php &> /dev/null; then
+ sudo apt-get update -qq
+ sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
+ fi
+
+ - name: PHP syntax check
+ if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
+ run: |
+ ERRORS=0
+ while IFS= read -r -d '' file; do
+ if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
+ ERRORS=$((ERRORS + 1))
+ fi
+ done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
+ echo "PHP lint: ${ERRORS} error(s)"
+ [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
+
+ - name: Validate platform manifest
+ run: |
+ PLATFORM="${{ steps.platform.outputs.platform }}"
+ case "$PLATFORM" in
+ joomla)
+ MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1)
+ if [ -z "$MANIFEST" ]; then
+ echo "::warning::No Joomla manifest found (WaaS site)"
+ exit 0
+ fi
+ echo "Manifest: ${MANIFEST}"
+ if command -v php &> /dev/null; then
+ php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
+ fi
+ for ELEMENT in name version description; do
+ grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
+ done
+ echo "Joomla manifest valid"
+ ;;
+ dolibarr)
+ MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
+ if [ -z "$MOD_FILE" ]; then
+ echo "::error::No mod*.class.php found"
+ exit 1
+ fi
+ echo "Dolibarr module: ${MOD_FILE}"
+ ;;
+ *)
+ echo "Generic platform — no manifest validation"
+ ;;
+ esac
+
+ - name: Check update stream format
+ run: |
+ PLATFORM="${{ steps.platform.outputs.platform }}"
+ case "$PLATFORM" in
+ joomla)
+ if [ -f "updates.xml" ]; then
+ if command -v php &> /dev/null; then
+ php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
+ fi
+ echo "updates.xml valid"
+ fi
+ ;;
+ dolibarr)
+ [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
+ ;;
+ esac
+
+ - name: Check changelog has unreleased entry
+ run: |
+ if [ ! -f "CHANGELOG.md" ]; then
+ echo "::warning::No CHANGELOG.md found"
+ exit 0
+ fi
+ # Check for content under [Unreleased] section
+ if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
+ echo "::error::CHANGELOG.md missing [Unreleased] section"
+ exit 1
+ fi
+ # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
+ UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
+ if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
+ echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
+ echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
+ echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
+ echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
+ exit 1
+ fi
+ echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
+
+ - name: Verify package source
+ run: |
+ SOURCE_DIR="src"
+ [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
+ if [ ! -d "$SOURCE_DIR" ]; then
+ echo "::warning::No src/ or htdocs/ directory"
+ exit 0
+ fi
+ FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
+ echo "Source: ${FILE_COUNT} files"
+ [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
+
+ # ── Pre-Release RC Build ─────────────────────────────────────────────────
+ pre-release:
+ name: Build RC Package
+ runs-on: ubuntu-latest
+ needs: [branch-policy, validate]
+
+ steps:
+ - name: Trigger RC pre-release
+ env:
+ GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
+ REPO: ${{ github.repository }}
+ BRANCH: ${{ github.head_ref }}
+ GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
+ run: |
+ curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
+ echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
+ echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
+
+ # ── 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."
--
2.52.0
From 9bc915817a002b8ae6f520a34eb5a65d239c7ae7 Mon Sep 17 00:00:00 2001
From: Moko Consulting
Date: Tue, 2 Jun 2026 20:37:02 +0000
Subject: [PATCH 35/40] chore(ci): add CI issue reporter for auto-filing gate
failures
---
automation/ci-issue-reporter.sh | 237 ++++++++++++++++++++++++++++++++
1 file changed, 237 insertions(+)
create mode 100644 automation/ci-issue-reporter.sh
diff --git a/automation/ci-issue-reporter.sh b/automation/ci-issue-reporter.sh
new file mode 100644
index 0000000..65c47ba
--- /dev/null
+++ b/automation/ci-issue-reporter.sh
@@ -0,0 +1,237 @@
+#!/usr/bin/env bash
+# ============================================================================
+# Copyright (C) 2026 Moko Consulting
+#
+# 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 </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 </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
--
2.52.0
From ad0175319aeba43caeea2ec5bf4194cdcd159361 Mon Sep 17 00:00:00 2001
From: Jonathan Miller
Date: Tue, 2 Jun 2026 15:54:51 -0500
Subject: [PATCH 36/40] fix(ci): add conflict-marker guard to PR check workflow
Prevents PRs with unresolved merge conflict markers from passing
validation. Scans source files and fails with a clear summary.
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.mokogitea/workflows/pr-check.yml | 541 +++++++++++++++---------------
1 file changed, 277 insertions(+), 264 deletions(-)
diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml
index e2c82ef..65063f9 100644
--- a/.mokogitea/workflows/pr-check.yml
+++ b/.mokogitea/workflows/pr-check.yml
@@ -1,264 +1,277 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.CI
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
-# PATH: /templates/workflows/universal/pr-check.yml.template
-# VERSION: 09.23.00
-# BRIEF: PR gate — branch policy + code validation before merge
-
-name: "Universal: PR Check"
-
-on:
- pull_request:
- types: [opened, synchronize, reopened, edited]
-
-permissions:
- contents: read
- pull-requests: write
-
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
-
-jobs:
- # ── Branch Policy ──────────────────────────────────────────────────────
- branch-policy:
- name: Branch Policy
- runs-on: ubuntu-latest
- steps:
- - name: Check branch merge target
- run: |
- HEAD="${{ github.head_ref }}"
- BASE="${{ github.base_ref }}"
-
- echo "PR: ${HEAD} → ${BASE}"
-
- ALLOWED=true
- REASON=""
-
- case "$HEAD" in
- feature/*|feat/*)
- if [ "$BASE" != "dev" ]; then
- ALLOWED=false
- REASON="Feature branches must target 'dev', not '${BASE}'"
- fi
- ;;
- fix/*|bugfix/*)
- if [ "$BASE" != "dev" ]; then
- ALLOWED=false
- REASON="Fix branches must target 'dev', not '${BASE}'"
- fi
- ;;
- patch/*)
- if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
- ALLOWED=false
- REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
- fi
- ;;
- hotfix/*)
- if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
- ALLOWED=false
- REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
- fi
- ;;
- rc)
- if [ "$BASE" != "main" ]; then
- ALLOWED=false
- REASON="RC branch can only merge into 'main', not '${BASE}'"
- fi
- ;;
- dev)
- if [ "$BASE" != "main" ]; then
- ALLOWED=false
- REASON="Dev branch can only merge into 'main', not '${BASE}'"
- fi
- ;;
- esac
-
- if [ "$ALLOWED" = false ]; then
- echo "::error::${REASON}"
- echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "${REASON}" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
- echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
- exit 1
- fi
-
- echo "Branch policy: OK (${HEAD} → ${BASE})"
- echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
-
- # ── Code Validation ────────────────────────────────────────────────────
- validate:
- name: Validate PR
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Detect platform
- id: platform
- run: |
- # Read platform from XML manifest ( tag) or plain text fallback
- PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
- [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
- [ -z "$PLATFORM" ] && PLATFORM="generic"
- echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
-
- - name: Setup PHP
- if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
- run: |
- if ! command -v php &> /dev/null; then
- sudo apt-get update -qq
- sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
- fi
-
- - name: PHP syntax check
- if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
- run: |
- ERRORS=0
- while IFS= read -r -d '' file; do
- if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
- ERRORS=$((ERRORS + 1))
- fi
- done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
- echo "PHP lint: ${ERRORS} error(s)"
- [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
-
- - name: Validate platform manifest
- run: |
- PLATFORM="${{ steps.platform.outputs.platform }}"
- case "$PLATFORM" in
- joomla)
- MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1)
- if [ -z "$MANIFEST" ]; then
- echo "::warning::No Joomla manifest found (WaaS site)"
- exit 0
- fi
- echo "Manifest: ${MANIFEST}"
- if command -v php &> /dev/null; then
- php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
- fi
- for ELEMENT in name version description; do
- grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
- done
- echo "Joomla manifest valid"
- ;;
- dolibarr)
- MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
- if [ -z "$MOD_FILE" ]; then
- echo "::error::No mod*.class.php found"
- exit 1
- fi
- echo "Dolibarr module: ${MOD_FILE}"
- ;;
- *)
- echo "Generic platform — no manifest validation"
- ;;
- esac
-
- - name: Check update stream format
- run: |
- PLATFORM="${{ steps.platform.outputs.platform }}"
- case "$PLATFORM" in
- joomla)
- if [ -f "updates.xml" ]; then
- if command -v php &> /dev/null; then
- php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
- fi
- echo "updates.xml valid"
- fi
- ;;
- dolibarr)
- [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
- ;;
- esac
-
- - name: Check changelog has unreleased entry
- run: |
- if [ ! -f "CHANGELOG.md" ]; then
- echo "::warning::No CHANGELOG.md found"
- exit 0
- fi
- # Check for content under [Unreleased] section
- if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
- echo "::error::CHANGELOG.md missing [Unreleased] section"
- exit 1
- fi
- # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
- UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
- if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
- echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
- echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
- echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
- echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
- exit 1
- fi
- echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
-
- - name: Verify package source
- run: |
- SOURCE_DIR="src"
- [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
- if [ ! -d "$SOURCE_DIR" ]; then
- echo "::warning::No src/ or htdocs/ directory"
- exit 0
- fi
- FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
- echo "Source: ${FILE_COUNT} files"
- [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
-
- # ── Pre-Release RC Build ─────────────────────────────────────────────────
- pre-release:
- name: Build RC Package
- runs-on: ubuntu-latest
- needs: [branch-policy, validate]
-
- steps:
- - name: Trigger RC pre-release
- env:
- GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
- REPO: ${{ github.repository }}
- BRANCH: ${{ github.head_ref }}
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- run: |
- curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
- echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
- echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
-
- # ── 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."
+# Copyright (C) 2026 Moko Consulting
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# FILE INFORMATION
+# DEFGROUP: Gitea.Workflow
+# INGROUP: moko-platform.CI
+# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
+# PATH: /templates/workflows/universal/pr-check.yml.template
+# VERSION: 09.23.00
+# BRIEF: PR gate — branch policy + code validation before merge
+
+name: "Universal: PR Check"
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened, edited]
+
+permissions:
+ contents: read
+ pull-requests: write
+
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+
+jobs:
+ # ── Branch Policy ──────────────────────────────────────────────────────
+ branch-policy:
+ name: Branch Policy
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check branch merge target
+ run: |
+ HEAD="${{ github.head_ref }}"
+ BASE="${{ github.base_ref }}"
+
+ echo "PR: ${HEAD} → ${BASE}"
+
+ ALLOWED=true
+ REASON=""
+
+ case "$HEAD" in
+ feature/*|feat/*)
+ if [ "$BASE" != "dev" ]; then
+ ALLOWED=false
+ REASON="Feature branches must target 'dev', not '${BASE}'"
+ fi
+ ;;
+ fix/*|bugfix/*)
+ if [ "$BASE" != "dev" ]; then
+ ALLOWED=false
+ REASON="Fix branches must target 'dev', not '${BASE}'"
+ fi
+ ;;
+ patch/*)
+ if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
+ ALLOWED=false
+ REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
+ fi
+ ;;
+ hotfix/*)
+ if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
+ ALLOWED=false
+ REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
+ fi
+ ;;
+ rc)
+ if [ "$BASE" != "main" ]; then
+ ALLOWED=false
+ REASON="RC branch can only merge into 'main', not '${BASE}'"
+ fi
+ ;;
+ dev)
+ if [ "$BASE" != "main" ]; then
+ ALLOWED=false
+ REASON="Dev branch can only merge into 'main', not '${BASE}'"
+ fi
+ ;;
+ esac
+
+ if [ "$ALLOWED" = false ]; then
+ echo "::error::${REASON}"
+ echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "${REASON}" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
+ echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
+ echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
+ echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
+ echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
+ echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
+ exit 1
+ fi
+
+ echo "Branch policy: OK (${HEAD} → ${BASE})"
+ echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
+
+ # ── Code Validation ────────────────────────────────────────────────────
+ validate:
+ name: Validate PR
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: 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 in source files"
+ echo "## Conflict Markers Found" >> $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: Detect platform
+ id: platform
+ run: |
+ # Read platform from XML manifest ( tag) or plain text fallback
+ PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
+ [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
+ [ -z "$PLATFORM" ] && PLATFORM="generic"
+ echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
+
+ - name: Setup PHP
+ if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
+ run: |
+ if ! command -v php &> /dev/null; then
+ sudo apt-get update -qq
+ sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
+ fi
+
+ - name: PHP syntax check
+ if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
+ run: |
+ ERRORS=0
+ while IFS= read -r -d '' file; do
+ if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
+ ERRORS=$((ERRORS + 1))
+ fi
+ done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
+ echo "PHP lint: ${ERRORS} error(s)"
+ [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
+
+ - name: Validate platform manifest
+ run: |
+ PLATFORM="${{ steps.platform.outputs.platform }}"
+ case "$PLATFORM" in
+ joomla)
+ MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1)
+ if [ -z "$MANIFEST" ]; then
+ echo "::warning::No Joomla manifest found (WaaS site)"
+ exit 0
+ fi
+ echo "Manifest: ${MANIFEST}"
+ if command -v php &> /dev/null; then
+ php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
+ fi
+ for ELEMENT in name version description; do
+ grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
+ done
+ echo "Joomla manifest valid"
+ ;;
+ dolibarr)
+ MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
+ if [ -z "$MOD_FILE" ]; then
+ echo "::error::No mod*.class.php found"
+ exit 1
+ fi
+ echo "Dolibarr module: ${MOD_FILE}"
+ ;;
+ *)
+ echo "Generic platform — no manifest validation"
+ ;;
+ esac
+
+ - name: Check update stream format
+ run: |
+ PLATFORM="${{ steps.platform.outputs.platform }}"
+ case "$PLATFORM" in
+ joomla)
+ if [ -f "updates.xml" ]; then
+ if command -v php &> /dev/null; then
+ php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
+ fi
+ echo "updates.xml valid"
+ fi
+ ;;
+ dolibarr)
+ [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
+ ;;
+ esac
+
+ - name: Check changelog has unreleased entry
+ run: |
+ if [ ! -f "CHANGELOG.md" ]; then
+ echo "::warning::No CHANGELOG.md found"
+ exit 0
+ fi
+ # Check for content under [Unreleased] section
+ if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
+ echo "::error::CHANGELOG.md missing [Unreleased] section"
+ exit 1
+ fi
+ # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
+ UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
+ if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
+ echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
+ echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
+ echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
+ echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
+ exit 1
+ fi
+ echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
+
+ - name: Verify package source
+ run: |
+ SOURCE_DIR="src"
+ [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
+ if [ ! -d "$SOURCE_DIR" ]; then
+ echo "::warning::No src/ or htdocs/ directory"
+ exit 0
+ fi
+ FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
+ echo "Source: ${FILE_COUNT} files"
+ [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
+
+ # ── Pre-Release RC Build ─────────────────────────────────────────────────
+ pre-release:
+ name: Build RC Package
+ runs-on: ubuntu-latest
+ needs: [branch-policy, validate]
+
+ steps:
+ - name: Trigger RC pre-release
+ env:
+ GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
+ REPO: ${{ github.repository }}
+ BRANCH: ${{ github.head_ref }}
+ GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
+ run: |
+ curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
+ echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
+ echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
+
+ # ── 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."
--
2.52.0
From a701cc3331b5113be2d954ce21dcbca059dea6e0 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Tue, 2 Jun 2026 21:51:38 +0000
Subject: [PATCH 37/40] chore: sync .mokogitea/workflows/pr-check.yml from
moko-platform [skip ci]
---
.mokogitea/workflows/pr-check.yml | 554 +++++++++++++++---------------
1 file changed, 277 insertions(+), 277 deletions(-)
diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml
index 65063f9..0ac0ef1 100644
--- a/.mokogitea/workflows/pr-check.yml
+++ b/.mokogitea/workflows/pr-check.yml
@@ -1,277 +1,277 @@
-# Copyright (C) 2026 Moko Consulting
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# FILE INFORMATION
-# DEFGROUP: Gitea.Workflow
-# INGROUP: moko-platform.CI
-# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
-# PATH: /templates/workflows/universal/pr-check.yml.template
-# VERSION: 09.23.00
-# BRIEF: PR gate — branch policy + code validation before merge
-
-name: "Universal: PR Check"
-
-on:
- pull_request:
- types: [opened, synchronize, reopened, edited]
-
-permissions:
- contents: read
- pull-requests: write
-
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
-
-jobs:
- # ── Branch Policy ──────────────────────────────────────────────────────
- branch-policy:
- name: Branch Policy
- runs-on: ubuntu-latest
- steps:
- - name: Check branch merge target
- run: |
- HEAD="${{ github.head_ref }}"
- BASE="${{ github.base_ref }}"
-
- echo "PR: ${HEAD} → ${BASE}"
-
- ALLOWED=true
- REASON=""
-
- case "$HEAD" in
- feature/*|feat/*)
- if [ "$BASE" != "dev" ]; then
- ALLOWED=false
- REASON="Feature branches must target 'dev', not '${BASE}'"
- fi
- ;;
- fix/*|bugfix/*)
- if [ "$BASE" != "dev" ]; then
- ALLOWED=false
- REASON="Fix branches must target 'dev', not '${BASE}'"
- fi
- ;;
- patch/*)
- if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
- ALLOWED=false
- REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
- fi
- ;;
- hotfix/*)
- if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
- ALLOWED=false
- REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
- fi
- ;;
- rc)
- if [ "$BASE" != "main" ]; then
- ALLOWED=false
- REASON="RC branch can only merge into 'main', not '${BASE}'"
- fi
- ;;
- dev)
- if [ "$BASE" != "main" ]; then
- ALLOWED=false
- REASON="Dev branch can only merge into 'main', not '${BASE}'"
- fi
- ;;
- esac
-
- if [ "$ALLOWED" = false ]; then
- echo "::error::${REASON}"
- echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "${REASON}" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
- echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
- echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
- exit 1
- fi
-
- echo "Branch policy: OK (${HEAD} → ${BASE})"
- echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
-
- # ── Code Validation ────────────────────────────────────────────────────
- validate:
- name: Validate PR
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: 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 in source files"
- echo "## Conflict Markers Found" >> $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: Detect platform
- id: platform
- run: |
- # Read platform from XML manifest ( tag) or plain text fallback
- PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
- [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
- [ -z "$PLATFORM" ] && PLATFORM="generic"
- echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
-
- - name: Setup PHP
- if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
- run: |
- if ! command -v php &> /dev/null; then
- sudo apt-get update -qq
- sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
- fi
-
- - name: PHP syntax check
- if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
- run: |
- ERRORS=0
- while IFS= read -r -d '' file; do
- if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
- ERRORS=$((ERRORS + 1))
- fi
- done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
- echo "PHP lint: ${ERRORS} error(s)"
- [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
-
- - name: Validate platform manifest
- run: |
- PLATFORM="${{ steps.platform.outputs.platform }}"
- case "$PLATFORM" in
- joomla)
- MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1)
- if [ -z "$MANIFEST" ]; then
- echo "::warning::No Joomla manifest found (WaaS site)"
- exit 0
- fi
- echo "Manifest: ${MANIFEST}"
- if command -v php &> /dev/null; then
- php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
- fi
- for ELEMENT in name version description; do
- grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
- done
- echo "Joomla manifest valid"
- ;;
- dolibarr)
- MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
- if [ -z "$MOD_FILE" ]; then
- echo "::error::No mod*.class.php found"
- exit 1
- fi
- echo "Dolibarr module: ${MOD_FILE}"
- ;;
- *)
- echo "Generic platform — no manifest validation"
- ;;
- esac
-
- - name: Check update stream format
- run: |
- PLATFORM="${{ steps.platform.outputs.platform }}"
- case "$PLATFORM" in
- joomla)
- if [ -f "updates.xml" ]; then
- if command -v php &> /dev/null; then
- php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
- fi
- echo "updates.xml valid"
- fi
- ;;
- dolibarr)
- [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
- ;;
- esac
-
- - name: Check changelog has unreleased entry
- run: |
- if [ ! -f "CHANGELOG.md" ]; then
- echo "::warning::No CHANGELOG.md found"
- exit 0
- fi
- # Check for content under [Unreleased] section
- if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
- echo "::error::CHANGELOG.md missing [Unreleased] section"
- exit 1
- fi
- # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
- UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
- if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
- echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
- echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
- echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
- echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
- exit 1
- fi
- echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
-
- - name: Verify package source
- run: |
- SOURCE_DIR="src"
- [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
- if [ ! -d "$SOURCE_DIR" ]; then
- echo "::warning::No src/ or htdocs/ directory"
- exit 0
- fi
- FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
- echo "Source: ${FILE_COUNT} files"
- [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
-
- # ── Pre-Release RC Build ─────────────────────────────────────────────────
- pre-release:
- name: Build RC Package
- runs-on: ubuntu-latest
- needs: [branch-policy, validate]
-
- steps:
- - name: Trigger RC pre-release
- env:
- GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
- REPO: ${{ github.repository }}
- BRANCH: ${{ github.head_ref }}
- GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
- run: |
- curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
- echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
- echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
-
- # ── 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."
+# Copyright (C) 2026 Moko Consulting
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# FILE INFORMATION
+# DEFGROUP: Gitea.Workflow
+# INGROUP: moko-platform.CI
+# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
+# PATH: /templates/workflows/universal/pr-check.yml.template
+# VERSION: 09.23.00
+# BRIEF: PR gate — branch policy + code validation before merge
+
+name: "Universal: PR Check"
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened, edited]
+
+permissions:
+ contents: read
+ pull-requests: write
+
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+
+jobs:
+ # ── Branch Policy ──────────────────────────────────────────────────────
+ branch-policy:
+ name: Branch Policy
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check branch merge target
+ run: |
+ HEAD="${{ github.head_ref }}"
+ BASE="${{ github.base_ref }}"
+
+ echo "PR: ${HEAD} → ${BASE}"
+
+ ALLOWED=true
+ REASON=""
+
+ case "$HEAD" in
+ feature/*|feat/*)
+ if [ "$BASE" != "dev" ]; then
+ ALLOWED=false
+ REASON="Feature branches must target 'dev', not '${BASE}'"
+ fi
+ ;;
+ fix/*|bugfix/*)
+ if [ "$BASE" != "dev" ]; then
+ ALLOWED=false
+ REASON="Fix branches must target 'dev', not '${BASE}'"
+ fi
+ ;;
+ patch/*)
+ if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
+ ALLOWED=false
+ REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
+ fi
+ ;;
+ hotfix/*)
+ if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
+ ALLOWED=false
+ REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
+ fi
+ ;;
+ rc)
+ if [ "$BASE" != "main" ]; then
+ ALLOWED=false
+ REASON="RC branch can only merge into 'main', not '${BASE}'"
+ fi
+ ;;
+ dev)
+ if [ "$BASE" != "main" ]; then
+ ALLOWED=false
+ REASON="Dev branch can only merge into 'main', not '${BASE}'"
+ fi
+ ;;
+ esac
+
+ if [ "$ALLOWED" = false ]; then
+ echo "::error::${REASON}"
+ echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "${REASON}" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
+ echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
+ echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
+ echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
+ echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
+ echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
+ exit 1
+ fi
+
+ echo "Branch policy: OK (${HEAD} → ${BASE})"
+ echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
+
+ # ── Code Validation ────────────────────────────────────────────────────
+ validate:
+ name: Validate PR
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: 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 in source files"
+ echo "## Conflict Markers Found" >> $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: Detect platform
+ id: platform
+ run: |
+ # Read platform from XML manifest ( tag) or plain text fallback
+ PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
+ [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
+ [ -z "$PLATFORM" ] && PLATFORM="generic"
+ echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
+
+ - name: Setup PHP
+ if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
+ run: |
+ if ! command -v php &> /dev/null; then
+ sudo apt-get update -qq
+ sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
+ fi
+
+ - name: PHP syntax check
+ if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
+ run: |
+ ERRORS=0
+ while IFS= read -r -d '' file; do
+ if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
+ ERRORS=$((ERRORS + 1))
+ fi
+ done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
+ echo "PHP lint: ${ERRORS} error(s)"
+ [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
+
+ - name: Validate platform manifest
+ run: |
+ PLATFORM="${{ steps.platform.outputs.platform }}"
+ case "$PLATFORM" in
+ joomla)
+ MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1)
+ if [ -z "$MANIFEST" ]; then
+ echo "::warning::No Joomla manifest found (WaaS site)"
+ exit 0
+ fi
+ echo "Manifest: ${MANIFEST}"
+ if command -v php &> /dev/null; then
+ php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
+ fi
+ for ELEMENT in name version description; do
+ grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
+ done
+ echo "Joomla manifest valid"
+ ;;
+ dolibarr)
+ MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
+ if [ -z "$MOD_FILE" ]; then
+ echo "::error::No mod*.class.php found"
+ exit 1
+ fi
+ echo "Dolibarr module: ${MOD_FILE}"
+ ;;
+ *)
+ echo "Generic platform — no manifest validation"
+ ;;
+ esac
+
+ - name: Check update stream format
+ run: |
+ PLATFORM="${{ steps.platform.outputs.platform }}"
+ case "$PLATFORM" in
+ joomla)
+ if [ -f "updates.xml" ]; then
+ if command -v php &> /dev/null; then
+ php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
+ fi
+ echo "updates.xml valid"
+ fi
+ ;;
+ dolibarr)
+ [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
+ ;;
+ esac
+
+ - name: Check changelog has unreleased entry
+ run: |
+ if [ ! -f "CHANGELOG.md" ]; then
+ echo "::warning::No CHANGELOG.md found"
+ exit 0
+ fi
+ # Check for content under [Unreleased] section
+ if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
+ echo "::error::CHANGELOG.md missing [Unreleased] section"
+ exit 1
+ fi
+ # Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
+ UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
+ if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
+ echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
+ echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
+ echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
+ echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
+ exit 1
+ fi
+ echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
+
+ - name: Verify package source
+ run: |
+ SOURCE_DIR="src"
+ [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
+ if [ ! -d "$SOURCE_DIR" ]; then
+ echo "::warning::No src/ or htdocs/ directory"
+ exit 0
+ fi
+ FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
+ echo "Source: ${FILE_COUNT} files"
+ [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
+
+ # ── Pre-Release RC Build ─────────────────────────────────────────────────
+ pre-release:
+ name: Build RC Package
+ runs-on: ubuntu-latest
+ needs: [branch-policy, validate]
+
+ steps:
+ - name: Trigger RC pre-release
+ env:
+ GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
+ REPO: ${{ github.repository }}
+ BRANCH: ${{ github.head_ref }}
+ GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
+ run: |
+ curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
+ echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
+ echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
+
+ # ── 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."
--
2.52.0
From 98e9d7d8c5494694c34c50bb73167719b878c73b Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Tue, 2 Jun 2026 23:47:20 +0000
Subject: [PATCH 38/40] chore: add .mokogitea/workflows/auto-release.yml from
moko-platform [skip ci]
---
.mokogitea/workflows/auto-release.yml | 283 ++++++++++++++++++++++++++
1 file changed, 283 insertions(+)
create mode 100644 .mokogitea/workflows/auto-release.yml
diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml
new file mode 100644
index 0000000..2325032
--- /dev/null
+++ b/.mokogitea/workflows/auto-release.yml
@@ -0,0 +1,283 @@
+# Copyright (C) 2026 Moko Consulting
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# FILE INFORMATION
+# DEFGROUP: Gitea.Workflow
+# INGROUP: moko-platform.Release
+# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
+# PATH: /templates/workflows/universal/auto-release.yml.template
+# VERSION: 05.00.00
+# BRIEF: Universal build & release � detects platform from manifest.xml
+#
+# +========================================================================+
+# | UNIVERSAL BUILD & RELEASE PIPELINE |
+# +========================================================================+
+# | |
+# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
+# | |
+# | Platform-specific: |
+# | joomla: XML manifest, updates.xml, type-prefixed packages |
+# | dolibarr: mod*.class.php, update.txt, dev version reset |
+# | generic: README-only, no update stream |
+# | |
+# +========================================================================+
+
+name: "Universal: Build & Release"
+
+on:
+ pull_request:
+ types: [opened, closed]
+ branches:
+ - main
+ workflow_dispatch:
+ inputs:
+ action:
+ description: 'Action to perform'
+ required: false
+ type: choice
+ default: release
+ options:
+ - release
+ - promote-rc
+
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+ GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
+ GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
+ GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
+
+permissions:
+ contents: write
+
+jobs:
+ # ── PR Opened → Rename branch to RC and build RC release ─────────────────────
+ promote-rc:
+ name: Promote to RC
+ runs-on: release
+ if: >-
+ (github.event.action == 'opened' && github.event.pull_request.merged != true) ||
+ (github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ token: ${{ secrets.MOKOGITEA_TOKEN }}
+ fetch-depth: 1
+
+ - name: Setup moko-platform tools
+ env:
+ MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
+ MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
+ run: |
+ if ! command -v composer &> /dev/null; then
+ sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
+ fi
+ # Always fetch latest CLI tools — never use stale cache from previous runs
+ rm -rf /tmp/moko-platform-api
+ git clone --depth 1 --branch main --quiet \
+ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
+ /tmp/moko-platform-api
+ cd /tmp/moko-platform-api
+ composer install --no-dev --no-interaction --quiet
+
+ - name: Rename branch to rc
+ run: |
+ php /tmp/moko-platform-api/cli/branch_rename.php \
+ --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}" \
+ --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
+ --pr "${{ github.event.pull_request.number }}"
+
+ - name: Checkout rc and configure git
+ run: |
+ git fetch origin rc
+ git checkout rc
+ git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
+ git config --local user.name "gitea-actions[bot]"
+ git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
+
+ - name: Publish RC release
+ run: |
+ php /tmp/moko-platform-api/cli/release_publish.php \
+ --path . --stability rc --bump minor --branch rc \
+ --token "${{ secrets.MOKOGITEA_TOKEN }}"
+
+ - name: Summary
+ if: always()
+ run: |
+ echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
+ echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY
+
+ # ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
+ release:
+ name: Build & Release Pipeline
+ runs-on: release
+ if: >-
+ github.event.pull_request.merged == true ||
+ (github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ token: ${{ secrets.MOKOGITEA_TOKEN }}
+ fetch-depth: 0
+
+ - name: Configure git for bot pushes
+ run: |
+ git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
+ git config --local user.name "gitea-actions[bot]"
+ git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
+
+ - name: 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
--
2.52.0
From 1150f073b5aaccb720cb887c9cd6d0364b0b78b9 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Wed, 3 Jun 2026 03:00:18 +0000
Subject: [PATCH 39/40] chore: add CHANGELOG.md [skip ci]
---
CHANGELOG.md | 11 +++++++++++
1 file changed, 11 insertions(+)
create mode 100644 CHANGELOG.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..faf4dde
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,11 @@
+
+
+## [Unreleased]
+
+### Changed
+- Migrated all workflow and template paths from `.github/` to `.mokogitea/`
+- Template source paths updated: `templates/gitea/` to `templates/mokogitea/`
+- HCL definition files removed -- Template repos are now the canonical source
+
+### Added
+- `branch-cleanup.yml`: auto-delete merged feature branches after PR merge
--
2.52.0
From c0bb1c8adcb376799027ae633d729d44cbd89313 Mon Sep 17 00:00:00 2001
From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech>
Date: Wed, 3 Jun 2026 03:10:46 +0000
Subject: [PATCH 40/40] chore: sync .mokogitea/workflows/repo-health.yml from
moko-platform [skip ci]
---
.mokogitea/workflows/repo-health.yml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml
index b23d971..d7743f0 100644
--- a/.mokogitea/workflows/repo-health.yml
+++ b/.mokogitea/workflows/repo-health.yml
@@ -41,7 +41,8 @@ permissions:
env:
# Release policy - Repository Variables Only
- RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX
+ # RS_FTP_PATH_SUFFIX removed — MokoGitea handles all releases now
+ RELEASE_REQUIRED_REPO_VARS:
RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX
# Scripts governance policy
--
2.52.0