Skip to content

Commit e8f5e2f

Browse files
comment format payload limit fixes
1 parent a931edd commit e8f5e2f

2 files changed

Lines changed: 85 additions & 7 deletions

File tree

sync2jira/downstream_issue.py

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646
# This is used to ensure legacy comments are not touched
4747
UPDATE_DATE = datetime(2019, 7, 9, 18, 18, 36, 480291, tzinfo=timezone.utc)
4848

49+
# Jira REST API rejects comment bodies longer than this (characters).
50+
JIRA_COMMENT_BODY_MAX_CHARS = 32767
51+
4952
log = logging.getLogger("sync2jira")
5053
logging.getLogger("snowflake.connector").setLevel(logging.WARNING)
5154

@@ -250,6 +253,52 @@ def _comment_format(comment):
250253
)
251254

252255

256+
def _truncate_jira_comment_body(
257+
body: str, upstream_issue_url: Optional[str] = None
258+
) -> str:
259+
"""
260+
Ensure ``body`` fits Jira's comment size limit.
261+
262+
If truncated, prepend a notice (and optional link to the upstream GitHub issue)
263+
and append a marker so readers know where to find the full thread.
264+
"""
265+
if len(body) <= JIRA_COMMENT_BODY_MAX_CHARS:
266+
return body
267+
268+
log.info(
269+
"Truncating Jira comment body from %d to max %d characters",
270+
len(body),
271+
JIRA_COMMENT_BODY_MAX_CHARS,
272+
)
273+
274+
link_block = ""
275+
if upstream_issue_url:
276+
link_block = f"[View upstream issue on GitHub|{upstream_issue_url}]\n\n"
277+
278+
head = (
279+
"{warning}*(This comment was truncated to fit Jira's "
280+
f"{JIRA_COMMENT_BODY_MAX_CHARS}-character limit.)*{{warning}}\n\n" + link_block
281+
)
282+
tail = (
283+
"\n\n{warning}*(Comment truncated"
284+
+ (
285+
" — see GitHub link above for the full thread.)*"
286+
if upstream_issue_url
287+
else " — full thread not linked.)*"
288+
)
289+
+ "{warning}"
290+
)
291+
292+
budget = JIRA_COMMENT_BODY_MAX_CHARS - len(head) - len(tail)
293+
if budget < 1:
294+
head = "{warning}*(Truncated.)*{warning}\n"
295+
tail = "\n{warning}*(…)*{warning}"
296+
budget = JIRA_COMMENT_BODY_MAX_CHARS - len(head) - len(tail)
297+
298+
truncated_core = body[: max(budget, 0)]
299+
return head + truncated_core + tail
300+
301+
253302
def _comment_format_legacy(comment):
254303
"""
255304
Legacy function to format JIRA comments.
@@ -472,12 +521,15 @@ def check_comments_for_duplicate(client, result, username):
472521
return None
473522

474523

475-
def _find_comment_in_jira(comment, j_comments):
524+
def _find_comment_in_jira(
525+
comment, j_comments, upstream_issue_url: Optional[str] = None
526+
):
476527
"""
477528
Helper function to filter out comments that are matching.
478529
479530
:param Dict comment: Individual comment from upstream
480531
:param List j_comments: Comments from JIRA downstream
532+
:param Optional[str] upstream_issue_url: Upstream issue URL for truncation notices
481533
:returns: Item/None
482534
:rtype: jira.resource.Comment/None
483535
"""
@@ -486,7 +538,9 @@ def _find_comment_in_jira(comment, j_comments):
486538
# touch the comment; return the item as is.
487539
return comment
488540

489-
formatted_comment = _comment_format(comment)
541+
formatted_comment = _truncate_jira_comment_body(
542+
_comment_format(comment), upstream_issue_url
543+
)
490544
legacy_formatted_comment = _comment_format_legacy(comment)
491545
for item in j_comments:
492546
if item.raw["body"] == legacy_formatted_comment:
@@ -505,18 +559,19 @@ def _find_comment_in_jira(comment, j_comments):
505559
return None
506560

507561

508-
def _comment_matching(g_comments, j_comments):
562+
def _comment_matching(g_comments, j_comments, upstream_issue_url: Optional[str] = None):
509563
"""
510564
Function to filter out comments that are matching.
511565
512566
:param List g_comments: Comments from Issue object
513567
:param List j_comments: Comments from JIRA downstream
568+
:param Optional[str] upstream_issue_url: Upstream issue URL (for Jira truncation)
514569
:returns: Returns a list of comments that are not matching
515570
:rtype: List
516571
"""
517572
return list(
518573
filter(
519-
lambda x: _find_comment_in_jira(x, j_comments) is None
574+
lambda x: _find_comment_in_jira(x, j_comments, upstream_issue_url) is None
520575
or x["changed"] is not None,
521576
g_comments,
522577
)
@@ -1066,11 +1121,11 @@ def _update_comments(client, existing, issue):
10661121
# Get all existing comments
10671122
comments = client.comments(existing)
10681123
# Remove any comments that have already been added
1069-
comments_d = _comment_matching(issue.comments, comments)
1124+
comments_d = _comment_matching(issue.comments, comments, issue.url)
10701125
# Loop through the comments that remain
10711126
for comment in comments_d:
10721127
# Format and add them
1073-
comment_body = _comment_format(comment)
1128+
comment_body = _truncate_jira_comment_body(_comment_format(comment), issue.url)
10741129
client.add_comment(existing, comment_body)
10751130
if len(comments_d) > 0:
10761131
log.info("Comments synchronization done on %i comments.", len(comments_d))

tests/test_downstream_issue.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1313,7 +1313,7 @@ def test_update_comments(
13131313
# Assert all calls were made correctly
13141314
mock_client.comments.assert_called_with(self.mock_downstream)
13151315
mock_comment_matching.assert_called_with(
1316-
self.mock_issue.comments, "mock_comments"
1316+
self.mock_issue.comments, "mock_comments", self.mock_issue.url
13171317
)
13181318
mock_comment_format.assert_called_with("mock_comments_d")
13191319
mock_client.add_comment.assert_called_with(
@@ -1730,6 +1730,29 @@ def test_jira_user_display_label(self):
17301730
d._jira_user_display_label(types.SimpleNamespace()),
17311731
)
17321732

1733+
def test_truncate_jira_comment_body_short_unchanged(self):
1734+
text = "short comment"
1735+
self.assertEqual(
1736+
d._truncate_jira_comment_body(text, "https://github.com/o/r/issues/1"),
1737+
text,
1738+
)
1739+
1740+
def test_truncate_jira_comment_body_long_with_issue_url(self):
1741+
issue_url = "https://github.com/o/r/issues/1"
1742+
body = "B" * (d.JIRA_COMMENT_BODY_MAX_CHARS + 500)
1743+
out = d._truncate_jira_comment_body(body, issue_url)
1744+
self.assertLessEqual(len(out), d.JIRA_COMMENT_BODY_MAX_CHARS)
1745+
self.assertIn("truncated to fit Jira's", out)
1746+
self.assertIn(f"[View upstream issue on GitHub|{issue_url}]", out)
1747+
self.assertIn("see GitHub link above for the full thread", out)
1748+
self.assertTrue(out.startswith("{warning}"))
1749+
1750+
def test_truncate_jira_comment_body_long_without_url(self):
1751+
body = "C" * (d.JIRA_COMMENT_BODY_MAX_CHARS + 100)
1752+
out = d._truncate_jira_comment_body(body, None)
1753+
self.assertLessEqual(len(out), d.JIRA_COMMENT_BODY_MAX_CHARS)
1754+
self.assertIn("full thread not linked", out)
1755+
17331756
@mock.patch("jira.client.JIRA")
17341757
def test_check_comments_for_duplicates(self, mock_client):
17351758
"""

0 commit comments

Comments
 (0)