diff --git a/actions/setup/js/create_project.cjs b/actions/setup/js/create_project.cjs index ebe8f461785..749ca6243c5 100644 --- a/actions/setup/js/create_project.cjs +++ b/actions/setup/js/create_project.cjs @@ -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} temporaryIdMap - Unified map of temporary IDs * @param {Object} resolvedTemporaryIds - Plain object version of temporaryIdMap for backward compatibility + * @param {Map|null} temporaryIdMap - Unified map of temporary IDs * @returns {Promise} 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`); @@ -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 @@ -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}`); @@ -488,6 +496,7 @@ async function main(config = {}, githubClient = null) { projectTitle: projectInfo.projectTitle, projectUrl: projectInfo.projectUrl, itemId: projectInfo.itemId, + temporaryId, }; } catch (err) { // prettier-ignore diff --git a/actions/setup/js/create_project_status_update.cjs b/actions/setup/js/create_project_status_update.cjs index 65e6d70949c..b241020411e 100644 --- a/actions/setup/js/create_project_status_update.cjs +++ b/actions/setup/js/create_project_status_update.cjs @@ -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"); /** @@ -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} temporaryIdMap - Unified map of temporary IDs * @param {Object} resolvedTemporaryIds - Plain object version of temporaryIdMap for backward compatibility + * @param {Map|null} temporaryIdMap - Unified map of temporary IDs * @returns {Promise} 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`); @@ -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": "..."}'); @@ -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) { + 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 { diff --git a/actions/setup/js/create_project_status_update.test.cjs b/actions/setup/js/create_project_status_update.test.cjs index 59e18fe63b1..5dcac4498a8 100644 --- a/actions/setup/js/create_project_status_update.test.cjs +++ b/actions/setup/js/create_project_status_update.test.cjs @@ -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: "

Test status update

", + 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 + ); + + 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: "

Test status update

", + 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(); + }); }); diff --git a/pkg/workflow/action_pins_logging_test.go b/pkg/workflow/action_pins_logging_test.go index 2c293e0607b..d2010363fea 100644 --- a/pkg/workflow/action_pins_logging_test.go +++ b/pkg/workflow/action_pins_logging_test.go @@ -34,11 +34,11 @@ func TestActionPinResolutionWithMismatchedVersions(t *testing.T) { expectMismatch: true, }, { - name: "setup-dotnet v5 resolves to v5.1.0 pin but comment shows v5", + name: "setup-dotnet v5 resolves to v5.2.0 pin but comment shows v5", repo: "actions/setup-dotnet", requestedVer: "v5", expectedCommentVer: "v5", // Comment shows requested version - fallbackPinVer: "v5.1.0", + fallbackPinVer: "v5.2.0", expectMismatch: true, }, { diff --git a/pkg/workflow/action_pins_test.go b/pkg/workflow/action_pins_test.go index 877c2488cac..62ab46ccf12 100644 --- a/pkg/workflow/action_pins_test.go +++ b/pkg/workflow/action_pins_test.go @@ -297,9 +297,9 @@ func TestApplyActionPinToStep(t *testing.T) { func TestGetActionPinsSorting(t *testing.T) { pins := getActionPins() - // Verify we got all the pins (32 as of March 2026) - if len(pins) != 32 { - t.Errorf("getActionPins() returned %d pins, expected 32", len(pins)) + // Verify we got all the pins (34 as of March 2026) + if len(pins) != 34 { + t.Errorf("getActionPins() returned %d pins, expected 34", len(pins)) } // Verify they are sorted by version (descending) then by repository name (ascending) diff --git a/pkg/workflow/data/action_pins.json b/pkg/workflow/data/action_pins.json index 36a2a2ba46d..b5b7efc0af9 100644 --- a/pkg/workflow/data/action_pins.json +++ b/pkg/workflow/data/action_pins.json @@ -35,9 +35,9 @@ "version": "v3.0.0-beta.2", "sha": "bf559f85448f9380bcfa2899dbdc01eb5b37be3a" }, - "actions/download-artifact@v8.0.0": { + "actions/download-artifact@v8": { "repo": "actions/download-artifact", - "version": "v8.0.0", + "version": "v8", "sha": "70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3" }, "actions/github-script@v8": { @@ -45,10 +45,10 @@ "version": "v8", "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd" }, - "actions/setup-dotnet@v5.1.0": { + "actions/setup-dotnet@v5.2.0": { "repo": "actions/setup-dotnet", - "version": "v5.1.0", - "sha": "baa11fbfe1d6520db94683bd5c7a3818018e4309" + "version": "v5.2.0", + "sha": "c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7" }, "actions/setup-go@v6.3.0": { "repo": "actions/setup-go", @@ -75,9 +75,9 @@ "version": "v6.2.0", "sha": "a309ff8b426b58ec0e2a45f0f869d46889d02405" }, - "actions/upload-artifact@v7.0.0": { + "actions/upload-artifact@v7": { "repo": "actions/upload-artifact", - "version": "v7.0.0", + "version": "v7", "sha": "bbbca2ddaa5d8feaa63e36b76fdaad77386f024f" }, "anchore/sbom-action@v0.23.0": { @@ -110,6 +110,11 @@ "version": "v3.7.0", "sha": "c94ce9fb468520275223c153574b00df6fe4bcc9" }, + "docker/login-action@v4": { + "repo": "docker/login-action", + "version": "v4", + "sha": "b45d80f862d83dbcd57f89517bcf500b2ab88fb2" + }, "docker/metadata-action@v5.10.0": { "repo": "docker/metadata-action", "version": "v5.10.0", @@ -120,6 +125,11 @@ "version": "v3.12.0", "sha": "8d2750c68a42422c14e847fe6c8ac0403b4cbd6f" }, + "docker/setup-buildx-action@v4": { + "repo": "docker/setup-buildx-action", + "version": "v4", + "sha": "4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd" + }, "erlef/setup-beam@v1.21.0": { "repo": "erlef/setup-beam", "version": "v1.21.0", @@ -145,15 +155,15 @@ "version": "v2.10.3", "sha": "9cd1b7bf3f36d5a3c3b17abc3545bfb5481912ea" }, - "oven-sh/setup-bun@v2.1.2": { + "oven-sh/setup-bun@v2.1.3": { "repo": "oven-sh/setup-bun", - "version": "v2.1.2", - "sha": "3d267786b128fe76c2f16a390aa2448b815359f3" + "version": "v2.1.3", + "sha": "ecf28ddc73e819eb6fa29df6b34ef8921c743461" }, - "ruby/setup-ruby@v1.288.0": { + "ruby/setup-ruby@v1.289.0": { "repo": "ruby/setup-ruby", - "version": "v1.288.0", - "sha": "09a7688d3b55cf0e976497ff046b70949eeaccfd" + "version": "v1.289.0", + "sha": "19a43a6a2428d455dbd1b85344698725179c9d8c" }, "super-linter/super-linter@v8.5.0": { "repo": "super-linter/super-linter",