Skip to content

Commit 4582e2e

Browse files
authored
Fix #723: turn markdown into Jira syntax on comments and description (#839)
* Fix #723: turn markdown into Jira syntax on comments and description * Add more tests about converted fields * Add instructions in README * Check pandoc install in healthcheck * Check pandoc in built container * Install pandoc for CI unit tests * Truncate on last word, as suggested by @grahamalama
1 parent 21b3633 commit 4582e2e

File tree

12 files changed

+147
-17
lines changed

12 files changed

+147
-17
lines changed

.github/workflows/ci.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ jobs:
2828
id: setup-python
2929
with:
3030
python-version: "3.12"
31+
- name: Install pandoc
32+
run: sudo apt-get install -y pandoc
3133
- name: Install poetry
3234
run: pipx install poetry
3335
- uses: actions/cache@v3

Dockerfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ RUN $POETRY_HOME/bin/poetry install --without dev --no-root
2323

2424
# `production` stage uses the dependencies downloaded in the `base` stage
2525
FROM python:3.12.2-slim as production
26+
27+
# Install pandoc for markdown to Jira conversions.
28+
RUN apt-get -y update && \
29+
apt-get -y install --no-install-recommends pandoc
30+
2631
ENV PORT=8000 \
2732
PYTHONUNBUFFERED=1 \
2833
PYTHONDONTWRITEBYTECODE=1 \

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ graph TD
7272
7373
# Development
7474
75+
We use [pandoc](https://pandoc.org) to convert markdown to the Jira syntax. Make sure the binary is found in path or [specify your custom location](https://github.com/JessicaTegner/pypandoc#specifying-the-location-of-pandoc-binaries).
76+
7577
- `make start`: run the application locally (http://localhost:8000)
7678
- `make test`: run the unit tests suites
7779
- `make lint`: static analysis of the code base

bin/healthcheck.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,29 @@
22

33
import os
44

5+
import backoff
56
import requests
6-
from requests.adapters import HTTPAdapter, Retry
77

8-
PORT = os.environ["PORT"]
8+
PORT = os.getenv("PORT", "8000")
99

10-
session = requests.Session()
1110

12-
retries = Retry(total=5, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504])
13-
session.mount("http://", HTTPAdapter(max_retries=retries))
14-
if __name__ == "__main__":
15-
response = session.get(f"http://0.0.0.0:{PORT}/")
11+
@backoff.on_exception(
12+
backoff.expo,
13+
requests.exceptions.RequestException,
14+
max_tries=5,
15+
)
16+
def check_server():
17+
url = f"http://0.0.0.0:{PORT}"
18+
response = requests.get(f"{url}/")
1619
response.raise_for_status()
20+
21+
hb_response = requests.get(f"{url}/__heartbeat__")
22+
hb_details = hb_response.json()
23+
# Check that pandoc is installed, but ignore other checks
24+
# like connection to Jira or Bugzilla.
25+
assert hb_details["jira"]["pandoc_install"]
26+
print("Ok")
27+
28+
29+
if __name__ == "__main__":
30+
check_server()

jbi/jira/service.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from jbi import Operation, bugzilla, environment
1717
from jbi.common.instrument import ServiceHealth
18+
from jbi.jira.utils import markdown_to_jira
1819
from jbi.models import ActionContext
1920

2021
from .client import JiraClient, JiraCreateError
@@ -66,6 +67,7 @@ def check_health(self, actions: Actions) -> ServiceHealth:
6667
and self._all_project_custom_components_exist(actions),
6768
"all_projects_issue_types_exist": is_up
6869
and self._all_project_issue_types_exist(actions),
70+
"pandoc_install": markdown_to_jira("- Test") == "* Test",
6971
}
7072
return health
7173

