Handle update_pull_request.update_branch workflow-permission failures as non-fatal#32900
Conversation
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
update_pull_request.update_branch workflow-permission failures as non-fatal
There was a problem hiding this comment.
Pull request overview
Treats GitHub's "refusing to allow a GitHub App to create or update workflow ... without workflows permission" rejection from pulls.updateBranch as non-fatal so that the rest of an update_pull_request safe output (title/body/state) can still be applied.
Changes:
- Extends
isNonFatalUpdateBranchErrorto also match workflow-permission rejection messages, gated by a refusal/update-pull-request context to avoid over-matching. - Adds a unit test verifying that a workflows-permission rejection during
updateBranchstill allows the title update to succeed and emits the non-fatal warning.
Show a summary per file
| File | Description |
|---|---|
| actions/setup/js/update_pull_request.cjs | Adds workflow-permission phrase detection to isNonFatalUpdateBranchError. |
| actions/setup/js/update_pull_request.test.cjs | Adds regression test asserting title update proceeds when updateBranch is blocked by workflows permission. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comments suppressed due to low confidence (2)
actions/setup/js/update_pull_request.cjs:48
- The comment block above (lines 38-41) enumerates the benign 422 messages handled by this function but was not updated to mention the new workflow-permission rejection case. Readers will be misled about which conditions are treated as non-fatal. Consider adding a third bullet describing the "refusing to allow a GitHub App to create or update workflow ... without
workflowspermission" case.
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"));
return message.includes("there are no new commits on the base branch") || message.includes("merge conflict between base and head") || hasWorkflowsPermissionError;
actions/setup/js/update_pull_request.cjs:47
- The fallback condition
message.includes("update pull request")is very broad and will match the retry context label passed towithRetry("update pull request #N branch from base"), which gets appended to many error messages by the retry helper. Combined with the workflows-permission phrase regex this is probably acceptable, but it means almost any 422 containing "without workflows permission" anywhere will be swallowed for this code path, not just the workflow-mutation refusal. Consider tightening this to a more specific marker (e.g. requiringhasWorkflowMutationRefusal) so unrelated workflow-permission errors aren't silently downgraded to warnings.
const hasWorkflowsPermissionError = hasWorkflowsPermissionPhrase && (hasWorkflowMutationRefusal || message.includes("update pull request"));
- Files reviewed: 2/2 changed files
- Comments generated: 1
| // 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")); | ||
| return message.includes("there are no new commits on the base branch") || message.includes("merge conflict between base and head") || hasWorkflowsPermissionError; |
|
@copilot review all comments
|
There was a problem hiding this comment.
Skills-Based Review 🧠
Applied /diagnose and /tdd — this is a targeted bug fix with a regression test, exactly where these skills add value.
Key Themes
- Minor implementation nit (
/diagnose): The regexiflag is redundant on an already-lowercased string (line 43). Harmless but worth cleaning up. - Partial test coverage (
/tdd): The regression test covers thehasWorkflowMutationRefusalpath throughisNonFatalUpdateBranchError. The second matching branch (message.includes("update pull request")) and the guard's negative case (phrase without context = still fatal) are not tested.
Positive Highlights
- ✅ The conjunction guard (
hasWorkflowsPermissionPhrase && (...)) is well-reasoned and directly addresses the over-catching concern called out in the description. - ✅ A regression test was added in the same PR — good
/diagnosediscipline. - ✅ Clear, descriptive variable names make the matching logic easy to audit.
Verdict
Fix is sound. The suggestions are non-blocking — the main failure mode is covered. The secondary branch test and negative guard test would meaningfully harden the scoped-matching promise made in the PR description.
🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · ● 5.5M
| // 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"); | ||
| const hasWorkflowsPermissionPhrase = /without\s+`?workflows`?\s+permission/i.test(message); |
There was a problem hiding this comment.
[/diagnose] The i flag on this regex is redundant — message was already lowercased on the line above (getErrorMessage(error).toLowerCase()), so the flag has no effect. It is harmless but misleading; remove it for clarity:
const hasWorkflowsPermissionPhrase = /without\s+`?workflows`?\s+permission/.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")); |
There was a problem hiding this comment.
[/tdd] The message.includes("update pull request") branch has no dedicated test. The regression test added in this PR exercises hasWorkflowMutationRefusal (the "refusing to allow a GitHub App..." path), but not the fallback path where only "update pull request" appears in the message. Consider adding a second test case — e.g. an error like "without workflows permission for update pull request" — to ensure both branches are covered and the conjunction logic is verified independently.
| }); | ||
| expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("branch from base (non-fatal)")); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
[/tdd] The new test verifies the passing case but there's no negative/guard test confirming that a workflows-permission phrase alone (without the mutation-refusal or "update pull request" context) is still treated as fatal. This matters because the PR description explicitly calls out "scoped matching to avoid over-catching". A test like:
it("should propagate error when workflows permission phrase appears without update-branch context", async () => {
mockGithub.rest.pulls.updateBranch.mockRejectedValueOnce(
new Error("missing `workflows` permission")
);
const handler = await updatePRModule.main({ update_branch: true });
const result = await handler({ pull_request_number: 100, title: "PR" });
expect(result.success).toBe(false);
});would lock down the guard condition and prevent accidental over-broadening in future edits.
🧪 Test Quality Sentinel ReportTest Quality Score: 80/100✅ Excellent
Test Classification Details
Flagged Tests — Requires ReviewNo tests require mandatory review. One minor observation is noted below. i️ Test inflation (informational, not blocking)The test file added 20 lines while the production file added 6 lines (ratio ≈ 3.3:1, threshold 2:1). However, the 6 production lines introduce a non-trivial regex-based error detection path that justifies thorough behavioral verification. The test covers the full observable contract: Language SupportTests analyzed:
Verdict
📖 Understanding Test ClassificationsDesign Tests (High Value) verify what the system does:
Implementation Tests (Low Value) verify how the system does it:
Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.
|
|
@copilot review all comments |
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Addressed in 2b8eb85.
|
Safe-output processing was failing when
update_pull_requestattemptedupdate_branchand GitHub rejected workflow-file updates due to missingworkflowspermission. This caused the safe-outputs job to fail instead of continuing with remaining actionable updates.Failure classification adjustment
update_pull_requestbranch-update error handling to classify workflow-permission rejections as non-fatal for theupdate_branchstep.Scoped matching to avoid over-catching
Regression coverage
update_branchdoes not fail the overallupdate_pull_requestoperation when other updates are present.