Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions sqlmesh/integrations/github/cicd/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
19 changes: 19 additions & 0 deletions tests/integrations/github/cicd/test_github_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)