diff --git a/.github/workflows/create-hotfix-pr.yml b/.github/workflows/create-hotfix-pr.yml index 06d261ffb..2db8c1e7f 100644 --- a/.github/workflows/create-hotfix-pr.yml +++ b/.github/workflows/create-hotfix-pr.yml @@ -138,7 +138,18 @@ jobs: # Attempt cherry-pick if ! git cherry-pick ${{ steps.get_commit.outputs.sha }} --empty=keep; then - echo "Cherry-pick encountered conflicts for $base_branch, attempting to resolve..." + echo "Cherry-pick failed for $base_branch, checking for merge conflicts..." + CONFLICT_FILES=$(git diff --name-only --diff-filter=U) + + if [[ -z "$CONFLICT_FILES" ]]; then + echo "::error::Cherry-pick failed for $base_branch without merge conflicts." + git status --short + exit 1 + fi + + echo "Cherry-pick encountered conflicts for $base_branch:" + echo "$CONFLICT_FILES" + had_conflicts="true" git add -A # Check if there are actual changes staged after conflict resolution attempt if git diff --cached --quiet; then @@ -147,15 +158,7 @@ jobs: continue # Skip PR creation if no changes after resolution fi git cherry-pick --continue - - if [ $? -eq 0 ]; then - echo "Successfully resolved conflicts for $base_branch" - had_conflicts="true" - else - echo "Failed to resolve conflicts for $base_branch" - git cherry-pick --abort - continue - fi + echo "Continuing with conflict hotfix PR for $base_branch" else echo "Cherry-pick successful for $base_branch" fi @@ -250,14 +253,19 @@ jobs: if git push origin "$new_branch"; then echo "Successfully pushed branch $new_branch" - PR_BODY="Hotfix of PR #${PR_NUMBER} (${PR_URL}) to the \`${base_branch}\` branch. - Hey @${PR_AUTHOR}, please review this hotfix PR created from your original PR." + PR_BODY=$(printf '%s\n' \ + "Hotfix of PR #${PR_NUMBER} (${PR_URL}) to the \`${base_branch}\` branch." \ + "Hey @${PR_AUTHOR}, please review this hotfix PR created from your original PR.") # Add conflict warning if needed if [[ "$had_conflicts" == "true" ]]; then - PR_BODY="${PR_BODY} - - ### ⚠️ **Note:** This PR had conflicts with the base branch and was resolved automatically. Please review the changes carefully." + PR_BODY=$(printf '%s\n\n%s\n\n%s\n%s\n\n%s\n```\n%s\n```' \ + "$PR_BODY" \ + "### Manual conflict resolution required" \ + "This hotfix PR contains cherry-pick conflicts against \`${base_branch}\`." \ + "Resolve the conflict markers in this branch and push a follow-up commit before merging." \ + "**Conflicted files:**" \ + "$CONFLICT_FILES") fi # Create PR using gh cli diff --git a/.github/workflows/hotfix-tracking-guard.yml b/.github/workflows/hotfix-tracking-guard.yml index 5fbb8508c..a5c388727 100644 --- a/.github/workflows/hotfix-tracking-guard.yml +++ b/.github/workflows/hotfix-tracking-guard.yml @@ -19,16 +19,15 @@ jobs: permissions: pull-requests: read steps: - - name: Fail on auto-hotfix placeholder files + - name: Skip non-auto-hotfix PRs + if: ${{ !contains(github.event.pull_request.labels.*.name, 'auto-hotfix') }} + run: echo "PR does not have auto-hotfix label. Passing." + + - name: Fail on placeholder files and unresolved conflict markers + if: contains(github.event.pull_request.labels.*.name, 'auto-hotfix') uses: actions/github-script@v7 with: script: | - const labels = (context.payload.pull_request.labels || []).map((label) => label.name); - if (!labels.includes("auto-hotfix")) { - core.info("PR does not have auto-hotfix label. Passing."); - return; - } - const files = await github.paginate(github.rest.pulls.listFiles, { owner: context.repo.owner, repo: context.repo.repo, @@ -36,16 +35,49 @@ jobs: per_page: 100, }); - const placeholderFiles = files - .map((file) => file.filename) + const fileNames = files.map((file) => file.filename); + const placeholderFiles = fileNames .filter((filename) => filename.startsWith(".github/hotfix-manual/")); - if (placeholderFiles.length === 0) { - core.info("auto-hotfix PR has no hotfix tracking placeholder files. Passing."); + if (placeholderFiles.length > 0) { + core.error(`Found hotfix tracking placeholder file(s): ${placeholderFiles.join(", ")}`); + core.setFailed( + "Manual hotfix tracking PR detected (.github/hotfix-manual/*). Close this PR after manually cherry-picking the changes. Do NOT merge." + ); + return; + } + + const conflictStartPattern = /^<{7,}(?:$| )/; + const conflictMiddlePattern = /^={7,}$/; + const conflictEndPattern = /^>{7,}(?:$| )/; + + const filesWithoutPatch = fileNames.filter((filename, index) => typeof files[index].patch !== "string"); + if (filesWithoutPatch.length > 0) { + core.warning(`Could not inspect diff patch for: ${filesWithoutPatch.join(", ")}`); + } + + const conflictedFiles = files + .filter((file) => typeof file.patch === "string") + .filter((file) => { + const addedLines = file.patch + .split("\n") + .filter((line) => line.startsWith("+") && !line.startsWith("+++")) + .map((line) => line.slice(1)); + + return ( + addedLines.some((line) => conflictStartPattern.test(line)) && + addedLines.some((line) => conflictMiddlePattern.test(line)) && + addedLines.some((line) => conflictEndPattern.test(line)) + ); + }) + .map((file) => file.filename); + + if (conflictedFiles.length === 0) { + core.info("No unresolved conflict markers found in added lines for auto-hotfix PR files."); return; } - core.error(`Found hotfix tracking placeholder file(s): ${placeholderFiles.join(", ")}`); + core.error(`Found unresolved conflict markers in: ${conflictedFiles.join(", ")}`); core.setFailed( - "Manual hotfix tracking PR detected (.github/hotfix-manual/*). Close this PR after manually cherry-picking the changes. Do NOT merge." + "Auto-hotfix PR still contains unresolved conflict markers in added lines. Resolve them in this branch and push a follow-up commit before merging." );