Skip to content

start-ticket: render real markdown in Jira comments - #5

Merged
kurowski merged 2 commits into
mainfrom
fix-jira-comment-markdown
Aug 21, 2026
Merged

start-ticket: render real markdown in Jira comments#5
kurowski merged 2 commits into
mainfrom
fix-jira-comment-markdown

Conversation

@kurowski

Copy link
Copy Markdown
Member

jira-comment advertises itself as taking markdown, but its jq converter only understood headings, bullet lists, ordered lists and paragraphs. Everything else was passed through as literal text with no warning, so a comment written with tables, bold, inline code or [text](url) links posted as visible syntax.

I hit this posting a status comment on UCDCW-158. The table came out as one paragraph of pipes and **bold** rendered with the asterisks showing:

- heading: Status
- paragraph: PHP 8.4 is live and verified on both **dev** and **qa**.
- paragraph: | Environment | Type | PHP | Drupal | |---|---|---|---| | dev | canonical | ...

The failure is silent, which is what makes it easy to hit — the caller gets Posted comment 98666 either way, and only notices when someone looks at the ticket.

jira-ticket had the mirror-image gap: its adf2md filter dropped all text marks and had no table cases, so a table fell through to the generic child-concatenation branch and its cells ran together with no delimiters. Between the two, a comment could not survive a round trip.

Changes

  • New md2adf.py — markdown → ADF covering headings, bullet and ordered lists (2-space indent nests), tables with a |---| delimiter row, blockquotes, fenced code blocks with optional language, thematic rules, and the inline marks **strong**, *em*, _em_, `code`, ~~strike~~, [text](url). Inline code is literal, so `**this**` stays as written. Unrecognised input still becomes paragraph text rather than being dropped.
  • jira-comment now calls it. Interface, argument parsing, env validation, curl call and error handling are all unchanged.
  • jira-ticket adf2md now emits marks and tables. Ordered lists keep their numbering instead of rendering as bullets, nested lists indent rather than flatten, and blockquote continuation lines keep their > prefix.
  • Docs — supported syntax is listed in SKILL.md and in the jira-comment header, so a caller doesn't have to read the converter to know what survives.

On the choice of Python

The conversion needs inline tokenisation with mark nesting and precedence, which is awkward to express and harder to maintain in jq. The bash entrypoint is untouched and jq still builds the request payload, so the only new dependency is python3 — already present in the devcontainer image these skills run in. Happy to rework it in jq if uniformity across the four helpers matters more here.

Testing

Verified end to end against a real Jira instance, not just locally: a fixture exercising every supported construct posts successfully and reads back identical apart from _em_ normalising to *em*. All test comments were deleted afterward.

Edge cases checked: empty input, whitespace-only input, an unclosed code fence, an unclosed bold run, a header-only table, ragged table rows (short and long), four-level list nesting, escaped pipes inside cells, links nested inside bold, and non-ASCII/emoji text. Every one produces a valid ADF document.

Two bugs were found and fixed through that pass:

  • escaped pipes (\|) were splitting table cells, dropping a column
  • pipes were double-escaped on read-back (\\| instead of \|)

One more worth calling out, since it produces a completely opaque error: ADF rejects "attrs": []. Python emits {} for an empty dict so the converter avoids it naturally, but the original symptom was a bare HTTP 400 {"errorMessages":["INVALID_INPUT"],"errors":{}} with no indication of which node was at fault.

Known remaining approximations

adf2md is still a plaintext approximation by design. Nested lists render with a blank line before the nested block (valid markdown, renders correctly), and _em_ normalises to *em*. Panels, media and status lozenges fall through to their text content as before.

🤖 Generated with Claude Code

jira-comment advertised itself as taking markdown, but its jq converter only
understood headings, bullet lists, ordered lists and paragraphs. Everything
else was passed through as literal text with no warning, so a comment written
with tables, bold, inline code or [text](url) links posted as visible syntax:
a markdown table became one paragraph of pipes, and `**bold**` rendered with
the asterisks showing. The failure was silent, which is what made it easy to
hit -- the caller gets a "Posted comment" success either way.

