From d5411fcaf488fd96192079808e385040951c28c8 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Sun, 8 Nov 2026 14:49:29 +0000 Subject: [PATCH] fix(github): a rate-limited workflow approval read no longer reports action-required The REST half of listWorkflowRunsRequiringApproval wrapped every failure, rate limits and host pauses included, as a read error. The provider only passes rate limits through, so a paused read became the "Workflow approval status" action-required placeholder and woke PR watches on green heads. Rate-limit and pause errors now keep their tags. Co-Authored-By: Claude Opus 5.5 --- .../src/server/GitHubPullRequestApi.test.ts | 37 +++++++++++++++++++ .../src/server/GitHubPullRequestApi.ts | 10 ++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/source-control-github/src/server/GitHubPullRequestApi.test.ts b/packages/source-control-github/src/server/GitHubPullRequestApi.test.ts index f46c91fd6957..fcab54973d7f 100644 --- a/packages/source-control-github/src/server/GitHubPullRequestApi.test.ts +++ b/packages/source-control-github/src/server/GitHubPullRequestApi.test.ts @@ -12,6 +12,7 @@ import * as TestClock from "effect/testing/TestClock"; import * as Redacted from "effect/Redacted"; import { HttpClient, HttpClientResponse } from "effect/http"; +import { SourceControlProviderKind } from "@t3tools/contracts"; import * as GitHubRest from "./GitHubRest.ts"; import * as GitHubRequestPolicy from "./GitHubRequestPolicy.ts"; @@ -2864,6 +2865,42 @@ layer("GitHubPullRequestApi.layer", (it) => { }), ); + it.effect.each([ + new GitHubRequestPolicy.GitHubApiRateLimitError({ + host: "github.com", + operation: "listWorkflowRunsRequiringApproval", + }), + new SourceControlRateLimit.SourceControlRateLimitPausedError({ + provider: SourceControlProviderKind.make("github"), + host: "github.com", + retryAt: Date.parse("2099-08-13T14:00:00Z"), + }), + ])("keeps $_tag when the workflow run read is rate-limited", (refusal) => + Effect.gen(function* () { + mockedExecute.mockImplementation((call) => + call.kind === "graphql" + ? Effect.succeed(output(encodeJson(heads([7])))) + : Effect.fail(refusal), + ); + const cli = yield* GitHubPullRequestApi.GitHubPullRequestApi; + + const error = yield* Effect.flip( + cli.listWorkflowRunsRequiringApproval({ + cwd: "/w", + repository: "acme/web", + host: "github.com", + number: 7, + headSha: "abc123", + headBranch: "feat/page", + headRepositoryOwner: "octocat", + isCrossRepository: true, + }), + ); + + assert.strictEqual(error._tag, refusal._tag); + }), + ); + it.effect("sends a comment body as a variable, never inside the document", () => Effect.gen(function* () { route([NODE_ID_QUERY, nodeIdAnswer("PR_7")]); diff --git a/packages/source-control-github/src/server/GitHubPullRequestApi.ts b/packages/source-control-github/src/server/GitHubPullRequestApi.ts index ce38cc726a24..affb8bc41afc 100644 --- a/packages/source-control-github/src/server/GitHubPullRequestApi.ts +++ b/packages/source-control-github/src/server/GitHubPullRequestApi.ts @@ -1562,7 +1562,15 @@ export const make = Effect.gen(function* () { operation: "listWorkflowRunsRequiringApproval", path: `repos/${owner}/${name}/actions/runs?head_sha=${encodeURIComponent(input.headSha)}&branch=${encodeURIComponent(input.headBranch)}&event=pull_request&status=action_required&per_page=100&page=${page}`, decode: decodeWorkflowRunsJson, - }).pipe(Effect.mapError((error) => workflowApprovalReadError(input.cwd, error))); + }).pipe( + // A rate-limited read is no answer; callers wait instead of reporting approval unknown. + Effect.mapError((error) => + error._tag === "GitHubApiRateLimitError" || + error._tag === "SourceControlRateLimitPausedError" + ? error + : workflowApprovalReadError(input.cwd, error), + ), + ); runs.push(...read.runs); if (read.rawCount < 100) break; }