diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index 7b733ce7cf0..c927be63a3f 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -372,6 +372,7 @@ jobs: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_CREATED_ISSUE_URL: ${{ needs.create_issue.outputs.issue_url }} GH_AW_CREATED_ISSUE_NUMBER: ${{ needs.create_issue.outputs.issue_number }} + GH_AW_TEMPORARY_ID_MAP: ${{ needs.create_issue.outputs.temporary_id_map }} GH_AW_WORKFLOW_NAME: "Smoke Claude" GH_AW_SAFE_OUTPUTS_STAGED: "true" with: @@ -1009,7 +1010,7 @@ jobs: {"add_comment":{"max":1},"create_issue":{"max":1},"missing_tool":{"max":0},"noop":{"max":1}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description (the title acts as the h1 header, so do not duplicate it in the body)","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] + [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description (the title acts as the h1 header, so do not duplicate it in the body)","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of. Can be a real issue number or a temporary_id from a previously created issue in this workflow.","type":["number","string"]},"temporary_id":{"description":"A temporary identifier (12 character hex string) for this issue that can be referenced by other issues in the same workflow run. Useful when creating a parent issue first, then sub-issues that reference it. Use '#temp:ID' format in body text to reference other issues by their temporary_id.","type":"string"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -4557,6 +4558,7 @@ jobs: outputs: issue_number: ${{ steps.create_issue.outputs.issue_number }} issue_url: ${{ steps.create_issue.outputs.issue_url }} + temporary_id_map: ${{ steps.create_issue.outputs.temporary_id_map }} steps: - name: Download agent output artifact continue-on-error: true @@ -4675,9 +4677,48 @@ jobs: } return ""; } + const crypto = require("crypto"); + const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi; + function generateTemporaryId() { + return "aw_" + crypto.randomBytes(6).toString("hex"); + } + function isTemporaryId(value) { + if (typeof value === "string") { + return /^aw_[0-9a-f]{12}$/i.test(value); + } + return false; + } + function normalizeTemporaryId(tempId) { + return String(tempId).toLowerCase(); + } + function replaceTemporaryIdReferences(text, tempIdMap) { + return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => { + const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId)); + if (issueNumber !== undefined) { + return `#${issueNumber}`; + } + return match; + }); + } + function loadTemporaryIdMap() { + const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP; + if (!mapJson || mapJson === "{}") { + return new Map(); + } + try { + const mapObject = JSON.parse(mapJson); + return new Map(Object.entries(mapObject).map(([k, v]) => [normalizeTemporaryId(k), Number(v)])); + } catch (error) { + if (typeof core !== "undefined") { + core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`); + } + return new Map(); + } + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); + core.setOutput("temporary_id_map", "{}"); const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const result = loadAgentOutput(); if (!result.success) { @@ -4697,18 +4738,25 @@ jobs: renderItem: (item, index) => { let content = `### Issue ${index + 1}\n`; content += `**Title:** ${item.title || "No title provided"}\n\n`; + if (item.temporary_id) { + content += `**Temporary ID:** ${item.temporary_id}\n\n`; + } if (item.body) { content += `**Body:**\n${item.body}\n\n`; } if (item.labels && item.labels.length > 0) { content += `**Labels:** ${item.labels.join(", ")}\n\n`; } + if (item.parent) { + content += `**Parent:** ${item.parent}\n\n`; + } return content; }, }); return; } const parentIssueNumber = context.payload?.issue?.number; + const temporaryIdMap = new Map(); const triggeringIssueNumber = context.payload?.issue?.number && !context.payload?.issue?.pull_request ? context.payload.issue.number : undefined; const triggeringPRNumber = @@ -4724,12 +4772,35 @@ jobs: const createdIssues = []; for (let i = 0; i < createIssueItems.length; i++) { const createIssueItem = createIssueItems[i]; + const temporaryId = createIssueItem.temporary_id || generateTemporaryId(); core.info( - `Processing create-issue item ${i + 1}/${createIssueItems.length}: title=${createIssueItem.title}, bodyLength=${createIssueItem.body.length}` + `Processing create-issue item ${i + 1}/${createIssueItems.length}: title=${createIssueItem.title}, bodyLength=${createIssueItem.body.length}, temporaryId=${temporaryId}` ); core.info(`Debug: createIssueItem.parent = ${JSON.stringify(createIssueItem.parent)}`); core.info(`Debug: parentIssueNumber from context = ${JSON.stringify(parentIssueNumber)}`); - const effectiveParentIssueNumber = createIssueItem.parent !== undefined ? createIssueItem.parent : parentIssueNumber; + let effectiveParentIssueNumber; + if (createIssueItem.parent !== undefined) { + if (isTemporaryId(createIssueItem.parent)) { + const resolvedParent = temporaryIdMap.get(normalizeTemporaryId(createIssueItem.parent)); + if (resolvedParent !== undefined) { + effectiveParentIssueNumber = resolvedParent; + core.info(`Resolved parent temporary ID '${createIssueItem.parent}' to issue #${effectiveParentIssueNumber}`); + } else { + core.warning( + `Parent temporary ID '${createIssueItem.parent}' not found in map. Ensure parent issue is created before sub-issues.` + ); + effectiveParentIssueNumber = undefined; + } + } else { + effectiveParentIssueNumber = parseInt(String(createIssueItem.parent), 10); + if (isNaN(effectiveParentIssueNumber)) { + core.warning(`Invalid parent value: ${createIssueItem.parent}`); + effectiveParentIssueNumber = undefined; + } + } + } else { + effectiveParentIssueNumber = parentIssueNumber; + } core.info(`Debug: effectiveParentIssueNumber = ${JSON.stringify(effectiveParentIssueNumber)}`); if (effectiveParentIssueNumber && createIssueItem.parent !== undefined) { core.info(`Using explicit parent issue number from item: #${effectiveParentIssueNumber}`); @@ -4747,7 +4818,8 @@ jobs: .map(label => (label.length > 64 ? label.substring(0, 64) : label)) .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; - let bodyLines = createIssueItem.body.split("\n"); + let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap); + let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; } @@ -4799,6 +4871,8 @@ jobs: }); core.info("Created issue #" + issue.number + ": " + issue.html_url); createdIssues.push(issue); + temporaryIdMap.set(normalizeTemporaryId(temporaryId), issue.number); + core.info(`Stored temporary ID mapping: ${temporaryId} -> #${issue.number}`); core.info(`Debug: About to check if sub-issue linking is needed. effectiveParentIssueNumber = ${effectiveParentIssueNumber}`); if (effectiveParentIssueNumber) { core.info(`Attempting to link issue #${issue.number} as sub-issue of #${effectiveParentIssueNumber}`); @@ -4890,6 +4964,9 @@ jobs: } await core.summary.addRaw(summaryContent).write(); } + const tempIdMapObject = Object.fromEntries(temporaryIdMap); + core.setOutput("temporary_id_map", JSON.stringify(tempIdMapObject)); + core.info(`Temporary ID map: ${JSON.stringify(tempIdMapObject)}`); core.info(`Successfully created ${createdIssues.length} issue(s)`); } (async () => { diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index 1d928c6ca4b..eb332e350b9 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -257,6 +257,7 @@ jobs: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_CREATED_ISSUE_URL: ${{ needs.create_issue.outputs.issue_url }} GH_AW_CREATED_ISSUE_NUMBER: ${{ needs.create_issue.outputs.issue_number }} + GH_AW_TEMPORARY_ID_MAP: ${{ needs.create_issue.outputs.temporary_id_map }} GH_AW_WORKFLOW_NAME: "Smoke Codex" GH_AW_SAFE_OUTPUTS_STAGED: "true" with: @@ -785,7 +786,7 @@ jobs: {"add_comment":{"max":1},"create_issue":{"max":1},"missing_tool":{"max":0},"noop":{"max":1}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description (the title acts as the h1 header, so do not duplicate it in the body)","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] + [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description (the title acts as the h1 header, so do not duplicate it in the body)","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of. Can be a real issue number or a temporary_id from a previously created issue in this workflow.","type":["number","string"]},"temporary_id":{"description":"A temporary identifier (12 character hex string) for this issue that can be referenced by other issues in the same workflow run. Useful when creating a parent issue first, then sub-issues that reference it. Use '#temp:ID' format in body text to reference other issues by their temporary_id.","type":"string"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -3957,6 +3958,7 @@ jobs: outputs: issue_number: ${{ steps.create_issue.outputs.issue_number }} issue_url: ${{ steps.create_issue.outputs.issue_url }} + temporary_id_map: ${{ steps.create_issue.outputs.temporary_id_map }} steps: - name: Download agent output artifact continue-on-error: true @@ -4075,9 +4077,48 @@ jobs: } return ""; } + const crypto = require("crypto"); + const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi; + function generateTemporaryId() { + return "aw_" + crypto.randomBytes(6).toString("hex"); + } + function isTemporaryId(value) { + if (typeof value === "string") { + return /^aw_[0-9a-f]{12}$/i.test(value); + } + return false; + } + function normalizeTemporaryId(tempId) { + return String(tempId).toLowerCase(); + } + function replaceTemporaryIdReferences(text, tempIdMap) { + return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => { + const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId)); + if (issueNumber !== undefined) { + return `#${issueNumber}`; + } + return match; + }); + } + function loadTemporaryIdMap() { + const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP; + if (!mapJson || mapJson === "{}") { + return new Map(); + } + try { + const mapObject = JSON.parse(mapJson); + return new Map(Object.entries(mapObject).map(([k, v]) => [normalizeTemporaryId(k), Number(v)])); + } catch (error) { + if (typeof core !== "undefined") { + core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`); + } + return new Map(); + } + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); + core.setOutput("temporary_id_map", "{}"); const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const result = loadAgentOutput(); if (!result.success) { @@ -4097,18 +4138,25 @@ jobs: renderItem: (item, index) => { let content = `### Issue ${index + 1}\n`; content += `**Title:** ${item.title || "No title provided"}\n\n`; + if (item.temporary_id) { + content += `**Temporary ID:** ${item.temporary_id}\n\n`; + } if (item.body) { content += `**Body:**\n${item.body}\n\n`; } if (item.labels && item.labels.length > 0) { content += `**Labels:** ${item.labels.join(", ")}\n\n`; } + if (item.parent) { + content += `**Parent:** ${item.parent}\n\n`; + } return content; }, }); return; } const parentIssueNumber = context.payload?.issue?.number; + const temporaryIdMap = new Map(); const triggeringIssueNumber = context.payload?.issue?.number && !context.payload?.issue?.pull_request ? context.payload.issue.number : undefined; const triggeringPRNumber = @@ -4124,12 +4172,35 @@ jobs: const createdIssues = []; for (let i = 0; i < createIssueItems.length; i++) { const createIssueItem = createIssueItems[i]; + const temporaryId = createIssueItem.temporary_id || generateTemporaryId(); core.info( - `Processing create-issue item ${i + 1}/${createIssueItems.length}: title=${createIssueItem.title}, bodyLength=${createIssueItem.body.length}` + `Processing create-issue item ${i + 1}/${createIssueItems.length}: title=${createIssueItem.title}, bodyLength=${createIssueItem.body.length}, temporaryId=${temporaryId}` ); core.info(`Debug: createIssueItem.parent = ${JSON.stringify(createIssueItem.parent)}`); core.info(`Debug: parentIssueNumber from context = ${JSON.stringify(parentIssueNumber)}`); - const effectiveParentIssueNumber = createIssueItem.parent !== undefined ? createIssueItem.parent : parentIssueNumber; + let effectiveParentIssueNumber; + if (createIssueItem.parent !== undefined) { + if (isTemporaryId(createIssueItem.parent)) { + const resolvedParent = temporaryIdMap.get(normalizeTemporaryId(createIssueItem.parent)); + if (resolvedParent !== undefined) { + effectiveParentIssueNumber = resolvedParent; + core.info(`Resolved parent temporary ID '${createIssueItem.parent}' to issue #${effectiveParentIssueNumber}`); + } else { + core.warning( + `Parent temporary ID '${createIssueItem.parent}' not found in map. Ensure parent issue is created before sub-issues.` + ); + effectiveParentIssueNumber = undefined; + } + } else { + effectiveParentIssueNumber = parseInt(String(createIssueItem.parent), 10); + if (isNaN(effectiveParentIssueNumber)) { + core.warning(`Invalid parent value: ${createIssueItem.parent}`); + effectiveParentIssueNumber = undefined; + } + } + } else { + effectiveParentIssueNumber = parentIssueNumber; + } core.info(`Debug: effectiveParentIssueNumber = ${JSON.stringify(effectiveParentIssueNumber)}`); if (effectiveParentIssueNumber && createIssueItem.parent !== undefined) { core.info(`Using explicit parent issue number from item: #${effectiveParentIssueNumber}`); @@ -4147,7 +4218,8 @@ jobs: .map(label => (label.length > 64 ? label.substring(0, 64) : label)) .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; - let bodyLines = createIssueItem.body.split("\n"); + let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap); + let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; } @@ -4199,6 +4271,8 @@ jobs: }); core.info("Created issue #" + issue.number + ": " + issue.html_url); createdIssues.push(issue); + temporaryIdMap.set(normalizeTemporaryId(temporaryId), issue.number); + core.info(`Stored temporary ID mapping: ${temporaryId} -> #${issue.number}`); core.info(`Debug: About to check if sub-issue linking is needed. effectiveParentIssueNumber = ${effectiveParentIssueNumber}`); if (effectiveParentIssueNumber) { core.info(`Attempting to link issue #${issue.number} as sub-issue of #${effectiveParentIssueNumber}`); @@ -4290,6 +4364,9 @@ jobs: } await core.summary.addRaw(summaryContent).write(); } + const tempIdMapObject = Object.fromEntries(temporaryIdMap); + core.setOutput("temporary_id_map", JSON.stringify(tempIdMapObject)); + core.info(`Temporary ID map: ${JSON.stringify(tempIdMapObject)}`); core.info(`Successfully created ${createdIssues.length} issue(s)`); } (async () => { diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 31855c0cb2e..e6a925958ae 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -257,6 +257,7 @@ jobs: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} GH_AW_CREATED_ISSUE_URL: ${{ needs.create_issue.outputs.issue_url }} GH_AW_CREATED_ISSUE_NUMBER: ${{ needs.create_issue.outputs.issue_number }} + GH_AW_TEMPORARY_ID_MAP: ${{ needs.create_issue.outputs.temporary_id_map }} GH_AW_WORKFLOW_NAME: "Smoke Copilot" GH_AW_SAFE_OUTPUTS_STAGED: "true" with: @@ -793,7 +794,7 @@ jobs: {"add_comment":{"max":1},"create_issue":{"max":1},"missing_tool":{"max":0},"noop":{"max":1}} EOF cat > /tmp/gh-aw/safeoutputs/tools.json << 'EOF' - [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description (the title acts as the h1 header, so do not duplicate it in the body)","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of","type":"number"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] + [{"description":"Create a new GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Issue body/description (the title acts as the h1 header, so do not duplicate it in the body)","type":"string"},"labels":{"description":"Issue labels","items":{"type":"string"},"type":"array"},"parent":{"description":"Parent issue number to create this issue as a sub-issue of. Can be a real issue number or a temporary_id from a previously created issue in this workflow.","type":["number","string"]},"temporary_id":{"description":"A temporary identifier (12 character hex string) for this issue that can be referenced by other issues in the same workflow run. Useful when creating a parent issue first, then sub-issues that reference it. Use '#temp:ID' format in body text to reference other issues by their temporary_id.","type":"string"},"title":{"description":"Issue title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_issue"},{"description":"Add a comment to a GitHub issue, pull request, or discussion","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body/content","type":"string"},"item_number":{"description":"Issue, pull request or discussion number","type":"number"}},"required":["body","item_number"],"type":"object"},"name":"add_comment"},{"description":"Report a missing tool or functionality needed to complete tasks","inputSchema":{"additionalProperties":false,"properties":{"alternatives":{"description":"Possible alternatives or workarounds (max 256 characters)","type":"string"},"reason":{"description":"Why this tool is needed (max 256 characters)","type":"string"},"tool":{"description":"Name of the missing tool (max 128 characters)","type":"string"}},"required":["tool","reason"],"type":"object"},"name":"missing_tool"},{"description":"Log a message for transparency when no significant actions are needed. Use this to ensure workflows produce human-visible artifacts even when no other actions are taken (e.g., 'Analysis complete - no issues found').","inputSchema":{"additionalProperties":false,"properties":{"message":{"description":"Message to log for transparency","type":"string"}},"required":["message"],"type":"object"},"name":"noop"}] EOF cat > /tmp/gh-aw/safeoutputs/mcp-server.cjs << 'EOF' const fs = require("fs"); @@ -4936,6 +4937,7 @@ jobs: outputs: issue_number: ${{ steps.create_issue.outputs.issue_number }} issue_url: ${{ steps.create_issue.outputs.issue_url }} + temporary_id_map: ${{ steps.create_issue.outputs.temporary_id_map }} steps: - name: Download agent output artifact continue-on-error: true @@ -5054,9 +5056,48 @@ jobs: } return ""; } + const crypto = require("crypto"); + const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi; + function generateTemporaryId() { + return "aw_" + crypto.randomBytes(6).toString("hex"); + } + function isTemporaryId(value) { + if (typeof value === "string") { + return /^aw_[0-9a-f]{12}$/i.test(value); + } + return false; + } + function normalizeTemporaryId(tempId) { + return String(tempId).toLowerCase(); + } + function replaceTemporaryIdReferences(text, tempIdMap) { + return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => { + const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId)); + if (issueNumber !== undefined) { + return `#${issueNumber}`; + } + return match; + }); + } + function loadTemporaryIdMap() { + const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP; + if (!mapJson || mapJson === "{}") { + return new Map(); + } + try { + const mapObject = JSON.parse(mapJson); + return new Map(Object.entries(mapObject).map(([k, v]) => [normalizeTemporaryId(k), Number(v)])); + } catch (error) { + if (typeof core !== "undefined") { + core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`); + } + return new Map(); + } + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); + core.setOutput("temporary_id_map", "{}"); const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const result = loadAgentOutput(); if (!result.success) { @@ -5076,18 +5117,25 @@ jobs: renderItem: (item, index) => { let content = `### Issue ${index + 1}\n`; content += `**Title:** ${item.title || "No title provided"}\n\n`; + if (item.temporary_id) { + content += `**Temporary ID:** ${item.temporary_id}\n\n`; + } if (item.body) { content += `**Body:**\n${item.body}\n\n`; } if (item.labels && item.labels.length > 0) { content += `**Labels:** ${item.labels.join(", ")}\n\n`; } + if (item.parent) { + content += `**Parent:** ${item.parent}\n\n`; + } return content; }, }); return; } const parentIssueNumber = context.payload?.issue?.number; + const temporaryIdMap = new Map(); const triggeringIssueNumber = context.payload?.issue?.number && !context.payload?.issue?.pull_request ? context.payload.issue.number : undefined; const triggeringPRNumber = @@ -5103,12 +5151,35 @@ jobs: const createdIssues = []; for (let i = 0; i < createIssueItems.length; i++) { const createIssueItem = createIssueItems[i]; + const temporaryId = createIssueItem.temporary_id || generateTemporaryId(); core.info( - `Processing create-issue item ${i + 1}/${createIssueItems.length}: title=${createIssueItem.title}, bodyLength=${createIssueItem.body.length}` + `Processing create-issue item ${i + 1}/${createIssueItems.length}: title=${createIssueItem.title}, bodyLength=${createIssueItem.body.length}, temporaryId=${temporaryId}` ); core.info(`Debug: createIssueItem.parent = ${JSON.stringify(createIssueItem.parent)}`); core.info(`Debug: parentIssueNumber from context = ${JSON.stringify(parentIssueNumber)}`); - const effectiveParentIssueNumber = createIssueItem.parent !== undefined ? createIssueItem.parent : parentIssueNumber; + let effectiveParentIssueNumber; + if (createIssueItem.parent !== undefined) { + if (isTemporaryId(createIssueItem.parent)) { + const resolvedParent = temporaryIdMap.get(normalizeTemporaryId(createIssueItem.parent)); + if (resolvedParent !== undefined) { + effectiveParentIssueNumber = resolvedParent; + core.info(`Resolved parent temporary ID '${createIssueItem.parent}' to issue #${effectiveParentIssueNumber}`); + } else { + core.warning( + `Parent temporary ID '${createIssueItem.parent}' not found in map. Ensure parent issue is created before sub-issues.` + ); + effectiveParentIssueNumber = undefined; + } + } else { + effectiveParentIssueNumber = parseInt(String(createIssueItem.parent), 10); + if (isNaN(effectiveParentIssueNumber)) { + core.warning(`Invalid parent value: ${createIssueItem.parent}`); + effectiveParentIssueNumber = undefined; + } + } + } else { + effectiveParentIssueNumber = parentIssueNumber; + } core.info(`Debug: effectiveParentIssueNumber = ${JSON.stringify(effectiveParentIssueNumber)}`); if (effectiveParentIssueNumber && createIssueItem.parent !== undefined) { core.info(`Using explicit parent issue number from item: #${effectiveParentIssueNumber}`); @@ -5126,7 +5197,8 @@ jobs: .map(label => (label.length > 64 ? label.substring(0, 64) : label)) .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; - let bodyLines = createIssueItem.body.split("\n"); + let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap); + let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; } @@ -5178,6 +5250,8 @@ jobs: }); core.info("Created issue #" + issue.number + ": " + issue.html_url); createdIssues.push(issue); + temporaryIdMap.set(normalizeTemporaryId(temporaryId), issue.number); + core.info(`Stored temporary ID mapping: ${temporaryId} -> #${issue.number}`); core.info(`Debug: About to check if sub-issue linking is needed. effectiveParentIssueNumber = ${effectiveParentIssueNumber}`); if (effectiveParentIssueNumber) { core.info(`Attempting to link issue #${issue.number} as sub-issue of #${effectiveParentIssueNumber}`); @@ -5269,6 +5343,9 @@ jobs: } await core.summary.addRaw(summaryContent).write(); } + const tempIdMapObject = Object.fromEntries(temporaryIdMap); + core.setOutput("temporary_id_map", JSON.stringify(tempIdMapObject)); + core.info(`Temporary ID map: ${JSON.stringify(tempIdMapObject)}`); core.info(`Successfully created ${createdIssues.length} issue(s)`); } (async () => {