pm-jira
v2026.6.10
Published
Jira issue sync for pm-cli
Downloads
2,355
Maintainers
Readme
pm-jira
A pm-cli extension that syncs Jira issues into pm items using the Jira REST API v3.
Features
- Pull issues from any Jira project into pm items via
pm jira importorpm jira sync - Export pm items back out as Jira create payloads via
pm jira export(preview, or--pushto create issues) - Convenience JQL filters —
--project,--status,--assignee,--issue-type,--label,--updated-sincecompose into a single JQL query (or pass full--jqlto override) - Rich field mapping — Jira status (by name, with a
statusCategoryfallback for custom workflows), priority, issue type → pm type, labels/fix-versions/components/sprints → tags, assignee → tag; configurable with--status-mapand a general--map jiraField=pmField --dry-runeverywhere — both import and export print the exact request / mutations they would make and perform no network call (the offline-testable, creds-free path)pm jira validate— report Jira credential/base-URL readiness without leaking any secret (hostname-only preview);--jsonaware- Fail-fast preflight credential gate — a network-mutating
pm jira sync/pm jira import(andpm jira export --push) aborts immediately with a clear, actionable message before any pm-store read or Jira call ifJIRA_BASE_URL/JIRA_EMAIL/JIRA_API_TOKENare missing.--dry-runandpm jira validateare exempt (offline). See Preflight gate. - Optional, opt-in export-on-write hook (
PM_JIRA_PUSH_ON_WRITE) — best-effort, never breaks your pm command, no-op without credentials - Jira provenance (key + browse URL) persisted in the item description and declared as
jira_key/jira_urlschema fields; on export, items that already carry a Jira key become anupdate(PUT) instead of a duplicate create - Works as a
pm jira sync/pm jira importcommand, apm jira exportexporter, and a config-drivenjira-syncimporter
Which features need live Jira credentials? Everything except the actual network round-trip is usable and testable offline. Building JQL, previewing requests/mutations with
--dry-run, field mapping in both directions, andpm jira validateall work with no creds and no network. You only needJIRA_BASE_URL/JIRA_EMAIL/JIRA_API_TOKENfor: a real import fetch,pm jira export --push(creating/updating issues), and live reachability.
Installation
Install with pm from GitHub:
pm install github.com/unbraind/pm-jiraOr build locally from source:
npm ci
npm run buildJira API Setup
You need a Jira API token to authenticate. To create one:
- Go to https://id.atlassian.com/manage-profile/security/api-tokens
- Click Create API token
- Give it a label (e.g.
pm-cli-sync) and copy the token
Required Environment Variables
| Variable | Description | Example |
|---|---|---|
| JIRA_BASE_URL | Your Jira instance base URL | https://company.atlassian.net |
| JIRA_EMAIL | Email address for your Jira account | [email protected] |
| JIRA_API_TOKEN | API token generated above | <jira-api-token> |
Set them in your shell or .env:
export JIRA_BASE_URL=https://company.atlassian.net
export [email protected]
export JIRA_API_TOKEN=<jira-api-token>Usage
Command: pm jira sync
# Sync all open issues from a project (default JQL: statusCategory != Done)
pm jira sync --project PROJ
# Sync up to 200 issues
pm jira sync --project PROJ --max-results 200
# Use custom JQL
pm jira sync --jql "project = PROJ AND assignee = currentUser() ORDER BY updated DESC"
# Preview without writing (dry run)
pm jira sync --project PROJ --dry-run
# Only sync issues that map to the "wip" pm status
pm jira sync --project PROJ --status wip
# Combine flags
pm jira sync --project PROJ --max-results 100 --status todo --dry-runCommand / Importer: pm jira import
pm jira import is the native importer pipeline equivalent of pm jira sync — it
pulls issues via JQL and creates pm items. Both share the same flags and logic.
# Pull all open issues from a project
pm jira import --project PROJ
# Use a custom Jira host (instead of JIRA_BASE_URL) + custom JQL
pm jira import --host https://company.atlassian.net --jql "project = PROJ AND assignee = currentUser()"
# Override status mapping
pm jira import --project PROJ --status-map "QA=blocked,Done=closed"Pull flags (pm jira sync / pm jira import)
| Flag | Type | Default | Description |
|---|---|---|---|
| --project | string | — | Jira project key (e.g. PROJ). Composed into JQL. |
| --jql | string | — | Custom JQL query. Used verbatim; overrides all convenience filters below. |
| --status | string | — | Filter by pm status (open/in_progress/closed/blocked, mapped to a statusCategory clause) or a raw Jira status name. Also filters imported items client-side. |
| --assignee | string | — | Filter by assignee (accountId, name, or a function like currentUser()). |
| --issue-type | string | — | Filter by Jira issue type (e.g. Bug). |
| --label | string | — | Filter by Jira label. |
| --updated-since | string | — | Filter by updated date, relative (-7d) or absolute (2026-01-01). |
| --status-map | string | — | Override status mapping, e.g. "In Review=in_progress,QA=blocked". |
| --map | string | — | Override field mapping, e.g. "issuetype=Task,assignee=skip". |
| --host | string | $JIRA_BASE_URL | Jira base URL override. |
| --max-results | number | 500 | Maximum number of issues to pull. |
| --dry-run | boolean | false | Print the JQL + exact GET request that would run; no network call. |
When no --jql is given, the convenience filters are AND-combined; if you don't
filter on status, a statusCategory != Done clause is appended so an unscoped
pull stays focused on active work (the historical default).
--map field overrides
--map accepts a comma list of jiraField=pmTarget pairs. Recognized Jira-side
keys: status, statuscategory, priority, issuetype (alias type),
labels, fixversions, components, sprint/sprints/customfield_10020,
assignee, duedate. Imported tags include component:<name> from Jira
components and sprint:<name> from Jira's common Sprint custom field
(customfield_10020) when present; set noisy context fields to skip or
ignore to suppress them. Examples:
# Pin every imported item's type, and skip the assignee tag
pm jira import --project PROJ --map "issuetype=Task,assignee=skip"
# Keep labels but suppress sprint/component context tags
pm jira import --project PROJ --map "components=ignore,sprint=ignore"
# Force a pm status regardless of the Jira workflow state
pm jira import --project PROJ --map "status=in_progress"Progress + transparency notes (STDERR)
For large paginated imports the importer prints Fetched N/total... progress to
STDERR after each page, so a multi-page pull surfaces feedback instead of
looking hung. This is additive and never touches the stdout / --json output.
If any fetched issue carries attachments or comments, the importer logs a one-line note to STDERR that those are not imported (pm-jira imports title / body / status / priority / labels / due-date only). This prevents a silent expectation that attachment or comment data came across.
Exporter: pm jira export
Render pm items as Jira create payloads. Prints JSON by default; with --push
(and credentials + --project) it POSTs each payload to Jira's create-issue API.
# Preview the Jira create payloads for all pm items (no network, no creds needed)
pm jira export --project PROJ
# Print the exact mutations that WOULD run (create vs update), no network/creds
pm jira export --project PROJ --dry-run
# Derive Jira issuetype + priority from each pm item's type/priority
pm jira export --project PROJ --rich --dry-run
# Actually create the issues in Jira (requires creds + --project)
pm jira export --push --project PROJ
# Also PUT changed fields back to issues that already carry a Jira key
pm jira export --push --project PROJ --update-existing
# Preview exactly what --update-existing would do (no network)
pm jira export --project PROJ --update-existing --dry-run| Flag | Type | Default | Description |
|---|---|---|---|
| --project | string | — | Target Jira project key for created issues (required for --push). |
| --map | string | — | Override field mapping, e.g. "issuetype=Story". |
| --rich | boolean | false | Derive Jira issuetype + priority from the pm item type/priority. |
| --update-existing | boolean | false | PUT changed fields to issues that already carry a Jira key. Without it, those items are skipped (no duplicate, no mutation). |
| --dry-run | boolean | false | Print the Jira POST/PUT mutations that would run; no network call. |
| --host | string | $JIRA_BASE_URL | Jira base URL override. |
| --push | boolean | false | POST payloads to Jira (requires credentials + --project). |
Items whose description carries a Jira <KEY>: <url> provenance marker (added on
import) are matched back to their upstream issue and become an update (PUT):
- By default (
--pushalone) those items are skipped so a re-export never duplicates or unexpectedly mutates an existing Jira issue — only items without a key are created.--dry-runshows them asSKIP. - With
--update-existingeach matched item is PUT to Jira's edit-issue endpoint (/rest/api/3/issue/<KEY>) with its changed fields (the immutableprojectfield is stripped, as Jira's edit API rejects it).--dry-runshows them asUPDATE PUT ...so you can review the exact mutations offline first.
Command: pm jira validate
Report whether pm-jira has the credentials/base URL it needs — without making a network call and without leaking any secret value (it prints a hostname-only preview, never the token or email):
pm jira validate # human-readable readiness summary
pm jira validate --json # structured object: { ready, baseUrlPresent, ... }
pm jira validate --host https://company.atlassian.netPreflight gate
pm-jira registers a preflight credential gate (pm-cli preflight capability /
registerPreflight). Before a network-mutating command runs, it checks that the
required credentials are present and, if not, aborts immediately with a clear,
actionable error and a non-zero exit code — before any pm-store read or Jira REST
call. This turns a deep, late failure into a fast, obvious one.
It fires only for these invocations:
| Invocation | Gated? |
| --- | --- |
| pm jira sync (no --dry-run) | yes — pulls over the network |
| pm jira import (no --dry-run) | yes — pulls over the network |
| pm jira export --push (no --dry-run) | yes — POSTs/PUTs to Jira |
| pm jira sync\|import --dry-run | no — offline preview, no creds needed |
| pm jira export (no --push) | no — prints payloads offline |
| pm jira validate | no — diagnostics, must run without creds |
| any other / non-pm-jira command | no |
Example (no credentials set):
$ pm jira sync --project PROJ
pm-jira preflight: cannot run "pm jira sync" — missing Jira credentials:
JIRA_BASE_URL (or --host), JIRA_EMAIL, JIRA_API_TOKEN. Set JIRA_BASE_URL
(or pass --host), JIRA_EMAIL, and JIRA_API_TOKEN before a mutating command.
Create a token at https://id.atlassian.com/manage-profile/security/api-tokens .
Run "pm jira validate" to diagnose, or add --dry-run to preview offline.
# exit code 2The message names only the missing variable names — never the token or email value. When credentials are present the gate is a silent pass-through.
Export-on-write hook (opt-in)
Setting PM_JIRA_PUSH_ON_WRITE=1 activates a best-effort onWrite hook. It is a
strict no-op unless both the env flag is truthy and Jira credentials are
present, and it can never fail your pm command (the pm hook runtime swallows
any error). It intentionally does not auto-POST on every write; use the
explicit, reviewable pm jira export --push to mirror items upstream.
Importer: jira-sync (config-driven)
Use the jira-sync importer in your pm-cli config for automated syncing:
{
"importers": {
"jira-sync": {
"project": "PROJ",
"maxResults": 300
}
}
}Credentials are read from environment variables or from the importer config:
{
"importers": {
"jira-sync": {
"JIRA_BASE_URL": "https://company.atlassian.net",
"JIRA_EMAIL": "[email protected]",
"JIRA_API_TOKEN": "<jira-api-token>",
"project": "PROJ"
}
}
}Status Mapping
The default mapping (override per-status with --status-map):
| Jira Status | pm Status |
|---|---|
| To Do, Open, Backlog, (any other) | open |
| In Progress, In Review, In Development, Code Review | in_progress |
| Done, Resolved, Closed, Complete, Completed | closed |
| Blocked | blocked |
When the Jira status name is unrecognized (custom workflows), pm-jira falls
back to the issue's statusCategory bucket: new → open, indeterminate →
in_progress, done → closed.
Issue type mapping
| Jira Issue Type | pm Type |
|---|---|
| Bug, Defect | Bug |
| Story, Epic | Feature |
| Task, Sub-task | Task |
| (any other) | Issue |
Override with --map issuetype=<pmType> (import) or --map issuetype=<jiraType>
(export, with --rich).
Priority Mapping
| Jira Priority | pm Priority |
|---|---|
| Highest, Critical | 1 (highest) |
| High | 2 |
| Medium, (any other) | 3 |
| Low, Lowest | 4 (lowest) |
Item Structure
Each imported item includes:
- title:
[PROJ-123] Issue summary - body: Issue description (converted from Atlassian Document Format to plain text)
- description: A provenance marker
Jira PROJ-123: https://…/browse/PROJ-123so the Jira key + URL survive round-trips and powerpm jira export. The extension also declaresjira_keyandjira_urlas optional item schema fields (capabilityschema). - status: Mapped from Jira status (see table above)
- priority: Mapped from Jira priority (see table above)
- tags: Jira labels + fix version names
- deadline: Jira due date (
YYYY-MM-DD), if set
Note: pm's
createhas no generic custom-field setter for a standalone extension, so provenance is stored in the description marker rather than as structured metadata.
JQL Examples
# All open issues in a project, by priority
project = PROJ AND statusCategory != Done ORDER BY priority ASC
# Only issues assigned to you
project = PROJ AND assignee = currentUser()
# Issues updated in the last 7 days
project = PROJ AND updated >= -7d ORDER BY updated DESC
# Issues in a specific sprint
project = PROJ AND sprint in openSprints()
# Issues with a specific label
project = PROJ AND labels = "backend"
# Issues by type
project = PROJ AND issuetype = Bug AND statusCategory != DoneDevelopment
# Install dev dependencies
npm install
# Build TypeScript
npm run build
# Watch mode
npm run devRequirements
- Node.js 20+ (uses native
httpsmodule andBuffer) - pm-cli
>=2026.5.31 - TypeScript 6.x (dev dependency)
License
MIT
Release Automation
This package is release-ready for GitHub, npm, and Bun-compatible installs. CI runs type checking, build, production dependency audit, package packing, Bun install verification, and pm-changelog validation. The daily release workflow publishes only when commits exist after the latest release tag and uses pm-changelog to generate CHANGELOG.md and GitHub release notes.
