diff --git a/actions/setup/js/run_validate_workflows.cjs b/actions/setup/js/run_validate_workflows.cjs
index e2ca7f1ac5..ff0d3be0ce 100644
--- a/actions/setup/js/run_validate_workflows.cjs
+++ b/actions/setup/js/run_validate_workflows.cjs
@@ -4,6 +4,7 @@
const { getErrorMessage } = require("./error_helpers.cjs");
const { generateFooterWithMessages, generateXMLMarker } = require("./messages_footer.cjs");
const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs");
+const { sanitizeContent } = require("./sanitize_content.cjs");
/**
* Run full workflow validation using gh-aw compile --validate and all known
@@ -89,6 +90,7 @@ async function main() {
core.info(`Found existing issue #${existingIssue.number}: ${existingIssue.html_url}`);
const truncatedOutput = combinedOutput.substring(0, 50000) + (combinedOutput.length > 50000 ? "\n\n... (output truncated)" : "");
+ const sanitizedOutput = sanitizeContent(truncatedOutput);
const xmlMarker = generateXMLMarker(workflowName, runUrl);
const commentBody = `Validation still has findings (exit code: ${exitCode}).
@@ -97,7 +99,7 @@ async function main() {
Validation output
\`\`\`
-${truncatedOutput}
+${sanitizedOutput}
\`\`\`
@@ -130,6 +132,7 @@ ${xmlMarker}`;
core.info("No existing issue found, creating a new issue with validation findings");
const truncatedOutput = combinedOutput.substring(0, 50000) + (combinedOutput.length > 50000 ? "\n\n... (output truncated)" : "");
+ const sanitizedOutput = sanitizeContent(truncatedOutput);
const xmlMarker = generateXMLMarker(workflowName, runUrl);
const issueBody = `## Problem
@@ -145,7 +148,7 @@ Workflow validation found errors or warnings that need to be addressed.
Full output
\`\`\`
-${truncatedOutput}
+${sanitizedOutput}
\`\`\`
diff --git a/actions/setup/js/run_validate_workflows.test.cjs b/actions/setup/js/run_validate_workflows.test.cjs
index 4dd59f4e6a..e2efcaa25a 100644
--- a/actions/setup/js/run_validate_workflows.test.cjs
+++ b/actions/setup/js/run_validate_workflows.test.cjs
@@ -230,4 +230,51 @@ describe("run_validate_workflows", () => {
const createCall = mockGithub.rest.issues.create.mock.calls[0][0];
expect(createCall.body).toContain("... (output truncated)");
});
+
+ it("should sanitize output containing @mentions in new issue body", async () => {
+ mockExec.exec.mockImplementation(async (_cmd, _args, options) => {
+ if (options?.listeners?.stderr) {
+ options.listeners.stderr(Buffer.from("Error: @malicious-user triggered a warning\n"));
+ }
+ return 1;
+ });
+
+ mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
+ data: { total_count: 0, items: [] },
+ });
+
+ mockGithub.rest.issues.create.mockResolvedValue({
+ data: { number: 45, html_url: "https://github.com/testowner/testrepo/issues/45" },
+ });
+
+ const { main } = await import("./run_validate_workflows.cjs");
+ await main();
+
+ const createCall = mockGithub.rest.issues.create.mock.calls[0][0];
+ // sanitizeContent should neutralize @mentions so they don't ping users
+ expect(createCall.body).not.toMatch(/@malicious-user(?!`)/);
+ });
+
+ it("should sanitize output containing @mentions in comment body", async () => {
+ mockExec.exec.mockImplementation(async (_cmd, _args, options) => {
+ if (options?.listeners?.stderr) {
+ options.listeners.stderr(Buffer.from("Error: @malicious-user triggered a warning\n"));
+ }
+ return 1;
+ });
+
+ mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
+ data: {
+ total_count: 1,
+ items: [{ number: 10, html_url: "https://github.com/testowner/testrepo/issues/10" }],
+ },
+ });
+
+ const { main } = await import("./run_validate_workflows.cjs");
+ await main();
+
+ const commentCall = mockGithub.rest.issues.createComment.mock.calls[0][0];
+ // sanitizeContent should neutralize @mentions so they don't ping users
+ expect(commentCall.body).not.toMatch(/@malicious-user(?!`)/);
+ });
});