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
2 changes: 2 additions & 0 deletions .github/workflows/agentic-campaign-generator.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions .github/workflows/security-alert-burndown.campaign.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 13 additions & 3 deletions actions/setup/js/safe_output_project_handler_manager.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ async function loadHandlers(config) {
* Process project-related safe output messages
* @param {Map<string, Function>} messageHandlers - Map of type to handler function
* @param {Array<Object>} messages - Array of safe output messages
* @returns {Promise<{results: Array<Object>, processedCount: number}>} Processing results
* @returns {Promise<{results: Array<Object>, processedCount: number, temporaryProjectMap: Object}>} Processing results
*/
async function processMessages(messageHandlers, messages) {
const results = [];
Expand Down Expand Up @@ -181,7 +181,10 @@ async function processMessages(messageHandlers, messages) {
}
}

return { results, processedCount };
// Convert temporaryProjectMap to plain object for serialization
const temporaryProjectMapObj = Object.fromEntries(temporaryProjectMap);

return { results, processedCount, temporaryProjectMap: temporaryProjectMapObj };
}

/**
Expand Down Expand Up @@ -221,11 +224,17 @@ async function main() {
}

// Process messages
const { results, processedCount } = await processMessages(messageHandlers, messages);
const { results, processedCount, temporaryProjectMap } = await processMessages(messageHandlers, messages);

// Set outputs
core.setOutput("processed_count", processedCount);

// Export temporary project map as output so the regular handler manager can use it
// to resolve project URLs in text (e.g., update_issue body)
const temporaryProjectMapJson = JSON.stringify(temporaryProjectMap || {});
core.setOutput("temporary_project_map", temporaryProjectMapJson);
core.info(`Exported temporary project map with ${Object.keys(temporaryProjectMap || {}).length} mapping(s)`);

// Summary
const successCount = results.filter(r => r.success).length;
const failureCount = results.filter(r => !r.success).length;
Expand All @@ -235,6 +244,7 @@ async function main() {
core.info(`Project-related messages processed: ${processedCount}`);
core.info(`Successful: ${successCount}`);
core.info(`Failed: ${failureCount}`);
core.info(`Temporary project IDs registered: ${Object.keys(temporaryProjectMap || {}).length}`);

if (failureCount > 0) {
core.setFailed(`${failureCount} project-related message(s) failed to process`);
Expand Down
49 changes: 49 additions & 0 deletions actions/setup/js/temporary_id.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,53 @@ function serializeTemporaryIdMap(tempIdMap) {
return JSON.stringify(obj);
}

/**
* Load the temporary project map from environment variable
* @returns {Map<string, string>} Map of temporary_project_id to project URL
*/
function loadTemporaryProjectMap() {
const mapJson = process.env.GH_AW_TEMPORARY_PROJECT_MAP;
if (!mapJson || mapJson === "{}") {
return new Map();
}
try {
const mapObject = JSON.parse(mapJson);
/** @type {Map<string, string>} */
const result = new Map();

for (const [key, value] of Object.entries(mapObject)) {
const normalizedKey = normalizeTemporaryId(key);
if (typeof value === "string") {
result.set(normalizedKey, value);
}
}
return result;
} catch (error) {
if (typeof core !== "undefined") {
core.warning(`Failed to parse temporary project map: ${getErrorMessage(error)}`);
}
return new Map();
}
}

/**
* Replace temporary project ID references in text with actual project URLs
* Format: #aw_XXXXXXXXXXXX -> https://github.com/orgs/myorg/projects/123
* @param {string} text - The text to process
* @param {Map<string, string>} tempProjectMap - Map of temporary_project_id to project URL
* @returns {string} Text with temporary project IDs replaced with project URLs
*/
function replaceTemporaryProjectReferences(text, tempProjectMap) {
return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => {
const resolved = tempProjectMap.get(normalizeTemporaryId(tempId));
if (resolved !== undefined) {
return resolved;
}
// Return original if not found (it may be an issue ID)
return match;
});
}

module.exports = {
TEMPORARY_ID_PATTERN,
generateTemporaryId,
Expand All @@ -224,4 +271,6 @@ module.exports = {
resolveIssueNumber,
hasUnresolvedTemporaryIds,
serializeTemporaryIdMap,
loadTemporaryProjectMap,
replaceTemporaryProjectReferences,
};
63 changes: 63 additions & 0 deletions actions/setup/js/temporary_id.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -401,4 +401,67 @@ describe("temporary_id.cjs", () => {
expect(hasUnresolvedTemporaryIds(text, map)).toBe(true);
});
});

describe("replaceTemporaryProjectReferences", () => {
it("should replace #aw_ID with project URLs", async () => {
const { replaceTemporaryProjectReferences } = await import("./temporary_id.cjs");
const map = new Map([["aw_abc123def456", "https://github.com/orgs/myorg/projects/123"]]);
const text = "Project created: #aw_abc123def456";
expect(replaceTemporaryProjectReferences(text, map)).toBe("Project created: https://github.com/orgs/myorg/projects/123");
});

it("should handle multiple project references", async () => {
const { replaceTemporaryProjectReferences } = await import("./temporary_id.cjs");
const map = new Map([
["aw_abc123def456", "https://github.com/orgs/myorg/projects/123"],
["aw_111222333444", "https://github.com/orgs/myorg/projects/456"],
]);
const text = "See #aw_abc123def456 and #aw_111222333444";
expect(replaceTemporaryProjectReferences(text, map)).toBe("See https://github.com/orgs/myorg/projects/123 and https://github.com/orgs/myorg/projects/456");
});

it("should leave unresolved project references unchanged", async () => {
const { replaceTemporaryProjectReferences } = await import("./temporary_id.cjs");
const map = new Map([["aw_abc123def456", "https://github.com/orgs/myorg/projects/123"]]);
const text = "See #aw_unresolved";
expect(replaceTemporaryProjectReferences(text, map)).toBe("See #aw_unresolved");
});

it("should be case insensitive", async () => {
const { replaceTemporaryProjectReferences } = await import("./temporary_id.cjs");
const map = new Map([["aw_abc123def456", "https://github.com/orgs/myorg/projects/123"]]);
const text = "Project: #AW_ABC123DEF456";
expect(replaceTemporaryProjectReferences(text, map)).toBe("Project: https://github.com/orgs/myorg/projects/123");
});
});

