diff --git a/actions/setup/js/firewall_blocked_domains.cjs b/actions/setup/js/firewall_blocked_domains.cjs index 69beb00f2ec..d9cd4150422 100644 --- a/actions/setup/js/firewall_blocked_domains.cjs +++ b/actions/setup/js/firewall_blocked_domains.cjs @@ -11,6 +11,8 @@ const fs = require("fs"); const path = require("path"); const { sanitizeDomainName } = require("./sanitize_content_core.cjs"); +const { renderTemplateFromFile } = require("./messages_core.cjs"); +const { renderMarkdownTemplate } = require("./render_template.cjs"); /** * Parses a single firewall log line @@ -184,43 +186,50 @@ function getBlockedDomains(logsDir) { /** * Generates HTML details/summary section for blocked domains wrapped in a GitHub warning alert - * @param {string[]} blockedDomains - Array of blocked domain names + * @param {string[]} blockedDomains - Array of blocked domain names (expected to be pre-sanitized via getBlockedDomains) + * @param {string} [templatePath] - Optional path to template file (defaults to RUNNER_TEMP/gh-aw/prompts/firewall_blocked_domains.md) * @returns {string} GitHub warning alert with details section, or empty string if no blocked domains */ -function generateBlockedDomainsSection(blockedDomains) { +function generateBlockedDomainsSection(blockedDomains, templatePath) { if (!blockedDomains || blockedDomains.length === 0) { return ""; } const domainCount = blockedDomains.length; const domainWord = domainCount === 1 ? "domain" : "domains"; + const verb = domainCount === 1 ? "was" : "were"; - let section = "\n\n> [!WARNING]\n"; - section += `> **⚠️ Firewall blocked ${domainCount} ${domainWord}**\n`; - section += `>\n`; - section += `> The following ${domainWord} ${domainCount === 1 ? "was" : "were"} blocked by the firewall during workflow execution:\n`; - section += `>\n`; + // Build domain bullet list lines + const domainList = blockedDomains.map(domain => `> - \`${domain}\`\n`).join(""); - // List domains as bullet points (within the alert) - for (const domain of blockedDomains) { - section += `> - \`${domain}\`\n`; - } + // Build YAML network.allowed list lines + const yamlNetworkList = blockedDomains.map(domain => `> - "${domain}"\n`).join(""); + + const hasGitHubApiBlocked = blockedDomains.includes("api.github.com"); - section += `>\n`; - section += `> To allow these domains, add them to the \`network.allowed\` list in your workflow frontmatter:\n`; - section += `>\n`; - section += `> \`\`\`yaml\n`; - section += `> network:\n`; - section += `> allowed:\n`; - section += `> - defaults\n`; - for (const domain of blockedDomains) { - section += `> - "${domain}"\n`; + // Resolve template path: explicit > 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"); } - section += `> \`\`\`\n`; - section += `>\n`; - section += `> See [Network Configuration](https://github.github.com/gh-aw/reference/network/) for more information.\n`; - return section; + // First pass: substitute {key} placeholders. + // has_github_api_blocked is set to the string "true" or "false" so that + // renderMarkdownTemplate's isTruthy() correctly evaluates the + // {{#if {has_github_api_blocked}}} conditional in the template + // (isTruthy("false") === false per the template engine's explicit check). + const rendered = renderTemplateFromFile(resolvedTemplatePath, { + domain_count: domainCount, + domain_word: domainWord, + verb, + domain_list: domainList, + yaml_network_list: yamlNetworkList, + has_github_api_blocked: hasGitHubApiBlocked ? "true" : "false", + }); + + // Second pass: evaluate {{#if ...}} conditional blocks (e.g. the gh-proxy tip section) + // Template starts without leading newlines; prepend separator expected by callers + return "\n\n" + renderMarkdownTemplate(rendered); } module.exports = { diff --git a/actions/setup/js/firewall_blocked_domains.test.cjs b/actions/setup/js/firewall_blocked_domains.test.cjs index 71c70f8b65f..a693ad4b3eb 100644 --- a/actions/setup/js/firewall_blocked_domains.test.cjs +++ b/actions/setup/js/firewall_blocked_domains.test.cjs @@ -2,6 +2,13 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import fs from "fs"; import path from "path"; import os from "os"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Path to the template file in the source tree (used in tests instead of RUNNER_TEMP) +const TEMPLATE_PATH = path.join(__dirname, "../md/firewall_blocked_domains.md"); describe("firewall_blocked_domains.cjs", () => { let parseFirewallLogLine; @@ -306,7 +313,7 @@ describe("firewall_blocked_domains.cjs", () => { }); it("should generate warning section for single blocked domain", () => { - const result = generateBlockedDomainsSection(["blocked.example.com"]); + const result = generateBlockedDomainsSection(["blocked.example.com"], TEMPLATE_PATH); expect(result).toContain("> [!WARNING]"); expect(result).toContain("> **⚠️ Firewall blocked 1 domain**"); @@ -318,7 +325,7 @@ describe("firewall_blocked_domains.cjs", () => { it("should generate warning section for multiple blocked domains", () => { const domains = ["alpha.example.com", "beta.example.com", "gamma.example.com"]; - const result = generateBlockedDomainsSection(domains); + const result = generateBlockedDomainsSection(domains, TEMPLATE_PATH); expect(result).toContain("> [!WARNING]"); expect(result).toContain("> **⚠️ Firewall blocked 3 domains**"); @@ -330,23 +337,55 @@ describe("firewall_blocked_domains.cjs", () => { }); it("should use correct singular/plural form", () => { - const singleResult = generateBlockedDomainsSection(["single.com"]); + const singleResult = generateBlockedDomainsSection(["single.com"], TEMPLATE_PATH); expect(singleResult).toContain("1 domain"); expect(singleResult).toContain("domain was blocked"); - const multiResult = generateBlockedDomainsSection(["one.com", "two.com"]); + const multiResult = generateBlockedDomainsSection(["one.com", "two.com"], TEMPLATE_PATH); expect(multiResult).toContain("2 domains"); expect(multiResult).toContain("domains were blocked"); }); it("should format domains with backticks", () => { - const result = generateBlockedDomainsSection(["example.com"]); + const result = generateBlockedDomainsSection(["example.com"], TEMPLATE_PATH); expect(result).toMatch(/> - `example\.com`/); }); it("should start with double newline and warning alert", () => { - const result = generateBlockedDomainsSection(["example.com"]); + const result = generateBlockedDomainsSection(["example.com"], TEMPLATE_PATH); expect(result).toMatch(/^\n\n> \[!WARNING\]/); }); + + it("should suggest gh-proxy mode when api.github.com is blocked", () => { + const result = generateBlockedDomainsSection(["api.github.com"], TEMPLATE_PATH); + + expect(result).toContain("> [!WARNING]"); + expect(result).toContain("> **⚠️ Firewall blocked 1 domain**"); + expect(result).toContain("> - `api.github.com`"); + expect(result).toContain("`tools.github.mode: gh-proxy`"); + expect(result).toContain("> ```yaml\n> tools:\n> github:\n> mode: gh-proxy\n> ```"); + expect(result).toContain("> See [GitHub Tools](https://github.github.com/gh-aw/reference/github-tools/) for more information on `gh-proxy` mode."); + expect(result).toContain("> See [Network Configuration](https://github.github.com/gh-aw/reference/network/) for more information."); + }); + + it("should suggest gh-proxy mode when api.github.com is among other blocked domains", () => { + const domains = ["api.github.com", "other.example.com"]; + const result = generateBlockedDomainsSection(domains, TEMPLATE_PATH); + + expect(result).toContain("> [!WARNING]"); + expect(result).toContain("> **⚠️ Firewall blocked 2 domains**"); + expect(result).toContain("> - `api.github.com`"); + expect(result).toContain("> - `other.example.com`"); + expect(result).toContain("> ```yaml\n> tools:\n> github:\n> mode: gh-proxy\n> ```"); + expect(result).toContain("> See [GitHub Tools](https://github.github.com/gh-aw/reference/github-tools/) for more information on `gh-proxy` mode."); + }); + + it("should not suggest gh-proxy mode when api.github.com is not blocked", () => { + const result = generateBlockedDomainsSection(["other.example.com"], TEMPLATE_PATH); + + expect(result).not.toContain("gh-proxy"); + expect(result).not.toContain("GitHub Tools"); + expect(result).toContain("> See [Network Configuration](https://github.github.com/gh-aw/reference/network/) for more information."); + }); }); }); diff --git a/actions/setup/md/firewall_blocked_domains.md b/actions/setup/md/firewall_blocked_domains.md new file mode 100644 index 00000000000..cd47bc3548c --- /dev/null +++ b/actions/setup/md/firewall_blocked_domains.md @@ -0,0 +1,27 @@ +> [!WARNING] +> **⚠️ Firewall blocked {domain_count} {domain_word}** +> +> The following {domain_word} {verb} blocked by the firewall during workflow execution: +> +{domain_list}> +{{#if {has_github_api_blocked}}} +> **💡 Tip:** `api.github.com` is blocked because GitHub API access uses the built-in GitHub tools by default. Instead of adding `api.github.com` to `network.allowed`, use `tools.github.mode: gh-proxy` for direct pre-authenticated GitHub CLI access without requiring network access to `api.github.com`: +> +> ```yaml +> tools: +> github: +> mode: gh-proxy +> ``` +> +> See [GitHub Tools](https://github.github.com/gh-aw/reference/github-tools/) for more information on `gh-proxy` mode. +> +{{/if}} +> To allow these domains, add them to the `network.allowed` list in your workflow frontmatter: +> +> ```yaml +> network: +> allowed: +> - defaults +{yaml_network_list}> ``` +> +> See [Network Configuration](https://github.github.com/gh-aw/reference/network/) for more information.