From 418bb5b9a108302e98d877a396af74fcf466eb4a Mon Sep 17 00:00:00 2001 From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com> Date: Wed, 8 Apr 2026 05:45:53 -0500 Subject: [PATCH] ci: fix manifest parsing + branch-freeze workflow [skip ci] Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/CODEOWNERS | 1 + .github/workflows/auto-release.yml | 124 ++++++++++-------- .github/workflows/branch-freeze.yml | 114 ++++++++++++++++ .github/workflows/changelog-validation.yml | 4 +- .github/workflows/ci-joomla.yml | 4 +- .../workflows/enterprise-firewall-setup.yml | 2 +- .github/workflows/standards-compliance.yml | 4 +- .github/workflows/sync-version-on-merge.yml | 8 +- .github/workflows/update-server.yml | 62 +++++++-- 9 files changed, 247 insertions(+), 76 deletions(-) create mode 100644 .github/workflows/branch-freeze.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0108cc2..e7e6e80 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -27,6 +27,7 @@ /.github/workflows/ci-dolibarr.yml @jmiller-moko /.github/workflows/publish-to-mokodolimods.yml @jmiller-moko /.github/workflows/changelog-validation.yml @jmiller-moko +/.github/workflows/branch-freeze.yml @jmiller-moko # Custom workflows in .github/workflows/ not listed above are repo-owned. # ── GitHub configuration ───────────────────────────────────────────────── diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 4b4706a..62eff0f 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -35,13 +35,14 @@ name: Build & Release on: - push: + pull_request: + types: [closed] branches: - main - - master paths: - 'src/**' - 'htdocs/**' + workflow_dispatch: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true @@ -54,8 +55,7 @@ jobs: name: Build & Release Pipeline runs-on: ubuntu-latest if: >- - !contains(github.event.head_commit.message, '[skip ci]') && - github.actor != 'github-actions[bot]' + github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' steps: - name: Checkout repository @@ -147,7 +147,7 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY # -- Version drift check (must pass before release) -------- - README_VER=$(grep -oP 'VERSION:\s*\K[\d.]+' README.md 2>/dev/null | head -1) + 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)) @@ -156,7 +156,7 @@ jobs: fi # Check CHANGELOG version matches - CL_VER=$(grep -oP 'VERSION:\s*\K[\d.]+' CHANGELOG.md 2>/dev/null | head -1) + 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)) @@ -164,7 +164,7 @@ jobs: # Check composer.json version if present if [ -f "composer.json" ]; then - COMP_VER=$(grep -oP '"version"\s*:\s*"\K[^"]+' composer.json 2>/dev/null | head -1) + 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)) @@ -188,7 +188,7 @@ jobs: # -- Joomla: manifest version drift -------- MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) if [ -n "$MANIFEST" ]; then - XML_VER=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1) + 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)) @@ -205,7 +205,7 @@ jobs: echo "- Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY # -- Joomla: extension type check -------- - TYPE=$(grep -oP ']+type="\K[^"]+' "$MANIFEST" 2>/dev/null) + TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null) echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY fi @@ -275,17 +275,22 @@ jobs: exit 0 fi - EXT_NAME=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}") - EXT_TYPE=$(grep -oP ']+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component") - EXT_ELEMENT=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "") - EXT_CLIENT=$(grep -oP ']+client="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") - EXT_FOLDER=$(grep -oP ']+group="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") - TARGET_PLATFORM=$(grep -oP '' "$MANIFEST" 2>/dev/null | head -1 || echo "") - PHP_MINIMUM=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "") + # Extract fields using sed (portable — no grep -P) + 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) + TARGET_PLATFORM=$(sed -n 's/.*\(\).*/\1/p' "$MANIFEST" | head -1) + PHP_MINIMUM=$(sed -n 's/.*\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1) - # Derive element from manifest filename if not in XML + # Fallbacks + [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}" + [ -z "$EXT_TYPE" ] && EXT_TYPE="component" + + # Templates/modules don't have — derive from (lowercased) if [ -z "$EXT_ELEMENT" ]; then - EXT_ELEMENT=$(basename "$MANIFEST" .xml) + EXT_ELEMENT=$(echo "$EXT_NAME" | tr '[:upper:]' '[:lower:]' | tr -d ' ') fi # Build client tag: plugins and frontend modules need site @@ -341,24 +346,28 @@ jobs: } > /tmp/stable_entry.xml # -- Write updates.xml preserving dev/rc entries ────────────── - RC_ENTRY="" - DEV_ENTRY="" + # Extract existing entries for other stability levels + # Order reflects release workflow: development → alpha → beta → rc → stable if [ -f "updates.xml" ]; then - printf 'import re\n' > /tmp/extract.py + printf 'import re, sys\n' > /tmp/extract.py printf 'with open("updates.xml") as f: c = f.read()\n' >> /tmp/extract.py - printf 'import sys; tag = sys.argv[1]\n' >> /tmp/extract.py + printf 'tag = sys.argv[1]\n' >> /tmp/extract.py printf 'm = re.search(r"( .*?" + re.escape(tag) + r".*?)", c, re.DOTALL)\n' >> /tmp/extract.py printf 'if m: print(m.group(1))\n' >> /tmp/extract.py - RC_ENTRY=$(python3 /tmp/extract.py rc 2>/dev/null || true) - DEV_ENTRY=$(python3 /tmp/extract.py development 2>/dev/null || true) fi + DEV_ENTRY=$(python3 /tmp/extract.py development 2>/dev/null || true) + ALPHA_ENTRY=$(python3 /tmp/extract.py alpha 2>/dev/null || true) + BETA_ENTRY=$(python3 /tmp/extract.py beta 2>/dev/null || true) + RC_ENTRY=$(python3 /tmp/extract.py rc 2>/dev/null || true) { printf '%s\n' '' printf '%s\n' '' - cat /tmp/stable_entry.xml - [ -n "$RC_ENTRY" ] && echo "$RC_ENTRY" [ -n "$DEV_ENTRY" ] && echo "$DEV_ENTRY" + [ -n "$ALPHA_ENTRY" ] && echo "$ALPHA_ENTRY" + [ -n "$BETA_ENTRY" ] && echo "$BETA_ENTRY" + [ -n "$RC_ENTRY" ] && echo "$RC_ENTRY" + cat /tmp/stable_entry.xml printf '%s\n' '' } > updates.xml @@ -468,55 +477,64 @@ jobs: MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1 || true) [ -z "$MANIFEST" ] && exit 0 - EXT_ELEMENT=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml) - PACKAGE_NAME="${EXT_ELEMENT}-${VERSION}.zip" + EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml) + ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" + TAR_NAME="${EXT_ELEMENT}-${VERSION}.tar.gz" - # -- Build install-ready ZIP from src/ ---------------------------- + # -- Build install packages from src/ ---------------------------- SOURCE_DIR="src" [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ — skipping package"; exit 0; } + EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*" + + # ZIP package cd "$SOURCE_DIR" - zip -r "/tmp/${PACKAGE_NAME}" . -x '.ftpignore' 'sftp-config*' '*.ppk' '*.pem' '*.key' '.env*' + zip -r "/tmp/${ZIP_NAME}" . -x $EXCLUDES cd .. - FILESIZE=$(stat -c%s "/tmp/${PACKAGE_NAME}" 2>/dev/null || stat -f%z "/tmp/${PACKAGE_NAME}" 2>/dev/null || echo "unknown") + # tar.gz package + tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \ + --exclude='.ftpignore' --exclude='sftp-config*' \ + --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' . - # -- Calculate SHA-256 ------------------------------------------- - SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1) + 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") - # -- Upload ZIP to the minor release tag ------------------------- - gh release upload "$RELEASE_TAG" "/tmp/${PACKAGE_NAME}" --clobber 2>/dev/null || { - echo "Could not upload with --clobber, retrying..." - gh release upload "$RELEASE_TAG" "/tmp/${PACKAGE_NAME}" 2>/dev/null || true - } + # -- Calculate SHA-256 for both ---------------------------------- + SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1) + SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1) - # -- Update updates.xml with SHA-256 for latest patch ------------- + # -- Upload both to release tag ---------------------------------- + gh release upload "$RELEASE_TAG" "/tmp/${ZIP_NAME}" --clobber 2>/dev/null || true + gh release upload "$RELEASE_TAG" "/tmp/${TAR_NAME}" --clobber 2>/dev/null || true + + # -- Update updates.xml with both download formats --------------- if [ -f "updates.xml" ]; then + ZIP_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}" + TAR_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${TAR_NAME}" + + # Replace downloads block with both formats + SHA + sed -i "s|.*|\n ${ZIP_URL}\n ${TAR_URL}\n |" updates.xml 2>/dev/null || true if grep -q '' updates.xml; then - sed -i "s|.*|sha256:${SHA256}|" updates.xml + sed -i "s|.*|sha256:${SHA256_ZIP}|" updates.xml else - sed -i "s||\n sha256:${SHA256}|" updates.xml + sed -i "s||\n sha256:${SHA256_ZIP}|" updates.xml fi - # Also update the download URL to point to this patch's ZIP - DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}" - sed -i "s|]*>[^<]*|${DOWNLOAD_URL}|" updates.xml - git add updates.xml - git commit -m "chore(release): SHA-256 + download URL for ${VERSION} [skip ci]" \ + git commit -m "chore(release): ZIP + tar.gz for ${VERSION} [skip ci]" \ --author="github-actions[bot] " || true git push || true fi - echo "### Joomla Package" >> $GITHUB_STEP_SUMMARY + echo "### Joomla Packages" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY - echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Package | \`${PACKAGE_NAME}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Size | ${FILESIZE} bytes |" >> $GITHUB_STEP_SUMMARY - echo "| SHA-256 | \`${SHA256}\` |" >> $GITHUB_STEP_SUMMARY - echo "| Release | \`${RELEASE_TAG}\` |" >> $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 | [${PACKAGE_NAME}](https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}) |" >> $GITHUB_STEP_SUMMARY # -- Summary -------------------------------------------------------------- diff --git a/.github/workflows/branch-freeze.yml b/.github/workflows/branch-freeze.yml new file mode 100644 index 0000000..7a908f0 --- /dev/null +++ b/.github/workflows/branch-freeze.yml @@ -0,0 +1,114 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Automation +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /templates/workflows/shared/branch-freeze.yml.template +# VERSION: 04.06.00 +# BRIEF: Freeze or unfreeze any branch via ruleset — manual workflow_dispatch + +name: Branch Freeze + +on: + workflow_dispatch: + inputs: + branch: + description: 'Branch to freeze/unfreeze (e.g., version/04, dev/feature)' + required: true + type: string + action: + description: 'Action to perform' + required: true + type: choice + options: + - freeze + - unfreeze + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +permissions: + contents: read + +jobs: + manage-freeze: + name: "${{ inputs.action }} branch: ${{ inputs.branch }}" + runs-on: ubuntu-latest + + steps: + - name: Check permissions + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + ACTOR="${{ github.actor }}" + REPO="${{ github.repository }}" + PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" \ + --jq '.permission' 2>/dev/null || echo "read") + if [ "$PERMISSION" != "admin" ]; then + echo "Denied: only admins can freeze/unfreeze branches (${ACTOR} has ${PERMISSION})" + exit 1 + fi + + - name: "${{ inputs.action }} branch" + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + BRANCH="${{ inputs.branch }}" + ACTION="${{ inputs.action }}" + REPO="${{ github.repository }}" + RULESET_NAME="FROZEN: ${BRANCH}" + + echo "## Branch Freeze" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "$ACTION" = "freeze" ]; then + # Check if ruleset already exists + EXISTING=$(gh api "repos/${REPO}/rulesets" \ + --jq ".[] | select(.name == \"${RULESET_NAME}\") | .id" 2>/dev/null || true) + + if [ -n "$EXISTING" ]; then + echo "Branch \`${BRANCH}\` is already frozen (ruleset #${EXISTING})" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + # Create freeze ruleset — blocks all updates except admin bypass + printf '{"name":"%s","target":"branch","enforcement":"active",' "${RULESET_NAME}" > /tmp/ruleset.json + printf '"bypass_actors":[{"actor_id":5,"actor_type":"RepositoryRole","bypass_mode":"always"}],' >> /tmp/ruleset.json + printf '"conditions":{"ref_name":{"include":["refs/heads/%s"],"exclude":[]}},' "${BRANCH}" >> /tmp/ruleset.json + printf '"rules":[{"type":"update"},{"type":"deletion"},{"type":"non_fast_forward"}]}' >> /tmp/ruleset.json + + RESULT=$(gh api "repos/${REPO}/rulesets" -X POST --input /tmp/ruleset.json --jq '.id' 2>&1) || true + + if echo "$RESULT" | grep -qE '^[0-9]+$'; then + echo "Frozen \`${BRANCH}\` — ruleset #${RESULT}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${BRANCH}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Ruleset | #${RESULT} |" >> $GITHUB_STEP_SUMMARY + echo "| Rules | No updates, no deletion, no force push |" >> $GITHUB_STEP_SUMMARY + echo "| Bypass | Repository admins only |" >> $GITHUB_STEP_SUMMARY + else + echo "Failed to freeze: ${RESULT}" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + elif [ "$ACTION" = "unfreeze" ]; then + # Find and delete the freeze ruleset + RULESET_ID=$(gh api "repos/${REPO}/rulesets" \ + --jq ".[] | select(.name == \"${RULESET_NAME}\") | .id" 2>/dev/null || true) + + if [ -z "$RULESET_ID" ]; then + echo "Branch \`${BRANCH}\` is not frozen (no ruleset found)" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + gh api "repos/${REPO}/rulesets/${RULESET_ID}" -X DELETE --silent 2>/dev/null + + echo "Unfrozen \`${BRANCH}\` — ruleset #${RULESET_ID} deleted" >> $GITHUB_STEP_SUMMARY + fi + + rm -f /tmp/ruleset.json diff --git a/.github/workflows/changelog-validation.yml b/.github/workflows/changelog-validation.yml index bfd1be3..5521195 100644 --- a/.github/workflows/changelog-validation.yml +++ b/.github/workflows/changelog-validation.yml @@ -16,10 +16,10 @@ name: Changelog Validation on: - push: + pull_request: branches: - main - - version/* + - 'dev/**' workflow_dispatch: permissions: diff --git a/.github/workflows/ci-joomla.yml b/.github/workflows/ci-joomla.yml index fa1b81a..7329a62 100644 --- a/.github/workflows/ci-joomla.yml +++ b/.github/workflows/ci-joomla.yml @@ -16,10 +16,10 @@ name: Joomla Extension CI on: - push: + pull_request: branches: - main - - version/* + - 'dev/**' workflow_dispatch: permissions: diff --git a/.github/workflows/enterprise-firewall-setup.yml b/.github/workflows/enterprise-firewall-setup.yml index 46ef7d2..1a533fb 100644 --- a/.github/workflows/enterprise-firewall-setup.yml +++ b/.github/workflows/enterprise-firewall-setup.yml @@ -90,7 +90,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up Python uses: actions/setup-python@v6 diff --git a/.github/workflows/standards-compliance.yml b/.github/workflows/standards-compliance.yml index 418a297..79aaedd 100644 --- a/.github/workflows/standards-compliance.yml +++ b/.github/workflows/standards-compliance.yml @@ -89,7 +89,9 @@ env: on: push: - branches: [main, version/*] + branches: [main, dev/**, rc/**, version/**] + pull_request: + branches: [main, dev/**, rc/**] workflow_dispatch: permissions: diff --git a/.github/workflows/sync-version-on-merge.yml b/.github/workflows/sync-version-on-merge.yml index 60715f6..4761168 100644 --- a/.github/workflows/sync-version-on-merge.yml +++ b/.github/workflows/sync-version-on-merge.yml @@ -17,10 +17,10 @@ name: Sync Version from README on: - push: + pull_request: + types: [closed] branches: - main - - master workflow_dispatch: inputs: dry_run: @@ -39,6 +39,8 @@ jobs: sync-version: name: Propagate README version runs-on: ubuntu-latest + if: >- + github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' steps: - name: Checkout repository @@ -65,7 +67,7 @@ jobs: composer install --no-dev --no-interaction --quiet - name: Auto-bump patch version - if: ${{ github.event_name == 'push' && github.actor != 'github-actions[bot]' }} + if: ${{ github.event_name != 'workflow_dispatch' && github.actor != 'github-actions[bot]' }} run: | if git diff --name-only HEAD~1 HEAD 2>/dev/null | grep -q '^README\.md$'; then echo "README.md changed in this push — skipping auto-bump" diff --git a/.github/workflows/update-server.yml b/.github/workflows/update-server.yml index 30c8abd..83c8e0d 100644 --- a/.github/workflows/update-server.yml +++ b/.github/workflows/update-server.yml @@ -20,6 +20,16 @@ name: Update Joomla Update Server XML Feed on: + pull_request: + types: [closed] + branches: + - 'dev/**' + - 'alpha/**' + - 'beta/**' + - 'rc/**' + paths: + - 'src/**' + - 'htdocs/**' workflow_dispatch: inputs: stability: @@ -44,6 +54,8 @@ jobs: update-xml: name: Update updates.xml runs-on: ubuntu-latest + if: >- + github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' steps: - name: Checkout repository @@ -99,22 +111,35 @@ jobs: STABILITY="stable" fi - # Parse manifest + # Parse manifest (portable — no grep -P) MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) if [ -z "$MANIFEST" ]; then echo "No Joomla manifest found — skipping" exit 0 fi - EXT_NAME=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}") - EXT_TYPE=$(grep -oP ']+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component") - EXT_ELEMENT=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml) - EXT_CLIENT=$(grep -oP ']+client="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") - EXT_FOLDER=$(grep -oP ']+group="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "") - TARGET_PLATFORM=$(grep -oP '' "$MANIFEST" 2>/dev/null | head -1 || echo "") - PHP_MINIMUM=$(grep -oP '\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "") + # 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" + + # Templates and modules don't have — derive from + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(echo "$EXT_NAME" | tr '[:upper:]' '[:lower:]' | tr -d ' ') + fi + + # Use manifest version if README version is empty + [ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION" - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml) [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '' "/") CLIENT_TAG="" @@ -151,24 +176,31 @@ jobs: DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}" INFO_URL="https://github.com/${REPO}" - # ── Build install-ready ZIP ───────────────────────────────── + # ── 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 '.ftpignore' 'sftp-config*' '*.ppk' '*.pem' '*.key' '.env*' + 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 draft release exists for this major + # Ensure release exists gh release view "$RELEASE_TAG" --json tagName > /dev/null 2>&1 || \ gh release create "$RELEASE_TAG" --title "${RELEASE_TAG} (${DISPLAY_VERSION})" --notes "${STABILITY} release" --prerelease --target main 2>/dev/null || true - # Upload ZIP to the major release + # Upload both formats gh release upload "$RELEASE_TAG" "/tmp/${PACKAGE_NAME}" --clobber 2>/dev/null || true + gh release upload "$RELEASE_TAG" "/tmp/${TAR_NAME}" --clobber 2>/dev/null || true - echo "Package: ${PACKAGE_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY + echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY else SHA256="" fi @@ -188,7 +220,9 @@ jobs: NEW_ENTRY="${NEW_ENTRY} \n" NEW_ENTRY="${NEW_ENTRY} ${INFO_URL}\n" NEW_ENTRY="${NEW_ENTRY} \n" + TAR_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz" NEW_ENTRY="${NEW_ENTRY} ${DOWNLOAD_URL}\n" + NEW_ENTRY="${NEW_ENTRY} ${TAR_URL}\n" NEW_ENTRY="${NEW_ENTRY} \n" [ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} sha256:${SHA256}\n" NEW_ENTRY="${NEW_ENTRY} ${TARGET_PLATFORM}\n"