Skip to content
Merged
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
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) {
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
);

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();
});
});
4 changes: 2 additions & 2 deletions pkg/workflow/action_pins_logging_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
{
Expand Down
6 changes: 3 additions & 3 deletions pkg/workflow/action_pins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
36 changes: 23 additions & 13 deletions pkg/workflow/data/action_pins.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,20 @@
"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": {
"repo": "actions/github-script",
"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",
Expand All @@ -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": {
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
Loading