Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions actions/setup/js/checkout_pr_branch.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@
*/

const { getErrorMessage } = require("./error_helpers.cjs");
const { renderTemplate } = require("./messages_core.cjs");
const { renderTemplateFromFile } = require("./messages_core.cjs");
const { detectForkPR } = require("./pr_helpers.cjs");
const fs = require("fs");
const { ERR_API } = require("./error_codes.cjs");

/**
Expand Down Expand Up @@ -277,8 +276,7 @@ Pull request #${pullRequest.number} is closed. The checkout failed because the b

// Load and render step summary template
const templatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/pr_checkout_failure.md`;
const template = fs.readFileSync(templatePath, "utf8");
const summaryContent = renderTemplate(template, {
const summaryContent = renderTemplateFromFile(templatePath, {
error_message: errorMsg,
});

Expand Down
3 changes: 2 additions & 1 deletion actions/setup/js/checkout_pr_branch.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ describe("checkout_pr_branch.cjs", () => {
}
if (module === "./messages_core.cjs") {
return {
renderTemplate: (template, context) => {
renderTemplateFromFile: (templatePath, context) => {
const template = mockRequire("fs").readFileSync(templatePath, "utf8");
return template.replace(/\{(\w+)\}/g, (match, key) => {
const value = context[key];
return value !== undefined && value !== null ? String(value) : match;
Expand Down
12 changes: 4 additions & 8 deletions actions/setup/js/create_issue.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,12 @@ const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_help
const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs");
const { removeDuplicateTitleFromDescription } = require("./remove_duplicate_title.cjs");
const { getErrorMessage } = require("./error_helpers.cjs");
const { renderTemplate } = require("./messages_core.cjs");
const { renderTemplateFromFile } = require("./messages_core.cjs");
const { createExpirationLine, addExpirationToFooter } = require("./ephemerals.cjs");
const { MAX_SUB_ISSUES, getSubIssueCount } = require("./sub_issue_helpers.cjs");
const { closeOlderIssues } = require("./close_older_issues.cjs");
const { parseBoolTemplatable } = require("./templatable.cjs");
const { tryEnforceArrayLimit } = require("./limit_enforcement_helpers.cjs");
const fs = require("fs");
const { logStagedPreviewInfo } = require("./staged_preview.cjs");
const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs");
const { MAX_LABELS, MAX_ASSIGNEES } = require("./constants.cjs");
Expand Down Expand Up @@ -167,19 +166,16 @@ function createParentIssueTemplate(groupId, titlePrefix, workflowName, workflowS
// Use applyTitlePrefix to ensure proper spacing after prefix
const title = applyTitlePrefix(`${groupId} - Issue Group`, titlePrefix);

// Load issue template
const issueTemplatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/issue_group_parent.md`;
const issueTemplate = fs.readFileSync(issueTemplatePath, "utf8");

// Create template context
const templateContext = {
group_id: groupId,
workflow_name: workflowName,
workflow_source_url: workflowSourceURL || "#",
};

// Render the issue template
let body = renderTemplate(issueTemplate, templateContext);
// Load and render the issue template
const issueTemplatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/issue_group_parent.md`;
let body = renderTemplateFromFile(issueTemplatePath, templateContext);

// Add footer with workflow information
const footer = `\n\n> Workflow: [${workflowName}](${workflowSourceURL})`;
Expand Down
11 changes: 4 additions & 7 deletions actions/setup/js/create_pull_request.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,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 { renderTemplate } = require("./messages_core.cjs");
const { renderTemplateFromFile } = require("./messages_core.cjs");
const { COPILOT_REVIEWER_BOT, FAQ_CREATE_PR_PERMISSIONS_URL } = require("./constants.cjs");

Comment on lines 25 to 30
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description mentions removing an unused fs require from this file, but fs is still required and used throughout (e.g., patch file exists/read operations). Either update the PR description or clarify which fs import was removed to avoid confusion for reviewers.

Copilot uses AI. Check for mistakes.
/**
Expand Down Expand Up @@ -985,8 +985,7 @@ ${patchPreview}`;
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 pushFailedTemplate = fs.readFileSync(pushFailedTemplatePath, "utf8");
fallbackBody = renderTemplate(pushFailedTemplate, {
fallbackBody = renderTemplateFromFile(pushFailedTemplatePath, {
main_body: mainBodyContent,
footer: footerContent,
files: filesFormatted,
Expand All @@ -1004,8 +1003,7 @@ ${patchPreview}`;
const encodedHead = branchName.split("/").map(encodeURIComponent).join("/");
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 template = fs.readFileSync(templatePath, "utf8");
fallbackBody = renderTemplate(template, {
fallbackBody = renderTemplateFromFile(templatePath, {
main_body: mainBodyContent,
footer: footerContent,
files: filesFormatted,
Expand Down Expand Up @@ -1188,8 +1186,7 @@ ${patchPreview}`;
}

const fallbackTemplatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/pr_permission_denied_fallback.md`;
const fallbackTemplate = fs.readFileSync(fallbackTemplatePath, "utf8");
const fallbackBody = renderTemplate(fallbackTemplate, {
const fallbackBody = renderTemplateFromFile(fallbackTemplatePath, {
body,
branch_name: branchName,
create_pr_url: createPrUrl,
Expand Down
8 changes: 3 additions & 5 deletions actions/setup/js/handle_agent_failure.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
const { getErrorMessage } = require("./error_helpers.cjs");
const { sanitizeContent } = require("./sanitize_content.cjs");
const { getFooterAgentFailureIssueMessage, getFooterAgentFailureCommentMessage, generateXMLMarker } = require("./messages.cjs");
const { renderTemplate } = require("./messages_core.cjs");
const { renderTemplate, renderTemplateFromFile } = 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");
Expand Down Expand Up @@ -609,8 +609,7 @@ function buildTimeoutContext(isTimedOut, timeoutMinutes) {
const suggestedMinutes = currentMinutes + 10;

const templatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/agent_timeout.md`;
const template = fs.readFileSync(templatePath, "utf8");
return "\n" + renderTemplate(template, { current_minutes: currentMinutes, suggested_minutes: suggestedMinutes });
return "\n" + renderTemplateFromFile(templatePath, { current_minutes: currentMinutes, suggested_minutes: suggestedMinutes });
}

/**
Expand Down Expand Up @@ -639,8 +638,7 @@ function buildAppTokenMintingFailedContext(hasAppTokenMintingFailed) {
}

const templatePath = "/opt/gh-aw/prompts/app_token_minting_failed.md";
const template = fs.readFileSync(templatePath, "utf8");
return "\n" + renderTemplate(template, {});
return "\n" + renderTemplateFromFile(templatePath, {});
}

/**
Expand Down
9 changes: 3 additions & 6 deletions actions/setup/js/handle_noop_message.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 { renderTemplate } = require("./messages_core.cjs");
const { renderTemplateFromFile } = require("./messages_core.cjs");

/**
* Search for or create the parent issue for all agentic workflow no-op runs
Expand Down Expand Up @@ -139,12 +139,9 @@ async function main() {
return;
}

// Load comment template from file
// Load and render comment template from file
const commentTemplatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/noop_comment.md`;
const commentTemplate = fs.readFileSync(commentTemplatePath, "utf8");

// Build the comment body by replacing template variables
const commentBody = renderTemplate(commentTemplate, {
const commentBody = renderTemplateFromFile(commentTemplatePath, {
workflow_name: workflowName,
message: noopMessage,
run_url: runUrl,
Expand Down
3 changes: 2 additions & 1 deletion actions/setup/js/messages.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
*/

