In Part 1 I showed the trick: a 303 redirect hands you the media UUID that Jira needs to render an image inline. Manual redirect, read the Location header, pull the UUID with a regex. Deterministic. No model.

That is one step. A real report is more than one step. Here is the whole flow, and the repo that runs it.

Create, upload, resolve, embed

Posting an image-rich report to Jira is a four-stage sequence, and the order matters:

  1. Create the issue or comment with placeholder ADF.
  2. Upload each image as an attachment. Multipart, with the X-Atlassian-Token: no-check header. Skip that header and Jira rejects you with a cross-site message that tells you nothing useful.
  3. Resolve each attachment id into a media UUID via the 303 trick from Part 1.
  4. Embed by rebuilding the ADF with real media nodes pointing at the resolved UUIDs.

Every stage is deterministic. There is no point in the pipeline where a language model decides anything. The model, if you use one at all, wrote the words of the report upstream. Everything from “turn this into Jira ADF” onward is byte work.

Raw, Enriched, Builders

The code separates into three layers, and keeping them separate is the whole reason this stays maintainable:

  • Raw - the transport. HTTP calls to Jira. Upload, the 303 resolve, the final update. This layer knows nothing about your report’s shape.
  • Enriched - image handling. Pull width and height straight from the PNG or JPEG bytes, because ADF media nodes want real dimensions and guessing gives you stretched screenshots.
  • Builders - ADF construction. mediaSingle wrapping a media node, tables, status lozenges, the report structure. Pure functions. Input in, ADF out, same every time.

The builders carry one guard worth mentioning: ADF has a size ceiling, and a report with many inline images plus tables can blow past it. The builder caps at 60KB and degrades gracefully rather than letting Jira reject the whole comment. A non-image attachment falls back to an external media node instead of failing the run.

The repo

It is public, AGPL-3.0:

github.com/darco81/context-first-qa-patterns

Clone it, run npm run example, and it executes the full flow against an in-process mock Jira that emits a real 303 - so you see the UUID resolution happen without touching a live instance. 25 tests, green. TypeScript, zero dependency on any orchestration platform. The four n8n workflows that inspired it are included as reference, but the core is standalone - you can lift it into a plain script.

What you get is the transport layer. The deterministic floor. The part that turns “I have a report and some screenshots” into “it renders inline in Jira, correctly, every time.”

Where this stops being a snippet

The repo is the mechanism. It is not the machine.

In production I run this as one stage inside a larger deterministic QA pipeline - orchestration across many tasks, an audit trail, status lifecycle, multi-project config, screenshots captured and diffed before they ever reach this layer. That orchestration is where the real leverage is, and it is not in the repo. The repo gives you the hardest-won trick so you stop losing afternoons to it.

If you are running image-heavy QA reporting at scale and the four-stage flow is a fraction of what you need - multi-task orchestration, deterministic verification, the full pipeline - that is the consulting conversation. See sdet.it/services.

But the 303 trick? That one is yours. Clone it, and go render some images.

Code and mock in the first comment.