diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml new file mode 100644 index 0000000..6afd4cd --- /dev/null +++ b/.github/workflows/auto-release.yml @@ -0,0 +1,439 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: GitHub.Workflow +# INGROUP: MokoStandards.Release +# REPO: https://github.com/mokoconsulting-tech/MokoStandards +# PATH: /templates/workflows/shared/auto-release.yml +# VERSION: 04.04.01 +# BRIEF: Unified build & release pipeline — version branch, platform version, badges, tag, release +# +# ╔════════════════════════════════════════════════════════════════════════╗ +# ║ 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 ║ +# ║ ║ +# ╚════════════════════════════════════════════════════════════════════════╝ + +name: Build & Release + +on: + push: + branches: + - main + - master + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +permissions: + contents: write + +jobs: + release: + name: Build & Release Pipeline + runs-on: ubuntu-latest + if: >- + !contains(github.event.head_commit.message, '[skip ci]') && + github.actor != 'github-actions[bot]' + + 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.04 --quiet \ + "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \ + /tmp/mokostandards + cd /tmp/mokostandards + composer install --no-dev --no-interaction --quiet + + # ── 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 "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + # Derive major.minor for branch naming (patches update existing branch) + MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}') + PATCH=$(echo "$VERSION" | awk -F. '{print $3}') + + 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" + if [ "$PATCH" = "00" ]; then + echo "is_minor=true" >> "$GITHUB_OUTPUT" + echo "✅ Version: $VERSION (minor release — full pipeline)" + else + echo "is_minor=false" >> "$GITHUB_OUTPUT" + echo "✅ Version: $VERSION (patch — platform version + badges only)" + fi + + - name: Check if already released + if: steps.version.outputs.skip != 'true' + id: check + run: | + TAG="${{ steps.version.outputs.tag }}" + BRANCH="${{ steps.version.outputs.branch }}" + + TAG_EXISTS=false + BRANCH_EXISTS=false + + git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true + git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true + + echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" + echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT" + + if [ "$TAG_EXISTS" = "true" ] && [ "$BRANCH_EXISTS" = "true" ]; then + echo "already_released=true" >> "$GITHUB_OUTPUT" + else + echo "already_released=false" >> "$GITHUB_OUTPUT" + fi + + # ── SANITY CHECKS ──────────────────────────────────────────────────── + - name: "Sanity: Platform-specific 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 "" >> $GITHUB_STEP_SUMMARY + + # Common checks + if [ ! -f "LICENSE" ]; then + echo "❌ Missing LICENSE file" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "✅ LICENSE" >> $GITHUB_STEP_SUMMARY + fi + + if [ ! -d "src" ]; then + echo "⚠️ No src/ directory" >> $GITHUB_STEP_SUMMARY + else + echo "✅ src/ directory" >> $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 + 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 + 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 + + # Check extension type + TYPE=$(grep -oP ']+type="\K[^"]+' "$MANIFEST" 2>/dev/null) + echo "✅ Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$ERRORS" -gt 0 ]; then + echo "**❌ ${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY + else + echo "**✅ All sanity checks passed**" >> $GITHUB_STEP_SUMMARY + fi + + # ── STEP 2: Create or update version/XX.YY branch ────────────────── + - name: "Step 2: Version branch" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + BRANCH="${{ steps.version.outputs.branch }}" + IS_MINOR="${{ steps.version.outputs.is_minor }}" + if [ "$IS_MINOR" = "true" ]; then + 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 + fi + + # ── STEP 3: Set platform version ─────────────────────────────────── + - name: "Step 3: Set platform version" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.version.outputs.version }}" + php /tmp/mokostandards/api/cli/version_set_platform.php \ + --path . --version "$VERSION" --branch main + + # ── STEP 4: Update version badges ────────────────────────────────── + - name: "Step 4: Update version badges" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.version.outputs.version }}" + find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" | while read -r f; do + if grep -q '\[VERSION:' "$f" 2>/dev/null; then + sed -i "s/\[VERSION:[[:space:]]*[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\]/[VERSION: ${VERSION}]/" "$f" + fi + done + + # ── STEP 5: Write update files (Dolibarr: update.txt / Joomla: update.xml) + - name: "Step 5: Write update files" + 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 + 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 "") + + # 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 4+5 if not in manifest) + if [ -z "$TARGET_PLATFORM" ]; then + TARGET_PLATFORM='' + 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 + fi + + # ── 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" + exit 0 + fi + VERSION="${{ steps.version.outputs.version }}" + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add -A + git commit -m "chore(release): build ${VERSION} [skip ci]" \ + --author="github-actions[bot] " + git push + + # ── STEP 6: Create tag ───────────────────────────────────────────── + - name: "Step 6: Create git tag" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' + run: | + TAG="${{ steps.version.outputs.tag }}" + git tag "$TAG" + git push origin "$TAG" + echo "🏷️ Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY + + # ── STEP 7: Create or update GitHub Release ────────────────────────── + - name: "Step 7: GitHub Release" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' + env: + GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} + run: | + VERSION="${{ steps.version.outputs.version }}" + TAG="${{ steps.version.outputs.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}" + + 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}" \ + --notes-file /tmp/release_notes.md \ + --target "$BRANCH" + echo "🚀 Release created: ${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 + + 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 + fi + + # ── 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 "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY + elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then + echo "## ℹ️ Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "## ✅ Build & Release Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | [View](https://github.com/${{ github.repository }}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY + fi