diff --git a/actions/setup/js/setup_comment_memory_files.cjs b/actions/setup/js/setup_comment_memory_files.cjs index 147795ee5f..ba2e6a075f 100644 --- a/actions/setup/js/setup_comment_memory_files.cjs +++ b/actions/setup/js/setup_comment_memory_files.cjs @@ -4,7 +4,9 @@ require("./shim.cjs"); const fs = require("fs"); const path = require("path"); +const { ERR_VALIDATION } = require("./error_codes.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { parseAllowedRepos, validateTargetRepo } = require("./repo_helpers.cjs"); const { COMMENT_MEMORY_DIR, COMMENT_MEMORY_MAX_SCAN_PAGES, @@ -74,6 +76,23 @@ async function collectCommentMemoryFiles(githubClient, commentMemoryConfig) { return []; } + const contextRepoSlug = `${context.repo.owner}/${context.repo.repo}`; + const normalizedTargetRepoSlug = targetRepo.slug.toLowerCase(); + const normalizedContextRepoSlug = contextRepoSlug.toLowerCase(); + const isCrossRepo = normalizedTargetRepoSlug !== normalizedContextRepoSlug; + if (isCrossRepo) { + const allowedRepos = parseAllowedRepos(commentMemoryConfig?.allowed_repos); + if (allowedRepos.size === 0) { + throw new Error(`${ERR_VALIDATION}: E004: Cross-repository comment-memory setup to '${targetRepo.slug}' is not permitted. No allowlist is configured. Define 'allowed_repos' to enable cross-repository access.`); + } + + const repoValidation = validateTargetRepo(targetRepo.slug, contextRepoSlug, allowedRepos); + if (!repoValidation.valid) { + throw new Error(`${ERR_VALIDATION}: E004: ${repoValidation.error}`); + } + core.info(`comment_memory setup: cross-repo allowlist check passed for ${targetRepo.slug}`); + } + core.info(`comment_memory setup: loading managed comment memory from ${targetRepo.slug}#${targetNumber}`); const memoryMap = new Map(); let emptyPageCount = 0; diff --git a/actions/setup/js/setup_comment_memory_files.test.cjs b/actions/setup/js/setup_comment_memory_files.test.cjs index 9167cc70e6..1580880cf3 100644 --- a/actions/setup/js/setup_comment_memory_files.test.cjs +++ b/actions/setup/js/setup_comment_memory_files.test.cjs @@ -96,4 +96,122 @@ describe("setup_comment_memory_files", () => { expect(fs.readFileSync(memoryFile, "utf8")).toBe("Late memory\n"); expect(listComments).toHaveBeenCalledTimes(6); }); + + it("rejects cross-repo comment-memory setup when no allowlist is configured", async () => { + fs.writeFileSync(CONFIG_PATH, JSON.stringify({ "comment-memory": { target: "triggering", "target-repo": "other-org/other-repo" } })); + const listComments = vi.fn().mockResolvedValue({ data: [] }); + global.github = { + rest: { + issues: { + listComments, + }, + }, + }; + + const module = await import("./setup_comment_memory_files.cjs"); + await module.main(); + + expect(listComments).not.toHaveBeenCalled(); + expect(global.core.warning).toHaveBeenCalledWith(expect.stringContaining("E004")); + expect(global.core.warning).toHaveBeenCalledWith(expect.stringContaining("No allowlist is configured")); + }); + + it("rejects cross-repo comment-memory setup when target repo is not in allowlist", async () => { + fs.writeFileSync( + CONFIG_PATH, + JSON.stringify({ + "comment-memory": { + target: "triggering", + "target-repo": "other-org/other-repo", + allowed_repos: ["other-org/different-repo"], + }, + }) + ); + const listComments = vi.fn().mockResolvedValue({ data: [] }); + global.github = { + rest: { + issues: { + listComments, + }, + }, + }; + + const module = await import("./setup_comment_memory_files.cjs"); + await module.main(); + + expect(listComments).not.toHaveBeenCalled(); + expect(global.core.warning).toHaveBeenCalledWith(expect.stringContaining("E004")); + expect(global.core.warning).toHaveBeenCalledWith(expect.stringContaining("not in the allowed-repos list")); + }); + + it("allows cross-repo comment-memory setup when target repo is in allowlist", async () => { + fs.writeFileSync( + CONFIG_PATH, + JSON.stringify({ + "comment-memory": { + target: "triggering", + "target-repo": "other-org/other-repo", + allowed_repos: ["other-org/other-repo"], + }, + }) + ); + const listComments = vi.fn().mockResolvedValue({ + data: [{ body: '\nCross repo memory\n' }], + }); + global.github = { + rest: { + issues: { + listComments, + }, + }, + }; + + const module = await import("./setup_comment_memory_files.cjs"); + await module.main(); + + expect(listComments).toHaveBeenCalledWith( + expect.objectContaining({ + owner: "other-org", + repo: "other-repo", + issue_number: 42, + }) + ); + const memoryFile = path.join(COMMENT_MEMORY_DIR, "default.md"); + expect(fs.existsSync(memoryFile)).toBe(true); + expect(fs.readFileSync(memoryFile, "utf8")).toBe("Cross repo memory\n"); + }); + + it("treats target-repo as same repo when slug differs only by case", async () => { + fs.writeFileSync( + CONFIG_PATH, + JSON.stringify({ + "comment-memory": { + target: "triggering", + "target-repo": "Octo/Repo", + }, + }) + ); + const listComments = vi.fn().mockResolvedValue({ + data: [{ body: '\nSame repo memory\n' }], + }); + global.github = { + rest: { + issues: { + listComments, + }, + }, + }; + + const module = await import("./setup_comment_memory_files.cjs"); + await module.main(); + + expect(listComments).toHaveBeenCalledWith( + expect.objectContaining({ + owner: "Octo", + repo: "Repo", + issue_number: 42, + }) + ); + expect(global.core.warning).not.toHaveBeenCalledWith(expect.stringContaining("E004")); + }); });