diff --git a/actions/setup/js/safe_output_project_handler_manager.cjs b/actions/setup/js/safe_output_project_handler_manager.cjs index 5fd80ec3860..834379563d4 100644 --- a/actions/setup/js/safe_output_project_handler_manager.cjs +++ b/actions/setup/js/safe_output_project_handler_manager.cjs @@ -111,6 +111,9 @@ async function processMessages(messageHandlers, messages) { const results = []; let processedCount = 0; + // Build a temporary project ID map as we process create_project messages + const temporaryProjectMap = new Map(); + core.info(`Processing ${messages.length} project-related message(s)...`); // Process messages in order of appearance @@ -136,8 +139,8 @@ async function processMessages(messageHandlers, messages) { core.info(`Processing message ${i + 1}/${messages.length}: ${messageType}`); // Call the message handler with the individual message - // Note: Project handlers don't use temporary ID resolution - const result = await messageHandler(message, {}); + // Pass the temporary project map for resolution + const result = await messageHandler(message, temporaryProjectMap); // Check if the handler explicitly returned a failure if (result && result.success === false) { @@ -152,6 +155,12 @@ async function processMessages(messageHandlers, messages) { continue; } + // If this was a create_project, store the mapping + if (messageType === "create_project" && result && result.projectUrl && message.temporary_id) { + temporaryProjectMap.set(message.temporary_id.toLowerCase(), result.projectUrl); + core.info(`✓ Stored project mapping: ${message.temporary_id} -> ${result.projectUrl}`); + } + results.push({ type: messageType, messageIndex: i, diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index 6ad14f74653..5e3425b9f4a 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -341,11 +341,43 @@ function createHandlers(server, appendSafeOutput, config = {}) { }; }; + /** + * Handler for create_project tool + * Auto-generates a temporary ID if not provided and returns it to the agent + */ + const createProjectHandler = args => { + const entry = { ...(args || {}), type: "create_project" }; + + // Generate temporary_id if not provided + if (!entry.temporary_id) { + entry.temporary_id = "aw_" + crypto.randomBytes(6).toString("hex"); + server.debug(`Auto-generated temporary_id for create_project: ${entry.temporary_id}`); + } + + // Append to safe outputs + appendSafeOutput(entry); + + // Return the temporary_id to the agent so it can reference this project + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "success", + temporary_id: entry.temporary_id, + project: `#${entry.temporary_id}`, + }), + }, + ], + }; + }; + return { defaultHandler, uploadAssetHandler, createPullRequestHandler, pushToPullRequestBranchHandler, + createProjectHandler, }; } diff --git a/actions/setup/js/safe_outputs_tools.json b/actions/setup/js/safe_outputs_tools.json index fc4ac40ca5f..124796dbb6b 100644 --- a/actions/setup/js/safe_outputs_tools.json +++ b/actions/setup/js/safe_outputs_tools.json @@ -607,8 +607,8 @@ "properties": { "project": { "type": "string", - "pattern": "^https://github\\.com/(orgs|users)/[^/]+/projects/\\d+$", - "description": "Full GitHub project URL (e.g., 'https://github.com/orgs/myorg/projects/42' or 'https://github.com/users/username/projects/5'). Project names or numbers alone are NOT accepted." + "pattern": "^(https://github\\.com/(orgs|users)/[^/]+/projects/\\d+|#?aw_[0-9a-f]{12})$", + "description": "Full GitHub project URL (e.g., 'https://github.com/orgs/myorg/projects/42' or 'https://github.com/users/username/projects/5'), or a temporary project ID from a recent create_project call (e.g., '#aw_abc123def456' or 'aw_abc123def456'). Project names or numbers alone are NOT accepted." }, "operation": { "type": "string", @@ -729,7 +729,7 @@ }, { "name": "create_project", - "description": "Create a new empty GitHub Projects v2 board. Use this to create a project board for organizing work. The project is created empty and can be populated with issues and custom fields after creation.", + "description": "Create a new empty GitHub Projects v2 board. Use this to create a project board for organizing work. The project is created empty and can be populated with issues and custom fields after creation. Returns a temporary project ID that can be used in subsequent update_project calls before the actual project URL is available.", "inputSchema": { "type": "object", "required": [], @@ -751,6 +751,11 @@ "type": "string", "pattern": "^https://github\\\\.com/[^/]+/[^/]+/issues/\\\\d+$", "description": "Optional GitHub issue URL to add as the first item to the project (e.g., 'https://github.com/owner/repo/issues/123')." + }, + "temporary_id": { + "type": "string", + "pattern": "^aw_[0-9a-f]{12}$", + "description": "Optional temporary identifier for referencing this project before it's created. Format: 'aw_' followed by 12 hex characters (e.g., 'aw_abc123def456'). If not provided, one will be auto-generated and returned in the response. Use '#aw_ID' in update_project to reference this project by its temporary_id." } }, "additionalProperties": false diff --git a/actions/setup/js/safe_outputs_tools_loader.cjs b/actions/setup/js/safe_outputs_tools_loader.cjs index 6d463a410c6..2f6f126dbbf 100644 --- a/actions/setup/js/safe_outputs_tools_loader.cjs +++ b/actions/setup/js/safe_outputs_tools_loader.cjs @@ -46,6 +46,7 @@ function attachHandlers(tools, handlers) { create_pull_request: handlers.createPullRequestHandler, push_to_pull_request_branch: handlers.pushToPullRequestBranchHandler, upload_asset: handlers.uploadAssetHandler, + create_project: handlers.createProjectHandler, }; tools.forEach(tool => { diff --git a/actions/setup/js/update_project.cjs b/actions/setup/js/update_project.cjs index 425358495e0..41804e1d69c 100644 --- a/actions/setup/js/update_project.cjs +++ b/actions/setup/js/update_project.cjs @@ -978,10 +978,10 @@ async function main(config = {}) { /** * Message handler function that processes a single update_project message * @param {Object} message - The update_project message to process - * @param {Object} resolvedTemporaryIds - Map of temporary IDs (unused for update_project) + * @param {Map} temporaryProjectMap - Map of temporary project IDs to actual URLs * @returns {Promise} Result with success/error status */ - return async function handleUpdateProject(message, resolvedTemporaryIds) { + return async function handleUpdateProject(message, temporaryProjectMap) { // Check max limit if (processedCount >= maxCount) { core.warning(`Skipping update_project: max count of ${maxCount} reached`); @@ -994,15 +994,38 @@ async function main(config = {}) { processedCount++; try { + // Resolve temporary project ID if present + let effectiveProjectUrl = message.project; + + if (effectiveProjectUrl && typeof effectiveProjectUrl === "string") { + // Strip # prefix if present + const projectStr = effectiveProjectUrl.trim(); + const projectWithoutHash = projectStr.startsWith("#") ? projectStr.substring(1) : projectStr; + + // Check if it's a temporary ID (aw_XXXXXXXXXXXX) + if (/^aw_[0-9a-f]{12}$/i.test(projectWithoutHash)) { + const resolved = temporaryProjectMap.get(projectWithoutHash.toLowerCase()); + if (resolved) { + core.info(`Resolved temporary project ID ${projectStr} to ${resolved}`); + effectiveProjectUrl = resolved; + } else { + throw new Error(`Temporary project ID '${projectStr}' not found. Ensure create_project was called before update_project.`); + } + } + } + + // Create effective message with resolved project URL + const resolvedMessage = { ...message, project: effectiveProjectUrl }; + // Store the first project URL for view creation - if (!firstProjectUrl && message.project) { - firstProjectUrl = message.project; + if (!firstProjectUrl && effectiveProjectUrl) { + firstProjectUrl = effectiveProjectUrl; } // Create configured fields once before processing the first message // This ensures campaign-required fields exist even if the agent doesn't explicitly emit operation=create_fields. if (!fieldsCreated && configuredFieldDefinitions.length > 0 && firstProjectUrl) { - const operation = typeof message?.operation === "string" ? message.operation : ""; + const operation = typeof resolvedMessage?.operation === "string" ? resolvedMessage.operation : ""; if (operation !== "create_fields") { fieldsCreated = true; core.info(`Creating ${configuredFieldDefinitions.length} configured field(s) on project: ${firstProjectUrl}`); @@ -1027,7 +1050,7 @@ async function main(config = {}) { } // If the agent requests create_fields but omitted field_definitions, fall back to configured definitions. - const effectiveMessage = { ...message }; + const effectiveMessage = { ...resolvedMessage }; if (effectiveMessage?.operation === "create_fields" && !effectiveMessage.field_definitions && configuredFieldDefinitions.length > 0) { effectiveMessage.field_definitions = configuredFieldDefinitions; }