From 799bdd5793c88ad1d93689753e54b20275a094d9 Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Thu, 18 Jun 2026 23:27:18 -0700 Subject: [PATCH] fix: avoid public manifest cache poisoning --- src/signals/focus-manifest-loader.ts | 10 ++++++---- test/unit/focus-manifest-loader.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/signals/focus-manifest-loader.ts b/src/signals/focus-manifest-loader.ts index 8b06fd6746..52d056381e 100644 --- a/src/signals/focus-manifest-loader.ts +++ b/src/signals/focus-manifest-loader.ts @@ -94,10 +94,12 @@ async function loadRepoFocusManifestWithCachePolicy( } catch { manifest = parseFocusManifest(null); } - // Persist even an ABSENT manifest (negative cache): effective settings are resolved from - // `.gittensory.yml` on every webhook, so a repo without one must not re-fetch the raw file each time. - // The TTL still refreshes it, so a newly-added manifest is picked up on the next window. - await persistRepoFocusManifest(env, repoFullName, manifest); + if (!cachePolicy.publicOnly) { + // Persist even an ABSENT manifest (negative cache): effective settings are resolved from + // `.gittensory.yml` on every webhook, so a repo without one must not re-fetch the raw file each time. + // The TTL still refreshes it, so a newly-added manifest is picked up on the next window. + await persistRepoFocusManifest(env, repoFullName, manifest); + } return manifest; } diff --git a/test/unit/focus-manifest-loader.test.ts b/test/unit/focus-manifest-loader.test.ts index 9daaf8519c..b71054861a 100644 --- a/test/unit/focus-manifest-loader.test.ts +++ b/test/unit/focus-manifest-loader.test.ts @@ -114,6 +114,29 @@ describe("focus-manifest loader", () => { expect(manifest.gate.readinessMinScore).toBeNull(); }); + it("does not let public-only loads overwrite API-backed private manifests", async () => { + const env = createTestEnv(); + await upsertRepoFocusManifest(env, "owner/private-gates", { + wantedPaths: ["private/"], + gate: { linkedIssue: "block", readinessMinScore: 99 }, + }); + + const publicManifest = await loadPublicRepoFocusManifest(env, "owner/private-gates", { + fetcher: async () => JSON.stringify({ wantedPaths: ["public/"], gate: { linkedIssue: "advisory" } }), + }); + const privateManifest = await loadRepoFocusManifest(env, "owner/private-gates", { + fetcher: async () => { + throw new Error("should keep using the API-backed private manifest"); + }, + }); + + expect(publicManifest.source).toBe("repo_file"); + expect(publicManifest.wantedPaths).toEqual(["public/"]); + expect(privateManifest.source).toBe("api_record"); + expect(privateManifest.wantedPaths).toEqual(["private/"]); + expect(privateManifest.gate.linkedIssue).toBe("block"); + }); + it("bulk-loads manifests for many repos with a concurrency cap", async () => { const env = createTestEnv(); let active = 0;