From 179b07f0f457aeee986338b74da6a382c3886d1f Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sat, 1 Aug 2026 06:36:23 +0000 Subject: [PATCH] Fix the Pull Request association and the second artifact set PrepareJob.yml -------------- 'FindPullRequest' looked up a merged Pull Request whose base is the release branch, but it ran for every merge commit. A merge commit pushed to the development branch comes from a Pull Request based on the development branch, so the search returned an empty list and the step - and with it the whole 'Prepare' job - failed. Guard the step with 'is_release_commit', which 'Classify' already computes as 'is_merge_commit' restricted to the main and version branches, and which 'TriggerTaggedRelease' keys off too. This answers the TODO that sat right above the step. The search itself is now exact. It lists every Pull Request *containing* the second parent, which can be more than one - an older release Pull Request built from an ancestor also matches - and the code then took the first entry in an unordered list. The second parent (father) of a merge commit is the head commit of the merged branch, so the candidates are reduced to those whose 'headRefOid' equals it. If a Pull Request was closed and recreated from the same branch, both share a head commit and the most recently merged one is used, with a warning naming both. No match is now an error that lists the candidates instead of falling through to the literal 'null': 'gh pr list --json' returns an empty JSON array, not an empty string, so the previous guard never triggered. Everything stays derived from the commit graph. Deriving the Pull Request from the merge commit's message instead was tried and rejected: that message is free text - editable in the merge dialog, arbitrary for a local merge - so a subject ending in '(#123)' could select an issue number or an unrelated Pull Request and tag a wrong release. The v0.38.0 release merge of pyVHDLModel shows both sides of it: its subject is plain 'v0.38.0' with no number to parse, while its second parent '61bd8eb' equals the 'headRefOid' of Pull Request #133 exactly. CleanupArtifacts.yml -------------------- The second delete step guarded itself with 'steps.compute2', but passed the names computed by 'steps.compute'. So it re-deleted the first set - already deleted by the first step, and masked by 'continue-on-error' - while the artifacts named by 'artifact-json-ids2' survived. Present since the workflow was introduced, but only visible now that 'CompletePipeline' relies on the second set to delete the package artifact on non-release runs. Co-Authored-By: Patrick Lehmann --- .github/workflows/CleanupArtifacts.yml | 2 +- .github/workflows/PrepareJob.yml | 64 +++++++++++++++++++------- doc/JobTemplate/Setup/PrepareJob.rst | 9 ++++ myFramework/Extension/__init__.py | 2 +- myPackage/__init__.py | 2 +- 5 files changed, 59 insertions(+), 20 deletions(-) diff --git a/.github/workflows/CleanupArtifacts.yml b/.github/workflows/CleanupArtifacts.yml index 59f62b82..988a781e 100644 --- a/.github/workflows/CleanupArtifacts.yml +++ b/.github/workflows/CleanupArtifacts.yml @@ -161,7 +161,7 @@ jobs: continue-on-error: true with: name: | - ${{ join(fromJSON(steps.compute.outputs.artifacts).*, ' + ${{ join(fromJSON(steps.compute2.outputs.artifacts).*, ' ') }} - name: 🗑️ Delete other artifacts diff --git a/.github/workflows/PrepareJob.yml b/.github/workflows/PrepareJob.yml index 573d5209..6cd1fd07 100644 --- a/.github/workflows/PrepareJob.yml +++ b/.github/workflows/PrepareJob.yml @@ -397,12 +397,15 @@ jobs: git_submodule_paths=${git_submodule_paths} EOF -# TODO: why not is_release_commit? # TODO: how to support version branches and hotfix releases on version branches? + # This step searches a merged Pull Request whose base is the release branch, so it must only run for merge + # commits that actually landed there. A merge commit on the development branch originates from a Pull Request + # based on the development branch and could never be found. 'is_release_commit' is exactly 'is_merge_commit' + # restricted to the main and version branches, and it's the same flag 'TriggerTaggedRelease' keys off. - name: 🔁 Find merged PullRequest from second parent of current SHA (${{ github.sha }}) id: FindPullRequest - if: steps.Classify.outputs.is_merge_commit == 'true' + if: steps.Classify.outputs.is_release_commit == 'true' run: | set +e @@ -426,27 +429,54 @@ jobs: fi printf "Search Pull Request to '%s' and branch containing SHA %s ... " "${{ inputs.release_branch }}" "${FATHER_SHA}" - PULL_REQUESTS=$(gh pr list --base "${{ inputs.release_branch }}" --search "${FATHER_SHA}" --state "merged" --json "title,number,mergedBy,mergedAt") - if [[ $? -ne 0 || "${PULL_REQUESTS}" == "" ]]; then + PULL_REQUESTS=$(gh pr list --base "${{ inputs.release_branch }}" --search "${FATHER_SHA}" --state "merged" --json "title,number,headRefOid,mergedBy,mergedAt") + if [[ $? -ne 0 ]]; then printf "${ANSI_LIGHT_RED}[FAILED]${ANSI_NOCOLOR}\n" - printf "${ANSI_LIGHT_RED}Couldn't find a merged Pull Request to '%s'. -> %s${ANSI_NOCOLOR}\n" "${{ inputs.release_branch }}" "${PULL_REQUESTS}" - printf "::error title=PullRequest::Couldn't find a merged Pull Request to '%s'. -> %s\n" "${{ inputs.release_branch }}" "${PULL_REQUESTS}" + printf "${ANSI_LIGHT_RED}Couldn't search Pull Requests to '%s'. -> %s${ANSI_NOCOLOR}\n" "${{ inputs.release_branch }}" "${PULL_REQUESTS}" + printf "::error title=PullRequest::Couldn't search Pull Requests to '%s'. -> %s\n" "${{ inputs.release_branch }}" "${PULL_REQUESTS}" exit 1 - else - printf "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}\n" + fi + printf "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}\n" - PR_TITLE="$( printf "%s\n" "${PULL_REQUESTS}" | jq --raw-output ".[0].title")" - PR_NUMBER="$( printf "%s\n" "${PULL_REQUESTS}" | jq --raw-output ".[0].number")" - PR_MERGED_BY="$(printf "%s\n" "${PULL_REQUESTS}" | jq --raw-output ".[0].mergedBy.login")" - PR_MERGED_AT="$(printf "%s\n" "${PULL_REQUESTS}" | jq --raw-output ".[0].mergedAt")" + # The search lists every Pull Request *containing* that commit, so it can report more than one - for example + # an older release Pull Request built from an ancestor. The second parent (father) of a merge commit is the + # head commit of the merged branch, so a Pull Request whose 'headRefOid' equals it is the one that created + # this merge commit. Newest first, in case a Pull Request was closed and redone from the same branch. + MATCHES=$(printf "%s\n" "${PULL_REQUESTS}" | jq --arg sha "${FATHER_SHA}" '[.[] | select(.headRefOid == $sha)] | sort_by(.mergedAt) | reverse') + MATCH_COUNT=$(printf "%s\n" "${MATCHES}" | jq 'length') - printf "${ANSI_LIGHT_BLUE}Found Pull Request:${ANSI_NOCOLOR}\n" - printf " %s\n" "Title: ${PR_TITLE}" - printf " %s\n" "Number: ${PR_NUMBER}" - printf " %s\n" "MergedBy: ${PR_MERGED_BY}" - printf " %s\n" "MergedAt: ${PR_MERGED_AT} ($(date -d"${PR_MERGED_AT}" '+%d.%m.%Y - %H:%M:%S'))" + printf "Select Pull Request with head commit %s ... " "${FATHER_SHA}" + if [[ "${MATCH_COUNT}" -eq 0 ]]; then + printf "${ANSI_LIGHT_RED}[FAILED]${ANSI_NOCOLOR}\n" + printf "${ANSI_LIGHT_RED}No merged Pull Request to '%s' has head commit '%s'.${ANSI_NOCOLOR}\n" "${{ inputs.release_branch }}" "${FATHER_SHA}" + if [[ "$(printf "%s\n" "${PULL_REQUESTS}" | jq 'length')" -eq 0 ]]; then + printf "${ANSI_LIGHT_RED}The search returned no candidates at all.${ANSI_NOCOLOR}\n" + else + printf "${ANSI_LIGHT_RED}Candidates returned by the search: %s${ANSI_NOCOLOR}\n" "$(printf "%s\n" "${PULL_REQUESTS}" | jq --raw-output '[.[] | "#\(.number) (\(.headRefOid[0:7]))"] | join(", ")')" + fi + printf "::error title=PullRequest::No merged Pull Request to '%s' has head commit '%s'.\n" "${{ inputs.release_branch }}" "${FATHER_SHA}" + exit 1 + fi + printf "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}\n" + + if [[ "${MATCH_COUNT}" -gt 1 ]]; then + # Rare, but possible: a Pull Request was closed and recreated from the same branch, so both share a head + # commit. The most recently merged one describes this release. + printf "${ANSI_LIGHT_YELLOW}%s Pull Requests share head commit '%s': %s${ANSI_NOCOLOR}\n" "${MATCH_COUNT}" "${FATHER_SHA}" "$(printf "%s\n" "${MATCHES}" | jq --raw-output '[.[] | "#\(.number)"] | join(", ")')" + printf "${ANSI_LIGHT_YELLOW}Using the most recently merged one.${ANSI_NOCOLOR}\n" fi + PR_TITLE="$( printf "%s\n" "${MATCHES}" | jq --raw-output ".[0].title")" + PR_NUMBER="$( printf "%s\n" "${MATCHES}" | jq --raw-output ".[0].number")" + PR_MERGED_BY="$(printf "%s\n" "${MATCHES}" | jq --raw-output ".[0].mergedBy.login")" + PR_MERGED_AT="$(printf "%s\n" "${MATCHES}" | jq --raw-output ".[0].mergedAt")" + + printf "${ANSI_LIGHT_BLUE}Found Pull Request:${ANSI_NOCOLOR}\n" + printf " %s\n" "Title: ${PR_TITLE}" + printf " %s\n" "Number: ${PR_NUMBER}" + printf " %s\n" "MergedBy: ${PR_MERGED_BY}" + printf " %s\n" "MergedAt: ${PR_MERGED_AT} ($(date -d"${PR_MERGED_AT}" '+%d.%m.%Y - %H:%M:%S'))" + RELEASE_TAG_PATTERN='^${{ inputs.release_tag_pattern }}$' printf "Check Pull Request title against regexp '%s' ... " "${RELEASE_TAG_PATTERN}" if [[ "${PR_TITLE}" =~ $RELEASE_TAG_PATTERN ]]; then diff --git a/doc/JobTemplate/Setup/PrepareJob.rst b/doc/JobTemplate/Setup/PrepareJob.rst index eca51072..2368dc6a 100644 --- a/doc/JobTemplate/Setup/PrepareJob.rst +++ b/doc/JobTemplate/Setup/PrepareJob.rst @@ -48,6 +48,15 @@ The job template generates various output parameters derived from 3. Compute output parameters. 4. Find associated pull-request. + Runs for :ref:`release commits ` only - a merge commit on the + main-branch or a version-branch. A merge commit on the development-branch originates from a pull-request based on + the development-branch, which is not a release. + + Merged pull-requests are searched by the merge commit's second parent, then reduced to those whose ``headRefOid`` + **equals** it, because the second parent of a merge commit is the head commit of the merged branch. The search + alone would also list pull-requests that merely *contain* that commit. If a pull-request was closed and recreated + from the same branch, both share a head commit and the most recently merged one is used. + .. topic:: Job Execution .. image:: ../../_static/pyTooling-Actions-PrepareJob.png diff --git a/myFramework/Extension/__init__.py b/myFramework/Extension/__init__.py index 954bf398..59c81e9d 100644 --- a/myFramework/Extension/__init__.py +++ b/myFramework/Extension/__init__.py @@ -36,7 +36,7 @@ __email__ = "Paebbels@gmail.com" __copyright__ = "2017-2026, Patrick Lehmann" __license__ = "Apache License, Version 2.0" -__version__ = "7.14.0" +__version__ = "7.14.1" __keywords__ = ["GitHub Actions"] __project_url__ = "https://github.com/pyTooling/Actions" __documentation_url__ = "https://pyTooling.github.io/Actions" diff --git a/myPackage/__init__.py b/myPackage/__init__.py index 3fa36ada..23f81c90 100644 --- a/myPackage/__init__.py +++ b/myPackage/__init__.py @@ -36,7 +36,7 @@ __email__ = "Paebbels@gmail.com" __copyright__ = "2017-2026, Patrick Lehmann" __license__ = "Apache License, Version 2.0" -__version__ = "7.14.0" +__version__ = "7.14.1" __keywords__ = ["GitHub Actions"] __project_url__ = "https://github.com/pyTooling/Actions" __documentation_url__ = "https://pyTooling.github.io/Actions"