diff --git a/actions/setup/js/update_pull_request.cjs b/actions/setup/js/update_pull_request.cjs index adac21ada2c..4944e62f34a 100644 --- a/actions/setup/js/update_pull_request.cjs +++ b/actions/setup/js/update_pull_request.cjs @@ -31,16 +31,27 @@ function isNonFatalUpdateBranchError(error) { status = candidateStatus; } } - if (status !== undefined && status !== 422) { - return false; + const message = getErrorMessage(error).toLowerCase(); + const hasWorkflowsPermissionPhrase = /without\s+`?workflows`?\s+permission/i.test(message); + const hasWorkflowMutationRefusal = message.includes("refusing to allow a github app to create or update workflow"); + // Require both permission wording and update-branch context to avoid treating unrelated + // "workflows permission" errors as non-fatal for pull request branch updates. + const hasWorkflowsPermissionError = hasWorkflowsPermissionPhrase && (hasWorkflowMutationRefusal || message.includes("update pull request")); + + if (status !== undefined) { + if (status === 403 && hasWorkflowsPermissionError) { + return true; + } + if (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"); + return message.includes("there are no new commits on the base branch") || message.includes("merge conflict between base and head") || hasWorkflowsPermissionError; } /** diff --git a/actions/setup/js/update_pull_request.test.cjs b/actions/setup/js/update_pull_request.test.cjs index b33d7361847..618fd88c2c8 100644 --- a/actions/setup/js/update_pull_request.test.cjs +++ b/actions/setup/js/update_pull_request.test.cjs @@ -879,4 +879,26 @@ describe("update_pull_request.cjs - update_branch behavior", () => { }); expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("branch from base (non-fatal)")); }); + + it("should continue title/body updates when updateBranch gets workflows-permission 403", async () => { + const permissionError = new Error("refusing to allow a GitHub App to create or update workflow `.github/workflows/test.lock.yml` without `workflows` permission"); + permissionError.status = 403; + mockGithub.rest.pulls.updateBranch.mockRejectedValueOnce(permissionError); + + 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)")); + }); });