Skip to content

Commit 984fc7a

Browse files
authored
Fix safe output handler to gracefully ignore custom safe output job types (#20114)
1 parent 5ab3d52 commit 984fc7a

3 files changed

Lines changed: 116 additions & 2 deletions

File tree

actions/setup/js/safe_output_handler_manager.cjs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const { getIssuesToAssignCopilot } = require("./create_issue.cjs");
2020
const { createReviewBuffer } = require("./pr_review_buffer.cjs");
2121
const { sanitizeContent } = require("./sanitize_content.cjs");
2222
const { createManifestLogger, ensureManifestExists, extractCreatedItemFromResult } = require("./safe_output_manifest.cjs");
23+
const { loadCustomSafeOutputJobTypes } = require("./safe_output_helpers.cjs");
2324

2425
/**
2526
* Handler map configuration
@@ -244,6 +245,13 @@ async function processMessages(messageHandlers, messages, onItemCreated = null)
244245
/** @type {Array<{type: string, error: string}>} */
245246
const codePushFailures = [];
246247

248+
// Load custom safe output job types (from GH_AW_SAFE_OUTPUT_JOBS env var)
249+
// These are processed by dedicated custom job steps, not by this handler manager
250+
const customJobTypes = loadCustomSafeOutputJobTypes();
251+
if (customJobTypes.size > 0) {
252+
core.info(`Loaded ${customJobTypes.size} custom safe output job type(s): ${[...customJobTypes].join(", ")}`);
253+
}
254+
247255
core.info(`Processing ${messages.length} message(s) in order of appearance...`);
248256

249257
// Process messages in order of appearance
@@ -287,6 +295,20 @@ async function processMessages(messageHandlers, messages, onItemCreated = null)
287295
continue;
288296
}
289297

298+
// Check if this message type is handled by a custom safe output job
299+
if (customJobTypes.has(messageType)) {
300+
// Silently skip - this is handled by a custom safe output job step
301+
core.debug(`Message ${i + 1} (${messageType}) will be handled by custom safe output job`);
302+
results.push({
303+
type: messageType,
304+
messageIndex: i,
305+
success: false,
306+
skipped: true,
307+
reason: "Handled by custom safe output job",
308+
});
309+
continue;
310+
}
311+
290312
// Unknown message type - warn the user
291313
core.warning(
292314
`⚠️ 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.`
@@ -899,6 +921,7 @@ async function main() {
899921
const cancelledCount = processingResult.results.filter(r => r.cancelled).length;
900922
const deferredCount = processingResult.results.filter(r => r.deferred).length;
901923
const skippedStandaloneResults = processingResult.results.filter(r => r.skipped && r.reason === "Handled by standalone step");
924+
const skippedCustomJobResults = processingResult.results.filter(r => r.skipped && r.reason === "Handled by custom safe output job");
902925
const skippedNoHandlerResults = processingResult.results.filter(r => !r.success && !r.skipped && r.error?.includes("No handler loaded"));
903926

904927
core.info(`\n=== Processing Summary ===`);
@@ -916,6 +939,11 @@ async function main() {
916939
const standaloneTypes = [...new Set(skippedStandaloneResults.map(r => r.type))];
917940
core.info(` Types: ${standaloneTypes.join(", ")}`);
918941
}
942+
if (skippedCustomJobResults.length > 0) {
943+
core.info(`Skipped (custom safe output job): ${skippedCustomJobResults.length}`);
944+
const customJobTypesList = [...new Set(skippedCustomJobResults.map(r => r.type))];
945+
core.info(` Types: ${customJobTypesList.join(", ")}`);
946+
}
919947
if (skippedNoHandlerResults.length > 0) {
920948
core.warning(`Skipped (no handler): ${skippedNoHandlerResults.length}`);
921949
const noHandlerTypes = [...new Set(skippedNoHandlerResults.map(r => r.type))];

actions/setup/js/safe_output_handler_manager.test.cjs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ describe("Safe Output Handler Manager", () => {
2020
// Clean up environment variables
2121
delete process.env.GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG;
2222
delete process.env.GH_AW_TRACKER_LABEL;
23+
delete process.env.GH_AW_SAFE_OUTPUT_JOBS;
2324
});
2425

2526
describe("loadConfig", () => {
@@ -218,6 +219,91 @@ describe("Safe Output Handler Manager", () => {
218219
expect(core.warning).toHaveBeenCalledWith(expect.stringContaining("No handler loaded for message type 'unknown_type'"));
219220
});
220221

222+
it("should skip custom safe output job types gracefully without error", async () => {
223+
// Set up custom safe output jobs (e.g., send_slack_message handled by a dedicated job step)
224+
process.env.GH_AW_SAFE_OUTPUT_JOBS = JSON.stringify({
225+
send_slack_message: "message_url",
226+
});
227+
228+
const messages = [
229+
{ type: "create_issue", title: "Issue" },
230+
{ type: "send_slack_message", channel: "#alerts", text: "Hello" },
231+
];
232+
233+
const mockHandler = vi.fn().mockResolvedValue({ success: true });
234+
235+
// Only create_issue handler is available; send_slack_message is a custom job
236+
const handlers = new Map([["create_issue", mockHandler]]);
237+
238+
const result = await processMessages(handlers, messages);
239+
240+
expect(result.success).toBe(true);
241+
expect(result.results).toHaveLength(2);
242+
243+
// First message should succeed
244+
expect(result.results[0].success).toBe(true);
245+
expect(result.results[0].type).toBe("create_issue");
246+
247+
// Custom job message should be skipped gracefully (not an error)
248+
expect(result.results[1].success).toBe(false);
249+
expect(result.results[1].type).toBe("send_slack_message");
250+
expect(result.results[1].skipped).toBe(true);
251+
expect(result.results[1].reason).toBe("Handled by custom safe output job");
252+
expect(result.results[1].error).toBeUndefined();
253+
254+
// Should NOT have logged a "No handler loaded" warning
255+
expect(core.warning).not.toHaveBeenCalledWith(expect.stringContaining("No handler loaded for message type 'send_slack_message'"));
256+
});
257+
258+
it("should skip multiple custom safe output job types gracefully", async () => {
259+
process.env.GH_AW_SAFE_OUTPUT_JOBS = JSON.stringify({
260+
send_slack_message: "message_url",
261+
notion_add_comment: "comment_url",
262+
});
263+
264+
const messages = [
265+
{ type: "send_slack_message", channel: "#alerts", text: "Hello" },
266+
{ type: "notion_add_comment", page_id: "abc123", text: "Note" },
267+
{ type: "create_issue", title: "Issue" },
268+
];
269+
270+
const mockHandler = vi.fn().mockResolvedValue({ success: true });
271+
const handlers = new Map([["create_issue", mockHandler]]);
272+
273+
const result = await processMessages(handlers, messages);
274+
275+
expect(result.success).toBe(true);
276+
expect(result.results).toHaveLength(3);
277+
278+
// Custom job types should be skipped gracefully
279+
expect(result.results[0].skipped).toBe(true);
280+
expect(result.results[0].reason).toBe("Handled by custom safe output job");
281+
expect(result.results[1].skipped).toBe(true);
282+
expect(result.results[1].reason).toBe("Handled by custom safe output job");
283+
284+
// create_issue should succeed
285+
expect(result.results[2].success).toBe(true);
286+
});
287+
288+
it("should still warn for unknown types not in custom job types", async () => {
289+
process.env.GH_AW_SAFE_OUTPUT_JOBS = JSON.stringify({
290+
send_slack_message: "message_url",
291+
});
292+
293+
const messages = [{ type: "completely_unknown_type", data: "test" }];
294+
295+
const handlers = new Map();
296+
297+
const result = await processMessages(handlers, messages);
298+
299+
expect(result.success).toBe(true);
300+
expect(result.results[0].error).toContain("No handler loaded");
301+
expect(result.results[0].skipped).toBeUndefined();
302+
303+
// Should have logged a warning for truly unknown types
304+
expect(core.warning).toHaveBeenCalledWith(expect.stringContaining("No handler loaded for message type 'completely_unknown_type'"));
305+
});
306+
221307
it("should handle handler errors gracefully", async () => {
222308
const messages = [{ type: "create_issue", title: "Issue" }];
223309

pkg/workflow/guard_policy_experimental_warning_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ engine: copilot
2929
tools:
3030
github:
3131
repos: all
32-
min-integrity: reader
32+
min-integrity: unapproved
3333
permissions:
3434
contents: read
3535
---
@@ -77,7 +77,7 @@ tools:
7777
github:
7878
repos:
7979
- owner/repo
80-
min-integrity: writer
80+
min-integrity: approved
8181
permissions:
8282
contents: read
8383
---

0 commit comments

Comments
 (0)