Skip to content

update: create release yaml#60

Open
berfinyuksel wants to merge 30 commits intomainfrom
create_release_update
Open

update: create release yaml#60
berfinyuksel wants to merge 30 commits intomainfrom
create_release_update

Conversation

@berfinyuksel
Copy link

No description provided.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the GitHub Actions release workflow by improving code quality, fixing typos, and adding milestone management capabilities.

Changes:

  • Fixed indentation and spelling errors throughout the workflow ("prerelase" → "prerelease", "exitence" → "existence")
  • Added milestone management features: ability to close milestones, create new milestones, and move issues between milestones
  • Improved shell script robustness with better error handling, input validation, and variable quoting

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


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}"
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The newline escape sequence '\n' in FINAL_BODY will be interpreted literally by the shell, not as actual newlines. Consider using $'\n' syntax or a different approach to insert newlines, such as using actual newlines in the string or printf formatting.

Suggested change
FINAL_BODY="${GEN_NOTES}\n\n---\n\n${CHANGELOG}"
FINAL_BODY="${GEN_NOTES}"$'\n\n---\n\n'"${CHANGELOG}"

Copilot uses AI. Check for mistakes.
TARGET_REPO: ${{ needs.display-parameters.outputs.target_repo }}
LATEST: ${{ needs.display-parameters.outputs.latest }}
PRERELEASE: ${{ needs.display-parameters.outputs.prerelease }}
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RELEASE_TOKEN secret is now only available in the create-release job environment context (line 135), but it was previously available globally (removed from line 57). The milestone-maintenance job steps that need RELEASE_TOKEN declare it in their step-level env (lines 291, 343, 411), which is correct. However, this could be confusing - consider documenting why RELEASE_TOKEN is scoped differently or making it consistent across jobs.

Copilot uses AI. Check for mistakes.
needs: create-release

# Run only when at least one milestone-related input is provided
if: ${{ needs.create-release.result == 'success' && (inputs.close_milestone != '' || inputs.new_milestone != '' || inputs.move_issues != '') }}
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition uses legacy != '' syntax for checking non-empty strings. While this works, it's inconsistent with the bash-style [[ -n ]] checks used throughout the shell scripts. Consider using GitHub Actions' native syntax for consistency, or document the intentional use of != '' for readability.

Copilot uses AI. Check for mistakes.
type: string
description: 'The tag name of the release with leading v'
release_type:
description: 'Choose relase type'
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in description: "relase" should be "release".

Suggested change
description: 'Choose relase type'
description: 'Choose release type'

Copilot uses AI. Check for mistakes.
options:
options:
- latest
- prerelase
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in option value: "prerelase" should be "prerelease".

Suggested change
- prerelase
- prerelease

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +79
# Normalize release_type (keep accepting "prerelase" input option)
if [[ "${RELEASE_TYPE}" == "prerelase" ]]; then
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code normalizes "prerelase" input but the option value still contains the typo. Users selecting "prerelase" from the choice menu will have their selection properly handled by the normalization logic on line 79, but the typo should be fixed in the option list to prevent confusion. Consider whether backwards compatibility is needed for this workflow_dispatch input.

Suggested change
# Normalize release_type (keep accepting "prerelase" input option)
if [[ "${RELEASE_TYPE}" == "prerelase" ]]; then
# Normalize release_type (accept both legacy "prerelase" and correct "prerelease")
if [[ "${RELEASE_TYPE}" == "prerelase" || "${RELEASE_TYPE}" == "prerelease" ]]; then

Copilot uses AI. Check for mistakes.

echo "Closed milestone '${INPUT}' (number: ${MS_NUMBER})" >> "$GITHUB_STEP_SUMMARY"

- name: Create new milestone (by title or number) if provided (reuse if exists)
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent comment style: This comment uses lowercase "by title or number" while the similar comment on line 287 uses the same format. However, the comment on line 338 says "by title or number" when it should clarify that providing a number means "reuse existing milestone" rather than creating by number. The behavior differs from the close_milestone parameter where a number directly references a milestone to close.

Suggested change
- name: Create new milestone (by title or number) if provided (reuse if exists)
- name: Create or reuse milestone (create by title, reuse by number) if provided

Copilot uses AI. Check for mistakes.
@berfinyuksel berfinyuksel changed the title update; create release update: create release yaml Jan 19, 2026
Comment on lines +88 to +102
if [[ "${BASE}" == *.x ]]; then
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

TAG_NAME="v${TAG}"
RELEASE_NAME="${TAG}"

BASE_IS_X=false
if [[ "${BASE}" == *.x ]]; then
BASE_IS_X=true
fi
Copy link
Contributor

@kingjia90 kingjia90 Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if [[ "${BASE}" == *.x ]]; then
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
TAG_NAME="v${TAG}"
RELEASE_NAME="${TAG}"
BASE_IS_X=false
if [[ "${BASE}" == *.x ]]; then
BASE_IS_X=true
fi
BASE_IS_X=false
if [[ "${BASE}" == *.x ]]; then
BASE_IS_X=true
fi
if $[[ "${BASE_IS_X}" == "true"]]; then
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
TAG_NAME="v${TAG}"
RELEASE_NAME="${TAG}"

micro-optimization since if [[ "${BASE}" == *.x ]]; then appeared twice in this block

Copy link
Contributor