// Re-export core utilities
const { getMessages, renderTemplate } = require("./messages_core.cjs");
const { getMessages, renderTemplate, renderTemplateFromFile } = require("./messages_core.cjs");

// Re-export footer messages
const { getFooterMessage, getFooterInstallMessage, getFooterAgentFailureIssueMessage, getFooterAgentFailureCommentMessage, generateFooterWithMessages, generateXMLMarker } = require("./messages_footer.cjs");
Expand All @@ -45,6 +45,7 @@ const { getCloseOlderDiscussionMessage } = require("./messages_close_discussion.
module.exports = {
getMessages,
renderTemplate,
renderTemplateFromFile,
getFooterMessage,
getFooterInstallMessage,
getFooterAgentFailureIssueMessage,
Expand Down
14 changes: 14 additions & 0 deletions actions/setup/js/messages_core.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
*/

const { getErrorMessage } = require("./error_helpers.cjs");
const fs = require("fs");

/**
* @typedef {Object} SafeOutputMessages
Expand Down Expand Up @@ -78,6 +79,18 @@ function renderTemplate(template, context) {
});
}

/**
* Read a template file and render it with the given context.
* Combines file loading and template rendering into a single helper.
* @param {string} templatePath - Absolute path to the template file
* @param {Record<string, string|number|boolean|undefined>} context - Key-value pairs for replacement
* @returns {string} Rendered template with placeholders replaced
*/
function renderTemplateFromFile(templatePath, context) {
const template = fs.readFileSync(templatePath, "utf8");
return renderTemplate(template, context);
}

/**
* Convert context object keys to snake_case for template rendering.
* Also keeps original camelCase keys for backwards compatibility.
Expand All @@ -101,5 +114,6 @@ function toSnakeCase(obj) {
module.exports = {
getMessages,
renderTemplate,
renderTemplateFromFile,
toSnakeCase,
};
58 changes: 58 additions & 0 deletions actions/setup/js/messages_core.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
*
* Tests for the core message utilities module including:
* - Template rendering with placeholder replacement
* - Template rendering from file
* - Snake_case conversion with camelCase compatibility
* - Messages config parsing from environment variable
*/
import { describe, it, expect, beforeEach, vi } from "vitest";
import fs from "fs";
import os from "os";
import path from "path";

const mockCore = {
debug: vi.fn(),
Expand Down Expand Up @@ -78,6 +82,60 @@ describe("messages_core.cjs", () => {
});
});

describe("renderTemplateFromFile", () => {
it("should read a file and render its contents as a template", async () => {
const { renderTemplateFromFile } = await import("./messages_core.cjs?" + Date.now());
const tmpFile = path.join(os.tmpdir(), `msg-core-test-${Date.now()}.md`);
fs.writeFileSync(tmpFile, "Hello, {name}!", "utf8");
try {
const result = renderTemplateFromFile(tmpFile, { name: "World" });
expect(result).toBe("Hello, World!");
} finally {
fs.unlinkSync(tmpFile);
}
});

it("should replace multiple placeholders from a file", async () => {
const { renderTemplateFromFile } = await import("./messages_core.cjs?" + Date.now());
const tmpFile = path.join(os.tmpdir(), `msg-core-test-${Date.now()}.md`);
fs.writeFileSync(tmpFile, "{greeting}, {name}! Run: {run_url}", "utf8");
try {
const result = renderTemplateFromFile(tmpFile, {
greeting: "Hello",
name: "Alice",
run_url: "https://github.com/actions/runs/123",
});
expect(result).toBe("Hello, Alice! Run: https://github.com/actions/runs/123");
} finally {
fs.unlinkSync(tmpFile);
}
});

it("should leave unknown placeholders unchanged", async () => {
const { renderTemplateFromFile } = await import("./messages_core.cjs?" + Date.now());
const tmpFile = path.join(os.tmpdir(), `msg-core-test-${Date.now()}.md`);
fs.writeFileSync(tmpFile, "Hello, {name}! {unknown}", "utf8");
try {
const result = renderTemplateFromFile(tmpFile, { name: "World" });
expect(result).toBe("Hello, World! {unknown}");
} finally {
fs.unlinkSync(tmpFile);
}
});

it("should return file contents unchanged when context is empty", async () => {
const { renderTemplateFromFile } = await import("./messages_core.cjs?" + Date.now());
const tmpFile = path.join(os.tmpdir(), `msg-core-test-${Date.now()}.md`);
fs.writeFileSync(tmpFile, "No placeholders here.", "utf8");
try {
const result = renderTemplateFromFile(tmpFile, {});
expect(result).toBe("No placeholders here.");
} finally {
fs.unlinkSync(tmpFile);
}
});
});

describe("toSnakeCase", () => {
it("should convert camelCase keys to snake_case", async () => {
const { toSnakeCase } = await import("./messages_core.cjs?" + Date.now());
Expand Down
10 changes: 3 additions & 7 deletions actions/setup/js/missing_issue_helpers.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
/// <reference types="@actions/github-script" />

const { getErrorMessage } = require("./error_helpers.cjs");
const { renderTemplate } = require("./messages_core.cjs");
const { renderTemplateFromFile } = require("./messages_core.cjs");
const { generateFooterWithExpiration } = require("./ephemerals.cjs");
const fs = require("fs");
const { sanitizeContent } = require("./sanitize_content.cjs");

/**
Expand Down Expand Up @@ -111,9 +110,6 @@ function buildMissingIssueHandler(options) {
// No existing issue, create a new one
core.info("No existing issue found, creating a new one");

// Load issue template
const issueTemplate = fs.readFileSync(templatePath, "utf8");

// Build items list for template
const issueListLines = [];
items.forEach((item, index) => {
Expand All @@ -129,8 +125,8 @@ function buildMissingIssueHandler(options) {
[templateListKey]: issueListLines.join("\n"),
};

// Render the issue template
const issueBodyContent = renderTemplate(issueTemplate, templateContext);
// Load and render the issue template
const issueBodyContent = renderTemplateFromFile(templatePath, templateContext);

// Add expiration marker (1 week from now) in a quoted section using helper
const footer = generateFooterWithExpiration({
Expand Down
5 changes: 2 additions & 3 deletions actions/setup/js/push_to_pull_request_branch.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,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 { renderTemplate } = require("./messages_core.cjs");
const { renderTemplateFromFile } = require("./messages_core.cjs");
const { getGitAuthEnv } = require("./git_helpers.cjs");

/**
Expand Down Expand Up @@ -338,8 +338,7 @@ async function main(config = {}) {
const prUrl = `${githubServer}/${repoParts.owner}/${repoParts.repo}/pull/${pullNumber}`;
const issueTitle = `[gh-aw] Protected Files: ${prTitle || `PR #${pullNumber}`}`;
const templatePath = `${process.env.RUNNER_TEMP}/gh-aw/prompts/manifest_protection_push_to_pr_fallback.md`;
const template = fs.readFileSync(templatePath, "utf8");
const issueBody = renderTemplate(template, {
const issueBody = renderTemplateFromFile(templatePath, {
files: protectedFilesForFallback.map(f => `\`${f}\``).join(", "),
pull_number: pullNumber,
pr_url: prUrl,
Expand Down
Loading