Skip to content

Fall back to the COMMENT path when GitHub refuses the APPROVE - #17

Merged
william-gr merged 1 commit into
masterfrom
approve-fallback
Sep 13, 2026
Merged

william-gr merged 1 commit into
masterfrom
approve-fallback

Conversation

@william-gr

Copy link
Copy Markdown
Contributor

Summary

On truenas-file-manager#1797 a clean run left an earlier request-changes review standing. The run tried to APPROVE (approve-when-clean defaults on) and GitHub answered:

HTTP 422 "GitHub Actions is not permitted to approve pull requests."

That repo has not enabled Allow GitHub Actions to create and approve pull requests. The refusal was a warning in the log, but the APPROVE path relied on the approval superseding the old request-changes, so with no approval landing nothing cleared it.

Now a refused APPROVE falls back to the "would approve" COMMENT, which quotes the refusal on the PR itself and dismisses the run's own earlier request-changes and approvals, the same as the other COMMENT paths. A successful APPROVE is unchanged.

Verified against a mocked API: refused approve → COMMENT + dismissal, exit 0; successful approve → APPROVE only.

No Jira ticket on hand for the title; add one so PR ticket passes.

An APPROVE clears the identity's earlier REQUEST_CHANGES only by
superseding it, so when GitHub refuses the approval (the Actions
approve setting is off, as on truenas-file-manager#1797) nothing was
posted and the stale block stayed. A refused approval now posts the
"would approve" COMMENT quoting the refusal and dismisses the run's own
earlier state reviews.
@william-gr
william-gr merged commit cd436d0 into master Sep 13, 2026
5 of 7 checks passed
@william-gr
william-gr deleted the approve-fallback branch September 13, 2026 16:08
Comment thread review/submit-verdict.mjs
'',
'Nothing blocks and no human review is needed.',
]);
} catch (error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HIGH — this catch treats every failure of the APPROVE POST as "GitHub refused the approval", but api() also throws when the request never got an answer: AbortSignal.timeout(15_000) fires, or the connection drops, after GitHub has already created the review. In that sequence the APPROVE lands (id A, this login, body carries MARKER), the client throws, the fallback posts the "would approve" COMMENT (id B), and dismissOwnStateReviews(B) then matches id A — state === 'APPROVED', different id, same login, marker present — and dismisses it. The PR ends the run with its approval actively dismissed and a comment quoting The operation was aborted due to timeout as a refusal. Before this change the timed-out approval was left standing.

The narrower non-422 cases are wrong in a smaller way: a 403 or a 5xx produces a PR comment asserting a refusal that did not happen, and pointing at the Actions approve setting, which is not the cause.

approveHint right above already has the right predicate — gating the fallback on it keeps the intended 422 behaviour and sends everything else to the outer catch, which is where "the environment could not post this review" was already handled:

Suggested change
} catch (error) {
} catch (error) {
// Only a refusal falls back. A request that never got an answer (the
// 15s timeout, a dropped connection) may have created the review
// anyway, and commenting then would dismiss the approval that landed.
if (!/HTTP 422/.test(error.message)) throw error;
console.log(`::warning::could not approve: ${escapeData(error.message)}`);

Comment thread README.md
| Only LOW or none, a human must look | COMMENT listing the reasons | passes, no approval |
| Only LOW or none, `approve-when-clean` off | COMMENT "would approve" | passes |
| Only LOW or none, `approve-when-clean` on | APPROVE | passes |
| Only LOW or none, but GitHub refuses the APPROVE | COMMENT quoting the refusal | passes |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LOW — this row makes a third COMMENT outcome, but the paragraph below still counts two: "REQUEST_CHANGES and APPROVE change the PR's state; the two COMMENT outcomes do not" (README.md:307-308). A reader counting rows in the table now finds three.

@github-actions

Copy link
Copy Markdown

Two findings: one HIGH, one LOW. A human should look at this one.

The problem this fixes is real and correctly diagnosed: an APPROVE clears the
identity's earlier REQUEST_CHANGES only by superseding it, so when GitHub
refuses the approval nothing cleared the old block. Routing the refusal to the
existing COMMENT path — which already dismisses the run's own state reviews —
is the right shape, and approveHint being hoisted so both the inner and outer
catch can use it is a clean way to do it.

HIGH — review/submit-verdict.mjs:314: the new catch treats every APPROVE
failure as a refusal, including one where the request got no answer. If the 15s
AbortSignal.timeout fires (or the connection drops) after GitHub created the
review, the APPROVE has landed; the fallback then posts the "would approve"
COMMENT and dismissOwnStateReviews dismisses that just-landed approval, since
it matches on state, login and marker. The run ends having thrown away its own
approval and quoted a timeout as a refusal — where before the change the
approval stood. Non-422 errors that genuinely failed (403, 5xx) are wrong in a
smaller way: the PR gets a comment asserting a refusal that did not happen and
naming the Actions approve setting as the cause. Gating the fallback on the
same /HTTP 422/ that approveHint already tests sends everything else to the
outer catch, which is where "the environment could not post this review" was
handled before. Suggestion inline.

LOW — README.md:307: the new table row is a third COMMENT outcome, but
the paragraph below still says "the two COMMENT outcomes do not".

A human should look: review/submit-verdict.mjs:326 changes what clears a
merge gate. The run's own REQUEST_CHANGES is now dismissed on a path where no
approval landed, so a PR that was blocked by an earlier round is unblocked by a
round that GitHub declined to approve. That is a deliberate call — it is the
point of the change — and it is the kind of call the rubric asks a person to
confirm rather than the reviewer.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Changes requested. 2 finding(s): 1 HIGH, 1 LOW.

Findings at MEDIUM and above are on the lines they are about. Fix them, or say on the PR why one was mis-rated.

william-gr added a commit that referenced this pull request Sep 13, 2026
## Summary

Follow-up to #17, from its own review.

- **HIGH:** the fallback treated every failure of the APPROVE POST as a
refusal. A timeout or dropped connection after GitHub created the review
would then post the COMMENT and dismiss the approval that had landed.
Only an HTTP 422 now falls back; anything else goes to the outer catch,
which warns and leaves the score's exit status, as before.
- **LOW:** README said "the two COMMENT outcomes" after the table gained
a third.

Verified against a mocked API: a timeout on APPROVE warns and posts
nothing; a 422 still falls back to the COMMENT and dismisses the run's
own earlier request-changes.

No Jira ticket on hand for the title; add one so `PR ticket` passes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant