Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/patch-cross-repo-allowlist-validation.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions actions/setup/js/assign_to_agent.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const { getErrorMessage } = require("./error_helpers.cjs");
const { resolveTarget } = require("./safe_output_helpers.cjs");
const { loadTemporaryIdMap, resolveRepoIssueTarget } = require("./temporary_id.cjs");
const { sleep } = require("./error_recovery.cjs");
const { parseAllowedRepos, validateRepo } = require("./repo_helpers.cjs");

async function main() {
const result = loadAgentOutput();
Expand Down Expand Up @@ -93,9 +94,21 @@ async function main() {
let targetOwner = context.repo.owner;
let targetRepo = context.repo.repo;

// Get allowed repos configuration for cross-repo validation
const allowedReposEnv = process.env.GH_AW_AGENT_ALLOWED_REPOS?.trim();
const allowedRepos = parseAllowedRepos(allowedReposEnv);
const defaultRepo = `${context.repo.owner}/${context.repo.repo}`;

if (targetRepoEnv) {
const parts = targetRepoEnv.split("/");
if (parts.length === 2) {
// Validate target repository against allowlist
const repoValidation = validateRepo(targetRepoEnv, defaultRepo, allowedRepos);
if (!repoValidation.valid) {
core.setFailed(`E004: ${repoValidation.error}`);
return;
}

targetOwner = parts[0];
targetRepo = parts[1];
core.info(`Using target repository: ${targetOwner}/${targetRepo}`);
Expand Down
111 changes: 111 additions & 0 deletions actions/setup/js/assign_to_agent.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ describe("assign_to_agent", () => {

it("should use target repository when configured", async () => {
process.env.GH_AW_TARGET_REPO = "other-owner/other-repo";
process.env.GH_AW_AGENT_ALLOWED_REPOS = "other-owner/other-repo"; // Add to allowlist
setAgentOutput({
items: [
{
Expand Down Expand Up @@ -1033,4 +1034,114 @@ describe("assign_to_agent", () => {
const delayMessages = mockCore.info.mock.calls.filter(call => call[0].includes("Waiting 10 seconds before processing next agent assignment"));
expect(delayMessages).toHaveLength(2);
}, 30000); // Increase timeout to 30 seconds to account for 2x10s delays

describe("Cross-repository allowlist validation", () => {
it("should reject target repository not in allowlist", async () => {
process.env.GH_AW_TARGET_REPO = "other-owner/other-repo";
process.env.GH_AW_AGENT_ALLOWED_REPOS = "allowed-owner/allowed-repo";

setAgentOutput({
items: [
{
type: "assign_to_agent",
issue_number: 42,
agent: "copilot",
},
],
errors: [],
});

await eval(`(async () => { ${assignToAgentScript}; await main(); })()`);

expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("E004:"));
expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("not in the allowed-repos list"));
});

it("should allow target repository in allowlist", async () => {
process.env.GH_AW_TARGET_REPO = "allowed-owner/allowed-repo";
process.env.GH_AW_AGENT_ALLOWED_REPOS = "allowed-owner/allowed-repo,other-owner/other-repo";

setAgentOutput({
items: [
{
type: "assign_to_agent",
issue_number: 42,
agent: "copilot",
},
],
errors: [],
});

// Mock GraphQL responses
mockGithub.graphql
.mockResolvedValueOnce({
repository: {
suggestedActors: {
nodes: [{ login: "copilot-swe-agent", id: "MDQ6VXNlcjE=" }],
},
},
})
.mockResolvedValueOnce({
repository: {
issue: { id: "issue-id", assignees: { nodes: [] } },
},
})
.mockResolvedValueOnce({
addAssigneesToAssignable: {
assignable: { assignees: { nodes: [{ login: "copilot-swe-agent" }] } },
},
});

await eval(`(async () => { ${assignToAgentScript}; await main(); })()`);

expect(mockCore.setFailed).not.toHaveBeenCalled();
// Check that the target repository was used and assignment proceeded
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Using target repository: allowed-owner/allowed-repo"));
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Looking for copilot coding agent"));
}, 20000);

it("should allow default repository even without allowlist", async () => {
// Default repo is test-owner/test-repo (from mockContext)
process.env.GH_AW_TARGET_REPO = "test-owner/test-repo";
// Empty or no allowlist

setAgentOutput({
items: [
{
type: "assign_to_agent",
issue_number: 42,
agent: "copilot",
},
],
errors: [],
});

// Mock GraphQL responses
mockGithub.graphql
.mockResolvedValueOnce({
repository: {
suggestedActors: {
nodes: [{ login: "copilot-swe-agent", id: "MDQ6VXNlcjE=" }],
},
},
})
.mockResolvedValueOnce({
repository: {
issue: { id: "issue-id", assignees: { nodes: [] } },
},
})
.mockResolvedValueOnce({
addAssigneesToAssignable: {
assignable: { assignees: { nodes: [{ login: "copilot-swe-agent" }] } },
},
});

await eval(`(async () => { ${assignToAgentScript}; await main(); })()`);

expect(mockCore.setFailed).not.toHaveBeenCalled();
// Check that assignment proceeded without errors
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Using target repository: test-owner/test-repo"));
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Looking for copilot coding agent"));
}, 20000);
});
});
5 changes: 5 additions & 0 deletions actions/setup/js/checkout_pr_branch.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
* 3. Other PR events (issue_comment, pull_request_review, etc.):
* - Also run in base repository context
* - Must use `gh pr checkout` to get PR branch
*
* NOTE: This handler operates within the PR context from the workflow event
* and does not support cross-repository operations or target-repo parameters.
* No allowlist validation (checkAllowedRepo/validateTargetRepo) is needed as
* it only works with the PR from the triggering event.
*/

const { getErrorMessage } = require("./error_helpers.cjs");
Expand Down
14 changes: 14 additions & 0 deletions actions/setup/js/create_agent_session.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/// <reference types="@actions/github-script" />

const { getErrorMessage } = require("./error_helpers.cjs");
const { parseAllowedRepos, validateRepo } = require("./repo_helpers.cjs");

const fs = require("fs");
const path = require("path");
Expand Down Expand Up @@ -81,6 +82,19 @@ async function main() {
const baseBranch = process.env.GITHUB_AW_AGENT_SESSION_BASE || process.env.GITHUB_REF_NAME || "main";
const targetRepo = process.env.GITHUB_AW_TARGET_REPO;

// Validate target repository against allowlist if specified
if (targetRepo) {
const allowedReposEnv = process.env.GH_AW_AGENT_SESSION_ALLOWED_REPOS?.trim();
const allowedRepos = parseAllowedRepos(allowedReposEnv);
const defaultRepo = `${context.repo.owner}/${context.repo.repo}`;

const repoValidation = validateRepo(targetRepo, defaultRepo, allowedRepos);
if (!repoValidation.valid) {
core.setFailed(`E004: ${repoValidation.error}`);
return;
}
}

// Process all agent session items
const createdTasks = [];
let summaryContent = "## ✅ Agent Sessions Created\n\n";
Expand Down
Loading