@kingjia90 kingjia90 Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably won't need to be quoted comparison for every if $[[ "${BASE_IS_X}" == "true"]];, just IF $BASE_IS_X; would be enough

Comment on lines +128 to +135
MAKE_LATEST=false
PRERELEASE=false

if [[ "${DRAFT}" == "false" ]]; then
if [[ "${RELEASE_KIND}" == "major" || "${RELEASE_KIND}" == "minor" ]]; then
MAKE_LATEST=true
fi
fi
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAKE_LATEST need some refactoring in the logic here, it is actually possible to tag a bugfix only and it would be the latest (eg. in bugfix only release cycle)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically , it should be always LATEST and when we do releases in order, we start from bugfix and then minor and then major, or at least publish the draft in this order and it would be properly one on top of each other

Copy link
Contributor

@kingjia90 kingjia90 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some inputs to avoid duplicated code

permissions:
contents: write
needs: prepare
if: ${{ inputs.publish_immediately == false }}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the steps for draft and instant publish are mostly duplicated, making it more hard to maintain in case of changes, this IF should be removed

Suggested change
if: ${{ inputs.publish_immediately == false }}

and name adapted accordingly and changed as below

LATEST_SHA: ${{ needs.prepare.outputs.latest_sha }}
TAG_NAME: ${{ needs.prepare.outputs.tag_name }}
RELEASE_NAME: ${{ needs.prepare.outputs.release_name }}
DRAFT: ${{ needs.prepare.outputs.draft }}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
DRAFT: ${{ needs.prepare.outputs.draft }}
DRAFT: ${{ inputs.publish_immediately == false }}

Comment on lines +510 to +513
if [[ "${COUNT}" -lt 3 ]]; then
echo "Tag '${TAG}' must have at least 3 parts for major/minor/bugfix." >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
Copy link
Contributor

@kingjia90 kingjia90 Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this check could/should be removed here and done before the draft even being created
https://github.com/pimcore/workflows-collection-public/pull/60/files#diff-a318819fb0ec8889018cbef8e6dd222e317757d95ddd93b62b714ac7c49f04f6R117

in other words, this IF should be unreachable

by looking at the exit 0 on if [[ "${KIND}" == "hotfix" ]]; then, the code below this should in a else block

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 14 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +664 to +692
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}")
local LEN
LEN=$(echo "${RESP}" | jq '. | length')
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)
if [[ -n "${NUM}" && "${NUM}" != "null" ]]; then
echo "${NUM}"
return 0
fi
PAGE=$((PAGE+1))
if [[ "${PAGE}" -gt 20 ]]; then
echo ""
return 0
fi
done
}
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The milestone resolution logic at lines 664-692 is defined as a function within a single step. This pattern is repeated in multiple places with variations (lines 753-772, 603-626, 555-576). Consider consolidating this into a reusable utility function to reduce code duplication and improve maintainability.

Copilot uses AI. Check for mistakes.
run: |
set -euo pipefail

