Exports a single Jira workitem and its full dependency graph to markdown — including direct links, Epic children (recursively), comments, attachments, and pull request info. All person names and emails are anonymized.
Use case: You close a PRODUCT-XXXXX feature ticket and need to write release notes, documentation, or a blog post. This tool exports the full context — the PRODUCT ticket, linked Epics, all stories/bugs/tasks under those Epics, comments, and PR references — so you can write accurate docs even when the implementation diverged from the original plan.
# 1. Create and activate a virtual environment
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# 2. Install dependencies
pip install -r requirements.txt
# 3. Create your config
cp .jira.config.example .jira.config # then fill in your credentialsJira credentials. Never commit this file.
SERVER: https://your-org.atlassian.net
EMAIL: you@example.com
TOKEN: your_api_token_here
Generate an API token at: https://id.atlassian.com/manage-profile/security/api-tokens
Alternatively, set environment variables JIRA_SERVER, JIRA_EMAIL, JIRA_TOKEN (they take precedence).
# Recommended: use the wrapper script (handles venv + install automatically)
./runme.sh PRODUCT-12345Extra arguments are forwarded, so ./runme.sh PRODUCT-12345 --link-style=github works.
Alternatively, invoke Python directly after activating the venv:
source .venv/bin/activate
python src/main.py PRODUCT-12345 --export-dir=/path/to/output
SPECS_PATH=/path/to/specs-repo python src/main.py PRODUCT-12345
VAULT_PATH=/path/to/obsidian-vault python src/main.py PRODUCT-12345The destination is resolved from the first rule that applies. There is no default — with none of these set the run stops immediately with an error, before contacting Jira.
| # | Given | Destination | Layout | Links |
|---|---|---|---|---|
| 1 | --export-dir=<path> |
<path> |
nested | obsidian |
| 2 | SPECS_PATH=<repo> |
<repo>/<specifications|ideas>/<ID>-<slug>/jira-import |
flat | github |
| 3 | VAULT_PATH=<vault> |
<vault>/jira-products |
nested | obsidian |
| 4 | none of the above | error | — | — |
An environment variable set to the empty string counts as unset.
Under rule 2, PRODFB- keys go to ideas/ and every other key goes to
specifications/. The specs repo is used only when SPECS_PATH exists, the
target subfolder exists in it, and it is the root of a git repository (a .git
directory, or a .git file in a worktree). If any check fails, the run prints
a Note: with the reason and falls back to rule 3; with no usable VAULT_PATH
either, it stops with both reasons.
The <ID>-<slug> folder is matched case-insensitively, and an existing <ID>
or <ID>-* folder is used as it is, never renamed. If several match, the run
stops and asks you to pick one with --export-dir. If none exists, a new
folder is named from the root ticket's summary: people's names, emails and
mentions are scrubbed out, the rest is lowercased to ASCII letters, digits and
hyphens, and cut to at most 40 characters at a word boundary. Export a
distributed trace becomes PRODUCT-14279-export-a-distributed-trace. A
summary with nothing left after that gives a bare <ID> folder. Company names
are not scrubbed and stay in the slug.
--link-style=obsidian|github overrides the inferred link style.
Starting from the root workitem, the tool:
- Fetches the root issue with all fields, comments, and attachments
- Fetches all directly linked issues (all link types: blocks, relates to, duplicates, etc.)
- Traverses all Epics found in steps 1-2, recursively fetching children, grandchildren, etc. (stories, bugs, tasks, sub-tasks)
- Fetches pull request info for every issue via Jira's dev-status API
- Anonymizes PII — person names become
User-1,User-2(consistent across the export), emails are scrubbed, @mentions are replaced
Each issue is exported exactly once (deduplicated).
Nested (--export-dir or VAULT_PATH) — the destination holds many
imports, so each gets its own subdirectory and a registry lists them:
<destination>/
├── export-index.md # registry listing all imports
├── PRODUCT-12345/ # one import = one subdirectory
│ ├── PRODUCT-12345-index.md # per-import index (full detail, hierarchy, relationships)
│ ├── PRODUCT-12345/ # root ticket files
│ │ ├── PRODUCT-12345.md
│ │ ├── PRODUCT-12345-comments.md
│ │ └── attachments/
│ ├── EPIC-100/ # linked/epic_child ticket files
│ │ ├── EPIC-100.md
│ │ ├── EPIC-100-comments.md
│ │ └── attachments/
│ └── STORY-200/
│ └── ...
├── PRODUCT-67890/ # another import, fully separate
│ ├── PRODUCT-67890-index.md
│ └── ...
Flat (SPECS_PATH) — the destination already belongs to one Value
Increment, so there is no <ID> level and no registry. Comments are inlined
into the ticket page rather than written as separate files:
<repo>/specifications/PRODUCT-12345-some-slug/jira-import/
├── PRODUCT-12345-index.md
├── PRODUCT-12345/
│ ├── PRODUCT-12345.md # comments inlined under "## Comments"
│ └── attachments/
├── EPIC-100/
│ └── EPIC-100.md
└── STORY-200/
└── STORY-200.md
A simple table listing all root imported tickets, linked to their per-import indexes. Written for nested layouts only — a flat destination holds exactly one import.
Contains:
- Backlink to the registry (nested layouts only — a flat destination writes no registry, so the backlink is omitted)
- Summary table — all exported issues with type, status, summary, and role (root/linked/epic_child)
- Relationship map — link types between issues with directional arrows
- Epic hierarchy — tree view of Epic → Story → Task chains
- Statistics — counts by role
Each <KEY>.md includes:
- Backlink to the per-import index for navigation
- YAML frontmatter (all Jira fields)
- Metadata section (type, status, assignee, team, parent)
- Status details and description (converted from Jira markup)
- Attachments (embedded images, plus a file list)
- Release notes (if present)
- Linked issues (grouped by link type)
- Pull requests (title, URL, repo, branch, status)
- Comments (transcluded from
<KEY>-comments.md, or inlined — see below)
Two link styles, chosen by --link-style or inferred from the destination.
GitHub's file browser renders neither [[KEY]] nor ![[image]], so an export
meant to be read in a pull request needs plain relative markdown links.
obsidian |
github |
|
|---|---|---|
| Issue in the export | [[KEY]] |
[KEY](../KEY/KEY.md) |
| Issue outside it | Jira URL | Jira URL |
| Index backlink | [[PRODUCT-12345-index]] |
[PRODUCT-12345-index](../PRODUCT-12345-index.md) |
| Image | ![[image.png]] |
 |
| Other attachment | [[file.pdf]] |
[file.pdf](attachments/file.pdf) |
| Comments | ![[KEY-comments]] transclusion |
inlined into <KEY>.md |
Under either style, links inside markdown tables are pipe-escaped (\\|) so
they do not break the table, and issues outside the export fall back to Jira
URLs. GitHub link targets are percent-encoded, so spaces and parentheses in a
filename survive.
One deliberate asymmetry: a bare issue key mentioned in body text stays a
[[KEY]] wikilink under the obsidian style even when the key is outside this
export, because Obsidian resolves wikilinks by filename across the whole vault
and will reach a ticket exported by a different import. Structural links —
index rows, Parent, Linked Issues — fall back to a Jira URL in both
styles.
Under the obsidian style every attachment is downloaded. Under the github
style only an allowlist is: images, documents (pdf, office formats, csv),
and text/data formats. Everything else — archives, dumps, binaries, and
extensionless blobs — is left in Jira and rendered as a sized link:
- [SupportArchive3F642709.zip](https://…/attachment/98123) (11.2 MB)In a 163 MB vault export, archives accounted for 45.9 MB across 46 files — 40% of the attachment weight in 4% of the files — which is not something to commit to a specs repository.
An attachment whose download fails is rendered the same way, as a link back to
Jira. An attachment Jira no longer returns at all — deleted, or not visible to
your account — has no URL to fall back on, so references to it are rendered as
*(attachment unavailable: <filename>)* rather than as a link to a file that
was never written.
- Person names → consistent
User-Nplaceholders across the entire export - Email addresses →
[email] @mentionsand[~user]references → anonymized- Ticket summaries → scrubbed the same way wherever they appear: page titles,
frontmatter, indexes, and the slug of a new specs folder. A name is replaced
only when it belongs to a person on the exported tickets or is written as an
@Name Surnamemention - Team names, project names, ticket IDs → kept as-is (not PII)
Running the tool again for the same ticket will delete and recreate each issue's folder within its import subdirectory. The registry export-index.md is rewritten on every run to reflect all current imports. No snapshot history is maintained.
Only the folders of issues in the current graph are recreated. If a link is removed in Jira between runs, that issue's directory is left behind from the previous export and has to be deleted by hand — the tool never removes a directory it did not just write.