From b1614fa01acd7d85a9ac3ccdf352f361a2dc0958 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Wed, 12 Aug 2026 10:15:47 +0000 Subject: [PATCH] fix: keep pipes and backticks in checked values from breaking the table A commit subject is arbitrary text: a | inside a table cell ended the cell, and a backtick ended the single-backtick code span and rendered the rest of the row as markdown. The value cell now escapes pipes and grows the span delimiter past the longest backtick run in the value, with padding spaces when it starts or ends with one. --- main.py | 28 ++++++++++++++++++++++++++-- main_test.py | 26 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 649c3bf..1cc1bf0 100755 --- a/main.py +++ b/main.py @@ -558,12 +558,32 @@ def _skip_count(results: list[ScopeResult]) -> int: return sum(1 for scope in results if scope.status == "skip") +def _table_cell_code(text: str) -> str: + """Render a checked value as a code span safe for a Markdown table cell. + + A commit subject is arbitrary text. A ``|`` inside a cell ends the cell, + and a backtick would end a fixed single-backtick span, so the delimiter + grows past the longest backtick run in the value instead. A value that + starts or ends with a backtick additionally needs the padding spaces — + CommonMark strips exactly one of each. + """ + escaped = text.replace("|", "\\|") + delimiter = "`" + while delimiter in escaped: + delimiter += "`" + if escaped.startswith("`") or escaped.endswith("`"): + escaped = f" {escaped} " + return f"{delimiter}{escaped}{delimiter}" + + def _markdown_table(results: list[ScopeResult]) -> str: """Render the failure table shared by summary and PR comment. Only failed scopes appear, so a per-row result column would read ``\u274c`` on every row and carry no information; the pass/fail picture for everything - else lives in the details block. + else lives in the details block. Listing every scope was tried and + reverted: the table grew with the pull request, and it duplicated the + details block line for line. """ rows = [ "| Scope | Checked value | Failed checks |", @@ -576,7 +596,7 @@ def _markdown_table(results: list[ScopeResult]) -> str: if scope.status != "fail": continue value = _scope_value(scope) - value_display = f"`{value}`" if value else "\u2014" + value_display = _table_cell_code(value) if value else "\u2014" if scope.raw_text and not scope.checks: links = "_output could not be parsed \u2014 see details_" else: @@ -714,6 +734,10 @@ def _scope_value(scope: ScopeResult, max_len: int = 60) -> str: # the number of commits in the pull request or rules in the config. # - The table lists only failed scopes; there is no per-row result column # because it would read ❌ on every row. Passing scopes live in the details. +# An all-scope table with a verdict column was tried and reverted: it grew +# with the pull request and duplicated the details block line for line. +# - A checked value renders as a code span with pipes escaped and the span +# delimiter grown past any backtick run, so a subject cannot break the table. # - Values are capped at 60 characters with a literal "..." suffix, except on a # failing scope, where the details block prints the value in full — it is the # one value the reader has to act on and the cap can hide the reason. diff --git a/main_test.py b/main_test.py index 9fe0a34..e5f6353 100644 --- a/main_test.py +++ b/main_test.py @@ -803,6 +803,32 @@ def test_table_has_no_constant_result_column(self): self.assertIn("| Scope | Checked value | Failed checks |", body) self.assertNotIn("| Result |", body) + @staticmethod + def _failing_scope_with_value(value: str) -> main.ScopeResult: + return main.ScopeResult( + label="Commit 1/1", + checks=[make_check("message", status="fail", value=value)], + ) + + def test_pipe_in_a_value_does_not_break_the_table(self): + table = main._markdown_table([self._failing_scope_with_value("feat: a|b")]) + self.assertIn("`feat: a\\|b`", table) + + def test_backtick_in_a_value_does_not_end_the_code_span(self): + """The span delimiter grows past the longest backtick run inside.""" + table = main._markdown_table( + [self._failing_scope_with_value("fix: escape `quoted` text")] + ) + self.assertIn("``fix: escape `quoted` text``", table) + + def test_value_ending_in_a_backtick_gets_padding_spaces(self): + """CommonMark reads a backtick against the delimiter as part of it; + the padding spaces keep the content and the fence apart.""" + table = main._markdown_table( + [self._failing_scope_with_value("fix: dangling `")] + ) + self.assertIn("`` fix: dangling ` ``", table) + def test_body_opens_with_hidden_marker(self): body = main.render_job_summary([pass_scope("Branch")]) self.assertTrue(body.startswith(main.COMMENT_MARKER))