diff --git a/.github/workflows/archie.lock.yml b/.github/workflows/archie.lock.yml index 7a78ecbe74d..18366e14df6 100644 --- a/.github/workflows/archie.lock.yml +++ b/.github/workflows/archie.lock.yml @@ -1120,6 +1120,44 @@ jobs: return `${githubServer}/${context.repo.owner}/${context.repo.repo}`; } } + 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 commentOnDiscussion(github, owner, repo, discussionNumber, message, replyToId) { const { repository } = await github.graphql( ` @@ -1180,6 +1218,10 @@ jobs: async function main() { const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const isDiscussionExplicit = process.env.GITHUB_AW_COMMENT_DISCUSSION === "true"; + const temporaryIdMap = loadTemporaryIdMap(); + if (temporaryIdMap.size > 0) { + core.info(`Loaded temporary ID map with ${temporaryIdMap.size} entries`); + } const result = loadAgentOutput(); if (!result.success) { return; @@ -1317,7 +1359,7 @@ jobs: core.info("Could not determine issue, pull request, or discussion number"); continue; } - let body = commentItem.body.trim(); + let body = replaceTemporaryIdReferences(commentItem.body.trim(), temporaryIdMap); const createdIssueUrl = process.env.GH_AW_CREATED_ISSUE_URL; const createdIssueNumber = process.env.GH_AW_CREATED_ISSUE_NUMBER; const createdDiscussionUrl = process.env.GH_AW_CREATED_DISCUSSION_URL; diff --git a/.github/workflows/brave.lock.yml b/.github/workflows/brave.lock.yml index c82f6d61722..b70591ebd0e 100644 --- a/.github/workflows/brave.lock.yml +++ b/.github/workflows/brave.lock.yml @@ -1025,6 +1025,44 @@ jobs: return `${githubServer}/${context.repo.owner}/${context.repo.repo}`; } } + 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 commentOnDiscussion(github, owner, repo, discussionNumber, message, replyToId) { const { repository } = await github.graphql( ` @@ -1085,6 +1123,10 @@ jobs: async function main() { const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const isDiscussionExplicit = process.env.GITHUB_AW_COMMENT_DISCUSSION === "true"; + const temporaryIdMap = loadTemporaryIdMap(); + if (temporaryIdMap.size > 0) { + core.info(`Loaded temporary ID map with ${temporaryIdMap.size} entries`); + } const result = loadAgentOutput(); if (!result.success) { return; @@ -1222,7 +1264,7 @@ jobs: core.info("Could not determine issue, pull request, or discussion number"); continue; } - let body = commentItem.body.trim(); + let body = replaceTemporaryIdReferences(commentItem.body.trim(), temporaryIdMap); const createdIssueUrl = process.env.GH_AW_CREATED_ISSUE_URL; const createdIssueNumber = process.env.GH_AW_CREATED_ISSUE_NUMBER; const createdDiscussionUrl = process.env.GH_AW_CREATED_DISCUSSION_URL; diff --git a/.github/workflows/ci-doctor.lock.yml b/.github/workflows/ci-doctor.lock.yml index 89a8b5e9a25..2d6ebbdf7ec 100644 --- a/.github/workflows/ci-doctor.lock.yml +++ b/.github/workflows/ci-doctor.lock.yml @@ -394,6 +394,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: "CI Failure Doctor" GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/ci-doctor.md@ea350161ad5dcc9624cf510f134c6a9e39a6f94d" GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/tree/ea350161ad5dcc9624cf510f134c6a9e39a6f94d/workflows/ci-doctor.md" @@ -477,6 +478,44 @@ jobs: return `${githubServer}/${context.repo.owner}/${context.repo.repo}`; } } + 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 commentOnDiscussion(github, owner, repo, discussionNumber, message, replyToId) { const { repository } = await github.graphql( ` @@ -537,6 +576,10 @@ jobs: async function main() { const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const isDiscussionExplicit = process.env.GITHUB_AW_COMMENT_DISCUSSION === "true"; + const temporaryIdMap = loadTemporaryIdMap(); + if (temporaryIdMap.size > 0) { + core.info(`Loaded temporary ID map with ${temporaryIdMap.size} entries`); + } const result = loadAgentOutput(); if (!result.success) { return; @@ -674,7 +717,7 @@ jobs: core.info("Could not determine issue, pull request, or discussion number"); continue; } - let body = commentItem.body.trim(); + let body = replaceTemporaryIdReferences(commentItem.body.trim(), temporaryIdMap); const createdIssueUrl = process.env.GH_AW_CREATED_ISSUE_URL; const createdIssueNumber = process.env.GH_AW_CREATED_ISSUE_NUMBER; const createdDiscussionUrl = process.env.GH_AW_CREATED_DISCUSSION_URL; @@ -892,7 +935,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"); @@ -4796,6 +4839,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 @@ -4916,9 +4960,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) { @@ -4938,18 +5021,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 = @@ -4965,12 +5055,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}`); @@ -4988,7 +5101,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"; } @@ -5040,6 +5154,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}`); @@ -5131,6 +5247,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/cli-consistency-checker.lock.yml b/.github/workflows/cli-consistency-checker.lock.yml index 9daa609695f..471a6f0172c 100644 --- a/.github/workflows/cli-consistency-checker.lock.yml +++ b/.github/workflows/cli-consistency-checker.lock.yml @@ -457,7 +457,7 @@ jobs: {"create_issue":{"max":5},"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":"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":"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"); @@ -4728,6 +4728,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 @@ -4847,9 +4848,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) { @@ -4869,18 +4909,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 = @@ -4896,12 +4943,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}`); @@ -4919,7 +4989,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"; } @@ -4971,6 +5042,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}`); @@ -5062,6 +5135,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/cli-version-checker.lock.yml b/.github/workflows/cli-version-checker.lock.yml index 3bf51e0b1ad..ba764d473d3 100644 --- a/.github/workflows/cli-version-checker.lock.yml +++ b/.github/workflows/cli-version-checker.lock.yml @@ -622,7 +622,7 @@ jobs: {"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":"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":"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"); @@ -5057,6 +5057,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 @@ -5176,9 +5177,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) { @@ -5198,18 +5238,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 = @@ -5225,12 +5272,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}`); @@ -5248,7 +5318,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"; } @@ -5300,6 +5371,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}`); @@ -5391,6 +5464,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/cloclo.lock.yml b/.github/workflows/cloclo.lock.yml index 873dd75e819..9ad2f95edbf 100644 --- a/.github/workflows/cloclo.lock.yml +++ b/.github/workflows/cloclo.lock.yml @@ -1219,6 +1219,44 @@ jobs: return `${githubServer}/${context.repo.owner}/${context.repo.repo}`; } } + 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 commentOnDiscussion(github, owner, repo, discussionNumber, message, replyToId) { const { repository } = await github.graphql( ` @@ -1279,6 +1317,10 @@ jobs: async function main() { const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const isDiscussionExplicit = process.env.GITHUB_AW_COMMENT_DISCUSSION === "true"; + const temporaryIdMap = loadTemporaryIdMap(); + if (temporaryIdMap.size > 0) { + core.info(`Loaded temporary ID map with ${temporaryIdMap.size} entries`); + } const result = loadAgentOutput(); if (!result.success) { return; @@ -1416,7 +1458,7 @@ jobs: core.info("Could not determine issue, pull request, or discussion number"); continue; } - let body = commentItem.body.trim(); + let body = replaceTemporaryIdReferences(commentItem.body.trim(), temporaryIdMap); const createdIssueUrl = process.env.GH_AW_CREATED_ISSUE_URL; const createdIssueNumber = process.env.GH_AW_CREATED_ISSUE_NUMBER; const createdDiscussionUrl = process.env.GH_AW_CREATED_DISCUSSION_URL; diff --git a/.github/workflows/craft.lock.yml b/.github/workflows/craft.lock.yml index 01ddcb708af..11379387b98 100644 --- a/.github/workflows/craft.lock.yml +++ b/.github/workflows/craft.lock.yml @@ -1174,6 +1174,44 @@ jobs: return `${githubServer}/${context.repo.owner}/${context.repo.repo}`; } } + 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 commentOnDiscussion(github, owner, repo, discussionNumber, message, replyToId) { const { repository } = await github.graphql( ` @@ -1234,6 +1272,10 @@ jobs: async function main() { const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const isDiscussionExplicit = process.env.GITHUB_AW_COMMENT_DISCUSSION === "true"; + const temporaryIdMap = loadTemporaryIdMap(); + if (temporaryIdMap.size > 0) { + core.info(`Loaded temporary ID map with ${temporaryIdMap.size} entries`); + } const result = loadAgentOutput(); if (!result.success) { return; @@ -1371,7 +1413,7 @@ jobs: core.info("Could not determine issue, pull request, or discussion number"); continue; } - let body = commentItem.body.trim(); + let body = replaceTemporaryIdReferences(commentItem.body.trim(), temporaryIdMap); const createdIssueUrl = process.env.GH_AW_CREATED_ISSUE_URL; const createdIssueNumber = process.env.GH_AW_CREATED_ISSUE_NUMBER; const createdDiscussionUrl = process.env.GH_AW_CREATED_DISCUSSION_URL; diff --git a/.github/workflows/daily-file-diet.lock.yml b/.github/workflows/daily-file-diet.lock.yml index 7f07413f05f..98a4ec042a1 100644 --- a/.github/workflows/daily-file-diet.lock.yml +++ b/.github/workflows/daily-file-diet.lock.yml @@ -612,7 +612,7 @@ jobs: {"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":"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":"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"); @@ -4066,6 +4066,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: Generate GitHub App token id: app-token @@ -4197,9 +4198,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) { @@ -4219,18 +4259,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 = @@ -4246,12 +4293,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}`); @@ -4269,7 +4339,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"; } @@ -4321,6 +4392,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}`); @@ -4412,6 +4485,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/daily-multi-device-docs-tester.lock.yml b/.github/workflows/daily-multi-device-docs-tester.lock.yml index 0d34894d21e..9c2562a7f6c 100644 --- a/.github/workflows/daily-multi-device-docs-tester.lock.yml +++ b/.github/workflows/daily-multi-device-docs-tester.lock.yml @@ -491,7 +491,7 @@ jobs: {"create_issue":{"max":1},"missing_tool":{"max":0},"noop":{"max":1},"upload_asset":{"max":0}} 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":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"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":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"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"); @@ -4007,6 +4007,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 @@ -4125,9 +4126,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) { @@ -4147,18 +4187,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 = @@ -4174,12 +4221,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}`); @@ -4197,7 +4267,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"; } @@ -4249,6 +4320,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}`); @@ -4340,6 +4413,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/dependabot-go-checker.lock.yml b/.github/workflows/dependabot-go-checker.lock.yml index f11d3d7506f..6962fa82711 100644 --- a/.github/workflows/dependabot-go-checker.lock.yml +++ b/.github/workflows/dependabot-go-checker.lock.yml @@ -704,7 +704,7 @@ jobs: {"close_issue":{"max":20,"required_title_prefix":"[deps]"},"create_issue":{"max":10},"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":"Close a GitHub issue with a comment","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body to add when closing the issue","type":"string"},"issue_number":{"description":"Optional issue number (uses triggering issue if not provided)","type":["number","string"]}},"required":["body"],"type":"object"},"name":"close_issue"},{"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":"Close a GitHub issue with a comment","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body to add when closing the issue","type":"string"},"issue_number":{"description":"Optional issue number (uses triggering issue if not provided)","type":["number","string"]}},"required":["body"],"type":"object"},"name":"close_issue"},{"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"); @@ -5113,6 +5113,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 @@ -5232,9 +5233,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) { @@ -5254,18 +5294,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 = @@ -5281,12 +5328,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}`); @@ -5304,7 +5374,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"; } @@ -5356,6 +5427,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}`); @@ -5447,6 +5520,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/dev-hawk.lock.yml b/.github/workflows/dev-hawk.lock.yml index b016d8cccd2..055d0e60dd9 100644 --- a/.github/workflows/dev-hawk.lock.yml +++ b/.github/workflows/dev-hawk.lock.yml @@ -413,6 +413,44 @@ jobs: return `${githubServer}/${context.repo.owner}/${context.repo.repo}`; } } + 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 commentOnDiscussion(github, owner, repo, discussionNumber, message, replyToId) { const { repository } = await github.graphql( ` @@ -473,6 +511,10 @@ jobs: async function main() { const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const isDiscussionExplicit = process.env.GITHUB_AW_COMMENT_DISCUSSION === "true"; + const temporaryIdMap = loadTemporaryIdMap(); + if (temporaryIdMap.size > 0) { + core.info(`Loaded temporary ID map with ${temporaryIdMap.size} entries`); + } const result = loadAgentOutput(); if (!result.success) { return; @@ -610,7 +652,7 @@ jobs: core.info("Could not determine issue, pull request, or discussion number"); continue; } - let body = commentItem.body.trim(); + let body = replaceTemporaryIdReferences(commentItem.body.trim(), temporaryIdMap); const createdIssueUrl = process.env.GH_AW_CREATED_ISSUE_URL; const createdIssueNumber = process.env.GH_AW_CREATED_ISSUE_NUMBER; const createdDiscussionUrl = process.env.GH_AW_CREATED_DISCUSSION_URL; diff --git a/.github/workflows/duplicate-code-detector.lock.yml b/.github/workflows/duplicate-code-detector.lock.yml index 517d924c1b3..658fae7b733 100644 --- a/.github/workflows/duplicate-code-detector.lock.yml +++ b/.github/workflows/duplicate-code-detector.lock.yml @@ -516,7 +516,7 @@ jobs: {"create_issue":{"max":3},"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":"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":"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"); @@ -3841,6 +3841,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 @@ -3960,9 +3961,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) { @@ -3982,18 +4022,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 = @@ -4009,12 +4056,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}`); @@ -4032,7 +4102,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"; } @@ -4084,6 +4155,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}`); @@ -4175,6 +4248,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/go-pattern-detector.lock.yml b/.github/workflows/go-pattern-detector.lock.yml index 98ae8832c2d..33e94434490 100644 --- a/.github/workflows/go-pattern-detector.lock.yml +++ b/.github/workflows/go-pattern-detector.lock.yml @@ -533,7 +533,7 @@ jobs: {"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":"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":"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"); @@ -4009,6 +4009,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 @@ -4128,9 +4129,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) { @@ -4150,18 +4190,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 = @@ -4177,12 +4224,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}`); @@ -4200,7 +4270,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"; } @@ -4252,6 +4323,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}`); @@ -4343,6 +4416,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/grumpy-reviewer.lock.yml b/.github/workflows/grumpy-reviewer.lock.yml index 118aa33d36f..9b65deecfae 100644 --- a/.github/workflows/grumpy-reviewer.lock.yml +++ b/.github/workflows/grumpy-reviewer.lock.yml @@ -1055,6 +1055,44 @@ jobs: return `${githubServer}/${context.repo.owner}/${context.repo.repo}`; } } + 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 commentOnDiscussion(github, owner, repo, discussionNumber, message, replyToId) { const { repository } = await github.graphql( ` @@ -1115,6 +1153,10 @@ jobs: async function main() { const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const isDiscussionExplicit = process.env.GITHUB_AW_COMMENT_DISCUSSION === "true"; + const temporaryIdMap = loadTemporaryIdMap(); + if (temporaryIdMap.size > 0) { + core.info(`Loaded temporary ID map with ${temporaryIdMap.size} entries`); + } const result = loadAgentOutput(); if (!result.success) { return; @@ -1252,7 +1294,7 @@ jobs: core.info("Could not determine issue, pull request, or discussion number"); continue; } - let body = commentItem.body.trim(); + let body = replaceTemporaryIdReferences(commentItem.body.trim(), temporaryIdMap); const createdIssueUrl = process.env.GH_AW_CREATED_ISSUE_URL; const createdIssueNumber = process.env.GH_AW_CREATED_ISSUE_NUMBER; const createdDiscussionUrl = process.env.GH_AW_CREATED_DISCUSSION_URL; diff --git a/.github/workflows/issue-monster.lock.yml b/.github/workflows/issue-monster.lock.yml index 7febe2f8798..75e20ee9964 100644 --- a/.github/workflows/issue-monster.lock.yml +++ b/.github/workflows/issue-monster.lock.yml @@ -411,6 +411,44 @@ jobs: return `${githubServer}/${context.repo.owner}/${context.repo.repo}`; } } + 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 commentOnDiscussion(github, owner, repo, discussionNumber, message, replyToId) { const { repository } = await github.graphql( ` @@ -471,6 +509,10 @@ jobs: async function main() { const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const isDiscussionExplicit = process.env.GITHUB_AW_COMMENT_DISCUSSION === "true"; + const temporaryIdMap = loadTemporaryIdMap(); + if (temporaryIdMap.size > 0) { + core.info(`Loaded temporary ID map with ${temporaryIdMap.size} entries`); + } const result = loadAgentOutput(); if (!result.success) { return; @@ -608,7 +650,7 @@ jobs: core.info("Could not determine issue, pull request, or discussion number"); continue; } - let body = commentItem.body.trim(); + let body = replaceTemporaryIdReferences(commentItem.body.trim(), temporaryIdMap); const createdIssueUrl = process.env.GH_AW_CREATED_ISSUE_URL; const createdIssueNumber = process.env.GH_AW_CREATED_ISSUE_NUMBER; const createdDiscussionUrl = process.env.GH_AW_CREATED_DISCUSSION_URL; diff --git a/.github/workflows/pdf-summary.lock.yml b/.github/workflows/pdf-summary.lock.yml index f5dafa44559..bd53c93d5fd 100644 --- a/.github/workflows/pdf-summary.lock.yml +++ b/.github/workflows/pdf-summary.lock.yml @@ -1088,6 +1088,44 @@ jobs: return `${githubServer}/${context.repo.owner}/${context.repo.repo}`; } } + 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 commentOnDiscussion(github, owner, repo, discussionNumber, message, replyToId) { const { repository } = await github.graphql( ` @@ -1148,6 +1186,10 @@ jobs: async function main() { const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const isDiscussionExplicit = process.env.GITHUB_AW_COMMENT_DISCUSSION === "true"; + const temporaryIdMap = loadTemporaryIdMap(); + if (temporaryIdMap.size > 0) { + core.info(`Loaded temporary ID map with ${temporaryIdMap.size} entries`); + } const result = loadAgentOutput(); if (!result.success) { return; @@ -1285,7 +1327,7 @@ jobs: core.info("Could not determine issue, pull request, or discussion number"); continue; } - let body = commentItem.body.trim(); + let body = replaceTemporaryIdReferences(commentItem.body.trim(), temporaryIdMap); const createdIssueUrl = process.env.GH_AW_CREATED_ISSUE_URL; const createdIssueNumber = process.env.GH_AW_CREATED_ISSUE_NUMBER; const createdDiscussionUrl = process.env.GH_AW_CREATED_DISCUSSION_URL; diff --git a/.github/workflows/plan.lock.yml b/.github/workflows/plan.lock.yml index 7c2a127b161..cac30d8ae69 100644 --- a/.github/workflows/plan.lock.yml +++ b/.github/workflows/plan.lock.yml @@ -1027,7 +1027,7 @@ jobs: {"close_discussion":{"max":1,"required_category":"Ideas"},"create_issue":{"max":5},"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":"Close a GitHub discussion with a comment and optional resolution reason","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body to add when closing the discussion","type":"string"},"discussion_number":{"description":"Optional discussion number (uses triggering discussion if not provided)","type":["number","string"]},"reason":{"description":"Optional resolution reason","enum":["RESOLVED","DUPLICATE","OUTDATED","ANSWERED"],"type":"string"}},"required":["body"],"type":"object"},"name":"close_discussion"},{"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":"Close a GitHub discussion with a comment and optional resolution reason","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body to add when closing the discussion","type":"string"},"discussion_number":{"description":"Optional discussion number (uses triggering discussion if not provided)","type":["number","string"]},"reason":{"description":"Optional resolution reason","enum":["RESOLVED","DUPLICATE","OUTDATED","ANSWERED"],"type":"string"}},"required":["body"],"type":"object"},"name":"close_discussion"},{"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"); @@ -5210,6 +5210,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 @@ -5329,9 +5330,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) { @@ -5351,18 +5391,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 = @@ -5378,12 +5425,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}`); @@ -5401,7 +5471,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"; } @@ -5453,6 +5524,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}`); @@ -5544,6 +5617,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/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml index ec89f0969e3..05fa0be77e4 100644 --- a/.github/workflows/poem-bot.lock.yml +++ b/.github/workflows/poem-bot.lock.yml @@ -926,6 +926,7 @@ jobs: GH_AW_COMMENT_TARGET: "*" 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_CREATED_PULL_REQUEST_URL: ${{ needs.create_pull_request.outputs.pull_request_url }} GH_AW_CREATED_PULL_REQUEST_NUMBER: ${{ needs.create_pull_request.outputs.pull_request_number }} GH_AW_WORKFLOW_NAME: "Poem Bot - A Creative Agentic Workflow" @@ -1010,6 +1011,44 @@ jobs: return `${githubServer}/${context.repo.owner}/${context.repo.repo}`; } } + 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 commentOnDiscussion(github, owner, repo, discussionNumber, message, replyToId) { const { repository } = await github.graphql( ` @@ -1070,6 +1109,10 @@ jobs: async function main() { const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const isDiscussionExplicit = process.env.GITHUB_AW_COMMENT_DISCUSSION === "true"; + const temporaryIdMap = loadTemporaryIdMap(); + if (temporaryIdMap.size > 0) { + core.info(`Loaded temporary ID map with ${temporaryIdMap.size} entries`); + } const result = loadAgentOutput(); if (!result.success) { return; @@ -1207,7 +1250,7 @@ jobs: core.info("Could not determine issue, pull request, or discussion number"); continue; } - let body = commentItem.body.trim(); + let body = replaceTemporaryIdReferences(commentItem.body.trim(), temporaryIdMap); const createdIssueUrl = process.env.GH_AW_CREATED_ISSUE_URL; const createdIssueNumber = process.env.GH_AW_CREATED_ISSUE_NUMBER; const createdDiscussionUrl = process.env.GH_AW_CREATED_DISCUSSION_URL; @@ -1862,7 +1905,7 @@ jobs: {"add_comment":{"max":3,"target":"*"},"add_labels":{"allowed":["poetry","creative","automation","ai-generated","epic","haiku","sonnet","limerick"],"max":5},"create_issue":{"max":2},"create_pull_request":{},"create_pull_request_review_comment":{"max":2},"missing_tool":{"max":0},"noop":{"max":1},"push_to_pull_request_branch":{"max":0},"update_issue":{"max":2},"upload_asset":{"max":0}} 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":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Create a review comment on a GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body content","type":"string"},"line":{"description":"Line number for the comment","type":["number","string"]},"path":{"description":"File path for the review comment","type":"string"},"side":{"description":"Optional side of the diff: LEFT or RIGHT","enum":["LEFT","RIGHT"],"type":"string"},"start_line":{"description":"Optional start line for multi-line comments","type":["number","string"]}},"required":["path","line","body"],"type":"object"},"name":"create_pull_request_review_comment"},{"description":"Add labels to a GitHub issue or pull request","inputSchema":{"additionalProperties":false,"properties":{"item_number":{"description":"Issue or PR number (optional for current context)","type":"number"},"labels":{"description":"Labels to add","items":{"type":"string"},"type":"array"}},"required":["labels"],"type":"object"},"name":"add_labels"},{"description":"Update a GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Optional new issue body","type":"string"},"issue_number":{"description":"Optional issue number for target '*'","type":["number","string"]},"status":{"description":"Optional new issue status","enum":["open","closed"],"type":"string"},"title":{"description":"Optional new issue title","type":"string"}},"type":"object"},"name":"update_issue"},{"description":"Push changes to a pull request branch","inputSchema":{"additionalProperties":false,"properties":{"branch":{"description":"Optional branch name. Do not provide this parameter if you want to push changes from the current branch. If not provided, the current branch will be used.","type":"string"},"message":{"description":"Commit message","type":"string"},"pull_request_number":{"description":"Optional pull request number for target '*'","type":["number","string"]}},"required":["message"],"type":"object"},"name":"push_to_pull_request_branch"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"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":"Create a new GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Pull request body/description","type":"string"},"branch":{"description":"Optional branch name. If not provided, the current branch will be used.","type":"string"},"labels":{"description":"Optional labels to add to the PR","items":{"type":"string"},"type":"array"},"title":{"description":"Pull request title","type":"string"}},"required":["title","body"],"type":"object"},"name":"create_pull_request"},{"description":"Create a review comment on a GitHub pull request","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body content","type":"string"},"line":{"description":"Line number for the comment","type":["number","string"]},"path":{"description":"File path for the review comment","type":"string"},"side":{"description":"Optional side of the diff: LEFT or RIGHT","enum":["LEFT","RIGHT"],"type":"string"},"start_line":{"description":"Optional start line for multi-line comments","type":["number","string"]}},"required":["path","line","body"],"type":"object"},"name":"create_pull_request_review_comment"},{"description":"Add labels to a GitHub issue or pull request","inputSchema":{"additionalProperties":false,"properties":{"item_number":{"description":"Issue or PR number (optional for current context)","type":"number"},"labels":{"description":"Labels to add","items":{"type":"string"},"type":"array"}},"required":["labels"],"type":"object"},"name":"add_labels"},{"description":"Update a GitHub issue","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Optional new issue body","type":"string"},"issue_number":{"description":"Optional issue number for target '*'","type":["number","string"]},"status":{"description":"Optional new issue status","enum":["open","closed"],"type":"string"},"title":{"description":"Optional new issue title","type":"string"}},"type":"object"},"name":"update_issue"},{"description":"Push changes to a pull request branch","inputSchema":{"additionalProperties":false,"properties":{"branch":{"description":"Optional branch name. Do not provide this parameter if you want to push changes from the current branch. If not provided, the current branch will be used.","type":"string"},"message":{"description":"Commit message","type":"string"},"pull_request_number":{"description":"Optional pull request number for target '*'","type":["number","string"]}},"required":["message"],"type":"object"},"name":"push_to_pull_request_branch"},{"description":"Publish a file as a URL-addressable asset to an orphaned git branch","inputSchema":{"additionalProperties":false,"properties":{"path":{"description":"Path to the file to publish as an asset. Must be a file under the current workspace or /tmp directory. By default, images (.png, .jpg, .jpeg) are allowed, but can be configured via workflow settings.","type":"string"}},"required":["path"],"type":"object"},"name":"upload_asset"},{"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"); @@ -5767,6 +5810,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 @@ -5887,9 +5931,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) { @@ -5909,18 +5992,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 = @@ -5936,12 +6026,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}`); @@ -5959,7 +6072,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"; } @@ -6011,6 +6125,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}`); @@ -6102,6 +6218,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/pr-nitpick-reviewer.lock.yml b/.github/workflows/pr-nitpick-reviewer.lock.yml index 14dd8adffe9..9c4785f609a 100644 --- a/.github/workflows/pr-nitpick-reviewer.lock.yml +++ b/.github/workflows/pr-nitpick-reviewer.lock.yml @@ -1112,6 +1112,44 @@ jobs: return `${githubServer}/${context.repo.owner}/${context.repo.repo}`; } } + 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 commentOnDiscussion(github, owner, repo, discussionNumber, message, replyToId) { const { repository } = await github.graphql( ` @@ -1172,6 +1210,10 @@ jobs: async function main() { const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const isDiscussionExplicit = process.env.GITHUB_AW_COMMENT_DISCUSSION === "true"; + const temporaryIdMap = loadTemporaryIdMap(); + if (temporaryIdMap.size > 0) { + core.info(`Loaded temporary ID map with ${temporaryIdMap.size} entries`); + } const result = loadAgentOutput(); if (!result.success) { return; @@ -1309,7 +1351,7 @@ jobs: core.info("Could not determine issue, pull request, or discussion number"); continue; } - let body = commentItem.body.trim(); + let body = replaceTemporaryIdReferences(commentItem.body.trim(), temporaryIdMap); const createdIssueUrl = process.env.GH_AW_CREATED_ISSUE_URL; const createdIssueNumber = process.env.GH_AW_CREATED_ISSUE_NUMBER; const createdDiscussionUrl = process.env.GH_AW_CREATED_DISCUSSION_URL; diff --git a/.github/workflows/q.lock.yml b/.github/workflows/q.lock.yml index d595b9048f9..ce691f142f5 100644 --- a/.github/workflows/q.lock.yml +++ b/.github/workflows/q.lock.yml @@ -1332,6 +1332,44 @@ jobs: return `${githubServer}/${context.repo.owner}/${context.repo.repo}`; } } + 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 commentOnDiscussion(github, owner, repo, discussionNumber, message, replyToId) { const { repository } = await github.graphql( ` @@ -1392,6 +1430,10 @@ jobs: async function main() { const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const isDiscussionExplicit = process.env.GITHUB_AW_COMMENT_DISCUSSION === "true"; + const temporaryIdMap = loadTemporaryIdMap(); + if (temporaryIdMap.size > 0) { + core.info(`Loaded temporary ID map with ${temporaryIdMap.size} entries`); + } const result = loadAgentOutput(); if (!result.success) { return; @@ -1529,7 +1571,7 @@ jobs: core.info("Could not determine issue, pull request, or discussion number"); continue; } - let body = commentItem.body.trim(); + let body = replaceTemporaryIdReferences(commentItem.body.trim(), temporaryIdMap); const createdIssueUrl = process.env.GH_AW_CREATED_ISSUE_URL; const createdIssueNumber = process.env.GH_AW_CREATED_ISSUE_NUMBER; const createdDiscussionUrl = process.env.GH_AW_CREATED_DISCUSSION_URL; diff --git a/.github/workflows/scout.lock.yml b/.github/workflows/scout.lock.yml index c8343841876..650b43c863b 100644 --- a/.github/workflows/scout.lock.yml +++ b/.github/workflows/scout.lock.yml @@ -1293,6 +1293,44 @@ jobs: return `${githubServer}/${context.repo.owner}/${context.repo.repo}`; } } + 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 commentOnDiscussion(github, owner, repo, discussionNumber, message, replyToId) { const { repository } = await github.graphql( ` @@ -1353,6 +1391,10 @@ jobs: async function main() { const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const isDiscussionExplicit = process.env.GITHUB_AW_COMMENT_DISCUSSION === "true"; + const temporaryIdMap = loadTemporaryIdMap(); + if (temporaryIdMap.size > 0) { + core.info(`Loaded temporary ID map with ${temporaryIdMap.size} entries`); + } const result = loadAgentOutput(); if (!result.success) { return; @@ -1490,7 +1532,7 @@ jobs: core.info("Could not determine issue, pull request, or discussion number"); continue; } - let body = commentItem.body.trim(); + let body = replaceTemporaryIdReferences(commentItem.body.trim(), temporaryIdMap); const createdIssueUrl = process.env.GH_AW_CREATED_ISSUE_URL; const createdIssueNumber = process.env.GH_AW_CREATED_ISSUE_NUMBER; const createdDiscussionUrl = process.env.GH_AW_CREATED_DISCUSSION_URL; diff --git a/.github/workflows/semantic-function-refactor.lock.yml b/.github/workflows/semantic-function-refactor.lock.yml index d758c716e92..d61a2655c13 100644 --- a/.github/workflows/semantic-function-refactor.lock.yml +++ b/.github/workflows/semantic-function-refactor.lock.yml @@ -901,7 +901,7 @@ jobs: {"close_issue":{"max":10,"required_title_prefix":"[refactor] "},"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":"Close a GitHub issue with a comment","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body to add when closing the issue","type":"string"},"issue_number":{"description":"Optional issue number (uses triggering issue if not provided)","type":["number","string"]}},"required":["body"],"type":"object"},"name":"close_issue"},{"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":"Close a GitHub issue with a comment","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Comment body to add when closing the issue","type":"string"},"issue_number":{"description":"Optional issue number (uses triggering issue if not provided)","type":["number","string"]}},"required":["body"],"type":"object"},"name":"close_issue"},{"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"); @@ -5070,6 +5070,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 @@ -5189,9 +5190,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) { @@ -5211,18 +5251,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 = @@ -5238,12 +5285,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}`); @@ -5261,7 +5331,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"; } @@ -5313,6 +5384,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}`); @@ -5404,6 +5477,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-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index 9757d90dcd4..e798c05b03d 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -446,6 +446,44 @@ jobs: return `${githubServer}/${context.repo.owner}/${context.repo.repo}`; } } + 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 commentOnDiscussion(github, owner, repo, discussionNumber, message, replyToId) { const { repository } = await github.graphql( ` @@ -506,6 +544,10 @@ jobs: async function main() { const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const isDiscussionExplicit = process.env.GITHUB_AW_COMMENT_DISCUSSION === "true"; + const temporaryIdMap = loadTemporaryIdMap(); + if (temporaryIdMap.size > 0) { + core.info(`Loaded temporary ID map with ${temporaryIdMap.size} entries`); + } const result = loadAgentOutput(); if (!result.success) { return; @@ -643,7 +685,7 @@ jobs: core.info("Could not determine issue, pull request, or discussion number"); continue; } - let body = commentItem.body.trim(); + let body = replaceTemporaryIdReferences(commentItem.body.trim(), temporaryIdMap); const createdIssueUrl = process.env.GH_AW_CREATED_ISSUE_URL; const createdIssueNumber = process.env.GH_AW_CREATED_ISSUE_NUMBER; const createdDiscussionUrl = process.env.GH_AW_CREATED_DISCUSSION_URL; diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index b7c5d9bb39e..a8de6054880 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -331,6 +331,44 @@ jobs: return `${githubServer}/${context.repo.owner}/${context.repo.repo}`; } } + 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 commentOnDiscussion(github, owner, repo, discussionNumber, message, replyToId) { const { repository } = await github.graphql( ` @@ -391,6 +429,10 @@ jobs: async function main() { const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const isDiscussionExplicit = process.env.GITHUB_AW_COMMENT_DISCUSSION === "true"; + const temporaryIdMap = loadTemporaryIdMap(); + if (temporaryIdMap.size > 0) { + core.info(`Loaded temporary ID map with ${temporaryIdMap.size} entries`); + } const result = loadAgentOutput(); if (!result.success) { return; @@ -528,7 +570,7 @@ jobs: core.info("Could not determine issue, pull request, or discussion number"); continue; } - let body = commentItem.body.trim(); + let body = replaceTemporaryIdReferences(commentItem.body.trim(), temporaryIdMap); const createdIssueUrl = process.env.GH_AW_CREATED_ISSUE_URL; const createdIssueNumber = process.env.GH_AW_CREATED_ISSUE_NUMBER; const createdDiscussionUrl = process.env.GH_AW_CREATED_DISCUSSION_URL; diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 40bb8336f2f..a3d17d1c27b 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -331,6 +331,44 @@ jobs: return `${githubServer}/${context.repo.owner}/${context.repo.repo}`; } } + 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 commentOnDiscussion(github, owner, repo, discussionNumber, message, replyToId) { const { repository } = await github.graphql( ` @@ -391,6 +429,10 @@ jobs: async function main() { const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const isDiscussionExplicit = process.env.GITHUB_AW_COMMENT_DISCUSSION === "true"; + const temporaryIdMap = loadTemporaryIdMap(); + if (temporaryIdMap.size > 0) { + core.info(`Loaded temporary ID map with ${temporaryIdMap.size} entries`); + } const result = loadAgentOutput(); if (!result.success) { return; @@ -528,7 +570,7 @@ jobs: core.info("Could not determine issue, pull request, or discussion number"); continue; } - let body = commentItem.body.trim(); + let body = replaceTemporaryIdReferences(commentItem.body.trim(), temporaryIdMap); const createdIssueUrl = process.env.GH_AW_CREATED_ISSUE_URL; const createdIssueNumber = process.env.GH_AW_CREATED_ISSUE_NUMBER; const createdDiscussionUrl = process.env.GH_AW_CREATED_DISCUSSION_URL; diff --git a/.github/workflows/smoke-detector.lock.yml b/.github/workflows/smoke-detector.lock.yml index 7f83f6a71ea..5cd9d04cbd4 100644 --- a/.github/workflows/smoke-detector.lock.yml +++ b/.github/workflows/smoke-detector.lock.yml @@ -913,6 +913,7 @@ jobs: GH_AW_COMMENT_TARGET: "*" 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 Detector - Smoke Test Failure Investigator" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -994,6 +995,44 @@ jobs: return `${githubServer}/${context.repo.owner}/${context.repo.repo}`; } } + 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 commentOnDiscussion(github, owner, repo, discussionNumber, message, replyToId) { const { repository } = await github.graphql( ` @@ -1054,6 +1093,10 @@ jobs: async function main() { const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const isDiscussionExplicit = process.env.GITHUB_AW_COMMENT_DISCUSSION === "true"; + const temporaryIdMap = loadTemporaryIdMap(); + if (temporaryIdMap.size > 0) { + core.info(`Loaded temporary ID map with ${temporaryIdMap.size} entries`); + } const result = loadAgentOutput(); if (!result.success) { return; @@ -1191,7 +1234,7 @@ jobs: core.info("Could not determine issue, pull request, or discussion number"); continue; } - let body = commentItem.body.trim(); + let body = replaceTemporaryIdReferences(commentItem.body.trim(), temporaryIdMap); const createdIssueUrl = process.env.GH_AW_CREATED_ISSUE_URL; const createdIssueNumber = process.env.GH_AW_CREATED_ISSUE_NUMBER; const createdDiscussionUrl = process.env.GH_AW_CREATED_DISCUSSION_URL; @@ -1531,7 +1574,7 @@ jobs: {"add_comment":{"max":1,"target":"*"},"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"); @@ -5225,6 +5268,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 @@ -5344,9 +5388,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) { @@ -5366,18 +5449,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 = @@ -5393,12 +5483,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}`); @@ -5416,7 +5529,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"; } @@ -5468,6 +5582,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}`); @@ -5559,6 +5675,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/super-linter.lock.yml b/.github/workflows/super-linter.lock.yml index a929fbcafde..f098548a186 100644 --- a/.github/workflows/super-linter.lock.yml +++ b/.github/workflows/super-linter.lock.yml @@ -510,7 +510,7 @@ jobs: {"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":"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":"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"); @@ -4450,6 +4450,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 @@ -4569,9 +4570,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) { @@ -4591,18 +4631,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 = @@ -4618,12 +4665,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}`); @@ -4641,7 +4711,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"; } @@ -4693,6 +4764,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}`); @@ -4784,6 +4857,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/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml index 518a6409e09..9759740cfea 100644 --- a/.github/workflows/technical-doc-writer.lock.yml +++ b/.github/workflows/technical-doc-writer.lock.yml @@ -714,6 +714,44 @@ jobs: return `${githubServer}/${context.repo.owner}/${context.repo.repo}`; } } + 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 commentOnDiscussion(github, owner, repo, discussionNumber, message, replyToId) { const { repository } = await github.graphql( ` @@ -774,6 +812,10 @@ jobs: async function main() { const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const isDiscussionExplicit = process.env.GITHUB_AW_COMMENT_DISCUSSION === "true"; + const temporaryIdMap = loadTemporaryIdMap(); + if (temporaryIdMap.size > 0) { + core.info(`Loaded temporary ID map with ${temporaryIdMap.size} entries`); + } const result = loadAgentOutput(); if (!result.success) { return; @@ -911,7 +953,7 @@ jobs: core.info("Could not determine issue, pull request, or discussion number"); continue; } - let body = commentItem.body.trim(); + let body = replaceTemporaryIdReferences(commentItem.body.trim(), temporaryIdMap); const createdIssueUrl = process.env.GH_AW_CREATED_ISSUE_URL; const createdIssueNumber = process.env.GH_AW_CREATED_ISSUE_NUMBER; const createdDiscussionUrl = process.env.GH_AW_CREATED_DISCUSSION_URL; diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index 3749528ef30..46eb7c1208c 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -971,6 +971,44 @@ jobs: return `${githubServer}/${context.repo.owner}/${context.repo.repo}`; } } + 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 commentOnDiscussion(github, owner, repo, discussionNumber, message, replyToId) { const { repository } = await github.graphql( ` @@ -1031,6 +1069,10 @@ jobs: async function main() { const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const isDiscussionExplicit = process.env.GITHUB_AW_COMMENT_DISCUSSION === "true"; + const temporaryIdMap = loadTemporaryIdMap(); + if (temporaryIdMap.size > 0) { + core.info(`Loaded temporary ID map with ${temporaryIdMap.size} entries`); + } const result = loadAgentOutput(); if (!result.success) { return; @@ -1168,7 +1210,7 @@ jobs: core.info("Could not determine issue, pull request, or discussion number"); continue; } - let body = commentItem.body.trim(); + let body = replaceTemporaryIdReferences(commentItem.body.trim(), temporaryIdMap); const createdIssueUrl = process.env.GH_AW_CREATED_ISSUE_URL; const createdIssueNumber = process.env.GH_AW_CREATED_ISSUE_NUMBER; const createdDiscussionUrl = process.env.GH_AW_CREATED_DISCUSSION_URL; diff --git a/.github/workflows/video-analyzer.lock.yml b/.github/workflows/video-analyzer.lock.yml index 4a82990e204..15e645e7cfe 100644 --- a/.github/workflows/video-analyzer.lock.yml +++ b/.github/workflows/video-analyzer.lock.yml @@ -556,7 +556,7 @@ jobs: {"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":"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":"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"); @@ -4536,6 +4536,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 @@ -4655,9 +4656,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) { @@ -4677,18 +4717,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 = @@ -4704,12 +4751,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}`); @@ -4727,7 +4797,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"; } @@ -4779,6 +4850,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}`); @@ -4870,6 +4943,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/docs/src/content/docs/labs.mdx b/docs/src/content/docs/labs.mdx index 2b7b9ef1d8d..cd162c3a2ed 100644 --- a/docs/src/content/docs/labs.mdx +++ b/docs/src/content/docs/labs.mdx @@ -52,10 +52,9 @@ These are experimental agentic workflows used by the GitHub Next team to learn, | [Grumpy Code Reviewer 🔥](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/grumpy-reviewer.md) | copilot | [![Grumpy Code Reviewer 🔥](https://github.com/githubnext/gh-aw/actions/workflows/grumpy-reviewer.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/grumpy-reviewer.lock.yml) | - | `/grumpy` | | [Instructions Janitor](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/instructions-janitor.md) | claude | [![Instructions Janitor](https://github.com/githubnext/gh-aw/actions/workflows/instructions-janitor.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/instructions-janitor.lock.yml) | `0 9 * * *` | - | | [Issue Classifier](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/issue-classifier.md) | copilot | [![Issue Classifier](https://github.com/githubnext/gh-aw/actions/workflows/issue-classifier.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/issue-classifier.lock.yml) | - | - | -| [Issue Monster](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/issue-monster.md) | copilot | [![Issue Monster](https://github.com/githubnext/gh-aw/actions/workflows/issue-monster.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/issue-monster.lock.yml) | `0 * * * *` | - | +| [Issue Monster](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/issue-monster.md) | copilot | [![Issue Monster](https://github.com/githubnext/gh-aw/actions/workflows/issue-monster.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/issue-monster.lock.yml) | - | - | | [Issue Summary to Notion](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/notion-issue-summary.md) | copilot | [![Issue Summary to Notion](https://github.com/githubnext/gh-aw/actions/workflows/notion-issue-summary.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/notion-issue-summary.lock.yml) | - | - | | [Lockfile Statistics Analysis Agent](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/lockfile-stats.md) | claude | [![Lockfile Statistics Analysis Agent](https://github.com/githubnext/gh-aw/actions/workflows/lockfile-stats.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/lockfile-stats.lock.yml) | `0 3 * * *` | - | -| [Main Workflow](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/test-xml-import.md) | copilot | [![Main Workflow](https://github.com/githubnext/gh-aw/actions/workflows/test-xml-import.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/test-xml-import.lock.yml) | - | - | | [MCP Inspector Agent](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/mcp-inspector.md) | copilot | [![MCP Inspector Agent](https://github.com/githubnext/gh-aw/actions/workflows/mcp-inspector.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/mcp-inspector.lock.yml) | `0 18 * * 1` | - | | [Mergefest](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/mergefest.md) | copilot | [![Mergefest](https://github.com/githubnext/gh-aw/actions/workflows/mergefest.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/mergefest.lock.yml) | - | `/mergefest` | | [Multi-Device Docs Tester](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/daily-multi-device-docs-tester.md) | claude | [![Multi-Device Docs Tester](https://github.com/githubnext/gh-aw/actions/workflows/daily-multi-device-docs-tester.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/daily-multi-device-docs-tester.lock.yml) | `0 9 * * *` | - | diff --git a/pkg/workflow/add_comment.go b/pkg/workflow/add_comment.go index d6011a21018..ced2b445066 100644 --- a/pkg/workflow/add_comment.go +++ b/pkg/workflow/add_comment.go @@ -53,6 +53,7 @@ func (c *Compiler) buildCreateOutputAddCommentJob(data *WorkflowData, mainJobNam if createIssueJobName != "" { customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_CREATED_ISSUE_URL: ${{ needs.%s.outputs.issue_url }}\n", createIssueJobName)) customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_CREATED_ISSUE_NUMBER: ${{ needs.%s.outputs.issue_number }}\n", createIssueJobName)) + customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_TEMPORARY_ID_MAP: ${{ needs.%s.outputs.temporary_id_map }}\n", createIssueJobName)) } if createDiscussionJobName != "" { customEnvVars = append(customEnvVars, fmt.Sprintf(" GH_AW_CREATED_DISCUSSION_URL: ${{ needs.%s.outputs.discussion_url }}\n", createDiscussionJobName)) diff --git a/pkg/workflow/create_issue.go b/pkg/workflow/create_issue.go index f39f5992ce1..55a31b00261 100644 --- a/pkg/workflow/create_issue.go +++ b/pkg/workflow/create_issue.go @@ -102,8 +102,9 @@ func (c *Compiler) buildCreateOutputIssueJob(data *WorkflowData, mainJobName str // Create outputs for the job outputs := map[string]string{ - "issue_number": "${{ steps.create_issue.outputs.issue_number }}", - "issue_url": "${{ steps.create_issue.outputs.issue_url }}", + "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 }}", } // Use the shared builder function to create the job diff --git a/pkg/workflow/js.go b/pkg/workflow/js.go index c939c4266c3..c3488c34b82 100644 --- a/pkg/workflow/js.go +++ b/pkg/workflow/js.go @@ -197,6 +197,9 @@ var getBaseBranchScript string //go:embed js/generate_git_patch.cjs var generateGitPatchJSScript string +//go:embed js/temporary_id.cjs +var temporaryIdScript string + // GetJavaScriptSources returns a map of all embedded JavaScript sources // The keys are the relative paths from the js directory func GetJavaScriptSources() map[string]string { @@ -208,6 +211,7 @@ func GetJavaScriptSources() map[string]string { "staged_preview.cjs": stagedPreviewScript, "safe_output_helpers.cjs": safeOutputHelpersScript, "safe_output_validator.cjs": safeOutputValidatorScript, + "temporary_id.cjs": temporaryIdScript, "is_truthy.cjs": isTruthyScript, "log_parser_bootstrap.cjs": logParserBootstrapScript, "log_parser_shared.cjs": logParserSharedScript, diff --git a/pkg/workflow/js/add_comment.cjs b/pkg/workflow/js/add_comment.cjs index 100f18d290b..d032a797434 100644 --- a/pkg/workflow/js/add_comment.cjs +++ b/pkg/workflow/js/add_comment.cjs @@ -5,6 +5,7 @@ const { loadAgentOutput } = require("./load_agent_output.cjs"); const { generateFooter } = require("./generate_footer.cjs"); const { getTrackerID } = require("./get_tracker_id.cjs"); const { getRepositoryUrl } = require("./get_repository_url.cjs"); +const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs"); /** * Comment on a GitHub Discussion using GraphQL @@ -88,6 +89,12 @@ async function main() { const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; const isDiscussionExplicit = process.env.GITHUB_AW_COMMENT_DISCUSSION === "true"; + // Load the temporary ID map from create_issue job + const temporaryIdMap = loadTemporaryIdMap(); + if (temporaryIdMap.size > 0) { + core.info(`Loaded temporary ID map with ${temporaryIdMap.size} entries`); + } + const result = loadAgentOutput(); if (!result.success) { return; @@ -257,8 +264,8 @@ async function main() { continue; } - // Extract body from the JSON item - let body = commentItem.body.trim(); + // Extract body from the JSON item and replace temporary ID references + let body = replaceTemporaryIdReferences(commentItem.body.trim(), temporaryIdMap); // Append references to created issues, discussions, and pull requests if they exist const createdIssueUrl = process.env.GH_AW_CREATED_ISSUE_URL; diff --git a/pkg/workflow/js/add_comment.test.cjs b/pkg/workflow/js/add_comment.test.cjs index b88e56da513..862dd95d4d5 100644 --- a/pkg/workflow/js/add_comment.test.cjs +++ b/pkg/workflow/js/add_comment.test.cjs @@ -750,4 +750,107 @@ describe("add_comment.cjs", () => { delete process.env.GITHUB_AW_COMMENT_DISCUSSION; delete global.github.graphql; }); + + it("should replace temporary ID references in comment body using the temporary ID map", async () => { + setAgentOutput({ + items: [ + { + type: "add_comment", + body: "This comment references issue #aw_aabbccdd1122 which was created earlier.", + }, + ], + }); + + // Set up the temporary ID map from the create_issue job + process.env.GH_AW_TEMPORARY_ID_MAP = JSON.stringify({ aw_aabbccdd1122: 456 }); + + mockGithub.rest.issues.createComment.mockResolvedValue({ + data: { + id: 99999, + html_url: "https://github.com/testowner/testrepo/issues/123#issuecomment-99999", + }, + }); + + // Execute the script + await eval(`(async () => { ${createCommentScript} })()`); + + // The comment body should have the temporary ID replaced + expect(mockGithub.rest.issues.createComment).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.stringContaining("#456"), + }) + ); + expect(mockGithub.rest.issues.createComment).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.not.stringContaining("#aw_aabbccdd1122"), + }) + ); + + // Clean up + delete process.env.GH_AW_TEMPORARY_ID_MAP; + }); + + it("should load temporary ID map and log the count", async () => { + setAgentOutput({ + items: [ + { + type: "add_comment", + body: "Test comment", + }, + ], + }); + + // Set up the temporary ID map + process.env.GH_AW_TEMPORARY_ID_MAP = JSON.stringify({ aw_abc123: 100, aw_def456: 200 }); + + mockGithub.rest.issues.createComment.mockResolvedValue({ + data: { + id: 99999, + html_url: "https://github.com/testowner/testrepo/issues/123#issuecomment-99999", + }, + }); + + // Execute the script + await eval(`(async () => { ${createCommentScript} })()`); + + // Should log that the map was loaded with 2 entries + expect(mockCore.info).toHaveBeenCalledWith("Loaded temporary ID map with 2 entries"); + + // Clean up + delete process.env.GH_AW_TEMPORARY_ID_MAP; + }); + + it("should handle empty temporary ID map gracefully", async () => { + setAgentOutput({ + items: [ + { + type: "add_comment", + body: "Comment with #aw_000000000000 that won't be resolved", + }, + ], + }); + + // Empty or missing temporary ID map + process.env.GH_AW_TEMPORARY_ID_MAP = "{}"; + + mockGithub.rest.issues.createComment.mockResolvedValue({ + data: { + id: 99999, + html_url: "https://github.com/testowner/testrepo/issues/123#issuecomment-99999", + }, + }); + + // Execute the script + await eval(`(async () => { ${createCommentScript} })()`); + + // The unresolved reference should remain in the body + expect(mockGithub.rest.issues.createComment).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.stringContaining("#aw_000000000000"), + }) + ); + + // Clean up + delete process.env.GH_AW_TEMPORARY_ID_MAP; + }); }); diff --git a/pkg/workflow/js/create_issue.cjs b/pkg/workflow/js/create_issue.cjs index ff9f3905ece..c636c9cfa24 100644 --- a/pkg/workflow/js/create_issue.cjs +++ b/pkg/workflow/js/create_issue.cjs @@ -6,11 +6,13 @@ const { loadAgentOutput } = require("./load_agent_output.cjs"); const { generateStagedPreview } = require("./staged_preview.cjs"); const { generateFooter } = require("./generate_footer.cjs"); const { getTrackerID } = require("./get_tracker_id.cjs"); +const { generateTemporaryId, isTemporaryId, normalizeTemporaryId, replaceTemporaryIdReferences } = require("./temporary_id.cjs"); async function main() { // Initialize outputs to empty strings to ensure they're always set core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); + core.setOutput("temporary_id_map", "{}"); const isStaged = process.env.GH_AW_SAFE_OUTPUTS_STAGED === "true"; @@ -33,12 +35,18 @@ async function main() { 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; }, }); @@ -46,6 +54,10 @@ async function main() { } const parentIssueNumber = context.payload?.issue?.number; + // Map to track temporary_id -> issue_number relationships + /** @type {Map} */ + const temporaryIdMap = new Map(); + // Extract triggering context for footer generation const triggeringIssueNumber = context.payload?.issue?.number && !context.payload?.issue?.pull_request ? context.payload.issue.number : undefined; @@ -63,16 +75,43 @@ async function main() { const createdIssues = []; for (let i = 0; i < createIssueItems.length; i++) { const createIssueItem = createIssueItems[i]; + + // Get or generate the temporary ID for this issue + 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}` ); // Debug logging for parent field core.info(`Debug: createIssueItem.parent = ${JSON.stringify(createIssueItem.parent)}`); core.info(`Debug: parentIssueNumber from context = ${JSON.stringify(parentIssueNumber)}`); - // Use the parent field from the item if provided, otherwise fall back to context - const effectiveParentIssueNumber = createIssueItem.parent !== undefined ? createIssueItem.parent : parentIssueNumber; + // Resolve parent: check if it's a temporary ID reference + let effectiveParentIssueNumber; + if (createIssueItem.parent !== undefined) { + if (isTemporaryId(createIssueItem.parent)) { + // It's a temporary ID, look it up in the map + 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 { + // It's a real issue number + 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) { @@ -91,7 +130,11 @@ async function main() { .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"); + + // Replace temporary ID references in the body using already-created issues + let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap); + let bodyLines = processedBody.split("\n"); + if (!title) { title = createIssueItem.body || "Agent Output"; } @@ -147,6 +190,10 @@ async function main() { core.info("Created issue #" + issue.number + ": " + issue.html_url); createdIssues.push(issue); + // Store the mapping of temporary_id -> issue_number + temporaryIdMap.set(normalizeTemporaryId(temporaryId), issue.number); + core.info(`Stored temporary ID mapping: ${temporaryId} -> #${issue.number}`); + // Debug logging for sub-issue linking core.info(`Debug: About to check if sub-issue linking is needed. effectiveParentIssueNumber = ${effectiveParentIssueNumber}`); @@ -250,6 +297,12 @@ async function main() { } await core.summary.addRaw(summaryContent).write(); } + + // Output the temporary ID map as JSON for use by downstream jobs + 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/pkg/workflow/js/create_issue.test.cjs b/pkg/workflow/js/create_issue.test.cjs index 761f7512625..95760d7bc97 100644 --- a/pkg/workflow/js/create_issue.test.cjs +++ b/pkg/workflow/js/create_issue.test.cjs @@ -102,6 +102,8 @@ describe("create_issue.cjs", () => { const scriptPath = path.join(process.cwd(), "create_issue.cjs"); createIssueScript = fs.readFileSync(scriptPath, "utf8"); createIssueScript = createIssueScript.replace("export {};", ""); + // Remove the outer async IIFE wrapper so we can properly await main() + createIssueScript = createIssueScript.replace(/\(async \(\) => \{\s*await main\(\);\s*\}\)\(\);?\s*$/, "await main();"); }); afterEach(() => { @@ -770,4 +772,217 @@ describe("create_issue.cjs", () => { // Clean up delete process.env.GH_AW_SAFE_OUTPUTS_STAGED; }); + + it("should generate temporary_id_map output with created issues", async () => { + setAgentOutput({ + items: [ + { + type: "create_issue", + title: "Parent Issue", + body: "This is a parent issue", + temporary_id: "aw_abc123def456", + }, + ], + }); + + const mockIssue = { + number: 100, + title: "Parent Issue", + html_url: "https://github.com/testowner/testrepo/issues/100", + }; + + mockGithub.rest.issues.create.mockResolvedValue({ data: mockIssue }); + + // Execute the script + await eval(`(async () => { ${createIssueScript} })()`); + + // Should set the temporary_id_map output - find all calls and get the last non-empty one + const setOutputCalls = mockCore.setOutput.mock.calls; + const tempIdMapCalls = setOutputCalls.filter(call => call[0] === "temporary_id_map"); + // Get the last call (the one with actual data) + const lastTempIdMapCall = tempIdMapCalls[tempIdMapCalls.length - 1]; + expect(lastTempIdMapCall).toBeDefined(); + expect(JSON.parse(lastTempIdMapCall[1])).toEqual({ aw_abc123def456: 100 }); + expect(mockCore.info).toHaveBeenCalledWith("Stored temporary ID mapping: aw_abc123def456 -> #100"); + }); + + it("should resolve parent temporary_id to issue number when creating sub-issues", async () => { + // Create both parent and sub-issue in same workflow run + // Use valid aw_ prefixed hex strings for temporary IDs + setAgentOutput({ + items: [ + { + type: "create_issue", + title: "Parent Issue", + body: "This is a parent issue", + temporary_id: "aw_aabbccdd1122", // Valid aw_ prefixed hex string + }, + { + type: "create_issue", + title: "Sub Issue", + body: "This is a sub-issue", + parent: "aw_aabbccdd1122", // Reference parent by temporary_id + }, + ], + }); + + const parentIssue = { + number: 100, + title: "Parent Issue", + html_url: "https://github.com/testowner/testrepo/issues/100", + }; + + const subIssue = { + number: 101, + title: "Sub Issue", + html_url: "https://github.com/testowner/testrepo/issues/101", + }; + + // First call creates parent, second creates sub-issue + mockGithub.rest.issues.create.mockResolvedValueOnce({ data: parentIssue }).mockResolvedValueOnce({ data: subIssue }); + + // Mock graphql for sub-issue linking + mockGithub.graphql.mockResolvedValue({ + repository: { issue: { id: "test-node-id" } }, + addSubIssue: { subIssue: { id: "test-child-id", number: 101 } }, + }); + + // Execute the script + await eval(`(async () => { ${createIssueScript} })()`); + + // Should have resolved parent temporary_id to issue #100 + expect(mockCore.info).toHaveBeenCalledWith("Resolved parent temporary ID 'aw_aabbccdd1122' to issue #100"); + + // Both issues should be created + expect(mockGithub.rest.issues.create).toHaveBeenCalledTimes(2); + }); + + it("should replace #aw_ID references in issue body with real issue numbers", async () => { + setAgentOutput({ + items: [ + { + type: "create_issue", + title: "Parent Issue", + body: "This is a parent issue", + temporary_id: "aw_aabbccdd1122", + }, + { + type: "create_issue", + title: "Sub Issue", + body: "This issue references #aw_aabbccdd1122 in the body", + }, + ], + }); + + const parentIssue = { + number: 200, + title: "Parent Issue", + html_url: "https://github.com/testowner/testrepo/issues/200", + }; + + const subIssue = { + number: 201, + title: "Sub Issue", + html_url: "https://github.com/testowner/testrepo/issues/201", + }; + + mockGithub.rest.issues.create.mockResolvedValueOnce({ data: parentIssue }).mockResolvedValueOnce({ data: subIssue }); + + // Execute the script + await eval(`(async () => { ${createIssueScript} })()`); + + // The second issue body should have the reference replaced + const secondCallArgs = mockGithub.rest.issues.create.mock.calls[1][0]; + expect(secondCallArgs.body).toContain("#200"); + expect(secondCallArgs.body).not.toContain("#aw_aabbccdd1122"); + }); + + it("should generate auto temporary_id when none is provided", async () => { + setAgentOutput({ + items: [ + { + type: "create_issue", + title: "Issue without explicit temporary_id", + body: "Body content", + }, + ], + }); + + const mockIssue = { + number: 300, + title: "Issue without explicit temporary_id", + html_url: "https://github.com/testowner/testrepo/issues/300", + }; + + mockGithub.rest.issues.create.mockResolvedValue({ data: mockIssue }); + + // Execute the script + await eval(`(async () => { ${createIssueScript} })()`); + + // Should output temporary_id_map with auto-generated ID + const setOutputCalls = mockCore.setOutput.mock.calls; + // Find all temporary_id_map calls and get the last one + const tempIdMapCalls = setOutputCalls.filter(call => call[0] === "temporary_id_map"); + const lastTempIdMapCall = tempIdMapCalls[tempIdMapCalls.length - 1]; + expect(lastTempIdMapCall).toBeDefined(); + + const tempIdMap = JSON.parse(lastTempIdMapCall[1]); + const keys = Object.keys(tempIdMap); + expect(keys.length).toBe(1); + // Auto-generated ID should be aw_ prefix + 12 hex characters + expect(keys[0]).toMatch(/^aw_[0-9a-f]{12}$/); + expect(tempIdMap[keys[0]]).toBe(300); + }); + + it("should show temporary_id in staged mode preview", async () => { + setAgentOutput({ + items: [ + { + type: "create_issue", + title: "Issue with temp ID", + body: "Body content", + temporary_id: "aw_a1b2c3d4e5f6", // Valid aw_ prefixed hex string + }, + ], + }); + process.env.GH_AW_SAFE_OUTPUTS_STAGED = "true"; + + // Execute the script + await eval(`(async () => { ${createIssueScript} })()`); + + // Should include temporary_id in preview + const infoCall = mockCore.info.mock.calls.find(call => call[0].includes("🎭 Staged Mode: Create Issues Preview")); + expect(infoCall).toBeDefined(); + expect(infoCall[0]).toContain("**Temporary ID:** aw_a1b2c3d4e5f6"); + + // Clean up + delete process.env.GH_AW_SAFE_OUTPUTS_STAGED; + }); + + it("should warn when parent temporary_id is not found", async () => { + setAgentOutput({ + items: [ + { + type: "create_issue", + title: "Sub Issue", + body: "References non-existent parent", + parent: "aw_000000000000", // Valid aw_ format but doesn't exist + }, + ], + }); + + const mockIssue = { + number: 400, + title: "Sub Issue", + html_url: "https://github.com/testowner/testrepo/issues/400", + }; + + mockGithub.rest.issues.create.mockResolvedValue({ data: mockIssue }); + + // Execute the script + await eval(`(async () => { ${createIssueScript} })()`); + + // Should warn about missing parent + expect(mockCore.warning).toHaveBeenCalledWith(expect.stringContaining("Parent temporary ID 'aw_000000000000' not found")); + }); }); diff --git a/pkg/workflow/js/safe_outputs_tools.json b/pkg/workflow/js/safe_outputs_tools.json index 9cfb1290beb..d0bdff91853 100644 --- a/pkg/workflow/js/safe_outputs_tools.json +++ b/pkg/workflow/js/safe_outputs_tools.json @@ -22,8 +22,12 @@ "description": "Issue labels" }, "parent": { - "type": "number", - "description": "Parent issue number to create this issue as a sub-issue of" + "type": ["number", "string"], + "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." + }, + "temporary_id": { + "type": "string", + "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." } }, "additionalProperties": false diff --git a/pkg/workflow/js/temporary_id.cjs b/pkg/workflow/js/temporary_id.cjs new file mode 100644 index 00000000000..e91ac2eee0a --- /dev/null +++ b/pkg/workflow/js/temporary_id.cjs @@ -0,0 +1,86 @@ +// @ts-check +/// + +const crypto = require("crypto"); + +/** + * Regex pattern for matching temporary ID references in text + * Format: #aw_XXXXXXXXXXXX (aw_ prefix + 12 hex characters) + */ +const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi; + +/** + * Generate a temporary ID with aw_ prefix for temporary issue IDs + * @returns {string} A temporary ID in format aw_XXXXXXXXXXXX (12 hex characters) + */ +function generateTemporaryId() { + return "aw_" + crypto.randomBytes(6).toString("hex"); +} + +/** + * Check if a value is a valid temporary ID (aw_ prefix + 12-character hex string) + * @param {any} value - The value to check + * @returns {boolean} True if the value is a valid temporary ID + */ +function isTemporaryId(value) { + if (typeof value === "string") { + return /^aw_[0-9a-f]{12}$/i.test(value); + } + return false; +} + +/** + * Normalize a temporary ID to lowercase for consistent map lookups + * @param {string} tempId - The temporary ID to normalize + * @returns {string} Lowercase temporary ID + */ +function normalizeTemporaryId(tempId) { + return String(tempId).toLowerCase(); +} + +/** + * Replace temporary ID references in text with actual issue numbers + * Format: #aw_XXXXXXXXXXXX -> #123 + * @param {string} text - The text to process + * @param {Map} tempIdMap - Map of temporary_id to issue number + * @returns {string} Text with temporary IDs replaced with issue numbers + */ +function replaceTemporaryIdReferences(text, tempIdMap) { + return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => { + const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId)); + if (issueNumber !== undefined) { + return `#${issueNumber}`; + } + // Return original if not found (it may be created later) + return match; + }); +} + +/** + * Load the temporary ID map from environment variable + * @returns {Map} Map of temporary_id to issue number + */ +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(); + } +} + +module.exports = { + TEMPORARY_ID_PATTERN, + generateTemporaryId, + isTemporaryId, + normalizeTemporaryId, + replaceTemporaryIdReferences, + loadTemporaryIdMap, +}; diff --git a/pkg/workflow/js/temporary_id.test.cjs b/pkg/workflow/js/temporary_id.test.cjs new file mode 100644 index 00000000000..21db51a2fec --- /dev/null +++ b/pkg/workflow/js/temporary_id.test.cjs @@ -0,0 +1,147 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; + +// Mock core for loadTemporaryIdMap +const mockCore = { + warning: vi.fn(), +}; +global.core = mockCore; + +describe("temporary_id.cjs", () => { + beforeEach(() => { + vi.clearAllMocks(); + delete process.env.GH_AW_TEMPORARY_ID_MAP; + }); + + describe("generateTemporaryId", () => { + it("should generate an aw_ prefixed 12-character hex string", async () => { + const { generateTemporaryId } = await import("./temporary_id.cjs"); + const id = generateTemporaryId(); + expect(id).toMatch(/^aw_[0-9a-f]{12}$/); + }); + + it("should generate unique IDs", async () => { + const { generateTemporaryId } = await import("./temporary_id.cjs"); + const ids = new Set(); + for (let i = 0; i < 100; i++) { + ids.add(generateTemporaryId()); + } + expect(ids.size).toBe(100); + }); + }); + + describe("isTemporaryId", () => { + it("should return true for valid aw_ prefixed 12-char hex strings", async () => { + const { isTemporaryId } = await import("./temporary_id.cjs"); + expect(isTemporaryId("aw_abc123def456")).toBe(true); + expect(isTemporaryId("aw_000000000000")).toBe(true); + expect(isTemporaryId("aw_AABBCCDD1122")).toBe(true); + expect(isTemporaryId("aw_aAbBcCdDeEfF")).toBe(true); + }); + + it("should return false for invalid strings", async () => { + const { isTemporaryId } = await import("./temporary_id.cjs"); + expect(isTemporaryId("abc123def456")).toBe(false); // Missing aw_ prefix + expect(isTemporaryId("aw_abc123")).toBe(false); // Too short + expect(isTemporaryId("aw_abc123def4567")).toBe(false); // Too long + expect(isTemporaryId("aw_parent123456")).toBe(false); // Contains non-hex chars + expect(isTemporaryId("aw_ghijklmnopqr")).toBe(false); // Non-hex letters + expect(isTemporaryId("")).toBe(false); + expect(isTemporaryId("temp_abc123def456")).toBe(false); // Wrong prefix + }); + + it("should return false for non-string values", async () => { + const { isTemporaryId } = await import("./temporary_id.cjs"); + expect(isTemporaryId(123)).toBe(false); + expect(isTemporaryId(null)).toBe(false); + expect(isTemporaryId(undefined)).toBe(false); + expect(isTemporaryId({})).toBe(false); + }); + }); + + describe("normalizeTemporaryId", () => { + it("should convert to lowercase", async () => { + const { normalizeTemporaryId } = await import("./temporary_id.cjs"); + expect(normalizeTemporaryId("aw_ABC123DEF456")).toBe("aw_abc123def456"); + expect(normalizeTemporaryId("AW_aAbBcCdDeEfF")).toBe("aw_aabbccddeeff"); + }); + }); + + describe("replaceTemporaryIdReferences", () => { + it("should replace #aw_ID with issue numbers", async () => { + const { replaceTemporaryIdReferences } = await import("./temporary_id.cjs"); + const map = new Map([["aw_abc123def456", 100]]); + const text = "Check #aw_abc123def456 for details"; + expect(replaceTemporaryIdReferences(text, map)).toBe("Check #100 for details"); + }); + + it("should handle multiple references", async () => { + const { replaceTemporaryIdReferences } = await import("./temporary_id.cjs"); + const map = new Map([ + ["aw_abc123def456", 100], + ["aw_111222333444", 200], + ]); + const text = "See #aw_abc123def456 and #aw_111222333444"; + expect(replaceTemporaryIdReferences(text, map)).toBe("See #100 and #200"); + }); + + it("should preserve unresolved references", async () => { + const { replaceTemporaryIdReferences } = await import("./temporary_id.cjs"); + const map = new Map(); + const text = "Check #aw_000000000000 for details"; + expect(replaceTemporaryIdReferences(text, map)).toBe("Check #aw_000000000000 for details"); + }); + + it("should be case-insensitive", async () => { + const { replaceTemporaryIdReferences } = await import("./temporary_id.cjs"); + const map = new Map([["aw_abc123def456", 100]]); + const text = "Check #AW_ABC123DEF456 for details"; + expect(replaceTemporaryIdReferences(text, map)).toBe("Check #100 for details"); + }); + + it("should not match invalid temporary ID formats", async () => { + const { replaceTemporaryIdReferences } = await import("./temporary_id.cjs"); + const map = new Map([["aw_abc123def456", 100]]); + const text = "Check #aw_abc123 and #temp:abc123def456 for details"; + expect(replaceTemporaryIdReferences(text, map)).toBe("Check #aw_abc123 and #temp:abc123def456 for details"); + }); + }); + + describe("loadTemporaryIdMap", () => { + it("should return empty map when env var is not set", async () => { + const { loadTemporaryIdMap } = await import("./temporary_id.cjs"); + const map = loadTemporaryIdMap(); + expect(map.size).toBe(0); + }); + + it("should return empty map when env var is empty object", async () => { + process.env.GH_AW_TEMPORARY_ID_MAP = "{}"; + const { loadTemporaryIdMap } = await import("./temporary_id.cjs"); + const map = loadTemporaryIdMap(); + expect(map.size).toBe(0); + }); + + it("should parse valid JSON map", async () => { + process.env.GH_AW_TEMPORARY_ID_MAP = JSON.stringify({ aw_abc123def456: 100, aw_111222333444: 200 }); + const { loadTemporaryIdMap } = await import("./temporary_id.cjs"); + const map = loadTemporaryIdMap(); + expect(map.size).toBe(2); + expect(map.get("aw_abc123def456")).toBe(100); + expect(map.get("aw_111222333444")).toBe(200); + }); + + it("should normalize keys to lowercase", async () => { + process.env.GH_AW_TEMPORARY_ID_MAP = JSON.stringify({ AW_ABC123DEF456: 100 }); + const { loadTemporaryIdMap } = await import("./temporary_id.cjs"); + const map = loadTemporaryIdMap(); + expect(map.get("aw_abc123def456")).toBe(100); + }); + + it("should warn and return empty map on invalid JSON", async () => { + process.env.GH_AW_TEMPORARY_ID_MAP = "not valid json"; + const { loadTemporaryIdMap } = await import("./temporary_id.cjs"); + const map = loadTemporaryIdMap(); + expect(map.size).toBe(0); + expect(mockCore.warning).toHaveBeenCalled(); + }); + }); +}); diff --git a/pkg/workflow/js/types/safe-outputs.d.ts b/pkg/workflow/js/types/safe-outputs.d.ts index a73a2cd6e9a..aa852943008 100644 --- a/pkg/workflow/js/types/safe-outputs.d.ts +++ b/pkg/workflow/js/types/safe-outputs.d.ts @@ -22,8 +22,10 @@ interface CreateIssueItem extends BaseSafeOutputItem { body: string; /** Optional labels to add to the issue */ labels?: string[]; - /** Optional parent issue number to link as sub-issue */ - parent?: number; + /** Optional parent issue number or temporary_id to link as sub-issue */ + parent?: number | string; + /** Optional temporary identifier for this issue that can be referenced by other issues */ + temporary_id?: string; } /**