diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c70eb15..e7e6e80 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,7 +11,7 @@ # ── Synced workflows (managed by MokoStandards — do not edit manually) ──── /.github/workflows/deploy-dev.yml @jmiller-moko /.github/workflows/deploy-demo.yml @jmiller-moko -/.github/workflows/deploy-rs.yml @jmiller-moko +/.github/workflows/deploy-manual.yml @jmiller-moko /.github/workflows/auto-release.yml @jmiller-moko /.github/workflows/auto-dev-issue.yml @jmiller-moko /.github/workflows/auto-assign.yml @jmiller-moko @@ -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-assign.yml b/.github/workflows/auto-assign.yml index 08d0dab..d0b70f6 100644 --- a/.github/workflows/auto-assign.yml +++ b/.github/workflows/auto-assign.yml @@ -6,7 +6,7 @@ # INGROUP: MokoStandards.Workflows.Shared # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /.github/workflows/auto-assign.yml -# VERSION: 03.09.03 +# VERSION: 04.06.00 # BRIEF: Auto-assign jmiller-moko to unassigned issues and PRs every 15 minutes name: Auto-Assign Issues & PRs diff --git a/.github/workflows/auto-dev-issue.yml b/.github/workflows/auto-dev-issue.yml index 75c1dae..9b5fbe2 100644 --- a/.github/workflows/auto-dev-issue.yml +++ b/.github/workflows/auto-dev-issue.yml @@ -9,7 +9,7 @@ # INGROUP: MokoStandards.Automation # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /templates/workflows/shared/auto-dev-issue.yml.template -# VERSION: 03.09.03 +# VERSION: 04.06.00 # 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. diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 52c01c1..62eff0f 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -7,7 +7,7 @@ # INGROUP: MokoStandards.Release # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /templates/workflows/joomla/auto-release.yml.template -# VERSION: 03.09.03 +# VERSION: 04.06.00 # BRIEF: Joomla build & release — ZIP package, updates.xml, SHA-256 checksum # # +========================================================================+ @@ -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' + 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 67dfc76..5521195 100644 --- a/.github/workflows/changelog-validation.yml +++ b/.github/workflows/changelog-validation.yml @@ -9,19 +9,17 @@ # INGROUP: MokoStandards.CI # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /templates/workflows/shared/changelog-validation.yml.template -# VERSION: 03.09.03 +# VERSION: 04.06.00 # 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 + - 'dev/**' workflow_dispatch: permissions: diff --git a/.github/workflows/ci-joomla.yml b/.github/workflows/ci-joomla.yml index 861770c..7329a62 100644 --- a/.github/workflows/ci-joomla.yml +++ b/.github/workflows/ci-joomla.yml @@ -9,24 +9,17 @@ # INGROUP: MokoStandards.CI # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /templates/workflows/joomla/ci-joomla.yml.template -# VERSION: 03.09.03 +# VERSION: 04.06.00 # 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/** + - 'dev/**' workflow_dispatch: permissions: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1639497..72cacae 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -21,14 +21,7 @@ on: push: branches: - main - - dev/** - - rc/** - - version/** - pull_request: - branches: - - main - - dev/** - - rc/** + - version/* schedule: # Weekly on Monday at 06:00 UTC - cron: '0 6 * * 1' diff --git a/.github/workflows/deploy-manual.yml b/.github/workflows/deploy-manual.yml index de5143c..e127f0e 100644 --- a/.github/workflows/deploy-manual.yml +++ b/.github/workflows/deploy-manual.yml @@ -7,7 +7,7 @@ # INGROUP: MokoStandards.Deploy # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /templates/workflows/joomla/deploy-manual.yml.template -# VERSION: 03.09.03 +# VERSION: 04.06.00 # BRIEF: Manual SFTP deploy to dev server for Joomla repos # NOTE: Joomla repos use update.xml for distribution. This is for manual # dev server testing only — triggered via workflow_dispatch. diff --git a/.github/workflows/enterprise-firewall-setup.yml b/.github/workflows/enterprise-firewall-setup.yml index 8f8d13c..1a533fb 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: 03.09.03 +# VERSION: 04.06.00 # 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. @@ -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/repo_health.yml b/.github/workflows/repo_health.yml index ffd52dd..73308be 100644 --- a/.github/workflows/repo_health.yml +++ b/.github/workflows/repo_health.yml @@ -10,7 +10,7 @@ # INGROUP: MokoStandards.Validation # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /.github/workflows/repo_health.yml -# VERSION: 03.09.03 +# VERSION: 04.06.00 # BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts. # NOTE: Field is user-managed. # ============================================================================ diff --git a/.github/workflows/repository-cleanup.yml b/.github/workflows/repository-cleanup.yml index ea4c07b..ea9219d 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: 03.09.03 +# VERSION: 04.06.00 # 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. @@ -154,6 +154,10 @@ jobs: ".github/workflows/auto-version-branch.yml" ".github/workflows/publish-to-mokodolibarr.yml" ".github/workflows/ci.yml" + ".github/workflows/deploy-rs.yml" + "sftp-config.json" + "sftp-config.json.template" + "scripts/sftp-config" ) DELETED=0 diff --git a/.github/workflows/standards-compliance.yml b/.github/workflows/standards-compliance.yml index 24ab00d..79aaedd 100644 --- a/.github/workflows/standards-compliance.yml +++ b/.github/workflows/standards-compliance.yml @@ -5,7 +5,7 @@ # INGROUP: MokoStandards.Compliance # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /.github/workflows/standards-compliance.yml -# VERSION: 03.09.03 +# VERSION: 04.06.00 # BRIEF: MokoStandards compliance validation workflow # NOTE: Validates repository structure, documentation, and coding standards diff --git a/.github/workflows/sync-version-on-merge.yml b/.github/workflows/sync-version-on-merge.yml index 59acc9e..4761168 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: 03.09.03 +# VERSION: 04.06.00 # 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. @@ -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 c0becfa..83c8e0d 100644 --- a/.github/workflows/update-server.yml +++ b/.github/workflows/update-server.yml @@ -7,7 +7,7 @@ # INGROUP: MokoStandards.Joomla # REPO: https://github.com/mokoconsulting-tech/MokoStandards # PATH: /templates/workflows/joomla/update-server.yml.template -# VERSION: 03.09.03 +# VERSION: 04.06.00 # BRIEF: Update Joomla update server XML feed with stable/rc/dev entries # # Writes updates.xml with multiple entries: @@ -20,7 +20,8 @@ name: Update Joomla Update Server XML Feed on: - push: + pull_request: + types: [closed] branches: - 'dev/**' - 'alpha/**' @@ -53,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 @@ -108,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="" @@ -160,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' + 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 @@ -197,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" diff --git a/updates.xml b/updates.xml index 3298aac..3709a8d 100644 --- a/updates.xml +++ b/updates.xml @@ -1,184 +1,21 @@ - - - + - - - MokoCassiopeia - Moko Consulting's site template based on Cassiopeia. - mokocassiopeia - template - site - - 03.09.07 - 2026-04-07 - Jonathan Miller || Moko Consulting - hello@mokoconsulting.tech - (C)GNU General Public License Version 3 - 2026 Moko Consulting - - https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/v03 - - - https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.09.07.zip - - 16a7ac98dd6e26144618a4ba534ea8fae0d115ec8373712743ab6daff0960916 - + + update + + component + stable - - Moko Consulting - https://mokoconsulting.tech - - - - - - - MokoCassiopeia - MokoCassiopeia release candidate — testing only. - mokocassiopeia - template - site - - 03.09.07 - 2026-04-07 - Jonathan Miller || Moko Consulting - hello@mokoconsulting.tech - (C)GNU General Public License Version 3 - 2026 Moko Consulting - - https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/release-candidate - + https://github.com/mokoconsulting-tech/MokoCassiopeia - https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/release-candidate/mokocassiopeia-rc.zip + https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v/-.zip + https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v/-.tar.gz - 31e660078e728e8c9177b5a2d75efd89fea1fd4e9320d77444ab8fe28d3b354d - - - rc - - + Moko Consulting https://mokoconsulting.tech - - - - - - MokoCassiopeia - MokoCassiopeia development build — unstable. - mokocassiopeia - template - site - - 03.09.08 - 2026-04-07 - Jonathan Miller || Moko Consulting - hello@mokoconsulting.tech - (C)GNU General Public License Version 3 - 2026 Moko Consulting - - https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/development - - - https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/development/mokocassiopeia-03.09.08-dev.zip - - ecff187531e65a40ae958ae91fff74da0c8856d1cc13e17a6e3d6905806b189e - - - development - - - Moko Consulting - https://mokoconsulting.tech - - - - - - - MokoCassiopeia - MokoCassiopeia alpha build — early testing. - mokocassiopeia - template - site - - 03.09.07 - 2026-04-07 - Jonathan Miller || Moko Consulting - hello@mokoconsulting.tech - (C)GNU General Public License Version 3 - 2026 Moko Consulting - - https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/alpha - - - https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/alpha/mokocassiopeia-03.09.07-alpha.zip - - 1a32180f8b26749bf5daf0602262e33464bcb3a042a8ff51ec2844cdeef2f9e5 - - - alpha - - - Moko Consulting - https://mokoconsulting.tech - - - - - - - MokoCassiopeia - MokoCassiopeia beta build — feature complete, stability testing. - mokocassiopeia - template - site - - 03.09.07 - 2026-04-07 - Jonathan Miller || Moko Consulting - hello@mokoconsulting.tech - (C)GNU General Public License Version 3 - 2026 Moko Consulting - - https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/tag/beta - - - https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/beta/mokocassiopeia-03.09.07-beta.zip - - 1a32180f8b26749bf5daf0602262e33464bcb3a042a8ff51ec2844cdeef2f9e5 - - - beta - - - Moko Consulting - https://mokoconsulting.tech - - - -