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
14 changes: 9 additions & 5 deletions actions/setup/js/assign_to_agent.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,15 @@ async function main(config = {}) {
}

// Defer if issue_number is a temporary ID that hasn't been resolved yet
if (message.issue_number != null && isTemporaryId(message.issue_number)) {
const normalized = normalizeTemporaryId(String(message.issue_number));
if (!temporaryIdMap.has(normalized)) {
core.info(`Deferring assign_to_agent — temporary ID ${message.issue_number} not yet resolved`);
return { success: false, deferred: true };
// Strip leading '#' so both 'aw_abc1' and '#aw_abc1' (canonical validator form) are handled
if (message.issue_number != null) {
const issueNumStr = String(message.issue_number).trim();
if (isTemporaryId(issueNumStr)) {
const normalized = normalizeTemporaryId(issueNumStr);
if (!temporaryIdMap.has(normalized)) {
core.info(`Deferring assign_to_agent — temporary ID ${message.issue_number} not yet resolved`);
return { success: false, deferred: true };
}
}
}

Expand Down
76 changes: 76 additions & 0 deletions actions/setup/js/assign_to_agent.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,82 @@ describe("assign_to_agent", () => {
expect(variables.issueNumber).toBe(99);
});

it("should resolve temporary issue IDs with '#' prefix (#aw_...) using GH_AW_TEMPORARY_ID_MAP", async () => {
process.env.GH_AW_TEMPORARY_ID_MAP = JSON.stringify({
aw_abc123: { repo: "test-owner/test-repo", number: 99 },
});

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

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

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

expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Resolved temporary issue id"));

const secondCallArgs = mockGithub.graphql.mock.calls[1];
expect(secondCallArgs).toBeDefined();
const variables = secondCallArgs[1];
expect(variables.issueNumber).toBe(99);
});

it("should defer when issue_number is a '#aw_' temporary ID not yet in map", async () => {
// No temporary ID map set — the ID is unresolved
delete process.env.GH_AW_TEMPORARY_ID_MAP;

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

// Call main() factory then invoke the handler directly so we can inspect the deferred result
const deferred = await eval(`(async () => {
${assignToAgentScript};
const _handler = await main({});
const { loadTemporaryIdMap } = require("./temporary_id.cjs");
const _map = loadTemporaryIdMap();
return _handler({ type: "assign_to_agent", issue_number: "#aw_abc123", agent: "copilot" }, {}, _map);
})()`);

expect(deferred).toMatchObject({ success: false, deferred: true });
expect(mockGithub.graphql).not.toHaveBeenCalled();
});

it("should reject unsupported agents", async () => {
setAgentOutput({
items: [
Expand Down
30 changes: 4 additions & 26 deletions actions/setup/js/claude_harness.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -264,12 +264,7 @@ describe("claude_harness.cjs", () => {
});

it("extracts command from line with box-drawing pipe marker (│) before permission denied", () => {
const output = [
"\u2713 Some successful step",
"\u2717 Check if go command works (shell)",
" \u2502 go version 2>&1",
" \u2514 Permission denied and could not request permission from user",
].join("\n");
const output = ["\u2713 Some successful step", "\u2717 Check if go command works (shell)", " \u2502 go version 2>&1", " \u2514 Permission denied and could not request permission from user"].join("\n");
expect(extractDeniedCommands(output)).toEqual(["go version 2>&1"]);
});

Expand All @@ -279,26 +274,12 @@ describe("claude_harness.cjs", () => {
});

it("deduplicates repeated denied commands", () => {
const output = [
" \u2502 go version",
" Permission denied",
" \u2502 go version",
" Permission denied",
" \u2502 go version",
" Permission denied",
].join("\n");
const output = [" \u2502 go version", " Permission denied", " \u2502 go version", " Permission denied", " \u2502 go version", " Permission denied"].join("\n");
expect(extractDeniedCommands(output)).toEqual(["go version"]);
});

it("extracts multiple distinct denied commands", () => {
const output = [
" \u2502 go version 2>&1",
" Permission denied",
" \u2502 ls /usr/local/go/bin/go",
" Permission denied",
" \u2502 which go",
" Permission denied",
].join("\n");
const output = [" \u2502 go version 2>&1", " Permission denied", " \u2502 ls /usr/local/go/bin/go", " Permission denied", " \u2502 which go", " Permission denied"].join("\n");
const result = extractDeniedCommands(output);
expect(result).toContain("go version 2>&1");
expect(result).toContain("ls /usr/local/go/bin/go");
Expand All @@ -312,10 +293,7 @@ describe("claude_harness.cjs", () => {

it("does not capture suffix of a command containing an internal pipe", () => {
// "find . -name '*.go' | sort" should not match by splitting on the internal |
const output = [
" find . -name '*.go' | sort",
" Permission denied",
].join("\n");
const output = [" find . -name '*.go' | sort", " Permission denied"].join("\n");
expect(extractDeniedCommands(output)).toEqual([]);
});
});
Expand Down
45 changes: 6 additions & 39 deletions actions/setup/js/codex_harness.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,7 @@ describe("codex_harness.cjs", () => {
});

it("extracts command from line with box-drawing pipe marker (│) before permission denied", () => {
const output = [
"\u2713 Some successful step",
"\u2717 Check if go command works (shell)",
" \u2502 go version 2>&1",
" \u2514 Permission denied and could not request permission from user",
].join("\n");
const output = ["\u2713 Some successful step", "\u2717 Check if go command works (shell)", " \u2502 go version 2>&1", " \u2514 Permission denied and could not request permission from user"].join("\n");
expect(extractDeniedCommands(output)).toEqual(["go version 2>&1"]);
});

Expand All @@ -160,27 +155,13 @@ describe("codex_harness.cjs", () => {
});

it("deduplicates repeated denied commands", () => {
const output = [
" \u2502 go version",
" Permission denied",
" \u2502 go version",
" Permission denied",
" \u2502 go version",
" Permission denied",
].join("\n");
const output = [" \u2502 go version", " Permission denied", " \u2502 go version", " Permission denied", " \u2502 go version", " Permission denied"].join("\n");
const result = extractDeniedCommands(output);
expect(result).toEqual(["go version"]);
});

it("extracts multiple distinct denied commands", () => {
const output = [
" \u2502 go version 2>&1",
" Permission denied",
" \u2502 ls /usr/local/go/bin/go",
" Permission denied",
" \u2502 which go",
" Permission denied",
].join("\n");
const output = [" \u2502 go version 2>&1", " Permission denied", " \u2502 ls /usr/local/go/bin/go", " Permission denied", " \u2502 which go", " Permission denied"].join("\n");
const result = extractDeniedCommands(output);
expect(result).toContain("go version 2>&1");
expect(result).toContain("ls /usr/local/go/bin/go");
Expand All @@ -193,32 +174,18 @@ describe("codex_harness.cjs", () => {
});

it("looks back up to 3 lines for command context", () => {
const output = [
" \u2502 make test",
"Running...",
"Still running...",
" Permission denied",
].join("\n");
const output = [" \u2502 make test", "Running...", "Still running...", " Permission denied"].join("\n");
expect(extractDeniedCommands(output)).toEqual(["make test"]);
});

it("does not look back more than 3 lines", () => {
const output = [
" \u2502 make test",
"line2",
"line3",
"line4",
" Permission denied",
].join("\n");
const output = [" \u2502 make test", "line2", "line3", "line4", " Permission denied"].join("\n");
expect(extractDeniedCommands(output)).toEqual([]);
});

it("does not capture suffix of a command containing an internal pipe", () => {
// "find . -name '*.go' | sort" should not match by splitting on the internal |
const output = [
" find . -name '*.go' | sort",
" Permission denied",
].join("\n");
const output = [" find . -name '*.go' | sort", " Permission denied"].join("\n");
expect(extractDeniedCommands(output)).toEqual([]);
});
});
Expand Down
2 changes: 1 addition & 1 deletion actions/setup/js/collect_ndjson_output.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1548,7 +1548,7 @@ describe("collect_ndjson_output.cjs", () => {
outputCall = setOutputCalls.find(call => "output" === call[0]);
expect(outputCall).toBeDefined();
const parsedOutput = JSON.parse(outputCall[1]);
(expect(parsedOutput.items).toHaveLength(1), expect(parsedOutput.items[0].issue_number).toBe("aw_abc123"), expect(parsedOutput.errors).toHaveLength(0));
(expect(parsedOutput.items).toHaveLength(1), expect(parsedOutput.items[0].issue_number).toBe("#aw_abc123"), expect(parsedOutput.errors).toHaveLength(0));
}),
it("should validate assign_to_agent with optional fields", async () => {
const testFile = "/tmp/gh-aw/test-ndjson-output.txt",
Expand Down
45 changes: 6 additions & 39 deletions actions/setup/js/copilot_harness.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -246,12 +246,7 @@ describe("copilot_harness.cjs", () => {
});

it("extracts command from line with box-drawing pipe marker (│) before permission denied", () => {
const output = [
"\u2713 Some successful step",
"\u2717 Check if go command works (shell)",
" \u2502 go version 2>&1",
" \u2514 Permission denied and could not request permission from user",
].join("\n");
const output = ["\u2713 Some successful step", "\u2717 Check if go command works (shell)", " \u2502 go version 2>&1", " \u2514 Permission denied and could not request permission from user"].join("\n");
expect(extractDeniedCommands(output)).toEqual(["go version 2>&1"]);
});

Expand All @@ -261,27 +256,13 @@ describe("copilot_harness.cjs", () => {
});

it("deduplicates repeated denied commands", () => {
const output = [
" \u2502 go version",
" Permission denied",
" \u2502 go version",
" Permission denied",
" \u2502 go version",
" Permission denied",
].join("\n");
const output = [" \u2502 go version", " Permission denied", " \u2502 go version", " Permission denied", " \u2502 go version", " Permission denied"].join("\n");
const result = extractDeniedCommands(output);
expect(result).toEqual(["go version"]);
});

it("extracts multiple distinct denied commands", () => {
const output = [
" \u2502 go version 2>&1",
" Permission denied",
" \u2502 ls /usr/local/go/bin/go",
" Permission denied",
" \u2502 which go",
" Permission denied",
].join("\n");
const output = [" \u2502 go version 2>&1", " Permission denied", " \u2502 ls /usr/local/go/bin/go", " Permission denied", " \u2502 which go", " Permission denied"].join("\n");
const result = extractDeniedCommands(output);
expect(result).toContain("go version 2>&1");
expect(result).toContain("ls /usr/local/go/bin/go");
Expand All @@ -294,32 +275,18 @@ describe("copilot_harness.cjs", () => {
});

it("looks back up to 3 lines for command context", () => {
const output = [
" \u2502 make test",
"Running...",
"Still running...",
" Permission denied",
].join("\n");
const output = [" \u2502 make test", "Running...", "Still running...", " Permission denied"].join("\n");
expect(extractDeniedCommands(output)).toEqual(["make test"]);
});

it("does not look back more than 3 lines", () => {
const output = [
" \u2502 make test",
"line2",
"line3",
"line4",
" Permission denied",
].join("\n");
const output = [" \u2502 make test", "line2", "line3", "line4", " Permission denied"].join("\n");
expect(extractDeniedCommands(output)).toEqual([]);
});

it("does not capture suffix of a command containing an internal pipe", () => {
// "find . -name '*.go' | sort" should not match by splitting on the internal |
const output = [
" find . -name '*.go' | sort",
" Permission denied",
].join("\n");
const output = [" find . -name '*.go' | sort", " Permission denied"].join("\n");
expect(extractDeniedCommands(output)).toEqual([]);
});
});
Expand Down
13 changes: 6 additions & 7 deletions actions/setup/js/create_issue.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -677,13 +677,11 @@ async function main(config = {}) {
let effectiveParentIssueNumber;
let effectiveParentRepo = qualifiedItemRepo; // Default to same repo
if (message.parent !== undefined) {
// Strip # prefix if present to allow flexible temporary ID format
const parentStr = String(message.parent).trim();
const parentWithoutHash = parentStr.startsWith("#") ? parentStr.substring(1) : parentStr;

if (isTemporaryId(parentWithoutHash)) {
if (isTemporaryId(parentStr)) {
// It's a temporary ID, look it up in the map
const resolvedParent = temporaryIdMap.get(normalizeTemporaryId(parentWithoutHash));
const resolvedParent = temporaryIdMap.get(normalizeTemporaryId(parentStr));
if (resolvedParent) {
effectiveParentIssueNumber = resolvedParent.number;
effectiveParentRepo = resolvedParent.repo;
Expand All @@ -693,11 +691,12 @@ async function main(config = {}) {
}
} else {
// Check if it looks like a malformed temporary ID
if (parentWithoutHash.startsWith("aw_")) {
core.warning(`Invalid temporary ID format for parent: '${message.parent}'. Temporary IDs must be in format 'aw_' followed by 3 to 12 alphanumeric characters (A-Za-z0-9). Example: 'aw_abc' or 'aw_Test123'`);
const withoutHash = parentStr.startsWith("#") ? parentStr.substring(1) : parentStr;
if (withoutHash.startsWith("aw_")) {
core.warning(`Invalid temporary ID format for parent: '${message.parent}'. Temporary IDs must be in format 'aw_' followed by 3 to 12 alphanumeric or underscore characters (A-Za-z0-9_). Example: 'aw_abc' or 'aw_pr_fix'`);
} else {
// It's a real issue number
const parsed = parseInt(parentWithoutHash, 10);
const parsed = parseInt(withoutHash, 10);
if (!isNaN(parsed)) {
effectiveParentIssueNumber = parsed;
} else {
Expand Down
11 changes: 5 additions & 6 deletions actions/setup/js/create_project.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -335,18 +335,17 @@ async function main(config = {}, githubClient = null) {
// Resolve temporary ID in item_url if present
if (item_url && typeof item_url === "string") {
// Check if item_url contains a temporary ID (either as URL or plain ID)
// Format: https://github.com/owner/repo/issues/#aw_XXXXXXXXXXXX or #aw_XXXXXXXXXXXX
const urlMatch = item_url.match(/issues\/(#?aw_[0-9a-f]{12})\s*$/i);
const plainMatch = item_url.match(/^(#?aw_[0-9a-f]{12})\s*$/i);
// Format: https://github.com/owner/repo/issues/#aw_XXX or #aw_XXX
const urlMatch = item_url.match(/issues\/(#?aw_[A-Za-z0-9_]{3,12})\s*$/i);
const plainMatch = item_url.match(/^(#?aw_[A-Za-z0-9_]{3,12})\s*$/i);

if (urlMatch || plainMatch) {
const tempIdStr = (urlMatch && urlMatch[1]) || (plainMatch && plainMatch[1]) || "";
const tempIdWithoutHash = tempIdStr.startsWith("#") ? tempIdStr.substring(1) : tempIdStr;

// Check if it's a valid temporary ID
if (isTemporaryId(tempIdWithoutHash)) {
if (isTemporaryId(tempIdStr)) {
// Look up in the unified temporaryIdMap (Map) or resolvedTemporaryIds (plain object)
const normalizedKey = normalizeTemporaryId(tempIdWithoutHash);
const normalizedKey = normalizeTemporaryId(tempIdStr);
const resolved = temporaryIdMap instanceof Map ? temporaryIdMap.get(normalizedKey) : resolvedTemporaryIds[normalizedKey];

if (resolved && resolved.repo && resolved.number) {
Expand Down
Loading