Skip to content

Create release

Create release #106

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