Create release #106
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Create release | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| repository: | |
| required: true | |
| type: string | |
| description: "Target repository in owner/repo format (e.g. pimcore/pimcore)" | |
| base_branch: | |
| required: true | |
| type: string | |
| description: "Base branch to take latest commit from (e.g. 12.x or 12.3)" | |
| to_tag: | |
| required: true | |
| type: string | |
| description: "Release version without leading v (e.g. 12.4.0)" | |
| publish_immediately: | |
| required: true | |
| default: false | |
| type: boolean | |
| description: "Release directly (not recommended) by default releases are created as drafts" | |
| autoChangelog: | |
| description: "Auto generated release notes" | |
| required: true | |
| default: true | |
| type: boolean | |
| changeLog: | |
| description: "Custom release notes (optional)" | |
| required: false | |
| type: string | |
| env: | |
| REPOSITORY: ${{ inputs.repository }} | |
| BASE_BRANCH: ${{ inputs.base_branch }} | |
| TO_TAG: ${{ inputs.to_tag }} | |
| PUBLISH_IMMEDIATELY: ${{ inputs.publish_immediately }} | |
| AUTO_CHANGELOG: ${{ inputs.autoChangelog }} | |
| CHANGELOG: ${{ inputs.changeLog }} | |
| jobs: | |
| prepare: | |
| name: Prepare parameters | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| env: | |
| RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} | |
| outputs: | |
| owner: ${{ steps.calc.outputs.owner }} | |
| repo: ${{ steps.calc.outputs.repo }} | |
| base_branch: ${{ steps.calc.outputs.base_branch }} | |
| latest_sha: ${{ steps.calc.outputs.latest_sha }} | |
| tag_name: ${{ steps.calc.outputs.tag_name }} | |
| release_name: ${{ steps.calc.outputs.release_name }} | |
| release_kind: ${{ steps.calc.outputs.release_kind }} | |
| prerelease: ${{ steps.calc.outputs.prerelease }} | |
| auto_changelog_norm: ${{ steps.calc.outputs.auto_changelog_norm }} | |
| steps: | |
| - name: Validate and calculate | |
| id: calc | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| # Verify token is available | |
| if [[ -z "${RELEASE_TOKEN}" ]]; then | |
| echo "❌ RELEASE_TOKEN secret is not set or empty" >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| # Test token validity with a simple API call | |
| TEST_RESP=$(curl -s -o /dev/null -w "%{http_code}" --location \ | |
| "https://api.github.com/user" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${RELEASE_TOKEN}") | |
| if [[ "${TEST_RESP}" -eq 401 ]]; then | |
| echo "❌ RELEASE_TOKEN is invalid or expired (HTTP 401)" >> "$GITHUB_STEP_SUMMARY" | |
| echo "Please ensure:" >> "$GITHUB_STEP_SUMMARY" | |
| echo "1. The token has not expired" >> "$GITHUB_STEP_SUMMARY" | |
| echo "2. The token has 'repo' scope for accessing repositories" >> "$GITHUB_STEP_SUMMARY" | |
| echo "3. The token is a classic PAT or fine-grained token with repository access" >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| elif [[ "${TEST_RESP}" -ne 200 ]]; then | |
| echo "⚠️ Unexpected response from GitHub API when validating token (HTTP ${TEST_RESP})" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| if ! [[ "${REPOSITORY}" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then | |
| echo "Invalid repository format. Expected owner/repo, got: ${REPOSITORY}" >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| OWNER="${REPOSITORY%%/*}" | |
| REPO="${REPOSITORY##*/}" | |
| BASE="${BASE_BRANCH}" | |
| TAG="${TO_TAG}" | |
| if [[ -z "${BASE}" || -z "${TAG}" ]]; then | |
| echo "base_branch and to_tag must be non-empty" >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| if ! [[ "${TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then | |
| echo "Invalid to_tag format. Expected X.Y.Z or X.Y.Z.W (numeric), got: ${TAG}" >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| TAG_NAME="v${TAG}" | |
| RELEASE_NAME="${TAG}" | |
| BASE_IS_X=false | |
| if [[ "${BASE}" == *.x ]]; then | |
| BASE_IS_X=true | |
| BASE_MAJOR_PART="${BASE%%.*}" | |
| if ! [[ "${BASE_MAJOR_PART}" =~ ^[0-9]+$ ]]; then | |
| echo "Invalid base_branch format. Expected numeric major version, got: ${BASE}" >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| fi | |
| BASE_MAJOR="$(echo "${BASE}" | cut -d'.' -f1)" | |
| TAG_MAJOR="$(echo "${TAG}" | cut -d'.' -f1)" | |
| TAG_PARTS_COUNT="$(echo "${TAG}" | awk -F'.' '{print NF}')" | |
| RELEASE_KIND="" | |
| if [[ "${BASE_IS_X}" == "true" ]]; then | |
| # .x branches should only have 3-part versions (major/minor releases) | |
| if [[ "${TAG_PARTS_COUNT}" -eq 4 ]]; then | |
| echo "Invalid configuration: 4-part version (${TAG}) cannot be released from .x branch (${BASE})" >> "$GITHUB_STEP_SUMMARY" | |
| echo "Hotfix releases must use a maintenance branch (e.g., 12.3)" >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| TAG_MINOR="$(echo "${TAG}" | cut -d'.' -f2)" | |
| TAG_PATCH="$(echo "${TAG}" | cut -d'.' -f3)" | |
| if [[ "${TAG_MAJOR}" != "${BASE_MAJOR}" ]]; then | |
| RELEASE_KIND="major" | |
| # Major releases must be X.0.0 | |
| if [[ "${TAG_MINOR}" != "0" || "${TAG_PATCH}" != "0" ]]; then | |
| echo "Invalid major release format. Expected ${TAG_MAJOR}.0.0, got: ${TAG}" >> "$GITHUB_STEP_SUMMARY" | |
| echo "Major releases must use X.0.0 format" >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| else | |
| RELEASE_KIND="minor" | |
| # Minor releases must be X.Y.0 | |
| if [[ "${TAG_PATCH}" != "0" ]]; then | |
| echo "Invalid minor release format. Expected ${TAG_MAJOR}.${TAG_MINOR}.0, got: ${TAG}" >> "$GITHUB_STEP_SUMMARY" | |
| echo "Minor releases must use X.Y.0 format" >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| fi | |
| else | |
| if [[ "${TAG_PARTS_COUNT}" -eq 4 ]]; then | |
| RELEASE_KIND="hotfix" | |
| elif [[ "${TAG_PARTS_COUNT}" -eq 3 ]]; then | |
| RELEASE_KIND="bugfix" | |
| else | |
| echo "Could not classify release kind. base_branch=${BASE} to_tag=${TAG}" >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| fi | |
| PRERELEASE=false | |
| SHA_RESP=$(curl -s -w "\nHTTP_STATUS:%{http_code}" --location \ | |
| "https://api.github.com/repos/${OWNER}/${REPO}/commits/${BASE}" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${RELEASE_TOKEN}") | |
| SHA_STATUS=$(echo "${SHA_RESP}" | awk -F: '/HTTP_STATUS:/ {print $2}') | |
| if [[ "${SHA_STATUS}" -ne 200 ]]; then | |
| echo "Failed to fetch latest commit from ${OWNER}/${REPO}@${BASE}. HTTP ${SHA_STATUS}" >> "$GITHUB_STEP_SUMMARY" | |
| echo "$(echo "${SHA_RESP}" | head -c 2000)" >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| LATEST_SHA=$(echo "${SHA_RESP}" | sed '$d' | jq -r '.sha') | |
| if [[ -z "${LATEST_SHA}" || "${LATEST_SHA}" == "null" ]]; then | |
| echo "Failed to parse sha for ${OWNER}/${REPO}@${BASE}" >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| echo "owner=${OWNER}" >> "$GITHUB_OUTPUT" | |
| echo "repo=${REPO}" >> "$GITHUB_OUTPUT" | |
| echo "base_branch=${BASE}" >> "$GITHUB_OUTPUT" | |
| echo "latest_sha=${LATEST_SHA}" >> "$GITHUB_OUTPUT" | |
| echo "tag_name=${TAG_NAME}" >> "$GITHUB_OUTPUT" | |
| echo "release_name=${RELEASE_NAME}" >> "$GITHUB_OUTPUT" | |
| echo "release_kind=${RELEASE_KIND}" >> "$GITHUB_OUTPUT" | |
| echo "prerelease=${PRERELEASE}" >> "$GITHUB_OUTPUT" | |
| AUTO_CHANGELOG_NORM=false | |
| if [[ "${AUTO_CHANGELOG}" == "true" ]]; then AUTO_CHANGELOG_NORM=true; fi | |
| echo "auto_changelog_norm=${AUTO_CHANGELOG_NORM}" >> "$GITHUB_OUTPUT" | |
| { | |
| echo "Repository: ${OWNER}/${REPO}" | |
| echo "Base branch: ${BASE}" | |
| echo "Latest SHA: ${LATEST_SHA}" | |
| echo "To tag: ${TAG}" | |
| echo "Tag name: ${TAG_NAME}" | |
| echo "Release kind: ${RELEASE_KIND}" | |
| echo "Publish immediately: ${PUBLISH_IMMEDIATELY}" | |
| echo "Auto changelog: ${AUTO_CHANGELOG}" | |
| echo "Custom changelog provided: $([[ -n "${CHANGELOG}" ]] && echo yes || echo no)" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| create-release: | |
| name: Create release | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| needs: prepare | |
| env: | |
| RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} | |
| OWNER: ${{ needs.prepare.outputs.owner }} | |
| REPO: ${{ needs.prepare.outputs.repo }} | |
| BASE_BRANCH: ${{ needs.prepare.outputs.base_branch }} | |
| LATEST_SHA: ${{ needs.prepare.outputs.latest_sha }} | |
| TAG_NAME: ${{ needs.prepare.outputs.tag_name }} | |
| RELEASE_NAME: ${{ needs.prepare.outputs.release_name }} | |
| DRAFT: ${{ inputs.publish_immediately == false }} | |
| MAKE_LATEST: ${{ inputs.publish_immediately == true }} | |
| PRERELEASE: ${{ needs.prepare.outputs.prerelease }} | |
| RELEASE_KIND: ${{ needs.prepare.outputs.release_kind }} | |
| AUTO_CHANGELOG: ${{ needs.prepare.outputs.auto_changelog_norm }} | |
| outputs: | |
| owner: ${{ needs.prepare.outputs.owner }} | |
| repo: ${{ needs.prepare.outputs.repo }} | |
| release_kind: ${{ needs.prepare.outputs.release_kind }} | |
| tag: ${{ needs.prepare.outputs.release_name }} | |
| steps: | |
| - name: Ensure tag does not already exist | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| CHECK_TAG=$(curl -s -o /dev/null -w "%{http_code}" --location \ | |
| "https://api.github.com/repos/${OWNER}/${REPO}/git/refs/tags/${TAG_NAME}" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${RELEASE_TOKEN}") | |
| if [[ "${CHECK_TAG}" -eq 200 ]]; then | |
| echo "Tag already exists: ${TAG_NAME} on ${OWNER}/${REPO}" >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| - name: Create release | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| FINAL_BODY="${CHANGELOG}" | |
| # If auto notes requested and custom notes provided, generate and append. | |
| if [[ "${AUTO_CHANGELOG}" == "true" && -n "${CHANGELOG}" ]]; then | |
| GEN_RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" -X POST --location \ | |
| "https://api.github.com/repos/${OWNER}/${REPO}/releases/generate-notes" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${RELEASE_TOKEN}" \ | |
| -d "$(jq -n --arg tag "${TAG_NAME}" --arg target "${LATEST_SHA}" '{tag_name:$tag, target_commitish:$target}')") | |
| GEN_STATUS=$(echo "${GEN_RESPONSE}" | awk -F: '/HTTP_STATUS:/ {print $2}') | |
| if [[ "${GEN_STATUS}" -eq 200 ]]; then | |
| GEN_NOTES=$(echo "${GEN_RESPONSE}" | sed '$d' | jq -r '.body // ""') | |
| FINAL_BODY="${GEN_NOTES}"$'\n\n---\n\n'"${CHANGELOG}" | |
| else | |
| echo "⚠️ Warning: generate-notes failed (HTTP ${GEN_STATUS}); using custom notes only" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| fi | |
| # Build request body with proper JSON types | |
| # Convert boolean env vars to proper strings for GitHub API | |
| MAKE_LATEST_STR="true" | |
| if [[ "${MAKE_LATEST}" == "false" ]]; then | |
| MAKE_LATEST_STR="false" | |
| fi | |
| if [[ -n "${FINAL_BODY}" ]]; then | |
| REQ_BODY=$(jq -n \ | |
| --arg target_commitish "${LATEST_SHA}" \ | |
| --arg name "${RELEASE_NAME}" \ | |
| --arg tag_name "${TAG_NAME}" \ | |
| --arg make_latest "${MAKE_LATEST_STR}" \ | |
| --argjson draft "${DRAFT}" \ | |
| --argjson prerelease "${PRERELEASE}" \ | |
| --arg body "${FINAL_BODY}" \ | |
| '{ | |
| target_commitish: $target_commitish, | |
| name: $name, | |
| draft: $draft, | |
| make_latest: $make_latest, | |
| prerelease: $prerelease, | |
| tag_name: $tag_name, | |
| generate_release_notes: false, | |
| body: $body | |
| }') | |
| else | |
| REQ_BODY=$(jq -n \ | |
| --arg target_commitish "${LATEST_SHA}" \ | |
| --arg name "${RELEASE_NAME}" \ | |
| --arg tag_name "${TAG_NAME}" \ | |
| --arg make_latest "${MAKE_LATEST_STR}" \ | |
| --argjson draft "${DRAFT}" \ | |
| --argjson prerelease "${PRERELEASE}" \ | |
| --argjson generate_release_notes "${AUTO_CHANGELOG}" \ | |
| '{ | |
| target_commitish: $target_commitish, | |
| name: $name, | |
| draft: $draft, | |
| make_latest: $make_latest, | |
| prerelease: $prerelease, | |
| tag_name: $tag_name, | |
| generate_release_notes: $generate_release_notes | |
| }') | |
| fi | |
| CREATE_RELEASE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" -X POST --location \ | |
| "https://api.github.com/repos/${OWNER}/${REPO}/releases" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${RELEASE_TOKEN}" \ | |
| -d "${REQ_BODY}") | |
| HTTP_STATUS=$(echo "${CREATE_RELEASE}" | awk -F: '/HTTP_STATUS:/ {print $2}') | |
| if [[ "${HTTP_STATUS}" -ne 201 ]]; then | |
| echo "Failed to create release. HTTP ${HTTP_STATUS}" >> "$GITHUB_STEP_SUMMARY" | |
| echo "$(echo "${CREATE_RELEASE}" | head -c 2000)" >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| RELEASE_URL=$(echo "${CREATE_RELEASE}" | sed '$d' | jq -r '.html_url // ""') | |
| if [[ "${DRAFT}" == "true" ]]; then | |
| echo "Release created (draft): ${RELEASE_URL}" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "Release created and published: ${RELEASE_URL}" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| select-release: | |
| name: Select release outputs | |
| runs-on: ubuntu-latest | |
| needs: [create-release] | |
| if: ${{ !cancelled() && needs.create-release.result == 'success' }} | |
| outputs: | |
| owner: ${{ needs.create-release.outputs.owner }} | |
| repo: ${{ needs.create-release.outputs.repo }} | |
| release_kind: ${{ needs.create-release.outputs.release_kind }} | |
| tag: ${{ needs.create-release.outputs.tag }} | |
| steps: | |
| - name: Pass through outputs | |
| shell: bash | |
| run: | | |
| echo "Release outputs passed through from create-release job" >> "$GITHUB_STEP_SUMMARY" | |
| milestone-automation: | |
| name: Milestone automation | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| contents: read | |
| needs: [select-release] | |
| if: ${{ !cancelled() && needs.select-release.result == 'success' }} | |
| env: | |
| RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} | |
| OWNER: ${{ needs.select-release.outputs.owner }} | |
| REPO: ${{ needs.select-release.outputs.repo }} | |
| RELEASE_KIND: ${{ needs.select-release.outputs.release_kind }} | |
| TAG: ${{ needs.select-release.outputs.tag }} | |
| steps: | |
| - name: Decide milestone actions | |
| id: plan | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| TAG="${TAG}" | |
| KIND="${RELEASE_KIND}" | |
| IFS='.' read -r A B C D <<< "${TAG}" | |
| echo "close_title=${TAG}" >> "$GITHUB_OUTPUT" | |
| echo "do_milestones=true" >> "$GITHUB_OUTPUT" | |
| echo "create_titles=" >> "$GITHUB_OUTPUT" | |
| echo "move_from_title=" >> "$GITHUB_OUTPUT" | |
| echo "move_to_title=" >> "$GITHUB_OUTPUT" | |
| echo "minor_move_prefix=" >> "$GITHUB_OUTPUT" | |
| if [[ "${KIND}" == "hotfix" ]]; then | |
| echo "do_milestones=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| else | |
| # Major, minor, or bugfix release - proceed with milestone automation | |
| if [[ "${KIND}" == "major" ]]; then | |
| NEXT_MAJOR="$((A+1)).0.0" | |
| NEXT_PATCH="${A}.${B}.$((C+1))" | |
| echo "create_titles=${NEXT_MAJOR},${NEXT_PATCH}" >> "$GITHUB_OUTPUT" | |
| echo "move_from_title=" >> "$GITHUB_OUTPUT" | |
| echo "move_to_title=" >> "$GITHUB_OUTPUT" | |
| elif [[ "${KIND}" == "minor" ]]; then | |
| NEXT_MINOR="${A}.$((B+1)).0" | |
| NEXT_PATCH="${A}.${B}.$((C+1))" | |
| echo "create_titles=${NEXT_MINOR},${NEXT_PATCH}" >> "$GITHUB_OUTPUT" | |
| echo "move_to_title=${NEXT_PATCH}" >> "$GITHUB_OUTPUT" | |
| if [[ "${B}" =~ ^[0-9]+$ && "${B}" -gt 0 ]]; then | |
| PREV_MINOR="${A}.$((B-1))." | |
| echo "minor_move_prefix=${PREV_MINOR}" >> "$GITHUB_OUTPUT" | |
| fi | |
| elif [[ "${KIND}" == "bugfix" ]]; then | |
| NEXT_PATCH="${A}.${B}.$((C+1))" | |
| NEXT_PATCH_2="${A}.${B}.$((C+2))" | |
| echo "create_titles=${NEXT_PATCH},${NEXT_PATCH_2}" >> "$GITHUB_OUTPUT" | |
| echo "move_from_title=${TAG}" >> "$GITHUB_OUTPUT" | |
| echo "move_to_title=${NEXT_PATCH}" >> "$GITHUB_OUTPUT" | |
| fi | |
| fi | |
| - name: Skip milestones for hotfix | |
| if: ${{ steps.plan.outputs.do_milestones == 'false' }} | |
| run: | | |
| echo "Hotfix release detected; no milestone automation performed." >> "$GITHUB_STEP_SUMMARY" | |
| - name: Find milestone to close | |
| id: ms_find | |
| if: ${{ steps.plan.outputs.do_milestones == 'true' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| TITLE="${{ steps.plan.outputs.close_title }}" | |
| PAGE=1 | |
| MS_NUMBER="" | |
| while true; do | |
| RESP=$(curl -s --location \ | |
| "https://api.github.com/repos/${OWNER}/${REPO}/milestones?state=all&per_page=100&page=${PAGE}" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${RELEASE_TOKEN}") | |
| LEN=$(echo "${RESP}" | jq '. | length') | |
| if [[ "${LEN}" -eq 0 ]]; then | |
| break | |
| fi | |
| MS_NUMBER=$(echo "${RESP}" | jq -r --arg t "${TITLE}" '.[] | select(.title == $t) | .number' | head -n 1) | |
| if [[ -n "${MS_NUMBER}" && "${MS_NUMBER}" != "null" ]]; then | |
| break | |
| fi | |
| PAGE=$((PAGE+1)) | |
| if [[ "${PAGE}" -gt 20 ]]; then | |
| break | |
| fi | |
| done | |
| if [[ -z "${MS_NUMBER}" || "${MS_NUMBER}" == "null" ]]; then | |
| echo "⚠️ Close milestone not found by title: ${TITLE} (will not close)" >> "$GITHUB_STEP_SUMMARY" | |
| echo "close_ms_number=" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "close_ms_number=${MS_NUMBER}" >> "$GITHUB_OUTPUT" | |
| echo "Found milestone '${TITLE}' as #${MS_NUMBER}" >> "$GITHUB_STEP_SUMMARY" | |
| - name: Create next milestones | |
| id: ms_create | |
| if: ${{ steps.plan.outputs.do_milestones == 'true' && steps.plan.outputs.create_titles != '' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| IFS=',' read -ra TITLES <<< "${{ steps.plan.outputs.create_titles }}" | |
| CREATED="" | |
| for TITLE in "${TITLES[@]}"; do | |
| TITLE="$(echo "${TITLE}" | xargs)" | |
| [[ -z "${TITLE}" ]] && continue | |
| # Check if milestone exists (open only) | |
| PAGE=1 | |
| FOUND="" | |
| while true; do | |
| RESP=$(curl -s --location \ | |
| "https://api.github.com/repos/${OWNER}/${REPO}/milestones?state=open&per_page=100&page=${PAGE}" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${RELEASE_TOKEN}") | |
| LEN=$(echo "${RESP}" | jq '. | length') | |
| if [[ "${LEN}" -eq 0 ]]; then | |
| break | |
| fi | |
| FOUND=$(echo "${RESP}" | jq -r --arg t "${TITLE}" '.[] | select(.title == $t) | .number' | head -n 1) | |
| if [[ -n "${FOUND}" && "${FOUND}" != "null" ]]; then | |
| break | |
| fi | |
| PAGE=$((PAGE+1)) | |
| if [[ "${PAGE}" -gt 20 ]]; then | |
| break | |
| fi | |
| done | |
| if [[ -n "${FOUND}" && "${FOUND}" != "null" ]]; then | |
| echo "Milestone exists '${TITLE}' (#${FOUND}); reusing" >> "$GITHUB_STEP_SUMMARY" | |
| CREATED="${CREATED}${TITLE}:${FOUND}," | |
| continue | |
| fi | |
| # Milestone not found, create it | |
| NEW_JSON=$(curl -s --fail-with-body -X POST --location \ | |
| "https://api.github.com/repos/${OWNER}/${REPO}/milestones" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${RELEASE_TOKEN}" \ | |
| -d "$(jq -n --arg t "${TITLE}" '{title:$t}')" ) | |
| NEW_NUM=$(echo "${NEW_JSON}" | jq -r '.number') | |
| if [[ -z "${NEW_NUM}" || "${NEW_NUM}" == "null" ]]; then | |
| echo "Failed to create milestone '${TITLE}'" >> "$GITHUB_STEP_SUMMARY" | |
| echo "$(echo "${NEW_JSON}" | head -c 2000)" >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| echo "Created milestone '${TITLE}' (#${NEW_NUM})" >> "$GITHUB_STEP_SUMMARY" | |
| CREATED="${CREATED}${TITLE}:${NEW_NUM}," | |
| done | |
| echo "created_map=${CREATED}" >> "$GITHUB_OUTPUT" | |
| - name: Move issues for bugfix (from old milestone to new milestone) | |
| if: ${{ steps.plan.outputs.do_milestones == 'true' && steps.plan.outputs.move_from_title != '' && steps.plan.outputs.move_to_title != '' && env.RELEASE_KIND == 'bugfix' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| FROM_TITLE="${{ steps.plan.outputs.move_from_title }}" | |
| TO_TITLE="${{ steps.plan.outputs.move_to_title }}" | |
| resolve_ms () { | |
| local TITLE="$1" | |
| local PAGE=1 | |
| while true; do | |
| local RESP | |
| RESP=$(curl -s --location \ | |
| "https://api.github.com/repos/${OWNER}/${REPO}/milestones?state=all&per_page=100&page=${PAGE}" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${RELEASE_TOKEN}") || return 1 | |
| local LEN | |
| LEN=$(echo "${RESP}" | jq '. | length') || return 1 | |
| if [[ "${LEN}" -eq 0 ]]; then | |
| echo "" | |
| return 0 | |
| fi | |
| local NUM | |
| NUM=$(echo "${RESP}" | jq -r --arg t "${TITLE}" '.[] | select(.title == $t) | .number' | head -n 1) || return 1 | |
| if [[ -n "${NUM}" && "${NUM}" != "null" ]]; then | |
| echo "${NUM}" | |
| return 0 | |
| fi | |
| PAGE=$((PAGE+1)) | |
| if [[ "${PAGE}" -gt 20 ]]; then | |
| echo "" | |
| return 0 | |
| fi | |
| done | |
| } | |
| FROM_NUM="$(resolve_ms "${FROM_TITLE}")" | |
| TO_NUM="$(resolve_ms "${TO_TITLE}")" | |
| if [[ -z "${FROM_NUM}" || -z "${TO_NUM}" ]]; then | |
| echo "⚠️ Cannot move issues: milestone numbers not found (from='${FROM_TITLE}', to='${TO_TITLE}')" >> "$GITHUB_STEP_SUMMARY" | |
| exit 0 | |
| fi | |
| PAGE=1 | |
| MOVED=0 | |
| while true; do | |
| ISSUES=$(curl -s --location \ | |
| "https://api.github.com/repos/${OWNER}/${REPO}/issues?state=open&milestone=${FROM_NUM}&per_page=100&page=${PAGE}" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${RELEASE_TOKEN}") | |
| LEN=$(echo "${ISSUES}" | jq '. | length') | |
| if [[ "${LEN}" -eq 0 ]]; then | |
| break | |
| fi | |
| IDS=$(echo "${ISSUES}" | jq -r '.[].number') | |
| for N in ${IDS}; do | |
| RES=$(curl -s -o /dev/null -w "%{http_code}" -X PATCH --location \ | |
| "https://api.github.com/repos/${OWNER}/${REPO}/issues/${N}" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${RELEASE_TOKEN}" \ | |
| -d "$(jq -n --argjson m "${TO_NUM}" '{milestone:$m}')") | |
| if [[ "${RES}" -eq 200 ]]; then | |
| MOVED=$((MOVED+1)) | |
| fi | |
| done | |
| PAGE=$((PAGE+1)) | |
| if [[ "${PAGE}" -gt 50 ]]; then | |
| echo "⚠️ Stopped bugfix issue move after 50 pages (5000 issues)" >> "$GITHUB_STEP_SUMMARY" | |
| break | |
| fi | |
| done | |
| if [[ "${MOVED}" -gt 0 ]]; then | |
| echo "Moved ${MOVED} open issues from '${FROM_TITLE}' -> '${TO_TITLE}'" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "No open issues found in milestone '${FROM_TITLE}'" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: Move issues (minor) | |
| if: ${{ steps.plan.outputs.do_milestones == 'true' && env.RELEASE_KIND == 'minor' && steps.plan.outputs.minor_move_prefix != '' && steps.plan.outputs.move_to_title != '' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| PREFIX="${{ steps.plan.outputs.minor_move_prefix }}" | |
| TO_TITLE="${{ steps.plan.outputs.move_to_title }}" | |
| PAGE=1 | |
| TO_NUM="" | |
| while true; do | |
| RESP=$(curl -s --location \ | |
| "https://api.github.com/repos/${OWNER}/${REPO}/milestones?state=all&per_page=100&page=${PAGE}" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${RELEASE_TOKEN}") | |
| LEN=$(echo "${RESP}" | jq '. | length') | |
| if [[ "${LEN}" -eq 0 ]]; then | |
| break | |
| fi | |
| TO_NUM=$(echo "${RESP}" | jq -r --arg t "${TO_TITLE}" '.[] | select(.title == $t) | .number' | head -n 1) | |
| if [[ -n "${TO_NUM}" && "${TO_NUM}" != "null" ]]; then | |
| break | |
| fi | |
| PAGE=$((PAGE+1)) | |
| if [[ "${PAGE}" -gt 20 ]]; then | |
| break | |
| fi | |
| done | |
| if [[ -z "${TO_NUM}" || "${TO_NUM}" == "null" ]]; then | |
| echo "⚠️ Cannot move minor issues: target milestone '${TO_TITLE}' not found" >> "$GITHUB_STEP_SUMMARY" | |
| exit 0 | |
| fi | |
| PAGE=1 | |
| MOVED=0 | |
| while true; do | |
| MS=$(curl -s --location \ | |
| "https://api.github.com/repos/${OWNER}/${REPO}/milestones?state=open&per_page=100&page=${PAGE}" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${RELEASE_TOKEN}") | |
| LEN=$(echo "${MS}" | jq '. | length') | |
| if [[ "${LEN}" -eq 0 ]]; then | |
| break | |
| fi | |
| while IFS= read -r MS_TITLE; do | |
| [[ -z "${MS_TITLE}" ]] && continue | |
| MS_NUM=$(echo "${MS}" | jq -r --arg t "${MS_TITLE}" '.[] | select(.title == $t) | .number' | head -n 1) | |
| if [[ -z "${MS_NUM}" || "${MS_NUM}" == "null" ]]; then | |
| continue | |
| fi | |
| ISSUE_PAGE=1 | |
| while true; do | |
| ISSUES=$(curl -s --location \ | |
| "https://api.github.com/repos/${OWNER}/${REPO}/issues?state=open&milestone=${MS_NUM}&per_page=100&page=${ISSUE_PAGE}" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${RELEASE_TOKEN}") | |
| ISSUE_LEN=$(echo "${ISSUES}" | jq '. | length') | |
| if [[ "${ISSUE_LEN}" -eq 0 ]]; then | |
| break | |
| fi | |
| IDS=$(echo "${ISSUES}" | jq -r '.[].number') | |
| for N in ${IDS}; do | |
| RES=$(curl -s -o /dev/null -w "%{http_code}" -X PATCH --location \ | |
| "https://api.github.com/repos/${OWNER}/${REPO}/issues/${N}" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${RELEASE_TOKEN}" \ | |
| -d "$(jq -n --argjson m "${TO_NUM}" '{milestone:$m}')") | |
| if [[ "${RES}" -eq 200 ]]; then | |
| MOVED=$((MOVED+1)) | |
| fi | |
| done | |
| ISSUE_PAGE=$((ISSUE_PAGE+1)) | |
| if [[ "${ISSUE_PAGE}" -gt 50 ]]; then | |
| echo "⚠️ Stopped after 50 pages for milestone '${MS_TITLE}'" >> "$GITHUB_STEP_SUMMARY" | |
| break | |
| fi | |
| done | |
| done <<< "$(echo "${MS}" | jq -r --arg p "${PREFIX}" '.[] | select(.title | startswith($p)) | .title')" | |
| PAGE=$((PAGE+1)) | |
| if [[ "${PAGE}" -gt 20 ]]; then | |
| echo "⚠️ Stopped minor issue move after 20 pages of milestones" >> "$GITHUB_STEP_SUMMARY" | |
| break | |
| fi | |
| done | |
| if [[ "${MOVED}" -gt 0 ]]; then | |
| echo "Moved ${MOVED} open issues from milestones starting with '${PREFIX}' -> '${TO_TITLE}'" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "No open issues found in milestones starting with '${PREFIX}'" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: Close current milestone (after issue moves) | |
| if: ${{ steps.plan.outputs.do_milestones == 'true' && steps.ms_find.outputs.close_ms_number != '' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| MS_NUMBER="${{ steps.ms_find.outputs.close_ms_number }}" | |
| curl -s --fail-with-body -X PATCH --location \ | |
| "https://api.github.com/repos/${OWNER}/${REPO}/milestones/${MS_NUMBER}" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${RELEASE_TOKEN}" \ | |
| -d "$(jq -n '{state:"closed"}')" >/dev/null | |
| echo "Closed milestone #${MS_NUMBER} (after moving issues)" >> "$GITHUB_STEP_SUMMARY" | |
| branch-automation: | |
| name: Branch automation | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| needs: [select-release, milestone-automation] | |
| if: ${{ !cancelled() && needs.select-release.result == 'success' && (needs.select-release.outputs.release_kind == 'major' || needs.select-release.outputs.release_kind == 'minor') }} | |
| env: | |
| RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} | |
| OWNER: ${{ needs.select-release.outputs.owner }} | |
| REPO: ${{ needs.select-release.outputs.repo }} | |
| RELEASE_KIND: ${{ needs.select-release.outputs.release_kind }} | |
| TAG: ${{ needs.select-release.outputs.tag }} | |
| steps: | |
| - name: Determine branch operations | |
| id: plan | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| TAG="${TAG}" | |
| KIND="${RELEASE_KIND}" | |
| # Parse TAG parts | |
| IFS='.' read -r A B C _ <<< "${TAG}" | |
| if [[ "${KIND}" == "major" ]]; then | |
| if [[ "${A}" =~ ^[0-9]+$ && "${A}" -gt 0 ]]; then | |
| PREV_MAJOR=$((A-1)) | |
| CLONE_FROM="${PREV_MAJOR}.x" | |
| CLONE_TO="${A}.0" | |
| RENAME_FROM="${PREV_MAJOR}.x" | |
| RENAME_TO="${A}.x" | |
| else | |
| echo "Cannot determine previous major version for tag ${TAG}" >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| echo "clone_from=${CLONE_FROM}" >> "$GITHUB_OUTPUT" | |
| echo "clone_to=${CLONE_TO}" >> "$GITHUB_OUTPUT" | |
| echo "rename_from=${RENAME_FROM}" >> "$GITHUB_OUTPUT" | |
| echo "rename_to=${RENAME_TO}" >> "$GITHUB_OUTPUT" | |
| echo "do_clone=true" >> "$GITHUB_OUTPUT" | |
| echo "do_rename=true" >> "$GITHUB_OUTPUT" | |
| elif [[ "${KIND}" == "minor" ]]; then | |
| CLONE_FROM="${A}.x" | |
| CLONE_TO="${A}.${B}" | |
| echo "clone_from=${CLONE_FROM}" >> "$GITHUB_OUTPUT" | |
| echo "clone_to=${CLONE_TO}" >> "$GITHUB_OUTPUT" | |
| echo "do_clone=true" >> "$GITHUB_OUTPUT" | |
| echo "do_rename=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Clone branch for maintenance | |
| if: ${{ steps.plan.outputs.do_clone == 'true' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| FROM="${{ steps.plan.outputs.clone_from }}" | |
| TO="${{ steps.plan.outputs.clone_to }}" | |
| SHA_RESP=$(curl -s -w "\nHTTP_STATUS:%{http_code}" --location \ | |
| "https://api.github.com/repos/${OWNER}/${REPO}/git/refs/heads/${FROM}" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${RELEASE_TOKEN}") | |
| SHA_STATUS=$(echo "${SHA_RESP}" | awk -F: '/HTTP_STATUS:/ {print $2}') | |
| if [[ "${SHA_STATUS}" -ne 200 ]]; then | |
| echo "❌ Failed to get SHA for branch '${FROM}'. HTTP ${SHA_STATUS}" >> "$GITHUB_STEP_SUMMARY" | |
| echo "Branch '${FROM}' does not exist or is not accessible." >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| SHA=$(echo "${SHA_RESP}" | sed '$d' | jq -r '.object.sha') | |
| if [[ -z "${SHA}" || "${SHA}" == "null" ]]; then | |
| echo "❌ Failed to parse SHA for branch '${FROM}'" >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| CREATE_RESP=$(curl -s -w "\nHTTP_STATUS:%{http_code}" -X POST --location \ | |
| "https://api.github.com/repos/${OWNER}/${REPO}/git/refs" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${RELEASE_TOKEN}" \ | |
| -d "$(jq -n --arg ref "refs/heads/${TO}" --arg sha "${SHA}" '{ref:$ref, sha:$sha}')") | |
| CREATE_STATUS=$(echo "${CREATE_RESP}" | awk -F: '/HTTP_STATUS:/ {print $2}') | |
| if [[ "${CREATE_STATUS}" -eq 201 ]]; then | |
| echo "✅ Created branch '${TO}' from '${FROM}' (${SHA})" >> "$GITHUB_STEP_SUMMARY" | |
| elif [[ "${CREATE_STATUS}" -eq 422 ]]; then | |
| echo "⚠️ Branch '${TO}' already exists; skipping creation" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "⚠️ Failed to create branch '${TO}'. HTTP ${CREATE_STATUS}" >> "$GITHUB_STEP_SUMMARY" | |
| echo "$(echo "${CREATE_RESP}" | sed '$d' | head -c 2000)" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: Rename branch (manual step 1) | |
| if: ${{ steps.plan.outputs.do_rename == 'true' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| FROM="${{ steps.plan.outputs.rename_from }}" | |
| TO="${{ steps.plan.outputs.rename_to }}" | |
| echo "⚠️ **Manual Action Required**: Rename branch '${FROM}' to '${TO}'" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "After renaming manually, the next step will create a PR to update version references" >> "$GITHUB_STEP_SUMMARY" | |
| echo "See: https://github.com/${OWNER}/${REPO}/branches" >> "$GITHUB_STEP_SUMMARY" | |
| - name: Update version references (automated step 2) | |
| if: ${{ steps.plan.outputs.do_rename == 'true' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| # Variables from previous step | |
| FROM="${{ steps.plan.outputs.rename_from }}" | |
| TO="${{ steps.plan.outputs.rename_to }}" | |
| FROM_VER="${FROM%.x}" | |
| TO_VER="${TO%.x}" | |
| echo "🔄 Starting automated update: ${FROM} → ${TO}" >> "$GITHUB_STEP_SUMMARY" | |
| # Configure git | |
| git config --global user.name "github-actions[bot]" | |
| git config --global user.email "github-actions[bot]@users.noreply.github.com" | |
| # Create temp working directory | |
| WORK_DIR=$(mktemp -d) | |
| cd "${WORK_DIR}" | |
| # Detect default branch (master or main) | |
| DEFAULT_BRANCH=$(curl -s --location \ | |
| "https://api.github.com/repos/${OWNER}/${REPO}" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${RELEASE_TOKEN}" | jq -r '.default_branch // "master"') | |
| echo "Detected default branch: ${DEFAULT_BRANCH}" >> "$GITHUB_STEP_SUMMARY" | |
| # Sparse checkout: only get the files we need to update | |
| git clone --filter=blob:none --no-checkout --depth 1 --branch "${DEFAULT_BRANCH}" \ | |
| "https://x-access-token:${RELEASE_TOKEN}@github.com/${OWNER}/${REPO}.git" repo | |
| cd repo | |
| git sparse-checkout init --cone | |
| FILES=( | |
| ".github/ISSUE_TEMPLATE/Bug-Report.yaml" | |
| ".github/PULL_REQUEST_TEMPLATE.md" | |
| ".github/workflows/pull-request-checklist.yaml" | |
| ".github/workflows/codeception.yaml" | |
| ".github/workflows/sync-changes-scheduled.yml" | |
| ".github/workflows/composer-checks-outdated.yaml" | |
| ".github/workflows/composer-checks-vulnerabilities.yaml" | |
| "composer.json" | |
| "CONTRIBUTING.md" | |
| ) | |
| # Optional doc directories for broader coverage (pipe-separated, e.g., "doc|docs") | |
| DOC_GLOB="${DOC_GLOB:-doc|docs}" | |
| IFS='|' read -r -a DOC_DIRS <<< "${DOC_GLOB}" | |
| SPARSE_DIRS=(".github" "composer.json" "CONTRIBUTING.md") | |
| for DIR in "${DOC_DIRS[@]}"; do | |
| [[ -n "${DIR}" ]] && SPARSE_DIRS+=("${DIR}") | |
| done | |
| git sparse-checkout set "${SPARSE_DIRS[@]}" | |
| git checkout "${DEFAULT_BRANCH}" | |
| # Dynamically determine latest bugfix version from branches (with pagination) | |
| ALL_BRANCHES="" | |
| PAGE=1 | |
| while true; do | |
| BRANCHES_RESP=$(curl -s --location \ | |
| "https://api.github.com/repos/${OWNER}/${REPO}/branches?per_page=100&page=${PAGE}" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${RELEASE_TOKEN}") | |
| LEN=$(echo "${BRANCHES_RESP}" | jq '. | length') | |
| if [[ "${LEN}" -eq 0 ]]; then | |
| break | |
| fi | |
| ALL_BRANCHES+=$(echo "${BRANCHES_RESP}" | jq -r '.[].name') | |
| ALL_BRANCHES+=$'\n' | |
| if [[ "${LEN}" -lt 100 ]]; then | |
| break | |
| fi | |
| PAGE=$((PAGE+1)) | |
| if [[ "${PAGE}" -gt 50 ]]; then | |
| echo "⚠️ Stopped branch pagination after 50 pages" >> "$GITHUB_STEP_SUMMARY" | |
| break | |
| fi | |
| done | |
| # Find highest X.Y branch for FROM_VER (e.g., find 12.3 when FROM_VER=12) | |
| LATEST_MAINTENANCE=$(echo "${ALL_BRANCHES}" | \ | |
| grep -E "^${FROM_VER}\.[0-9]+$" | \ | |
| sort -V | tail -n 1) | |
| if [[ -n "${LATEST_MAINTENANCE}" ]]; then | |
| FROM_BUGFIX="${LATEST_MAINTENANCE}" | |
| echo "Detected latest maintenance branch: ${FROM_BUGFIX}" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| # Fallback to .3 if no maintenance branches found | |
| FROM_BUGFIX="${FROM_VER}.3" | |
| echo "⚠️ No maintenance branches found, using fallback: ${FROM_BUGFIX}" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| TO_BUGFIX="${TO_VER}.0" | |
| # Verify the renamed branch exists before proceeding | |
| VERIFY_BRANCH=$(curl -s -o /dev/null -w "%{http_code}" --location \ | |
| "https://api.github.com/repos/${OWNER}/${REPO}/git/refs/heads/${TO}" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${RELEASE_TOKEN}") | |
| if [[ "${VERIFY_BRANCH}" -ne 200 ]]; then | |
| echo "⚠️ Branch '${TO}' not found yet. Skipping version reference updates." >> "$GITHUB_STEP_SUMMARY" | |
| echo "After manually renaming '${FROM}' → '${TO}', re-run the branch-automation job to apply updates." >> "$GITHUB_STEP_SUMMARY" | |
| cd / | |
| rm -rf "${WORK_DIR}" | |
| exit 0 | |
| fi | |
| # Create PR branch | |
| PR_BRANCH="automated/update-refs-${TO_VER}.x-$(date +%s)" | |
| git checkout -b "${PR_BRANCH}" | |
| # Build file list (curated + optional doc directories) with exclusions | |
| FILE_LIST=("${FILES[@]}") | |
| if [[ ${#DOC_DIRS[@]} -gt 0 && -n "${DOC_DIRS[0]}" ]]; then | |
| for DIR in "${DOC_DIRS[@]}"; do | |
| [[ -z "${DIR}" ]] && continue | |
| if [[ -d "${DIR}" ]]; then | |
| while IFS= read -r f; do | |
| # Normalize path | |
| f="${f#./}" | |
| FILE_LIST+=("${f}") | |
| done < <(find "${DIR}" -type f -name "*.md" \ | |
| ! -path "*/CHANGELOG*" \ | |
| ! -path "*/UPGRADE*" \ | |
| ! -path "*/deprecation*" \ | |
| ! -path "*/migration*" \ | |
| ! -path "*/bc-*" \ | |
| ! -path "*/BC-*" \ | |
| ! -path "*/bc_*" \ | |
| 2>/dev/null) | |
| fi | |
| done | |
| fi | |
| # De-duplicate using array key check | |
| declare -A SEEN | |
| UNIQUE_FILES=() | |
| for f in "${FILE_LIST[@]}"; do | |
| [[ -z "${f}" ]] && continue | |
| # Normalize path (remove ./ prefix if present) | |
| f="${f#./}" | |
| # Check if key exists in associative array | |
| if [[ -v SEEN[$f] ]]; then continue; fi | |
| SEEN[$f]=1 | |
| UNIQUE_FILES+=("${f}") | |
| done | |
| FILE_LIST=("${UNIQUE_FILES[@]}") | |
| if [[ ${#FILE_LIST[@]} -eq 0 ]]; then | |
| echo "⚠️ No files found to update" >> "$GITHUB_STEP_SUMMARY" | |
| cd / | |
| rm -rf "${WORK_DIR}" | |
| exit 0 | |
| fi | |
| echo "Found ${#FILE_LIST[@]} files to process" >> "$GITHUB_STEP_SUMMARY" | |
| # === CAREFUL REPLACEMENTS === | |
| # Only update version refs in specific contexts, avoiding CHANGELOG/UPGRADE/deprecations | |
| for FILE in "${FILE_LIST[@]}"; do | |
| [[ -f "${FILE}" ]] || continue | |
| # Skip excluded paths defensively | |
| if [[ "${FILE}" =~ (CHANGELOG|UPGRADE|deprecation|migration|bc-|bc_|BC-) ]]; then | |
| continue | |
| fi | |
| # 1) Documentation blob/tree URLs: blob/12.x/ → blob/13.x/ (and tree/) | |
| sed -i "s|blob/${FROM}/|blob/${TO}/|g" "${FILE}" | |
| sed -i "s|tree/${FROM}|tree/${TO}|g" "${FILE}" | |
| # 2) Workflow branches: branches: [12.x] → branches: [13.x] | |
| sed -i "s/branches: \[${FROM}\]/branches: [${TO}]/g" "${FILE}" | |
| sed -i "s/branches: \[\"${FROM}\"\]/branches: [\"${TO}\"]/g" "${FILE}" | |
| sed -i "s/branches: \['${FROM}'\]/branches: ['${TO}']/g" "${FILE}" | |
| # 3) Workflow matrix pimcore_version (including matrix.pimcore_version) | |
| sed -i "s/pimcore_version: \[\"${FROM}\"\]/pimcore_version: [\"${TO}\"]/g" "${FILE}" | |
| sed -i "s/pimcore_version: \[${FROM}\]/pimcore_version: [${TO}]/g" "${FILE}" | |
| sed -i "s/matrix\.pimcore_version: \"${FROM}\"/matrix.pimcore_version: \"${TO}\"/g" "${FILE}" | |
| sed -i "s/matrix\.pimcore_version: ${FROM}/matrix.pimcore_version: ${TO}/g" "${FILE}" | |
| # 4) Bugfix references: 12.3 → 13.0 (targeted list only) | |
| sed -i "s/${FROM_BUGFIX}/${TO_BUGFIX}/g" "${FILE}" | |
| done | |
| # 5) composer.json branch-alias: "12.x-dev" → "13.x-dev" | |
| if [[ -f "composer.json" ]]; then | |
| sed -i "s/\"${FROM_VER}\.x-dev\"/\"${TO_VER}.x-dev\"/g" composer.json | |
| fi | |
| # Check if we made any changes | |
| if [[ -z "$(git status --porcelain)" ]]; then | |
| echo "ℹ️ No version reference updates needed" >> "$GITHUB_STEP_SUMMARY" | |
| cd / | |
| rm -rf "${WORK_DIR}" | |
| exit 0 | |
| fi | |
| # Commit changes | |
| git add -A | |
| git commit -m "Update version references from ${FROM} to ${TO}" \ | |
| -m "Automated changes after branch rename:" \ | |
| -m "- GitHub blob/tree URLs in documentation" \ | |
| -m "- Workflow branch triggers" \ | |
| -m "- composer.json branch-alias" \ | |
| -m "- Issue and PR template links" \ | |
| -m "- Bugfix version: ${FROM_BUGFIX} → ${TO_BUGFIX}" \ | |
| -m "" \ | |
| -m "Files excluded: CHANGELOG.md, UPGRADE.md, deprecations, migrations" | |
| # Push PR branch | |
| git push origin "${PR_BRANCH}" | |
| # Create draft PR via GitHub API | |
| PR_TITLE="Update version references: ${FROM} → ${TO}" | |
| PR_BODY="## Automated Version Reference Update" | |
| PR_BODY="${PR_BODY}\n\nAfter renaming \\\`${FROM}\\\` → \\\`${TO}\\\`, this PR updates version references." | |
| PR_BODY="${PR_BODY}\n\n### ✅ Updated:" | |
| PR_BODY="${PR_BODY}\n- Documentation URLs (\\\`blob/${FROM}/\\\` → \\\`blob/${TO}/\\\`)" | |
| PR_BODY="${PR_BODY}\n- Workflow \\\`branches:\\\` triggers" | |
| PR_BODY="${PR_BODY}\n- Workflow \\\`matrix.pimcore_version\\\` references" | |
| PR_BODY="${PR_BODY}\n- composer.json \\\`branch-alias\\\` (\\\`${FROM_VER}.x-dev\\\` → \\\`${TO_VER}.x-dev\\\`)" | |
| PR_BODY="${PR_BODY}\n- Issue/PR template links" | |
| PR_BODY="${PR_BODY}\n- Bugfix version references (\\\`${FROM_BUGFIX}\\\` → \\\`${TO_BUGFIX}\\\`)" | |
| PR_BODY="${PR_BODY}\n\n### ❌ Intentionally NOT changed:" | |
| PR_BODY="${PR_BODY}\n- CHANGELOG.md, UPGRADE.md (preserve history)" | |
| PR_BODY="${PR_BODY}\n- Deprecation notices" | |
| PR_BODY="${PR_BODY}\n- Migration guides" | |
| PR_BODY="${PR_BODY}\n- Hardcoded version checks in code" | |
| PR_BODY="${PR_BODY}\n\n**Please review before merging!**" | |
| CREATE_PR=$(curl -s -w "\nHTTP_STATUS:%{http_code}" -X POST \ | |
| "https://api.github.com/repos/${OWNER}/${REPO}/pulls" \ | |
| -H "Authorization: Bearer ${RELEASE_TOKEN}" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -d "$(jq -n \ | |
| --arg title "${PR_TITLE}" \ | |
| --arg head "${PR_BRANCH}" \ | |
| --arg base "${DEFAULT_BRANCH}" \ | |
| --arg body "${PR_BODY}" \ | |
| '{title:$title, head:$head, base:$base, body:$body, draft:true}')") | |
| HTTP_CODE=$(echo "${CREATE_PR}" | tail -n1 | cut -d: -f2) | |
| if [[ "${HTTP_CODE}" == "201" ]]; then | |
| PR_URL=$(echo "${CREATE_PR}" | sed '$d' | jq -r '.html_url') | |
| echo "✅ Created draft PR: ${PR_URL}" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "⚠️ Failed to create PR (HTTP ${HTTP_CODE})" >> "$GITHUB_STEP_SUMMARY" | |
| echo "$(echo "${CREATE_PR}" | sed '$d' | head -c 500)" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| # Cleanup | |
| cd / | |
| rm -rf "${WORK_DIR}" | |
| summary: | |
| name: Release summary | |
| runs-on: ubuntu-latest | |
| needs: [prepare, create-release, select-release, milestone-automation, branch-automation] | |
| if: always() | |
| steps: | |
| - name: Generate summary | |
| run: | | |
| echo "## Release Workflow Complete" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| if [[ "${{ needs.select-release.result }}" == "success" ]]; then | |
| echo "**Repository**: ${{ needs.select-release.outputs.owner }}/${{ needs.select-release.outputs.repo }}" >> "$GITHUB_STEP_SUMMARY" | |
| echo "**Tag**: v${{ needs.select-release.outputs.tag }}" >> "$GITHUB_STEP_SUMMARY" | |
| echo "**Release Type**: ${{ needs.select-release.outputs.release_kind }}" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "**Repository**: ${{ needs.prepare.outputs.owner }}/${{ needs.prepare.outputs.repo }}" >> "$GITHUB_STEP_SUMMARY" | |
| echo "**Tag**: ${{ needs.prepare.outputs.tag_name }}" >> "$GITHUB_STEP_SUMMARY" | |
| echo "**Release Type**: ${{ needs.prepare.outputs.release_kind }}" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| echo "**Draft**: ${{ inputs.publish_immediately == false }}" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| if [[ "${{ needs.create-release.result }}" == "success" ]]; then | |
| echo "✅ Release created successfully" >> "$GITHUB_STEP_SUMMARY" | |
| elif [[ "${{ needs.create-release.result }}" == "skipped" ]]; then | |
| echo "⏭️ Release creation skipped" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "❌ Release creation failed" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| if [[ "${{ needs.milestone-automation.result }}" == "success" ]]; then | |
| echo "✅ Milestone automation completed" >> "$GITHUB_STEP_SUMMARY" | |
| elif [[ "${{ needs.milestone-automation.result }}" == "skipped" ]]; then | |
| echo "⏭️ Milestone automation skipped" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "❌ Milestone automation failed" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| if [[ "${{ needs.branch-automation.result }}" == "success" ]]; then | |
| echo "✅ Branch automation completed" >> "$GITHUB_STEP_SUMMARY" | |
| elif [[ "${{ needs.branch-automation.result }}" == "skipped" ]]; then | |
| echo "⏭️ Branch automation skipped (not major/minor release)" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "❌ Branch automation failed" >> "$GITHUB_STEP_SUMMARY" | |
| fi |