diff --git a/actions/setup/js/repo_helpers.cjs b/actions/setup/js/repo_helpers.cjs index 4f9e2d3c688..d0fc57e236d 100644 --- a/actions/setup/js/repo_helpers.cjs +++ b/actions/setup/js/repo_helpers.cjs @@ -109,8 +109,16 @@ function getDefaultTargetRepo(config) { if (targetRepoSlug) { return targetRepoSlug; } - // Fall back to context repo - return `${context.repo.owner}/${context.repo.repo}`; + // Fall back to context repo (only available in github-script or shim-provided context) + if (typeof context !== "undefined" && context.repo?.owner && context.repo?.repo) { + return `${context.repo.owner}/${context.repo.repo}`; + } + // Fall back to GITHUB_REPOSITORY env var (available in standalone daemon mode) + const githubRepo = process.env.GITHUB_REPOSITORY; + if (githubRepo) { + return githubRepo; + } + return ""; } /** @@ -214,11 +222,23 @@ function resolveTargetRepoConfig(config) { * @returns {RepoResolutionResult} */ function resolveAndValidateRepo(item, defaultTargetRepo, allowedRepos, operationType) { - // Determine target repository for this operation - const itemRepo = item.repo ? String(item.repo).trim() : defaultTargetRepo; + // Normalize the default target repo (may be empty if not configured) + const trimmedDefaultTargetRepo = defaultTargetRepo ? String(defaultTargetRepo).trim() : ""; + + // Determine target repository for this operation, allowing item.repo to override + const rawItemRepo = item && item.repo != null ? String(item.repo).trim() : ""; + const itemRepo = rawItemRepo || trimmedDefaultTargetRepo; + + // If we still don't have a repo after considering overrides, treat as configuration/environment issue + if (!itemRepo) { + return { + success: false, + error: `Unable to determine target repository for ${operationType}. Set GH_AW_TARGET_REPO_SLUG, ensure GITHUB_REPOSITORY is available, or configure target-repo in safe-outputs settings.`, + }; + } // Validate the repository is allowed - const repoValidation = validateRepo(itemRepo, defaultTargetRepo, allowedRepos); + const repoValidation = validateRepo(itemRepo, trimmedDefaultTargetRepo, allowedRepos); if (!repoValidation.valid) { // When valid is false, error is guaranteed to be non-null const errorMessage = repoValidation.error; diff --git a/actions/setup/js/repo_helpers.test.cjs b/actions/setup/js/repo_helpers.test.cjs index 8df2ab0c72f..53e5fe1c841 100644 --- a/actions/setup/js/repo_helpers.test.cjs +++ b/actions/setup/js/repo_helpers.test.cjs @@ -14,6 +14,7 @@ describe("repo_helpers", () => { beforeEach(() => { vi.resetModules(); delete process.env.GH_AW_TARGET_REPO_SLUG; + delete process.env.GITHUB_REPOSITORY; global.context = mockContext; }); @@ -97,6 +98,31 @@ describe("repo_helpers", () => { const result = getDefaultTargetRepo(); expect(result).toBe("test-owner/test-repo"); }); + + it("should use GITHUB_REPOSITORY env var when context is not defined", async () => { + process.env.GITHUB_REPOSITORY = "env-owner/env-repo"; + // @ts-expect-error - Simulating standalone daemon where context is not available + delete global.context; + const { getDefaultTargetRepo } = await import("./repo_helpers.cjs"); + const result = getDefaultTargetRepo(); + expect(result).toBe("env-owner/env-repo"); + }); + + it("should prefer GH_AW_TARGET_REPO_SLUG over GITHUB_REPOSITORY", async () => { + process.env.GH_AW_TARGET_REPO_SLUG = "slug-org/slug-repo"; + process.env.GITHUB_REPOSITORY = "env-owner/env-repo"; + const { getDefaultTargetRepo } = await import("./repo_helpers.cjs"); + const result = getDefaultTargetRepo(); + expect(result).toBe("slug-org/slug-repo"); + }); + + it("should return empty string when no config, no env vars, and no context", async () => { + // @ts-expect-error - Simulating standalone daemon where context is not available + delete global.context; + const { getDefaultTargetRepo } = await import("./repo_helpers.cjs"); + const result = getDefaultTargetRepo(); + expect(result).toBe(""); + }); }); describe("isRepoAllowed", () => { @@ -371,6 +397,31 @@ describe("repo_helpers", () => { expect(result.repo).toBe("github/gh-aw"); expect(result.repoParts).toEqual({ owner: "github", repo: "gh-aw" }); }); + + it("should fail with descriptive error when defaultTargetRepo is empty", async () => { + const { resolveAndValidateRepo } = await import("./repo_helpers.cjs"); + const item = {}; + const allowedRepos = new Set(); + + const result = resolveAndValidateRepo(item, "", allowedRepos, "pull request"); + + expect(result.success).toBe(false); + expect(result.error).toContain("Unable to determine target repository for pull request"); + expect(result.error).toContain("GH_AW_TARGET_REPO_SLUG"); + expect(result.error).toContain("GITHUB_REPOSITORY"); + }); + + it("should succeed when item.repo is provided even if defaultTargetRepo is empty", async () => { + const { resolveAndValidateRepo } = await import("./repo_helpers.cjs"); + const item = { repo: "org/explicit-repo" }; + const allowedRepos = new Set(["org/explicit-repo"]); + + const result = resolveAndValidateRepo(item, "", allowedRepos, "pull request"); + + expect(result.success).toBe(true); + expect(result.repo).toBe("org/explicit-repo"); + expect(result.repoParts).toEqual({ owner: "org", repo: "explicit-repo" }); + }); }); describe("resolveTargetRepoConfig", () => { diff --git a/actions/setup/js/unassign_from_user.test.cjs b/actions/setup/js/unassign_from_user.test.cjs index 47f65861b9a..af12277978c 100644 --- a/actions/setup/js/unassign_from_user.test.cjs +++ b/actions/setup/js/unassign_from_user.test.cjs @@ -43,6 +43,8 @@ describe("unassign_from_user (Handler Factory Architecture)", () => { beforeEach(async () => { vi.clearAllMocks(); + delete process.env.GITHUB_REPOSITORY; + delete process.env.GH_AW_TARGET_REPO_SLUG; const { main } = require("./unassign_from_user.cjs"); handler = await main({