Skip to content

Commit e8c640f

Browse files
authored
Merge pull request #358 from hdamker/fix/annotation-body-colon-escaping
fix(validation): keep colons readable in annotation bodies
2 parents 01ae1b8 + ac34c37 commit e8c640f

2 files changed

Lines changed: 34 additions & 22 deletions

File tree

validation/output/annotations.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,21 +63,22 @@ class AnnotationResult:
6363
# ---------------------------------------------------------------------------
6464

6565

66-
def _sanitize_message(text: str) -> str:
67-
"""Sanitize a message for use in a workflow command.
66+
def _sanitize_data(text: str) -> str:
67+
"""Sanitize command data for use after the workflow command delimiter.
6868
69-
Workflow commands use ``::`` as delimiters and newlines as
70-
terminators. Both must be escaped in the message body.
69+
GitHub Actions command data must escape percent signs and line breaks.
70+
Colons are only escaped in command properties, not the annotation body.
7171
"""
7272
text = text.replace("\r\n", " ").replace("\r", " ").replace("\n", " ")
73-
# Percent-encode the characters that GitHub Actions interprets specially
74-
# in workflow command data: %, \r, \n, and :
75-
# Using the documented encoding: https://github.com/actions/toolkit
7673
text = text.replace("%", "%25")
77-
text = text.replace(":", "%3A")
7874
return text
7975

8076

77+
def _sanitize_property(text: str) -> str:
78+
"""Sanitize a workflow command property value."""
79+
return _sanitize_data(text).replace(":", "%3A").replace(",", "%2C")
80+
81+
8182
def _build_command(finding: dict) -> str:
8283
"""Build a single ``::error``/``::warning``/``::notice`` command."""
8384
level = finding.get("level", "hint")
@@ -104,9 +105,9 @@ def _build_command(finding: dict) -> str:
104105
params = f"file={path},line={line}"
105106
if col is not None:
106107
params += f",col={col}"
107-
params += f",title={_sanitize_message(title)}"
108+
params += f",title={_sanitize_property(title)}"
108109

109-
return f"::{command} {params}::{_sanitize_message(message)}"
110+
return f"::{command} {params}::{_sanitize_data(message)}"
110111

111112

112113
# ---------------------------------------------------------------------------

validation/tests/test_output_annotations.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
ANNOTATION_LIMIT,
77
AnnotationResult,
88
_build_command,
9-
_sanitize_message,
9+
_sanitize_data,
10+
_sanitize_property,
1011
generate_annotations,
1112
)
1213
from validation.postfilter.engine import PostFilterResult
@@ -56,29 +57,39 @@ def _make_result(findings: list[dict]) -> PostFilterResult:
5657

5758

5859
# ---------------------------------------------------------------------------
59-
# _sanitize_message
60+
# _sanitize_data
6061
# ---------------------------------------------------------------------------
6162

6263

63-
class TestSanitizeMessage:
64+
class TestSanitizeData:
6465
def test_newlines_replaced(self):
65-
assert " " in _sanitize_message("line1\nline2")
66-
assert "\n" not in _sanitize_message("line1\nline2")
66+
assert " " in _sanitize_data("line1\nline2")
67+
assert "\n" not in _sanitize_data("line1\nline2")
6768

6869
def test_carriage_return_replaced(self):
69-
assert "\r" not in _sanitize_message("a\rb")
70+
assert "\r" not in _sanitize_data("a\rb")
7071

7172
def test_crlf_replaced(self):
72-
assert "\r\n" not in _sanitize_message("a\r\nb")
73+
assert "\r\n" not in _sanitize_data("a\r\nb")
7374

75+
def test_colons_unchanged(self):
76+
assert _sanitize_data("key::value") == "key::value"
77+
78+
def test_plain_text_unchanged(self):
79+
assert _sanitize_data("hello world") == "hello world"
80+
81+
82+
# ---------------------------------------------------------------------------
83+
# _sanitize_property
84+
# ---------------------------------------------------------------------------
85+
86+
87+
class TestSanitizeProperty:
7488
def test_colons_encoded(self):
75-
result = _sanitize_message("key::value")
89+
result = _sanitize_property("key::value")
7690
assert "::" not in result
7791
assert "%3A" in result
7892

79-
def test_plain_text_unchanged(self):
80-
assert _sanitize_message("hello world") == "hello world"
81-
8293

8394
# ---------------------------------------------------------------------------
8495
# _build_command
@@ -163,7 +174,7 @@ def test_rule_id_fallback_in_message_body(self):
163174
def test_suggestion_appended(self):
164175
f = _make_finding(message="Bad path", suggestion="Use kebab-case")
165176
cmd = _build_command(f)
166-
assert "Bad path | Suggestion%3A Use kebab-case" in cmd
177+
assert "Bad path | Suggestion: Use kebab-case" in cmd
167178

168179
def test_no_suggestion(self):
169180
f = _make_finding(message="Bad path")

0 commit comments

Comments
 (0)