diff --git a/actions/setup/js/determine_automatic_lockdown.cjs b/actions/setup/js/determine_automatic_lockdown.cjs index 0af3452b3d..be17474d77 100644 --- a/actions/setup/js/determine_automatic_lockdown.cjs +++ b/actions/setup/js/determine_automatic_lockdown.cjs @@ -87,18 +87,25 @@ async function determineAutomaticLockdown(github, context, core) { // Write resolved guard policy values to the step summary const autoLabel = isPrivate ? "automatic (private repo)" : "automatic (public repo)"; - await core.summary - .addHeading("GitHub MCP Guard Policy", 3) - .addTable([ - [ - { data: "Field", header: true }, - { data: "Value", header: true }, - { data: "Source", header: true }, - ], - ["min-integrity", resolvedMinIntegrity, configuredMinIntegrity ? "workflow config" : autoLabel], - ["repos", resolvedRepos, configuredRepos ? "workflow config" : autoLabel], - ]) - .write(); + const minIntegritySource = configuredMinIntegrity ? "workflow config" : autoLabel; + const reposSource = configuredRepos ? "workflow config" : autoLabel; + + /** + * Escapes a value for safe embedding in a markdown table cell. + * Replaces HTML-special characters and pipe characters that would break the table. + * @param {string} value + * @returns {string} + */ + const escapeCell = value => value.replace(/&/g, "&").replace(//g, ">").replace(/\|/g, "\\|").replace(/\n/g, " "); + + const tableRows = [ + "| Field | Value | Source |", + "|-------|-------|--------|", + `| min-integrity | ${escapeCell(resolvedMinIntegrity)} | ${escapeCell(minIntegritySource)} |`, + `| repos | ${escapeCell(resolvedRepos)} | ${escapeCell(reposSource)} |`, + ].join("\n"); + const details = `
\nGitHub MCP Guard Policy\n\n${tableRows}\n\n
\n`; + await core.summary.addRaw(details).write(); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); core.error(`Failed to determine automatic guard policy: ${errorMessage}`); diff --git a/actions/setup/js/determine_automatic_lockdown.test.cjs b/actions/setup/js/determine_automatic_lockdown.test.cjs index e34e0f17ce..54c4caba78 100644 --- a/actions/setup/js/determine_automatic_lockdown.test.cjs +++ b/actions/setup/js/determine_automatic_lockdown.test.cjs @@ -33,8 +33,7 @@ describe("determine_automatic_lockdown", () => { error: vi.fn(), setOutput: vi.fn(), summary: { - addHeading: vi.fn().mockReturnThis(), - addTable: vi.fn().mockReturnThis(), + addRaw: vi.fn().mockReturnThis(), write: vi.fn().mockResolvedValue(undefined), }, }; @@ -218,16 +217,21 @@ describe("determine_automatic_lockdown", () => { await determineAutomaticLockdown(mockGithub, mockContext, mockCore); - expect(mockCore.summary.addHeading).toHaveBeenCalledWith("GitHub MCP Guard Policy", 3); - expect(mockCore.summary.addTable).toHaveBeenCalledWith([ - [ - { data: "Field", header: true }, - { data: "Value", header: true }, - { data: "Source", header: true }, - ], - ["min-integrity", "approved", "automatic (public repo)"], - ["repos", "all", "automatic (public repo)"], - ]); + expect(mockCore.summary.addRaw).toHaveBeenCalledTimes(1); + const publicSummaryArg = mockCore.summary.addRaw.mock.calls[0][0]; + expect(publicSummaryArg).toContain("
"); + expect(publicSummaryArg).toContain("GitHub MCP Guard Policy"); + // Ensure we have a well-formed
block with a and closing
+ expect(publicSummaryArg).toMatch(/
[\s\S]*<\/details>/); + expect(publicSummaryArg).toMatch(/[\s\S]*GitHub MCP Guard Policy[\s\S]*<\/summary>/); + // Ensure the markdown table header and separator are present + expect(publicSummaryArg).toMatch(/\| *Field *\| *Value *\| *Source *\|/); + expect(publicSummaryArg).toMatch(/\|[- ]+\|[- ]+\|[- ]+\|/); + expect(publicSummaryArg).toContain("min-integrity"); + expect(publicSummaryArg).toContain("approved"); + expect(publicSummaryArg).toContain("automatic (public repo)"); + expect(publicSummaryArg).toContain("repos"); + expect(publicSummaryArg).toContain("all"); expect(mockCore.summary.write).toHaveBeenCalled(); }); @@ -244,15 +248,18 @@ describe("determine_automatic_lockdown", () => { await determineAutomaticLockdown(mockGithub, mockContext, mockCore); - expect(mockCore.summary.addTable).toHaveBeenCalledWith([ - [ - { data: "Field", header: true }, - { data: "Value", header: true }, - { data: "Source", header: true }, - ], - ["min-integrity", "merged", "workflow config"], - ["repos", "public", "workflow config"], - ]); + expect(mockCore.summary.addRaw).toHaveBeenCalledTimes(1); + const configuredSummaryArg = mockCore.summary.addRaw.mock.calls[0][0]; + // Ensure we have a well-formed
block with closing
+ expect(configuredSummaryArg).toMatch(/
[\s\S]*<\/details>/); + // Ensure the markdown table header and separator are present + expect(configuredSummaryArg).toMatch(/\| *Field *\| *Value *\| *Source *\|/); + expect(configuredSummaryArg).toMatch(/\|[- ]+\|[- ]+\|[- ]+\|/); + expect(configuredSummaryArg).toContain("min-integrity"); + expect(configuredSummaryArg).toContain("merged"); + expect(configuredSummaryArg).toContain("workflow config"); + expect(configuredSummaryArg).toContain("repos"); + expect(configuredSummaryArg).toContain("public"); expect(mockCore.summary.write).toHaveBeenCalled(); }); @@ -266,15 +273,18 @@ describe("determine_automatic_lockdown", () => { await determineAutomaticLockdown(mockGithub, mockContext, mockCore); - expect(mockCore.summary.addTable).toHaveBeenCalledWith([ - [ - { data: "Field", header: true }, - { data: "Value", header: true }, - { data: "Source", header: true }, - ], - ["min-integrity", "none", "automatic (private repo)"], - ["repos", "all", "automatic (private repo)"], - ]); + expect(mockCore.summary.addRaw).toHaveBeenCalledTimes(1); + const privateSummaryArg = mockCore.summary.addRaw.mock.calls[0][0]; + // Ensure we have a well-formed
block with closing
+ expect(privateSummaryArg).toMatch(/
[\s\S]*<\/details>/); + // Ensure the markdown table header and separator are present + expect(privateSummaryArg).toMatch(/\| *Field *\| *Value *\| *Source *\|/); + expect(privateSummaryArg).toMatch(/\|[- ]+\|[- ]+\|[- ]+\|/); + expect(privateSummaryArg).toContain("min-integrity"); + expect(privateSummaryArg).toContain("none"); + expect(privateSummaryArg).toContain("automatic (private repo)"); + expect(privateSummaryArg).toContain("repos"); + expect(privateSummaryArg).toContain("all"); expect(mockCore.summary.write).toHaveBeenCalled(); }); });