if ! [[ "${REPOSITORY}" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation regex for repository format allows dots in repository names, which is non-standard for GitHub repositories. GitHub repository names can contain alphanumeric characters, hyphens, and underscores, but typically not dots. Consider if this is intentional or if the regex should be more restrictive.

Suggested change
if ! [[ "${REPOSITORY}" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then
if ! [[ "${REPOSITORY}" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_-]+$ ]]; then

Copilot uses AI. Check for mistakes.
echo "Invalid to_tag format. Expected X.Y.Z or X.Y.Z.W (numeric), got: ${TAG}" >> "$GITHUB_STEP_SUMMARY"
exit 1
fi

Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version format validation at line 83 allows a fourth component (X.Y.Z.W) but doesn't validate that each component is within reasonable bounds. Very large version numbers could cause issues downstream. Consider adding validation for the numeric range of version components.

Suggested change
# Enforce reasonable numeric bounds on each version component to avoid overflow issues downstream
MAX_VERSION_COMPONENT=2147483647
IFS='.' read -r -a VERSION_PARTS <<< "${TAG}"
for part in "${VERSION_PARTS[@]}"; do
if (( part < 0 || part > MAX_VERSION_COMPONENT )); then
echo "Invalid to_tag component '${part}'. Each numeric component must be between 0 and ${MAX_VERSION_COMPONENT}." >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
done

Copilot uses AI. Check for mistakes.
-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
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At line 862, the curl command uses 'jq -n' to construct JSON with a state field, but the value 'closed' is passed as a bare string which will be treated as a JSON string literal. While this appears to be correct, the inconsistency with how other API calls construct their JSON (using --arg for string values) could lead to confusion. Consider using consistent JSON construction patterns throughout.

Suggested change
-d "$(jq -n '{state:"closed"}')" >/dev/null
-d "$(jq -n --arg state "closed" '{state:$state}')" >/dev/null

Copilot uses AI. Check for mistakes.
fi
done

PAGE=$((PAGE+1))
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HTTP status code checking at lines 730-732 limits pagination to 50 pages (5000 issues) for bugfix releases, while lines 828-832 use the same 50-page limit for minor releases. However, lines 573-575 and 622-624 use a 20-page limit for milestone searches. This inconsistency in pagination limits could lead to unpredictable behavior. Consider standardizing the pagination limits or documenting why different limits are used.

Suggested change
PAGE=$((PAGE+1))
PAGE=$((PAGE+1))
# NOTE: This job intentionally allows up to 50 pages (5000 issues) when moving
# bugfix milestone issues. Other milestone/search steps in this workflow may
# use a lower page limit (e.g. 20 pages) for performance, but this higher
# limit ensures large bugfix milestones are fully processed.

Copilot uses AI. Check for mistakes.
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
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling at lines 643-645 exits with status 1 when failing to create a milestone, which will fail the entire workflow. However, this is inside a loop that's trying to create multiple milestones. A failure to create one milestone will prevent the creation of subsequent milestones. Consider whether partial success should be allowed or if all-or-nothing is the desired behavior.

Suggested change
exit 1
continue

Copilot uses AI. Check for mistakes.
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' }}
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition checking at line 655 uses a simple equality comparison for the RELEASE_KIND environment variable, but the RELEASE_KIND is passed from the previous job's output. If the previous job fails or is skipped, this could lead to unexpected behavior. Consider adding additional safety checks to ensure RELEASE_KIND is properly set before using it in conditions.

Suggested change
if: ${{ steps.plan.outputs.do_milestones == 'true' && steps.plan.outputs.move_from_title != '' && steps.plan.outputs.move_to_title != '' && env.RELEASE_KIND == 'bugfix' }}
if: ${{ steps.plan.outputs.do_milestones == 'true' && steps.plan.outputs.move_from_title != '' && steps.plan.outputs.move_to_title != '' && env.RELEASE_KIND != '' && env.RELEASE_KIND == 'bugfix' }}

Copilot uses AI. Check for mistakes.

CREATE_RELEASE=$(curl -v -w "\nHTTP_STATUS:%{http_code}" -X POST --location "https://api.github.com/repos/pimcore/$TARGET_REPO/releases" \
RELEASE_URL=$(echo "${CREATE_RELEASE}" | sed '$d' | jq -r '.html_url // ""')
echo "Release created (draft): ${RELEASE_URL}" >> "$GITHUB_STEP_SUMMARY"
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The release URL extraction at line 310 uses 'sed' and 'jq' to parse the response, but doesn't validate if the URL is empty or null before writing to the summary. If the API response is malformed, this could result in a confusing summary message. Consider adding validation to ensure RELEASE_URL is not empty before logging it.

Suggested change
echo "Release created (draft): ${RELEASE_URL}" >> "$GITHUB_STEP_SUMMARY"
if [[ -n "${RELEASE_URL}" ]]; then
echo "Release created (draft): ${RELEASE_URL}" >> "$GITHUB_STEP_SUMMARY"
else
echo "Release created (draft), but release URL could not be determined from API response." >> "$GITHUB_STEP_SUMMARY"
fi

Copilot uses AI. Check for mistakes.
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') }}
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The branch-automation job at line 871 has a compound condition that checks if release_kind is 'major' or 'minor'. However, this condition is evaluated at the job level, which means if the select-release job fails but sets outputs, the condition might still evaluate incorrectly. Consider adding explicit null/empty checks for the release_kind output value.

Suggested change
if: ${{ !cancelled() && needs.select-release.result == 'success' && (needs.select-release.outputs.release_kind == 'major' || needs.select-release.outputs.release_kind == 'minor') }}
if: ${{ !cancelled() && needs.select-release.result == 'success' && needs.select-release.outputs.release_kind != '' && needs.select-release.outputs.release_kind != null && (needs.select-release.outputs.release_kind == 'major' || needs.select-release.outputs.release_kind == 'minor') }}

Copilot uses AI. Check for mistakes.
Comment on lines +605 to +607
while true; do
RESP=$(curl -s --location \
"https://api.github.com/repos/${OWNER}/${REPO}/milestones?state=all&per_page=100&page=${PAGE}" \
Copy link
Contributor

@kingjia90 kingjia90 Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to loop through all (open/closed) milestones, the direct get should be enough to check if it's FOUND or not
https://docs.github.com/en/rest/issues/milestones?apiVersion=2022-11-28#get-a-milestone--status-codes

with paging and going through all, it would be an overkill and slower

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +104 to +119
if [[ "${BASE_IS_X}" == "true" ]]; then
if [[ "${TAG_MAJOR}" != "${BASE_MAJOR}" ]]; then
RELEASE_KIND="major"
else
RELEASE_KIND="minor"
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
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation regex allows versions like "12.4.0" or "12.4.0.1" (up to 4 numeric segments), but the release kind classification logic in lines 104-119 only handles 3 or 4 segments for non-X branches. However, there's no validation that ensures the tag format matches the expected branch format. For example, if base_branch is "12.x" but to_tag is "12.4.0.1" (4 segments), the code classifies it as "minor" on line 108, but hotfix releases (4 segments) shouldn't come from .x branches. This inconsistency could lead to incorrect release classification.

Copilot uses AI. Check for mistakes.
echo "Please create release manually..."
exit 1
# Parse TAG parts
IFS='.' read -r A B C D <<< "${TAG}"
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same variable 'D' is declared but never used here as well. Same issue as in the milestone-automation job - it's declared to handle 4-part version numbers but not referenced in this block. Consider using '_' for the unused variable for clarity.

Suggested change
IFS='.' read -r A B C D <<< "${TAG}"
IFS='.' read -r A B C _ <<< "${TAG}"

Copilot uses AI. Check for mistakes.
Comment on lines +505 to +513
-H "Authorization: Bearer ${RELEASE_TOKEN}")
local LEN
LEN=$(echo "${RESP}" | jq '. | length')
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)
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function variable declarations use 'local' keyword without explicit declaration. While 'local RESP' on line 500 works in bash, the pattern on lines 506 and 512 where variables are declared local and then assigned on the next line using command substitution could fail silently if the command fails. The 'local' command returns 0 even if the assignment fails. Consider combining declaration and assignment 'local LEN=$(...)' or checking command exit status separately to ensure errors are caught by 'set -e'.

Suggested change
-H "Authorization: Bearer ${RELEASE_TOKEN}")
local LEN
LEN=$(echo "${RESP}" | jq '. | length')
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)
-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

Copilot uses AI. Check for mistakes.
Comment on lines +123 to +127
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}")
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'RELEASE_TOKEN' secret is accessed directly in the 'prepare' job step without proper error handling. If this secret is not set or is invalid, the curl command on line 123 will fail silently or provide unclear errors. Consider adding validation to check if RELEASE_TOKEN is set before making API calls, or at minimum improve the error message when the API call fails to indicate that it might be due to missing/invalid credentials.

