From 472dd9d09386ac3bcb591524c3efea64040eb361 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 26 Apr 2026 11:59:11 +0000
Subject: [PATCH 1/3] Initial plan
From 7aca7cd2606866714ddf01e15b000f2bba14e528 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 26 Apr 2026 12:13:00 +0000
Subject: [PATCH 2/3] refactor: centralize prompt template path construction
with getPromptPath helper
Add getPromptPath(name) to messages_core.cjs that prefers GH_AW_PROMPTS_DIR
when set, otherwise falls back to ${RUNNER_TEMP}/gh-aw/prompts. Migrate all
26 repeated path construction sites across 11 handler files to use the helper.
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/8c62a493-c53a-4b24-9427-e8e7e986c45f
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
---
actions/setup/js/checkout_pr_branch.cjs | 4 +--
actions/setup/js/checkout_pr_branch.test.cjs | 1 +
.../setup/js/create_missing_data_issue.cjs | 3 +-
.../setup/js/create_missing_tool_issue.cjs | 3 +-
actions/setup/js/create_pull_request.cjs | 8 ++---
.../js/create_report_incomplete_issue.cjs | 3 +-
actions/setup/js/firewall_blocked_domains.cjs | 6 ++--
actions/setup/js/handle_agent_failure.cjs | 24 +++++++-------
actions/setup/js/handle_detection_runs.cjs | 6 ++--
actions/setup/js/handle_noop_message.cjs | 6 ++--
actions/setup/js/messages_core.cjs | 13 ++++++++
actions/setup/js/messages_core.test.cjs | 33 +++++++++++++++++++
.../setup/js/push_to_pull_request_branch.cjs | 4 +--
actions/setup/js/setup_threat_detection.cjs | 4 +--
14 files changed, 84 insertions(+), 34 deletions(-)
diff --git a/actions/setup/js/checkout_pr_branch.cjs b/actions/setup/js/checkout_pr_branch.cjs
index 4506e3b8372..0add419699d 100644
--- a/actions/setup/js/checkout_pr_branch.cjs
+++ b/actions/setup/js/checkout_pr_branch.cjs
@@ -26,7 +26,7 @@
*/
const { getErrorMessage } = require("./error_helpers.cjs");
-const { renderTemplateFromFile } = require("./messages_core.cjs");
+const { renderTemplateFromFile, getPromptPath } = require("./messages_core.cjs");
const { detectForkPR } = require("./pr_helpers.cjs");
const { ERR_API } = require("./error_codes.cjs");
@@ -323,7 +323,7 @@ Pull request #${pullRequest.number} was merged after this workflow was triggered
core.setOutput("checkout_pr_success", "false");
// Load and render step summary template
- const templatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/pr_checkout_failure.md`;
+ const templatePath = getPromptPath("pr_checkout_failure.md");
const summaryContent = renderTemplateFromFile(templatePath, {
error_message: errorMsg,
});
diff --git a/actions/setup/js/checkout_pr_branch.test.cjs b/actions/setup/js/checkout_pr_branch.test.cjs
index 224f3b01cb0..ddb937d7295 100644
--- a/actions/setup/js/checkout_pr_branch.test.cjs
+++ b/actions/setup/js/checkout_pr_branch.test.cjs
@@ -121,6 +121,7 @@ describe("checkout_pr_branch.cjs", () => {
}
if (module === "./messages_core.cjs") {
return {
+ getPromptPath: name => `/mock/prompts/${name}`,
renderTemplateFromFile: (templatePath, context) => {
const template = mockRequire("fs").readFileSync(templatePath, "utf8");
return template.replace(/\{(\w+)\}/g, (match, key) => {
diff --git a/actions/setup/js/create_missing_data_issue.cjs b/actions/setup/js/create_missing_data_issue.cjs
index c5d1f3f34ac..c08dfd60383 100644
--- a/actions/setup/js/create_missing_data_issue.cjs
+++ b/actions/setup/js/create_missing_data_issue.cjs
@@ -2,6 +2,7 @@
///
const { buildMissingIssueHandler } = require("./missing_issue_helpers.cjs");
+const { getPromptPath } = require("./messages_core.cjs");
/**
* @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction
@@ -20,7 +21,7 @@ const main = buildMissingIssueHandler({
defaultTitlePrefix: "[missing data]",
defaultLabels: ["agentic-workflows"],
itemsField: "missing_data",
- templatePath: `${process.env.RUNNER_TEMP}/gh-aw/prompts/missing_data_issue.md`,
+ templatePath: getPromptPath("missing_data_issue.md"),
templateListKey: "missing_data_list",
buildCommentHeader: runUrl => [`## Missing Data Reported`, ``, `The following data was reported as missing during [workflow run](${runUrl}):`, ``],
renderCommentItem: (item, index) => {
diff --git a/actions/setup/js/create_missing_tool_issue.cjs b/actions/setup/js/create_missing_tool_issue.cjs
index 14ce8af1bb4..4df226438ad 100644
--- a/actions/setup/js/create_missing_tool_issue.cjs
+++ b/actions/setup/js/create_missing_tool_issue.cjs
@@ -2,6 +2,7 @@
///
const { buildMissingIssueHandler } = require("./missing_issue_helpers.cjs");
+const { getPromptPath } = require("./messages_core.cjs");
/**
* @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction
@@ -20,7 +21,7 @@ const main = buildMissingIssueHandler({
defaultTitlePrefix: "[missing tool]",
defaultLabels: ["agentic-workflows"],
itemsField: "missing_tools",
- templatePath: `${process.env.RUNNER_TEMP}/gh-aw/prompts/missing_tool_issue.md`,
+ templatePath: getPromptPath("missing_tool_issue.md"),
templateListKey: "missing_tools_list",
buildCommentHeader: runUrl => [`## Missing Tools Reported`, ``, `The following tools were reported as missing during [workflow run](${runUrl}):`, ``],
renderCommentItem: (tool, index) => {
diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs
index bbee0b6bbe0..d90ac8d2d15 100644
--- a/actions/setup/js/create_pull_request.cjs
+++ b/actions/setup/js/create_pull_request.cjs
@@ -26,7 +26,7 @@ const { getBaseBranch } = require("./get_base_branch.cjs");
const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs");
const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs");
const { checkFileProtection } = require("./manifest_file_helpers.cjs");
-const { renderTemplateFromFile, buildProtectedFileList, encodePathSegments } = require("./messages_core.cjs");
+const { renderTemplateFromFile, buildProtectedFileList, encodePathSegments, getPromptPath } = require("./messages_core.cjs");
const { COPILOT_REVIEWER_BOT, FAQ_CREATE_PR_PERMISSIONS_URL, MAX_ASSIGNEES } = require("./constants.cjs");
const { isStagedMode } = require("./safe_output_helpers.cjs");
const { withRetry, isTransientError } = require("./error_recovery.cjs");
@@ -1617,7 +1617,7 @@ ${patchPreview}`;
// Use the push-failed template with artifact download instructions.
const runId = context.runId;
const patchFileName = patchFilePath ? patchFilePath.replace("/tmp/gh-aw/", "") : "aw-unknown.patch";
- const pushFailedTemplatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/manifest_protection_push_failed_fallback.md`;
+ const pushFailedTemplatePath = getPromptPath("manifest_protection_push_failed_fallback.md");
fallbackBody = renderTemplateFromFile(pushFailedTemplatePath, {
main_body: mainBodyContent,
footer: footerContent,
@@ -1634,7 +1634,7 @@ ${patchPreview}`;
const encodedBase = encodePathSegments(baseBranch);
const encodedHead = encodePathSegments(branchName);
const createPrUrl = `${githubServer}/${repoParts.owner}/${repoParts.repo}/compare/${encodedBase}...${encodedHead}?expand=1&title=${encodeURIComponent(title)}`;
- const templatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/manifest_protection_create_pr_fallback.md`;
+ const templatePath = getPromptPath("manifest_protection_create_pr_fallback.md");
fallbackBody = renderTemplateFromFile(templatePath, {
main_body: mainBodyContent,
footer: footerContent,
@@ -1834,7 +1834,7 @@ ${patchPreview}`;
patchPreview = generatePatchPreview(patchContent);
}
- const fallbackTemplatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/pr_permission_denied_fallback.md`;
+ const fallbackTemplatePath = getPromptPath("pr_permission_denied_fallback.md");
const fallbackBody = renderTemplateFromFile(fallbackTemplatePath, {
body,
branch_name: branchName,
diff --git a/actions/setup/js/create_report_incomplete_issue.cjs b/actions/setup/js/create_report_incomplete_issue.cjs
index 63cca4ffc72..09001cbc789 100644
--- a/actions/setup/js/create_report_incomplete_issue.cjs
+++ b/actions/setup/js/create_report_incomplete_issue.cjs
@@ -2,6 +2,7 @@
///
const { buildMissingIssueHandler } = require("./missing_issue_helpers.cjs");
+const { getPromptPath } = require("./messages_core.cjs");
/**
* @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction
@@ -22,7 +23,7 @@ const main = buildMissingIssueHandler({
defaultTitlePrefix: "[incomplete]",
defaultLabels: ["agentic-workflows"],
itemsField: "incomplete_signals",
- templatePath: `${process.env.RUNNER_TEMP}/gh-aw/prompts/missing_tool_issue.md`,
+ templatePath: getPromptPath("missing_tool_issue.md"),
templateListKey: "incomplete_signals_list",
buildCommentHeader: runUrl => [`## Incomplete Run Reported`, ``, `The agent reported that the task could not be completed during [workflow run](${runUrl}):`, ``],
renderCommentItem: (item, index) => {
diff --git a/actions/setup/js/firewall_blocked_domains.cjs b/actions/setup/js/firewall_blocked_domains.cjs
index d9cd4150422..10f075497c6 100644
--- a/actions/setup/js/firewall_blocked_domains.cjs
+++ b/actions/setup/js/firewall_blocked_domains.cjs
@@ -11,7 +11,7 @@
const fs = require("fs");
const path = require("path");
const { sanitizeDomainName } = require("./sanitize_content_core.cjs");
-const { renderTemplateFromFile } = require("./messages_core.cjs");
+const { renderTemplateFromFile, getPromptPath } = require("./messages_core.cjs");
const { renderMarkdownTemplate } = require("./render_template.cjs");
/**
@@ -207,10 +207,10 @@ function generateBlockedDomainsSection(blockedDomains, templatePath) {
const hasGitHubApiBlocked = blockedDomains.includes("api.github.com");
- // Resolve template path: explicit > RUNNER_TEMP (production) > source tree (local dev/test)
+ // Resolve template path: explicit > GH_AW_PROMPTS_DIR / RUNNER_TEMP (production) > source tree (local dev/test)
let resolvedTemplatePath = templatePath;
if (!resolvedTemplatePath) {
- resolvedTemplatePath = process.env.RUNNER_TEMP ? `${process.env.RUNNER_TEMP}/gh-aw/prompts/firewall_blocked_domains.md` : path.join(__dirname, "../md/firewall_blocked_domains.md");
+ resolvedTemplatePath = process.env.RUNNER_TEMP || process.env.GH_AW_PROMPTS_DIR ? getPromptPath("firewall_blocked_domains.md") : path.join(__dirname, "../md/firewall_blocked_domains.md");
}
// First pass: substitute {key} placeholders.
diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs
index cc68ab0825f..ef878328078 100644
--- a/actions/setup/js/handle_agent_failure.cjs
+++ b/actions/setup/js/handle_agent_failure.cjs
@@ -4,7 +4,7 @@
const { getErrorMessage } = require("./error_helpers.cjs");
const { sanitizeContent } = require("./sanitize_content.cjs");
const { getDetectionCautionAlert, getFooterAgentFailureIssueMessage, getFooterAgentFailureCommentMessage, generateXMLMarker } = require("./messages.cjs");
-const { renderTemplate, renderTemplateFromFile } = require("./messages_core.cjs");
+const { renderTemplate, renderTemplateFromFile, getPromptPath } = require("./messages_core.cjs");
const { getCurrentBranch } = require("./get_current_branch.cjs");
const { createExpirationLine, generateFooterWithExpiration } = require("./ephemerals.cjs");
const { MAX_SUB_ISSUES, getSubIssueCount } = require("./sub_issue_helpers.cjs");
@@ -618,7 +618,7 @@ function buildMissingDataContext(cacheMemoryEnabled) {
const hasCacheMiss = missingDataMessages.some(m => m.reason === "cache_memory_miss");
if (cacheMemoryEnabled && hasCacheMiss) {
core.info("Cache-miss detected despite cache-memory being available — likely a configuration problem");
- const templatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/cache_memory_miss.md`;
+ const templatePath = getPromptPath("cache_memory_miss.md");
context += "\n" + renderTemplateFromFile(templatePath, {}) + "\n";
}
@@ -697,7 +697,7 @@ function buildTimeoutContext(isTimedOut, timeoutMinutes) {
const currentMinutes = parseInt(timeoutMinutes || "20", 10);
const suggestedMinutes = currentMinutes + 10;
- const templatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/agent_timeout.md`;
+ const templatePath = getPromptPath("agent_timeout.md");
return "\n" + renderTemplateFromFile(templatePath, { current_minutes: currentMinutes, suggested_minutes: suggestedMinutes });
}
@@ -711,7 +711,7 @@ function buildInferenceAccessErrorContext(hasInferenceAccessError) {
return "";
}
- const templatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/inference_access_error.md`;
+ const templatePath = getPromptPath("inference_access_error.md");
const template = fs.readFileSync(templatePath, "utf8");
return "\n" + template;
}
@@ -727,7 +727,7 @@ function buildMCPPolicyErrorContext(hasMCPPolicyError) {
return "";
}
- const templatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/mcp_policy_error.md`;
+ const templatePath = getPromptPath("mcp_policy_error.md");
try {
const template = fs.readFileSync(templatePath, "utf8");
return "\n" + template;
@@ -752,7 +752,7 @@ function buildModelNotSupportedErrorContext(hasModelNotSupportedError) {
return "";
}
- const templatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/model_not_supported_error.md`;
+ const templatePath = getPromptPath("model_not_supported_error.md");
try {
const template = fs.readFileSync(templatePath, "utf8");
return "\n" + template;
@@ -790,7 +790,7 @@ function buildLockdownCheckFailedContext(hasLockdownCheckFailed) {
return "";
}
- const templatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/lockdown_check_failed.md`;
+ const templatePath = getPromptPath("lockdown_check_failed.md");
const template = fs.readFileSync(templatePath, "utf8");
return "\n" + template;
}
@@ -807,7 +807,7 @@ function buildStaleLockFileFailedContext(hasStaleLockFileFailed) {
return "";
}
- const templatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/stale_lock_file_failed.md`;
+ const templatePath = getPromptPath("stale_lock_file_failed.md");
const template = fs.readFileSync(templatePath, "utf8");
return "\n" + template;
}
@@ -837,7 +837,7 @@ function buildAssignCopilotFailureContext(hasAssignCopilotFailures, assignCopilo
}
}
- const templatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/assign_copilot_to_created_issues_failure.md`;
+ const templatePath = getPromptPath("assign_copilot_to_created_issues_failure.md");
return "\n" + renderTemplateFromFile(templatePath, { issues: issueList });
}
@@ -915,7 +915,7 @@ function buildEngineFailureContext() {
const hasCyberPolicyViolation = Array.from(errorMessages).some(msg => msg.includes("cyber_policy_violation"));
if (hasCyberPolicyViolation) {
core.info("Detected cyber_policy_violation error — using dedicated context message");
- const templatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/cyber_policy_violation.md`;
+ const templatePath = getPromptPath("cyber_policy_violation.md");
try {
return "\n" + renderTemplateFromFile(templatePath, {});
} catch {
@@ -1263,7 +1263,7 @@ async function main() {
core.info(`Found existing issue #${existingIssue.number}: ${existingIssue.html_url}`);
// Read comment template
- const commentTemplatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/agent_failure_comment.md`;
+ const commentTemplatePath = getPromptPath("agent_failure_comment.md");
const commentTemplate = fs.readFileSync(commentTemplatePath, "utf8");
// Extract run ID from URL (e.g., https://github.com/owner/repo/actions/runs/123 -> "123")
@@ -1428,7 +1428,7 @@ async function main() {
core.info("No existing issue found, creating a new one");
// Read issue template
- const issueTemplatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/agent_failure_issue.md`;
+ const issueTemplatePath = getPromptPath("agent_failure_issue.md");
const issueTemplate = fs.readFileSync(issueTemplatePath, "utf8");
// Get current branch information
diff --git a/actions/setup/js/handle_detection_runs.cjs b/actions/setup/js/handle_detection_runs.cjs
index ff86d0e3de4..91fe932988e 100644
--- a/actions/setup/js/handle_detection_runs.cjs
+++ b/actions/setup/js/handle_detection_runs.cjs
@@ -6,7 +6,7 @@ const { getErrorMessage } = require("./error_helpers.cjs");
const { ERR_API } = require("./error_codes.cjs");
const { sanitizeContent } = require("./sanitize_content.cjs");
const { generateFooterWithExpiration } = require("./ephemerals.cjs");
-const { renderTemplateFromFile } = require("./messages_core.cjs");
+const { renderTemplateFromFile, getPromptPath } = require("./messages_core.cjs");
const { getEffectiveTokensSuffix } = require("./effective_tokens.cjs");
/**
@@ -46,7 +46,7 @@ async function ensureDetectionRunsIssue() {
core.info(`No detection runs issue found, creating one`);
// Load template from file
- const templatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/detection_runs_issue.md`;
+ const templatePath = getPromptPath("detection_runs_issue.md");
const parentBodyContent = fs.readFileSync(templatePath, "utf8");
const parentBody = generateFooterWithExpiration({
@@ -107,7 +107,7 @@ async function main() {
}
// Load and render comment template from file
- const commentTemplatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/detection_runs_comment.md`;
+ const commentTemplatePath = getPromptPath("detection_runs_comment.md");
// Compute effective tokens suffix from environment variable (set by parse_token_usage.cjs / parse_mcp_gateway_log.cjs)
const effectiveTokensSuffix = getEffectiveTokensSuffix();
diff --git a/actions/setup/js/handle_noop_message.cjs b/actions/setup/js/handle_noop_message.cjs
index e583b6e310c..bd522b76988 100644
--- a/actions/setup/js/handle_noop_message.cjs
+++ b/actions/setup/js/handle_noop_message.cjs
@@ -6,7 +6,7 @@ const { getErrorMessage } = require("./error_helpers.cjs");
const { ERR_API } = require("./error_codes.cjs");
const { sanitizeContent } = require("./sanitize_content.cjs");
const { generateFooterWithExpiration } = require("./ephemerals.cjs");
-const { renderTemplateFromFile } = require("./messages_core.cjs");
+const { renderTemplateFromFile, getPromptPath } = require("./messages_core.cjs");
const { loadAgentOutput } = require("./load_agent_output.cjs");
const { isStagedMode } = require("./safe_output_helpers.cjs");
const { getEffectiveTokensSuffix } = require("./effective_tokens.cjs");
@@ -48,7 +48,7 @@ async function ensureAgentRunsIssue() {
core.info(`No no-op runs issue found, creating one`);
// Load template from file
- const templatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/noop_runs_issue.md`;
+ const templatePath = getPromptPath("noop_runs_issue.md");
const parentBodyContent = fs.readFileSync(templatePath, "utf8");
const parentBody = generateFooterWithExpiration({
@@ -184,7 +184,7 @@ async function main() {
}
// Load and render comment template from file
- const commentTemplatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/noop_comment.md`;
+ const commentTemplatePath = getPromptPath("noop_comment.md");
// Compute effective tokens suffix from environment variable (set by parse_token_usage.cjs / parse_mcp_gateway_log.cjs)
const effectiveTokensSuffix = getEffectiveTokensSuffix();
diff --git a/actions/setup/js/messages_core.cjs b/actions/setup/js/messages_core.cjs
index ad1c3e8880f..762c2a7fdc5 100644
--- a/actions/setup/js/messages_core.cjs
+++ b/actions/setup/js/messages_core.cjs
@@ -84,6 +84,18 @@ function renderTemplate(template, context) {
});
}
+/**
+ * Resolve the absolute path to a prompt template file.
+ * Prefers GH_AW_PROMPTS_DIR when set, otherwise falls back to
+ * ${RUNNER_TEMP}/gh-aw/prompts (the runtime location used in production).
+ * @param {string} name - Template filename (e.g. "agent_timeout.md")
+ * @returns {string} Absolute path to the prompt template file
+ */
+function getPromptPath(name) {
+ const promptsDir = process.env.GH_AW_PROMPTS_DIR || `${process.env.RUNNER_TEMP}/gh-aw/prompts`;
+ return `${promptsDir}/${name}`;
+}
+
/**
* Read a template file and render it with the given context.
* Combines file loading and template rendering into a single helper.
@@ -169,6 +181,7 @@ function buildProtectedFileList(files, githubServer, owner, repo, branch) {
module.exports = {
getMessages,
+ getPromptPath,
renderTemplate,
renderTemplateFromFile,
toSnakeCase,
diff --git a/actions/setup/js/messages_core.test.cjs b/actions/setup/js/messages_core.test.cjs
index 078dc273b05..c52d870f199 100644
--- a/actions/setup/js/messages_core.test.cjs
+++ b/actions/setup/js/messages_core.test.cjs
@@ -26,6 +26,8 @@ describe("messages_core.cjs", () => {
vi.clearAllMocks();
vi.resetModules();
delete process.env.GH_AW_SAFE_OUTPUT_MESSAGES;
+ delete process.env.GH_AW_PROMPTS_DIR;
+ delete process.env.RUNNER_TEMP;
});
describe("renderTemplate", () => {
@@ -181,6 +183,37 @@ describe("messages_core.cjs", () => {
});
});
+ describe("getPromptPath", () => {
+ it("should use RUNNER_TEMP when no override is set", async () => {
+ process.env.RUNNER_TEMP = "/tmp/runner";
+ const { getPromptPath } = await import("./messages_core.cjs?" + Date.now());
+ const result = getPromptPath("agent_timeout.md");
+ expect(result).toBe("/tmp/runner/gh-aw/prompts/agent_timeout.md");
+ });
+
+ it("should prefer GH_AW_PROMPTS_DIR over RUNNER_TEMP", async () => {
+ process.env.RUNNER_TEMP = "/tmp/runner";
+ process.env.GH_AW_PROMPTS_DIR = "/custom/prompts";
+ const { getPromptPath } = await import("./messages_core.cjs?" + Date.now());
+ const result = getPromptPath("agent_timeout.md");
+ expect(result).toBe("/custom/prompts/agent_timeout.md");
+ });
+
+ it("should use GH_AW_PROMPTS_DIR when RUNNER_TEMP is not set", async () => {
+ process.env.GH_AW_PROMPTS_DIR = "/custom/prompts";
+ const { getPromptPath } = await import("./messages_core.cjs?" + Date.now());
+ const result = getPromptPath("cache_memory_miss.md");
+ expect(result).toBe("/custom/prompts/cache_memory_miss.md");
+ });
+
+ it("should include the template name in the path", async () => {
+ process.env.RUNNER_TEMP = "/tmp/runner";
+ const { getPromptPath } = await import("./messages_core.cjs?" + Date.now());
+ expect(getPromptPath("foo.md")).toBe("/tmp/runner/gh-aw/prompts/foo.md");
+ expect(getPromptPath("bar.md")).toBe("/tmp/runner/gh-aw/prompts/bar.md");
+ });
+ });
+
describe("getMessages", () => {
it("should return null when env var is not set", async () => {
const { getMessages } = await import("./messages_core.cjs?" + Date.now());
diff --git a/actions/setup/js/push_to_pull_request_branch.cjs b/actions/setup/js/push_to_pull_request_branch.cjs
index 70d9bf14ba6..7742d6585fa 100644
--- a/actions/setup/js/push_to_pull_request_branch.cjs
+++ b/actions/setup/js/push_to_pull_request_branch.cjs
@@ -15,7 +15,7 @@ const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_help
const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs");
const { checkFileProtection } = require("./manifest_file_helpers.cjs");
const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs");
-const { renderTemplateFromFile, buildProtectedFileList } = require("./messages_core.cjs");
+const { renderTemplateFromFile, buildProtectedFileList, getPromptPath } = require("./messages_core.cjs");
const { getGitAuthEnv } = require("./git_helpers.cjs");
const { findRepoCheckout } = require("./find_repo_checkout.cjs");
@@ -467,7 +467,7 @@ async function main(config = {}) {
const prUrl = `${githubServer}/${repoParts.owner}/${repoParts.repo}/pull/${pullNumber}`;
const issueTitle = `[gh-aw] Protected Files: ${prTitle || `PR #${pullNumber}`}`;
const fileList = buildProtectedFileList(protectedFilesForFallback, githubServer, repoParts.owner, repoParts.repo, branchName);
- const templatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/manifest_protection_push_to_pr_fallback.md`;
+ const templatePath = getPromptPath("manifest_protection_push_to_pr_fallback.md");
const issueBody = renderTemplateFromFile(templatePath, {
files: fileList,
pull_number: pullNumber,
diff --git a/actions/setup/js/setup_threat_detection.cjs b/actions/setup/js/setup_threat_detection.cjs
index 754f9901711..febd94eb7b2 100644
--- a/actions/setup/js/setup_threat_detection.cjs
+++ b/actions/setup/js/setup_threat_detection.cjs
@@ -17,6 +17,7 @@ const path = require("path");
const { checkFileExists } = require("./file_helpers.cjs");
const { AGENT_OUTPUT_FILENAME } = require("./constants.cjs");
const { ERR_VALIDATION } = require("./error_codes.cjs");
+const { getPromptPath } = require("./messages_core.cjs");
/**
* Main entry point for setting up threat detection
@@ -24,8 +25,7 @@ const { ERR_VALIDATION } = require("./error_codes.cjs");
*/
async function main() {
// Read the threat detection template from file
- // At runtime, markdown files are copied to ${RUNNER_TEMP}/gh-aw/prompts/ by the setup action
- const templatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/threat_detection.md`;
+ const templatePath = getPromptPath("threat_detection.md");
if (!fs.existsSync(templatePath)) {
core.setFailed(`${ERR_VALIDATION}: Threat detection template not found at: ${templatePath}`);
return;
From 43b09bdcdb1d8bc1075fec6f0a942263ebcbf024 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 26 Apr 2026 15:34:34 +0000
Subject: [PATCH 3/3] fix: validate env vars in getPromptPath and clarify
firewall condition
- getPromptPath now throws a clear error when neither GH_AW_PROMPTS_DIR
nor RUNNER_TEMP is set, preventing silent "undefined/gh-aw/prompts/..."
paths
- firewall_blocked_domains.cjs: extract hasRuntimePromptsDir boolean to
make the ternary intent unambiguous (Prettier-friendly alternative to
parentheses)
- Update test fixtures for buildLockdownCheckFailedContext,
buildStaleLockFileFailedContext, and buildTimeoutContext to set a
dummy RUNNER_TEMP so the new guard doesn't fire
- Add test case asserting the new throw behaviour
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/934f6270-5038-4893-a6da-b8c8a34ce8b6
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/firewall_blocked_domains.cjs | 3 ++-
actions/setup/js/handle_agent_failure.test.cjs | 6 ++++++
actions/setup/js/messages_core.cjs | 6 +++++-
actions/setup/js/messages_core.test.cjs | 5 +++++
4 files changed, 18 insertions(+), 2 deletions(-)
diff --git a/actions/setup/js/firewall_blocked_domains.cjs b/actions/setup/js/firewall_blocked_domains.cjs
index 10f075497c6..eb5dbc07ca4 100644
--- a/actions/setup/js/firewall_blocked_domains.cjs
+++ b/actions/setup/js/firewall_blocked_domains.cjs
@@ -210,7 +210,8 @@ function generateBlockedDomainsSection(blockedDomains, templatePath) {
// Resolve template path: explicit > GH_AW_PROMPTS_DIR / RUNNER_TEMP (production) > source tree (local dev/test)
let resolvedTemplatePath = templatePath;
if (!resolvedTemplatePath) {
- resolvedTemplatePath = process.env.RUNNER_TEMP || process.env.GH_AW_PROMPTS_DIR ? getPromptPath("firewall_blocked_domains.md") : path.join(__dirname, "../md/firewall_blocked_domains.md");
+ const hasRuntimePromptsDir = !!(process.env.RUNNER_TEMP || process.env.GH_AW_PROMPTS_DIR);
+ resolvedTemplatePath = hasRuntimePromptsDir ? getPromptPath("firewall_blocked_domains.md") : path.join(__dirname, "../md/firewall_blocked_domains.md");
}
// First pass: substitute {key} placeholders.
diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs
index 4934ffdd4ac..acc08845cdd 100644
--- a/actions/setup/js/handle_agent_failure.test.cjs
+++ b/actions/setup/js/handle_agent_failure.test.cjs
@@ -505,6 +505,7 @@ describe("handle_agent_failure", () => {
beforeEach(() => {
vi.resetModules();
+ process.env.RUNNER_TEMP = "/nonexistent";
// Stub readFileSync so the runtime path resolves to the source-tree template
fs.readFileSync = (filePath, encoding) => {
if (typeof filePath === "string" && filePath.includes("lockdown_check_failed.md")) {
@@ -517,6 +518,7 @@ describe("handle_agent_failure", () => {
afterEach(() => {
fs.readFileSync = originalReadFileSync;
+ delete process.env.RUNNER_TEMP;
});
it("returns empty string when no failure", () => {
@@ -553,6 +555,7 @@ describe("handle_agent_failure", () => {
beforeEach(() => {
vi.resetModules();
+ process.env.RUNNER_TEMP = "/nonexistent";
fs.readFileSync = (filePath, encoding) => {
if (typeof filePath === "string" && filePath.includes("stale_lock_file_failed.md")) {
return templateContent;
@@ -564,6 +567,7 @@ describe("handle_agent_failure", () => {
afterEach(() => {
fs.readFileSync = originalReadFileSync;
+ delete process.env.RUNNER_TEMP;
});
it("returns empty string when check did not fail", () => {
@@ -605,6 +609,7 @@ describe("handle_agent_failure", () => {
beforeEach(() => {
vi.resetModules();
+ process.env.RUNNER_TEMP = "/nonexistent";
// Stub readFileSync so the runtime path resolves to the source-tree template
fs.readFileSync = (filePath, encoding) => {
if (typeof filePath === "string" && filePath.includes("agent_timeout.md")) {
@@ -617,6 +622,7 @@ describe("handle_agent_failure", () => {
afterEach(() => {
fs.readFileSync = originalReadFileSync;
+ delete process.env.RUNNER_TEMP;
});
it("returns empty string when not timed out", () => {
diff --git a/actions/setup/js/messages_core.cjs b/actions/setup/js/messages_core.cjs
index 762c2a7fdc5..646f467f6f7 100644
--- a/actions/setup/js/messages_core.cjs
+++ b/actions/setup/js/messages_core.cjs
@@ -88,11 +88,15 @@ function renderTemplate(template, context) {
* Resolve the absolute path to a prompt template file.
* Prefers GH_AW_PROMPTS_DIR when set, otherwise falls back to
* ${RUNNER_TEMP}/gh-aw/prompts (the runtime location used in production).
+ * Throws if neither GH_AW_PROMPTS_DIR nor RUNNER_TEMP is set.
* @param {string} name - Template filename (e.g. "agent_timeout.md")
* @returns {string} Absolute path to the prompt template file
*/
function getPromptPath(name) {
- const promptsDir = process.env.GH_AW_PROMPTS_DIR || `${process.env.RUNNER_TEMP}/gh-aw/prompts`;
+ const promptsDir = process.env.GH_AW_PROMPTS_DIR || (process.env.RUNNER_TEMP ? `${process.env.RUNNER_TEMP}/gh-aw/prompts` : null);
+ if (!promptsDir) {
+ throw new Error("Cannot resolve prompt path: neither GH_AW_PROMPTS_DIR nor RUNNER_TEMP is set");
+ }
return `${promptsDir}/${name}`;
}
diff --git a/actions/setup/js/messages_core.test.cjs b/actions/setup/js/messages_core.test.cjs
index c52d870f199..69ca483d64e 100644
--- a/actions/setup/js/messages_core.test.cjs
+++ b/actions/setup/js/messages_core.test.cjs
@@ -212,6 +212,11 @@ describe("messages_core.cjs", () => {
expect(getPromptPath("foo.md")).toBe("/tmp/runner/gh-aw/prompts/foo.md");
expect(getPromptPath("bar.md")).toBe("/tmp/runner/gh-aw/prompts/bar.md");
});
+
+ it("should throw when neither GH_AW_PROMPTS_DIR nor RUNNER_TEMP is set", async () => {
+ const { getPromptPath } = await import("./messages_core.cjs?" + Date.now());
+ expect(() => getPromptPath("any.md")).toThrow("Cannot resolve prompt path: neither GH_AW_PROMPTS_DIR nor RUNNER_TEMP is set");
+ });
});
describe("getMessages", () => {