Skip to content

Commit a7f469a

Browse files
committed
fix(cicd): chunk bot messages on character boundaries
_chunk_up_api_message sliced the UTF-8 encoded message at fixed byte offsets and decoded each slice with errors="ignore". A multibyte character that straddled a chunk boundary had its bytes split across two slices, so the "ignore" handler silently dropped it from the bot's PR comment or check-run output. Accumulate characters into a chunk while the running UTF-8 byte length stays within MAX_BYTE_LENGTH instead of slicing raw bytes, so no character is ever bisected. ASCII-only messages chunk identically to before, and an empty message still yields no chunks. Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
1 parent b0bc176 commit a7f469a

2 files changed

Lines changed: 38 additions & 6 deletions

File tree

sqlmesh/integrations/github/cicd/controller.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,13 +1182,26 @@ def get_command_from_comment(self) -> BotCommand:
11821182

11831183
def _chunk_up_api_message(self, message: str) -> t.List[str]:
11841184
"""
1185-
Chunks up the message into `MAX_BYTE_LENGTH` byte chunks
1185+
Chunks up the message into chunks of at most `MAX_BYTE_LENGTH` bytes when
1186+
UTF-8 encoded.
1187+
1188+
Chunk boundaries are placed between characters rather than raw bytes so a
1189+
multibyte character that lands on a boundary is never split and dropped.
11861190
"""
1187-
message_encoded = message.encode("utf-8")
1188-
return [
1189-
message_encoded[i : i + self.MAX_BYTE_LENGTH].decode("utf-8", "ignore")
1190-
for i in range(0, len(message_encoded), self.MAX_BYTE_LENGTH)
1191-
]
1191+
chunks: t.List[str] = []
1192+
current = ""
1193+
current_length = 0
1194+
for char in message:
1195+
char_length = len(char.encode("utf-8"))
1196+
if current and current_length + char_length > self.MAX_BYTE_LENGTH:
1197+
chunks.append(current)
1198+
current = ""
1199+
current_length = 0
1200+
current += char
1201+
current_length += char_length
1202+
if current:
1203+
chunks.append(current)
1204+
return chunks
11921205

11931206
@property
11941207
def running_in_github_actions(self) -> bool:

tests/integrations/github/cicd/test_github_controller.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,3 +915,22 @@ def test_forward_only_config_falls_back_to_plan_config(
915915

916916
controller._context.config.plan.forward_only = False
917917
assert controller.forward_only_plan is False
918+
919+
920+
def test_chunk_up_api_message_preserves_multibyte_chars(
921+
github_client, make_event_issue_comment, make_controller
922+
):
923+
controller = make_controller(make_event_issue_comment("created", "test"), github_client)
924+
925+
max_bytes = controller.MAX_BYTE_LENGTH
926+
# Place a multibyte character exactly on the byte boundary between two chunks so
927+
# that byte-oriented slicing would bisect its UTF-8 encoding and drop it.
928+
message = ("a" * (max_bytes - 1)) + "é" + ("b" * max_bytes)
929+
assert len(message.encode("utf-8")) > max_bytes
930+
931+
chunks = controller._chunk_up_api_message(message)
932+
933+
# No character is lost, so the chunks reassemble into the original message.
934+
assert "".join(chunks) == message
935+
# Each chunk still respects the GitHub API byte limit.
936+
assert all(len(chunk.encode("utf-8")) <= max_bytes for chunk in chunks)

0 commit comments

Comments
 (0)