@@ -213,7 +215,9 @@ def create_jira_issue(
213215
fields: dict[str, Any] = {
214216
"summary": bug.summary,
215217
"issuetype": {"name": issue_type},
216-
"description": description[:JIRA_DESCRIPTION_CHAR_LIMIT],
218+
"description": markdown_to_jira(
219+
description, max_length=JIRA_DESCRIPTION_CHAR_LIMIT
220+
),
217221
"project": {"key": context.jira.project},
218222
}
219223
logger.debug(
@@ -257,7 +261,7 @@ def add_jira_comment(self, context: ActionContext):
257261

258262
issue_key = context.jira.issue
259263
formatted_comment = (
260-
f"*({commenter})* commented: \n{{quote}}{comment.body}{{quote}}"
264+
f"*{commenter}* commented: \n{markdown_to_jira(comment.body or "")}"
261265
)
262266
jira_response = self.client.issue_add_comment(
263267
issue_key=issue_key,
@@ -409,7 +413,9 @@ def update_issue_summary(self, context: ActionContext):
409413
bug.id,
410414
extra=context.model_dump(),
411415
)
412-
truncated_summary = (bug.summary or "")[:JIRA_DESCRIPTION_CHAR_LIMIT]
416+
truncated_summary = markdown_to_jira(
417+
bug.summary or "", max_length=JIRA_DESCRIPTION_CHAR_LIMIT
418+
)
413419
fields: dict[str, str] = {
414420
"summary": truncated_summary,
415421
}

jbi/jira/utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import logging
2+
3+
import pypandoc # type: ignore
4+
5+
logging.getLogger("pypandoc").addHandler(logging.NullHandler())
6+
7+
8+
def markdown_to_jira(markdown: str, max_length: int = 0) -> str:
9+
"""
10+
Convert markdown content into Jira specific markup language.
11+
"""
12+
if max_length > 0 and len(markdown) > max_length:
13+
# Truncate on last word.
14+
markdown = markdown[:max_length].rsplit(maxsplit=1)[0]
15+
return pypandoc.convert_text(markdown, "jira", format="md").strip() # type: ignore

poetry.lock

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ statsd = "^4.0.1"
2020
requests = "^2.31.0"
2121
pydantic-settings = "^2.1.0"
2222
asgi-correlation-id = "^4.3.0"
23+
pypandoc = "^1.12"
2324

2425
[tool.poetry.group.dev.dependencies]
2526
pre-commit = "^3.6.1"

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def context_comment_example(action_context_factory) -> ActionContext:
131131
bug__see_also=["https://mozilla.atlassian.net/browse/JBI-234"],
132132
bug__with_comment=True,
133133
bug__comment__number=2,
134-
bug__comment__body="hello",
134+
bug__comment__body="> hello\n>\n\nworld",
135135
event__target="comment",
136136
event__user__login="[email protected]",
137137
jira__issue="JBI-234",

tests/unit/jira/test_utils.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from textwrap import dedent
2+
3+
import pytest
4+
5+
from jbi.jira.utils import markdown_to_jira
6+
7+
8+
def test_markdown_to_jira():
9+
markdown = dedent(
10+
"""
11+
Mixed nested lists
12+
13+
* a
14+
* bulleted
15+
- with
16+
- nested
17+
1. nested-nested
18+
- numbered
19+
* list
20+
21+
this was `inline` value ``that`` is turned into ```monospace``` tag.
22+
23+
this sentence __has__ **bold** and _has_ *italic*.
24+
25+
this was ~~wrong~~.
26+
"""
27+
).lstrip()
28+
29+
jira = dedent(
30+
"""
31+
Mixed nested lists
32+
33+
* a
34+
* bulleted
35+
** with
36+
** nested
37+
**# nested-nested
38+
** numbered
39+
* list
40+
41+
this was {{inline}} value {{that}} is turned into {{monospace}} tag.
42+
43+
this sentence *has* *bold* and _has_ _italic_.
44+
45+
this was -wrong-.
46+
"""
47+
).strip()
48+
49+
assert markdown_to_jira(markdown) == jira
50+
51+
52+
def test_markdown_to_jira_with_malformed_input():
53+
assert markdown_to_jira("[link|http://noend") == "\\[link|http://noend"
54+
55+
56+
@pytest.mark.parametrize(
57+
"markdown, expected, max_length",
58+
[
59+
("a" * 10, "aaaaa", 5),
60+
("aa aaa", "aa", 5),
61+
("aa\naaa", "aa", 5),
62+
("aa\taaa", "aa", 5),
63+
("aaaaaa", "aaaaa", 5),
64+
("aaaaa ", "aaaaa", 5),
65+
],
66+
)
67+
def test_markdown_to_jira_with_max_chars(markdown, expected, max_length):
68+
assert markdown_to_jira(markdown, max_length=max_length) == expected

0 commit comments

Comments
 (0)