diff --git a/actions/setup/js/create_project.cjs b/actions/setup/js/create_project.cjs index f1834774d3..3d2c3a21ee 100644 --- a/actions/setup/js/create_project.cjs +++ b/actions/setup/js/create_project.cjs @@ -320,11 +320,10 @@ async function main(config = {}, githubClient = null) { /** * Message handler function that processes a single create_project message * @param {Object} message - The create_project message to process - * @param {Map} temporaryIdMap - Unified map of temporary IDs - * @param {Object} resolvedTemporaryIds - Plain object version of temporaryIdMap for backward compatibility + * @param {Object} resolvedTemporaryIds - Plain object map of temporary IDs to resolved values * @returns {Promise} Result with success/error status */ - return async function handleCreateProject(message, temporaryIdMap, resolvedTemporaryIds = {}) { + return async function handleCreateProject(message, resolvedTemporaryIds = {}) { // Check max limit if (processedCount >= maxCount) { core.warning(`Skipping create_project: max count of ${maxCount} reached`); diff --git a/actions/setup/js/create_project_status_update.cjs b/actions/setup/js/create_project_status_update.cjs index b0a4e10479..be50b577b7 100644 --- a/actions/setup/js/create_project_status_update.cjs +++ b/actions/setup/js/create_project_status_update.cjs @@ -300,11 +300,10 @@ async function main(config = {}, githubClient = null) { /** * Message handler function that processes a single create_project_status_update message * @param {Object} message - The create_project_status_update message to process - * @param {Map} temporaryIdMap - Unified map of temporary IDs - * @param {Object} resolvedTemporaryIds - Plain object version of temporaryIdMap for backward compatibility + * @param {Object} resolvedTemporaryIds - Plain object map of temporary IDs to resolved values * @returns {Promise} Result with success/error status and status update details */ - return async function handleCreateProjectStatusUpdate(message, temporaryIdMap, resolvedTemporaryIds = {}) { + return async function handleCreateProjectStatusUpdate(message, resolvedTemporaryIds = {}) { // Check if we've hit the max limit if (processedCount >= maxCount) { core.warning(`Skipping create-project-status-update: max count of ${maxCount} reached`); diff --git a/actions/setup/js/safe_output_unified_handler_manager.cjs b/actions/setup/js/safe_output_unified_handler_manager.cjs index 45ce87977c..c54164aa04 100644 --- a/actions/setup/js/safe_output_unified_handler_manager.cjs +++ b/actions/setup/js/safe_output_unified_handler_manager.cjs @@ -428,14 +428,10 @@ async function processMessages(messageHandlers, messages, projectOctokit = null) // Convert Map to plain object for handler - both handler types use the same unified map const resolvedTemporaryIds = Object.fromEntries(temporaryIdMap); - if (isProjectHandler) { - // Project handlers receive: (message, temporaryIdMap, resolvedTemporaryIds) - // Note: Project handlers already have the project Octokit bound during initialization - result = await messageHandler(message, temporaryIdMap, resolvedTemporaryIds); - } else { - // Regular handlers receive: (message, resolvedTemporaryIds) - result = await messageHandler(message, resolvedTemporaryIds); - } + // Both project and regular handlers receive: (message, resolvedTemporaryIds) + // Project handlers no longer need the Map directly - they read from resolvedTemporaryIds + // and return temporary ID mappings for the manager to store + result = await messageHandler(message, resolvedTemporaryIds); // Check if the handler explicitly returned a failure if (result && result.success === false && !result.deferred) { @@ -575,6 +571,7 @@ async function processMessages(messageHandlers, messages, projectOctokit = null) const tempIdMapSizeBefore = temporaryIdMap.size; // Call the handler again with updated temp ID map + // All handlers receive: (message, resolvedTemporaryIds) const result = await deferred.handler(deferred.message, resolvedTemporaryIds); // Check if the handler explicitly returned a failure @@ -613,6 +610,24 @@ async function processMessages(messageHandlers, messages, projectOctokit = null) core.info(`Registered temporary ID: ${result.temporaryId} -> ${result.repo}#${result.number}`); } + // If this was a create_project during retry, store the project URL in the unified map + if (deferred.type === "create_project" && result && result.projectUrl && deferred.message.temporary_id) { + const normalizedTempId = normalizeTemporaryId(deferred.message.temporary_id); + temporaryIdMap.set(normalizedTempId, { + projectUrl: result.projectUrl, + }); + core.info(`✓ Stored project mapping: ${deferred.message.temporary_id} -> ${result.projectUrl}`); + } + + // If this was an update_project that created a draft issue during retry, store the draft item mapping + if (deferred.type === "update_project" && result && result.temporaryId && result.draftItemId) { + const normalizedTempId = normalizeTemporaryId(result.temporaryId); + temporaryIdMap.set(normalizedTempId, { + draftItemId: result.draftItemId, + }); + core.info(`✓ Stored draft issue mapping: ${result.temporaryId} -> draft item ${result.draftItemId}`); + } + // Check if this output was created with unresolved temporary IDs // For create_issue, create_discussion - check if body has unresolved IDs // This enables synthetic updates to resolve references after all items are created diff --git a/actions/setup/js/temporary_id.cjs b/actions/setup/js/temporary_id.cjs index 482aac72ff..fbe9284d30 100644 --- a/actions/setup/js/temporary_id.cjs +++ b/actions/setup/js/temporary_id.cjs @@ -169,7 +169,7 @@ function loadTemporaryIdMapFromResolved(resolvedTemporaryIds) { * Resolve an issue number that may be a temporary ID or an actual issue number * Returns structured result with the resolved number, repo, and metadata * @param {any} value - The value to resolve (can be temporary ID, number, or string) - * @param {Map} temporaryIdMap - Map of temporary ID to resolved value (supports legacy formats) + * @param {Map|Object} temporaryIdMap - Map or object of temporary ID to resolved value * @returns {{resolved: RepoIssuePair|null, wasTemporaryId: boolean, errorMessage: string|null}} */ function resolveIssueNumber(value, temporaryIdMap) { @@ -183,7 +183,9 @@ function resolveIssueNumber(value, temporaryIdMap) { // Check if it's a temporary ID if (isTemporaryId(valueWithoutHash)) { - const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueWithoutHash)); + // Support both Map and plain object + const normalizedId = normalizeTemporaryId(valueWithoutHash); + const resolvedPair = temporaryIdMap instanceof Map ? temporaryIdMap.get(normalizedId) : temporaryIdMap[normalizedId]; if (resolvedPair !== undefined) { // Support legacy format where the map value is the issue number. const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : ""; diff --git a/actions/setup/js/update_project.cjs b/actions/setup/js/update_project.cjs index ebdc507ff8..cb183a4de1 100644 --- a/actions/setup/js/update_project.cjs +++ b/actions/setup/js/update_project.cjs @@ -387,11 +387,11 @@ async function findExistingDraftByTitle(github, projectId, targetTitle) { /** * Update a GitHub Project v2 * @param {any} output - Safe output configuration - * @param {Map} temporaryIdMap - Map of temporary IDs to resolved issue numbers + * @param {Object} resolvedTemporaryIds - Plain object map of temporary IDs to resolved values * @param {Object} githubClient - GitHub client (Octokit instance) to use for GraphQL queries * @returns {Promise} Returns undefined for most operations, or an object with temporary ID mapping for draft issue creation */ -async function updateProject(output, temporaryIdMap = new Map(), githubClient = null) { +async function updateProject(output, resolvedTemporaryIds = {}, githubClient = null) { output = normalizeUpdateProjectOutput(output); // Use the provided github client, or fall back to the global github object @@ -720,9 +720,10 @@ async function updateProject(output, temporaryIdMap = new Map(), githubClient = // This ensures the mapping is preserved when updating existing drafts resolvedTemporaryId = draftIssueId; - // Try to resolve draft_issue_id from temporaryIdMap using normalized ID + // Try to resolve draft_issue_id from resolvedTemporaryIds using normalized ID const normalized = normalizeTemporaryId(draftIssueId); - const resolved = temporaryIdMap.get(normalized); + // Support both Map and plain object + const resolved = resolvedTemporaryIds instanceof Map ? resolvedTemporaryIds.get(normalized) : resolvedTemporaryIds[normalized]; if (resolved && resolved.draftItemId) { itemId = resolved.draftItemId; core.info(`✓ Resolved draft_issue_id "${draftIssueId}" to item ${itemId}`); @@ -771,13 +772,6 @@ async function updateProject(output, temporaryIdMap = new Map(), githubClient = ); itemId = result.addProjectV2DraftIssue.projectItem.id; core.info(`✓ Created new draft issue "${draftTitle}"`); - - // Store temporary_id mapping if provided - if (temporaryId) { - const normalized = normalizeTemporaryId(temporaryId); - temporaryIdMap.set(normalized, { draftItemId: itemId }); - core.info(`✓ Stored temporary_id mapping: ${temporaryId} -> ${itemId}`); - } } } @@ -926,7 +920,7 @@ async function updateProject(output, temporaryIdMap = new Map(), githubClient = if (sanitizedContentNumber) { // Try to resolve as temporary ID first - const resolved = resolveIssueNumber(sanitizedContentNumber, temporaryIdMap); + const resolved = resolveIssueNumber(sanitizedContentNumber, resolvedTemporaryIds); if (resolved.wasTemporaryId) { if (resolved.errorMessage || !resolved.resolved) { @@ -1173,11 +1167,10 @@ async function main(config = {}, githubClient = null) { /** * Message handler function that processes a single update_project message * @param {Object} message - The update_project message to process - * @param {Map} temporaryIdMap - Unified map of temporary IDs - * @param {Object} resolvedTemporaryIds - Plain object version of temporaryIdMap for backward compatibility + * @param {Object} resolvedTemporaryIds - Plain object map of temporary IDs to resolved values * @returns {Promise} Result with success/error status, and optionally temporaryId/draftItemId for draft issue creation */ - return async function handleUpdateProject(message, temporaryIdMap, resolvedTemporaryIds = {}) { + return async function handleUpdateProject(message, resolvedTemporaryIds = {}) { message = normalizeUpdateProjectOutput(message); // Check max limit @@ -1226,8 +1219,10 @@ async function main(config = {}, githubClient = null) { // Check if it's a temporary ID (aw_XXXXXXXXXXXX) if (/^aw_[0-9a-f]{12}$/i.test(projectWithoutHash)) { - // Look up in the unified temporaryIdMap - const resolved = temporaryIdMap.get(projectWithoutHash.toLowerCase()); + // Look up in the resolvedTemporaryIds + // Support both Map and plain object + const normalizedId = projectWithoutHash.toLowerCase(); + const resolved = resolvedTemporaryIds instanceof Map ? resolvedTemporaryIds.get(normalizedId) : resolvedTemporaryIds[normalizedId]; if (resolved && resolved.projectUrl) { core.info(`Resolved temporary project ID ${projectStr} to ${resolved.projectUrl}`); effectiveProjectUrl = resolved.projectUrl; @@ -1261,7 +1256,7 @@ async function main(config = {}, githubClient = null) { }; try { - await updateProject(fieldsOutput, temporaryIdMap, github); + await updateProject(fieldsOutput, resolvedTemporaryIds, github); core.info("✓ Created configured fields"); } catch (err) { // prettier-ignore @@ -1279,7 +1274,7 @@ async function main(config = {}, githubClient = null) { } // Process the update_project message - const updateResult = await updateProject(effectiveMessage, temporaryIdMap, github); + const updateResult = await updateProject(effectiveMessage, resolvedTemporaryIds, github); // After processing the first message, create configured views if any // Views are created after the first item is processed to ensure the project exists @@ -1304,7 +1299,7 @@ async function main(config = {}, githubClient = null) { }, }; - await updateProject(viewOutput, temporaryIdMap, github); + await updateProject(viewOutput, resolvedTemporaryIds, github); core.info(`✓ Created view ${i + 1}/${configuredViews.length}: ${viewConfig.name} (${viewConfig.layout})`); } catch (err) { // prettier-ignore diff --git a/actions/setup/js/update_project.test.cjs b/actions/setup/js/update_project.test.cjs index 60dbd0d3a7..68bcaaccb4 100644 --- a/actions/setup/js/update_project.test.cjs +++ b/actions/setup/js/update_project.test.cjs @@ -515,7 +515,7 @@ describe("updateProject", () => { expect(result.draftItemId).toBe("draft-item-2"); expect(getOutput("item-id")).toBe("draft-item-2"); expect(getOutput("temporary-id")).toBe(temporaryId); - expect(mockCore.info).toHaveBeenCalledWith(`✓ Stored temporary_id mapping: ${temporaryId} -> draft-item-2`); + // Note: temporaryIdMap is no longer mutated by updateProject - the handler manager stores mappings based on return value }); it("rejects draft issues without a title", async () => { @@ -587,9 +587,8 @@ describe("updateProject", () => { expect(mockGithub.graphql.mock.calls.some(([query]) => query.includes("addProjectV2DraftIssue"))).toBe(true); expect(getOutput("item-id")).toBe("draft-item-temp"); expect(getOutput("temporary-id")).toBe("aw_9f11121ed7df"); - expect(temporaryIdMap.get("aw_9f11121ed7df")).toEqual({ draftItemId: "draft-item-temp" }); + // Note: temporaryIdMap is no longer mutated by updateProject - the handler manager stores mappings based on return value expect(mockCore.info).toHaveBeenCalledWith('✓ Created new draft issue "Draft with temp ID"'); - expect(mockCore.info).toHaveBeenCalledWith("✓ Stored temporary_id mapping: aw_9f11121ed7df -> draft-item-temp"); }); it("creates draft issue with temporary_id (with # prefix) and strips prefix", async () => { @@ -608,8 +607,8 @@ describe("updateProject", () => { await updateProject(output, temporaryIdMap); expect(getOutput("temporary-id")).toBe("aw_abc123def456"); - expect(temporaryIdMap.get("aw_abc123def456")).toEqual({ draftItemId: "draft-item-hash" }); - expect(mockCore.info).toHaveBeenCalledWith("✓ Stored temporary_id mapping: aw_abc123def456 -> draft-item-hash"); + // Note: temporaryIdMap is no longer mutated by updateProject - the handler manager stores mappings based on return value + expect(mockCore.info).toHaveBeenCalledWith('✓ Created new draft issue "Draft with hash prefix"'); }); it("updates draft issue via draft_issue_id using temporary ID map", async () => { @@ -776,8 +775,8 @@ describe("updateProject", () => { expect(result).toBeDefined(); expect(result.temporaryId).toBe("draft-1"); expect(result.draftItemId).toBe("draft-item-friendly"); - expect(temporaryIdMap.get("draft-1")).toEqual({ draftItemId: "draft-item-friendly" }); - expect(mockCore.info).toHaveBeenCalledWith("✓ Stored temporary_id mapping: draft-1 -> draft-item-friendly"); + // Note: temporaryIdMap is no longer mutated by updateProject - the handler manager stores mappings based on return value + expect(mockCore.info).toHaveBeenCalledWith('✓ Created new draft issue "User Friendly Draft"'); }); it("allows user-friendly draft_issue_id like 'draft-1' when updating draft", async () => { diff --git a/docs/src/content/docs/agent-factory-status.mdx b/docs/src/content/docs/agent-factory-status.mdx index 950a8d5f72..946cd1956e 100644 --- a/docs/src/content/docs/agent-factory-status.mdx +++ b/docs/src/content/docs/agent-factory-status.mdx @@ -132,7 +132,7 @@ These are experimental agentic workflows used by the GitHub Next team to learn, | [Smoke Codex](https://github.com/github/gh-aw/blob/main/.github/workflows/smoke-codex.md) | codex | [![Smoke Codex](https://github.com/github/gh-aw/actions/workflows/smoke-codex.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/smoke-codex.lock.yml) | - | - | | [Smoke Copilot](https://github.com/github/gh-aw/blob/main/.github/workflows/smoke-copilot.md) | copilot | [![Smoke Copilot](https://github.com/github/gh-aw/actions/workflows/smoke-copilot.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/smoke-copilot.lock.yml) | - | - | | [Smoke OpenCode](https://github.com/github/gh-aw/blob/main/.github/workflows/smoke-opencode.md) | copilot | [![Smoke OpenCode](https://github.com/github/gh-aw/actions/workflows/smoke-opencode.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/smoke-opencode.lock.yml) | - | - | -| [Smoke Project](https://github.com/github/gh-aw/blob/main/.github/workflows/smoke-project.md) | codex | [![Smoke Project](https://github.com/github/gh-aw/actions/workflows/smoke-project.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/smoke-project.lock.yml) | - | - | +| [Smoke Project](https://github.com/github/gh-aw/blob/main/.github/workflows/smoke-project.md) | copilot | [![Smoke Project](https://github.com/github/gh-aw/actions/workflows/smoke-project.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/smoke-project.lock.yml) | - | - | | [Stale Repository Identifier](https://github.com/github/gh-aw/blob/main/.github/workflows/stale-repo-identifier.md) | copilot | [![Stale Repository Identifier](https://github.com/github/gh-aw/actions/workflows/stale-repo-identifier.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/stale-repo-identifier.lock.yml) | - | - | | [Static Analysis Report](https://github.com/github/gh-aw/blob/main/.github/workflows/static-analysis-report.md) | claude | [![Static Analysis Report](https://github.com/github/gh-aw/actions/workflows/static-analysis-report.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/static-analysis-report.lock.yml) | - | - | | [Step Name Alignment](https://github.com/github/gh-aw/blob/main/.github/workflows/step-name-alignment.md) | claude | [![Step Name Alignment](https://github.com/github/gh-aw/actions/workflows/step-name-alignment.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/step-name-alignment.lock.yml) | `daily` | - |