Copilot uses AI. Check for mistakes.

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}"
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential newline interpretation issue in changelog concatenation. The '\n\n---\n\n' separator uses literal backslash-n characters rather than actual newlines. Depending on how this is interpreted by the GitHub API, this might result in literal '\n' strings appearing in the release notes instead of actual line breaks. Consider using printf or a heredoc to properly format multi-line strings with actual newlines.

Suggested change
FINAL_BODY="${GEN_NOTES}\n\n---\n\n${CHANGELOG}"
FINAL_BODY="$GEN_NOTES"$'\n\n---\n\n'"$CHANGELOG"

Copilot uses AI. Check for mistakes.
echo "move_from_title=${TAG}" >> "$GITHUB_OUTPUT"
echo "move_to_title=${NEXT_PATCH}" >> "$GITHUB_OUTPUT"
fi
fi
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing opening 'else' statement. The 'fi' on this line closes an 'if' statement that was never opened at this level. Looking at the structure, line 349 opens an 'else' block after checking for hotfix, but line 372 has an extra 'fi' that doesn't match any 'if'. This will cause a shell syntax error when the workflow runs.

Copilot uses AI. Check for mistakes.
permissions:
contents: write
needs: prepare
environment: ${{ inputs.publish_immediately && 'production' || '' }}
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The environment protection is conditionally set using a ternary-like expression, but GitHub Actions doesn't support empty string as "no environment". When 'publish_immediately' is false, this evaluates to an empty string, which might not behave as intended. The workflow may either fail or create an environment with an empty name. Consider using a separate job or using 'if' conditions to control whether the production environment is required, rather than trying to conditionally set the environment name.

Suggested change
environment: ${{ inputs.publish_immediately && 'production' || '' }}
environment: production

Copilot uses AI. Check for mistakes.
berfinyuksel added 3 commits January 21, 2026 13:41
- Add validation to prevent 4-part versions from .x branches
- Fix make_latest API parameter to use string type instead of boolean
- Replace unused variable D with underscore
- Fix newline handling in changelog concatenation
- Remove conditional environment (not supported by GitHub Actions)
- Improve error handling in resolve_ms function
Comment on lines +835 to +842
echo "⚠️ **Manual Action Required**: Rename branch '${FROM}' to '${TO}'" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "After renaming, please check and update:" >> "$GITHUB_STEP_SUMMARY"
echo "- composer.json (branch-alias)" >> "$GITHUB_STEP_SUMMARY"
echo "- Workflow files (any hardcoded version references)" >> "$GITHUB_STEP_SUMMARY"
echo "- Documentation files" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "See: https://github.com/${OWNER}/${REPO}/branches" >> "$GITHUB_STEP_SUMMARY"
Copy link
Contributor

@kingjia90 kingjia90 Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be even better if this part of the workflow could do a partial git clone of the affected files (i will provide you the full list), a search and replace via sed in terminal then save the file, git add all, commit and push in the master branch

Copy link
Contributor

@kingjia90 kingjia90 Jan 26, 2026

Copy link
Contributor

@kingjia90 kingjia90 Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous comment was restricted to pimcore/pimcore, for other bundles, might be trickier but
basically matrix.pimcore_version

once core is tagged, go through all repos, mhh.. might be ad hoc step

Copy link
Contributor

@kingjia90 kingjia90 Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

beware the search and replace must be not too greedy but more specific strings, eg. search for (12.3for bug fixes, others12.x) and replace with (13.0for bug fixes, others13.x), because there are places where 12.3, 12.x has to stay like that, eg. deprecations, changelog/upgrade notes, bc-layers, hardcoded comments

Comment on lines +157 to +160
if ! [[ "${BASE_MINOR}" =~ ^[0-9]+$ ]]; then
echo "Invalid base_branch format. Expected X.Y for maintenance branches, got: ${BASE}" >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if ! [[ "${BASE_MINOR}" =~ ^[0-9]+$ ]]; then
echo "Invalid base_branch format. Expected X.Y for maintenance branches, got: ${BASE}" >> "$GITHUB_STEP_SUMMARY"
exit 1
fi

