diff --git a/actions/setup/js/update_pull_request.cjs b/actions/setup/js/update_pull_request.cjs index 8d22e06f8ff..59e72cf3e85 100644 --- a/actions/setup/js/update_pull_request.cjs +++ b/actions/setup/js/update_pull_request.cjs @@ -18,6 +18,30 @@ const { generateHistoryUrl } = require("./generate_history_link.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { withRetry, isTransientError } = require("./error_recovery.cjs"); +/** + * @param {unknown} error + * @returns {boolean} + */ +function isNonFatalUpdateBranchError(error) { + /** @type {number | undefined} */ + let status; + if (typeof error === "object" && error !== null && "status" in error) { + status = /** @type {{status?: number}} */ (error).status; + } + if (status !== undefined && status !== 422) { + return false; + } + + // GitHub update-branch API can return these 422 messages for benign conditions: + // - already up to date ("There are no new commits on the base branch") + // - cannot auto-update due to conflict ("merge conflict between base and head") + // These should not fail safe output processing. + const message = getErrorMessage(error).toLowerCase(); + return ( + message.includes("there are no new commits on the base branch") || message.includes("merge conflict between base and head") + ); +} + /** * Execute the pull request update API call * @param {any} github - GitHub API client @@ -55,8 +79,13 @@ async function executePRUpdate(github, context, prNumber, updateData) { `update pull request #${prNumber} branch from base` ); } catch (error) { - core.warning(`Failed to update pull request #${prNumber} branch from base: ${getErrorMessage(error)}`); - throw error; + const errorMessage = getErrorMessage(error); + if (isNonFatalUpdateBranchError(error)) { + core.warning(`Failed to update pull request #${prNumber} branch from base (non-fatal): ${errorMessage}`); + } else { + core.warning(`Failed to update pull request #${prNumber} branch from base: ${errorMessage}`); + throw error; + } } } diff --git a/actions/setup/js/update_pull_request.test.cjs b/actions/setup/js/update_pull_request.test.cjs index e4a34d88f67..b33d7361847 100644 --- a/actions/setup/js/update_pull_request.test.cjs +++ b/actions/setup/js/update_pull_request.test.cjs @@ -847,4 +847,36 @@ describe("update_pull_request.cjs - update_branch behavior", () => { expect(mockGithub.rest.pulls.updateBranch).toHaveBeenCalledTimes(1); expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Failed to update pull request #100 branch from base")); }); + + it("should treat no-new-commits updateBranch response as a non-fatal no-op", async () => { + mockGithub.rest.pulls.updateBranch.mockRejectedValueOnce(new Error("There are no new commits on the base branch.")); + + const handler = await updatePRModule.main({ update_branch: true }); + const result = await handler({ pull_request_number: 100 }); + + expect(result.success).toBe(true); + expect(mockGithub.rest.pulls.updateBranch).toHaveBeenCalledTimes(1); + expect(mockGithub.rest.pulls.update).not.toHaveBeenCalled(); + expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("branch from base (non-fatal)")); + }); + + it("should continue title/body updates when updateBranch reports merge conflict", async () => { + mockGithub.rest.pulls.updateBranch.mockRejectedValueOnce(new Error("merge conflict between base and head")); + + const handler = await updatePRModule.main({ update_branch: true }); + const result = await handler({ + pull_request_number: 100, + title: "Updated PR", + }); + + expect(result.success).toBe(true); + expect(mockGithub.rest.pulls.updateBranch).toHaveBeenCalledTimes(1); + expect(mockGithub.rest.pulls.update).toHaveBeenCalledWith({ + owner: "testowner", + repo: "testrepo", + pull_number: 100, + title: "Updated PR", + }); + expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("branch from base (non-fatal)")); + }); }); diff --git a/docs/src/content/docs/reference/safe-outputs-pull-requests.md b/docs/src/content/docs/reference/safe-outputs-pull-requests.md index 4802975a1b6..f07fcaee013 100644 --- a/docs/src/content/docs/reference/safe-outputs-pull-requests.md +++ b/docs/src/content/docs/reference/safe-outputs-pull-requests.md @@ -137,6 +137,8 @@ safe-outputs: When `update-branch: true` is set, the handler calls the GitHub REST `pulls.updateBranch` API to merge the latest base branch changes into the PR branch before applying title or body updates. This requires `contents: write` permission; without it only `contents: read` is needed. The field can also be used alone (with `title: false` and `body: false`) to update the branch without changing the PR description. +If GitHub reports `There are no new commits on the base branch.` or `merge conflict between base and head`, the branch update is treated as best-effort: the workflow logs a warning and continues processing the safe output. + When using `target: "*"`, the agent must provide `pull_request_number` in the output to identify which pull request to update. **Operation Types**: Same as `update-issue` (`append`, `prepend`, `replace`). Title updates always replace the existing title. Disable fields by setting to `false`.