diff --git a/.github/workflows/release_from_version.yml b/.github/workflows/release_from_version.yml deleted file mode 100644 index d6b66c1..0000000 --- a/.github/workflows/release_from_version.yml +++ /dev/null @@ -1,533 +0,0 @@ -# -# Copyright (C) 2025 Moko Consulting -# -# This file is part of a Moko Consulting project. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# FILE INFORMATION -# DEFGROUP: GitHub.Workflow -# INGROUP: MokoStandards.Release -# REPO: https://github.com/mokoconsulting-tech/MokoStandards -# PATH: /.github/workflows/release_from_version.yml -# VERSION: 01.00.00 -# BRIEF: Enterprise release pipeline that promotes dev/ to version/, deletes dev branch, builds Joomla artifacts, publishes prereleases, and optionally creates a squash PR to main. -# NOTE: Invocation is restricted to dev/.. branches. -# -name: Release from Version Branch Pipeline - -on: - workflow_dispatch: - inputs: - squash_to_main: - description: "Create a PR that squashes version/ into main (enterprise-safe)" - required: true - default: false - type: boolean - delete_version_branch: - description: "Delete version/ after PR creation (best-effort)" - required: true - default: false - type: boolean - -concurrency: - group: release-from-dev-${{ github.ref_name }} - cancel-in-progress: false - -defaults: - run: - shell: bash - -permissions: - contents: read - -jobs: - guard: - name: 00 Guard and derive release metadata - runs-on: ubuntu-latest - - outputs: - version: ${{ steps.extract.outputs.version }} - source_branch: ${{ steps.extract.outputs.source_branch }} - version_branch: ${{ steps.extract.outputs.version_branch }} - today_utc: ${{ steps.extract.outputs.today_utc }} - - steps: - - name: Validate calling branch and extract version - id: extract - run: | - set -euo pipefail - - BRANCH="${GITHUB_REF_NAME}" - echo "Invoked from branch: ${BRANCH}" - - # Gate: only allow manual runs from dev/.. or rc/.. - echo "${BRANCH}" | grep -E '^(dev|rc)/[0-9]+\.[0-9]+\.[0-9]+$' - - VERSION="${BRANCH#*/}" - SOURCE_BRANCH="${BRANCH}" - VERSION_BRANCH="version/${VERSION}" - TODAY_UTC="$(date -u +%Y-%m-%d)" - - echo "version=${VERSION}" >> "${GITHUB_OUTPUT}" - echo "source_branch=${SOURCE_BRANCH}" >> "${GITHUB_OUTPUT}" - echo "version_branch=${VERSION_BRANCH}" >> "${GITHUB_OUTPUT}" - echo "today_utc=${TODAY_UTC}" >> "${GITHUB_OUTPUT}" - - promote_branch: - name: 01 Promote dev to version branch (mandatory) - runs-on: ubuntu-latest - needs: guard - - permissions: - contents: write - - steps: - - name: Checkout dev branch - uses: actions/checkout@v4 - with: - ref: ${{ needs.guard.outputs.dev_branch }} - fetch-depth: 0 - - - name: Configure Git identity - run: | - set -euo pipefail - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git config --global --add safe.directory "${GITHUB_WORKSPACE}" - - - name: Enforce promotion preconditions - run: | - set -euo pipefail - - SRC="${{ needs.guard.outputs.dev_branch }}" - DST="${{ needs.guard.outputs.version_branch }}" - - git fetch origin --prune - - if ! git show-ref --verify --quiet "refs/remotes/origin/${SRC}"; then - echo "ERROR: origin/${SRC} not found." - exit 1 - fi - - if git show-ref --verify --quiet "refs/remotes/origin/${DST}"; then - echo "ERROR: origin/${DST} already exists." - exit 1 - fi - - - name: Promote dev branch to version branch and delete dev branch - run: | - set -euo pipefail - - SRC="${{ needs.guard.outputs.dev_branch }}" - DST="${{ needs.guard.outputs.version_branch }}" - - git checkout -B "${DST}" "origin/${SRC}" - git push origin "${DST}" - - # Mandatory hygiene: always delete dev/ after promotion. - git push origin --delete "${SRC}" - - echo "Promotion complete: ${SRC} -> ${DST} (dev branch deleted)" - - normalize_dates: - name: 02 Normalize dates on version branch - runs-on: ubuntu-latest - needs: - - guard - - promote_branch - - if: ${{ needs.promote_branch.result == 'success' }} - - permissions: - contents: write - - steps: - - name: Checkout version branch - uses: actions/checkout@v4 - with: - ref: ${{ needs.guard.outputs.version_branch }} - fetch-depth: 0 - - - name: Configure Git identity - run: | - set -euo pipefail - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git config --global --add safe.directory "${GITHUB_WORKSPACE}" - - - name: Validate repository release prerequisites - run: | - set -euo pipefail - test -d src || (echo "ERROR: src directory missing." && exit 1) - test -f CHANGELOG.md || (echo "ERROR: CHANGELOG.md missing." && exit 1) - - VERSION="${{ needs.guard.outputs.version }}" - if ! grep -qE "^## \[${VERSION}\] " CHANGELOG.md; then - echo "ERROR: CHANGELOG.md does not contain a heading for version [${VERSION}]." - exit 1 - fi - - - name: Update dates using repo script when available, otherwise apply baseline updates - run: | - set -euo pipefail - - TODAY="${{ needs.guard.outputs.today_utc }}" - VERSION="${{ needs.guard.outputs.version }}" - - echo "Release version: ${VERSION}" - echo "Release date (UTC): ${TODAY}" - - if [ -f scripts/update_dates.sh ]; then - chmod +x scripts/update_dates.sh - scripts/update_dates.sh "${TODAY}" "${VERSION}" - else - echo "scripts/update_dates.sh not found. Applying baseline date normalization." - - find . -type f -name "*.xml" \ - -not -path "./.git/*" \ - -print0 | while IFS= read -r -d '' f; do - sed -i "s#[^<]*#${TODAY}#g" "${f}" || true - sed -i "s#[^<]*#${TODAY}#g" "${f}" || true - sed -i "s#[^<]*#${TODAY}#g" "${f}" || true - done - - sed -i -E "s#^(## \[${VERSION}\]) [0-9]{4}-[0-9]{2}-[0-9]{2}#\1 ${TODAY}#g" CHANGELOG.md || true - fi - - - name: Commit and push date updates - run: | - set -euo pipefail - - if git diff --quiet; then - echo "No date changes detected. No commit required." - exit 0 - fi - - git add -A - git commit -m "chore(release): normalize dates for ${{ needs.guard.outputs.version }}" - git push origin "HEAD:${{ needs.guard.outputs.version_branch }}" - - build_update_and_release: - name: 03 Build Joomla ZIP, update updates.xml, prerelease - runs-on: ubuntu-latest - needs: - - guard - - normalize_dates - - permissions: - contents: write - id-token: write - attestations: write - - steps: - - name: Checkout version branch - uses: actions/checkout@v4 - with: - ref: ${{ needs.guard.outputs.version_branch }} - fetch-depth: 0 - - - name: Configure Git identity - run: | - set -euo pipefail - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git config --global --add safe.directory "${GITHUB_WORKSPACE}" - - - name: Build Joomla compliant ZIP (template, component, module, plugin) - id: build - run: | - set -euo pipefail - - VERSION="${{ needs.guard.outputs.version }}" - REPO="${{ github.event.repository.name }}" - - test -d src || (echo "ERROR: src directory missing." && exit 1) - - mkdir -p dist - - # Determine extension root inside src. - # - If src contains a single top-level directory, that directory is the extension root. - # - Otherwise, src itself is the extension root. - ROOT="src" - TOP_DIRS="$(find src -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')" - if [ "${TOP_DIRS}" = "1" ]; then - ROOT="$(find src -mindepth 1 -maxdepth 1 -type d -print -quit)" - fi - - echo "Candidate extension root: ${ROOT}" - - # Require a manifest at the root of ROOT. - MANIFEST="" - - # Primary: templateDetails.xml at root - if [ -f "${ROOT}/templateDetails.xml" ]; then - MANIFEST="${ROOT}/templateDetails.xml" - - # Secondary: standard Joomla template layouts - elif [ -f "src/templates/templateDetails.xml" ]; then - MANIFEST="src/templates/templateDetails.xml" - - # Tertiary: namespaced Joomla template layout src/templates//templateDetails.xml - elif find "src/templates" -mindepth 2 -maxdepth 2 -name "templateDetails.xml" -type f | grep -q .; then - MANIFEST="$(find "src/templates" -mindepth 2 -maxdepth 2 -name "templateDetails.xml" -type f | head -n 1)" - - # Fallback: any root-level XML with an element - else - while IFS= read -r -d '' f; do - if grep -qE ']' "${f}"; then - MANIFEST="${f}" - break - fi - done < <(find "${ROOT}" -maxdepth 1 -type f -name "*.xml" -print0) - fi - - if [ -z "${MANIFEST}" ]; then - echo "ERROR: No Joomla manifest XML found at root of ${ROOT}." - echo "Expected templateDetails.xml or a root-level *.xml containing an element." - exit 1 - fi - - echo "Manifest: ${MANIFEST}" - - EXT_TYPE="$(grep -oE ']*type=\"[^\"]+\"' "${MANIFEST}" | head -n 1 | sed -E 's/.*type=\"([^\"]+)\".*/\1/')" - if [ -z "${EXT_TYPE}" ]; then - EXT_TYPE="unknown" - fi - echo "Detected extension type: ${EXT_TYPE}" - - case "${EXT_TYPE}" in - template) - test -f "${ROOT}/templateDetails.xml" || (echo "ERROR: templateDetails.xml missing for template build." && exit 1) - ;; - component) - if ! ls "${ROOT}"/com_*.xml >/dev/null 2>&1; then - echo "WARNING: No com_*.xml manifest found at root. Using detected manifest anyway." - fi - ;; - module) - if ! ls "${ROOT}"/mod_*.xml >/dev/null 2>&1; then - echo "WARNING: No mod_*.xml manifest found at root. Using detected manifest anyway." - fi - ;; - plugin) - : - ;; - *) - echo "WARNING: Extension type could not be determined reliably. Proceeding with generic packaging." - ;; - esac - - ZIP="${REPO}-${VERSION}.zip" - - # Joomla install expectation: the ZIP root is the extension root. - # Zip the CONTENTS of ROOT. - (cd "${ROOT}" && zip -r -X "../dist/${ZIP}" . \ - -x "**/.git/**" \ - -x "**/.github/**" \ - -x "**/.DS_Store" \ - -x "**/__MACOSX/**") - - echo "zip_name=${ZIP}" >> "${GITHUB_OUTPUT}" - echo "root=${ROOT}" >> "${GITHUB_OUTPUT}" - echo "manifest=${MANIFEST}" >> "${GITHUB_OUTPUT}" - echo "ext_type=${EXT_TYPE}" >> "${GITHUB_OUTPUT}" - ls -la dist - - - name: Compute SHA256 for ZIP - id: sha - run: | - set -euo pipefail - ZIP="${{ steps.build.outputs.zip_name }}" - SHA="$(sha256sum "dist/${ZIP}" | awk '{print $1}')" - echo "sha256=${SHA}" >> "${GITHUB_OUTPUT}" - printf "%s %s\n" "${SHA}" "${ZIP}" > dist/SHA256SUMS.txt - cat dist/SHA256SUMS.txt - - - - - name: Create and push annotated tag after final release commit - run: | - set -euo pipefail - - VERSION="${{ needs.guard.outputs.version }}" - - git fetch --tags - - if git rev-parse -q --verify "refs/tags/${VERSION}" >/dev/null; then - echo "ERROR: Tag ${VERSION} already exists." - exit 1 - fi - - git tag -a "${VERSION}" -m "Prerelease ${VERSION}" - git push origin "refs/tags/${VERSION}" - - - name: Generate release notes from CHANGELOG.md - run: | - set -euo pipefail - - VERSION="${{ needs.guard.outputs.version }}" - - awk "/^## \[${VERSION}\]/{flag=1;next}/^## \[/ {flag=0}flag" CHANGELOG.md > RELEASE_NOTES.md || true - - if [ ! -s RELEASE_NOTES.md ]; then - echo "ERROR: Release notes extraction failed for ${VERSION}." - exit 1 - fi - - ZIP_ASSET="${{ steps.build.outputs.zip_name }}" - { - echo "" - echo "Assets:" - echo "- ${ZIP_ASSET}" - - echo "- SHA256SUMS.txt" - } >> RELEASE_NOTES.md - - name: Publish JSON report to job summary (JSON-only, no file) - run: | - set -euo pipefail - - OWNER="${{ github.repository_owner }}" - REPO="${{ github.event.repository.name }}" - VERSION="${{ needs.guard.outputs.version }}" - BRANCH="${{ needs.guard.outputs.version_branch }}" - TAG="${{ needs.guard.outputs.version }}" - TODAY_UTC="${{ needs.guard.outputs.today_utc }}" - ZIP_NAME="${{ steps.build.outputs.zip_name }}" - ZIP_SHA256="${{ steps.sha.outputs.sha256 }}" - EXT_ROOT="${{ steps.build.outputs.root }}" - MANIFEST_PATH="${{ steps.build.outputs.manifest }}" - EXT_TYPE="${{ steps.build.outputs.ext_type }}" - - DOWNLOAD_URL="https://github.com/${OWNER}/${REPO}/releases/download/${VERSION}/${ZIP_NAME}" - - echo "### Release report (JSON)" >> "${GITHUB_STEP_SUMMARY}" - echo "```json" >> "${GITHUB_STEP_SUMMARY}" - - jq -n \ - --arg repository "${{ github.repository }}" \ - --arg version "${VERSION}" \ - --arg branch "${BRANCH}" \ - --arg tag "${TAG}" \ - --arg today_utc "${TODAY_UTC}" \ - --arg commit_sha "${{ github.sha }}" \ - --arg ext_type "${EXT_TYPE}" \ - --arg ext_root "${EXT_ROOT}" \ - --arg manifest_path "${MANIFEST_PATH}" \ - --arg zip_name "${ZIP_NAME}" \ - --arg zip_sha256 "${ZIP_SHA256}" \ - --arg download_url "${DOWNLOAD_URL}" \ - '{ - repository: $repository, - version: $version, - branch: $branch, - tag: $tag, - prerelease: true, - today_utc: $today_utc, - commit_sha: $commit_sha, - joomla: { - extension_type: $ext_type, - extension_root: $ext_root, - manifest_path: $manifest_path - }, - assets: { - zip: { - name: $zip_name, - sha256: $zip_sha256, - download_url: $download_url - }, - - sha256sums: "dist/SHA256SUMS.txt", - release_notes: "RELEASE_NOTES.md" - } - }' >> "${GITHUB_STEP_SUMMARY}" - - echo "```" >> "${GITHUB_STEP_SUMMARY}" - squash_to_main: - name: 04 Optional squash merge version branch to main (PR-based) - runs-on: ubuntu-latest - needs: - - guard - - build_update_and_release - - if: ${{ github.event.inputs.squash_to_main == true }} - - permissions: - contents: write - pull-requests: write - - steps: - - name: Checkout main - uses: actions/checkout@v4 - with: - ref: main - fetch-depth: 0 - - - name: Configure Git identity - run: | - set -euo pipefail - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git config --global --add safe.directory "${GITHUB_WORKSPACE}" - - - name: Fetch branches - run: | - set -euo pipefail - git fetch origin --prune - - - name: Create squash PR targeting main - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - VERSION="${{ needs.guard.outputs.version }}" - MERGE_BRANCH="merge/${VERSION}" - SOURCE_REF="origin/${{ needs.guard.outputs.version_branch }}" - - git checkout main - git pull --ff-only origin main - - if git show-ref --verify --quiet "refs/heads/${MERGE_BRANCH}"; then - git branch -D "${MERGE_BRANCH}" - fi - - git checkout -b "${MERGE_BRANCH}" main - git merge --squash "${SOURCE_REF}" - - if git diff --cached --quiet; then - echo "No changes to merge from ${SOURCE_REF}." - exit 0 - fi - - git commit -m "chore(release): squash ${VERSION} into main" - git push -u origin "${MERGE_BRANCH}" - - gh pr create \ - --base main \ - --head "${MERGE_BRANCH}" \ - --title "Release ${VERSION} (squash)" \ - --body "Squash merge prepared by release pipeline." \ - || echo "PR may already exist for ${MERGE_BRANCH}." - - - name: Optional delete version branch after PR creation - run: | - set -euo pipefail - if [ "${{ github.event.inputs.delete_version_branch }}" = "true" ]; then - git push origin --delete "${{ needs.guard.outputs.version_branch }}" || true - else - echo "Version branch retention enabled. Skipping deletion." - fi