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);
+ });
});
});