fix: report a workflow action that the GitHub API refused as failed [patch] - #288
Conversation
…patch] MakeGitHubRequestAsync absorbs the failures this provider knows how to handle -- AuthorizationException, a 403 for rate limiting or for authorization, a 429, and a connection error. It sets the status and logs, but does not rethrow, which is right for a polling update that should not tear down over a rate limit. RerunWorkflowAsync, CancelWorkflowAsync and TriggerWorkflowAsync read reaching the next line as success and returned true unconditionally. Their own catch blocks never fired, because the exceptions had already been swallowed one level down. So cancelling a workflow while rate limited, or just after the token was revoked, returned true. ExecuteGitHubApiAction took that as success and force-refreshed the build, telling the user the cancel had worked while nothing had happened server-side. Have MakeGitHubRequestAsync answer whether the request completed, and have the three actions return that answer. An unhandled ApiException status still propagates, so a caller that genuinely cannot continue is not quietly handed a false instead. The other five callers ignore the result, as they did before: they are polling updates that already surface a failure through the provider status. Fixes #286 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EdV5iCFkUxFqkLZQAGLAVT
The guard and the two catch blocks were copied between RerunWorkflowAsync, CancelWorkflowAsync and TriggerWorkflowAsync, and all three made the same mistake with the result. RunWorkflowActionAsync is that shape once, returning what the request returned. Taking the API call as a delegate is what makes the path testable at all: the four new tests drive a refused request, a successful one, a NotFound and the no-credentials guard without reaching the network. The owner carries its own token, which gets past the credential guard without the provider needing one. Coverage on new code goes from 57% to 67%. The three remaining uncovered lines are each method's single line of Octokit call construction, which cannot run without either a live request or an IConnection test double; see the note on the pull request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EdV5iCFkUxFqkLZQAGLAVT
|
SonarCloud Code Analysis is failing this PR on What I did. I folded the three workflow actions into one That takes new-code coverage from 57% to about 67%. Still under the gate. What is left, and why I stopped. The three remaining uncovered lines are one per method, and each is nothing but the Octokit call construction: internal async Task<bool> RerunWorkflowAsync(Run run) => await RunWorkflowActionAsync(run.Owner, $"…/rerun/{run.Id}", async () => await GitHubRuns.Rerun(…));
Option 2 is real work: a mutable test-only seam in production code, plus a fake implementing the whole of I did not re-run the check: the gate is deterministic, so a re-run returns the same number. Suggested resolutions, in the order I would pick them:
Happy to do 3 as a separate PR if you want it — it would pay off across the provider, not just these three lines. Generated by Claude Code |
|


Fixes #286
What was wrong
MakeGitHubRequestAsyncabsorbs the failures this provider knows how to handle —AuthorizationException, a 403 for rate limiting, a 403 for authorization, a 429, and a connection error. It sets the status and logs, but does not rethrow. That is right for a polling update, which should not tear down over a rate limit.RerunWorkflowAsync,CancelWorkflowAsyncandTriggerWorkflowAsynceach read reaching the next line as success:Their own
catch (NotFoundException)/catch (ApiException)blocks never fired, because those exceptions had already been swallowed one level down.So clicking "Cancel Running Workflow" while rate limited, or just after the token was revoked, returned
true.ExecuteGitHubApiActiontook that as success and force-refreshed the build — the UI told the user the cancel had worked while nothing had happened server-side.The fix
MakeGitHubRequestAsyncnow answers whether the request completed, and the three actions return that answer.An unhandled
ApiExceptionstatus still propagates exactly as before, so a caller that genuinely cannot continue is not quietly handed afalseinstead —AnUnhandledStatusStillPropagatespins that.The other five callers ignore the result, as they did before. They are polling updates (
DiscoverOwnersAsync,UpdateRepositoriesAsyncand siblings) that already surface a failure through the provider status, and widening them is not what this issue describes.The second commit: one workflow action, not three
The guard and both catch blocks were copied between all three methods, and all three made the same mistake with the result.
RunWorkflowActionAsyncis that shape once, returning what the request returned.It takes the API call as a delegate, which is what makes the path testable at all — four further tests drive a refused request, a successful one, a
NotFound, and the no-credentials guard, none of them reaching the network. The owner carries its own token, which gets past the credential guard without the provider needing one. That path had no coverage before.Tests
BuildMonitor.Test/GitHubRequestOutcomeTests.cs.The request body is a
Func<Task>the caller supplies, so each failure is injected directly and the real method is driven — no rule had to be lifted out of it, and the assertions cover both halves of each case: the returned answer and the provider status the user actually sees (RateLimited,AuthFailed,Error).Octokit's own
Responseis internal, so the tests use a smallIResponsefake carrying the status code and headers, which is allIsRateLimitResponseandParseRateLimitResetTimeread.Verified by substituting the original behaviour back in (every swallowed failure reporting success, as
await …; return true;did):ARateLimited403ReportsFailure,APlain403ReportsFailure,A429ReportsFailure,AnAuthorizationExceptionReportsFailureandAConnectionErrorReportsFailureall fail against it and pass against the fix.Full suite on this branch: 43 total, 43 passed, 0 failed.
dotnet build --configuration Releaseis clean. Tests pass on Windows, macOS and Ubuntu.Known: the coverage gate is red, and needs a decision
SonarCloud fails this PR on
Coverage on New Codeat 66.7%, against a required 80%. The refactor above took it from 57.1% to 66.7%, and 66.7% is the ceiling without changes I do not think belong in this PR.The three remaining uncovered lines are one per method, each nothing but the Octokit call construction. They cannot be kept out of the diff, because they are exactly the lines that must change to return the result rather than
true. Covering them needs either a live request to api.github.com from a unit test, or an injectable client factory plus a fakeIConnection.I did not build that double: it is well outside what #286 describes, and it cuts against the approach CLAUDE.md sets out for this repository —
ColumnStrideTestsdeliberately pulls the decidable part out into a plain method rather than faking the untestable layer underneath, andRunWorkflowActionAsyncis that same move.See the full note for the three ways out: accept on the new tests' merits, exclude the provider-to-Octokit glue from coverage, or land the injectable client factory as its own change.
🤖 Generated with Claude Code
https://claude.ai/code/session_01EdV5iCFkUxFqkLZQAGLAVT