From 6cc8fdcf9e7a42e88065692012f0ad373179dae6 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com> Date: Tue, 23 Dec 2025 18:55:10 -0600 Subject: [PATCH] Update release_from_version.yml --- .github/workflows/release_from_version.yml | 293 +++------------------ 1 file changed, 38 insertions(+), 255 deletions(-) diff --git a/.github/workflows/release_from_version.yml b/.github/workflows/release_from_version.yml index 03ff0e8..ea2331b 100644 --- a/.github/workflows/release_from_version.yml +++ b/.github/workflows/release_from_version.yml @@ -27,7 +27,6 @@ # BRIEF: Enterprise release pipeline for promoting dev branches, building Joomla artifacts, publishing prereleases, and optionally squashing to main. # NOTE: Designed for Joomla and Dolibarr projects following MokoStandards governance. # -# name: Release from Version Branch Pipeline on: @@ -79,31 +78,28 @@ jobs: set -euo pipefail BRANCH="${GITHUB_REF_NAME}" - echo "Invoked from branch: $BRANCH" - echo "${BRANCH}" | grep -E '^(dev|version)/[0-9]+\.[0-9]+\.[0-9]+$' + echo "Invoked from branch: ${BRANCH}" + + # Enterprise gate: only allow manual runs from dev/.. + echo "${BRANCH}" | grep -E '^dev/[0-9]+\.[0-9]+\.[0-9]+$' VERSION="${BRANCH#dev/}" - VERSION="${VERSION#version/}" DEV_BRANCH="dev/${VERSION}" VERSION_BRANCH="version/${VERSION}" - - # If invoked from an existing version/ branch, treat it as already promoted - if echo "${BRANCH}" | grep -qE '^version/'; then - VERSION_BRANCH="${BRANCH}" - fi TODAY_UTC="$(date -u +%Y-%m-%d)" - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "dev_branch=$DEV_BRANCH" >> "$GITHUB_OUTPUT" - echo "version_branch=$VERSION_BRANCH" >> "$GITHUB_OUTPUT" - echo "today_utc=$TODAY_UTC" >> "$GITHUB_OUTPUT" + echo "version=${VERSION}" >> "${GITHUB_OUTPUT}" + echo "dev_branch=${DEV_BRANCH}" >> "${GITHUB_OUTPUT}" + echo "version_branch=${VERSION_BRANCH}" >> "${GITHUB_OUTPUT}" + echo "today_utc=${TODAY_UTC}" >> "${GITHUB_OUTPUT}" promote_branch: - if: ${{ github.event.inputs.promote_to_version == 'true' && startsWith(github.ref_name, 'dev/') }} name: 01 Promote dev to version branch runs-on: ubuntu-latest needs: guard + if: ${{ github.event.inputs.promote_to_version == 'true' && startsWith(github.ref_name, 'dev/') }} + permissions: contents: write @@ -119,7 +115,7 @@ jobs: 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" + git config --global --add safe.directory "${GITHUB_WORKSPACE}" - name: Enforce branch promotion preconditions run: | @@ -130,13 +126,13 @@ jobs: git fetch origin --prune - if ! git show-ref --verify --quiet "refs/remotes/origin/$SRC"; then - echo "ERROR: origin/$SRC not found." + 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." + if git show-ref --verify --quiet "refs/remotes/origin/${DST}"; then + echo "ERROR: origin/${DST} already exists." exit 1 fi @@ -147,8 +143,8 @@ jobs: SRC="${{ needs.guard.outputs.dev_branch }}" DST="${{ needs.guard.outputs.version_branch }}" - git checkout -B "$DST" "origin/$SRC" - git push origin "$DST" + git checkout -B "${DST}" "origin/${SRC}" + git push origin "${DST}" if [ "${{ github.event.inputs.delete_dev_branch }}" = "true" ]; then git push origin --delete "${SRC}" @@ -156,7 +152,7 @@ jobs: echo "Dev branch retention enabled. Skipping deletion of ${SRC}." fi - echo "Promotion complete: $SRC -> $DST" + echo "Promotion complete: ${SRC} -> ${DST}" normalize_dates: name: 02 Normalize dates on version branch @@ -165,14 +161,17 @@ jobs: - guard - promote_branch + # If promotion is disabled, run normalization directly on dev branch. + if: ${{ always() }} + permissions: contents: write steps: - - name: Checkout version branch + - name: Checkout release working branch uses: actions/checkout@v4 with: - ref: ${{ needs.guard.outputs.version_branch }} + ref: ${{ (github.event.inputs.promote_to_version == 'true' && startsWith(github.ref_name, 'dev/')) && needs.guard.outputs.version_branch || needs.guard.outputs.dev_branch }} fetch-depth: 0 - name: Configure Git identity @@ -180,7 +179,7 @@ jobs: 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" + git config --global --add safe.directory "${GITHUB_WORKSPACE}" - name: Validate repository release prerequisites run: | @@ -201,24 +200,24 @@ jobs: TODAY="${{ needs.guard.outputs.today_utc }}" VERSION="${{ needs.guard.outputs.version }}" - echo "Release version: $VERSION" - echo "Release date (UTC): $TODAY" + 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" + 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 + 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 + sed -i -E "s#^(## \\[${VERSION}\\]) [0-9]{4}-[0-9]{2}-[0-9]{2}#\ ${TODAY}#g" CHANGELOG.md || true fi - name: Commit and push date updates @@ -232,10 +231,12 @@ jobs: git add -A git commit -m "chore(release): normalize dates for ${{ needs.guard.outputs.version }}" - git push origin "HEAD:${{ needs.guard.outputs.version_branch }}" + + TARGET="${{ (github.event.inputs.promote_to_version == 'true' && startsWith(github.ref_name, 'dev/')) && needs.guard.outputs.version_branch || needs.guard.outputs.dev_branch }}" + git push origin "HEAD:${TARGET}" build_update_and_release: - name: 03 Build Joomla ZIP, update update.xml, prerelease + name: 03 Build Joomla ZIP, update updates.xml, prerelease runs-on: ubuntu-latest needs: - guard @@ -249,10 +250,10 @@ jobs: name: release steps: - - name: Checkout version branch + - name: Checkout release working branch uses: actions/checkout@v4 with: - ref: ${{ needs.guard.outputs.version_branch }} + ref: ${{ (github.event.inputs.promote_to_version == 'true' && startsWith(github.ref_name, 'dev/')) && needs.guard.outputs.version_branch || needs.guard.outputs.dev_branch }} fetch-depth: 0 - name: Configure Git identity @@ -260,222 +261,4 @@ jobs: 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 from src - id: build - run: | - set -euo pipefail - - VERSION="${{ needs.guard.outputs.version }}" - REPO="${{ github.event.repository.name }}" - ZIP="${REPO}-${VERSION}.zip" - - test -d src || (echo "ERROR: src directory missing." && exit 1) - - mkdir -p dist - - # Joomla compliant packaging: src contents at ZIP root (no nested src folder) - cd src - zip -r "../dist/$ZIP" . - cd .. - - echo "zip_name=$ZIP" >> "$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: Update update.xml with download URL and sha256 - run: | - set -euo pipefail - - VERSION="${{ needs.guard.outputs.version }}" - TODAY="${{ needs.guard.outputs.today_utc }}" - ZIP="${{ steps.build.outputs.zip_name }}" - SHA="${{ steps.sha.outputs.sha256 }}" - - OWNER="${{ github.repository_owner }}" - REPO="${{ github.event.repository.name }}" - - DOWNLOAD_URL="https://github.com/${OWNER}/${REPO}/releases/download/${VERSION}/${ZIP}" - - echo "Version: $VERSION" - echo "Download URL: $DOWNLOAD_URL" - echo "SHA256: $SHA" - - # If a template exists, instantiate it - # Preferred canonical template location: docs/templates/ - if [ -f "docs/templates/template_update.xml" ]; then - cp -f "docs/templates/template_update.xml" "updates.xml" - elif [ -f "docs/templates/update_template.xml" ]; then - cp -f "docs/templates/update_template.xml" "updates.xml" - fi - - if [ ! -f "updates.xml" ]; then - # Backward compatibility: allow repos that still keep updates.xml - if [ -f "update.xml" ]; then - mv -f "update.xml" "updates.xml" - else - echo "ERROR: updates.xml not found and no template present in docs/templates." - exit 1 - fi - fi - - # Replace common placeholders if present - sed -i "s#{{VERSION}}#${VERSION}#g" updates.xml || true - sed -i "s#{{DATE}}#${TODAY}#g" updates.xml || true - sed -i "s#{{DOWNLOADURL}}#${DOWNLOAD_URL}#g" updates.xml || true - sed -i "s#{{SHA256}}#${SHA}#g" updates.xml || true - sed -i "s#{{ZIP}}#${ZIP}#g" updates.xml || true - - # Also enforce canonical tag replacement inside common XML elements - sed -i "s#[^<]*#${DOWNLOAD_URL}#g" updates.xml || true - sed -i "s#[^<]*#${SHA}#g" updates.xml || true - sed -i "s#[^<]*#${SHA}#g" updates.xml || true - sed -i "s#[^<]*#${VERSION}#g" updates.xml || true - sed -i "s#[^<]*#${TODAY}#g" updates.xml || true - - echo "updates.xml updated." - - - name: Commit update.xml changes (and any related date deltas) to version branch - run: | - set -euo pipefail - - if git diff --quiet; then - echo "No updates.xml changes detected. No commit required." - exit 0 - fi - - git add -A - git commit -m "chore(release): update updates.xml for ${{ needs.guard.outputs.version }}" - git push origin "HEAD:${{ needs.guard.outputs.version_branch }}" - - - 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 - - printf "\n\nAssets:\n- %s\n- update.xml\n- SHA256SUMS.txt\n" "${{ steps.build.outputs.zip_name }}" >> RELEASE_NOTES.md - - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: release-assets - path: | - dist/*.zip - dist/SHA256SUMS.txt - updates.xml - RELEASE_NOTES.md - retention-days: 30 - - - name: Attest build provenance - uses: actions/attest-build-provenance@v2 - with: - subject-path: | - dist/*.zip - dist/SHA256SUMS.txt - - - name: Create GitHub prerelease and attach assets - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ needs.guard.outputs.version }} - name: Prerelease ${{ needs.guard.outputs.version }} - prerelease: true - body_path: RELEASE_NOTES.md - files: | - dist/*.zip - updates.xml - dist/SHA256SUMS.txt - - squash_to_main: - name: 04 Optional squash merge version branch to main - runs-on: ubuntu-latest - needs: - - guard - - build_update_and_release - - if: ${{ github.event.inputs.squash_to_main == 'true' }} - - permissions: - contents: 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 version branch - run: | - set -euo pipefail - git fetch origin --prune - - - name: Squash merge version branch into main - run: | - set -euo pipefail - - VERSION="${{ needs.guard.outputs.version }}" - VBRANCH="origin/${{ needs.guard.outputs.version_branch }}" - - # Governance control: if main is protected from direct pushes, this will fail by design. - # Enforce PR-based merge in that scenario. - - git checkout main - git merge --squash "${VBRANCH}" - - if git diff --cached --quiet; then - echo "No changes to merge from ${VBRANCH}." - exit 0 - fi - - git commit -m "chore(release): squash ${VERSION} into main" - git push origin "HEAD:main" - - - name: Optional delete version branch after squash - run: | - set -euo pipefail - if [ "${{ github.event.inputs.delete_version_branch }}" = "true" ]; then - git push origin --delete "${{ needs.guard.outputs.version_branch }}" - else - echo "Version branch retention enabled. Skipping deletion." - fi + git config --global --add safe.directory "${GITHUB