diff --git a/.github/scripts/cherry-pick-to-release.sh b/.github/scripts/cherry-pick-to-release.sh index a8612e7453..69a655d110 100755 --- a/.github/scripts/cherry-pick-to-release.sh +++ b/.github/scripts/cherry-pick-to-release.sh @@ -193,10 +193,29 @@ else lookup_milestone "${VERSION}" + # Build the PR body using printf to avoid quoting pitfalls with embedded + # newlines (mixed $'...' and '...' quoting can leave literal \n in output). + CONFLICT_BODY="$(printf '%s' \ + "Cherry-pick of #${PR_NUMBER} (${MERGE_COMMIT_SHA}) into " \ + "\`${TARGET_BRANCH}\` **failed due to merge conflicts**." \ + "${MILESTONE_NOTE}" \ + $'\n\nPlease resolve manually:\n```bash\n' \ + "git fetch origin" \ + $'\n' \ + "git checkout ${CHERRY_PICK_BRANCH}" \ + $'\n' \ + "${CHERRY_PICK_CMD}" \ + $'\n' \ + "# resolve conflicts" \ + $'\n' \ + "git push origin ${CHERRY_PICK_BRANCH} --force" \ + $'\n```')" + gh pr create \ + --draft \ --base "${TARGET_BRANCH}" \ --head "${CHERRY_PICK_BRANCH}" \ --title "[${VERSION} Cherry-pick - CONFLICTS] ${PR_TITLE}" \ ${MILESTONE_ARG} \ - --body $'Cherry-pick of #'"${PR_NUMBER}"' ('"${MERGE_COMMIT_SHA}"') into `'"${TARGET_BRANCH}"'` **failed due to merge conflicts**.'"${MILESTONE_NOTE}"$'\n\nPlease resolve manually:\n```bash\ngit fetch origin\ngit checkout '"${CHERRY_PICK_BRANCH}"'\n'"${CHERRY_PICK_CMD}"$'\n# resolve conflicts\ngit push origin '"${CHERRY_PICK_BRANCH}"' --force\n```' + --body "${CONFLICT_BODY}" fi diff --git a/.github/scripts/tests/cherry-pick-to-release.bats b/.github/scripts/tests/cherry-pick-to-release.bats index 68ef3d7f8c..3484b48ea9 100644 --- a/.github/scripts/tests/cherry-pick-to-release.bats +++ b/.github/scripts/tests/cherry-pick-to-release.bats @@ -296,3 +296,52 @@ STUB # Should have created an empty commit. grep -q "GIT: commit --allow-empty" "${STUB_DIR}/git.log" } + +@test "conflict PR body contains real newlines, not literal backslash-n" { + write_git_mock ' + if [[ "$1" == "fetch" ]]; then exit 0; fi + if [[ "$1" == "cherry" ]]; then echo "+ abc123"; exit 0; fi + if [[ "$1" == "checkout" ]]; then exit 0; fi + if [[ "$1" == "rev-list" ]]; then echo "abc123def456 parent1"; exit 0; fi + if [[ "$1" == "cherry-pick" ]]; then + if [[ "$2" == "--abort" ]]; then exit 0; fi + exit 1 + fi + if [[ "$1" == "commit" ]]; then exit 0; fi + if [[ "$1" == "push" ]]; then exit 0; fi + exit 0 + ' + # Capture the full --body argument to a file for inspection. + write_gh_mock ' + if [[ "$1" == "api" ]]; then echo "7.0.1"; exit 0; fi + if [[ "$1" == "pr" && "$2" == "create" ]]; then + while [[ $# -gt 0 ]]; do + if [[ "$1" == "--body" ]]; then + printf "%s" "$2" > "'"${STUB_DIR}"'/pr-body.txt" + break + fi + shift + done + exit 0 + fi + exit 0 + ' + + run bash "${SCRIPT}" + [ "$status" -eq 0 ] + + # The body file must exist (gh pr create was called with --body). + [ -f "${STUB_DIR}/pr-body.txt" ] + + local body + body="$(cat "${STUB_DIR}/pr-body.txt")" + + # Must NOT contain literal two-character sequence '\n'. + [[ "$body" != *'\\n'* ]] + # Each command in the code block must be on its own line. + [[ "$body" == *$'\ngit fetch origin\n'* ]] + [[ "$body" == *$'\ngit checkout dev/automation/pr-42-to-7.0.1\n'* ]] + [[ "$body" == *$'\ngit cherry-pick abc123def456\n'* ]] + [[ "$body" == *$'\n# resolve conflicts\n'* ]] + [[ "$body" == *$'\ngit push origin dev/automation/pr-42-to-7.0.1 --force\n'* ]] +}