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"));
+ });
});