Skip to main content

jirasify

Terminal UI for logging work and moving Jira issues, built with Textual.

The distribution name on PyPI is jirasify; the main CLI command is jirasify-tui; the importable Python module is jiratui.

Install

pip install jirasify

Each command (jirasify, jirasify-tui, jirasify-list) has a --version flag that prints the installed version and a bundled changelog.

Configure

On first run, jirasify-tui writes a template config to ~/.local/etc/jiratui/config.yaml and prints a usage page. Edit the file (or answer the interactive prompt to fill it) with your Jira URL, project key, and username.

Your Jira token is read from the environment variable named by authentication.token_env (default: JIRATOKEN):

export JIRATOKEN=your_token
  • Self-hosted Jira (Data Center / Server): use a Personal Access Token. It is sent as Authorization: Bearer <token>.
  • Atlassian Cloud (URL contains atlassian.net): use an API token from your Atlassian account. It is combined with assignee.username (your account email) via HTTP Basic auth.

Both hierarchy conventions are supported: Data Center's Epic Link custom field, and Cloud's standard parent field for the Epic → Story link. The subtask issue type name is auto-discovered per project (Cloud team-managed projects use Subtask; most others use Sub-task).

The Sprint / Epic Link / Epic Name / Parent Link custom fields are matched first by display name and then by their Greenhopper schema.custom value, so localized or renamed installations still resolve. The enhanced JQL endpoint (POST /rest/api/2/search/jql) is tried first and the client falls back to the classic /rest/api/2/search on any of 400/404/405/501, so old Data Center instances that answer differently still work.

Verify connectivity before launching the TUI:

jirasify-tui --test

Prints the configured URL and token env var, then hits /rest/api/2/myself. Exits with a non-zero status on missing config, unset token, or auth failure.

Usage

jirasify-tui
Key Action
l Log view
o Overview
r Monthly report
t/w Today's / this week's report
m This month's report
s Change status of selected issue
e Set estimate on selected issue
p Switch active timer to selected issue (logs elapsed on previous). Fires timew start <ISSUE> if Timewarrior is installed.
P Stop timer entirely (timew stop). Prompts to log elapsed.

When a timer is running, the active row's rightmost Tracked column shows a green ⏱ next to the total time Timewarrior has ever logged against that issue (all closed intervals plus the running session), ticking once per second.

| h | Help | | Ctrl+R | Refresh | | q | Quit |

Time input accepts 30m, 1h, 2h30m, 1d (a day = 8h).

jirasify

pip install jirasify also installs a jirasify command that turns a structured Markdown file into Jira user stories using the same ~/.local/etc/jiratui/config.yaml and JIRATOKEN env var as jirasify-tui.

Expected Markdown layout:

# My Epic Name - User Stories

## 1. Project Overview

Free text.

---

# Phase 1 - Foundations

## User Story 001 - Inventory Environment

**Estimate:** 15 h

> As a developer, I will document ... so that ...

### Scope
- Item A
- Item B

### Substories
- Break out first sub-piece
- Break out second sub-piece

### Acceptance Criteria
- Criterion 1
- Criterion 2
  • The top-level # ... heading (any H1 that is not a # Phase ...) is used as the Epic name. A trailing - User Stories suffix is stripped.
  • Stories are only picked up under a ## User Story ... heading inside a # Phase ... section.
  • Recognised subsections: Scope, Validate, Include, Examples, Substories, Acceptance Criteria.
  • Each ### Substories bullet becomes a Sub-task in Jira under the parent story. Substories are excluded from the story's Jira description so they aren't duplicated.
  • A **Tags:** foo, bar, baz line anywhere in a user story block adds extra Jira labels (spaces replaced with _). Combined with the auto-generated Phase_... label on every created issue.

Commands

jirasify --example                              # write ./example.md (plain markdown starter)
jirasify --template --file stories.md.j2        # write an Ansible/Jinja2 template

jirasify --file stories.md                      # parse + write .jirastories.state.json
jirasify --file stories.md --create             # ensure Epic exists, create missing stories linked to it (idempotent)
jirasify --file stories.md --status             # key/status/assignee/epic/estimate/used per story, grouped by phase with totals
jirasify --file stories.md --epic VOS-100       # relink stories to a specific epic
jirasify --file stories.md --epic VOS-100 --story VOS-142   # relink a single story
jirasify --file stories.md --link-epic          # relink recorded stories to the Epic named by the H1 (creates it if missing)
jirasify --file stories.md --tag                # add Phase_ and **Tags:** labels to already-created stories/substories (union)
jirasify --file stories.md --delete             # delete every recorded issue (prompts unless --yes)

jirasify --reverse VOS-1234                     # print a markdown reconstruction of an Epic and its stories
jirasify --reverse VOS-1234 --save              # write it to <Epic_Name>.md (spaces -> _)

--create skips stories that already have an issue_key recorded in .jirastories.state.json, and also skips summaries that already exist in the target project, so re-runs won't duplicate issues. If no Epic with the H1's name exists, one is created before the stories. Substories are handled the same way — each story tracks its sub_keys map, and existing subtasks are adopted by summary before creating new ones.

--reverse also emits each fetched story's subtasks as a ### Substories block, so round-tripping --create then --reverse preserves the sub-structure.

Each created story (and its substories) is tagged with a Jira label derived from its markdown phase — spaces are replaced with underscores, e.g. Phase_1_-_Foundations. This lets jirasify-list --epics --full render a phase level in its tree; the labels are inert otherwise.

Any **Tags:** ... line in a user story adds additional labels to that story (and its substories). --tag performs a label alignment on already- created issues — it unions the phase label and markdown tags onto each recorded issue via an atomic PUT update op, so any Jira-side labels you added manually are preserved.

--reverse reads back the labels and emits one # Phase N - ... heading per phase, and a **Tags:** ... line under stories that have non-phase labels — so create → reverse round-trips the tag structure.

--delete undoes --create for the current state file: it lists the recorded stories and substories, prompts for confirmation (skip with --yes), then deletes each in Jira (substories first). The epic itself is left alone — remove it in the Jira UI if you want.

Ansible / Jinja2 template

--template emits a .j2 scaffold expecting these variables:

  • epic_name (string)
  • project_overview (string, optional)
  • phases: list of { name, stories: [ { id, summary, estimate, description, scope?, acceptance_criteria? } ] }

Render it with ansible.builtin.template (or jinja2.Template) to produce a stories.md, then run jirasify --file stories.md --create.

jirasify-story

pip install jirasify also installs a jirasify-story command that prints a single Jira story's status, assignee, and time totals. With --detail it also prints the description, subtasks (open first, then done), and every worklog entry with a running total.

jirasify-story VOS-1234                # header only (status/assignee/estimate/logged/remaining)
jirasify-story VOS-1234 --detail       # + description, subtasks, and worklog history

jirasify-story --assigned              # list every issue assigned to you (most recently updated first)
jirasify-story --assigned --epic VOS-100   # only the assigned issues linked to Epic VOS-100

jirasify-story VOS-1234 --log 30m --comment "Fixed handler"    # quick single worklog entry
jirasify-story VOS-1234 --log 1h --comment "Retro" --started 2026-09-13T08:00:00   # backdate

jirasify-story --worklog                          # your worklogs, grouped by ISO week (last 4 weeks)
jirasify-story --worklog --start 2026-08-01       # since Aug 1
jirasify-story --worklog --start 2026-08-01 --end 2026-08-31   # August only

jirasify-story --worklog --start 2026-08-01 --download-worklog aug.yaml  # dump August rollup (YAML)
jirasify-story --worklog --start 2026-08-01 --download-worklog aug.tsv   # dump as a tab-separated table
# edit aug.yaml / aug.tsv (tweak time/comment/date; add rows w/o id; delete rows)
jirasify-story --worklog --upload-worklog aug.yaml           # sync YAML back
jirasify-story --worklog --upload-worklog aug.tsv            # sync TSV back

.tsv / .txt files open cleanly in Excel / Numbers / Google Sheets and diff nicely in git. Columns: week | date | time | issue | id | comment. Leave id blank to create a new worklog on the given issue; delete a row to remove that worklog. date accepts either YYYY-MM-DD (expanded to 09:00 UTC) or a full ISO-8601 timestamp.

Per-story flow (no --worklog):

jirasify-story VOS-1234 --download-worklog worklog.yaml   # dump worklogs to YAML
# edit worklog.yaml (tweak time_spent, comment, started; add rows with no id)
jirasify-story VOS-1234 --upload-worklog worklog.yaml     # sync back (create + update + delete, no prompt)

Worklog round-trip

The YAML written by --download-worklog looks like:

issue: VOS-1234
summary: Fix widget
worklogs:
  - id: 12345
    started: 2026-09-13T08:00:00.000+0000
    author: Jane Doe   # informational, ignored on upload
    time_spent: 30m
    comment: Initial investigation
  - id: 12346
    started: 2026-09-14T09:15:00.000+0000
    author: Jane Doe
    time_spent: 30m
    comment: Wrote the failing test
  # add new entries at the end (no id) to create them:
  # - started: 2026-09-14T14:00:00.000+0000
  #   time_spent: 45m
  #   comment: Fixed the click handler

--upload-worklog diffs the file against the current Jira state and applies:

  • rows without id → create (Jira assigns the id)
  • rows whose time_spent, comment, or started differ → update
  • rows present in Jira but absent from the file → delete (no prompt — the file is the source of truth)

started can be edited but must remain ISO-8601 (2026-09-13T08:00:00.000+0000); a bare YYYY-MM-DD is rejected. Editing worklogs authored by someone else typically requires admin permission on the target Jira; if it fails the surfaced error will say so.

Uses the same ~/.local/etc/jiratui/config.yaml and JIRATOKEN env var as jirasify-tui.

jirasify-list

pip install jirasify also installs a jirasify-list command for read-only listing of Jira objects, using the same config as jirasify-tui.

jirasify-list --epics                     # Epics in jira.project (config), excluding Cancelled and Done
jirasify-list --epics --key VOS           # override the project key

Status filters (mutually exclusive)

jirasify-list --epics --active            # status = Implementing
jirasify-list --epics --planning          # status = Planning
jirasify-list --epics --done              # status = Done
jirasify-list --epics --cancelled         # status = Cancelled

Without any of these flags, Cancelled and Done epics are hidden by default.

jirasify-list --epics --parent VOS-100    # exact parent (JQL: "Parent Link" = VOS-100)
jirasify-list --epics --parent VOS        # any parent whose key starts with VOS-

Full hierarchy

jirasify-list --epics --full              # also show stories under each epic and subtasks under each story

--full issues one extra batched JQL for all epic children ("Epic Link" in (…)) and one for all subtasks (parent in (…)), regardless of item count.

Output

Epics are grouped by their Parent Link and printed as a tree with box-drawing branches (├──, └──, │):

VOS-100  Digital Platform Modernization
├── VOS-215      Implementing         Jane Doe                          Artifactory PyPI
│   ├── Phase 1 - Prepare Artifactory
│   │   ├── VOS-231    In Progress          Bob                               Provision PyPI local repo
│   │   │   └── VOS-232  To Do                Alice                             Configure retention
│   │   └── VOS-233    To Do                Bob                               Assemble virtual PyPI
│   └── Phase 2 - POC Publishing Pipeline
│       └── VOS-241    To Do                Alice                             Build POC publishing pipeline
└── VOS-220      To Do                Bob                               Sisyphos Onboarding
    └── (no phase)
        └── VOS-260    To Do                Alice                             Ad-hoc story

VOS-105  Infrastructure 2026
└── VOS-311      Implementing         Alice                             VMware 9.1 Enablement

(no parent)
└── VOS-999      Planning             Unassigned                        Ad-hoc Epic

Columns per row: issue key, status, assignee (truncated to 32 chars with …), and the label — Epic Name for epics (falls back to summary when the Epic Name custom field is absent), summary for stories and subtasks. Without --full only the epic level is shown; with --full, stories are grouped under a phase row derived from the story's Phase_... Jira label (falls back to (no phase) when a story has no such label — typical for stories created outside jirasify or before phase-labelling was added).

Rows are colorized when stdout is a terminal: cyan parent header and phase, magenta epic, green story, blue subtask. Piping or redirecting produces plain output.

Metadata

Release files for jirasify 2.7.53

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for jirasify 2.7.53
File Size Uploaded Provenance Actions
jirasify-2.7.53.tar.gz 72.7 kB Without provenance Details for jirasify-2.7.53.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for jirasify 2.7.53
File Interpreter ABI Platform Actions
jirasify-2.7.53-py3-none-any.whl Python 3 none any Details for jirasify-2.7.53-py3-none-any.whl

Total release size: 149.9 kB

Release files / jirasify-2.7.53.tar.gz

Download URL jirasify-2.7.53.tar.gz
Size 72.7 kB
Tags Source
SHA-256 checksum
How to use checksums
1fc6c72d20d8d6189fa813dd6d8e51633913c663f820a3eb19e80ec8d03441e4
BLAKE2b-256 checksum
How to use checksums
407467d464466272b9ab85c5b8bd76dc3b846bc1df69d5d7c7092e5b28115f2a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.12.14

Release files / jirasify-2.7.53-py3-none-any.whl

Download URL jirasify-2.7.53-py3-none-any.whl
Size 77.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d7493fcd35b42e230fc4632722b8a4ef4190e5b94da207dd320b5afd0df29d4f
BLAKE2b-256 checksum
How to use checksums
552ce9dae13102233374ce49f357c16778fa5d818d3b5fcab6ffdf7c30cc8f00
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.12.14

Security for jirasify 2.7.53

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

2.7.53 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page