describe("loadTemporaryProjectMap", () => {
it("should return empty map when env var is not set", async () => {
delete process.env.GH_AW_TEMPORARY_PROJECT_MAP;
const { loadTemporaryProjectMap } = await import("./temporary_id.cjs");
const map = loadTemporaryProjectMap();
expect(map.size).toBe(0);
});

it("should load project map from environment", async () => {
process.env.GH_AW_TEMPORARY_PROJECT_MAP = JSON.stringify({
aw_abc123def456: "https://github.com/orgs/myorg/projects/123",
aw_111222333444: "https://github.com/users/jdoe/projects/456",
});
const { loadTemporaryProjectMap } = await import("./temporary_id.cjs");
const map = loadTemporaryProjectMap();
expect(map.size).toBe(2);
expect(map.get("aw_abc123def456")).toBe("https://github.com/orgs/myorg/projects/123");
expect(map.get("aw_111222333444")).toBe("https://github.com/users/jdoe/projects/456");
});

it("should normalize keys to lowercase", async () => {
process.env.GH_AW_TEMPORARY_PROJECT_MAP = JSON.stringify({
AW_ABC123DEF456: "https://github.com/orgs/myorg/projects/123",
});
const { loadTemporaryProjectMap } = await import("./temporary_id.cjs");
const map = loadTemporaryProjectMap();
expect(map.get("aw_abc123def456")).toBe("https://github.com/orgs/myorg/projects/123");
});
});
});
11 changes: 10 additions & 1 deletion actions/setup/js/update_issue.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const HANDLER_TYPE = "update_issue";
const { resolveTarget } = require("./safe_output_helpers.cjs");
const { createUpdateHandlerFactory } = require("./update_handler_factory.cjs");
const { updateBody } = require("./update_pr_description_helpers.cjs");
const { loadTemporaryProjectMap, replaceTemporaryProjectReferences } = require("./temporary_id.cjs");

/**
* Execute the issue update API call
Expand All @@ -24,13 +25,21 @@ async function executeIssueUpdate(github, context, issueNumber, updateData) {
// Handle body operation (append/prepend/replace/replace-island)
// Default to "append" to add footer with AI attribution
const operation = updateData._operation || "append";
const rawBody = updateData._rawBody;
let rawBody = updateData._rawBody;

// Remove internal fields
const { _operation, _rawBody, ...apiData } = updateData;

// If we have a body, process it with the appropriate operation
if (rawBody !== undefined) {
// Load and apply temporary project URL replacements FIRST
// This resolves any temporary project IDs (e.g., #aw_abc123def456) to actual project URLs
const temporaryProjectMap = loadTemporaryProjectMap();
if (temporaryProjectMap.size > 0) {
rawBody = replaceTemporaryProjectReferences(rawBody, temporaryProjectMap);
core.debug(`Applied ${temporaryProjectMap.size} temporary project URL replacement(s)`);
}

// Fetch current issue body for all operations (needed for append/prepend/replace-island/replace)
const { data: currentIssue } = await github.rest.issues.get({
owner: context.repo.owner,
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/compiler_safe_outputs_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ func (c *Compiler) buildConsolidatedSafeOutputsJob(data *WorkflowData, mainJobNa

// Add outputs from project handler manager
outputs["process_project_safe_outputs_processed_count"] = "${{ steps.process_project_safe_outputs.outputs.processed_count }}"
outputs["process_project_safe_outputs_temporary_project_map"] = "${{ steps.process_project_safe_outputs.outputs.temporary_project_map }}"

// Add permissions for project-related types
// Note: Projects v2 cannot use GITHUB_TOKEN; it requires a PAT or GitHub App token
Expand Down
13 changes: 13 additions & 0 deletions pkg/workflow/compiler_safe_outputs_steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,19 @@ func (c *Compiler) buildHandlerManagerStep(data *WorkflowData) []string {
steps = append(steps, " env:\n")
steps = append(steps, " GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}\n")

// Check if any project-handler types are enabled
// If so, pass the temporary project map from the project handler step
hasProjectHandlerTypes := data.SafeOutputs.CreateProjects != nil ||
data.SafeOutputs.CreateProjectStatusUpdates != nil ||
data.SafeOutputs.UpdateProjects != nil ||
data.SafeOutputs.CopyProjects != nil

if hasProjectHandlerTypes {
// If project handler ran before this, pass its temporary project map
// This allows update_issue and other text-based handlers to resolve project temporary IDs
steps = append(steps, " GH_AW_TEMPORARY_PROJECT_MAP: ${{ steps.process_project_safe_outputs.outputs.temporary_project_map }}\n")
}

// Add custom safe output env vars
c.addCustomSafeOutputEnvVars(&steps, data)

Expand Down
Loading