Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .github/workflows/pr-sous-chef.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/pr-sous-chef.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ safe-outputs:
add-comment:
max: 4
target: "*"
github-token: ${{ secrets.AWI_MAINTENANCE_TOKEN }}
github-token: ${{ secrets.AWI_MAINTENANCE_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
resolve-pull-request-review-thread:
max: 40
dismiss-pull-request-review:
Expand Down
11 changes: 9 additions & 2 deletions actions/setup/js/update_pull_request.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,12 @@ function isNonFatalUpdateBranchError(error) {
// 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"));
// GitHub update-branch API also returns 403 with this message when a PR contains workflow
// file changes and the check times out, rather than the usual "refusing to allow" phrase.
const hasWorkflowsScopeRequired = message.includes("`workflows` scope may be required") || message.includes("unable to determine if workflow can be created or updated");

if (status !== undefined) {
if (status === 403 && hasWorkflowsPermissionError) {
if (status === 403 && (hasWorkflowsPermissionError || hasWorkflowsScopeRequired)) {
return true;
}
if (status !== 422) {
Expand All @@ -52,7 +55,11 @@ function isNonFatalUpdateBranchError(error) {
// - 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.
return message.includes("there are no new commits on the base branch") || message.includes("merge conflict between base and head") || hasWorkflowsPermissionError;
// hasWorkflowsPermissionError / hasWorkflowsScopeRequired are only checked here for errors
// with no numeric status (status === undefined). The explicit 403 case is already handled
// by the if-block above, and other numeric statuses (e.g. 422 with these phrases) should
// not be silently swallowed.
return message.includes("there are no new commits on the base branch") || message.includes("merge conflict between base and head") || ((hasWorkflowsPermissionError || hasWorkflowsScopeRequired) && status === undefined);
}

/**
Expand Down
52 changes: 52 additions & 0 deletions actions/setup/js/update_pull_request.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -941,4 +941,56 @@ 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-scope-required 403 (scope phrase variant)", async () => {
// Message matches only the "`workflows` scope may be required" branch of hasWorkflowsScopeRequired.
const scopeError = new Error("Validation failed; `workflows` scope may be required due to timeout in check.");
scopeError.status = 403;
// The message contains "timeout" which makes isTransientError return true, so withRetry
// retries once (maxRetries: 1, see executePRUpdate). Both attempts must fail to reach the
// non-fatal catch path. Update this assertion if maxRetries changes.
mockGithub.rest.pulls.updateBranch.mockRejectedValue(scopeError);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The test comment explains why mockRejectedValue (persistent) is used instead of mockRejectedValueOnce, but this implicit coupling to retry count is fragile — if maxRetries changes, the assertion toHaveBeenCalledTimes(2) will silently pass or fail for the wrong reason.

💡 Suggestion

Consider making the retry count explicit in the test by checking the warning call count or asserting that the final result still succeeds regardless of retry count. At minimum, add a comment linking to where maxRetries is defined so a future change there prompts updating this test:

// updateBranch is retried once (maxRetries: 1, see executePRUpdate in update_pull_request.cjs)
// Update this assertion if maxRetries changes.
expect(mockGithub.rest.pulls.updateBranch).toHaveBeenCalledTimes(2);

@copilot please address this.


const handler = await updatePRModule.main({ update_branch: true });
const result = await handler({
pull_request_number: 100,
title: "Updated PR",
});

expect(result.success).toBe(true);
// Called twice: initial attempt + 1 retry (maxRetries: 1 in executePRUpdate)
expect(mockGithub.rest.pulls.updateBranch).toHaveBeenCalledTimes(2);
expect(mockGithub.rest.pulls.update).toHaveBeenCalledWith({
owner: "testowner",
repo: "testrepo",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The test covers only the first message variant (workflows scope may be required). A second variant (unable to determine if workflow can be created or updated) in hasWorkflowsScopeRequired is untested — a typo or drift in that string would go undetected.

💡 Suggested additional test

Add a parallel test case for the second phrase:

it('should treat unable-to-determine 403 as non-fatal', async () => {
  const err = new Error('Unable to determine if workflow can be created or updated; contact support');
  err.status = 403;
  mockGithub.rest.pulls.updateBranch.mockRejectedValue(err);
  const handler = await updatePRModule.main({ update_branch: true });
  const result = await handler({ pull_request_number: 100, title: 'PR' });
  expect(result.success).toBe(true);
  expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining('branch from base (non-fatal)'));
});

@copilot please address this.

pull_number: 100,
title: "Updated PR",
});
expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("branch from base (non-fatal)"));
});

it("should continue title/body updates when updateBranch gets unable-to-determine-workflow 403 (unable-to-determine variant)", async () => {
// Message matches only the "unable to determine if workflow can be created or updated" branch
// of hasWorkflowsScopeRequired, independently of the scope-phrase variant above.
const unableToDetermineError = new Error("Unable to determine if workflow can be created or updated; contact support.");
unableToDetermineError.status = 403;
// No "timeout" in the message, so isTransientError returns false — no retry, called once.
mockGithub.rest.pulls.updateBranch.mockRejectedValueOnce(unableToDetermineError);

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)"));
});
});
Loading