From dc35ac74900be7cec520fa819f9d1876fa2b0875 Mon Sep 17 00:00:00 2001 From: lex00 Date: Fri, 19 Jun 2026 14:47:03 -0600 Subject: [PATCH] fix(rulesets): tolerate 403 on the rulesets fetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The live e2e surfaced it: GET /orgs/{org}/rulesets 403s when the App lacks that scope, and fetchRulesets only caught 404 — so an inaccessible org-rulesets read aborted the whole cycle (incl. repo rulesets). Treat 403 like 404 (return empty), matching the token cycles' graceful degradation. +1 unit test; action bundle rebuilt. Co-Authored-By: Claude Opus 4.8 --- action/index.mjs | 4 +++- src/cycles/rulesets.test.ts | 10 ++++++++++ src/cycles/rulesets.ts | 8 ++++++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/action/index.mjs b/action/index.mjs index 6881a36..6fe0a41 100644 --- a/action/index.mjs +++ b/action/index.mjs @@ -230025,7 +230025,9 @@ async function fetchRulesets(client, basePath, budget) { `${basePath}?per_page=${PER_PAGE3}&page=${page}` ); } catch (err) { - if (err instanceof Error && err.message.includes("404")) return []; + if (err instanceof Error && (err.message.includes("404") || err.message.includes("403"))) { + return []; + } throw err; } if (!Array.isArray(batch) || batch.length === 0) break; diff --git a/src/cycles/rulesets.test.ts b/src/cycles/rulesets.test.ts index e0fc5bd..89bea43 100644 --- a/src/cycles/rulesets.test.ts +++ b/src/cycles/rulesets.test.ts @@ -137,6 +137,16 @@ describe("fetchRulesets", () => { expect(live).toEqual([]); }); + it("returns empty on a 403 (App lacks permission for this rulesets scope)", async () => { + const client: MockClient = makeMockClient(); + client.request = async (method: string, path: string): Promise => { + client.calls.push({ method, path }); + throw new Error("GET /orgs/test-org/rulesets returned 403: Resource not accessible by integration"); + }; + const live = await fetchRulesets(client, "/orgs/test-org/rulesets", makeBudget()); + expect(live).toEqual([]); + }); + it("charges the budget: one list page + one detail per ruleset", async () => { const client = makeMockClient({ "GET /orgs/test-org/rulesets?per_page=100&page=1": [ diff --git a/src/cycles/rulesets.ts b/src/cycles/rulesets.ts index 7d3d16e..fafa125 100644 --- a/src/cycles/rulesets.ts +++ b/src/cycles/rulesets.ts @@ -82,7 +82,9 @@ const PER_PAGE = 100; * `/repos/{o}/{r}/rulesets`): list (paginated) then GET each ruleset's detail * so `rules`/`conditions`/`bypassActors` are populated for diffing. Charges the * budget per request and stops when exhausted. A 404 (rulesets unsupported / - * repo missing) yields an empty list. + * repo missing) or 403 (the App lacks permission for this rulesets scope — e.g. + * org rulesets) yields an empty list rather than aborting the cycle, so an + * inaccessible org-rulesets read never blocks repo-rulesets reconciliation. */ export async function fetchRulesets( client: AppClient, @@ -102,7 +104,9 @@ export async function fetchRulesets( `${basePath}?per_page=${PER_PAGE}&page=${page}`, ); } catch (err) { - if (err instanceof Error && err.message.includes("404")) return []; + if (err instanceof Error && (err.message.includes("404") || err.message.includes("403"))) { + return []; + } throw err; } if (!Array.isArray(batch) || batch.length === 0) break;