From a7f469a86e89ef0966e8d9ae93ede8b8ae75c7b7 Mon Sep 17 00:00:00 2001 From: Anas Khan <83116240+anxkhn@users.noreply.github.com> Date: Sun, 5 Jul 2026 03:51:07 +0530 Subject: [PATCH] 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> --- .../integrations/github/cicd/controller.py | 25 ++++++++++++++----- .../github/cicd/test_github_controller.py | 19 ++++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/sqlmesh/integrations/github/cicd/controller.py b/sqlmesh/integrations/github/cicd/controller.py index c693d09560..58b0664c2d 100644 --- a/sqlmesh/integrations/github/cicd/controller.py +++ b/sqlmesh/integrations/github/cicd/controller.py @@ -1182,13 +1182,26 @@ def get_command_from_comment(self) -> BotCommand: def _chunk_up_api_message(self, message: str) -> t.List[str]: """ - Chunks up the message into `MAX_BYTE_LENGTH` byte chunks + Chunks up the message into chunks of at most `MAX_BYTE_LENGTH` bytes when + UTF-8 encoded. + + Chunk boundaries are placed between characters rather than raw bytes so a + multibyte character that lands on a boundary is never split and dropped. """ - message_encoded = message.encode("utf-8") - return [ - message_encoded[i : i + self.MAX_BYTE_LENGTH].decode("utf-8", "ignore") - for i in range(0, len(message_encoded), self.MAX_BYTE_LENGTH) - ] + chunks: t.List[str] = [] + current = "" + current_length = 0 + for char in message: + char_length = len(char.encode("utf-8")) + if current and current_length + char_length > self.MAX_BYTE_LENGTH: + chunks.append(current) + current = "" + current_length = 0 + current += char + current_length += char_length + if current: + chunks.append(current) + return chunks @property def running_in_github_actions(self) -> bool: diff --git a/tests/integrations/github/cicd/test_github_controller.py b/tests/integrations/github/cicd/test_github_controller.py index 96895a5960..a309d56126 100644 --- a/tests/integrations/github/cicd/test_github_controller.py +++ b/tests/integrations/github/cicd/test_github_controller.py @@ -915,3 +915,22 @@ def test_forward_only_config_falls_back_to_plan_config( controller._context.config.plan.forward_only = False assert controller.forward_only_plan is False + + +def test_chunk_up_api_message_preserves_multibyte_chars( + github_client, make_event_issue_comment, make_controller +): + controller = make_controller(make_event_issue_comment("created", "test"), github_client) + + max_bytes = controller.MAX_BYTE_LENGTH + # Place a multibyte character exactly on the byte boundary between two chunks so + # that byte-oriented slicing would bisect its UTF-8 encoding and drop it. + message = ("a" * (max_bytes - 1)) + "é" + ("b" * max_bytes) + assert len(message.encode("utf-8")) > max_bytes + + chunks = controller._chunk_up_api_message(message) + + # No character is lost, so the chunks reassemble into the original message. + assert "".join(chunks) == message + # Each chunk still respects the GitHub API byte limit. + assert all(len(chunk.encode("utf-8")) <= max_bytes for chunk in chunks)