diff --git a/actions/setup/js/safe_output_helpers.cjs b/actions/setup/js/safe_output_helpers.cjs index dbd1f98d9c..2a925df020 100644 --- a/actions/setup/js/safe_output_helpers.cjs +++ b/actions/setup/js/safe_output_helpers.cjs @@ -228,8 +228,38 @@ function resolveTarget(params) { }; } +/** + * Load custom safe output job types from environment variable + * These are job names defined in safe-outputs.jobs that are processed by custom jobs + * @returns {Set} Set of custom safe output job type names + */ +function loadCustomSafeOutputJobTypes() { + const safeOutputJobsEnv = process.env.GH_AW_SAFE_OUTPUT_JOBS; + if (!safeOutputJobsEnv) { + return new Set(); + } + + try { + const safeOutputJobs = JSON.parse(safeOutputJobsEnv); + // The environment variable is a map of job names to output keys + // We need the job names (keys) as the message types to ignore + const jobTypes = Object.keys(safeOutputJobs); + if (typeof core !== "undefined") { + core.debug(`Loaded ${jobTypes.length} custom safe output job type(s): ${jobTypes.join(", ")}`); + } + return new Set(jobTypes); + } catch (error) { + if (typeof core !== "undefined") { + const { getErrorMessage } = require("./error_helpers.cjs"); + core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_JOBS: ${getErrorMessage(error)}`); + } + return new Set(); + } +} + module.exports = { parseAllowedItems, parseMaxCount, resolveTarget, + loadCustomSafeOutputJobTypes, }; diff --git a/actions/setup/js/safe_output_helpers.test.cjs b/actions/setup/js/safe_output_helpers.test.cjs index 1789eefa29..da078938ce 100644 --- a/actions/setup/js/safe_output_helpers.test.cjs +++ b/actions/setup/js/safe_output_helpers.test.cjs @@ -453,4 +453,53 @@ describe("safe_output_helpers", () => { }); }); }); + + describe("loadCustomSafeOutputJobTypes", () => { + beforeEach(() => { + // Clean up environment variables + delete process.env.GH_AW_SAFE_OUTPUT_JOBS; + }); + + it("should return empty set when GH_AW_SAFE_OUTPUT_JOBS is not set", () => { + const result = helpers.loadCustomSafeOutputJobTypes(); + + expect(result).toBeInstanceOf(Set); + expect(result.size).toBe(0); + }); + + it("should parse and return custom job types from GH_AW_SAFE_OUTPUT_JOBS", () => { + process.env.GH_AW_SAFE_OUTPUT_JOBS = JSON.stringify({ + notion_add_comment: "comment_url", + slack_post_message: "message_url", + custom_job: "output_url", + }); + + const result = helpers.loadCustomSafeOutputJobTypes(); + + expect(result).toBeInstanceOf(Set); + expect(result.size).toBe(3); + expect(result.has("notion_add_comment")).toBe(true); + expect(result.has("slack_post_message")).toBe(true); + expect(result.has("custom_job")).toBe(true); + }); + + it("should return empty set when GH_AW_SAFE_OUTPUT_JOBS is invalid JSON", () => { + process.env.GH_AW_SAFE_OUTPUT_JOBS = "invalid json"; + + const result = helpers.loadCustomSafeOutputJobTypes(); + + expect(result).toBeInstanceOf(Set); + expect(result.size).toBe(0); + // Note: Warning is logged but we don't test for it since core is not mocked in this test file + }); + + it("should handle empty object in GH_AW_SAFE_OUTPUT_JOBS", () => { + process.env.GH_AW_SAFE_OUTPUT_JOBS = JSON.stringify({}); + + const result = helpers.loadCustomSafeOutputJobTypes(); + + expect(result).toBeInstanceOf(Set); + expect(result.size).toBe(0); + }); + }); }); diff --git a/actions/setup/js/safe_output_project_handler_manager.cjs b/actions/setup/js/safe_output_project_handler_manager.cjs index 8e94e15b59..32a0fcc140 100644 --- a/actions/setup/js/safe_output_project_handler_manager.cjs +++ b/actions/setup/js/safe_output_project_handler_manager.cjs @@ -17,6 +17,7 @@ const { loadAgentOutput } = require("./load_agent_output.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { writeSafeOutputSummaries } = require("./safe_output_summary.cjs"); const { loadTemporaryIdMap } = require("./temporary_id.cjs"); +const { loadCustomSafeOutputJobTypes } = require("./safe_output_helpers.cjs"); /** * Handler map configuration for project-related safe outputs @@ -122,6 +123,9 @@ async function processMessages(messageHandlers, messages) { core.info(`Loaded temporary ID map with ${temporaryIdMap.size} entry(ies)`); } + // Load custom safe output job types that are processed by dedicated custom jobs + const customSafeOutputJobTypes = loadCustomSafeOutputJobTypes(); + core.info(`Processing ${messages.length} project-related message(s)...`); // Process messages in order of appearance @@ -137,6 +141,13 @@ async function processMessages(messageHandlers, messages) { const messageHandler = messageHandlers.get(messageType); if (!messageHandler) { + // Check if this message type is a custom safe output job + if (customSafeOutputJobTypes.has(messageType)) { + // Silently skip - this is handled by a custom safe output job + core.debug(`Message ${i + 1} (${messageType}) will be handled by custom safe output job`); + continue; + } + // Skip messages that are not project-related // These should be handled by other steps (main handler manager or standalone steps) core.debug(`Message ${i + 1} (${messageType}) is not a project-related type - skipping`); diff --git a/actions/setup/js/safe_output_unified_handler_manager.cjs b/actions/setup/js/safe_output_unified_handler_manager.cjs index 2b6327158e..aa1a1c87c2 100644 --- a/actions/setup/js/safe_output_unified_handler_manager.cjs +++ b/actions/setup/js/safe_output_unified_handler_manager.cjs @@ -22,6 +22,7 @@ const { setCollectedMissings } = require("./missing_messages_helper.cjs"); const { writeSafeOutputSummaries } = require("./safe_output_summary.cjs"); const { getIssuesToAssignCopilot } = require("./create_issue.cjs"); const { getCampaignLabelsFromEnv } = require("./campaign_labels.cjs"); +const { loadCustomSafeOutputJobTypes } = require("./safe_output_helpers.cjs"); /** * Merge labels with trimming + case-insensitive de-duplication. @@ -363,6 +364,9 @@ async function processMessages(messageHandlers, messages, projectOctokit = null) // Collect missing_tool and missing_data messages first const missings = collectMissingMessages(messages); + // Load custom safe output job types that are processed by dedicated custom jobs + const customSafeOutputJobTypes = loadCustomSafeOutputJobTypes(); + // Initialize unified temporary ID map // This will be populated by handlers as they create entities with temporary IDs // Stores both issue/PR references ({repo, number}) and project URLs ({projectUrl}) @@ -418,6 +422,20 @@ async function processMessages(messageHandlers, messages, projectOctokit = null) continue; } + // Check if this message type is a custom safe output job + if (customSafeOutputJobTypes.has(messageType)) { + // Silently skip - this is handled by a custom safe output job + core.debug(`Message ${i + 1} (${messageType}) will be handled by custom safe output job`); + results.push({ + type: messageType, + messageIndex: i, + success: false, + skipped: true, + reason: "Handled by custom safe output job", + }); + continue; + } + // Unknown message type - warn the user core.warning( `⚠️ No handler loaded for message type '${messageType}' (message ${i + 1}/${messages.length}). The message will be skipped. This may happen if the safe output type is not configured in the workflow's safe-outputs section.` diff --git a/docs/src/content/docs/agent-factory-status.mdx b/docs/src/content/docs/agent-factory-status.mdx index 8ba7e7f401..68afe5f09e 100644 --- a/docs/src/content/docs/agent-factory-status.mdx +++ b/docs/src/content/docs/agent-factory-status.mdx @@ -82,15 +82,7 @@ These are experimental agentic workflows used by the GitHub Next team to learn, | [Example: Custom Error Patterns](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/example-custom-error-patterns.md) | copilot | [![Example: Custom Error Patterns](https://github.com/githubnext/gh-aw/actions/workflows/example-custom-error-patterns.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/example-custom-error-patterns.lock.yml) | - | - | | [Example: Properly Provisioned Permissions](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/example-permissions-warning.md) | copilot | [![Example: Properly Provisioned Permissions](https://github.com/githubnext/gh-aw/actions/workflows/example-permissions-warning.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/example-permissions-warning.lock.yml) | - | - | | [Firewall Test Agent](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/firewall.md) | copilot | [![Firewall Test Agent](https://github.com/githubnext/gh-aw/actions/workflows/firewall.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/firewall.lock.yml) | - | - | -<<<<<<< HEAD | [Functional Pragmatist](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/functional-pragmatist.md) | copilot | [![Functional Pragmatist](https://github.com/githubnext/gh-aw/actions/workflows/functional-pragmatist.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/functional-pragmatist.lock.yml) | `0 9 * * 2,4` | - | -======= -<<<<<<< HEAD -| [Functional Enhancer](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/functional-enhancer.md) | claude | [![Functional Enhancer](https://github.com/githubnext/gh-aw/actions/workflows/functional-enhancer.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/functional-enhancer.lock.yml) | `0 9 * * 2,4` | - | -======= -| [Functional Pragmatist](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/functional-programming-enhancer.md) | claude | [![Functional Pragmatist](https://github.com/githubnext/gh-aw/actions/workflows/functional-programming-enhancer.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/functional-programming-enhancer.lock.yml) | `0 9 * * 2,4` | - | ->>>>>>> ba904f257b69bfcc9738b01ca4c61995b23506a4 ->>>>>>> origin/main | [GitHub MCP Remote Server Tools Report Generator](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/github-mcp-tools-report.md) | claude | [![GitHub MCP Remote Server Tools Report Generator](https://github.com/githubnext/gh-aw/actions/workflows/github-mcp-tools-report.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/github-mcp-tools-report.lock.yml) | - | - | | [GitHub MCP Structural Analysis](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/github-mcp-structural-analysis.md) | claude | [![GitHub MCP Structural Analysis](https://github.com/githubnext/gh-aw/actions/workflows/github-mcp-structural-analysis.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/github-mcp-structural-analysis.lock.yml) | `0 11 * * 1-5` | - | | [GitHub Remote MCP Authentication Test](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/github-remote-mcp-auth-test.md) | copilot | [![GitHub Remote MCP Authentication Test](https://github.com/githubnext/gh-aw/actions/workflows/github-remote-mcp-auth-test.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/github-remote-mcp-auth-test.lock.yml) | - | - | diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index 74d3fcaca7..a9163ca45e 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -2157,6 +2157,13 @@ safe-outputs: # (optional) github-token: "${{ secrets.GITHUB_TOKEN }}" + # Default project URL for update-project operations. When specified, safe output + # messages can omit the project field and will use this URL by default. Must be a + # valid GitHub Projects v2 URL. Overridden by explicit project field in safe + # output messages. + # (optional) + project: "example-value" + # Optional array of project views to create. Each view must have a name and # layout. Views are created during project setup. # (optional) @@ -2349,6 +2356,13 @@ safe-outputs: # (optional) github-token: "${{ secrets.GITHUB_TOKEN }}" + # Default project URL for status update operations. When specified, safe output + # messages can omit the project field and will use this URL by default. Must be a + # valid GitHub Projects v2 URL. Overridden by explicit project field in safe + # output messages. + # (optional) + project: "example-value" + # Option 2: Enable project status updates with default configuration (max=1) create-project-status-update: null