and eg. https://github.com/pimcore/workflows-collection-public/pull/60/files#diff-a318819fb0ec8889018cbef8e6dd222e317757d95ddd93b62b714ac7c49f04f6R117-R120

these more granular guards are not necessary here and redundant (and potentially slowing it down in terms of micro-optimization).

One preventive check at the beginning, like
https://github.com/pimcore/workflows-collection-public/pull/60/files#diff-a318819fb0ec8889018cbef8e6dd222e317757d95ddd93b62b714ac7c49f04f6R105-R108 but for ${BASE} would be enough.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only difference that i would do is to allow the v prefix to be added as user input (in case someone don't know it is not mandatory), and remove it from ${BASE} and have it automatically prefixed later when the v is necessary (for actual tag name), but it would be a nice-to-have: whoever is filling the input should be aware of it, or reminded by the input label.

else
echo "⚠️ Warning: generate-notes failed (HTTP ${GEN_STATUS}); using custom notes only" >> "$GITHUB_STEP_SUMMARY"
fi
fi
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice-to-have: maybe to "follow traditions", back in time when we didn't have generate release note function to rely on, we used to add the closed milestone as a link, see
https://github.com/pimcore/pimcore/releases/tag/v10.3.0
and we kept doing that for a while manually too (both generated and milestone link).

maybe it would be good to add it as a fixed suffix to the FINAL_BODY after a new line, so release notes would be technically never empty and have a link at "worst" case.

else
echo "⚠️ Warning: generate-notes failed (HTTP ${GEN_STATUS}); using custom notes only" >> "$GITHUB_STEP_SUMMARY"
fi
fi
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice-to-have: for EE bundles with a corresponding CE bundle, the generated release notes are like listing the PR of automated sync stuff.
eg. https://github.com/pimcore/ee-admin-ui-classic-bundle/releases/tag/v2.3.0
IIRC some partners complained in the past that this is not useful/meaningful, reason why i copy the same changelog from CE and paste it in EE, when i know there were no PRs for EE only.

https://github.com/pimcore/ee-admin-ui-classic-bundle/releases/tag/v2.2.2

If we could refine this workflow to perform the release on EE and CE at the same time and merge these release notes that could save some time.
More specifically, generate CE and EE release notes, store the CE one, go through the EE notes and remove everything starting with Automated PR by @pimcore-deployments in, prepend the CE notes (while removing Full Changelog: https://github.com/pimcore/ee-admin-ui-classic-bundle/compare/v2.2.0...v2.3.0).

The result would be the CE notes + any non-sync related PR in there.

Copy link
Contributor

@kingjia90 kingjia90 Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might the CE-EE part not be this workflow, which is focused on single repo (iteration).

Maybe for the one that goes through the list of all repos+milestones in a csv format or so.

Comment on lines +294 to +331
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

too much duplicated code, if the difference between these are 2 arguments/parameters

image

could it be refactored to share the identic code? reason is that any follow up on REQ_BODY base would require to edit in both cases and cause a micro-overhead.

FOUND=""
while true; do
RESP=$(curl -s --location \
"https://api.github.com/repos/${OWNER}/${REPO}/milestones?state=open&per_page=100&page=${PAGE}" \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still might need to refactor as
#60 (comment)

berfinyuksel added 6 commits January 30, 2026 12:46
- Strip leading v/V from to_tag input for flexibility
- Auto-determine base branch from tag pattern (X.x for major/minor, X.Y for bugfix/hotfix)
- Add upfront BASE validation (X.x or X.Y format)
- Remove redundant BASE_MINOR validation check
- Introduce JQ_ARGS array with common arguments
- Reduce code repetition in conditional REQ_BODY construction
- Maintain target_commitish as BASE_BRANCH for branch-targeted releases
- Try creating milestone directly instead of paginating to check existence
- Handle 422 (already exists) with single GET request to first page
- Reduces API calls from 20+ pages to 1-2 calls per milestone
- Eliminates unnecessary pagination loops
- Search open milestones first (most likely), then closed if not found
- Add sort=created&direction=desc for better relevance
- Limit to 3 pages per state (300 milestones max per state)
- Reduces API calls from 20 pages (2000 milestones) to max 6 pages (600)
- Reuse created_map from milestone creation in all move steps
- Reuse close_ms_number from find step in bugfix move (eliminates lookups)
- Replace grep -oP with awk for robust created_map parsing
- Extract title and number together with tab-separated jq output
- Eliminates 40+ redundant API calls per release (85-90% reduction)
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +378 to +379
TAG="${TAG}"
KIND="${RELEASE_KIND}"
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant variable assignments. The variables TAG and KIND are being assigned to themselves, which has no effect. These lines can be removed or if the intent was to copy environment variables to local shell variables, the assignment should reference the environment variables properly.

Copilot uses AI. Check for mistakes.
Comment on lines +391 to +416
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"
if [[ "${A}" =~ ^[0-9]+$ && "${A}" -gt 0 ]]; then
PREV_MAJOR="$((A-1))."
echo "major_move_prefix=${PREV_MAJOR}" >> "$GITHUB_OUTPUT"
echo "move_to_title=${NEXT_PATCH}" >> "$GITHUB_OUTPUT"
fi
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
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect indentation. The if block starting with "if [[ "${KIND}" == "major" ]];" should be indented to align with the surrounding code structure. The current indentation makes the code harder to read and violates standard bash scripting conventions.

Copilot uses AI. Check for mistakes.
Comment on lines +825 to +826
TAG="${TAG}"
KIND="${RELEASE_KIND}"
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant variable assignments. The variables TAG and KIND are being assigned to themselves, which has no effect. These lines can be removed or if the intent was to copy environment variables to local shell variables, the assignment should reference the environment variables properly.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 15 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +422 to +457
- 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 }}"

