Skip to content
Merged
17 changes: 13 additions & 4 deletions actions/setup/js/create_project.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -326,11 +326,11 @@ 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 {Map<string, {repo?: string, number?: number, projectUrl?: string}>|null} temporaryIdMap - Unified map of temporary IDs
* @returns {Promise<Object>} Result with success/error status
*/
return async function handleCreateProject(message, temporaryIdMap, resolvedTemporaryIds = {}) {
return async function handleCreateProject(message, resolvedTemporaryIds = {}, temporaryIdMap = null) {
// Check max limit
if (processedCount >= maxCount) {
core.warning(`Skipping create_project: max count of ${maxCount} reached`);
Expand Down Expand Up @@ -370,8 +370,9 @@ async function main(config = {}, githubClient = null) {

// Check if it's a valid temporary ID
if (isTemporaryId(tempIdWithoutHash)) {
// Look up in the unified temporaryIdMap
const resolved = temporaryIdMap.get(normalizeTemporaryId(tempIdWithoutHash));
// Look up in the unified temporaryIdMap (Map) or resolvedTemporaryIds (plain object)
const normalizedKey = normalizeTemporaryId(tempIdWithoutHash);
const resolved = temporaryIdMap instanceof Map ? temporaryIdMap.get(normalizedKey) : resolvedTemporaryIds[normalizedKey];

if (resolved && resolved.repo && resolved.number) {
// Build the proper GitHub issue URL
Expand Down Expand Up @@ -459,6 +460,13 @@ async function main(config = {}, githubClient = null) {

core.info(`✓ Successfully created project: ${projectInfo.projectUrl}`);

// Store temporary ID mapping so subsequent operations can reference this project
const normalizedTempId = normalizeTemporaryId(temporaryId ?? "");
if (temporaryIdMap instanceof Map) {
temporaryIdMap.set(normalizedTempId, { projectUrl: projectInfo.projectUrl });
}
core.info(`Stored temporary ID mapping: ${temporaryId} -> ${projectInfo.projectUrl}`);

// Create configured views if any
if (configuredViews.length > 0) {
core.info(`Creating ${configuredViews.length} configured view(s) on project: ${projectInfo.projectUrl}`);
Expand Down Expand Up @@ -488,6 +496,7 @@ async function main(config = {}, githubClient = null) {
projectTitle: projectInfo.projectTitle,
projectUrl: projectInfo.projectUrl,
itemId: projectInfo.itemId,
temporaryId,
};
} catch (err) {
// prettier-ignore
Expand Down
26 changes: 23 additions & 3 deletions actions/setup/js/create_project_status_update.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const { loadAgentOutput } = require("./load_agent_output.cjs");
const { getErrorMessage } = require("./error_helpers.cjs");
const { sanitizeContent } = require("./sanitize_content.cjs");
const { logStagedPreviewInfo } = require("./staged_preview.cjs");
const { isTemporaryId, normalizeTemporaryId } = require("./temporary_id.cjs");
const { ERR_CONFIG, ERR_NOT_FOUND, ERR_PARSE, ERR_VALIDATION } = require("./error_codes.cjs");

/**
Expand Down Expand Up @@ -308,11 +309,11 @@ 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 {Map<string, {repo?: string, number?: number, projectUrl?: string}>|null} temporaryIdMap - Unified map of temporary IDs
* @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 = {}, temporaryIdMap = null) {
// Check if we've hit the max limit
if (processedCount >= maxCount) {
core.warning(`Skipping create-project-status-update: max count of ${maxCount} reached`);
Expand All @@ -328,7 +329,8 @@ async function main(config = {}, githubClient = null) {

// Validate that project field is explicitly provided in the message
// The project field is required in agent output messages and must be a full GitHub project URL
const effectiveProjectUrl = output.project;
// or a temporary project ID (e.g., aw_abc123 or #aw_abc123) from create_project
let effectiveProjectUrl = output.project;

if (!effectiveProjectUrl || typeof effectiveProjectUrl !== "string" || effectiveProjectUrl.trim() === "") {
core.error('Missing required "project" field. The agent must explicitly include the project URL in the output message: {"type": "create_project_status_update", "project": "https://github.com/orgs/myorg/projects/42", "body": "..."}');
Expand All @@ -338,6 +340,24 @@ async function main(config = {}, githubClient = null) {
};
}

// Resolve temporary project ID if present
const projectStr = effectiveProjectUrl.trim();
const projectWithoutHash = projectStr.startsWith("#") ? projectStr.substring(1) : projectStr;
if (isTemporaryId(projectWithoutHash)) {
const normalizedId = normalizeTemporaryId(projectWithoutHash);
const resolved = temporaryIdMap instanceof Map ? temporaryIdMap.get(normalizedId) : resolvedTemporaryIds[normalizedId];
if (resolved && typeof resolved === "object" && "projectUrl" in resolved && resolved.projectUrl) {
Comment thread
pelikhan marked this conversation as resolved.
core.info(`Resolved temporary project ID ${projectStr} to ${resolved.projectUrl}`);
effectiveProjectUrl = resolved.projectUrl;
} else {
core.error(`Temporary project ID '${projectStr}' not found. Ensure create_project was called before create_project_status_update.`);
return {
success: false,
error: `${ERR_NOT_FOUND}: Temporary project ID '${projectStr}' not found. Ensure create_project was called before create_project_status_update.`,
};
}
}

if (!output.body) {
core.error("Missing required field: body (status update content)");
return {
Expand Down
113 changes: 113 additions & 0 deletions actions/setup/js/create_project_status_update.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -600,4 +600,117 @@ describe("create_project_status_update", () => {
// Cleanup
delete process.env.GH_AW_PROJECT_URL;
});

it("should resolve a temporary project ID from temporaryIdMap", async () => {
mockGithub.graphql
.mockResolvedValueOnce({
organization: {
projectV2: {
id: "PVT_test123",
number: 42,
title: "Test Project",
url: "https://github.com/orgs/test-org/projects/42",
},
},
})
.mockResolvedValueOnce({
createProjectV2StatusUpdate: {
statusUpdate: {
id: "PVTSU_test123",
body: "Test status update",
bodyHTML: "<p>Test status update</p>",
startDate: "2025-01-01",
targetDate: "2025-12-31",
status: "ON_TRACK",
createdAt: "2025-01-06T12:00:00Z",
},
},
});

const handler = await main({ max: 10 });

const temporaryIdMap = new Map();
temporaryIdMap.set("aw_abc12345", { projectUrl: "https://github.com/orgs/test-org/projects/42" });

const result = await handler(
{
project: "aw_abc12345",
body: "Test status update",
status: "ON_TRACK",
start_date: "2025-01-01",
target_date: "2025-12-31",
},
Object.fromEntries(temporaryIdMap),
temporaryIdMap
);
Comment thread
mnkiefer marked this conversation as resolved.

expect(result.success).toBe(true);
expect(result.status_update_id).toBe("PVTSU_test123");
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Resolved temporary project ID aw_abc12345"));
});

it("should resolve a temporary project ID with hash prefix from temporaryIdMap", async () => {
mockGithub.graphql
.mockResolvedValueOnce({
organization: {
projectV2: {
id: "PVT_test123",
number: 42,
title: "Test Project",
url: "https://github.com/orgs/test-org/projects/42",
},
},
})
.mockResolvedValueOnce({
createProjectV2StatusUpdate: {
statusUpdate: {
id: "PVTSU_test123",
body: "Test status update",
bodyHTML: "<p>Test status update</p>",
startDate: "2025-01-01",
targetDate: "2025-12-31",
status: "ON_TRACK",
createdAt: "2025-01-06T12:00:00Z",
},
},
});

const handler = await main({ max: 10 });

const temporaryIdMap = new Map();
temporaryIdMap.set("aw_abc12345", { projectUrl: "https://github.com/orgs/test-org/projects/42" });

const result = await handler(
{
project: "#aw_abc12345",
body: "Test status update",
},
Object.fromEntries(temporaryIdMap),
temporaryIdMap
);

expect(result.success).toBe(true);
expect(result.status_update_id).toBe("PVTSU_test123");
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Resolved temporary project ID #aw_abc12345"));
});

it("should return error when temporary project ID is not found in temporaryIdMap", async () => {
const handler = await main({ max: 10 });

const temporaryIdMap = new Map();

const result = await handler(
{
project: "aw_notfound",
body: "Test status update",
},
Object.fromEntries(temporaryIdMap),
temporaryIdMap
);

expect(result.success).toBe(false);
expect(result.error).toContain("aw_notfound");
expect(mockCore.error).toHaveBeenCalledWith(expect.stringContaining("aw_notfound"));
expect(mockGithub.graphql).not.toHaveBeenCalled();
});
});
Loading