jira-ticket had the mirror-image gap. Its adf2md filter dropped all text marks
and had no table cases, so a table fell through to the generic child-
concatenation branch and its cells ran together with no delimiters. Between
the two, a comment could not survive a round trip.

Changes:

- Add md2adf.py, a markdown-to-ADF converter covering headings, bullet and
  ordered lists (2-space indent nests), tables with a |---| delimiter row,
  blockquotes, fenced code blocks with an optional language, thematic rules,
  and the inline marks **strong**, *em*, _em_, `code`, ~~strike~~ and
  [text](url). Inline code is literal, so `**this**` stays as written.
  Unrecognised input still becomes paragraph text rather than being dropped.

- Point jira-comment at md2adf.py. The interface, argument parsing, env
  validation, curl call and error handling are unchanged.

- Teach jira-ticket's adf2md to emit marks and tables, so a posted comment
  reads back as the markdown that produced it. Ordered lists now keep their
  numbering instead of rendering as bullets, nested lists are indented rather
  than flattened, and blockquote continuation lines keep their "> " prefix.

- Document the supported syntax in SKILL.md and in the jira-comment header,
  so a caller does not have to read the converter to know what will survive.

On the choice of Python: the conversion needs inline tokenisation with mark
nesting and precedence, which is awkward to express and harder to maintain in
jq. The bash entrypoint is untouched and jq still builds the request payload.
Happy to rework it in jq if uniformity matters more here.

Verified end to end against a real Jira instance: a fixture exercising every
supported construct posts successfully and reads back byte-identical apart
from _em_ normalising to *em*. Edge cases checked include empty input, an
unclosed code fence, an unclosed bold run, a header-only table, ragged table
rows, four-level list nesting, escaped pipes inside cells, and non-ASCII text.
Two bugs found and fixed that way: escaped pipes were splitting cells, and
pipes were double-escaped on read-back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kurowski
kurowski requested a review from shaundrong August 21, 2026 13:07
ADF's code mark is exclusive. A code span nested inside another mark --
**`ref`**, *`ref`*, ~~`ref`~~ or [`ref`](url) -- emitted marks such as
["strong", "code"], which Jira rejects with

  HTTP 400 {"errorMessages":["INVALID_INPUT"],"errors":{"comment":"INVALID_INPUT"}}

The error names no node, so nothing points at the offending span. Hit while
posting an implementation plan that referred to a commit as **`fa78c34`**; the
comment failed outright and the size limit looked like the more obvious
suspect, which cost a detour.

The docstring already promised "inline code wins over everything" for the
opposite case, a code span containing markup. This extends that rule to the
nesting direction: text keeps the code mark and loses the decoration. Enforced
in text_node, the single point every text node passes through, so it also
covers link labels.

