| name | jira-image-comment |
|---|---|
| description | Post a Jira comment with images that actually render inline — screenshots, before/after strips, charts, diagrams. Use when asked to attach a screenshot to a ticket, add visual evidence to a Jira issue, put a before/after comparison on a ticket, or when a comment needs a picture rather than a link. Also use when images were posted to Jira but show as broken, grey, or as literal text. |
Jira has two comment APIs and only one of them will render your image. The
obvious route silently fails: it returns 201 Created and shows a grey
placeholder box. This skill is the route that works, plus the check that proves
it worked.
Posting to Jira writes to a shared system under the user's own identity. Teammates get notified. Comments cannot be un-sent, only deleted, and the notification has already gone out by then.
So: have an explicit instruction to post. "How would I get an image into a Jira ticket?" is a question about feasibility — answer it, do not act on it. "Post this to DONI-123" is an instruction.
If you have prepared a comment and are not certain you were told to send it, show the user the text and ask. That costs one turn. Getting it wrong costs them a retraction in front of their team.
Never invent the authorization. If you cannot point at the message that told you to post, you were not told to post.
Three environment variables, already set in most of this user's projects:
JIRA_BASE_URL https://<site>.atlassian.net
JIRA_USERNAME the account email
JIRA_API_TOKEN an Atlassian API token
Check them first — env | grep -i ^JIRA — and if they are missing, say so
rather than guessing at credentials. Never echo the token value.
If the Atlassian MCP tools are available, prefer them for text-only comments. They cannot upload attachments, which is the whole reason this skill exists.
curl -s -u "$JIRA_USERNAME:$JIRA_API_TOKEN" \
-X POST \
-H "X-Atlassian-Token: no-check" \
-F "file=@/path/to/shot.png" \
"$JIRA_BASE_URL/rest/api/3/issue/PROJ-123/attachments"X-Atlassian-Token: no-check is required — without it Jira rejects the
upload as a possible XSRF attempt. The form field must be named file.
The response is a JSON array; keep each filename. You do not need the
id — see the trap below.
curl -s -u "$JIRA_USERNAME:$JIRA_API_TOKEN" \
-X POST \
-H "Content-Type: application/json" \
--data '{"body": "Before and after:\n\n!shot.png!\n\nThe gap above the heading is the fix."}' \
"$JIRA_BASE_URL/rest/api/2/issue/PROJ-123/comment"!filename.png! is wiki markup for "embed this attachment". Jira converts it
server-side into a proper ADF media node and mints the Media Services UUID for
you. That minting is the part you cannot do yourself, and it is the entire
reason to use v2 here.
The filename must match the uploaded attachment exactly, including extension.
Sizing works too: !shot.png|width=800!.
Other v2 wiki markup that survives the conversion:
| heading | h3. Title |
| bold / italic | *bold* · _italic_ |
| inline code | {{code}} |
| code block | {code} … {code} |
| table | ||head||head|| then |cell|cell| |
| bullet / number | * item · # item |
| quote | bq. text |
The natural-looking approach is v3 with an Atlassian Document Format body
containing a mediaSingle → media node. It does not work from automation.
The media node's id must be a Media Services UUID, not the attachment id
you got back from the upload. There is no public endpoint that mints one. The
only way to obtain it is to scrape the 303 Location header from
/rest/api/3/attachment/content/{id}.
Put the attachment id in there instead and Jira answers 201 Created and
renders a grey placeholder box. Success status, broken comment — the worst
possible failure mode, because nothing tells you until a human looks at the
ticket.
Let Jira mint the UUID. Use v2.
201 means "comment created", not "image visible". Read it back:
curl -s -u "$JIRA_USERNAME:$JIRA_API_TOKEN" \
"$JIRA_BASE_URL/rest/api/3/issue/PROJ-123/comment/<COMMENT_ID>?expand=renderedBody" \
| python3 -c "
import sys, json, re
d = json.load(sys.stdin)
rb = d.get('renderedBody', '')
print('inline images :', len(re.findall(r'<img[^>]+attachment/content/\d+', rb)))
print('unrendered text :', len(re.findall(r'!\S+\.(png|jpe?g|gif|webp)!', rb)))
"Pass = inline images equals the number you embedded, AND unrendered text is zero.
A non-zero "unrendered text" count means the filename did not match an
attachment on that issue and your markup is sitting there as literal !x.png!
for everyone to see. Fix it with a PUT to the same comment id rather than
posting a second one.
A screenshot on a ticket is read by someone with none of your context, often on a phone, often weeks later.
- Compose pairs into one image. Two side-by-side panels in a single PNG beat two separate attachments — the reader cannot accidentally see one without the other, and it survives being viewed in a notification email.
- Label inside the pixels. A BEFORE / AFTER bar burned into the image keeps working when the caption is scrolled off.
- Print the measurement on the panel.
padding-top: 0px→40pxabove the relevant shot. A 40px change is invisible without a number next to it. - Crop to the change. A full-page screenshot of a 70px band is a picture of everything except the point.
- Say what the image does not prove. If a value was seeded directly into the database rather than entered through the UI, the screenshot proves the render path and nothing about the save path. Put that on the ticket, not just in your own notes — an unqualified "fixed and verified" that turns out to be narrower than it read is how QA loses trust in the evidence.
scripts/jira-image-comment.sh in this skill directory does upload → comment →
verify in one call:
bash scripts/jira-image-comment.sh PROJ-123 comment.md shot1.png shot2.pngcomment.md is v2 wiki markup and should already contain the !shot1.png!
references. The script fails loudly if verification finds unrendered markup.