Uh oh!
There was an error while loading. Please reload this page.
Render token table to core.info in parse_token_usage step - #40227
Conversation
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅ |
✅ PR Code Quality Reviewer completed the code quality review. |
✅ Test Quality Sentinel completed test quality analysis. |
There was a problem hiding this comment.
Pull request overview
This PR makes the token usage table visible in the raw GitHub Actions step logs by rendering the markdown token-usage table into a more log-friendly plain-text form and emitting it via core.info, in addition to the existing step summary output.
Changes:
- Added
renderTokenTableAsPlainText(title, markdown)to convert the generated markdown table into log-friendly text. - Updated
main()inparse_token_usage.cjstocore.info()the rendered table before writing the step summary. - Added unit tests for the plain-text renderer and extended the
main()integration test to assertcore.inforeceives the table content.
Show a summary per file
| File | Description |
|---|---|
| actions/setup/js/parse_token_usage.cjs | Adds a plain-text renderer and logs the token usage table via core.info during parsing. |
| actions/setup/js/parse_token_usage.test.cjs | Adds targeted tests for the renderer and updates main() integration expectations around core.info. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 2/2 changed files
- Comments generated: 2
| function renderTokenTableAsPlainText(title, markdown) { | ||
| const plainText = markdown | ||
| .replace(/^\|-+.*-+\|$/gm, "") // Remove table separator lines | ||
| .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}`; | ||
| } |
| expect(result).toContain("Token Usage"); | ||
| // separator line is removed | ||
| expect(result).not.toMatch(/\|--/); | ||
| // leading/trailing pipes are stripped | ||
| expect(result).not.toMatch(/^\|/m); |
✅ Design Decision Gate 🏗️ completed the design decision gate check. No ADR enforcement needed: PR #40227 does not have the 'implementation' label and has 0 new lines of code in business logic directories (default_business_additions=0, threshold=100). Neither enforcement condition is met. |
🧪 Test Quality Sentinel Report✅ Test Quality Score: 80/100 — Excellent
📊 Metrics & Test Classification (3 tests analyzed)
Go: 0 ( 📝 Test Notes
Missing edge cases — Verdict
|
There was a problem hiding this comment.
Skills-Based Review 🧠
Applied /diagnose and /tdd — requesting changes for one confirmed correctness bug plus test coverage gaps that mask it.
📋 Key Themes & Findings
❌ Confirmed Bug — Separator Regex (line 115)
The regex /^\|-+.*-+\|$/gm requires dashes immediately before the closing |, so it silently skips the production separator row:
|--:|-------|------:|-------:|-----------:|...
Every right-aligned column (---:|) ends with :|, not -|. The separator row therefore survives the strip step and lands in the step log as a garbage line like --: | ------- | ------: | -------:. Verifiable with:
node-e"const s='|--:|-------|------:|-------:|'; console.log(/^\\|-+.*-+\\|$/.test(s))"// falseSuggested fix: /^\|(?:[-:\s]+\|)+$/gm — matches any line whose pipe-delimited cells contain only -, :, and spaces.
⚠️ Test Coverage Gaps
- False-positive assertion (line 551):
not.toMatch(/\|--/)passes with the broken regex because the leading|was already stripped before this check runs. - Over-loose integration matchers (lines 199-200):
stringContaining("Alias")would pass even if the function was a no-op. - Missing production-format test: adding
expect(result).not.toContain("---")would immediately catch the separator bug.
✅ Positive Highlights
- Clean, focused, purely additive change — well-scoped to the rendering concern.
- Good JSDoc on the new function.
- The regex pipeline approach and
core.infousage is exactly right; only the first regex needs a fix. - Exporting the function for unit testing is a good practice.
🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 73.9 AIC · ⌖ 14.6 AIC · ⊞ 6.9K
| */ | ||
| function renderTokenTableAsPlainText(title, markdown) { | ||
| const plainText = markdown | ||
| .replace(/^\|-+.*-+\|$/gm, "") // Remove table separator lines |
There was a problem hiding this comment.
[/diagnose] Bug: this regex does not remove separator rows that use column-alignment notation (:---, ---:, :---:), which is exactly what the production table uses.
The production separator in parse_mcp_gateway_log.cjs is:
|--:|-------|------:|-------:|-----------:|...
The pattern ^\|-+.*-+\|$ requires dashes immediately before the closing |, but every right-aligned or center-aligned column ends with :| — so the separator survives stripping and appears verbatim in the step log.
💡 Suggested fix
Match any line whose cells contain only -, :, and spaces:
.replace(/^\|[-:\s|]+\|$/gm,"")// Remove table separator lines (including aligned columns)or more explicitly:
.replace(/^\|(?:[-:\s]+\|)+$/gm,"")// Remove table separator linesA quick verification:
node-e "
constsep='|--:|-------|------:|-------:|';console.log(/^\|(?:[-:\s]+\|)+$/.test(sep));// true
"| expect(result).toContain("Token Usage"); | ||
| // separator line is removed | ||
| expect(result).not.toMatch(/\|--/); |
There was a problem hiding this comment.
[/tdd] This assertion gives a false positive — it passes even when the separator is NOT removed.
After .replace(/^\|/gm, "") strips the leading pipe, the separator row |--:|-------|------:|-------:| becomes --:|-------|------:|-------:|, which no longer matches /\|--/. The separator content is still present in the output; it just lost its leading |.
This means the test green-lights a buggy implementation (the separator regex above misses aligned columns).
💡 Stronger assertion
// Separator dashes should be completely absent (not just moved)expect(result).not.toMatch(/^\s*--/m);// no line starting with dashes// OR more precisely:expect(result.split("\n")).not.toContain(expect.stringMatching(/^[-:\s|]+$/));| }); | ||
| 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"); |
There was a problem hiding this comment.
[/tdd] The unit test uses the same separator format (|--:| ends with :|) that proves the production bug — but the test’s own assertions are not tight enough to catch the failure (see comment on line 551).
Also worth adding a test using the exact separator from parse_mcp_gateway_log.cjs line 172 to lock in the real-world contract:
💡 Suggested additional test input
constproductionSeparator="|--:|-------|------:|-------:|-----------:|------------:|-------------:|-----------:|---------:|";constresult=renderTokenTableAsPlainText("T",`| # |\n${productionSeparator}\n| 1 |`);expect(result).not.toContain("---");// separator fully goneThis would have caught the bug immediately.
| 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")); |
There was a problem hiding this comment.
[/tdd] These matchers are too loose to verify the plain-text rendering actually works. stringContaining("Token Usage") and stringContaining("Alias") would both pass even if renderTokenTableAsPlainText returned the raw markdown unmodified (since | Alias | contains "Alias").
💡 Suggested stronger assertion
Add an assertion that proves markdown was stripped:
// Verify the output is plain text, not markdownexpect(mockCore.info).not.toHaveBeenCalledWith(expect.stringContaining("**"));expect(mockCore.info).not.toHaveBeenCalledWith(expect.stringMatching(/^\|/m));| } | ||
| const markdown = generateTokenUsageSummary(summary); | ||
| if (markdown.length > 0) { | ||
| core.info(renderTokenTableAsPlainText(getSummaryTitle(), markdown)); |
There was a problem hiding this comment.
[/tdd] Minor: calling getSummaryTitle() twice (here and in appendStepSummarySection) is harmless, but if the env var ever becomes expensive to read, this could be extracted into a local variable once.
consttitle=getSummaryTitle();core.info(renderTokenTableAsPlainText(title,markdown));awaitappendStepSummarySection(title,markdown);This also makes the intent clearer — same title for both the log and the summary.
There was a problem hiding this comment.
REQUEST_CHANGES — two distinct correctness bugs must be fixed before merging.
Blocking issues
Separator regex never fires (high)
The regex /^\|-+.*-+\|$/gm on line 115 requires the separator line to end in one or more dashes followed by |. Every separator produced by generateTokenUsageSummary ends in :| (alignment marker), e.g.:
|--:|-------|------:|-------:|-----------:|------------:|-------------:|-----------:|---------:|
So the separator is never removed — it just gets its outer pipes stripped by steps 2–3, becoming --:|-------|...: noise visible in the raw log. Existing inline comment on L123 covers this; the fix needs to land.
Global pipe normalization corrupts non-table content (medium)
See inline comment on line 118 for the full analysis and a suggested row-scoped rewrite that also solves the separator regex in one pass.
Test blind spot
not.toMatch(/\|--/) at L551 passes even when the separator survives (the leading | is stripped by steps 2–3). The test gives false confidence that separator removal works. Existing inline comment on L551 covers this.
Warning
Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
patchdiff.githubusercontent.com
To allow these domains, add them to the
network.allowedlist in your workflow frontmatter:
network:
allowed:
- defaults
- "patchdiff.githubusercontent.com"See Network Configuration for more information.
🔎 Code quality review by PR Code Quality Reviewer · 133.3 AIC · ⌖ 9.68 AIC · ⊞ 5.1K
| .replace(/^\|-+.*-+\|$/gm, "") // Remove table separator lines | ||
| .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 |
There was a problem hiding this comment.
Global pipe normalization is not scoped to table rows — it silently corrupts any | in legend text, inline code, or future non-table content.
💡 Details and suggested fix
After steps 2–3 strip only the outermost | from each table row, step 4's .replace(/\s*\|\s*/g, " | ") runs on the entire markdown string — including the Legend section and any inline code. If the legend (or any future content produced by generateTokenUsageSummary) ever contains a | character (e.g. Bitwise OR: \a | b``) it will be silently reformatted with no test catching the regression.
A safer approach scopes the transformation to table rows only, and also naturally fixes the separator regex bug (line 115) — a filter like /^\|[\|:\-\s]+\|$/ reliably matches any GFM alignment separator regardless of whether colons are present:
functionrenderTokenTableAsPlainText(title,markdown){constplainText=markdown.split("\n").filter(line=>!/^\|[\|:\-\s]+\|$/.test(line.trim()))// drop GFM separator rows reliably.map(line=>line.startsWith("|")&&line.endsWith("|")
? line.slice(1,-1).replace(/\s*\|\s*/g," | ")// normalize only table rows
: line).join("\n").replace(/\*\*(.*?)\*\*/g,"$1").replace(/\n{3,}/g,"\n\n").trim();return`${title}\n\n${plainText}`;}This also eliminates the need for three separate chained .replace() calls with global regexes over the whole string.
| }); | ||
| test("renderTokenTableAsPlainText prefixes output with title", () => { | ||
| const result = renderTokenTableAsPlainText("My Token Usage", "| A |\n|---|\n| 1 |"); |
There was a problem hiding this comment.
The |---| separator in this fixture accidentally exercises the broken regex path — the test doesn't prove the function handles real alignment-decorated separators.
💡 Details
|---| ends with -|, so it does match the current (buggy) regex /^\|-+.*-+\|$/gm. This means renderTokenTableAsPlainText prefixes output with title silently passes even with the broken separator regex, creating the illusion of correctness.
Every real separator produced by generateTokenUsageSummary ends with :|, e.g. |--:|-------|------:|-------:|. Fix the fixture to match:
constresult=renderTokenTableAsPlainText("My Token Usage","| A | B |\n|---|---:|\n| 1 | 2 |");With the current broken regex this test would then fail, correctly surfacing the bug in CI.
| } | ||
| const markdown = generateTokenUsageSummary(summary); | ||
| if (markdown.length > 0) { | ||
| core.info(renderTokenTableAsPlainText(getSummaryTitle(), markdown)); |
There was a problem hiding this comment.
getSummaryTitle() is called twice in the same conditional block — store it in a const to avoid a redundant env read and a theoretical TOCTOU on process.env.
💡 Suggested fix
constmarkdown=generateTokenUsageSummary(summary);if(markdown.length>0){consttitle=getSummaryTitle();// single readcore.info(renderTokenTableAsPlainText(title,markdown));awaitappendStepSummarySection(title,markdown);}In the current code, if process.env.GH_AW_TOKEN_USAGE_SUMMARY_TITLE is mutated between the two calls (e.g. by a test that doesn't reset env cleanly), the core.info title and the step-summary section title will diverge silently.
pelikhan
commented
Jun 19, 2026
@copilot run pr-finisher skill |
…tion Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Done. Fixed both issues raised in the review:
All 23 JS tests pass locally. |
The token usage table was only written to the GitHub step summary (collapsible
<details>block), making it invisible in the raw step log output. Step 58 now also emits the table directly tocore.info.Changes
parse_token_usage.cjs: AddedrenderTokenTableAsPlainText(title, markdown)— strips table separator rows, leading/trailing pipes, and**bold**markers — and callscore.info()with the result before writing to the step summary.parse_token_usage.test.cjs: Tests forrenderTokenTableAsPlainText(separator removal, pipe stripping, bold removal, title prefix) and assertscore.inforeceives table content in themain()integration test.Example log output (step 58):