Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions actions/setup/js/create_project.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, {repo?: string, number?: number, projectUrl?: string}>} 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<Object>} 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`);
Expand Down
5 changes: 2 additions & 3 deletions actions/setup/js/create_project_status_update.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, {repo?: string, number?: number, projectUrl?: string}>} 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<Object>} 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`);
Expand Down
31 changes: 23 additions & 8 deletions actions/setup/js/safe_output_unified_handler_manager.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions actions/setup/js/temporary_id.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>} temporaryIdMap - Map of temporary ID to resolved value (supports legacy formats)
* @param {Map<string, any>|Object<string, any>} temporaryIdMap - Map or object of temporary ID to resolved value
* @returns {{resolved: RepoIssuePair|null, wasTemporaryId: boolean, errorMessage: string|null}}
*/
function resolveIssueNumber(value, temporaryIdMap) {
Expand All @@ -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}` : "";
Expand Down
35 changes: 15 additions & 20 deletions actions/setup/js/update_project.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -387,11 +387,11 @@ async function findExistingDraftByTitle(github, projectId, targetTitle) {
/**
* Update a GitHub Project v2
* @param {any} output - Safe output configuration
* @param {Map<string, any>} 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<void|{temporaryId?: string, draftItemId?: string}>} 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
Expand Down Expand Up @@ -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}`);
Expand Down Expand Up @@ -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}`);
}
}
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<string, {repo?: string, number?: number, projectUrl?: string, draftItemId?: string}>} 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<Object>} 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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
13 changes: 6 additions & 7 deletions actions/setup/js/update_project.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/agent-factory-status.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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` | - |
Expand Down