diff --git a/actions/setup/js/parse_token_usage.cjs b/actions/setup/js/parse_token_usage.cjs index 5df4d3add1f..cae35904ee2 100644 --- a/actions/setup/js/parse_token_usage.cjs +++ b/actions/setup/js/parse_token_usage.cjs @@ -102,6 +102,26 @@ function buildStepSummarySection(title, markdown) { return `### ${title}\n\n
\nPer-request AI credits and token totals\n\n${markdown}
\n\n`; } +/** + * Renders the token usage markdown table as plain text for core.info output. + * Strips markdown table separators, pipes, and bold markers so the table is + * readable in the raw step log. + * @param {string} title + * @param {string} markdown + * @returns {string} + */ +function renderTokenTableAsPlainText(title, markdown) { + const plainText = markdown + .replace(/^\|(?:[-: ]+\|)+$/gm, "") // Remove table separator lines (handles alignment colons) + .replace(/^\|/gm, "") // Remove leading pipe from table rows + .replace(/\|$/gm, "") // Remove trailing pipe from table rows + .replace(/\s*\|\s*/g, " | ") // Normalize remaining pipes to spaced separators + .replace(/\*\*(.*?)\*\*/g, "$1") // Remove bold markers + .replace(/\n{3,}/g, "\n\n") // Collapse excess blank lines + .trim(); + return `${title}\n\n${plainText}`; +} + /** * Appends the token usage section to GITHUB_STEP_SUMMARY when available. * Falls back to the Actions summary API when the summary path is unavailable. @@ -142,6 +162,7 @@ async function main() { } const markdown = generateTokenUsageSummary(summary); if (markdown.length > 0) { + core.info(renderTokenTableAsPlainText(getSummaryTitle(), markdown)); await appendStepSummarySection(getSummaryTitle(), markdown); } @@ -199,6 +220,7 @@ if (typeof module !== "undefined" && module.exports) { getSummaryTitle, buildStepSummarySection, appendStepSummarySection, + renderTokenTableAsPlainText, TOKEN_USAGE_AUDIT_PATH, TOKEN_USAGE_PATH, TOKEN_USAGE_PATHS, diff --git a/actions/setup/js/parse_token_usage.test.cjs b/actions/setup/js/parse_token_usage.test.cjs index 6cd34123cc4..da63b8dce14 100644 --- a/actions/setup/js/parse_token_usage.test.cjs +++ b/actions/setup/js/parse_token_usage.test.cjs @@ -12,6 +12,7 @@ const { readDedupedTokenUsage, getSummaryTitle, buildStepSummarySection, + renderTokenTableAsPlainText, TOKEN_USAGE_AUDIT_PATH, TOKEN_USAGE_PATH, TOKEN_USAGE_PATHS, @@ -194,6 +195,9 @@ describe("parse_token_usage", () => { expect(mockCore.summary.addRaw).toHaveBeenCalledWith(expect.stringContaining("| Alias |"), true); expect(mockCore.summary.write).toHaveBeenCalled(); expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Token usage summary appended")); + // Token table should also be rendered to core.info + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Token Usage")); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Alias")); }); test("uses custom summary title when configured", async () => { @@ -536,5 +540,30 @@ describe("parse_token_usage", () => { expect(section).toContain("
"); expect(section).toContain("Per-request AI credits and token totals"); }); + + test("renderTokenTableAsPlainText strips table separator lines and pipes", () => { + const markdown = ["| # | Alias | Input | Output |", "|--:|-------|------:|-------:|", "| 1 | sonnet46 | 100 | 200 |", "| **Total** | | **100** | **200** |", "", "Legend: `Alias` is the model shorthand.", ""].join("\n"); + + const result = renderTokenTableAsPlainText("Token Usage", markdown); + + expect(result).toContain("Token Usage"); + // separator line is removed (no dash sequences that leak from separator rows) + expect(result).not.toMatch(/---/); + // leading/trailing pipes are stripped + expect(result).not.toMatch(/^\|/m); + expect(result).not.toMatch(/\|$/m); + // bold markers are removed + expect(result).not.toContain("**"); + // data is preserved + expect(result).toContain("sonnet46"); + expect(result).toContain("100"); + expect(result).toContain("200"); + expect(result).toContain("Legend:"); + }); + + test("renderTokenTableAsPlainText prefixes output with title", () => { + const result = renderTokenTableAsPlainText("My Token Usage", "| A |\n|---|\n| 1 |"); + expect(result.startsWith("My Token Usage")).toBe(true); + }); }); });