diff --git a/actions/setup/js/push_to_pull_request_branch.cjs b/actions/setup/js/push_to_pull_request_branch.cjs index 9cd1003967a..d41c2b4df61 100644 --- a/actions/setup/js/push_to_pull_request_branch.cjs +++ b/actions/setup/js/push_to_pull_request_branch.cjs @@ -328,6 +328,7 @@ async function main(config = {}) { // to branches with exactly one new commit (security: prevents use of CI trigger // token on multi-commit branches where workflow files may have been modified). let newCommitCount = 0; + let remoteHeadBeforePatch = ""; if (hasChanges) { core.info("Applying patch..."); try { @@ -356,7 +357,6 @@ async function main(config = {}) { // Apply patch // Capture HEAD before applying patch to compute new-commit count later - let remoteHeadBeforePatch = ""; try { const { stdout } = await exec.getExecOutput("git", ["rev-parse", "HEAD"]); remoteHeadBeforePatch = stdout.trim(); @@ -368,22 +368,6 @@ async function main(config = {}) { // This allows git to resolve create-vs-modify mismatches when a file exists in target but not source await exec.exec(`git am --3way ${patchFilePath}`); core.info("Patch applied successfully"); - - // Push the applied commits to the branch - await exec.exec(`git push origin ${branchName}`); - core.info(`Changes committed and pushed to branch: ${branchName}`); - - // Count new commits pushed for the CI trigger decision - if (remoteHeadBeforePatch) { - try { - const { stdout: countStr } = await exec.getExecOutput("git", ["rev-list", "--count", `${remoteHeadBeforePatch}..HEAD`]); - newCommitCount = parseInt(countStr.trim(), 10); - core.info(`${newCommitCount} new commit(s) pushed to branch`); - } catch { - // Non-fatal - newCommitCount stays 0, extra empty commit will be skipped - core.info("Could not count new commits - extra empty commit will be skipped"); - } - } } catch (error) { core.error(`Failed to apply patch: ${getErrorMessage(error)}`); @@ -416,6 +400,33 @@ async function main(config = {}) { return { success: false, error: "Failed to apply patch" }; } + + // Push the applied commits to the branch (outside patch try/catch so push failures are not misattributed) + try { + await exec.exec(`git push origin ${branchName}`); + core.info(`Changes committed and pushed to branch: ${branchName}`); + } catch (pushError) { + const pushErrorMessage = getErrorMessage(pushError); + core.error(`Failed to push changes: ${pushErrorMessage}`); + const nonFastForwardPatterns = ["non-fast-forward", "rejected", "fetch first", "Updates were rejected"]; + const isNonFastForward = nonFastForwardPatterns.some(pattern => pushErrorMessage.includes(pattern)); + const userMessage = isNonFastForward + ? "Failed to push changes: remote PR branch changed while the workflow was running (non-fast-forward). Re-run the workflow on the latest PR branch state." + : `Failed to push changes: ${pushErrorMessage}`; + return { success: false, error_type: "push_failed", error: userMessage }; + } + + // Count new commits pushed for the CI trigger decision + if (remoteHeadBeforePatch) { + try { + const { stdout: countStr } = await exec.getExecOutput("git", ["rev-list", "--count", `${remoteHeadBeforePatch}..HEAD`]); + newCommitCount = parseInt(countStr.trim(), 10); + core.info(`${newCommitCount} new commit(s) pushed to branch`); + } catch { + // Non-fatal - newCommitCount stays 0, extra empty commit will be skipped + core.info("Could not count new commits - extra empty commit will be skipped"); + } + } } else { core.info("Skipping patch application (empty patch)");