Verified against a real Jira instance: the fixture below posts HTTP 400 before
this change and succeeds after, reading back with the code spans intact.

  Bold with code: **`fa78c34`** and off (**`'0'`**) done.
  Em with code: *`italic-code`* done.
  Strike with code: ~~`gone`~~ done.
  Link with code: [`linked-code`](https://example.com) done.
  Nested both: **bold and `code` inside** done.
  Code containing markup: `**not bold**` stays literal.

Bold spanning a code span keeps its bold on the surrounding text; only the code
span itself drops it. Plain **strong**, *em*, _em_, ~~strike~~, links and code
are unaffected. Test comments were deleted afterward.

Also widens the --help sed range to 2,19 to match the grown header block, which
otherwise leaked "set -euo pipefail" into the output.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kurowski

Copy link
Copy Markdown
Member Author

Pushed 749cbc1, which fixes a case this branch rejects outright.

ADF's code mark is exclusive, so a code span nested inside another mark emits marks like ["strong", "code"] and Jira refuses the whole comment:

HTTP 400 {"errorMessages":["INVALID_INPUT"],"errors":{"comment":"INVALID_INPUT"}}

This is the same opaque failure mode called out in the PR description for "attrs": [] — the error names no node, so nothing points at the offending span.

I hit it posting an implementation plan that referred to a commit as **`fa78c34`**. Worth noting how the failure misleads: the plan was 33.7 KB of ADF against Jira's 32,767-byte comment limit, so the size looked like the obvious cause. I split the comment in two, and both halves still failed — which is what finally pointed at the marks. Once fixed, the whole 33,690-byte plan posted as a single comment, so the limit had never been the problem at all.

The docstring already promised "inline code wins over everything" for the opposite direction — a code span containing markup. The fix just extends that rule to the nesting direction: keep code so the text stays literal, drop the decoration. It's enforced in text_node, the single point every text node passes through, so link labels are covered too, and bold spanning a code span keeps its bold on the surrounding text — only the span itself loses it.

Verified against a real Jira instance the same way you did. This fixture returns 400 before the change and succeeds after:

Bold with code: **`fa78c34`** and off (**`'0'`**) done.
Em with code: *`italic-code`* done.
Strike with code: ~~`gone`~~ done.
Link with code: [`linked-code`](https://example.com) done.
Nested both: **bold and `code` inside** done.
Code containing markup: `**not bold**` stays literal.
Plain marks still work: **strong**, *em*, _em_, ~~strike~~, [link](https://example.com), `code`.

Test comments were deleted afterward.

Also in the commit: --help was printing set -euo pipefail, because the header block grew past the hardcoded sed -n '2,18p' range. Now 2,19p.

Two things I left alone

  • adf2md doesn't merge adjacent same-mark runs. **bold and `code` inside** reads back as **bold and **`code`** inside**. The ADF is correct (three text nodes: strong, code, strong) and it renders fine, it just looks odd raw. Pre-existing, and consistent with adf2md being a documented plaintext approximation — but my change makes it visible, since that input couldn't post at all before.
  • Comment threading. Unrelated to this PR, but worth recording since it came up while working around the size split: Jira Cloud issue comments are flat. The comment resource exposes no parent field and parentId on a PUT returns 400, so there's no way to post a reply to a specific comment. Long content has to fit one comment or read as separate ones.

@shaundrong

Copy link
Copy Markdown
Contributor

No interest in using John Gruber's original perl code instead? Kidding

@shaundrong shaundrong left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Can we have the inverse where we pull comments from jira into markdown for PR or issue comments? It would be nice to pull out code or design details from jira so we can better record them as repo artifacts.

@kurowski

Copy link
Copy Markdown
Member Author

Can we have the inverse where we pull comments from jira into markdown for PR or issue comments?

@shaundrong yes indeed, that's what the jira-ticket script does:

ucdcsis2 on  UCDCSISDEV-209-implement-multi-factor-authentication [?] via 🐘 v8.3.32 
⬢ [Docker] ❯ /home/vscode/.claude/plugins/cache/claude/uceap/*/skills/start-ticket/scripts/jira-ticket UP-1977
# UP-1977 — Update Drupal Core 10.6.15
Status: Ready for QA

## Description

https://www.drupal.org/project/drupal/releases/10.6.15 


## Comments (3)

### Shaun Drong — 2026-08-20T15:17:26.625-0700

feature branch for this and https://uceapit.atlassian.net/browse/UP-1978 available at: https://pr-3010-myeap2.pantheonsite.io/



### Brandt Kurowski — 2026-08-21T06:14:17.559-0700

@Shaun Drong since this is a small update should we merge it straight to QA for testing there and just skip thefeature environment?



### Shaun Drong — 2026-08-21T08:09:42.415-0700

@Brandt Kurowski yep we can do that. It seemed to a pretty clean composer update. 
@Que Do Note I’ve moved up to qa for regression testing per this conversation.```

@kurowski
kurowski merged commit f5adbf4 into main Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants