From a9181197d1de31b2bf6b804360fb91fcdff652ce Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 28 May 2026 13:39:38 -0500 Subject: [PATCH] feat(workflows): append stability suffix to manifest version fields All pre-release streams (dev, alpha, beta, rc) now write the suffix directly into manifest tags (e.g., 01.02.20-dev) instead of only showing it in updates.xml. Zip filenames also include the suffix. Stable releases on main are unaffected. Updated: auto-bump.yml, update-server.yml, pre-release.yml Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .mokogitea/workflows/auto-bump.yml | 9 +- .mokogitea/workflows/pre-release.yml | 23 +- .mokogitea/workflows/update-server.yml | 586 +++++++------------------ 3 files changed, 174 insertions(+), 444 deletions(-) diff --git a/.mokogitea/workflows/auto-bump.yml b/.mokogitea/workflows/auto-bump.yml index bfbd29a..770093f 100644 --- a/.mokogitea/workflows/auto-bump.yml +++ b/.mokogitea/workflows/auto-bump.yml @@ -68,6 +68,13 @@ jobs: --path . --version "$VERSION" --branch dev 2>/dev/null || true php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + # Append -dev suffix to all manifest tags + find . -maxdepth 4 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" \ + -exec grep -l "${VERSION}" {} \; 2>/dev/null | while read f; do + sed -i "s|${VERSION}|${VERSION}-dev|g" "$f" + done + VERSION="${VERSION}-dev" + # Commit if anything changed if git diff --quiet && git diff --cached --quiet; then echo "No version changes to commit" @@ -78,7 +85,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): auto-bump patch ${VERSION} [skip ci]" \ --author="gitea-actions[bot] " git push origin dev echo "Bumped to ${VERSION}" >> $GITHUB_STEP_SUMMARY diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml index cc48cc5..1e4970b 100644 --- a/.mokogitea/workflows/pre-release.yml +++ b/.mokogitea/workflows/pre-release.yml @@ -76,15 +76,14 @@ jobs: run: | STABILITY="${{ inputs.stability || 'development' }}" - # Map stability to Gitea release tag case "$STABILITY" in - development) TAG="development" ;; - alpha) TAG="alpha" ;; - beta) TAG="beta" ;; - release-candidate) TAG="release-candidate" ;; + 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 (includes suffix from manifest, e.g. 01.02.14-dev) + # 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" @@ -94,6 +93,15 @@ jobs: # Verify version consistency across all files php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + # Append suffix to all manifest tags + if [ -n "$SUFFIX" ]; then + find . -maxdepth 4 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" \ + -exec grep -l "${VERSION}" {} \; 2>/dev/null | while read f; do + sed -i "s|${VERSION}|${VERSION}${SUFFIX}|g" "$f" + done + 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]" @@ -117,11 +125,12 @@ jobs: 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} ===" + echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" - name: Create release id: release diff --git a/.mokogitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml index b011f1b..5242537 100644 --- a/.mokogitea/workflows/update-server.yml +++ b/.mokogitea/workflows/update-server.yml @@ -4,18 +4,16 @@ # # FILE INFORMATION # DEFGROUP: Gitea.Workflow -# INGROUP: MokoStandards.Universal +# INGROUP: moko-platform.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) +# VERSION: 05.00.00 +# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches # -# 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/** +# Thin wrapper around moko-platform CLI tools. +# Builds packages, updates updates.xml, and optionally deploys via SFTP. # -# Joomla filters by user's "Minimum Stability" setting. +# Joomla filters update entries by the user's "Minimum Stability" setting. name: "Update Server" @@ -66,7 +64,7 @@ permissions: jobs: update-xml: - name: Update updates.xml + name: Update Server runs-on: release if: >- github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push' @@ -97,28 +95,29 @@ jobs: 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: Generate updates.xml entry - id: update + - 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 }}" - 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) + # Auto-bump patch version 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 + php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true - # Determine stability from branch or input + VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0") + + # Propagate version to all manifest files + php ${MOKO_CLI}/version_set_platform.php --path . --version "$VERSION" --branch "$BRANCH" 2>/dev/null || true + php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true + + # Determine stability from branch or manual input if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then STABILITY="${{ inputs.stability }}" elif [[ "$BRANCH" == rc/* ]]; then @@ -127,252 +126,92 @@ jobs: STABILITY="beta" elif [[ "$BRANCH" == alpha/* ]]; then STABILITY="alpha" - elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then + else 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 already includes suffix from manifest (e.g. 01.02.14-dev) - # No separate DISPLAY_VERSION needed - - MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') - - # Each stability level has its own release tag + # Version suffix per stability stream case "$STABILITY" in - development) RELEASE_TAG="development" ;; - alpha) RELEASE_TAG="alpha" ;; - beta) RELEASE_TAG="beta" ;; - rc) RELEASE_TAG="release-candidate" ;; - *) RELEASE_TAG="v${MAJOR}" ;; + 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 - PACKAGE_NAME="${EXT_ELEMENT}-${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}-${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} (${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="" + # Append suffix to all manifest tags (non-stable only) + if [ -n "$SUFFIX" ]; then + find . -maxdepth 4 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" \ + -exec grep -l "${VERSION}" {} \; 2>/dev/null | while read f; do + sed -i "s|${VERSION}|${VERSION}${SUFFIX}|g" "$f" + done + VERSION="${VERSION}${SUFFIX}" 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} " + 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" - # -- 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 + # Commit version bump if changed + git add -A git diff --cached --quiet || { - git commit -m "chore: update updates.xml (${STABILITY}: ${VERSION}) [skip ci]" \ + git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \ --author="gitea-actions[bot] " git push } - # -- Sync updates.xml to main (for non-main branches) ---------------------- + - 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.GA_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.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 updates.xml + 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 ${STABILITY} channel ${VERSION} [skip ci]" + git push + } + - name: Sync updates.xml to main - if: github.ref_name != 'main' + if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla' run: | API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" GA_TOKEN="${{ secrets.GA_TOKEN }}" @@ -388,7 +227,7 @@ jobs: payload = json.dumps({ 'content': content, 'sha': '${FILE_SHA}', - 'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]', + 'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]', 'branch': 'main' }).encode() req = urllib.request.Request( @@ -402,198 +241,73 @@ jobs: 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 + print(f'WARNING: sync to main failed: {e}', file=sys.stderr) + " fi - - name: Validate updates.xml integrity + - 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: | - ERRORS=0 + # Permission check: admin or maintain role required + ACTOR="${{ github.actor }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - if [ ! -f "updates.xml" ]; then - echo "::error::updates.xml not found" - exit 1 + 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 - # 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)) + 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 - - 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 + rm -f /tmp/deploy_key /tmp/sftp-config.json + echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY - name: Summary if: always() run: | - echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY + 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 | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY