diff --git a/.github/workflows/smoke-checkout-pr-dispatch.lock.yml b/.github/workflows/smoke-checkout-pr-dispatch.lock.yml index da8cf360d0b..bb61f3eb769 100644 --- a/.github/workflows/smoke-checkout-pr-dispatch.lock.yml +++ b/.github/workflows/smoke-checkout-pr-dispatch.lock.yml @@ -880,7 +880,7 @@ jobs: # --allow-tool shell(wc) # --allow-tool shell(yq) # --allow-tool write - timeout-minutes: 10 + timeout-minutes: 20 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt @@ -927,7 +927,7 @@ jobs: GH_AW_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_TIMEOUT_MINUTES: 20 GH_AW_VERSION: dev GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs index 58f459c6277..e17d52ec36d 100644 --- a/actions/setup/js/create_pull_request.test.cjs +++ b/actions/setup/js/create_pull_request.test.cjs @@ -3593,6 +3593,8 @@ describe("create_pull_request - threat detection caution", () => { copyPromptTemplate(promptsDir, "manifest_protection_request_review.md"); copyPromptTemplate(promptsDir, "manifest_protection_request_changes_review.md"); copyPromptTemplate(promptsDir, "threat_warning_request_changes_review.md"); + copyPromptTemplate(promptsDir, "threat_detection_caution.md"); + copyPromptTemplate(promptsDir, "threat_detection_engine_error.md"); copyPromptTemplate(promptsDir, "safe_outputs_disclosure_header.md"); process.env.GH_AW_PROMPTS_DIR = promptsDir; diff --git a/actions/setup/js/generate_footer.cjs b/actions/setup/js/generate_footer.cjs index 77e5af6340f..7c1413d7c64 100644 --- a/actions/setup/js/generate_footer.cjs +++ b/actions/setup/js/generate_footer.cjs @@ -2,6 +2,7 @@ /// const { getDetectionReasonText, getThreatDetectedMarker, isToolingFailureReason } = require("./threat_detection_warning.cjs"); +const { getPromptPath, renderTemplateFromFile } = require("./messages_core.cjs"); /** * Generates a standalone workflow-id XML comment marker for searchability. @@ -113,12 +114,7 @@ function generateXMLMarker(workflowName, runUrl) { * admonition is used so reviewers can distinguish "detection engine crashed" from "detection * engine found something". Actual threat findings (threat_detected) keep [!CAUTION]. * - * Note: This function is intentionally kept inline (not imported from messages_footer.cjs) - * because importing messages_footer.cjs here would cause the bundler to inline - * messages_core.cjs which contains 'GH_AW_SAFE_OUTPUT_MESSAGES:' in a warning message, - * breaking tests that check for env var declarations. - * - * Warning reason text and threat marker formatting are centralized in + * Note: Warning reason text and threat marker formatting are centralized in * threat_detection_warning.cjs to keep warning-mode messaging consistent. * * @param {string} workflowName - Name of the workflow @@ -132,10 +128,11 @@ function getExpiredEntityCautionAlert(workflowName, runUrl) { } const detectionReason = process.env.GH_AW_DETECTION_REASON || ""; const reasonText = getDetectionReasonText(detectionReason); + const context = { threat_detected_marker: getThreatDetectedMarker(detectionReason), reason_text: reasonText, run_url: runUrl }; if (isToolingFailureReason(detectionReason)) { - return `> [!WARNING]\n> threat detection engine error\n> The threat detection engine encountered an error and could not complete analysis. This is a tooling failure, not a security finding.\n> ${getThreatDetectedMarker(detectionReason)}\n>\n>
\n> Details\n>\n> ${reasonText}\n>\n> Review the [workflow run logs](${runUrl}) for details.\n>
`; + return renderTemplateFromFile(getPromptPath("threat_detection_engine_error.md"), context).trimEnd(); } - return `> [!CAUTION]\n> agentic threat detected\n> Threat detection flagged this output in warn mode. Manual review is REQUIRED before any follow-up automation.\n> ${getThreatDetectedMarker(detectionReason)}\n>\n>
\n> Details\n>\n> ${reasonText}\n>\n> Review the [workflow run logs](${runUrl}) for details.\n>
`; + return renderTemplateFromFile(getPromptPath("threat_detection_caution.md"), context).trimEnd(); } /** diff --git a/actions/setup/js/generate_footer.test.cjs b/actions/setup/js/generate_footer.test.cjs index ebea285cdc9..618ee17f1c2 100644 --- a/actions/setup/js/generate_footer.test.cjs +++ b/actions/setup/js/generate_footer.test.cjs @@ -1,4 +1,6 @@ -import { describe, it, expect, beforeEach, vi } from "vitest"; +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { fileURLToPath } from "url"; +import path from "path"; // Mock the global objects that GitHub Actions provides const mockCore = { @@ -337,8 +339,14 @@ describe("generate_footer.cjs", () => { describe("generateExpiredEntityFooter", () => { let generateExpiredEntityFooter; let getExpiredEntityCautionAlert; + let originalPromptsDir; beforeEach(async () => { + // Point GH_AW_PROMPTS_DIR to the source md/ directory so getPromptPath() + // resolves to the local templates. This is needed because RUNNER_TEMP may be + // set but the runtime prompts directory is not populated in the test environment. + originalPromptsDir = process.env.GH_AW_PROMPTS_DIR; + process.env.GH_AW_PROMPTS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "../md"); // Reset modules and import fresh vi.resetModules(); const freshModule = await import("./generate_footer.cjs"); @@ -346,6 +354,14 @@ describe("generate_footer.cjs", () => { getExpiredEntityCautionAlert = freshModule.getExpiredEntityCautionAlert; }); + afterEach(() => { + if (originalPromptsDir !== undefined) { + process.env.GH_AW_PROMPTS_DIR = originalPromptsDir; + } else { + delete process.env.GH_AW_PROMPTS_DIR; + } + }); + it("should generate footer with 'Closed by' wording and workflow link", () => { const result = generateExpiredEntityFooter("Test Workflow", "https://github.com/test/repo/actions/runs/123", "test-workflow"); @@ -486,7 +502,8 @@ describe("generate_footer.cjs", () => { const result = getExpiredEntityCautionAlert("Test Workflow", "https://github.com/test/repo/actions/runs/123"); expect(result).toContain("> [!WARNING]"); - expect(result).toContain("threat detection engine error"); + expect(result).toContain("**Threat Detection Engine Failure**"); + expect(result).toContain("What happened"); expect(result).toContain(""); expect(result).not.toContain(""); expect(result).not.toContain("> [!CAUTION]"); @@ -501,7 +518,8 @@ describe("generate_footer.cjs", () => { const result = getExpiredEntityCautionAlert("Test Workflow", "https://github.com/test/repo/actions/runs/123"); expect(result).toContain("> [!WARNING]"); - expect(result).toContain("threat detection engine error"); + expect(result).toContain("**Threat Detection Engine Failure**"); + expect(result).toContain("What happened"); expect(result).toContain(""); expect(result).not.toContain(""); expect(result).not.toContain("> [!CAUTION]"); diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs index 31ec2878f7d..ecc666b34cb 100644 --- a/actions/setup/js/handle_agent_failure.test.cjs +++ b/actions/setup/js/handle_agent_failure.test.cjs @@ -184,6 +184,14 @@ describe("handle_agent_failure", () => { fs.writeFileSync(path.join(promptsDir, "daily_cap_rollup_issue.md"), "Daily cap rollup issue body cap={cap} window={window_hours}"); fs.writeFileSync(path.join(promptsDir, "daily_cap_rollup_comment.md"), "Failure suppressed workflow={workflow_name} run={run_url} categories={summary} cap={cap} window={window_hours}h"); fs.writeFileSync(path.join(promptsDir, "optimize_token_consumption_context.md"), "OPTIMIZE CONTEXT guardrail={guardrail_name} run={run_url}"); + fs.writeFileSync( + path.join(promptsDir, "threat_detection_caution.md"), + "> [!CAUTION]\n> agentic threat detected\n> Threat detection flagged this output in warn mode. Manual review is REQUIRED before any follow-up automation.\n> {threat_detected_marker}\n>\n>
\n> Details\n>\n> {reason_text}\n>\n> Review the [workflow run logs]({run_url}) for details.\n>
" + ); + fs.writeFileSync( + path.join(promptsDir, "threat_detection_engine_error.md"), + "> [!WARNING]\n> **Threat Detection Engine Failure** — The analysis engine could not complete. This is a tooling failure, not a security finding.\n> {threat_detected_marker}\n>\n>
\n> What happened\n>\n> {reason_text}\n>\n> Review the [workflow run logs]({run_url}) for details.\n>
" + ); process.env.RUNNER_TEMP = tmpDir; process.env.GH_AW_WORKFLOW_NAME = "Test Workflow"; diff --git a/actions/setup/js/messages.test.cjs b/actions/setup/js/messages.test.cjs index d19c06eec65..f4e79aac848 100644 --- a/actions/setup/js/messages.test.cjs +++ b/actions/setup/js/messages.test.cjs @@ -1411,7 +1411,8 @@ describe("messages.cjs", () => { const result = getDetectionCautionAlert("Test Workflow", "https://github.com/test/repo/actions/runs/123"); expect(result).toContain("> [!WARNING]"); - expect(result).toContain("threat detection engine error"); + expect(result).toContain("**Threat Detection Engine Failure**"); + expect(result).toContain("What happened"); expect(result).toContain(""); expect(result).not.toContain(""); expect(result).not.toContain("> [!CAUTION]"); diff --git a/actions/setup/js/messages_run_status.cjs b/actions/setup/js/messages_run_status.cjs index 6796c7a60ee..2834b3e7be8 100644 --- a/actions/setup/js/messages_run_status.cjs +++ b/actions/setup/js/messages_run_status.cjs @@ -7,8 +7,8 @@ * for workflow execution notifications. */ -const { getMessages, renderTemplate, toSnakeCase } = require("./messages_core.cjs"); -const { getDetectionReasonText, getThreatWarningPresentation, normalizeThreatKinds, isToolingFailureReason } = require("./threat_detection_warning.cjs"); +const { getMessages, renderTemplate, renderTemplateFromFile, toSnakeCase, getPromptPath } = require("./messages_core.cjs"); +const { getDetectionReasonText, getThreatDetectedMarkerTemplate, getThreatEngineErrorMarkerTemplate, normalizeThreatKinds, isToolingFailureReason } = require("./threat_detection_warning.cjs"); /** * Renders a message using a custom template from config or a default template. @@ -152,14 +152,16 @@ function getCommitPushedMessage(ctx) { */ function getDetectionWarningMessage(ctx) { const reasonText = getDetectionReasonText(ctx.reason); - const presentation = getThreatWarningPresentation(ctx.reason); const isEngineError = isToolingFailureReason(ctx.reason); - if (isEngineError) { - const defaultTemplate = `> [!${presentation.admonition}]\n> ${presentation.title}\n> ${presentation.summary}\n> ${presentation.marker}\n>\n>
\n> Details\n>\n> {reason_text}\n>\n> Review the [workflow run logs]({run_url}) for details.\n>
`; - return renderConfiguredMessage("detectionEngineError", defaultTemplate, { ...ctx, reasonText, threatKinds: normalizeThreatKinds(ctx.reason) }); + const templateFile = isEngineError ? "threat_detection_engine_error.md" : "threat_detection_caution.md"; + const messageKey = isEngineError ? "detectionEngineError" : "detectionWarning"; + const markerTemplate = isEngineError ? getThreatEngineErrorMarkerTemplate() : getThreatDetectedMarkerTemplate(); + const messages = getMessages(); + const configTemplate = messages?.[messageKey]; + if (configTemplate) { + return renderTemplate(configTemplate, toSnakeCase({ ...ctx, reasonText, threat_detected_marker: markerTemplate, threatKinds: normalizeThreatKinds(ctx.reason) })); } - const defaultTemplate = `> [!${presentation.admonition}]\n> ${presentation.title}\n> ${presentation.summary}\n> ${presentation.marker}\n>\n>
\n> Details\n>\n> {reason_text}\n>\n> Review the [workflow run logs]({run_url}) for details.\n>
`; - return renderConfiguredMessage("detectionWarning", defaultTemplate, { ...ctx, reasonText, threatKinds: normalizeThreatKinds(ctx.reason) }); + return renderTemplateFromFile(getPromptPath(templateFile), toSnakeCase({ ...ctx, reasonText, threat_detected_marker: markerTemplate, threatKinds: normalizeThreatKinds(ctx.reason) })).trimEnd(); } module.exports = { diff --git a/actions/setup/md/detection_runs_comment.md b/actions/setup/md/detection_runs_comment.md index 68cf428f937..edb408ab6ac 100644 --- a/actions/setup/md/detection_runs_comment.md +++ b/actions/setup/md/detection_runs_comment.md @@ -1,7 +1,14 @@ ### {workflow_name} +Threat detection produced a **{conclusion}** result for this run. + +
+Run details + | Field | Value | |---|---| | Conclusion | `{conclusion}` | | Reason | `{reason}` | | Run | [View run]({run_url}) | + +
diff --git a/actions/setup/md/threat_detection_caution.md b/actions/setup/md/threat_detection_caution.md new file mode 100644 index 00000000000..b3577a7703b --- /dev/null +++ b/actions/setup/md/threat_detection_caution.md @@ -0,0 +1,12 @@ +> [!CAUTION] +> agentic threat detected +> Threat detection flagged this output in warn mode. Manual review is REQUIRED before any follow-up automation. +> {threat_detected_marker} +> +>
+> Details +> +> {reason_text} +> +> Review the [workflow run logs]({run_url}) for details. +>
diff --git a/actions/setup/md/threat_detection_engine_error.md b/actions/setup/md/threat_detection_engine_error.md new file mode 100644 index 00000000000..d910df5df87 --- /dev/null +++ b/actions/setup/md/threat_detection_engine_error.md @@ -0,0 +1,11 @@ +> [!WARNING] +> **Threat Detection Engine Failure** — The analysis engine could not complete. This is a tooling failure, not a security finding. +> {threat_detected_marker} +> +>
+> What happened +> +> {reason_text} +> +> Review the [workflow run logs]({run_url}) for details. +>