start-ticket: render real markdown in Jira comments - #5
Conversation
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>
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>
|
Pushed 749cbc1, which fixes a case this branch rejects outright. ADF's This is the same opaque failure mode called out in the PR description for I hit it posting an implementation plan that referred to a commit as 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 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: Two things I left alone
|
|
No interest in using John Gruber's original perl code instead? Kidding |
shaundrong
left a comment
There was a problem hiding this comment.
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.
@shaundrong yes indeed, that's what the |
jira-commentadvertises 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:The failure is silent, which is what makes it easy to hit — the caller gets
Posted comment 98666either way, and only notices when someone looks at the ticket.jira-tickethad the mirror-image gap: itsadf2mdfilter dropped all text marks and had notablecases, 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
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-commentnow calls it. Interface, argument parsing, env validation, curl call and error handling are all unchanged.jira-ticketadf2mdnow 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.SKILL.mdand in thejira-commentheader, 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:
\|) were splitting table cells, dropping a column\\|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 bareHTTP 400 {"errorMessages":["INVALID_INPUT"],"errors":{}}with no indication of which node was at fault.Known remaining approximations
adf2mdis 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