# Try open milestones first (most likely), then closed if not found
for STATE in open closed; do
PAGE=1
MS_NUMBER=""
while [[ "${PAGE}" -le 3 ]]; do
RESP=$(curl -s --location \
"https://api.github.com/repos/${OWNER}/${REPO}/milestones?state=${STATE}&sort=created&direction=desc&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
echo "close_ms_number=${MS_NUMBER}" >> "$GITHUB_OUTPUT"
echo "Found milestone '${TITLE}' as #${MS_NUMBER} (state=${STATE})" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi

PAGE=$((PAGE+1))
done
done

echo "⚠️ Close milestone not found by title: ${TITLE} (searched 600 recent milestones; will not close)" >> "$GITHUB_STEP_SUMMARY"
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The milestone search logic iterates through only the first 3 pages (300 milestones) for each state. If a repository has more than 600 total milestones, the target milestone might not be found even if it exists. The warning message mentions "searched 600 recent milestones" which is accurate, but consider if this limit is sufficient for repositories with many milestones. Alternatively, consider using the milestone title as a search parameter if the GitHub API supports it.

Copilot uses AI. Check for mistakes.
Comment on lines +341 to +370
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 }}
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The select-release job serves only as a pass-through for outputs from the create-release job. This intermediate job appears unnecessary and adds complexity. The outputs from create-release could be consumed directly by the downstream jobs (milestone-automation and branch-automation) without this intermediary step. Consider removing this job to simplify the workflow structure.

Suggested change
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 }}
milestone-automation:
name: Milestone automation
runs-on: ubuntu-latest
permissions:
issues: write
contents: read
needs: [create-release]
if: ${{ !cancelled() && needs.create-release.result == 'success' }}
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
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 }}

Copilot uses AI. Check for mistakes.
Comment on lines +829 to 837
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"
else
RELEASE_URL=$(echo "$CREATE_RELEASE" | awk -F'"' '/"html_url":/ {print $4; exit}')
echo "Release created successfully: $RELEASE_URL" >> $GITHUB_STEP_SUMMARY
echo "Cannot determine previous major version for tag ${TAG}" >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The branch automation only runs for major and minor releases, but the logic doesn't handle the case where a major release is A.0.0 and A=1 (i.e., the first major version). In line 830-836, when A=1, PREV_MAJOR would be 0, and trying to clone from "0.x" branch might not be appropriate. Consider handling the edge case of the first major release (1.0.0) differently, or at least adding a validation to skip branch cloning if PREV_MAJOR=0.

Copilot uses AI. Check for mistakes.
name: Release summary
runs-on: ubuntu-latest
needs: [prepare, create-release, select-release, milestone-automation, branch-automation]
if: always()
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The permissions section is missing from the prepare and summary jobs. While these jobs might not need write permissions, it's a best practice to explicitly declare permissions for all jobs in a workflow to follow the principle of least privilege. Consider adding an explicit permissions: block to these jobs, even if it's just contents: read or permissions: {} if no permissions are needed.

Suggested change
if: always()
if: always()
permissions:
contents: read

Copilot uses AI. Check for mistakes.
exit 1
fi
fi

Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PRERELEASE variable is hardcoded to false at line 188, which means all releases created by this workflow will never be marked as prereleases. This seems inconsistent with the comprehensive release type detection logic (major, minor, bugfix, hotfix). If the workflow needs to support creating prereleases (e.g., alpha, beta, RC versions), this logic would need to be extended. If prereleases are intentionally not supported, consider adding a comment explaining this decision.

Suggested change
# Pre-releases are intentionally not supported by this workflow.
# If pre-release handling is required (e.g. alpha/beta/RC tags),
# extend the logic above to detect such tags and set PRERELEASE accordingly.

Copilot uses AI. Check for mistakes.
Comment on lines +790 to +802
- 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 "$REQ_BODY")
-H "Authorization: Bearer ${RELEASE_TOKEN}" \
-d "$(jq -n '{state:"closed"}')" >/dev/null
echo "Closed milestone #${MS_NUMBER} (after moving issues)" >> "$GITHUB_STEP_SUMMARY"
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The close milestone step uses --fail-with-body flag with curl but doesn't check the HTTP status code. If the PATCH request fails, the workflow will exit but won't provide detailed error information in the summary. Consider capturing the response and status code to provide better error messages, similar to how other API calls in the workflow handle errors.

Copilot uses AI. Check for mistakes.
Comment on lines +907 to +915
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
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The job uses both needs.prepare.outputs.* and needs.select-release.outputs.* depending on whether select-release succeeded. However, this fallback logic might not work as expected. If select-release fails, its outputs will be undefined (not populated), but the condition needs.select-release.result == 'success' is only checked once at line 907. The else block at line 911 tries to use needs.prepare.outputs.tag_name and other prepare outputs, but prepare.outputs.tag_name exists while select-release would have tag (different names). This inconsistency could cause issues. Verify that the output names match what's actually available from each job.

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +82
# 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
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow uses RELEASE_TOKEN secret extensively but doesn't validate its scope/permissions before performing operations. While there's a basic validity check at line 67-82, this only checks that the token is valid for authentication, not that it has the necessary permissions (repo access, milestone write, branch creation, etc.). Consider adding more specific permission checks early in the workflow, or at least improve error messages when operations fail due to insufficient permissions.

Copilot uses AI. Check for mistakes.
Comment on lines +280 to +282
MAKE_LATEST_STR="true"
if [[ "${MAKE_LATEST}" == "false" ]]; then
MAKE_LATEST_STR="false"
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The make_latest field in the GitHub Releases API accepts a string with specific values: "true", "false", or "legacy". The current implementation correctly converts the boolean environment variable to a string. However, this conversion logic has a flaw: when MAKE_LATEST is anything other than exactly "false", it will be set to "true". This could mask issues if the variable is undefined or has an unexpected value. Consider using a more explicit check like if [[ "${MAKE_LATEST}" == "true" ]]; then MAKE_LATEST_STR="true"; else MAKE_LATEST_STR="false"; fi to ensure proper handling of all cases.

Suggested change
MAKE_LATEST_STR="true"
if [[ "${MAKE_LATEST}" == "false" ]]; then
MAKE_LATEST_STR="false"
MAKE_LATEST_STR="false"
if [[ "${MAKE_LATEST}" == "true" ]]; then
MAKE_LATEST_STR="true"

Copilot uses AI. Check for mistakes.

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}"
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When both auto-generated notes and custom changelog are provided, they are concatenated with the auto-generated notes first. The separator "---" is used between them. However, if the auto-generated notes are empty or the API returns an empty body, this will result in starting with a newline and separator followed by the custom notes, which may look awkward. Consider checking if GEN_NOTES is non-empty before concatenating.

Suggested change
FINAL_BODY="${GEN_NOTES}"$'\n\n---\n\n'"${CHANGELOG}"
if [[ -n "${GEN_NOTES}" ]]; then
FINAL_BODY="${GEN_NOTES}"$'\n\n---\n\n'"${CHANGELOG}"
else
FINAL_BODY="${CHANGELOG}"
fi

Copilot uses AI. Check for mistakes.
berfinyuksel added 5 commits January 30, 2026 14:36
- Restore base_branch input field (12.x or 12.3)
- Fetch latest SHA from base branch during validation
- Use LATEST_SHA as target_commitish for release creation
- Add SHA to summary output for transparency
- Simplify tag parsing (no 'v' prefix handling in input)
Consolidated "Move issues (minor)" and "Move issues (major)" steps into a single
parameterized step "Move issues by prefix (major/minor)". This eliminates ~170 lines
of duplicated code and reduces maintenance burden.

Benefits:
- Single source of truth for prefix-based issue movement logic
- Dynamic prefix selection based on RELEASE_KIND variable
- Reduced risk of inconsistent updates between major/minor workflows
- Improved maintainability and readability
The summary job's fallback logic had a naming mismatch that would cause
incorrect tag display. When select-release fails and the fallback uses
prepare outputs, it was using 'tag_name' (which includes "v" prefix)
and adding another "v", resulting in "vv12.4.0".

Changed to use 'release_name' (without "v" prefix) and add "v" explicitly,
ensuring both success and fallback paths display tags consistently.
…lestones

- Add HTTP status validation for all issue/PR fetch API calls
- Add JSON response type validation to ensure we get arrays
- Add detailed logging showing how many items found per page
- Add individual failure tracking for each item move operation
- Improve error messages to distinguish between truly empty milestones vs API failures

This fixes the silent failure where PR #388 wasn't detected in milestone 2.4.1
because the API call failed without proper error handling.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +105 to +106
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"
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex pattern for validating tag format allows numeric-only components but doesn't validate against invalid semantic versioning patterns such as leading zeros (e.g., "01.02.03"). While this may be intentional, consider adding validation to reject versions with leading zeros if they violate the project's versioning conventions.

Suggested change
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"
if ! [[ "${TAG}" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))?$ ]]; then
echo "Invalid to_tag format. Expected X.Y.Z or X.Y.Z.W (numeric, no leading zeros), got: ${TAG}" >> "$GITHUB_STEP_SUMMARY"

Copilot uses AI. Check for mistakes.
FOUND=false
for STATE in open closed; do
PAGE=1
while [[ "${PAGE}" -le 3 ]]; do
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pagination logic stops after 3 pages (line 397), which limits milestone search to 300 items. If the repository has more than 300 milestones across open and closed states, the target milestone might not be found even if it exists. Consider increasing the page limit or adding a comment explaining why 3 pages is sufficient for this use case.

Suggested change
while [[ "${PAGE}" -le 3 ]]; do
# Allow searching through more pages to avoid missing milestones in large repositories.
# With per_page=100 and MAX_MILESTONE_PAGES=100, we can scan up to 10,000 milestones per state.
MAX_MILESTONE_PAGES=100
while [[ "${PAGE}" -le "${MAX_MILESTONE_PAGES}" ]]; do

