From 097eca9f22b54bd4dd283989e18c6ffd6f76f9de Mon Sep 17 00:00:00 2001 From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:39:22 -0500 Subject: [PATCH] ci: sync workflows from main Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/auto-assign.yml | 76 +++ .github/workflows/auto-dev-issue.yml | 176 ++++-- .github/workflows/auto-release.yml | 521 ++++++++++-------- .github/workflows/changelog-validation.yml | 101 ++++ .github/workflows/ci-joomla.yml | 391 +++++++++++++ .../workflows/enterprise-firewall-setup.yml | 2 +- .github/workflows/repository-cleanup.yml | 2 +- .github/workflows/sync-version-on-merge.yml | 2 +- .github/workflows/update-server.yml | 236 ++++++++ 9 files changed, 1241 insertions(+), 266 deletions(-) create mode 100644 .github/workflows/auto-assign.yml create mode 100644 .github/workflows/changelog-validation.yml create mode 100644 .github/workflows/ci-joomla.yml create mode 100644 .github/workflows/update-server.yml diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml new file mode 100644 index 0000000..3752b66 --- /dev/null +++ b/.github/workflows/auto-assign.yml @@ -0,0 +1,76 @@ +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Workflows.Shared +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /.github/workflows/auto-assign.yml +# VERSION: 04.05.11 +# BRIEF: Auto-assign jmiller-moko to unassigned issues and PRs every 15 minutes + +name: Auto-Assign Issues & PRs + +on: + issues: + types: [opened] + pull_request_target: + types: [opened] + schedule: + - cron: '0 */12 * * *' + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +jobs: + auto-assign: + name: Assign unassigned issues and PRs + runs-on: ubuntu-latest + + steps: + - name: Assign unassigned issues + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + REPO="${{ github.repository }}" + ASSIGNEE="jmiller-moko" + + echo "## đŸˇī¸ Auto-Assign Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + ASSIGNED_ISSUES=0 + ASSIGNED_PRS=0 + + # Assign unassigned open issues + ISSUES=$(gh api "repos/$REPO/issues?state=open&per_page=100&assignee=none" --jq '.[].number' 2>/dev/null || true) + for NUM in $ISSUES; do + # Skip PRs (the issues endpoint returns PRs too) + IS_PR=$(gh api "repos/$REPO/issues/$NUM" --jq '.pull_request // empty' 2>/dev/null || true) + if [ -z "$IS_PR" ]; then + gh api "repos/$REPO/issues/$NUM/assignees" -X POST -f "assignees[]=$ASSIGNEE" --silent 2>/dev/null && { + ASSIGNED_ISSUES=$((ASSIGNED_ISSUES + 1)) + echo " Assigned issue #$NUM" + } || true + fi + done + + # Assign unassigned open PRs + PRS=$(gh api "repos/$REPO/pulls?state=open&per_page=100" --jq '.[] | select(.assignees | length == 0) | .number' 2>/dev/null || true) + for NUM in $PRS; do + gh api "repos/$REPO/issues/$NUM/assignees" -X POST -f "assignees[]=$ASSIGNEE" --silent 2>/dev/null && { + ASSIGNED_PRS=$((ASSIGNED_PRS + 1)) + echo " Assigned PR #$NUM" + } || true + done + + echo "| Type | Assigned |" >> $GITHUB_STEP_SUMMARY + echo "|------|----------|" >> $GITHUB_STEP_SUMMARY + echo "| Issues | $ASSIGNED_ISSUES |" >> $GITHUB_STEP_SUMMARY + echo "| Pull Requests | $ASSIGNED_PRS |" >> $GITHUB_STEP_SUMMARY + + if [ "$ASSIGNED_ISSUES" -eq 0 ] && [ "$ASSIGNED_PRS" -eq 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ All issues and PRs already have assignees" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/auto-dev-issue.yml b/.github/workflows/auto-dev-issue.yml index c167000..38730ab 100644 --- a/.github/workflows/auto-dev-issue.yml +++ b/.github/workflows/auto-dev-issue.yml @@ -9,14 +9,22 @@ # INGROUP: MokoStandards.Automation # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /templates/workflows/shared/auto-dev-issue.yml.template -# VERSION: 04.05.00 -# BRIEF: Auto-create tracking issue when a dev/** or rc/** branch is pushed +# VERSION: 04.05.13 +# BRIEF: Auto-create tracking issue with sub-issues for dev/rc branch workflow # NOTE: Synced via bulk-repo-sync to .github/workflows/auto-dev-issue.yml in all governed repos. -name: Auto Dev Branch Issue +name: Dev/RC Branch Issue on: + # Auto-create on RC branch creation create: + # Manual trigger for dev branches + workflow_dispatch: + inputs: + branch: + description: 'Branch name (e.g., dev/my-feature or dev/04.06)' + required: true + type: string env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true @@ -30,15 +38,20 @@ jobs: name: Create version tracking issue runs-on: ubuntu-latest if: >- - github.event.ref_type == 'branch' && - (startsWith(github.event.ref, 'dev/') || startsWith(github.event.ref, 'rc/')) + (github.event_name == 'workflow_dispatch') || + (github.event.ref_type == 'branch' && startsWith(github.event.ref, 'rc/')) steps: - - name: Create tracking issue + - name: Create tracking issue and sub-issues env: GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} run: | - BRANCH="${{ github.event.ref }}" + # For manual dispatch, use input; for auto, use event ref + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + BRANCH="${{ inputs.branch }}" + else + BRANCH="${{ github.event.ref }}" + fi REPO="${{ github.repository }}" ACTOR="${{ github.actor }}" NOW=$(date -u '+%Y-%m-%d %H:%M UTC') @@ -58,45 +71,122 @@ jobs: TITLE="${TITLE_PREFIX}(${VERSION}): ${BRANCH_TYPE} tracking for ${BRANCH}" - BODY="## ${BRANCH_TYPE} Branch Created - - | Field | Value | - |-------|-------| - | **Branch** | \`${BRANCH}\` | - | **Version** | \`${VERSION}\` | - | **Type** | ${BRANCH_TYPE} | - | **Created by** | @${ACTOR} | - | **Created at** | ${NOW} | - | **Repository** | \`${REPO}\` | - - ## Checklist - - - [ ] Feature development complete - - [ ] Tests passing - - [ ] README.md version bumped to \`${VERSION}\` - - [ ] CHANGELOG.md updated - - [ ] PR created targeting \`main\` - - [ ] Code reviewed and approved - - [ ] Merged to \`main\` - - --- - *Auto-created by [auto-dev-issue.yml](.github/workflows/auto-dev-issue.yml) on branch creation.*" - - # Dedent heredoc - BODY=$(echo "$BODY" | sed 's/^ //') - # Check for existing issue with same title prefix - EXISTING=$(gh api "repos/${REPO}/issues?state=open&per_page=5" \ + EXISTING=$(gh api "repos/${REPO}/issues?state=open&per_page=10" \ --jq ".[] | select(.title | startswith(\"${TITLE_PREFIX}(${VERSION})\")) | .number" 2>/dev/null | head -1) if [ -n "$EXISTING" ]; then echo "â„šī¸ Issue #${EXISTING} already exists for ${VERSION}" >> $GITHUB_STEP_SUMMARY - else - ISSUE_URL=$(gh issue create \ - --repo "$REPO" \ - --title "$TITLE" \ - --body "$BODY" \ - --label "${LABEL_TYPE},version" \ - --assignee "jmiller-moko" 2>&1) - echo "✅ Created tracking issue: ${ISSUE_URL}" >> $GITHUB_STEP_SUMMARY + exit 0 fi + + # ── Define sub-issues for the dev workflow ──────────────────────── + if [[ "$BRANCH" == rc/* ]]; then + SUB_ISSUES=( + "RC Testing|Verify all features work on rc branch|type: test,release-candidate" + "Regression Testing|Run full regression suite before merge to main|type: test,release-candidate" + "Version Bump|Bump version in README.md and all headers|type: version,release-candidate" + "Changelog Update|Update CHANGELOG.md with release notes|documentation,release-candidate" + "Merge to Main|Create PR from rc branch to main|type: release,needs-review" + ) + else + SUB_ISSUES=( + "Development|Implement feature/fix on dev branch|type: feature,status: in-progress" + "Unit Testing|Write and pass unit tests|type: test,status: pending" + "Code Review|Request and complete code review|needs-review,status: pending" + "Version Bump|Bump version in README.md and all headers|type: version,status: pending" + "Changelog Update|Update CHANGELOG.md with release notes|documentation,status: pending" + "Create RC Branch|Promote dev to rc branch for final testing|type: release,status: pending" + "Merge to Main|Create PR from rc/dev to main|type: release,needs-review,status: pending" + ) + fi + + # ── Create sub-issues first ─────────────────────────────────────── + SUB_LIST="" + SUB_NUMBERS="" + for SUB in "${SUB_ISSUES[@]}"; do + IFS='|' read -r SUB_TITLE SUB_DESC SUB_LABELS <<< "$SUB" + SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}" + + SUB_BODY=$(printf '### %s\n\n%s\n\n| Field | Value |\n|-------|-------|\n| **Parent Branch** | `%s` |\n| **Version** | `%s` |\n\n---\n*Sub-issue of the %s tracking issue for `%s`.*' \ + "$SUB_TITLE" "$SUB_DESC" "$BRANCH" "$VERSION" "$BRANCH_TYPE" "$BRANCH") + + SUB_URL=$(gh issue create \ + --repo "$REPO" \ + --title "$SUB_FULL_TITLE" \ + --body "$SUB_BODY" \ + --label "${SUB_LABELS}" \ + --assignee "jmiller-moko" 2>&1) + + SUB_NUM=$(echo "$SUB_URL" | grep -oE '[0-9]+$') + if [ -n "$SUB_NUM" ]; then + SUB_LIST="${SUB_LIST}\n- [ ] ${SUB_TITLE} (#${SUB_NUM})" + SUB_NUMBERS="${SUB_NUMBERS} #${SUB_NUM}" + fi + sleep 0.3 + done + + # ── Create parent tracking issue ────────────────────────────────── + PARENT_BODY=$(printf '## %s Branch Created\n\n| Field | Value |\n|-------|-------|\n| **Branch** | `%s` |\n| **Version** | `%s` |\n| **Type** | %s |\n| **Created by** | @%s |\n| **Created at** | %s |\n| **Repository** | `%s` |\n\n## Workflow Sub-Issues\n\n%b\n\n---\n*Auto-created by [auto-dev-issue.yml](.github/workflows/auto-dev-issue.yml) on branch creation.*' \ + "$BRANCH_TYPE" "$BRANCH" "$VERSION" "$BRANCH_TYPE" "$ACTOR" "$NOW" "$REPO" "$SUB_LIST") + + PARENT_URL=$(gh issue create \ + --repo "$REPO" \ + --title "$TITLE" \ + --body "$PARENT_BODY" \ + --label "${LABEL_TYPE},version" \ + --assignee "jmiller-moko" 2>&1) + + PARENT_NUM=$(echo "$PARENT_URL" | grep -oE '[0-9]+$') + + # ── Link sub-issues back to parent ──────────────────────────────── + if [ -n "$PARENT_NUM" ]; then + for SUB in "${SUB_ISSUES[@]}"; do + IFS='|' read -r SUB_TITLE _ _ <<< "$SUB" + SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}" + SUB_NUM=$(gh api "repos/${REPO}/issues?state=open&per_page=20" \ + --jq ".[] | select(.title == \"${SUB_FULL_TITLE}\") | .number" 2>/dev/null | head -1) + if [ -n "$SUB_NUM" ]; then + gh api "repos/${REPO}/issues/${SUB_NUM}" -X PATCH \ + -f body="$(gh api "repos/${REPO}/issues/${SUB_NUM}" --jq '.body' 2>/dev/null) + + > **Parent Issue:** #${PARENT_NUM}" --silent 2>/dev/null || true + fi + sleep 0.2 + done + fi + + # ── RC: Create or update draft release ──────────────────────────── + if [[ "$BRANCH" == rc/* ]]; then + MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') + RELEASE_TAG="v${MAJOR}" + DRAFT_EXISTS=$(gh release view "$RELEASE_TAG" --json isDraft -q .isDraft 2>/dev/null || true) + + if [ -z "$DRAFT_EXISTS" ]; then + # No release exists — create draft + gh release create "$RELEASE_TAG" \ + --title "v${MAJOR} (RC: ${VERSION})" \ + --notes "## Release Candidate ${VERSION}\n\nRC branch: \`${BRANCH}\`\nTracking issue: ${PARENT_URL}" \ + --draft \ + --target main 2>/dev/null || true + echo "Draft release created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + elif [ "$DRAFT_EXISTS" = "true" ]; then + # Draft exists — update title + gh release edit "$RELEASE_TAG" \ + --title "v${MAJOR} (RC: ${VERSION})" --draft 2>/dev/null || true + echo "Draft release updated: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + else + # Release exists and is published — set back to draft for RC + gh release edit "$RELEASE_TAG" \ + --title "v${MAJOR} (RC: ${VERSION})" --draft 2>/dev/null || true + echo "Release ${RELEASE_TAG} set to draft for RC" >> $GITHUB_STEP_SUMMARY + fi + fi + + # ── Summary ─────────────────────────────────────────────────────── + echo "## Dev Workflow Issues Created" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Item | Issue |" >> $GITHUB_STEP_SUMMARY + echo "|------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| **Parent** | ${PARENT_URL} |" >> $GITHUB_STEP_SUMMARY + echo "| **Sub-issues** |${SUB_NUMBERS} |" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 5462926..e9c2570 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -6,29 +6,31 @@ # DEFGROUP: GitHub.Workflow # INGROUP: MokoStandards.Release # REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /templates/workflows/shared/auto-release.yml.template -# VERSION: 04.05.00 -# BRIEF: Unified build & release pipeline — version branch, platform version, badges, tag, release +# PATH: /templates/workflows/joomla/auto-release.yml.template +# VERSION: 04.05.13 +# BRIEF: Joomla build & release — ZIP package, update.xml, SHA-256 checksum # -# ╔════════════════════════════════════════════════════════════════════════╗ -# ║ BUILD & RELEASE PIPELINE ║ -# â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•Ŗ -# ║ ║ -# ║ Triggers on push to main (skips bot commits + [skip ci]): ║ -# ║ ║ -# ║ Every push: ║ -# ║ 1. Read version from README.md ║ -# ║ 3. Set platform version (Dolibarr $this->version, Joomla )║ -# ║ 4. Update [VERSION: XX.YY.ZZ] badges in markdown files ║ -# ║ 5. Write update.txt / update.xml ║ -# ║ 6. Create git tag vXX.YY.ZZ ║ -# ║ 7a. Patch: update existing GitHub Release for this minor ║ -# ║ ║ -# ║ Minor releases only (patch == 00): ║ -# ║ 2. Create/update version/XX.YY branch (patches update in-place) ║ -# ║ 7b. Create new GitHub Release ║ -# ║ ║ -# ╚════════════════════════════════════════════════════════════════════════╝ +# +========================================================================+ +# | BUILD & RELEASE PIPELINE (JOOMLA) | +# +========================================================================+ +# | | +# | Triggers on push to main (skips bot commits + [skip ci]): | +# | | +# | Every push: | +# | 1. Read version from README.md | +# | 3. Set platform version (Joomla ) | +# | 4. Update [VERSION: XX.YY.ZZ] badges in markdown files | +# | 5. Write update.xml (Joomla update server XML) | +# | 6. Create git tag vXX.YY.ZZ | +# | 7a. Patch: update existing GitHub Release for this minor | +# | 8. Build ZIP, upload asset, write SHA-256 to update.xml | +# | | +# | Every version change: archives main -> version/XX.YY branch | +# | Patch 00 = development (no release). First release = patch 01. | +# | First release only (patch == 01): | +# | 7b. Create new GitHub Release | +# | | +# +========================================================================+ name: Build & Release @@ -70,13 +72,13 @@ jobs: cd /tmp/mokostandards composer install --no-dev --no-interaction --quiet - # ── STEP 1: Read version ─────────────────────────────────────────── + # -- STEP 1: Read version ----------------------------------------------- - name: "Step 1: Read version from README.md" id: version run: | VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null) if [ -z "$VERSION" ]; then - echo "â­ī¸ No VERSION in README.md — skipping release" + echo "No VERSION in README.md — skipping release" echo "skip=true" >> "$GITHUB_OUTPUT" exit 0 fi @@ -84,24 +86,34 @@ jobs: MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}') PATCH=$(echo "$VERSION" | awk -F. '{print $3}') + MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') + MINOR_NUM=$(echo "$VERSION" | awk -F. '{print $2}') + echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT" echo "branch=version/${MINOR}" >> "$GITHUB_OUTPUT" echo "minor=$MINOR" >> "$GITHUB_OUTPUT" - echo "skip=false" >> "$GITHUB_OUTPUT" + echo "major=$MAJOR" >> "$GITHUB_OUTPUT" + echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT" if [ "$PATCH" = "00" ]; then - echo "is_minor=true" >> "$GITHUB_OUTPUT" - echo "✅ Version: $VERSION (minor release — full pipeline)" - else + echo "skip=true" >> "$GITHUB_OUTPUT" echo "is_minor=false" >> "$GITHUB_OUTPUT" - echo "✅ Version: $VERSION (patch — platform version + badges only)" + echo "Version: $VERSION (patch 00 = development — skipping release)" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + if [ "$PATCH" = "01" ]; then + echo "is_minor=true" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION (first release — full pipeline)" + else + echo "is_minor=false" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION (patch — platform version + badges only)" + fi fi - name: Check if already released if: steps.version.outputs.skip != 'true' id: check run: | - TAG="${{ steps.version.outputs.tag }}" + TAG="${{ steps.version.outputs.release_tag }}" BRANCH="${{ steps.version.outputs.branch }}" TAG_EXISTS=false @@ -119,102 +131,109 @@ jobs: echo "already_released=false" >> "$GITHUB_OUTPUT" fi - # ── SANITY CHECKS ──────────────────────────────────────────────────── - - name: "Sanity: Platform-specific validation" + # -- SANITY CHECKS ------------------------------------------------------- + - name: "Sanity: Pre-release validation" if: >- steps.version.outputs.skip != 'true' && steps.check.outputs.already_released != 'true' run: | VERSION="${{ steps.version.outputs.version }}" - PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null) ERRORS=0 - echo "## 🔍 Pre-Release Sanity Checks" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Platform: \`${PLATFORM}\`" >> $GITHUB_STEP_SUMMARY + echo "## Pre-Release Sanity Checks (Joomla)" >> $GITHUB_STEP_SUMMARY 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) + 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=$(grep -oP 'VERSION:\s*\K[\d.]+' 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=$(grep -oP '"version"\s*:\s*"\K[^"]+' 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 + echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY ERRORS=$((ERRORS+1)) else - echo "✅ LICENSE" >> $GITHUB_STEP_SUMMARY + echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY fi - if [ ! -d "src" ]; then - echo "âš ī¸ No src/ directory" >> $GITHUB_STEP_SUMMARY + if [ ! -d "src" ] && [ ! -d "htdocs" ]; then + echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY else - echo "✅ src/ directory" >> $GITHUB_STEP_SUMMARY + echo "- Source directory present" >> $GITHUB_STEP_SUMMARY fi - # Dolibarr-specific checks - if [ "$PLATFORM" = "crm-module" ]; then - MOD_FILE=$(find src htdocs -path "*/core/modules/mod*.class.php" -print -quit 2>/dev/null) - if [ -z "$MOD_FILE" ]; then - echo "❌ No module descriptor (src/core/modules/mod*.class.php)" >> $GITHUB_STEP_SUMMARY + # -- 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) + if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then + echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY ERRORS=$((ERRORS+1)) else - echo "✅ Module descriptor: \`${MOD_FILE}\`" >> $GITHUB_STEP_SUMMARY - - # Check module number - NUMERO=$(grep -oP '\$this->numero\s*=\s*\K\d+' "$MOD_FILE" 2>/dev/null || echo "0") - if [ "$NUMERO" = "0" ] || [ -z "$NUMERO" ]; then - echo "❌ Module number (\$this->numero) is 0 or not set" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - else - echo "✅ Module number: ${NUMERO}" >> $GITHUB_STEP_SUMMARY - fi - - # Check url_last_version exists - if grep -q 'url_last_version' "$MOD_FILE" 2>/dev/null; then - echo "✅ url_last_version is set" >> $GITHUB_STEP_SUMMARY - else - echo "âš ī¸ url_last_version not set — update checks won't work" >> $GITHUB_STEP_SUMMARY - fi + echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY fi fi - # Joomla-specific checks - if [ "$PLATFORM" = "waas-component" ]; then - MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) - if [ -z "$MANIFEST" ]; then - echo "❌ No Joomla XML manifest found" >> $GITHUB_STEP_SUMMARY - ERRORS=$((ERRORS+1)) - else - echo "✅ Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY + # -- Joomla: XML manifest existence -------- + if [ -z "$MANIFEST" ]; then + echo "- No Joomla XML manifest found" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY - # Check extension type - TYPE=$(grep -oP ']+type="\K[^"]+' "$MANIFEST" 2>/dev/null) - echo "✅ Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY - fi + # -- Joomla: extension type check -------- + TYPE=$(grep -oP ']+type="\K[^"]+' "$MANIFEST" 2>/dev/null) + echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY fi echo "" >> $GITHUB_STEP_SUMMARY if [ "$ERRORS" -gt 0 ]; then - echo "**❌ ${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY + echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY else - echo "**✅ All sanity checks passed**" >> $GITHUB_STEP_SUMMARY + echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY fi - # ── STEP 2: Create or update version/XX.YY branch ────────────────── - - name: "Step 2: Version branch" - if: >- - steps.version.outputs.skip != 'true' && - steps.check.outputs.already_released != '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 }}" - if [ "$IS_MINOR" = "true" ]; then + PATCH="${{ 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 branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY - else - git push origin HEAD:"$BRANCH" --force - echo "📝 Updated branch: ${BRANCH} (patch)" >> $GITHUB_STEP_SUMMARY + echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY fi - # ── STEP 3: Set platform version ─────────────────────────────────── + # -- STEP 3: Set platform version ---------------------------------------- - name: "Step 3: Set platform version" if: >- steps.version.outputs.skip != 'true' && @@ -224,7 +243,7 @@ jobs: php /tmp/mokostandards/api/cli/version_set_platform.php \ --path . --version "$VERSION" --branch main - # ── STEP 4: Update version badges ────────────────────────────────── + # -- STEP 4: Update version badges ---------------------------------------- - name: "Step 4: Update version badges" if: >- steps.version.outputs.skip != 'true' && @@ -237,107 +256,100 @@ jobs: fi done - # ── STEP 5: Write update files (Dolibarr: update.txt / Joomla: update.xml) - - name: "Step 5: Write update files" + # -- STEP 5: Write update.xml (Joomla update server) --------------------- + - name: "Step 5: Write update.xml" if: >- steps.version.outputs.skip != 'true' && steps.check.outputs.already_released != 'true' run: | - PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null) VERSION="${{ steps.version.outputs.version }}" REPO="${{ github.repository }}" - if [ "$PLATFORM" = "crm-module" ]; then - printf '%s' "$VERSION" > update.txt - echo "đŸ“Ļ update.txt: ${VERSION}" >> $GITHUB_STEP_SUMMARY + # -- Parse extension metadata from XML manifest ---------------- + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "Warning: No Joomla XML manifest found — skipping update.xml" >> $GITHUB_STEP_SUMMARY + exit 0 fi - if [ "$PLATFORM" = "waas-component" ]; then - # ── Parse extension metadata from XML manifest ────────────── - MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) - if [ -z "$MANIFEST" ]; then - echo "âš ī¸ No Joomla XML manifest found — skipping update.xml" >> $GITHUB_STEP_SUMMARY - else - 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 "") + 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 "") - # Derive element from manifest filename if not in XML - if [ -z "$EXT_ELEMENT" ]; then - EXT_ELEMENT=$(basename "$MANIFEST" .xml) - fi - - # Build client tag: plugins and frontend modules need site - CLIENT_TAG="" - if [ -n "$EXT_CLIENT" ]; then - CLIENT_TAG="${EXT_CLIENT}" - elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then - CLIENT_TAG="site" - fi - - # Build folder tag for plugins (required for Joomla to match the update) - FOLDER_TAG="" - if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then - FOLDER_TAG="${EXT_FOLDER}" - fi - - # Build targetplatform (fallback to Joomla 5+6 if not in manifest) - if [ -z "$TARGET_PLATFORM" ]; then - TARGET_PLATFORM=$(printf '' "/") - fi - - # Build php_minimum tag - PHP_TAG="" - if [ -n "$PHP_MINIMUM" ]; then - PHP_TAG="${PHP_MINIMUM}" - fi - - DOWNLOAD_URL="https://github.com/${REPO}/releases/download/v${VERSION}/${EXT_ELEMENT}-${VERSION}.zip" - INFO_URL="https://github.com/${REPO}/releases/tag/v${VERSION}" - - # ── Write update.xml (stable release) ─────────────────────── - { - printf '%s\n' '' - printf '%s\n' '' - printf '%s\n' ' ' - printf '%s\n' " ${EXT_NAME}" - printf '%s\n' " ${EXT_NAME} update" - printf '%s\n' " ${EXT_ELEMENT}" - printf '%s\n' " ${EXT_TYPE}" - printf '%s\n' " ${VERSION}" - [ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}" - [ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}" - printf '%s\n' ' ' - printf '%s\n' ' stable' - printf '%s\n' ' ' - printf '%s\n' " ${INFO_URL}" - printf '%s\n' ' ' - printf '%s\n' " ${DOWNLOAD_URL}" - printf '%s\n' ' ' - printf '%s\n' " ${TARGET_PLATFORM}" - [ -n "$PHP_TAG" ] && printf '%s\n' " ${PHP_TAG}" - printf '%s\n' ' Moko Consulting' - printf '%s\n' ' https://mokoconsulting.tech' - printf '%s\n' ' ' - printf '%s\n' '' - } > update.xml - - echo "đŸ“Ļ update.xml: ${VERSION} (stable) — ${EXT_TYPE}/${EXT_ELEMENT}" >> $GITHUB_STEP_SUMMARY - fi + # Derive element from manifest filename if not in XML + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(basename "$MANIFEST" .xml) fi - # ── Commit all changes ───────────────────────────────────────────── + # Build client tag: plugins and frontend modules need site + CLIENT_TAG="" + if [ -n "$EXT_CLIENT" ]; then + CLIENT_TAG="${EXT_CLIENT}" + elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then + CLIENT_TAG="site" + fi + + # Build folder tag for plugins (required for Joomla to match the update) + FOLDER_TAG="" + if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then + FOLDER_TAG="${EXT_FOLDER}" + fi + + # Build targetplatform (fallback to Joomla 5 if not in manifest) + if [ -z "$TARGET_PLATFORM" ]; then + TARGET_PLATFORM=$(printf '' "/") + fi + + # Build php_minimum tag + PHP_TAG="" + if [ -n "$PHP_MINIMUM" ]; then + PHP_TAG="${PHP_MINIMUM}" + fi + + DOWNLOAD_URL="https://github.com/${REPO}/releases/download/v${VERSION}/${EXT_ELEMENT}-${VERSION}.zip" + INFO_URL="https://github.com/${REPO}/releases/tag/v${VERSION}" + + # -- Write update.xml (stable release) -------------------------- + { + printf '%s\n' '' + printf '%s\n' '' + printf '%s\n' ' ' + printf '%s\n' " ${EXT_NAME}" + printf '%s\n' " ${EXT_NAME} update" + printf '%s\n' " ${EXT_ELEMENT}" + printf '%s\n' " ${EXT_TYPE}" + printf '%s\n' " ${VERSION}" + [ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}" + [ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}" + printf '%s\n' ' ' + printf '%s\n' ' stable' + printf '%s\n' ' ' + printf '%s\n' " ${INFO_URL}" + printf '%s\n' ' ' + printf '%s\n' " ${DOWNLOAD_URL}" + printf '%s\n' ' ' + printf '%s\n' " ${TARGET_PLATFORM}" + [ -n "$PHP_TAG" ] && printf '%s\n' " ${PHP_TAG}" + printf '%s\n' ' Moko Consulting' + printf '%s\n' ' https://mokoconsulting.tech' + printf '%s\n' ' ' + printf '%s\n' '' + } > update.xml + + echo "update.xml: ${VERSION} (stable) — ${EXT_TYPE}/${EXT_ELEMENT}" >> $GITHUB_STEP_SUMMARY + + # -- Commit all changes --------------------------------------------------- - 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" + echo "No changes to commit" exit 0 fi VERSION="${{ steps.version.outputs.version }}" @@ -348,18 +360,25 @@ jobs: --author="github-actions[bot] " git push - # ── STEP 6: Create tag ───────────────────────────────────────────── + # -- STEP 6: Create tag --------------------------------------------------- - name: "Step 6: Create git tag" if: >- steps.version.outputs.skip != 'true' && - steps.check.outputs.tag_exists != 'true' + steps.check.outputs.tag_exists != 'true' && + steps.version.outputs.is_minor == 'true' run: | - TAG="${{ steps.version.outputs.tag }}" - git tag "$TAG" - git push origin "$TAG" - echo "đŸˇī¸ Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY + 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 GitHub Release ────────────────────────── + # -- STEP 7: Create or update GitHub Release ------------------------------ - name: "Step 7: GitHub Release" if: >- steps.version.outputs.skip != 'true' && @@ -368,67 +387,129 @@ jobs: GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} run: | VERSION="${{ steps.version.outputs.version }}" - TAG="${{ steps.version.outputs.tag }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" BRANCH="${{ steps.version.outputs.branch }}" - IS_MINOR="${{ steps.version.outputs.is_minor }}" - - # Derive the minor version base (XX.YY.00) - MINOR_BASE=$(echo "$VERSION" | sed 's/\.[0-9]*$/.00/') - MINOR_TAG="v${MINOR_BASE}" + MAJOR="${{ steps.version.outputs.major }}" NOTES=$(php /tmp/mokostandards/api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null) [ -z "$NOTES" ] && NOTES="Release ${VERSION}" echo "$NOTES" > /tmp/release_notes.md - if [ "$IS_MINOR" = "true" ]; then - # Minor release: create new GitHub Release - gh release create "$TAG" \ - --title "${VERSION}" \ + # Check if the major release already exists + EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true) + + if [ -z "$EXISTING" ]; then + # First release for this major + gh release create "$RELEASE_TAG" \ + --title "v${MAJOR} (latest: ${VERSION})" \ --notes-file /tmp/release_notes.md \ --target "$BRANCH" - echo "🚀 Release created: ${VERSION}" >> $GITHUB_STEP_SUMMARY + echo "Release created: ${RELEASE_TAG} (${VERSION})" >> $GITHUB_STEP_SUMMARY else - # Patch release: update the existing minor release with new tag - # Find the latest release for this minor version - EXISTING=$(gh release view "$MINOR_TAG" --json tagName -q .tagName 2>/dev/null || true) - if [ -n "$EXISTING" ]; then - # Update existing release body with patch info - CURRENT_NOTES=$(gh release view "$MINOR_TAG" --json body -q .body 2>/dev/null || true) - { - echo "$CURRENT_NOTES" - echo "" - echo "---" - echo "### Patch ${VERSION}" - echo "" - cat /tmp/release_notes.md - } > /tmp/updated_notes.md + # Append version notes to existing major release + CURRENT_NOTES=$(gh release view "$RELEASE_TAG" --json body -q .body 2>/dev/null || true) + { + echo "$CURRENT_NOTES" + echo "" + echo "---" + echo "### ${VERSION}" + echo "" + cat /tmp/release_notes.md + } > /tmp/updated_notes.md - gh release edit "$MINOR_TAG" \ - --title "${MINOR_BASE} (latest: ${VERSION})" \ - --notes-file /tmp/updated_notes.md - echo "📝 Release updated: ${MINOR_BASE} → patch ${VERSION}" >> $GITHUB_STEP_SUMMARY - else - # No existing minor release found — create one for this patch - gh release create "$TAG" \ - --title "${VERSION}" \ - --notes-file /tmp/release_notes.md - echo "🚀 Release created: ${VERSION} (no minor release found)" >> $GITHUB_STEP_SUMMARY - fi + gh release edit "$RELEASE_TAG" \ + --title "v${MAJOR} (latest: ${VERSION})" \ + --notes-file /tmp/updated_notes.md + echo "Release updated: ${RELEASE_TAG} -> ${VERSION}" >> $GITHUB_STEP_SUMMARY fi - # ── Summary ──────────────────────────────────────────────────────── + # -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------ + # Every patch builds an install-ready ZIP and uploads it to the minor release. + # Result: one Release per minor version with a ZIP for each patch. + - name: "Step 8: Build Joomla package and update checksum" + if: >- + steps.version.outputs.skip != 'true' + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + VERSION="${{ steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + REPO="${{ github.repository }}" + + # All ZIPs upload to the major release tag (vXX) + gh release view "$RELEASE_TAG" --json tagName > /dev/null 2>&1 || { + echo "No release ${RELEASE_TAG} found — skipping ZIP upload" + exit 0 + } + + # Find extension element name from manifest + 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" + + # -- Build install-ready ZIP from src/ ---------------------------- + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ — skipping package"; exit 0; } + + cd "$SOURCE_DIR" + zip -r "/tmp/${PACKAGE_NAME}" . + cd .. + + FILESIZE=$(stat -c%s "/tmp/${PACKAGE_NAME}" 2>/dev/null || stat -f%z "/tmp/${PACKAGE_NAME}" 2>/dev/null || echo "unknown") + + # -- Calculate SHA-256 ------------------------------------------- + SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1) + + # -- 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 + } + + # -- Update update.xml with SHA-256 for latest patch ------------- + if [ -f "update.xml" ]; then + if grep -q '' update.xml; then + sed -i "s|.*|sha256:${SHA256}|" update.xml + else + sed -i "s||\n sha256:${SHA256}|" update.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}|" update.xml + + git add update.xml + git commit -m "chore(release): SHA-256 + download URL for ${VERSION} [skip ci]" \ + --author="github-actions[bot] " || true + git push || true + fi + + echo "### Joomla Package" >> $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 "| Download | [${PACKAGE_NAME}](https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}) |" >> $GITHUB_STEP_SUMMARY + + # -- Summary -------------------------------------------------------------- - name: Pipeline Summary if: always() run: | VERSION="${{ steps.version.outputs.version }}" if [ "${{ steps.version.outputs.skip }}" = "true" ]; then - echo "## â­ī¸ Release Skipped" >> $GITHUB_STEP_SUMMARY + 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 + echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY else echo "" >> $GITHUB_STEP_SUMMARY - echo "## ✅ Build & Release Complete" >> $GITHUB_STEP_SUMMARY + echo "## Build & Release Complete (Joomla)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY echo "|------|--------|" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/changelog-validation.yml b/.github/workflows/changelog-validation.yml new file mode 100644 index 0000000..9ed880a --- /dev/null +++ b/.github/workflows/changelog-validation.yml @@ -0,0 +1,101 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow.Template +# INGROUP: MokoStandards.CI +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /templates/workflows/shared/changelog-validation.yml.template +# VERSION: 04.05.13 +# BRIEF: Validates CHANGELOG.md format and version consistency +# NOTE: Deployed to .github/workflows/changelog-validation.yml in governed repos. + +name: Changelog Validation + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + validate-changelog: + name: Validate CHANGELOG.md + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Check CHANGELOG.md exists + run: | + echo "### Changelog Validation" >> $GITHUB_STEP_SUMMARY + if [ ! -f "CHANGELOG.md" ]; then + echo "CHANGELOG.md not found in repository root." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + echo "CHANGELOG.md exists." >> $GITHUB_STEP_SUMMARY + + - name: Check VERSION header matches README.md + run: | + # Extract version from README.md FILE INFORMATION block + README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1) + if [ -z "$README_VERSION" ]; then + echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # Check that CHANGELOG.md has a matching version header + CHANGELOG_VERSION=$(grep -oP '^\#\#\s*\[\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' CHANGELOG.md | head -1) + if [ -z "$CHANGELOG_VERSION" ]; then + echo "No version header found in CHANGELOG.md (expected \`## [XX.YY.ZZ] - YYYY-MM-DD\`)." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + if [ "$CHANGELOG_VERSION" != "$README_VERSION" ]; then + echo "CHANGELOG latest version \`${CHANGELOG_VERSION}\` does not match README VERSION \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "CHANGELOG version \`${CHANGELOG_VERSION}\` matches README VERSION." >> $GITHUB_STEP_SUMMARY + + - name: Validate conventional changelog format + run: | + ERRORS=0 + + # Check that version entries follow ## [XX.YY.ZZ] - YYYY-MM-DD format + while IFS= read -r LINE; do + if ! echo "$LINE" | grep -qP '^\#\#\s*\[[0-9]{2}\.[0-9]{2}\.[0-9]{2}\]\s*-\s*[0-9]{4}-[0-9]{2}-[0-9]{2}'; then + echo "Malformed version header: \`${LINE}\`" >> $GITHUB_STEP_SUMMARY + echo " Expected format: \`## [XX.YY.ZZ] - YYYY-MM-DD\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + done < <(grep -P '^\#\#\s*\[' CHANGELOG.md) + + ENTRY_COUNT=$(grep -cP '^\#\#\s*\[' CHANGELOG.md || echo "0") + if [ "$ENTRY_COUNT" -eq 0 ]; then + echo "No version entries found in CHANGELOG.md." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Found ${ENTRY_COUNT} version entr(ies) in CHANGELOG.md." >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} format issue(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Changelog format validation passed.**" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/ci-joomla.yml b/.github/workflows/ci-joomla.yml new file mode 100644 index 0000000..d7995d2 --- /dev/null +++ b/.github/workflows/ci-joomla.yml @@ -0,0 +1,391 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow.Template +# INGROUP: MokoStandards.CI +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /templates/workflows/joomla/ci-joomla.yml.template +# VERSION: 04.05.13 +# BRIEF: CI workflow for Joomla extensions — lint, validate, test +# NOTE: Deployed to .github/workflows/ci-joomla.yml in governed Joomla extension repos. + +name: Joomla Extension CI + +on: + push: + branches: + - main + - dev/** + - rc/** + - version/** + pull_request: + branches: + - main + - dev/** + - rc/** + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + lint-and-validate: + name: Lint & Validate + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: '8.2' + extensions: mbstring, xml, zip, gd, curl, json, simplexml + tools: composer:v2 + coverage: none + + - name: Clone MokoStandards + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + git clone --depth 1 --branch version/04.05 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install \ + --no-interaction \ + --prefer-dist \ + --optimize-autoloader + else + echo "No composer.json found — skipping dependency install" + fi + + - name: PHP syntax check + run: | + ERRORS=0 + for DIR in src/ htdocs/; do + if [ -d "$DIR" ]; then + FOUND=1 + while IFS= read -r -d '' FILE; do + OUTPUT=$(php -l "$FILE" 2>&1) + if echo "$OUTPUT" | grep -q "Parse error"; then + echo "::error file=${FILE}::${OUTPUT}" + ERRORS=$((ERRORS + 1)) + fi + done < <(find "$DIR" -name "*.php" -print0) + fi + done + echo "### PHP Syntax Check" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} syntax error(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY + fi + + - name: XML manifest validation + run: | + echo "### XML Manifest Validation" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Find the extension manifest (XML with /dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No Joomla extension manifest found (XML file with \`> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest found: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY + + # Validate well-formed XML + php -r " + \$xml = @simplexml_load_file('$MANIFEST'); + if (\$xml === false) { + echo 'INVALID'; + exit(1); + } + echo 'VALID'; + " > /tmp/xml_result 2>&1 + XML_RESULT=$(cat /tmp/xml_result) + if [ "$XML_RESULT" != "VALID" ]; then + echo "Manifest is not well-formed XML." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY + fi + + # Check required tags: name, version, author, namespace (Joomla 5+) + for TAG in name version author namespace; do + if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then + echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY + fi + done + fi + + if [ "${ERRORS}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${ERRORS} manifest issue(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check language files referenced in manifest + run: | + echo "### Language File Check" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -n "$MANIFEST" ]; then + # Extract language file references from manifest + LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true) + if [ -z "$LANG_FILES" ]; then + echo "No language file references found in manifest — skipping." >> $GITHUB_STEP_SUMMARY + else + while IFS= read -r LANG_FILE; do + LANG_FILE=$(echo "$LANG_FILE" | xargs) + if [ -z "$LANG_FILE" ]; then + continue + fi + # Check in common locations + FOUND=0 + for BASE in "." "src" "htdocs"; do + if [ -f "${BASE}/${LANG_FILE}" ]; then + FOUND=1 + break + fi + done + if [ "$FOUND" -eq 0 ]; then + echo "Missing language file: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Language file present: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY + fi + done <<< "$LANG_FILES" + fi + else + echo "No manifest found — skipping language check." >> $GITHUB_STEP_SUMMARY + fi + + if [ "${ERRORS}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${ERRORS} missing language file(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Language file check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check index.html files in directories + run: | + echo "### Index.html Check" >> $GITHUB_STEP_SUMMARY + MISSING=0 + CHECKED=0 + + for DIR in src/ htdocs/; do + if [ -d "$DIR" ]; then + while IFS= read -r -d '' SUBDIR; do + CHECKED=$((CHECKED + 1)) + if [ ! -f "${SUBDIR}/index.html" ]; then + echo "Missing index.html in: \`${SUBDIR}\`" >> $GITHUB_STEP_SUMMARY + MISSING=$((MISSING + 1)) + fi + done < <(find "$DIR" -type d -print0) + fi + done + + if [ "${CHECKED}" -eq 0 ]; then + echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY + elif [ "${MISSING}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY + fi + + release-readiness: + name: Release Readiness Check + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' && github.base_ref == 'main' + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Validate release readiness + run: | + echo "## Release Readiness" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Extract version from README.md + README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1) + if [ -z "$README_VERSION" ]; then + echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "README version: \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Find the extension manifest + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No Joomla extension manifest found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY + + # Check matches README VERSION + MANIFEST_VERSION=$(grep -oP '\K[^<]+' "$MANIFEST" | head -1) + if [ -z "$MANIFEST_VERSION" ]; then + echo "No \`\` tag in manifest." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + elif [ -n "$README_VERSION" ] && [ "$MANIFEST_VERSION" != "$README_VERSION" ]; then + echo "Manifest version \`${MANIFEST_VERSION}\` does not match README \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest version: \`${MANIFEST_VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Check extension type, element, client attributes + EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) + if [ -z "$EXT_TYPE" ]; then + echo "Missing \`type\` attribute on \`\` tag." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Extension type: \`${EXT_TYPE}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Element check (component/module/plugin name) + HAS_ELEMENT=$(grep -cP '<(element|name)>' "$MANIFEST" 2>/dev/null || echo "0") + if [ "$HAS_ELEMENT" -eq 0 ]; then + echo "Missing \`\` or \`\` in manifest." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + # Client attribute for site/admin modules and plugins + if echo "$EXT_TYPE" | grep -qP "^(module|plugin)$"; then + HAS_CLIENT=$(grep -cP ']*\bclient=' "$MANIFEST" 2>/dev/null || echo "0") + if [ "$HAS_CLIENT" -eq 0 ]; then + echo "Missing \`client\` attribute for ${EXT_TYPE} extension." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + fi + + # Check update.xml exists + if [ -f "update.xml" ] || [ -f "updates.xml" ]; then + echo "Update XML present." >> $GITHUB_STEP_SUMMARY + else + echo "No update.xml found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + # Check CHANGELOG.md exists + if [ -f "CHANGELOG.md" ]; then + echo "CHANGELOG.md present." >> $GITHUB_STEP_SUMMARY + else + echo "No CHANGELOG.md found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ $ERRORS -gt 0 ]; then + echo "**${ERRORS} issue(s) must be resolved before release.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Extension is ready for release.**" >> $GITHUB_STEP_SUMMARY + fi + + test: + name: Tests (PHP ${{ matrix.php }}) + runs-on: ubuntu-latest + needs: lint-and-validate + + strategy: + fail-fast: false + matrix: + php: ['8.2', '8.3'] + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP ${{ matrix.php }} + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.31.0 + with: + php-version: ${{ matrix.php }} + extensions: mbstring, xml, zip, gd, curl, json, simplexml + tools: composer:v2 + coverage: none + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install \ + --no-interaction \ + --prefer-dist \ + --optimize-autoloader + else + echo "No composer.json found — skipping dependency install" + fi + + - name: Run tests + run: | + echo "### Test Results (PHP ${{ matrix.php }})" >> $GITHUB_STEP_SUMMARY + if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then + vendor/bin/phpunit --testdox 2>&1 | tee /tmp/test-output.log + EXIT=${PIPESTATUS[0]} + if [ $EXIT -eq 0 ]; then + echo "All tests passed." >> $GITHUB_STEP_SUMMARY + else + echo "Test failures detected — see log." >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi + exit $EXIT + else + echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/enterprise-firewall-setup.yml b/.github/workflows/enterprise-firewall-setup.yml index 8979107..b6cf7ab 100644 --- a/.github/workflows/enterprise-firewall-setup.yml +++ b/.github/workflows/enterprise-firewall-setup.yml @@ -22,7 +22,7 @@ # INGROUP: MokoStandards.Firewall # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /templates/workflows/shared/enterprise-firewall-setup.yml.template -# VERSION: 04.05.00 +# VERSION: 04.05.13 # BRIEF: Enterprise firewall configuration — generates outbound allow-rules including SFTP deployment server # NOTE: Reads DEV_FTP_HOST / DEV_FTP_PORT variables to include SFTP egress rules alongside HTTPS rules. diff --git a/.github/workflows/repository-cleanup.yml b/.github/workflows/repository-cleanup.yml index e77c279..b078061 100644 --- a/.github/workflows/repository-cleanup.yml +++ b/.github/workflows/repository-cleanup.yml @@ -9,7 +9,7 @@ # INGROUP: MokoStandards.Maintenance # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /templates/workflows/shared/repository-cleanup.yml.template -# VERSION: 04.05.00 +# VERSION: 04.05.13 # BRIEF: Recurring repository maintenance — labels, branches, workflows, logs, doc indexes # NOTE: Synced via bulk-repo-sync to .github/workflows/repository-cleanup.yml in all governed repos. # Runs on the 1st and 15th of each month at 6:00 AM UTC, and on manual dispatch. diff --git a/.github/workflows/sync-version-on-merge.yml b/.github/workflows/sync-version-on-merge.yml index 5c60d37..7b2ef6c 100644 --- a/.github/workflows/sync-version-on-merge.yml +++ b/.github/workflows/sync-version-on-merge.yml @@ -9,7 +9,7 @@ # INGROUP: MokoStandards.Automation # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /templates/workflows/shared/sync-version-on-merge.yml.template -# VERSION: 04.05.00 +# VERSION: 04.05.13 # BRIEF: Auto-bump patch version on every push to main and propagate to all file headers # NOTE: Synced via bulk-repo-sync to .github/workflows/sync-version-on-merge.yml in all governed repos. # README.md is the single source of truth for the repository version. diff --git a/.github/workflows/update-server.yml b/.github/workflows/update-server.yml new file mode 100644 index 0000000..91d9365 --- /dev/null +++ b/.github/workflows/update-server.yml @@ -0,0 +1,236 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Joomla +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /templates/workflows/joomla/update-server.yml.template +# VERSION: 04.05.13 +# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries +# +# Writes update.xml with multiple entries: +# - stable on push to main (from auto-release) +# - rc on push to rc/** +# - development on push to dev/** +# +# Joomla filters by user's "Minimum Stability" setting. + +name: Update Joomla Update Server XML Feed + +on: + push: + branches: + - 'dev/**' + - 'rc/**' + paths: + - 'src/**' + - 'htdocs/**' + workflow_dispatch: + inputs: + stability: + description: 'Stability tag (development, rc, stable)' + required: true + default: 'development' + type: choice + options: + - development + - rc + - stable + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +permissions: + contents: write + +jobs: + update-xml: + name: Update update.xml + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.GH_TOKEN || github.token }} + fetch-depth: 0 + + - name: Setup MokoStandards tools + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}' + run: | + git clone --depth 1 --branch version/04.05 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards 2>/dev/null || true + if [ -d "/tmp/mokostandards" ] && [ -f "/tmp/mokostandards/composer.json" ]; then + cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true + fi + + - name: Generate update.xml entry + run: | + BRANCH="${{ github.ref_name }}" + REPO="${{ github.repository }}" + VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0") + + # Determine stability from branch or input + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + STABILITY="${{ inputs.stability }}" + elif [[ "$BRANCH" == rc/* ]]; then + STABILITY="rc" + elif [[ "$BRANCH" == dev/* ]]; then + STABILITY="development" + else + STABILITY="stable" + fi + + # Parse manifest + 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 "") + + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml) + [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '' "/") + + CLIENT_TAG="" + [ -n "$EXT_CLIENT" ] && CLIENT_TAG="${EXT_CLIENT}" + [ -z "$CLIENT_TAG" ] && ([ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]) && CLIENT_TAG="site" + + 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" + [ "$STABILITY" = "rc" ] && DISPLAY_VERSION="${VERSION}-rc" + [ "$STABILITY" = "development" ] && DISPLAY_VERSION="${VERSION}-dev" + + MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') + RELEASE_TAG="v${MAJOR}" + DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${EXT_ELEMENT}-${VERSION}.zip" + INFO_URL="https://github.com/${REPO}" + + # ── Build the new entry ─────────────────────────────────────── + NEW_ENTRY=$(cat < + ${EXT_NAME} + ${EXT_NAME} (${STABILITY}) + ${EXT_ELEMENT} + ${EXT_TYPE} + ${DISPLAY_VERSION} + $([ -n "$CLIENT_TAG" ] && echo " ${CLIENT_TAG}") + $([ -n "$FOLDER_TAG" ] && echo " ${FOLDER_TAG}") + + ${STABILITY} + + ${INFO_URL} + + ${DOWNLOAD_URL} + + ${TARGET_PLATFORM} + $([ -n "$PHP_TAG" ] && echo " ${PHP_TAG}") + Moko Consulting + https://mokoconsulting.tech + +XMLEOF +) + + # ── Merge into update.xml ───────────────────────────────────── + if [ ! -f "update.xml" ]; then + # Create fresh + printf '%s\n' '' > update.xml + printf '%s\n' '' >> update.xml + echo "$NEW_ENTRY" >> update.xml + printf '%s\n' '' >> update.xml + else + # Remove existing entry for this stability, add new one + # Use python for reliable XML manipulation + python3 -c " +import re, sys + +with open('update.xml', 'r') as f: + content = f.read() + +# Remove existing entry with this stability tag +pattern = r' .*?${STABILITY}.*?\n?' +content = re.sub(pattern, '', content, flags=re.DOTALL) + +# Insert new entry before +new_entry = '''${NEW_ENTRY}''' +content = content.replace('', new_entry + '\n') + +# Clean up empty lines +content = re.sub(r'\n{3,}', '\n\n', content) + +with open('update.xml', 'w') as f: + f.write(content) +" 2>/dev/null || { + # Fallback: just rewrite the whole file if python fails + # Keep existing stable entry if present + STABLE_ENTRY="" + if [ "$STABILITY" != "stable" ] && grep -q 'stable' update.xml; then + STABLE_ENTRY=$(sed -n '//,/<\/update>/{ /stable<\/tag>/,/<\/update>/p; //,/stable<\/tag>/p }' update.xml | sort -u) + fi + RC_ENTRY="" + if [ "$STABILITY" != "rc" ] && grep -q 'rc' update.xml; then + RC_ENTRY=$(python3 -c " +import re +with open('update.xml') as f: c = f.read() +m = re.search(r'(.*?rc.*?)', c, re.DOTALL) +if m: print(m.group(1)) +" 2>/dev/null || true) + fi + DEV_ENTRY="" + if [ "$STABILITY" != "development" ] && grep -q 'development' update.xml; then + DEV_ENTRY=$(python3 -c " +import re +with open('update.xml') as f: c = f.read() +m = re.search(r'(.*?development.*?)', c, re.DOTALL) +if m: print(m.group(1)) +" 2>/dev/null || true) + fi + + { + printf '%s\n' '' + printf '%s\n' '' + [ -n "$STABLE_ENTRY" ] && echo "$STABLE_ENTRY" + [ -n "$RC_ENTRY" ] && echo "$RC_ENTRY" + [ -n "$DEV_ENTRY" ] && echo "$DEV_ENTRY" + echo "$NEW_ENTRY" + printf '%s\n' '' + } > update.xml + } + fi + + # Commit + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add update.xml + git diff --cached --quiet || { + git commit -m "chore: update update.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \ + --author="github-actions[bot] " + git push + } + + 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