From 59b6bb98269ec5596df4cb7ffced529459d01c56 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 09:59:04 +0000 Subject: [PATCH 1/5] Initial plan From f3d248df04968842679957d67bb26618cdbced6b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 10:11:43 +0000 Subject: [PATCH 2/5] Implement project URL resolution in update_issue - Export temporary_project_map from project handler manager - Pass project map to regular handler via GH_AW_TEMPORARY_PROJECT_MAP - Add replaceTemporaryProjectReferences function in temporary_id.cjs - Apply project URL replacement in update_issue before body processing - Update compiler to wire up project map outputs and inputs Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> --- .../safe_output_project_handler_manager.cjs | 14 +++++- actions/setup/js/temporary_id.cjs | 49 +++++++++++++++++++ actions/setup/js/update_issue.cjs | 11 ++++- pkg/workflow/compiler_safe_outputs_job.go | 1 + pkg/workflow/compiler_safe_outputs_steps.go | 4 ++ 5 files changed, 76 insertions(+), 3 deletions(-) diff --git a/actions/setup/js/safe_output_project_handler_manager.cjs b/actions/setup/js/safe_output_project_handler_manager.cjs index 834379563d4..f6a1d86eb1c 100644 --- a/actions/setup/js/safe_output_project_handler_manager.cjs +++ b/actions/setup/js/safe_output_project_handler_manager.cjs @@ -181,7 +181,10 @@ async function processMessages(messageHandlers, messages) { } } - return { results, processedCount }; + // Convert temporaryProjectMap to plain object for serialization + const temporaryProjectMapObj = Object.fromEntries(temporaryProjectMap); + + return { results, processedCount, temporaryProjectMap: temporaryProjectMapObj }; } /** @@ -221,10 +224,16 @@ async function main() { } // Process messages - const { results, processedCount } = await processMessages(messageHandlers, messages); + const { results, processedCount, temporaryProjectMap } = await processMessages(messageHandlers, messages); // Set outputs core.setOutput("processed_count", processedCount); + + // Export temporary project map as output so the regular handler manager can use it + // to resolve project URLs in text (e.g., update_issue body) + const temporaryProjectMapJson = JSON.stringify(temporaryProjectMap || {}); + core.setOutput("temporary_project_map", temporaryProjectMapJson); + core.info(`Exported temporary project map with ${Object.keys(temporaryProjectMap || {}).length} mapping(s)`); // Summary const successCount = results.filter(r => r.success).length; @@ -235,6 +244,7 @@ async function main() { core.info(`Project-related messages processed: ${processedCount}`); core.info(`Successful: ${successCount}`); core.info(`Failed: ${failureCount}`); + core.info(`Temporary project IDs registered: ${Object.keys(temporaryProjectMap || {}).length}`); if (failureCount > 0) { core.setFailed(`${failureCount} project-related message(s) failed to process`); diff --git a/actions/setup/js/temporary_id.cjs b/actions/setup/js/temporary_id.cjs index 042f14f51d0..b4179a16159 100644 --- a/actions/setup/js/temporary_id.cjs +++ b/actions/setup/js/temporary_id.cjs @@ -213,6 +213,53 @@ function serializeTemporaryIdMap(tempIdMap) { return JSON.stringify(obj); } +/** + * Load the temporary project map from environment variable + * @returns {Map} Map of temporary_project_id to project URL + */ +function loadTemporaryProjectMap() { + const mapJson = process.env.GH_AW_TEMPORARY_PROJECT_MAP; + if (!mapJson || mapJson === "{}") { + return new Map(); + } + try { + const mapObject = JSON.parse(mapJson); + /** @type {Map} */ + const result = new Map(); + + for (const [key, value] of Object.entries(mapObject)) { + const normalizedKey = normalizeTemporaryId(key); + if (typeof value === "string") { + result.set(normalizedKey, value); + } + } + return result; + } catch (error) { + if (typeof core !== "undefined") { + core.warning(`Failed to parse temporary project map: ${getErrorMessage(error)}`); + } + return new Map(); + } +} + +/** + * Replace temporary project ID references in text with actual project URLs + * Format: #aw_XXXXXXXXXXXX -> https://github.com/orgs/myorg/projects/123 + * @param {string} text - The text to process + * @param {Map} tempProjectMap - Map of temporary_project_id to project URL + * @returns {string} Text with temporary project IDs replaced with project URLs + */ +function replaceTemporaryProjectReferences(text, tempProjectMap) { + return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => { + const resolved = tempProjectMap.get(normalizeTemporaryId(tempId)); + if (resolved !== undefined) { + return resolved; + } + // Return original if not found (it may be an issue ID) + return match; + }); +} + module.exports = { TEMPORARY_ID_PATTERN, generateTemporaryId, @@ -224,4 +271,6 @@ module.exports = { resolveIssueNumber, hasUnresolvedTemporaryIds, serializeTemporaryIdMap, + loadTemporaryProjectMap, + replaceTemporaryProjectReferences, }; diff --git a/actions/setup/js/update_issue.cjs b/actions/setup/js/update_issue.cjs index 7730f8b8d1e..8bad6fcfecf 100644 --- a/actions/setup/js/update_issue.cjs +++ b/actions/setup/js/update_issue.cjs @@ -11,6 +11,7 @@ const HANDLER_TYPE = "update_issue"; const { resolveTarget } = require("./safe_output_helpers.cjs"); const { createUpdateHandlerFactory } = require("./update_handler_factory.cjs"); const { updateBody } = require("./update_pr_description_helpers.cjs"); +const { loadTemporaryProjectMap, replaceTemporaryProjectReferences } = require("./temporary_id.cjs"); /** * Execute the issue update API call @@ -24,13 +25,21 @@ async function executeIssueUpdate(github, context, issueNumber, updateData) { // Handle body operation (append/prepend/replace/replace-island) // Default to "append" to add footer with AI attribution const operation = updateData._operation || "append"; - const rawBody = updateData._rawBody; + let rawBody = updateData._rawBody; // Remove internal fields const { _operation, _rawBody, ...apiData } = updateData; // If we have a body, process it with the appropriate operation if (rawBody !== undefined) { + // Load and apply temporary project URL replacements FIRST + // This resolves any temporary project IDs (e.g., #aw_abc123def456) to actual project URLs + const temporaryProjectMap = loadTemporaryProjectMap(); + if (temporaryProjectMap.size > 0) { + rawBody = replaceTemporaryProjectReferences(rawBody, temporaryProjectMap); + core.debug(`Applied ${temporaryProjectMap.size} temporary project URL replacement(s)`); + } + // Fetch current issue body for all operations (needed for append/prepend/replace-island/replace) const { data: currentIssue } = await github.rest.issues.get({ owner: context.repo.owner, diff --git a/pkg/workflow/compiler_safe_outputs_job.go b/pkg/workflow/compiler_safe_outputs_job.go index 2671b385e3b..b95cd9c7ecf 100644 --- a/pkg/workflow/compiler_safe_outputs_job.go +++ b/pkg/workflow/compiler_safe_outputs_job.go @@ -162,6 +162,7 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa // Add outputs from project handler manager outputs["process_project_safe_outputs_processed_count"] = "${{ steps.process_project_safe_outputs.outputs.processed_count }}" + outputs["process_project_safe_outputs_temporary_project_map"] = "${{ steps.process_project_safe_outputs.outputs.temporary_project_map }}" // Add permissions for project-related types // Note: Projects v2 cannot use GITHUB_TOKEN; it requires a PAT or GitHub App token diff --git a/pkg/workflow/compiler_safe_outputs_steps.go b/pkg/workflow/compiler_safe_outputs_steps.go index 547d2468d5f..d4cc2c22adc 100644 --- a/pkg/workflow/compiler_safe_outputs_steps.go +++ b/pkg/workflow/compiler_safe_outputs_steps.go @@ -159,6 +159,10 @@ func (c *Compiler) buildHandlerManagerStep(data *WorkflowData) []string { // Environment variables steps = append(steps, " env:\n") steps = append(steps, " GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}\n") + + // If project handler ran before this, pass its temporary project map + // This allows update_issue and other text-based handlers to resolve project temporary IDs + steps = append(steps, " GH_AW_TEMPORARY_PROJECT_MAP: ${{ steps.process_project_safe_outputs.outputs.temporary_project_map }}\n") // Add custom safe output env vars c.addCustomSafeOutputEnvVars(&steps, data) From c345ba9ff25a1bac83e68a73216d8ae4f4f4dc20 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 10:19:22 +0000 Subject: [PATCH 3/5] Make GH_AW_TEMPORARY_PROJECT_MAP conditional on project handlers - Only add env var when project-related safe outputs are enabled - Fixes tests that verify exact YAML structure - Add tests for replaceTemporaryProjectReferences - Add tests for loadTemporaryProjectMap - Update TypeScript types for safe_output_project_handler_manager Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> --- .../agentic-campaign-generator.lock.yml | 2 + .../safe_output_project_handler_manager.cjs | 2 +- actions/setup/js/temporary_id.test.cjs | 63 +++++++++++++++++++ pkg/workflow/compiler_safe_outputs_steps.go | 15 ++++- 4 files changed, 78 insertions(+), 4 deletions(-) diff --git a/.github/workflows/agentic-campaign-generator.lock.yml b/.github/workflows/agentic-campaign-generator.lock.yml index 21d99c0293a..355758b5492 100644 --- a/.github/workflows/agentic-campaign-generator.lock.yml +++ b/.github/workflows/agentic-campaign-generator.lock.yml @@ -1470,6 +1470,7 @@ jobs: assign_to_agent_assignment_error_count: ${{ steps.assign_to_agent.outputs.assignment_error_count }} assign_to_agent_assignment_errors: ${{ steps.assign_to_agent.outputs.assignment_errors }} process_project_safe_outputs_processed_count: ${{ steps.process_project_safe_outputs.outputs.processed_count }} + process_project_safe_outputs_temporary_project_map: ${{ steps.process_project_safe_outputs.outputs.temporary_project_map }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: @@ -1523,6 +1524,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_TEMPORARY_PROJECT_MAP: ${{ steps.process_project_safe_outputs.outputs.temporary_project_map }} GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"missing_data\":{},\"missing_tool\":{},\"update_issue\":{\"max\":1}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/actions/setup/js/safe_output_project_handler_manager.cjs b/actions/setup/js/safe_output_project_handler_manager.cjs index f6a1d86eb1c..92244d1dc81 100644 --- a/actions/setup/js/safe_output_project_handler_manager.cjs +++ b/actions/setup/js/safe_output_project_handler_manager.cjs @@ -105,7 +105,7 @@ async function loadHandlers(config) { * Process project-related safe output messages * @param {Map} messageHandlers - Map of type to handler function * @param {Array} messages - Array of safe output messages - * @returns {Promise<{results: Array, processedCount: number}>} Processing results + * @returns {Promise<{results: Array, processedCount: number, temporaryProjectMap: Object}>} Processing results */ async function processMessages(messageHandlers, messages) { const results = []; diff --git a/actions/setup/js/temporary_id.test.cjs b/actions/setup/js/temporary_id.test.cjs index 15910bfc0d8..87d8871187b 100644 --- a/actions/setup/js/temporary_id.test.cjs +++ b/actions/setup/js/temporary_id.test.cjs @@ -401,4 +401,67 @@ describe("temporary_id.cjs", () => { expect(hasUnresolvedTemporaryIds(text, map)).toBe(true); }); }); + + describe("replaceTemporaryProjectReferences", () => { + it("should replace #aw_ID with project URLs", async () => { + const { replaceTemporaryProjectReferences } = await import("./temporary_id.cjs"); + const map = new Map([["aw_abc123def456", "https://github.com/orgs/myorg/projects/123"]]); + const text = "Project created: #aw_abc123def456"; + expect(replaceTemporaryProjectReferences(text, map)).toBe("Project created: https://github.com/orgs/myorg/projects/123"); + }); + + it("should handle multiple project references", async () => { + const { replaceTemporaryProjectReferences } = await import("./temporary_id.cjs"); + const map = new Map([ + ["aw_abc123def456", "https://github.com/orgs/myorg/projects/123"], + ["aw_111222333444", "https://github.com/orgs/myorg/projects/456"], + ]); + const text = "See #aw_abc123def456 and #aw_111222333444"; + expect(replaceTemporaryProjectReferences(text, map)).toBe("See https://github.com/orgs/myorg/projects/123 and https://github.com/orgs/myorg/projects/456"); + }); + + it("should leave unresolved project references unchanged", async () => { + const { replaceTemporaryProjectReferences } = await import("./temporary_id.cjs"); + const map = new Map([["aw_abc123def456", "https://github.com/orgs/myorg/projects/123"]]); + const text = "See #aw_unresolved"; + expect(replaceTemporaryProjectReferences(text, map)).toBe("See #aw_unresolved"); + }); + + it("should be case insensitive", async () => { + const { replaceTemporaryProjectReferences } = await import("./temporary_id.cjs"); + const map = new Map([["aw_abc123def456", "https://github.com/orgs/myorg/projects/123"]]); + const text = "Project: #AW_ABC123DEF456"; + expect(replaceTemporaryProjectReferences(text, map)).toBe("Project: https://github.com/orgs/myorg/projects/123"); + }); + }); + + describe("loadTemporaryProjectMap", () => { + it("should return empty map when env var is not set", async () => { + delete process.env.GH_AW_TEMPORARY_PROJECT_MAP; + const { loadTemporaryProjectMap } = await import("./temporary_id.cjs"); + const map = loadTemporaryProjectMap(); + expect(map.size).toBe(0); + }); + + it("should load project map from environment", async () => { + process.env.GH_AW_TEMPORARY_PROJECT_MAP = JSON.stringify({ + aw_abc123def456: "https://github.com/orgs/myorg/projects/123", + aw_111222333444: "https://github.com/users/jdoe/projects/456", + }); + const { loadTemporaryProjectMap } = await import("./temporary_id.cjs"); + const map = loadTemporaryProjectMap(); + expect(map.size).toBe(2); + expect(map.get("aw_abc123def456")).toBe("https://github.com/orgs/myorg/projects/123"); + expect(map.get("aw_111222333444")).toBe("https://github.com/users/jdoe/projects/456"); + }); + + it("should normalize keys to lowercase", async () => { + process.env.GH_AW_TEMPORARY_PROJECT_MAP = JSON.stringify({ + AW_ABC123DEF456: "https://github.com/orgs/myorg/projects/123", + }); + const { loadTemporaryProjectMap } = await import("./temporary_id.cjs"); + const map = loadTemporaryProjectMap(); + expect(map.get("aw_abc123def456")).toBe("https://github.com/orgs/myorg/projects/123"); + }); + }); }); diff --git a/pkg/workflow/compiler_safe_outputs_steps.go b/pkg/workflow/compiler_safe_outputs_steps.go index d4cc2c22adc..3d3683794f5 100644 --- a/pkg/workflow/compiler_safe_outputs_steps.go +++ b/pkg/workflow/compiler_safe_outputs_steps.go @@ -160,9 +160,18 @@ func (c *Compiler) buildHandlerManagerStep(data *WorkflowData) []string { steps = append(steps, " env:\n") steps = append(steps, " GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}\n") - // If project handler ran before this, pass its temporary project map - // This allows update_issue and other text-based handlers to resolve project temporary IDs - steps = append(steps, " GH_AW_TEMPORARY_PROJECT_MAP: ${{ steps.process_project_safe_outputs.outputs.temporary_project_map }}\n") + // Check if any project-handler types are enabled + // If so, pass the temporary project map from the project handler step + hasProjectHandlerTypes := data.SafeOutputs.CreateProjects != nil || + data.SafeOutputs.CreateProjectStatusUpdates != nil || + data.SafeOutputs.UpdateProjects != nil || + data.SafeOutputs.CopyProjects != nil + + if hasProjectHandlerTypes { + // If project handler ran before this, pass its temporary project map + // This allows update_issue and other text-based handlers to resolve project temporary IDs + steps = append(steps, " GH_AW_TEMPORARY_PROJECT_MAP: ${{ steps.process_project_safe_outputs.outputs.temporary_project_map }}\n") + } // Add custom safe output env vars c.addCustomSafeOutputEnvVars(&steps, data) From b7ea6590504c2c70b950b3f838d97afe5fa91e7e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 10:25:12 +0000 Subject: [PATCH 4/5] Final validation: format code and verify tests Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> --- actions/setup/js/safe_output_project_handler_manager.cjs | 2 +- pkg/workflow/compiler_safe_outputs_steps.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/actions/setup/js/safe_output_project_handler_manager.cjs b/actions/setup/js/safe_output_project_handler_manager.cjs index 92244d1dc81..d4b69d7b5ba 100644 --- a/actions/setup/js/safe_output_project_handler_manager.cjs +++ b/actions/setup/js/safe_output_project_handler_manager.cjs @@ -228,7 +228,7 @@ async function main() { // Set outputs core.setOutput("processed_count", processedCount); - + // Export temporary project map as output so the regular handler manager can use it // to resolve project URLs in text (e.g., update_issue body) const temporaryProjectMapJson = JSON.stringify(temporaryProjectMap || {}); diff --git a/pkg/workflow/compiler_safe_outputs_steps.go b/pkg/workflow/compiler_safe_outputs_steps.go index 3d3683794f5..200d3a75412 100644 --- a/pkg/workflow/compiler_safe_outputs_steps.go +++ b/pkg/workflow/compiler_safe_outputs_steps.go @@ -159,14 +159,14 @@ func (c *Compiler) buildHandlerManagerStep(data *WorkflowData) []string { // Environment variables steps = append(steps, " env:\n") steps = append(steps, " GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}\n") - + // Check if any project-handler types are enabled // If so, pass the temporary project map from the project handler step hasProjectHandlerTypes := data.SafeOutputs.CreateProjects != nil || data.SafeOutputs.CreateProjectStatusUpdates != nil || data.SafeOutputs.UpdateProjects != nil || data.SafeOutputs.CopyProjects != nil - + if hasProjectHandlerTypes { // If project handler ran before this, pass its temporary project map // This allows update_issue and other text-based handlers to resolve project temporary IDs From f18b942fc876758b5d848d9df5f698d9fb96c63c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 11:46:33 +0000 Subject: [PATCH 5/5] Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> --- .github/workflows/security-alert-burndown.campaign.lock.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/security-alert-burndown.campaign.lock.yml b/.github/workflows/security-alert-burndown.campaign.lock.yml index 3561de38cd9..c63abc37628 100644 --- a/.github/workflows/security-alert-burndown.campaign.lock.yml +++ b/.github/workflows/security-alert-burndown.campaign.lock.yml @@ -2408,6 +2408,7 @@ jobs: GH_AW_WORKFLOW_NAME: "Security Alert Burndown" outputs: process_project_safe_outputs_processed_count: ${{ steps.process_project_safe_outputs.outputs.processed_count }} + process_project_safe_outputs_temporary_project_map: ${{ steps.process_project_safe_outputs.outputs.temporary_project_map }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: @@ -2451,6 +2452,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_TEMPORARY_PROJECT_MAP: ${{ steps.process_project_safe_outputs.outputs.temporary_project_map }} GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":3},\"create_issue\":{\"max\":1},\"missing_data\":{},\"missing_tool\":{}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}