Copilot uses AI. Check for mistakes.
Comment on lines +586 to +602
# If not in created_map, search open milestones (should be there since just created)
if [[ -z "${TO_NUM}" ]]; then
RESP=$(curl -s --location \
"https://api.github.com/repos/${OWNER}/${REPO}/milestones?state=open&sort=created&direction=desc&per_page=100" \
-H "X-GitHub-Api-Version: 2022-11-28" \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${RELEASE_TOKEN}")
TO_NUM=$(echo "${RESP}" | jq -r --arg t "${TO_TITLE}" '.[] | select(.title == $t) | .number' | head -n 1)
fi

# Get FROM_NUM from validated milestone
FROM_NUM="${{ steps.ms_find.outputs.close_ms_number }}"

if [[ -z "${FROM_NUM}" || "${FROM_NUM}" == "null" || -z "${TO_NUM}" || "${TO_NUM}" == "null" ]]; then
echo "⚠️ Cannot move issues: milestone numbers not found (from='${FROM_TITLE}', to='${TO_TITLE}')" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a potential race condition in the "Move issues for bugfix" step. If the milestone was just created in the previous step and issues are being fetched immediately, GitHub's API might not have fully indexed the new milestone yet. Consider adding a small delay (1-2 seconds) after milestone creation before attempting to query for issues, or add retry logic if the milestone isn't found.

Copilot uses AI. Check for mistakes.
Comment on lines +863 to 871
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"
else
RELEASE_URL=$(echo "$CREATE_RELEASE" | awk -F'"' '/"html_url":/ {print $4; exit}')
echo "Release created successfully: $RELEASE_URL" >> $GITHUB_STEP_SUMMARY
echo "Cannot determine previous major version for tag ${TAG}" >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the branch automation logic for major releases, if the tag is "1.0.0", the calculated PREV_MAJOR would be 0, resulting in CLONE_FROM being "0.x". The code should validate that PREV_MAJOR is greater than or equal to a minimum version (e.g., 1) or handle the first major release as a special case where no branch cloning is needed.

Copilot uses AI. Check for mistakes.
Comment on lines +902 to +904
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
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow creates branches for maintenance but doesn't verify if the source branch exists before attempting to create the new branch. If the CLONE_FROM branch doesn't exist, the operation will fail on lines 894-905. While the error is caught and reported, for the first major version release (e.g., 1.0.0 trying to clone from 0.x), this will always fail with a confusing error message. Consider adding a check to see if this is the first release and skip branch creation in that case.

Suggested change
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
if [[ "${SHA_STATUS}" -eq 404 ]]; then
echo "ℹ️ Source branch '${FROM}' not found (HTTP 404). Assuming this is the first release and skipping maintenance branch creation." >> "$GITHUB_STEP_SUMMARY"
exit 0
else
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

Copilot uses AI. Check for mistakes.

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}"
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When both auto-generated notes and custom notes are requested, the auto-generated notes are placed BEFORE the custom notes (line 283). This ordering might be unexpected for users who provide custom notes intending them to be prominent. Consider documenting this behavior in the input description or reversing the order so custom notes appear first.

Copilot uses AI. Check for mistakes.
Comment on lines +393 to +432
# Search for milestone in open and closed states
FOUND=false
for STATE in open closed; do
PAGE=1
while [[ "${PAGE}" -le 3 ]]; do
MS_RESP=$(curl -s -w "\nHTTP_STATUS:%{http_code}" --location \
"https://api.github.com/repos/${OWNER}/${REPO}/milestones?state=${STATE}&sort=created&direction=desc&per_page=100&page=${PAGE}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${RELEASE_TOKEN}")

MS_STATUS=$(echo "${MS_RESP}" | awk -F: '/HTTP_STATUS:/ {print $2}')
if [[ "${MS_STATUS}" -ne 200 ]]; then
echo "⚠️ Failed to fetch milestones (state=${STATE}, page=${PAGE}, HTTP ${MS_STATUS})" >> "$GITHUB_STEP_SUMMARY"
break
fi

RESP=$(echo "${MS_RESP}" | sed '$d')

if ! echo "${RESP}" | jq -e '. | type == "array"' > /dev/null 2>&1; then
echo "⚠️ Invalid API response (not a JSON array)" >> "$GITHUB_STEP_SUMMARY"
break
fi

if [[ $CHECK_TAG_NAME -eq 200 ]]; then
echo "Tag name already exists: $TAG_NAME" >> $GITHUB_STEP_SUMMARY
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
echo "milestone_number=${MS_NUMBER}" >> "$GITHUB_OUTPUT"
echo "✅ Found milestone '${TITLE}' (#${MS_NUMBER}, state=${STATE})" >> "$GITHUB_STEP_SUMMARY"
FOUND=true
break 2
fi

PAGE=$((PAGE+1))
done
done
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The milestone search iterates through both open and closed milestones, making multiple API calls (up to 6 total: 3 pages × 2 states). For repositories with many milestones, this could be slow and consume API rate limits. Consider optimizing by searching open milestones first (most likely case for new releases), and only searching closed milestones if not found in open ones.

Copilot uses AI. Check for mistakes.
berfinyuksel and others added 5 commits February 6, 2026 12:38
When searching for milestones by prefix (e.g., '1.1.'), also match
milestones with exact version without dot (e.g., '1.1'). This ensures
milestone '1.1' is found when releasing 1.2.0 and moving its issues.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants