Jira connector

Import Jira issues into a Sphinx-Needs needs.json file, and create Jira issues from needs while tracking their status.

Note

The jira connector requires an active ubConnect license. See License configuration for activation instructions. The connector accepts the license via UBCONNECT_LICENSE_KEY / UBCONNECT_LICENSE_USER environment variables, the ubconnect.toml license file, or the --license-key / --license-user CLI options.

Quick start

Add a [ubconnect.from_jira] section to your ubproject.toml:

[ubconnect.from_jira]
url = "https://example.atlassian.net"
username = "alice@example.com"
password = "your-api-token"
project_key = "PROJ"
outdir = "./generated"

Then run:

$ ubconnect jira from-jira --config ubproject.toml

This produces ./generated/needs.json.

Authentication

The connector supports two auth modes via auth_type:

  • "basic" (default): Cloud uses the Atlassian email plus API token; Server / Data Center uses username plus password. username is required.

  • "bearer": Jira Data Center personal access token (PAT). Set auth_type = "bearer" and put the PAT in password; username is ignored.

Type mapping defaults

Standard Jira issue types map by default:

Jira type

Need type

Epic

us

Story

req

Task

impl

Bug

risk

Sub-task

spec

Anything else falls back to fallback_type (default "req") and emits one WARN per unique unmapped type. Override per project:

[ubconnect.from_jira.type_mapping]
"Test Case" = "spec"     # Xray / Zephyr plugin
"Spike" = "dec"

Field mapping defaults

Three Jira fields map by default:

Jira field

Need field

summary

title

description

content

status

status

Add more by extending [ubconnect.from_jira.mapping]:

[ubconnect.from_jira.mapping]
priority = "priority"
assignee = "assignee"
labels = "labels"

Audit extras

Every imported need carries:

  • jira_key (always): the original Jira issue key, e.g. "PROJ-12".

  • jira_url (always): a deeplink built from url and the issue key.

  • jira_parent_key (conditional): set when the issue has a parent, always present on Sub-tasks. For Story-to-Epic relationships, present only on modern Jira (Cloud Next-gen, Cloud Company-managed, Data Center / Server 8.x and newer). On older Cloud classic projects, Epic Link lives in a custom field; extend [ubconnect.from_jira.mapping] with the custom field id to capture it.

Markup conversion

The connector translates Jira description bodies to RST automatically:

  • Server / Data Center wiki markup to RST.

  • Cloud ADF (Atlassian Document Format) to RST.

The converter covers the common surface (paragraphs, headings, lists, code blocks, basic inline marks, links). Advanced ADF nodes (tables, panels, media) emit placeholders. Full-fidelity conversion via Pandoc is tracked separately.

Covered constructs (full conversion):

  • Paragraphs, headings, hard breaks, horizontal rules.

  • Bullet and ordered lists, list items.

  • Code blocks (with language hint).

  • Blockquotes.

  • Inline marks: bold, italic, code, links, strikethrough.

Placeholder constructs (rendered as RST notes / minimal directives):

  • Tables (single-row .. list-table:: placeholder; full conversion tracked separately).

  • Mentions, emoji, panels, expand / nested expand, media.

Unsupported constructs are aggregated and logged as a single WARN per unique node type at end of run. The original text content survives the conversion in all known cases; advanced layout features may render as plain text.

JQL filtering

For complex selection, use jql:

[ubconnect.from_jira]
project_key = "PROJ"
jql = 'fixVersion = "1.4.0" AND status != "Done"'

When constructing JQL programmatically (CI templates, scripted config generation), sanitize external inputs (branch names, ticket IDs) before splicing into the query. The connector does not escape JQL values.

Status normalization

Jira status values are free-form workflow states (“To Do”, “In Progress”, “Won’t Fix”, “Done”); the default mapping is direct passthrough. To pin status to a single value across imports use static_fields:

[ubconnect.from_jira.static_fields]
status = "open"

Visualizing imported issues

Once ubconnect jira from-jira has produced needs.json, dashboard directives in your Sphinx project can render the imported needs. Starting points:

A traceability table with a clickable deeplink back to Jira (the jira_url extra is added by the connector to every imported need):

.. needtable::
   :filter: "jira_key" in extras
   :columns: id, type, title, jira_key, status
   :style: table

A flow diagram over the imported issues and their links:

.. needflow::
   :filter: "jira_key" in extras
   :show_link_names:
   :alt: Imported Jira issues and their relations

For multi-component layouts (needpie / needbar / needgantt), reusable filter definitions, and color-scheme conventions, see the Creating Dashboards tutorial.

Configuration reference

All available fields:

Field

Default

Description

url

required

Jira base URL.

username

required for basic auth

Email (Cloud) or username (Server / Data Center).

password

required

API token, password, or personal access token.

auth_type

"basic"

One of "basic" or "bearer".

ssl_verify

true

Disable for self-signed certificates.

timeout_seconds

unset

HTTP timeout (connect plus read).

project_key

XOR with project_id

Project key, e.g. "PROJ".

project_id

XOR with project_key

Numeric project id.

jql

unset

Free-form JQL filter.

issue_types_include

unset

Whitelist of Jira issue types; XOR with issue_types_exclude.

issue_types_exclude

unset

Blacklist of Jira issue types; XOR with issue_types_include.

mapping

3 defaults

Jira field to Need field map.

type_mapping

5 defaults

Jira issue type to Need type map.

fallback_type

"req"

Used for unmapped Jira types.

link_mapping

{}

See above.

static_fields

{}

Values applied unconditionally to every emitted need.

id_prefix

""

Prepended to Need.id.

on_error

"skip"

Per-issue error policy.

batch_size

100

Capped at the Jira API limit of 100.

retry_max_attempts

3

Retries on HTTP 429 and 5xx responses.

proxy

unset

HTTP proxy URL.

Creating Jira issues from needs

ubconnect jira to-jira creates one Jira issue per selected need. Content flows one way: need content is written into Jira, and only the issue status is read back. Updating an already-created issue when its need changes is out of scope, as is deciding which needs are new — pair this with ubc diff if you need change detection.

Quick start

[ubconnect.to_jira]
url            = "https://jira.example.com"
username       = "alice@example.com"
password       = "your-api-token"
project        = "SWDEV"
issue_type     = "Story"
needsjson_path = "./generated/needs.json"
select         = 'type == "req" and status == "approved"'

# Where a Jira user can browse the exported need
need_url = "https://ubtrace.example.com/org/proj/latest/needs/{{ need.id }}"

[ubconnect.to_jira.template]
summary     = "{{ need.id }} — {{ need.title }}"
description = """
{{ need.content }}

Parent: {{ links.parent_needs[0].id }} — {{ links.parent_needs[0].title }}
Browse the requirement: {{ need_url }}
"""

[ubconnect.to_jira.linked_needs]
follow = ["parent_needs"]
fields = ["id", "title", "status"]

[ubconnect.to_jira.status_tracking]
enabled = true
map = { "To Do" = "open", "In Progress" = "in_progress", "Done" = "done" }

Preview before you write anything:

$ ubconnect jira to-jira --config ubproject.toml --dry-run

Then run the export:

$ ubconnect jira to-jira --config ubproject.toml
REQ_001: created SWDEV-1
REQ_002: created SWDEV-2
created 2, skipped 0, failed 0

Authentication is identical to the import direction — see Authentication.

Selecting needs

select is a filter expression over need fields, including your own extra options. It admits comparisons (==, !=, <, in, …), boolean operators, names, and literals — nothing else, so a configuration file cannot smuggle code into the export run. Omit select to export every need.

Templates

Templates are MiniJinja and see three context entries:

  • need — the exported need, core fields plus extras.

  • links.<link_type> — needs reachable through the link types named in follow, projected onto fields. Traversal is one hop only.

  • need_url — the rendered back-link for this need.

Rendering is strict: a template referencing a field the need does not carry fails that need and is reported, rather than silently producing an issue with an empty field.

Writing to other Jira fields

Any key beyond summary and description is sent to the Jira field of the same name, so customfield_10101 = "{{ need.id }}" populates that custom field:

[ubconnect.to_jira.template]
summary           = "{{ need.id }} — {{ need.title }}"
description       = "{{ need.content }}"
customfield_10101 = "{{ need.id }}"     # a text field
duedate           = "{{ need.due }}"    # a date, as YYYY-MM-DD

A template always renders to text. The value is sent to Jira as a string, so this reaches only fields that accept one. Jira rejects the rest, and because the rejection happens per need, the run reports that need as failed and carries on (or aborts, under on_error = "fail"):

Jira field type

Reachable

Notes

Text (single-line, paragraph)

yes

The ordinary case. summary and description are these.

Date / datetime

yes

Rendered text must already be in Jira’s format, e.g. 2026-12-31.

URL

yes

Sent as a plain string.

Number

no

Jira answers Operation value must be a number.

Labels, multi-select, components, versions

no

Jira expects an array, e.g. Specify the value for Labels in an array.

Priority, assignee, reporter, user and option pickers

no

Jira expects an object, e.g. The Priority value must be an object.

There is no escape hatch for the unreachable types: a template cannot emit a JSON array or object, so populating them needs a follow-up step against the Jira API. labels in particular is worth avoiding — the connector already writes the ubconnect-need:<need id> tracking label there, and a template key of that name replaces it (Jira then rejects the string, so the export fails rather than quietly losing the mapping).

The target field must also be on the project’s create screen for the configured issue type. A field that exists but is not on that screen is refused with Field '<id>' cannot be set. It is not on the appropriate screen, or unknown.

Reconciliation

Because every created issue carries the exported need’s reference (in need_field, or in the ubconnect-need:<need id> label when no field is configured), the mapping can be read back out of Jira. That is what makes a lost state file recoverable, and what makes stateless CI possible.

[ubconnect.to_jira]
reconcile = "missing"   # "missing" (default) | "always" | "never"

Policy

Behaviour

"missing"

Query Jira before exporting whenever the state file holds no mapping. Repairs a lost state file. This is the default.

"always"

Query Jira before every export. Use this when the pipeline keeps no state between runs at all.

"never"

Never query. The state file is the only source of truth.

--reconcile on the command line raises the policy to "always" for one run:

$ ubconnect jira to-jira --config ubproject.toml --reconcile
reconciled 2 need-to-issue pair(s) from Jira
REQ_001: already exported as SWDEV-1
REQ_002: already exported as SWDEV-2
created 0, skipped 2, failed 0

For stateless CI, set reconcile = "always" and provision need_field. The state file then becomes a per-run cache rather than something you must commit.

Note

The default query scans the configured project (project = "<project>") and matches the need reference client-side. Server-side filtering is deliberately avoided because JQL custom-field syntax differs between Jira deployments (cf[10101] versus the field name), and getting it wrong would silently recover nothing. On a large project, narrow the scan with reconcile_jql:

[ubconnect.to_jira]
reconcile_jql = 'project = "SWDEV" AND issuetype = "Story" AND created > -90d'

An issue whose reference matches no loaded need is ignored. If two issues claim the same need, the first one wins and a warning names both, so the export cannot silently flip between them from run to run.

Tracking Jira status

ubconnect jira refresh-status reads the current status of every mapped issue and writes a needs.json into outdir, ready for needimport:

$ ubconnect jira refresh-status --config ubproject.toml
REQ_001: in_progress
REQ_002: open

Each emitted need carries its status translated through status_tracking.map (unmapped Jira status names pass through unchanged) and a jira_key extra option holding the issue key.

Export configuration reference

Field

Default

Description

url

required

Jira base URL.

username / password / auth_type

see Authentication

Same credential shape as from_jira.

project

required

Target Jira project key.

issue_type

required

Target issue type (Story, Epic, or a custom type).

needsjson_path

required

Source needs.json; must exist.

need_url

required

Back-link template rendered per need.

template.summary

required

Issue summary template.

template.description

""

Issue description template.

template.<jira field>

unset

Any further key writes to the Jira field of that name. Text-shaped fields only; see Writing to other Jira fields.

select

unset

Filter expression; unset exports every need.

linked_needs.follow

[]

Link types traversed for the template context.

linked_needs.fields

["id", "title", "status"]

Linked-need fields exposed to the template.

state_file

.ubconnect/jira_state.json

Need-to-issue mapping; resolved against the config file.

status_tracking.enabled

true

Whether status readback is active.

status_tracking.map

{}

Jira status name to tracked value.

need_field

unset

Text field carrying the need reference; omitted from the payload when unset.

need_field_value

"id"

What need_field holds: "id" or "url".

need_label_prefix

"ubconnect-need"

Prefix of the need-id label on created issues.

reconcile

"missing"

When to rebuild the mapping from Jira: "missing", "always", "never".

reconcile_jql

unset

Replaces the default project scan used by reconciliation.

dry_run

false

Also settable via --dry-run.

on_error

"skip"

"skip" isolates a failing need; "fail" aborts the run.

outdir

config directory

Where refresh-status writes its needs.json.