jira
'Interact with Atlassian Jira from the terminal: search issues with
Install
npx skills add https://github.com/magnus919/agent-skills/tree/main/jira
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install magnus919-agent-skills@llmmart
git clone https://github.com/magnus919/agent-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole magnus919/agent-skills collection as a plugin from our marketplace. Git is the plain clone.
README
Jira Issue Tracker from the Terminal
Interact with Atlassian Jira Cloud via the REST API v3: search issues with JQL, view details, create issues, add comments, count matches, list projects, and transition status — plus a full JQL language reference built in.
Why Install This Skill
When your agent loads this skill, it can run your entire Jira workflow without opening a browser:
- Search anything — by project, assignee, or arbitrary JQL; results over 50 auto-paginate
- Count before diving in — fast approximate counts instead of fetching every ticket
- Create, comment, edit — with Atlassian Document Format handled for you
- Transition safely — discovers valid workflow transitions per issue before changing status, and can set resolutions in the same call
- Write better queries — a 50-query cookbook by role, complete function catalog, performance rules, and history-operator/date-expression deep dives
The skill also knows where the bodies are buried: the legacy-vs-enhanced search endpoint split (offset paging vs nextPageToken), transition screens that silently require resolution fields, the != empty-value trap, and rate-limit headers worth honoring.
What You Get
| Path | Purpose |
|---|---|
SKILL.md |
Command reference: setup, intent-grouped commands, pipeline recipes, jq guidance, known gotchas |
scripts/jira |
CLI tool for Jira REST API v3 (--json, --dry-run, lazy auth) |
scripts/test_jira.py |
Offline test suite for the CLI (help/errors/dry-run/mocked client logic) |
references/rest-auth-and-search.md |
Auth models, rate limits, error envelopes, search pagination duality |
references/rest-issues-and-transitions.md |
Issue CRUD shapes, transitions GET→POST flow, ADF document model |
references/jql-functions-catalog.md |
Every JQL function with fields and operators, incl. JSM approvals & SLAs |
references/jql-best-practices.md |
Performance rules, precedence, empty-value trap, troubleshooting flows |
references/jql-cookbook.md |
50 ready-to-run JQL queries organized by role |
references/jql-history-and-dates.md |
WAS/CHANGED walkthrough, relative-date tables, saved-filter naming |
evals/evals.json |
Behavioral eval cases covering read-only use, pipelines, gotchas |
Quick Start
export JIRA_EMAIL="you@company.com"
export JIRA_API_TOKEN="YOUR_API_TOKEN" # free from https://id.atlassian.com/manage/api-tokens
export JIRA_SERVER="https://your-domain.atlassian.net"
jira me # verify auth works
jira list --project PROJ # newest tickets
jira count --jql 'issuetype = Bug AND resolution = Unresolved'
jira create --project PROJ --summary "Test" --dry-run # preview writes
jira --yes create --project PROJ --summary "Test" # authorize a write
Triggers
Load this when managing Jira issues, searching or counting tickets, creating bugs, transitioning sprint work, writing/debugging/optimizing JQL, or designing saved filters and dashboards on an Atlassian Jira Cloud site.
Requirements
- Python 3.8+ with the
requestslibrary - A free Atlassian account + API token (
JIRA_EMAIL,JIRA_API_TOKEN; optionalJIRA_SERVER) jqrecommended for processing--jsonoutput
Skill manifest
jira — Jira Issue Tracker from the Terminal
Interact with Atlassian Jira Cloud via the REST API v3. Search issues, view details, create issues, add comments, count matches, list projects, and transition status.
Setup
- Generate an API token at id.atlassian.com/manage/api-tokens
- Set environment variables:
export JIRA_EMAIL="your-email@example.com" # Atlassian account email
export JIRA_API_TOKEN="YOUR_API_TOKEN" # from id.atlassian.com
export JIRA_SERVER="https://your-domain.atlassian.net"
Auth is HTTP Basic over base64(email:token) — your email address, never a password (passwords are deprecated for API use). Cloud has no Personal Access Tokens; Bearer PATs are Data Center only. Tokens now expire after at most one year. --help and --dry-run work without credentials.
Essential Commands
me / projects — identity and scope
jira me # verify auth; your accountId, timezone
jira projects --json # all accessible projects
list — search issues
jira list # recent issues
jira list --project PROJ # by project
jira list --jql 'assignee=currentuser() AND status=Open' # custom JQL
jira list --project PROJ --max 120 --json # >50 auto-pages via startAt offsets
view — issue details
jira view PROJ-123 # summary, status, assignee, description
jira view PROJ-123 --json # machine-readable
Descriptions arrive as Atlassian Document Format (ADF); the CLI extracts plain text for display.
count — fast match total
jira count --jql 'issuetype = Bug AND resolution = Unresolved' # {"count": N}
Uses POST /search/approximate-count — no fetching rows. JQL itself has no COUNT/aggregation.
create — new issues
jira --yes create --project PROJ --summary "Fix login bug" # Task (default)
jira --yes create --project PROJ --summary "Crash on startup" --type Bug
jira --yes create --project PROJ --summary "Add dark mode" --type Story --priority High
jira create --project PROJ --summary "Test" --dry-run # preview payload
Descriptions are sent as ADF documents. Rich formatting beyond plain paragraphs needs raw ADF JSON — see references/rest-issues-and-transitions.md.
comment — add to threads
jira --yes comment PROJ-123 -m "Fixed in latest build"
jira comment PROJ-123 -m "Looking into it" --dry-run
transitions + transition — status changes
jira transitions PROJ-123 # LIST valid transition IDs first
jira --yes transition PROJ-123 --to "In Progress" # then apply by name or ID
jira --yes transition PROJ-123 --to Done --resolution Done
jira transition PROJ-123 --to "In Review" --dry-run
Always run transitions first when unsure: IDs differ per workflow and current status, and names repeat across workflows. --resolution satisfies Done-style screens that require one; omitting it yields 400 with an error naming the missing field.
Global Flags
All flags work in any position. Read commands need credentials; --help and --dry-run do not. Mutating commands require an explicit --yes/--force gate:
jira --json list --project PROJ # machine output anywhere
jira --dry-run create --project PROJ --summary "Test" # offline preview
jira --yes create --project PROJ --summary "Test" # explicit write authorization
jira --quiet list # suppress non-essential output
--json emits one JSON object per command on stdout — pipe to jq for structure.
Multi-Step Pipeline Recipes
Sprint hygiene sweep
Find stalled sprint work, review each ticket, close what's finished:
jira list --jql 'sprint IN openSprints() AND updated < -14d AND resolution = Unresolved' --json \
| jq -r '.issues[].key' \
| while read -r key; do jira view "$key"; jira transitions "$key"; done
# after human review, per key:
jira --yes transition "$key" --to Done --resolution Done
The list --json shape is {"total": N, "issues": [{"key", "summary", "status", "assignee", "issuetype", "priority"}]}.
Bulk-close with safe discovery
Transition IDs are workflow-specific — resolve before writing:
for key in $(jira list --jql 'status = "In Progress" AND updated < -30d' --json | jq -r '.issues[].key'); do
tid=$(jira transitions "$key" --json | jq -r '.transitions[] | select(.status_category=="completed") | .id' | head -1)
[ -n "$tid" ] && jira --yes transition "$key" --to "$tid" --resolution Done
done
Weekly digest via jq
jira list --jql 'assignee = currentUser() AND updated >= startOfWeek()' --max 50 --json \
| jq -r '.issues[] | "\(.key)\t\(.status)\t\(.summary)"'
More ready-to-run queries live in references/jql-cookbook.md, organized by role.
Using --json with jq
jira list --project PROJ --json | jq '.issues[] | {key, status, assignee}'
jira count --jql 'project = PROJ' --json | jq .count
jira transitions PROJ-123 --json | jq -r '.transitions[] | "\(.id)=\(.name) -> \(.to_status)"'
Known Gotchas
- Search endpoint duality — this CLI uses the classic
/rest/api/3/searchwith offset pagination (startAt,maxResults,total). Atlassian's enhanced/rest/api/3/search/jqlreplaces it with an opaquenextPageToken(+isLast), nostartAt, nototal, ids-only default fields, and it rejects unbounded JQL (order by key descalone → 400). The classic endpoint is deprecated ("currently being removed", announced Oct 2024, removal promised after May 1 2025), so expect forced migration; mixing the two pagination models is the classic source of infinite-page-one loops. - Pagination caps — legacy pages default to
maxResults=50;totalcan shrink between pages, so always tolerate empty pages instead of trusting a stale total. - Transitions need GET first — transition IDs (
"31","711") belong to one workflow/status; asking for an invalid one returns an empty list, not an error. Done-style screens frequently requireresolution; missing required fields come back as400with"errors": {"resolution": "..."}naming them. - ADF everywhere — descriptions, comments, and environment fields take ADF JSON objects in v3 payloads; bare strings are rejected.
- Authentication uses HTTP Basic with email + API token. CAPTCHA lockouts (repeated bad logins) block REST auth entirely; symptom header:
X-Seraph-LoginReason: AUTHENTICATION_DENIED. Fix in the browser, not by retrying. - Rate limits return 429 with
Retry-AfterandRateLimit-Reasonheaders; the CLI surfaces both but does not auto-retry. Writes also cap at ~20/2s per issue. - Project keys are case-sensitive in some contexts, though the API generally accepts either case.
- accountId, not username — user fields accept Atlassian account IDs (GDPR migration); usernames were removed from the API.
JQL gotchas
!=excludes empty values —assignee != currentUser()silently drops unassigned issues. Write(assignee != currentUser() OR assignee IS EMPTY).- AND binds tighter than OR —
A OR B AND Cparses asA OR (B AND C). Always parenthesize OR groups; without parentheses evaluation is left-to-right. - No leading wildcards —
summary ~ "*bug"forces a full scan; put wildcards after the first characters. - Filter by project first — the biggest performance lever on large instances (official optimization guidance).
- History operators have a field whitelist —
WAS/CHANGEDwork only on Assignee, Fix Version, Priority, Reporter, Resolution, Status, and silently return nothing on fields without history tracking. - Relative dates are case-sensitive —
-1mis minutes,-1Mis months; day-grain expressions evaluate in each user's timezone. - JQL has no aggregation — no COUNT/SUM; use
jira count(approximate-count endpoint) or dashboard gadgets.
When to use
- Any Jira Cloud interaction from the terminal: search, view, create, comment, transition
- Writing, debugging, or optimizing JQL queries — full language reference included
- Sprint reviews, triage sweeps, bulk status hygiene, dashboards and saved-filter design
When not to use
Do not use this skill for GitHub or GitLab issue tracking (use those platforms' own tooling such as gh), for Jira site administration like permission schemes or workflow editing (admin UI territory), for Confluence content, or for building server-side integrations against the Jira API (use official Atlassian SDK docs instead).
Reference Files
| File | Topic | Read when |
|---|---|---|
| references/rest-auth-and-search.md | Basic-auth/token mechanics vs OAuth/PATs, rate-limit headers, error envelopes, legacy-vs-enhanced search pagination duality | Setting up credentials, handling 429/401s, paginating large searches, or migrating off /search |
| references/rest-issues-and-transitions.md | GET/POST/PUT issue shapes, transitions GET→POST flow with screen-field requirements, ADF document model | Creating/editing issues programmatically, resolving transition failures, formatting rich text |
| references/jql-functions-catalog.md | Every JQL function with supported fields/operators — date/time, user, sprint/version, custom field, JSM approvals & SLAs | Checking which operators/functions a query can use |
| references/jql-best-practices.md | Operator precedence, performance rules, the empty-value trap, troubleshooting flows, marketplace extensions | A query is slow, wrong, or mixes AND/OR |
| references/jql-cookbook.md | 50 ready-to-run queries organized by role (developers, scrum masters, POs/managers, power users, admins) | Building filters, automation rules, sprint reviews |
| references/jql-history-and-dates.md | WAS/CHANGED predicate walkthrough, relative-date expression tables, saved-filter composition and naming conventions | History queries, date math, or designing reusable saved filters |
Available Scripts
| Script | Purpose | Invocation |
|---|---|---|
scripts/jira |
The CLI this skill drives: me, list, view, projects, create, comment, count, transitions, transition — all with --json/--dry-run, lazy auth, offset-pagination fetches above 50 results, parsed API error messages, and 429 Retry-After surfacing. Run it for every Jira data question above. |
scripts/jira list --project PROJ --json |
scripts/test_jira.py |
Offline pytest/unittest suite covering help text, argument errors, dry-run plans, pagination loops, error envelopes, and transition resolution logic — zero network. Run after modifying scripts/jira. |
.venv/bin/python3 -m pytest -p no:cacheprovider --strict-markers scripts/test_jira.py |
Prerequisites
- Python 3.8+ with
requests(stdlib otherwise); invoke aspython3 scripts/jira ...if not executable directly JIRA_EMAIL+JIRA_API_TOKENexported for any non-dry-run command (token from https://id.atlassian.com/manage/api-tokens);JIRA_SERVERdefaults tohttps://your-domain.atlassian.netjqrecommended for--jsonpost-processing
Limitations
- Targets Jira Cloud REST v3; Data Center sites authenticate differently (Bearer PAT) and expose older API surfaces
- The classic search endpoint this CLI uses is deprecated upstream; expect eventual forced migration to
/search/jqltoken paging - Rich-text creation beyond plain paragraphs requires hand-built ADF JSON
- No auto-retry on 429; loops over many writes should sleep between calls
Files (agent-skills)
-
evals
-
evals.json 5.5 KB
{ "schema_version": 1, "skill_name": "jira", "evals": [ { "id": "search-project-issues-readonly", "prompt": "What's currently open in the PROJ project? Show me the newest tickets first.", "expected_output": "Scenario: read-only issue search. The agent confirms JIRA_EMAIL/JIRA_API_TOKEN/JIRA_SERVER are set, then runs `jira list --project PROJ --json` (the --project shortcut builds `project=PROJ ORDER BY created DESC`). Results are presented from actual CLI output; for more than 50 matches the CLI follows offset pagination automatically. No write commands are invoked.", "assertions": [ "jira list is used with --project PROJ for the search", "--json output is requested when results feed further processing", "No create, comment, or transition commands run for this read-only request", "Results come from real command output rather than invented tickets" ] }, { "id": "stalled-sprint-bulk-close-pipeline", "prompt": "Find sprint work that hasn't been touched in two weeks and close out whatever is actually finished. Set a resolution when you close them.", "expected_output": "Scenario: multi-step write pipeline. The agent runs `jira list --jql 'sprint IN openSprints() AND updated < -14d AND resolution = Unresolved' --json`, pipes keys through jq (`jq -r '.issues[].key'`), inspects candidates with `jira view KEY`, discovers valid transitions per issue with `jira transitions KEY` (transition IDs are workflow- and status-specific, so names alone are unreliable), then applies `jira transition KEY --to Done --resolution Done`. It previews writes with --dry-run first and reports per-ticket outcomes.", "assertions": [ "The JQL uses openSprints() with an staleness bound such as updated < -14d and excludes resolved work", "jira transitions is used to discover valid transition IDs before transitioning", "jira transition carries --resolution so screen-required resolution fields do not fail", "Each ticket's outcome is reported rather than assumed" ] }, { "id": "transition-id-gotcha", "prompt": "Move PROJ-412 to Done.", "expected_output": "Scenario: gotcha-aware transition. The agent knows transition IDs differ per workflow/status and that Done-style transitions often require a resolution field on their screen. It runs `jira transitions PROJ-412` to list available IDs, picks the one whose target status category is completed, and runs `jira transition PROJ-412 --to <id-or-name> --resolution Done`. If the API returns 400 naming a missing field (errors.resolution), it retries including that field rather than giving up or claiming success.", "assertions": [ "Available transitions are listed before applying one", "The transition call includes --resolution to satisfy screen requirements", "A 400 error naming a missing field is handled by supplying that field", "Success is confirmed from command output (204-class response), not fabricated" ] }, { "id": "count-before-deep-dive", "prompt": "How many unresolved bugs do we have across the org right now?", "expected_output": "Scenario: fast aggregate question. JQL has no COUNT aggregation, but the CLI exposes the approximate-count endpoint: the agent runs `jira count --jql 'issuetype = Bug AND resolution = Unresolved' --json` and reads `.count` from the output. It does not fetch hundreds of issues with `jira list --max 1000` just to count them, though it may mention that list would page through rows if details were needed.", "assertions": [ "jira count is used instead of fetching all matching issues", "The JQL expresses bug + unresolved constraints without aggregation syntax", "The answer comes from the .count field of the JSON output", "No attempt is made to use COUNT/SUM inside JQL itself" ] }, { "id": "github-issue-not-jira", "prompt": "Can you open a GitHub issue for this crash on our repo?", "expected_output": "Scenario: should-not-trigger. The request targets GitHub issue tracking, which this skill explicitly does not cover. The agent does not load jira or invoke its CLI; it routes the request to GitHub tooling (e.g., gh issue create) instead, noting that jira handles Atlassian Jira sites only.", "assertions": [ "The jira skill is not loaded or executed for a GitHub-issue request", "A GitHub-native route such as the gh CLI is suggested", "No JQL is written or JIRA_* env vars requested" ] }, { "id": "auth-setup-and-token-expiry", "prompt": "Set up Jira access for me so you can start triaging my tickets, and explain what credentials you need.", "expected_output": "Scenario: auth setup guidance. The agent explains Jira Cloud auth: an API token created at id.atlassian.com/manage/api-tokens used over HTTP Basic auth as base64(email:token) via JIRA_EMAIL and JIRA_API_TOKEN env vars plus JIRA_SERVER; passwords are deprecated and tokens now expire after at most one year. It notes that Personal Access Tokens (Bearer) are Data Center only, not Cloud. It verifies connectivity read-only with `jira me`, and never asks for or echoes the token value itself.", "assertions": [ "Basic auth with email + API token is described as the Cloud method", "JIRA_EMAIL, JIRA_API_TOKEN, and JIRA_SERVER env vars are named", "PAT bearer tokens are identified as Data Center-only, not Cloud", "Verification proceeds via jira me without exposing the secret" ] } ] }
-
-
references
-
jql-best-practices.md 7.5 KB
# JQL Best Practices, Performance & Troubleshooting Performance rules, the traps JQL springs on unwary writers, and a troubleshooting flow for queries that misbehave. Pairs with [jql-functions-catalog.md](jql-functions-catalog.md) for the function reference and [jql-cookbook.md](jql-cookbook.md) for ready-to-run queries. ## Operator Precedence **AND binds tighter than OR.** `A OR B AND C` parses as `A OR (B AND C)` — almost never what you meant. Always parenthesize OR groups: ```text -- Correct (project = A OR project = B) AND status = Open -- Wrong — reads as project = A OR (project = B AND status = Open) project = A OR project = B AND status = Open ``` Mixing AND and OR without parentheses is listed in the common-mistakes table below for a reason: results will surprise you. ## Performance Optimization ### Do's - **Filter by project first** — narrows the search space immediately - **Use `IN` over chained `OR`** — `status IN ("X", "Y")` vs `status = X OR status = Y` - **Prefer indexed fields** — `project`, `issuetype`, `status`, `assignee` are always indexed - **Use IDs for stable entities** — `project = 1001` survives renames; `project = "Old Name"` breaks when the name changes - **Break complex queries into saved filters** — save the sub-query once, then compose with `filter = "Saved Filter Name"` - **Keep relative dates in saved filters** — they stay dynamic instead of freezing at creation time ### Don'ts - **Don't lead with wildcards** — `summary ~ "*bug"` forces a full-text scan across all issues; starting a text search with `*` is very expensive, put wildcards after the first few characters - **Don't overuse negations** — `!=`, `!~`, `NOT IN`, `NOT` scan wider than positive conditions - **Don't sort in JQL when downstream sorts** — redundant sorting wastes server cycles - **Don't mix AND/OR without parentheses** — see precedence rule above ## The Empty-Value Trap Negation does **not** include empty values. `!=` excludes nulls, so a plain negation silently drops unassigned issues. Explicitly include EMPTY: ```jql -- Finds all issues NOT assigned to current user, INCLUDING unassigned (assignee != currentUser() OR assignee IS EMPTY) -- NOT this — misses unassigned issues entirely assignee != currentUser() ``` The same trap applies to every negated comparison (`!=`, `NOT IN`) on optional fields. If you want "everything except X", write `(field != X OR field IS EMPTY)`. ## Common Mistakes | Mistake | Example | Fix | |---------|---------|-----| | Missing EMPTY on negation | `assignee != currentUser()` | `(assignee != currentUser() OR assignee IS EMPTY)` | | AND/OR precedence | `A OR B AND C` | `(A OR B) AND C` | | Name vs ID fragility | `project = "My Project"` | `project = 1001` (IDs survive renames) | | Searching by renamed sprint | `sprint = "Sprint 1"` | Use the sprint ID | | Status but no resolution | `status = Done` | Add `resolution IS NOT EMPTY` or `resolution = Fixed` | | Missing timezone offsets | `created > startOfDay()` | Jira evaluates dates in the user's configured timezone | | Forgetting sprint scope | `sprint IS EMPTY` | Also check `sprint NOT IN openSprints()` to catch backlog items | ## Gotchas ### Core Platform - **Atlassian is renaming "issue" to "work item"** — old terms (project, issue, fixVersion) still work; no migration needed. New docs say "work item", but existing queries are backward-compatible. - **JQL has NO aggregation** — COUNT, SUM, AVG do not exist. Use dashboard gadgets (pie chart, statistics), marketplace apps such as eazyBI or ScriptRunner, or pull via REST API and aggregate externally. - **No recursive hierarchy traversal** — you cannot fetch epics + their stories + their subtasks in one query. Run separate statements per level, or use marketplace plugins. - **`IS EMPTY` only works for fields that exist** — it cannot find issues where a field was *never* given a value. - **`!=` excludes nulls** — pair it with `OR field IS EMPTY` whenever you want truly everything except a value. ### Function-Specific - **`membersOf()` does NOT support project roles** — only Jira groups and teams (by team id). - **`updatedBy()` rounds to a 1-day minimum** — `updatedBy(jsmith, "-1h")` behaves as `-1d`. - **`votedWorkItems()` / `watchedWorkItems()` cap at 32,000** — beyond that, results truncate silently. - **`cascadeOption(none)` is keyword-based** — to literally match a value of "none", quote-escape it: `cascadeOption("\"none\"")`. ### Jira Cloud-Specific - **Saved filters can share names** — Jira does not prevent duplicates, which makes name-based dashboard gadget references fragile. - **Auto-suggest depends on permissions** — if a field never appears in autocomplete, you may simply lack permission to it. - **A JQL AI assistant exists in Cloud** — button left of the JQL bar. Early-stage; useful for beginners but misses nuance. - **Some custom fields don't log history** — `CHANGED` and `WAS` silently return nothing on fields without history tracking. ### ScriptRunner (if installed) - **Powerful but heavier** — `issueFunction` adds latency versus native JQL. - **Common uses:** `issueFunction in hasLinkType("Epic-Story Link")`, `issueFunction in commented("by user after -1d")`. ## Troubleshooting Flow **Query returns 0 results unexpectedly** 1. Check field-name spelling (custom fields especially) 2. Verify the project/sprint/version actually exists 3. Check value case sensitivity — it depends on your Jira configuration 4. Run `ORDER BY created DESC` alone to confirm the query executes and genuinely has no matches 5. Remember the empty-value trap: negations exclude empty fields **Query is valid but slow** 1. Remove leading wildcards from text searches 2. Add a project filter first 3. Replace `OR` chains with `IN` 4. Remove negations where possible 5. Tighten date/status bounds to shrink the candidate set **Jira says "Field 'X' does not exist"** 1. The field may be disabled for this project 2. It may be a custom field owned by an uninstalled marketplace app 3. The querying user may lack permission to that field **"Filter not found" when using `filter =`** - The user lacks permission to that saved filter (or the filter was deleted). **`CHANGED` returns nothing** 1. The field may not track history 2. Widen the date range — `CHANGED TO "Done" AFTER startOfDay()` may be too narrow 3. Confirm the transition actually happened; some workflows skip statuses **`WAS` operator returns no results** 1. Most system fields have trackable history, but verify this one does 2. `WAS` matches past values — if the field always held its current value there is no history to match ## Marketplace Extensions for Advanced Needs | Plugin | Hosting | What It Adds | |--------|---------|-------------| | JQL Tricks Plugin | Server/DC | 50+ extra functions | | JQL Search Extensions | Cloud | Find comments, attachments, subtasks, epics, links | | JQL Booster Pack | Server/DC | 15+ user-related functions, archived version filtering | | JQL Functions Collection | Server/DC | String and date format functions | | Groups & Organizations JQL | Server/DC | Match multi-group custom field values | | ScriptRunner (Adaptavist) | Cloud/Server/DC | Custom Groovy JQL functions — most powerful and flexible | Attribution: adapted from the retired jira-jql skill, sourced from Atlassian official documentation and community best practices. ## Sources - Advanced searching (JQL): https://support.atlassian.com/jira-software-cloud/docs/use-advanced-search-with-jira-query-language-jql/ - Search endpoint used to run JQL over REST: https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/ -
jql-cookbook.md 6.4 KB
# JQL Cookbook — Role-Based Ready Queries Fifty ready-to-run JQL queries organized by role. Every query is copy-pasteable; substitute your own project keys, group names, and issue keys. Pairs with [jql-functions-catalog.md](jql-functions-catalog.md) for function semantics and [jql-best-practices.md](jql-best-practices.md) for performance rules. ## Developers (10 queries) ```jql -- 1. My plate, sorted by urgency assignee = currentUser() AND resolution = Unresolved ORDER BY priority DESC ``` ```jql -- 2. Bugs I reported that haven't been fixed reporter = currentUser() AND status != Done ``` ```jql -- 3. Where I'm mentioned (standup prep) comment ~ currentUser() ``` ```jql -- 4. My completed work this week resolution = Fixed AND resolutiondate >= -7d AND assignee = currentUser() ``` ```jql -- 5. This week's deadlines duedate >= startOfWeek() AND duedate <= endOfWeek() ``` ```jql -- 6. My blocked tickets issueLinkType = "is blocked by" AND assignee = currentUser() ``` ```jql -- 7. Subtasks of a specific story parent = "PROJ-123" ``` ```jql -- 8. Watched but not closed watcher = currentUser() AND status != Closed ``` ```jql -- 9. Full-text search across summary, description, and comments text ~ "error message here" ``` ```jql -- 10. Recent unplanned work created >= -3d AND assignee = currentUser() AND resolution = Unresolved ``` ## Scrum Masters (9 queries) ```jql -- 11. Unassigned in active sprint sprint IN openSprints() AND assignee IS EMPTY ``` ```jql -- 12. Zombie tickets (not touched in 30 days) status NOT IN (Closed, Done) AND updated < -30d ``` ```jql -- 13. Recently completed this sprint status CHANGED TO Done AFTER startOfWeek() ``` ```jql -- 14. Reopened tickets (quality regression flag) status CHANGED FROM Done TO "In Progress" ``` ```jql -- 15. Volatility — new issues created into the current sprint sprint IN openSprints() AND created >= -1w ``` ```jql -- 16. Team's in-flight work assignee in membersOf("Dev Team") AND status = "In Progress" ``` ```jql -- 17. Carried-over issues (in current sprint, was in previous) fixVersion = "Current Sprint" AND fixVersion WAS "Last Sprint" ``` ```jql -- 18. Issues blocked by anything issueLinkType = "is blocked by" ``` ```jql -- 19. Sprint capacity check sprint IN openSprints() AND assignee in membersOf("Dev Team") ``` ## Product Owners / Managers (10 queries) ```jql -- 20. Pre-release readiness fixVersion = earliestUnreleasedVersion() AND status != Done ``` ```jql -- 21. Firefighting view — critical unresolved bugs priority IN (Critical, Highest) AND resolution = Unresolved ``` ```jql -- 22. Due within the calendar month duedate >= startOfMonth() AND duedate <= endOfMonth() AND resolution = Unresolved ``` ```jql -- 23. Pending approvals awaiting action (JSM) approvals = pending() ``` ```jql -- 24. Epics without stories attached (requires ScriptRunner) issuetype = Epic AND issueFunction not in hasLinkType("Epic-Story Link") ``` ```jql -- 25. Component-level tech debt component = "Backend" AND status != Done ORDER BY priority DESC ``` ```jql -- 26. Recently reported bugs for triage review issuetype = Bug AND created >= -14d ORDER BY created DESC ``` ```jql -- 27. Cross-project portfolio view project in ("Project Mercury", "PTC") AND issuetype in ("Epic", "Task") AND status = "To Do" AND created >= -180d ``` ```jql -- 28. Status-category rollup across workflows statusCategory = "In Progress" OR statusCategory = "To Do" ``` ```jql -- 29. Feature completeness for a release fixVersion = "v2.0" AND status != Done ORDER BY component, priority ``` ## Power Users (12 queries) ```jql -- 30. Cascading select matches a specific path location in cascadeOption("USA", "New York") ``` ```jql -- 31. Issues assigned to any administrator assignee in membersOf("jira-administrators") ``` ```jql -- 32. Issues where I was the previous assignee assignee WAS currentUser() ``` ```jql -- 33. Resolved by me this year (retrospective input) resolution CHANGED TO "Fixed" BY currentUser() DURING (startOfYear(), endOfYear()) ``` ```jql -- 34. Issues updated by a specific user in the last week issue in updatedBy(jsmith, "-8d") ``` ```jql -- 35. Issues linked through a specific link type issue in linkedIssues("PROJ-123", "is duplicated by") ``` ```jql -- 36. Issues that were in a specific sprint historically sprint WAS "Sprint 5" ``` ```jql -- 37. Children of an epic parentEpic = "PROJ-EPIC-1" ``` ```jql -- 38. Everything that changed status in the last 24 hours status CHANGED AFTER -1d ``` ```jql -- 39. All subtask issue types issuetype in subtaskWorkTypes() ``` ```jql -- 40. Issues that breached SLA (JSM) SLA = breached() ``` ```jql -- 41. Approval requests assigned to me as approver (JSM) approvals = myPendingApproval() ``` ## Automation / Admin Queries (9 queries) ```jql -- 42. Reports from users outside an internal group (access reviews) reporter NOT IN membersOf("internal-users") ``` ```jql -- 43. Projects where the user holds no Developers role (permission audit) project NOT IN spacesWhereUserHasRole("Developers") ``` ```jql -- 44. Old unassigned tickets (automation: assign or close) assignee IS EMPTY AND created < -90d AND resolution = Unresolved ``` ```jql -- 45. Bulk transition candidates (stalled in progress) status = "In Progress" AND updated < -14d ``` ```jql -- 46. Stale sprints still holding unresolved work (automation: warn or move) sprint IN closedSprints() AND resolution = Unresolved ``` ```jql -- 47. Approvals decided by a specific user (audit trail, JSM) approvals = approver(jsmith) ``` ```jql -- 48. SLA clocks at risk of breach soon (JSM monitoring) SLA != completed() AND SLA <= remaining("-4h") ``` ```jql -- 49. Requests from one customer organization (JSM triage split) reporter in organizationMembers("YOUR_ORG") AND resolution = Unresolved ``` ```jql -- 50. Backlog hygiene: never-scheduled and long untouched (sprint IS EMPTY OR sprint NOT IN openSprints()) AND updated < -60d AND resolution = Unresolved ``` Attribution: adapted from the retired jira-jql skill, sourced from Atlassian official documentation and community best practices. ## Sources - Advanced searching (JQL): https://support.atlassian.com/jira-software-cloud/docs/use-advanced-search-with-jira-query-language-jql/ - JQL functions reference: https://support.atlassian.com/jira-software-cloud/docs/jql-functions/ - Search endpoint used to run JQL over REST: https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/ -
jql-functions-catalog.md 8.4 KB
# JQL Functions — Complete Catalog Every JQL function with its supported fields, operators, and worked syntax. Pairs with [jql-best-practices.md](jql-best-practices.md) for performance rules and [jql-cookbook.md](jql-cookbook.md) for ready-to-run queries. Increment strings follow the `(+/-)nn(y|M|w|d|h|m)` pattern everywhere a date function accepts an offset; if the unit qualifier is omitted it defaults to the function's natural period. ## Date and Time Functions All accept an optional increment string `(+/-)nn(y|M|w|d|h|m)`. If the unit qualifier is omitted, it defaults to the natural period shown below. **Supported fields:** Created, Due, Resolved, Updated, custom Date/Time fields **Supported operators:** `=, !=, >, >=, <, <=, WAS*, WAS IN*, WAS NOT*, WAS NOT IN*, CHANGED*` (* predicate position only) **Unsupported operators:** `~, !~, IS, IS NOT, IN, NOT IN` | Function | Syntax | Notes | |----------|--------|-------| | `startOfDay()` | `created > startOfDay("-1")` | Start of current day. Default unit: d | | `endOfDay()` | `due < endOfDay("+2")` | End of current day. Default unit: d | | `startOfWeek()` | `created > startOfWeek("+1d")` | Start of week (Sunday default; +1d shifts to Monday) | | `endOfWeek()` | `due < endOfWeek("+1")` | End of week (Saturday default by Saturday) | | `startOfMonth()` | `created > startOfMonth("-1")` | Start of current month | | `endOfMonth()` | `due < endOfMonth("+15d")` | End of current month. +15d = 15th of next month | | `startOfYear()` | `created > startOfYear()` | January 1st | | `endOfYear()` | `due < endOfYear()` | December 31st | | `now()` | `updated < now()` | Current exact time | | `currentLogin()` | `updated > currentLogin()` | When the session started | | `lastLogin()` | `created > lastLogin()` | Previous login timestamp | Relative offsets work directly on date fields without a function: `created >= -7d`, `updated < -30d`. ## User Functions ### `currentUser()` Your identity. Only works for logged-in users (not anonymous access). - **Fields:** Assignee, Reporter, Voter, Watcher, Creator, custom User fields - **Operators:** `=`, `!=` ### `membersOf(group)` Members of a group or team. - **Syntax:** `membersOf("group-name")`, or `membersOf(id:<teamId>)` for teams - **Fields:** Assignee, Reporter, Voter, Watcher, Creator, custom User fields - **Operators:** `IN, NOT IN, WAS IN, WAS NOT IN` - Does **NOT** support project roles — groups and teams only ### `componentsLeadByUser(user)` Components led by a user. Omit the user argument to mean the current user. - **Fields:** Component - **Operators:** `IN, NOT IN` ### `spacesLeadByUser(user)` Projects led by a user. Omit the user argument to mean the current user. - **Fields:** Project (Space) - **Operators:** `IN, NOT IN` ### `spacesWhereUserHasPermission(permission)` Projects where you hold a specific permission, e.g. `"Edit work items"`. - **Fields:** Project - **Operators:** `IN, NOT IN` - Only available for logged-in users ### `spacesWhereUserHasRole(rolename)` Projects where you have a specific role, e.g. `"Administrators"`. - **Fields:** Project - **Operators:** `IN, NOT IN` ## Sprint and Version Functions ### `openSprints()` Active sprints that have started but not yet completed. - **Fields:** Sprint - **Operators:** `IN, NOT IN` - Issues can belong to open AND closed sprints simultaneously ### `closedSprints()` Completed sprints. - **Fields:** Sprint - **Operators:** `IN, NOT IN` ### `earliestUnreleasedVersion(project)` Earliest unreleased version, ordered by the Releases page order (bottom = earliest). - **Fields:** AffectedVersion, FixVersion, custom Version fields - **Operators:** `=, !=` ### `latestReleasedVersion(project)` Most recently released version. - **Fields:** AffectedVersion, FixVersion, custom Version fields - **Operators:** `=, !=` ### `releasedVersions(project)` All released versions. Omit the project argument to search across all projects. - **Fields:** AffectedVersion, FixVersion, custom Version fields - **Operators:** `IN, NOT IN` ### `unreleasedVersions(project)` All unreleased versions. Omit the project argument to search across all projects. - **Fields:** AffectedVersion, FixVersion, custom Version fields - **Operators:** `IN, NOT IN` ## Issue Functions ### `linkedIssues(key, linkType?)` Issues linked to a specific issue. The link type argument is optional. ```jql issue in linkedIssues("ABC-44") issue in linkedIssues("ABC-44", "is blocked by") ``` - **Fields:** Issue - **Operators:** `IN, NOT IN` ### `issueHistory()` / `votedWorkItems()` / `watchedWorkItems()` Recently viewed, voted-on, and watched issues respectively. - **Operators:** `IN, NOT IN` - `votedWorkItems()` and `watchedWorkItems()` return up to 32,000 issue IDs ### `updatedBy(user, dateFrom?, dateTo?)` Issues updated by a specific user — includes creating the issue, updating fields, creating/deleting comments, and editing comments. ```jql issue in updatedBy(jsmith, "-8d") issue in updatedBy(jsmith, "2024/01/01", "2024/06/01") ``` - **Operators:** `IN, NOT IN` (used with the `issue` field) - Minimum granularity is 1 day; smaller values such as `-1h` round up to `-1d` ### `parentEpic` (field, not a function) Find stories/subtasks belonging to a specific epic. ```jql parentEpic = DEMO-123 parentEpic in (DEMO-1, SAMPLE-4) ``` - **Fields:** Issue - **Operators:** `=, !=, IN, NOT IN` - Company-managed projects only ## Custom Field Functions ### `cascadeOption(parentOption, childOption?)` Cascading Select custom fields. ```jql location in cascadeOption("USA", "New York") ``` Use the `none` keyword to match an empty tier: `location in cascadeOption("USA", none)`. - **Operators:** `IN, NOT IN` ### `choiceOption(valueOption...)` Multiple Choice or Dropdown custom fields. - **Operators:** `IN, NOT IN` ### `standardWorkTypes()` / `subtaskWorkTypes()` Filter by standard versus subtask issue types. - **Fields:** Type - **Operators:** `IN, NOT IN` ## Jira Service Management Functions These require Jira Service Management and operate on the Approval and SLA custom fields. ### Approval Functions | Function | Syntax | Effect | |----------|--------|--------| | `approved()` | `approvals = approved()` | All approved requests | | `pending()` | `approvals = pending()` | Has a pending approval step | | `approver(user)` | `approvals = approver(jsmith)` | Specific user is an approver (pending or completed) | | `pendingApprovalBy(user)` | `approvals = pendingApprovalBy(jsmith)` | User has a pending approval | | `pendingBy(user)` | `approvals = pendingBy(jsmith)` | User is an approver, may or may not have decided | | `myApproval()` | `approvals = myApproval()` | Current user is an approver | | `myPendingApproval()` | `approvals = myPendingApproval()` | Current user has a pending approval | | `myPending()` | `approvals = myPending()` | Current user is the approver for a pending step | ### SLA Functions | Function | Operators | Effect | |----------|-----------|--------| | `breached()` | `=, !=` | SLA missed its goal | | `completed()` | `=, !=` | SLA cycle complete | | `running()` | `=, !=` | SLA clock running | | `paused()` | `=, !=` | SLA paused (out of calendar hours, etc.) | | `remaining()` | `=, !=, >, <, >=, <=` | Compare remaining time | | `withinCalendarHours()` | `=, !=` | Running within calendar hours | ### Organization and Customer Functions | Function | Fields | Syntax | |----------|--------|--------| | `customerDetail("Field", "Value")` | Reporter, Assignee, Voter, Watcher | `reporter in customerDetail("Region", "APAC")` | | `organizationDetail("Field", "Value")` | Organization | `organization in organizationDetail("Support level", "Platinum")` | | `organizationMembers("OrgName")` | Reporter, Assignee, Voter, Watcher | `reporter in organizationMembers("YOUR_ORG")` | `customerDetail()` and `organizationDetail()` pair with multi-select dropdown fields; chain multiple `AND` clauses for combined matches. Both return up to 32,000 records and include deleted/deactivated customers — exclude them with `AND reporter NOT IN inactiveUsers()`. - **Operators:** `IN, NOT IN` Attribution: adapted from the retired jira-jql skill, sourced from Atlassian official documentation and community best practices. ## Sources - JQL functions reference: https://support.atlassian.com/jira-software-cloud/docs/jql-functions/ - JQL fields reference: https://support.atlassian.com/jira-software-cloud/docs/jql-fields/ - Search endpoint used to run JQL over REST: https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/ -
jql-history-and-dates.md 10.8 KB
# JQL History Predicates, Date Expressions & Saved Filters Deep-dive on the three areas that trip up even experienced JQL writers: history operators (`WAS`, `CHANGED` and their predicates), relative-date expressions, and saved-filter composition. Pairs with [jql-functions-catalog.md](jql-functions-catalog.md), [jql-best-practices.md](jql-best-practices.md), and [jql-cookbook.md](jql-cookbook.md). A JQL clause is a field followed by an operator followed by one or more values or functions (`project = "TEST"`); clauses join with keywords like `AND`/`OR`. Without parentheses a statement evaluates left-to-right, which is why parenthesizing OR groups matters. ## History Operators: the WAS Family **Field restriction first:** `WAS`, `WAS IN`, `WAS NOT`, and `WAS NOT IN` work with **Assignee, Fix Version, Priority, Reporter, Resolution, and Status only**. On any other field they error out; custom-field history is simply not addressable through these operators. ### What WAS actually matches `status WAS "In Progress"` finds issues that currently have OR previously had that value. Two subtle matching rules: 1. It matches the value name **as it was configured at the time of the change** — if your workflow renamed "In Progress" to "Active" last quarter, `status WAS "In Progress"` still finds historical states recorded under the old name. 2. It also matches the value's numeric ID — `status WAS "Resolved"` and `status WAS "4"` hit the same issues when 4 was Resolved's ID. ### Optional predicates Every WAS-family operator accepts optional predicates: | Predicate | Form | Meaning | |-----------|------|---------| | `AFTER` | `AFTER "date"` | change happened after the date | | `BEFORE` | `BEFORE "date"` | change happened before the date | | `BY` | `BY "user"` / `BY (user1,user2)` | user who made the change | | `DURING` | `DURING ("date1","date2")` | change inside the window | | `ON` | `ON "date"` | change on that exact date | The `BY` user may be a username or an Atlassian account ID (`status WAS "Resolved" BY abcde-12345-fedcba BEFORE "2019/02/02"`). Dates use the standard JQL date format (`"2019/02/02"`) or any expression from the relative-date section below — `DURING (startOfYear(), endOfYear())` is valid. ### Walkthrough: build a "reopened bugs" query step by step Goal: bugs that went backwards from Done back to In Progress. ```jql -- Step 1: base form — did status ever hold "Done"? issuetype = Bug AND status WAS Done -- Step 2: add the transition direction with CHANGED (below): issuetype = Bug AND status CHANGED FROM Done TO "In Progress" -- Step 3: bound it to this year so the scan stays cheap: issuetype = Bug AND status CHANGED FROM Done TO "In Progress" DURING (startOfYear(), endOfYear()) ``` Each predicate composes: `priority CHANGED BY freddo BEFORE endOfWeek() AFTER startOfWeek()` chains two time bounds around a user bound. ### The other WAS operators | Operator | Equivalent longhand | Example | |----------|--------------------|---------| | `WAS IN ("Resolved","Closed")` | `status WAS "Resolved" OR status WAS "Closed"` | `status WAS IN ("Resolved","In Progress")` | | `WAS NOT "X"` | never held X | `status WAS NOT "In Progress" BEFORE "2011/02/02"` | | `WAS NOT IN (...)` | `WAS NOT A AND WAS NOT B` | `status WAS NOT IN ("Resolved","In Progress")` | ### The 10,000-change truncation If an issue has more than 10,000 changes, WAS-family queries search **only its most recent changes**. Ancient history on hyper-active issues is invisible to JQL — use the issue view or export for those. This is silent: you get results, just not complete ones. ## The CHANGED Operator `CHANGED` finds issues whose field value *changed* (not what it changed to — that is what `FROM`/`TO` refine). Predicates: everything WAS takes, **plus** `FROM "oldvalue"` and `TO "newvalue"`: | Predicate | Purpose | |-----------|---------| | `FROM "oldvalue"` | previous value equals | | `TO "newvalue"` | new value equals | | `AFTER` / `BEFORE` / `DURING` / `ON` | time bounds | | `BY "user"` | who performed the change | Same six-field restriction applies (Assignee, Fix Version, Priority, Reporter, Resolution, Status). Canonical patterns: ```jql -- Any assignee change at all: assignee CHANGED -- Regression detector: went backwards from In Progress to Open: status CHANGED FROM "In Progress" TO "Open" -- Priority churn by one user this week: priority CHANGED BY freddo AFTER startOfWeek() BEFORE endOfWeek() -- Resolved-by-me-this-year (cookbook #33): resolution CHANGED TO "Fixed" BY currentUser() DURING (startOfYear(), endOfYear()) ``` **Prerequisites and failure mode:** `CHANGED` and the WAS family return nothing for fields without history tracking — most system fields track, some custom fields do not. If a `CHANGED` query returns zero rows, confirm transitions actually occurred and widen the date window before assuming the data is missing. Note the docs' own quirk: the >10,000-changes truncation paragraph under CHANGED still says "the WAS operator" — same limit, shared implementation. ## Relative Dates and Expressions ### Direct offsets on date fields Date fields accept an increment string directly: `(+/-)nn(y|M|w|d|h)` — years, months (capital M!), weeks, days, hours. No function call needed: ```jql created >= -7d /* last seven days */ updated < -30d /* untouched for a month */ duedate <= 2w /* due within two weeks */ ``` Case matters: `-1m` is minutes, `-1M` is months. If you drop the unit entirely the default depends on context (days for the bare-number legacy form). ### Function forms | Expression | Evaluates to | Typical use | |------------|--------------|-------------| | `startOfDay()` | today 00:00 local | `created > startOfDay()` | | `endOfDay()` | today 23:59 local | `due < endOfDay("+1")` | | `startOfWeek()` | week start (Sunday default) | `created >= startOfWeek()` | | `endOfWeek()` | week end (Saturday default) | `due <= endOfWeek()` | | `startOfMonth()` / `endOfMonth()` | month boundaries | `resolved >= startOfMonth("-1M")` | | `startOfYear()` / `endOfYear()` | Jan 1 / Dec 31 | retrospective windows | | `now()` | exact current timestamp | `updated < now()` | Offsets compose inside functions: `startOfWeek("+1d")` shifts to Monday on Sunday-default sites; `endOfMonth("+15d")` lands mid-next-month. Full parameter tables per function live in [jql-functions-catalog.md](jql-functions-catalog.md). ### Timezone trap Jira evaluates dates in the querying user's timezone. A dashboard shared across regions shows different rows for the same `startOfDay()` query near midnight boundaries. For cross-timezone automation prefer explicit dates over day-grain relatives. ### Keep relatives in saved filters Relative expressions re-evaluate at every run — exactly what you want in a saved filter. Freezing an absolute date into a filter meant as "this week" is a classic mistake: the filter silently stops matching next week. ## Saved Filters: Composition and Naming Conventions Saved filters turn long JQL into reusable, shareable building blocks. From the filter lifecycle: save a search, manage/update/copy/delete it, star favorites, subscribe yourself or others to scheduled email delivery, share with colleagues (or outside the organization via links), export results (RSS, Excel), and drive dashboard gadgets. ### Composing queries with `filter =` ```jql filter = "My Team Open Bugs" AND priority in (High, Highest) filter = 10203 AND updated >= -7d -- numeric filter IDs also work ``` Sub-queries compose once and get reused everywhere; fix logic in one place instead of pasting the same clause into twenty dashboards. Performance-wise this does not make Jira faster by itself (Jira expands the filter), but it makes the optimization advice in [jql-best-practices.md](jql-best-practices.md) applyable from a single edit point. ### Naming conventions that survive contact with reality Jira does **not** enforce unique filter names — two people can each own "Open Bugs", and name-based references resolve ambiguously. Conventions that keep dashboards and subscriptions maintainable: 1. **Prefix by owning team or domain** — `platform-api-stale-prs`, `mobile-crash-triage`. Collisions become visible instead of silent. 2. **Encode scope and cadence** — `weekly-security-review`, `sprint-current-blocked`. Readers of a subscription email should know cadence without opening the filter. 3. **Never rename a filter others reference** — dashboard gadgets and subscriptions bind by filter identity, but humans navigate by name; renames strand both. Copy-and-deprecate instead. 4. **Prefer the numeric ID in scripts** — `filter = 10203` survives renames exactly like project IDs do; reserve name-based references for interactive use. 5. **Keep one canonical "definition" filter per recurring question** — then derive variants (`... AND assignee IS EMPTY`) rather than duplicating the whole query. Sharing rules matter before composition works: a gadget or subscription breaks with "Filter not found" for any viewer lacking permission to the underlying filter — grant the audience access to the filter itself, not just the dashboard. ## Quick Pitfall Reference | Symptom | Likely cause | |---------|--------------| | `WAS` errors on a custom field | History operators limited to Assignee/Fix Version/Priority/Reporter/Resolution/Status | | Old history missing on a busy issue | >10,000 changes truncated to recent-only search | | `CHANGED` returns nothing | No history tracking on the field, or transitions never actually happened | | Month offset behaved like minutes | `-1m` (minutes) vs `-1M` (months) case sensitivity | | Same filter shows different rows per region | Day-grain relatives evaluate in each user's timezone | | Gadget says "Filter not found" | Viewer lacks permission to the referenced saved filter | Attribution: adapted in part from the retired jira-jql skill, sourced from Atlassian official documentation. ## Sources - JQL operators (WAS/CHANGED/predicate reference): https://support.atlassian.com/jira-software-cloud/docs/jql-operators/ - Advanced searching overview (clause structure, precedence, bounded JQL): https://support.atlassian.com/jira-software-cloud/docs/use-advanced-search-with-jira-query-language-jql/ - What is advanced search (precedence, reserved words, bounded/unbounded): https://support.atlassian.com/jira-software-cloud/docs/what-is-advanced-search-in-jira-cloud/ - JQL functions (date functions, increment syntax): https://support.atlassian.com/jira-software-cloud/docs/jql-functions/ - Save your search as a filter: https://support.atlassian.com/jira-software-cloud/docs/save-your-search-as-a-filter/ - JQL optimization recommendations: https://support.atlassian.com/jira-software-cloud/docs/jql-optimization-recommendations/ - Search endpoint that runs JQL over REST (startAt/maxResults envelope): https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/ -
rest-auth-and-search.md 9.7 KB
# Jira Cloud REST API v3 — Auth, Search & Pagination Ground truth for authenticating against Jira Cloud and for running searches under both pagination models. Every claim traces to the Atlassian sources in the footer. ## Authentication ### Basic auth: email + API token (the default for scripts and CLIs) Atlassian's recommended method "for personal scripts, bots, and ad-hoc execution of the REST APIs": 1. Create an API token at `https://id.atlassian.com/manage/api-tokens` (shown once; cannot be recovered later). 2. Build the string `email:api_token` — email is your **Atlassian account email address**, never a password. 3. Base64-encode it and send it as a proactively-supplied header: ``` Authorization: Basic base64(email:token) ``` `requests` does this via `auth=(email, token)` tuple — no manual base64 needed. Facts worth knowing: - API tokens work even when the org has two-factor authentication or SAML enabled. - Since December 15 2024 new tokens expire after one year by default (configurable 1 day–1 year); tokens created before that were retroactively given expiry from March 13 2025. Expired tokens surface as 401s. - Password authentication is fully deprecated; there is no password fallback. - Jira does not send an auth challenge — clients must send the header unprompted. - Tokens can optionally carry OAuth-style scopes; scoped tokens are used against `https://api.atlassian.com/ex/jira/{cloudId}` instead of the site URL. Scopeless tokens keep working against `https://your-domain.atlassian.net`. ### The other methods, and when they apply | Method | Applies to | Header | Base URL | |--------|-----------|--------|----------| | Basic + API token | personal scripts, bots | `Basic base64(email:token)` | site URL | | OAuth 2.0 (3LO) | integrations acting for users, distributable apps | `Bearer ACCESS_TOKEN` | `https://api.atlassian.com/ex/jira/{cloudId}` | | Forge / Connect apps | apps on those platforms | built-in JWT/requestJira | varies | ### PATs: Data Center yes, Cloud no Personal Access Tokens (`Authorization: Bearer <token>`) exist only on **Data Center/Server** (Jira 8.14+). Jira Cloud has no PAT feature; its equivalent is the API-token model above. Point anyone asking about "Bearer tokens for Jira" at DC docs or to 3LO for Cloud. ### CAPTCHA lockout symptom After repeated failed logins Jira may trigger CAPTCHA, which blocks REST auth entirely. Symptom: response header `X-Seraph-LoginReason: AUTHENTICATION_DENIED` — login rejected "without even checking the password". Fix by logging in through the browser once, not by retrying the script. ## Rate Limits Three systems, all surfacing as HTTP 429 with these headers: ``` Retry-After: <seconds> X-RateLimit-Limit: <ceiling> X-RateLimit-Remaining: 0 X-RateLimit-Reset: <ISO 8601 timestamp> RateLimit-Reason: jira-quota-global-based ``` `RateLimit-Reason` values: `jira-quota-global-based`, `jira-quota-tenant-based` (hourly point quotas), `jira-burst-based` (per-second buckets; defaults ~100 req/s GET/POST), `jira-per-issue-on-write` (20 writes/2s per issue). Honor `Retry-After`; back off with jitter rather than tight retries. ## Error Envelopes Standard error collection everywhere in v3: ```json { "errorMessages": ["..."], "errors": {"field_name": "message"}, "status": 400 } ``` | Status | Meaning | |--------|---------| | 400 | Malformed request or bad JQL ("Field 'X' does not exist..."); `errors` map names offending fields | | 401 | Credentials rejected/expired (or CAPTCHA-gated — check `X-Seraph-LoginReason`) | | 403 | Authenticated but lacking permission | | 404 | Resource absent or invisible | | 422 | Validation failure on create/edit payloads | | 429 | Rate limited — see headers above | ## Search Endpoints: the Duality This is the single most important operational fact about Jira Cloud search in the current era: **two search endpoints with incompatible pagination models coexist**, and the legacy one is being removed. ### Legacy: `GET|POST /rest/api/3/search` — offset paging Status: documented as "**Currently being removed**" and marked deprecated in the OpenAPI spec. Announced 31 October 2024 with removal promised "after May 1, 2025" (CHANGE-2046); sunset has proceeded gradually since. Request parameters: `jql`, `startAt` (default 0), `maxResults` (default 50), `validateQuery` (`strict` default | `warn` | `none`), `fields`, `expand`, `properties`, `fieldsByKeys`, `failFast`. Response envelope (`SearchResults`): ```json { "issues": [{"id": "10002", "key": "ED-1", "fields": {}}], "startAt": 0, "maxResults": 50, "total": 1, "warningMessages": [] } ``` Loop shape: ```python start_at = 0 while True: page = get("/search", params={"jql": jql, "startAt": start_at, "maxResults": 100}) yield from page["issues"] start_at += len(page["issues"]) if start_at >= page.get("total", 0) or not page["issues"]: break ``` Caveats: `total` can change between pages, so always tolerate an empty page; there is no `isLast` field on this envelope; deep offsets re-scan everything before them. ### Enhanced: `GET|POST /rest/api/3/search/jql` — token paging The replacement, non-deprecated. Request body/params: `jql`, `nextPageToken`, `maxResults` (default 50, ceiling 5,000 — though real-world pages often cap near 100 even when more are requested, so follow the token instead of assuming page sizes), `fields` (**default is `id` only**, unlike every other endpoint), `expand`, `properties`, `fieldsByKeys`, `failFast`, `reconcileIssues`. Response envelope (`SearchAndReconcileResults`): ```json { "isLast": false, "issues": [{"id": "10002", "key": "ED-1"}], "nextPageToken": "CAEaAggB", "warnings": [] } ``` Key differences from legacy: | Aspect | Legacy `/search` | Enhanced `/search/jql` | |--------|------------------|------------------------| | Offset param | `startAt` | none — opaque `nextPageToken` | | Total count | `total` present | absent | | Last-page signal | none (compute from total) | `isLast` boolean; `nextPageToken` omitted on final page | | Default fields | all navigable | `id` only — pass explicit `fields` | | Warnings key | `warningMessages` | `warnings` | | JQL restriction | unbounded allowed | **bounded queries required** — bare `order by key desc` returns 400 | | `orderBy` cap | none | max 7 fields | | Consistency | immediate-ish | eventual; optional `reconcileIssues` (≤50 ids) for read-after-write | "Bounded" means at least one real condition: `assignee = currentUser() order by key` is bounded; `order by created DESC` alone is not. Loop shape: ```python body = {"jql": jql, "maxResults": 100, "fields": ["summary", "status"]} while True: page = post("/search/jql", json_data=body) yield from page["issues"] if page.get("isLast") or "nextPageToken" not in page: break body["nextPageToken"] = page["nextPageToken"] ``` Token continuation is sequential-only: you cannot fetch pages in parallel, and you must carry the exact previous token forward. ### Which model fails how — symptoms of mixing them up - Passing `startAt` to `/search/jql`: parameter ignored/rejected; you silently loop over page one forever if your loop advances the offset instead of the token. - Reading `total` off `/search/jql`: `KeyError` — the field does not exist; use `/search/approximate-count` first if you need a count. - Expecting populated `fields` from `/search/jql` without asking: you get `id`/`key` only. - Sending an unbounded query to `/search/jql`: immediate `400`. - Calling legacy `/search` after removal completes: connection-level failure/404-class errors; before that, responses still work but the endpoint is formally dead-ended. ### Approximate counts Need "how many?" without fetching? `POST /rest/api/3/search/approximate-count` with body `{"jql": "project = HSP"}` returns `{"count": 153}`. Works regardless of which search endpoint you use for rows; approximate because it skips permission filtering per row. ## Endpoint Cheat Sheet | Operation | Call | |-----------|------| | Current user | `GET /rest/api/3/myself` → `{accountId, displayName, emailAddress?, timeZone}` | | Search (legacy) | `GET/POST /rest/api/3/search` — deprecated, offset paging | | Search (current) | `GET/POST /rest/api/3/search/jql` — token paging | | Count matches | `POST /rest/api/3/search/approximate-count` | | Get issue | `GET /rest/api/3/issue/{key}?fields=summary,status,...` | | Create issue | `POST /rest/api/3/issue` — `{fields: {...}}` | | Edit issue | `PUT /rest/api/3/issue/{key}` — fields at top level, `notifyUsers=false` to silence mail | | Delete issue | `DELETE /rest/api/3/issue/{key}?deleteSubtasks=true` | | List transitions | `GET /rest/api/3/issue/{key}/transitions` | | Apply transition | `POST /rest/api/3/issue/{key}/transitions` — `{"transition": {"id": "..."}}` | | Add comment | `POST /rest/api/3/issue/{key}/comment` — ADF body | | Projects | `GET /rest/api/3/project/search` (paginated; plain `/project` is a deprecated bare array) | ## Sources - REST API v3 intro (auth modes, error collection): https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/ - Issue search group (both endpoints, approximate-count): https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/ - Basic auth for REST APIs: https://developer.atlassian.com/cloud/jira/platform/basic-auth-for-rest-apis/ - Deprecation changelog entry CHANGE-2046: https://developer.atlassian.com/changelog/#CHANGE-2046 - Rate limiting: https://developer.atlassian.com/cloud/jira/platform/rate-limiting/ - Manage API tokens: https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/ - OAuth 2.0 (3LO) apps: https://developer.atlassian.com/cloud/jira/platform/oauth-2-3lo-apps/ - PATs (Data Center/Server only): https://confluence.atlassian.com/enterprise/using-personal-access-tokens-1026032365.html -
rest-issues-and-transitions.md 9.5 KB
# Jira Cloud REST API v3 — Issues, Transitions & ADF Field-level semantics for the issue lifecycle: reading, creating, editing, commenting, and — the part everyone gets wrong — transitioning. Plus the Atlassian Document Format rules that decide whether your payload is accepted at all. ## Reading Issues `GET /rest/api/3/issue/{issueIdOrKey}` - Query params: `fields`, `fieldsByKeys`, `expand`, `properties`, `updateHistory`, `failFast`. - Default response includes all navigable fields. Trim with `?fields=summary,status,assignee` — faster pages, less noise. - Non-matching keys get a case-insensitive lookup plus moved-issue check; a found match returns directly (no redirect). - Response top level: `{id, key, self, fields:{...}}`. Field access patterns: ```json { "fields": { "summary": "Main order flow broken", "status": {"name": "In Progress", "statusCategory": {"key": "in-flight"}}, "issuetype": {"name": "Bug"}, "priority": {"name": "High"}, "assignee": {"accountId": "5b10a2844c20165700ede21g", "displayName": "Mia Krystof"}, "reporter": {"accountId": "...", "displayName": "..."}, "created": "2019-04-05T10:30:00.000+1000", "updated": "2024-01-11T08:15:00.000+0000", "description": {"type": "doc", "version": 1, "content": []} } } ``` Gotchas: - `assignee`/`reporter` may be `null` (unassigned) — null-check before `.displayName`. - `description` is an ADF object, not text (see ADF section). - User identity is `accountId` everywhere; `username`/`userKey` were removed in the GDPR migration (April 2019). Email visibility depends on each user's privacy settings. ## Creating Issues `POST /rest/api/3/issue` with body root keys `fields`, `update`, `historyMetadata`, `properties`, `transition`. Only `fields` matters for basic creation. ```json { "fields": { "project": { "key": "EX" }, "summary": "Order entry fails when selecting supplier.", "issuetype": { "name": "Bug" }, "description": { "type": "doc", "version": 1, "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "Steps to reproduce..." } ] } ] }, "priority": { "name": "High" }, "labels": ["bugfix"], "parent": { "key": "PROJ-123" } } } ``` Rules: - Project by `{"key": ...}` or `{"id": ...}`; issuetype by name or id. - `description`, `environment`, and any `textarea`-type custom fields **require ADF objects**; plain strings are rejected. Single-line `textfield` custom fields take plain strings. - Users are addressed as `assignee: {"accountId": "..."}` or `{"id": "<accountId>"}`. - Success: 201 with `{id, key, self}` (+ optional transition echo). - Failure: 400/422 with the error collection; `errors` map names the offending field ("Project 'XYZ' does not exist or you do not have permission..."). ## Editing Issues `PUT /rest/api/3/issue/{issueIdOrKey}` ```json { "fields": { "summary": "Completed orders still displaying in pending", "labels": ["bugfix", "triage"] } } ``` - Fields sit under `fields` (or granular ops under `update`, e.g. array field manipulation). - Success is **204 No Content** — an empty response body means it worked; don't parse for confirmation JSON. - Transitions are ignored on this endpoint; changing status requires the transitions endpoint below. - Suppress notification emails with query param `notifyUsers=false` (bulk edits especially). Deleting: `DELETE /rest/api/3/issue/{key}` refuses when subtasks exist unless you pass `deleteSubtasks=true`; success is 204. ## Comments `GET /rest/api/3/issue/{key}/comment?startAt=0&maxResults=50` → `{comments: [...], startAt, maxResults, total}` (offset paging, like legacy search). Each comment: `{id, author:{accountId, displayName}, body: <ADF>, created, updated, updateAuthor}`. Add one: `POST /rest/api/3/issue/{key}/comment` with `{"body": {ADF doc}}`. The body must be an ADF object — a bare string fails with a 400/500-class error naming the wrong type. ## Transitions: GET First, Then POST This is the highest-friction endpoint pair in Jira integration work. Two calls are always required because **transition IDs differ per workflow, per project, and per current status**, and names alone are ambiguous across workflows. ### Step 1 — discover available transitions `GET /rest/api/3/issue/{key}/transitions?expand=transitions.fields` ```json { "transitions": [ { "id": "31", "name": "Done", "hasScreen": true, "isGlobal": false, "isConditional": false, "to": { "name": "Done", "statusCategory": {"key": "completed"} }, "fields": { "resolution": { "required": true, "allowedValues": [{"name": "Done"}, {"name": "Fixed"}] }, "comment": { "required": false } } } ] } ``` Reading this shape: - `id` is the string you POST back. Never hardcode it across projects. - `to.statusCategory.key` (`to-do` / `in-flight` / `completed`) is the stable way to find "the Done-ish transition" without knowing its display name. - With `expand=transitions.fields`, `fields` lists what the target screen demands and each field's `required` flag plus `allowedValues`. - Asking for a nonexistent or status-invalid transition yields an **empty list**, not an error. ### Step 2 — apply the transition `POST /rest/api/3/issue/{key}/transitions` Minimal payload: ```json { "transition": { "id": "31" } } ``` Setting fields during the move (resolution, assignee, comments ride along): ```json { "transition": { "id": "31" }, "fields": { "resolution": { "name": "Fixed" } }, "update": { "comment": [ { "add": { "body": { "type": "doc", "version": 1, "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "Shipped in build 47" } ] } ] } } } ] } } ``` Semantics that bite: - Success is **204 No Content**. - If the target screen marks a field `required: true` and you omit it — classically `resolution` on a Done transition — you get **400** with `"errors": {"resolution": "..."}` naming the missing field. Pre-read the fields map from step 1 instead of guessing. - `resolution` values come from `allowedValues`; sending `{"name": "Done"}` where the site expects `"Fixed"` fails validation. - Transition names repeat across workflows; IDs do not. Resolve by ID after discovery, optionally filtering by `to.statusCategory.key`. ### Worked recipe: bulk-close stalled sprint issues ```python # 1) find candidates (legacy search shown; see rest-auth-and-search.md for token paging) issues = search('sprint in openSprints() AND updated < -14d AND resolution = Unresolved') # 2) per issue: discover + apply for issue in issues: trans = get(f"/issue/{issue['key']}/transitions")["transitions"] done = next(t for t in trans if t["to"]["statusCategory"]["key"] == "completed") post(f"/issue/{issue['key']}/transitions", json={"transition": {"id": done["id"]}, "fields": {"resolution": {"name": "Done"}}}) ``` Rate-limit note: writes count toward per-issue windows (20 per 2s) — sleep briefly between issues in loops. ## Atlassian Document Format (ADF) The document model for every rich-text field in v3 payloads: issue `description`/`environment`, comment bodies, textarea custom fields. Plain-text strings are rejected for these fields. ### Minimal document ```json { "type": "doc", "version": 1, "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "Hello world" } ] } ] } ``` Structure invariants: exactly one root `doc` with `version: 1`; content is an ordered tree of block nodes (`paragraph`, `heading`, `bulletList`/`orderedList` > `listItem` > `paragraph`, `codeBlock`, `panel`, `table`, `blockquote`); inline content is `text` nodes carrying optional `marks`. ### Formatting quick reference | Effect | Node/mark shape | |--------|-----------------| | Bold | `{"type":"text","text":"world","marks":[{"type":"strong"}]}` | | Italic | `marks: [{"type":"em"}]` | | Code | `marks: [{"type":"code"}]` | | Link | `marks: [{"type":"link","attrs":{"href":"https://..."}}]` | | Bullet list | `bulletList` node whose `listItem`s contain paragraphs | | Mention | inline node `{"type":"mention","attrs":{"id":"<accountId>"}}` | ### Practical guidance - Building from user input? Wrap each line/paragraph as its own `paragraph` node; escape nothing manually — text goes in the `text` property verbatim. - Extracting? Walk `content[]` recursively collecting `text` nodes' `text` values joined by newlines (the bundled CLI's `view` does this for descriptions). - Round-tripping rich content through plain text loses formatting permanently; if fidelity matters, fetch the ADF and re-post the same structure. ## Sources - Issues group (get/create/edit/delete, transitions GET+POST): https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/ - Issue comments group: https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/ - Issue links group (link payload shapes): https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-links/ - Projects group (paginated vs deprecated bare-array): https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-projects/ - Myself resource: https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-myself/ - REST v3 intro (error collection schema): https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/ - ADF structure reference: https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/ - GDPR accountId migration guide: https://developer.atlassian.com/cloud/jira/platform/deprecation-notice-user-privacy-api-migration-guide/
-
-
scripts
-
jira 26 KB · in bundle
-
test_jira.py 14.2 KB
"""Offline tests for the bundled jira CLI (scripts/jira). Four test classes per skill-builder contract: 1. --help output 2. argument-error paths 3. --dry-run behavior 4. mocked-client logic (requests mocked at the client-call site) Zero network calls in every test; the proxy-trap rerun proves egress-freedom. """ import contextlib import importlib.machinery import importlib.util import io import json import pathlib import unittest from unittest import mock import requests SCRIPT = pathlib.Path(__file__).resolve().parent / "jira" LOADER = importlib.machinery.SourceFileLoader("jira_cli", str(SCRIPT)) SPEC = importlib.util.spec_from_loader(LOADER.name, LOADER) jira_cli = importlib.util.module_from_spec(SPEC) LOADER.exec_module(jira_cli) def run_cli(*argv, env_email="ops@example.com", env_token="test-token-123", clear_env=False): """Invoke main() with argv[0] prepended; returns (exit_code, stdout, stderr). clear_env=True strips JIRA_EMAIL/JIRA_API_TOKEN to exercise lazy-auth paths. """ out, err = io.StringIO(), io.StringIO() code = 0 if clear_env: env = {} else: env = {"JIRA_EMAIL": env_email or "", "JIRA_API_TOKEN": env_token or ""} with mock.patch.dict("os.environ", env, clear=True): with mock.patch.object(jira_cli.sys, "argv", ["jira", *argv]): with mock.patch.object(jira_cli.sys, "stdout", out), \ mock.patch.object(jira_cli.sys, "stderr", err), \ contextlib.redirect_stdout(out), contextlib.redirect_stderr(err): try: jira_cli.main() except SystemExit as exc: code = exc.code if isinstance(exc.code, int) else 0 return code, out.getvalue(), err.getvalue() class FakeResponse: def __init__(self, status_code=200, payload=None, text="", headers=None): self.status_code = status_code self._payload = payload self.text = text or (json.dumps(payload) if payload is not None else "") self.headers = headers or {} def json(self): if self._payload is None: raise ValueError("no json") return self._payload class SearchPageFactory: """Builds legacy-offset search pages for pagination tests.""" def __init__(self, total, page_size=2, key_prefix="PROJ"): self.total = total self.page_size = page_size self.key_prefix = key_prefix def issue(self, n): return {"key": f"{self.key_prefix}-{n}", "fields": {"summary": f"issue {n}", "status": {"name": "Open"}, "issuetype": {"name": "Task"}, "assignee": None, "priority": {"name": "Medium"}}} def page(self, start_at): issues = [self.issue(n) for n in range( start_at + 1, min(start_at + self.page_size, self.total) + 1)] return {"issues": issues, "startAt": start_at, "maxResults": self.page_size, "total": self.total} # === Class 1: help output === class HelpOutputTests(unittest.TestCase): def test_help_lists_all_subcommands(self): code, out, _ = run_cli("--help") self.assertEqual(code, 0) for noun in ("me", "list", "view", "projects", "create", "comment", "transition", "transitions", "count"): self.assertIn(noun, out) def test_subcommand_help_mentions_flags(self): _, out, _ = run_cli("list", "--help") for flag in ("--jql", "--max", "--project"): self.assertIn(flag, out) # === Class 2: argument errors === class ArgumentErrorTests(unittest.TestCase): def test_missing_required_jql_on_count(self): code, _, err = run_cli("count") self.assertEqual(code, 2) self.assertIn("--jql", err) def test_transition_requires_to_flag(self): code, _, err = run_cli("transition", "PROJ-1") self.assertEqual(code, 2) self.assertIn("--to", err) def test_no_command_prints_help_and_exits(self): code, out, _ = run_cli() self.assertEqual(code, 1) self.assertIn("usage:", out) def test_unknown_subcommand_fails(self): code, _, err = run_cli("frobnicate") self.assertEqual(code, 2) self.assertIn("invalid choice", err) # === Class 3: dry-run behavior === class DryRunTests(unittest.TestCase): def test_dry_run_count_emits_plan_json_without_credentials(self): # No JIRA_EMAIL/JIRA_API_TOKEN set at all — lazy auth must not fire. code, out, _ = run_cli("--json", "--dry-run", "count", "--jql", "status=Open", clear_env=True) self.assertEqual(code, 0) plan = json.loads(out) self.assertTrue(plan["dry_run"]) self.assertEqual(plan["command"], "count") self.assertIn("status=Open", plan["jql"]) def test_dry_run_create_reports_payload_shape(self): code, out, _ = run_cli("--json", "--dry-run", "create", "--project", "PROJ", "--summary", "Test issue") self.assertEqual(code, 0) plan = json.loads(out) self.assertEqual(plan["command"], "create") self.assertEqual(plan["project"], "PROJ") def test_dry_run_never_touches_network(self): with mock.patch.object(requests, "request") as req: code, out, _ = run_cli("--dry-run", "list", "--project", "PROJ") self.assertEqual(code, 0) req.assert_not_called() class MutationGateTests(unittest.TestCase): def setUp(self): self._env = (jira_cli.ENV_EMAIL, jira_cli.ENV_TOKEN) jira_cli.ENV_EMAIL = "ops@example.com" jira_cli.ENV_TOKEN = "test-token-123" def tearDown(self): jira_cli.ENV_EMAIL, jira_cli.ENV_TOKEN = self._env def test_create_requires_explicit_yes_or_force(self): code, _, err = run_cli("create", "--project", "PROJ", "--summary", "Test") self.assertEqual(code, 1) self.assertIn("--yes", err) def test_yes_alias_allows_mutation(self): with mock.patch.object(requests, "request", return_value=FakeResponse( 201, {"key": "PROJ-1", "self": "https://jira.example/issue/PROJ-1"})): code, out, _ = run_cli("--yes", "create", "--project", "PROJ", "--summary", "Test", "--json") self.assertEqual(code, 0) self.assertEqual(json.loads(out)["status"], "created") # === Class 4: mocked client logic === class MockedClientTests(unittest.TestCase): def setUp(self): self.flags = dict(jira_cli.GLOBAL_FLAGS) jira_cli.GLOBAL_FLAGS.update(json=True, dry_run=False, quiet=False) # Module-level ENV_* constants are captured at import; pin them here so # run_cli's env dict is the single source of auth truth in tests. self._env = (jira_cli.ENV_EMAIL, jira_cli.ENV_TOKEN) jira_cli.ENV_EMAIL = "ops@example.com" jira_cli.ENV_TOKEN = "test-token-123" def tearDown(self): jira_cli.ENV_EMAIL, jira_cli.ENV_TOKEN = self._env jira_cli.GLOBAL_FLAGS.clear() jira_cli.GLOBAL_FLAGS.update(self.flags) def test_list_single_page_parses_issue_rows(self): page = SearchPageFactory(total=2, page_size=50) resp = FakeResponse(200, page.page(0)) with mock.patch.object(requests, "request", return_value=resp) as req: code, out, _ = run_cli("list", "--project", "PROJ", "--json") self.assertEqual(code, 0) req.assert_called_once() data = json.loads(out) self.assertEqual(data["total"], 2) self.assertEqual([i["key"] for i in data["issues"]], ["PROJ-1", "PROJ-2"]) self.assertEqual(data["issues"][0]["assignee"], "Unassigned") def test_list_multipage_follows_offset_pagination(self): factory = SearchPageFactory(total=5, page_size=2) responses = [FakeResponse(200, factory.page(s)) for s in (0, 2, 4)] captured = [] def fake_request(method, url, **kwargs): captured.append(kwargs["params"]["startAt"]) return responses[len(captured) - 1] with mock.patch.object(requests, "request", side_effect=fake_request): code, out, _ = run_cli("list", "--project", "PROJ", "--max", "100", "--json") self.assertEqual(code, 0) self.assertEqual(captured, [0, 2, 4]) data = json.loads(out) self.assertEqual(data["total"], 5) def test_list_multipage_stops_on_empty_page_when_total_shrinks(self): # Page 1 full, then `total` drops below startAt -> empty page terminates. factory = SearchPageFactory(total=4, page_size=2) first = factory.page(0) second = {"issues": [], "startAt": 2, "maxResults": 2, "total": 2} responses = [FakeResponse(200, first), FakeResponse(200, second)] def fake_request(method, url, **kwargs): return responses.pop(0) with mock.patch.object(requests, "request", side_effect=fake_request): code, out, _ = run_cli("list", "--max", "60", "--json") self.assertEqual(code, 0) data = json.loads(out) self.assertEqual(len(data["issues"]), 2) def test_error_envelope_is_parsed_into_message(self): resp = FakeResponse(400, {"errorMessages": [], "errors": {"resolution": "Resolution is required"}}) with mock.patch.object(requests, "request", return_value=resp): code, _, err = run_cli("--force", "comment", "PROJ-1", "-m", "hi") self.assertEqual(code, 1) self.assertIn("resolution: Resolution is required", err) def test_rate_limit_surfaces_retry_after(self): resp = FakeResponse(429, {"errorMessages": []}, headers={"Retry-After": "42", "RateLimit-Reason": "jira-burst-based"}) with mock.patch.object(requests, "request", return_value=resp): code, _, err = run_cli("me") self.assertEqual(code, 1) self.assertIn("429", err) self.assertIn("42", err) self.assertIn("jira-burst-based", err) def test_auth_failure_names_env_vars(self): resp = FakeResponse(401, {"errorMessages": ["Unauthorized"]}) with mock.patch.object(requests, "request", return_value=resp): code, _, err = run_cli("me") self.assertEqual(code, 1) self.assertIn("JIRA_EMAIL", err) self.assertIn("401", err) def test_transitions_listing_formats_rows(self): payload = {"transitions": [ {"id": "31", "name": "Done", "to": {"name": "Done", "statusCategory": {"key": "completed"}}}, {"id": "11", "name": "Start Progress", "to": {"name": "In Progress", "statusCategory": {"key": "in-flight"}}}, ]} resp = FakeResponse(200, payload) with mock.patch.object(requests, "request", return_value=resp): code, out, _ = run_cli("--force", "transitions", "PROJ-1", "--json") self.assertEqual(code, 0) data = json.loads(out) self.assertEqual([t["id"] for t in data["transitions"]], ["31", "11"]) def test_transition_resolves_name_to_id_and_posts_resolution(self): listing = FakeResponse(200, {"transitions": [ {"id": "31", "name": "Done", "to": {"name": "Done", "statusCategory": {"key": "completed"}}}]}) posted = [] def fake_request(method, url, **kwargs): if method == "GET": return listing posted.append((url, kwargs.get("json"))) return FakeResponse(204, None, text="") with mock.patch.object(requests, "request", side_effect=fake_request): code, out, _ = run_cli("--force", "transition", "PROJ-1", "--to", "Done", "--resolution", "Fixed", "--json") self.assertEqual(code, 0) self.assertEqual(len(posted), 1) url, body = posted[0] self.assertIn("/issue/PROJ-1/transitions", url) self.assertEqual(body["transition"], {"id": "31"}) self.assertEqual(body["fields"]["resolution"], {"name": "Fixed"}) def test_transition_unknown_name_lists_available_options(self): listing = FakeResponse(200, {"transitions": [ {"id": "11", "name": "Start Progress", "to": {"name": "In Progress", "statusCategory": {"key": "in-flight"}}}]}) with mock.patch.object(requests, "request", return_value=listing): code, _, err = run_cli("--force", "transition", "PROJ-1", "--to", "Done") self.assertEqual(code, 1) self.assertIn("Available: 11=Start Progress", err) def test_count_uses_approximate_count_endpoint(self): resp = FakeResponse(200, {"count": 153}) with mock.patch.object(requests, "request", return_value=resp) as req: code, out, _ = run_cli("count", "--jql", "status=Open", "--json") self.assertEqual(code, 0) url = req.call_args.kwargs["url"] self.assertIn("/search/approximate-count", url) self.assertEqual(req.call_args.kwargs["json"], {"jql": "status=Open"}) self.assertEqual(json.loads(out)["count"], 153) def test_view_extracts_plain_text_from_adf_description(self): adf = {"type": "doc", "version": 1, "content": [ {"type": "paragraph", "content": [ {"type": "text", "text": "First paragraph"}]}, {"type": "paragraph", "content": [ {"type": "text", "text": "Second paragraph"}]}, ]} payload = {"key": "PROJ-9", "fields": { "summary": "Broken flow", "status": {"name": "Open"}, "issuetype": {"name": "Bug"}, "assignee": None, "description": adf}} with mock.patch.object(requests, "request", return_value=FakeResponse(200, payload)): code, out, _ = run_cli("view", "PROJ-9", "--json") self.assertEqual(code, 0) data = json.loads(out) self.assertIn("First paragraph\nSecond paragraph", data["description"]) self.assertEqual(data["assignee"], "Unassigned") if __name__ == "__main__": unittest.main()
-
-
README.md 3.2 KB
# Jira Issue Tracker from the Terminal Interact with Atlassian Jira Cloud via the REST API v3: search issues with JQL, view details, create issues, add comments, count matches, list projects, and transition status — plus a full JQL language reference built in. ## Why Install This Skill When your agent loads this skill, it can **run your entire Jira workflow** without opening a browser: - **Search anything** — by project, assignee, or arbitrary JQL; results over 50 auto-paginate - **Count before diving in** — fast approximate counts instead of fetching every ticket - **Create, comment, edit** — with Atlassian Document Format handled for you - **Transition safely** — discovers valid workflow transitions per issue before changing status, and can set resolutions in the same call - **Write better queries** — a 50-query cookbook by role, complete function catalog, performance rules, and history-operator/date-expression deep dives The skill also knows where the bodies are buried: the legacy-vs-enhanced search endpoint split (offset paging vs `nextPageToken`), transition screens that silently require resolution fields, the `!=` empty-value trap, and rate-limit headers worth honoring. ## What You Get | Path | Purpose | |------|---------| | `SKILL.md` | Command reference: setup, intent-grouped commands, pipeline recipes, jq guidance, known gotchas | | `scripts/jira` | CLI tool for Jira REST API v3 (`--json`, `--dry-run`, lazy auth) | | `scripts/test_jira.py` | Offline test suite for the CLI (help/errors/dry-run/mocked client logic) | | `references/rest-auth-and-search.md` | Auth models, rate limits, error envelopes, search pagination duality | | `references/rest-issues-and-transitions.md` | Issue CRUD shapes, transitions GET→POST flow, ADF document model | | `references/jql-functions-catalog.md` | Every JQL function with fields and operators, incl. JSM approvals & SLAs | | `references/jql-best-practices.md` | Performance rules, precedence, empty-value trap, troubleshooting flows | | `references/jql-cookbook.md` | 50 ready-to-run JQL queries organized by role | | `references/jql-history-and-dates.md` | WAS/CHANGED walkthrough, relative-date tables, saved-filter naming | | `evals/evals.json` | Behavioral eval cases covering read-only use, pipelines, gotchas | ## Quick Start ```bash export JIRA_EMAIL="you@company.com" export JIRA_API_TOKEN="YOUR_API_TOKEN" # free from https://id.atlassian.com/manage/api-tokens export JIRA_SERVER="https://your-domain.atlassian.net" jira me # verify auth works jira list --project PROJ # newest tickets jira count --jql 'issuetype = Bug AND resolution = Unresolved' jira create --project PROJ --summary "Test" --dry-run # preview writes jira --yes create --project PROJ --summary "Test" # authorize a write ``` ## Triggers Load this when managing Jira issues, searching or counting tickets, creating bugs, transitioning sprint work, writing/debugging/optimizing JQL, or designing saved filters and dashboards on an Atlassian Jira Cloud site. ## Requirements - Python 3.8+ with the `requests` library - A free Atlassian account + API token (`JIRA_EMAIL`, `JIRA_API_TOKEN`; optional `JIRA_SERVER`) - `jq` recommended for processing `--json` output -
SKILL.md 12.9 KB
--- name: jira description: 'Interact with Atlassian Jira from the terminal: search issues with JQL, view details, create issues, add comments, count matches with fast approximate-count, list projects, discover valid transitions, and change status. Includes a full JQL language reference (functions, operators, history predicates, date expressions, saved filters, performance tuning) plus REST auth/pagination guidance. Use when the user mentions Jira, a ticket key (e.g. PROJ-123), asks about issues, bugs, tasks, projects, or sprint work, or needs to write, debug, or optimize JQL queries. Do not use for GitHub or GitLab issue tracking, Jira site administration, or generic ticketing systems.' license: MIT compatibility: Requires JIRA_EMAIL and JIRA_API_TOKEN env vars (free API token from id.atlassian.com/manage/api-tokens), Python 3.8+, and the `requests` library. JIRA_SERVER defaults to your-domain.atlassian.net format. metadata: tags: jira, atlassian, issue-tracking, project-management, api-client sources: https://developer.atlassian.com/cloud/jira/platform/rest/v3/, https://id.atlassian.com/manage/api-tokens --- # jira — Jira Issue Tracker from the Terminal Interact with Atlassian Jira Cloud via the REST API v3. Search issues, view details, create issues, add comments, count matches, list projects, and transition status. ## Setup 1. Generate an API token at [id.atlassian.com/manage/api-tokens](https://id.atlassian.com/manage/api-tokens) 2. Set environment variables: ```bash export JIRA_EMAIL="your-email@example.com" # Atlassian account email export JIRA_API_TOKEN="YOUR_API_TOKEN" # from id.atlassian.com export JIRA_SERVER="https://your-domain.atlassian.net" ``` Auth is HTTP Basic over `base64(email:token)` — your **email address**, never a password (passwords are deprecated for API use). Cloud has no Personal Access Tokens; Bearer PATs are Data Center only. Tokens now expire after at most one year. `--help` and `--dry-run` work without credentials. ## Essential Commands ### me / projects — identity and scope ```bash jira me # verify auth; your accountId, timezone jira projects --json # all accessible projects ``` ### list — search issues ```bash jira list # recent issues jira list --project PROJ # by project jira list --jql 'assignee=currentuser() AND status=Open' # custom JQL jira list --project PROJ --max 120 --json # >50 auto-pages via startAt offsets ``` ### view — issue details ```bash jira view PROJ-123 # summary, status, assignee, description jira view PROJ-123 --json # machine-readable ``` Descriptions arrive as Atlassian Document Format (ADF); the CLI extracts plain text for display. ### count — fast match total ```bash jira count --jql 'issuetype = Bug AND resolution = Unresolved' # {"count": N} ``` Uses `POST /search/approximate-count` — no fetching rows. JQL itself has no COUNT/aggregation. ### create — new issues ```bash jira --yes create --project PROJ --summary "Fix login bug" # Task (default) jira --yes create --project PROJ --summary "Crash on startup" --type Bug jira --yes create --project PROJ --summary "Add dark mode" --type Story --priority High jira create --project PROJ --summary "Test" --dry-run # preview payload ``` Descriptions are sent as ADF documents. Rich formatting beyond plain paragraphs needs raw ADF JSON — see references/rest-issues-and-transitions.md. ### comment — add to threads ```bash jira --yes comment PROJ-123 -m "Fixed in latest build" jira comment PROJ-123 -m "Looking into it" --dry-run ``` ### transitions + transition — status changes ```bash jira transitions PROJ-123 # LIST valid transition IDs first jira --yes transition PROJ-123 --to "In Progress" # then apply by name or ID jira --yes transition PROJ-123 --to Done --resolution Done jira transition PROJ-123 --to "In Review" --dry-run ``` Always run `transitions` first when unsure: IDs differ per workflow and current status, and names repeat across workflows. `--resolution` satisfies Done-style screens that require one; omitting it yields `400` with an error naming the missing field. ## Global Flags All flags work in any position. Read commands need credentials; `--help` and `--dry-run` do not. Mutating commands require an explicit `--yes`/`--force` gate: ```bash jira --json list --project PROJ # machine output anywhere jira --dry-run create --project PROJ --summary "Test" # offline preview jira --yes create --project PROJ --summary "Test" # explicit write authorization jira --quiet list # suppress non-essential output ``` `--json` emits one JSON object per command on stdout — pipe to jq for structure. ## Multi-Step Pipeline Recipes ### Sprint hygiene sweep Find stalled sprint work, review each ticket, close what's finished: ```bash jira list --jql 'sprint IN openSprints() AND updated < -14d AND resolution = Unresolved' --json \ | jq -r '.issues[].key' \ | while read -r key; do jira view "$key"; jira transitions "$key"; done # after human review, per key: jira --yes transition "$key" --to Done --resolution Done ``` The `list --json` shape is `{"total": N, "issues": [{"key", "summary", "status", "assignee", "issuetype", "priority"}]}`. ### Bulk-close with safe discovery Transition IDs are workflow-specific — resolve before writing: ```bash for key in $(jira list --jql 'status = "In Progress" AND updated < -30d' --json | jq -r '.issues[].key'); do tid=$(jira transitions "$key" --json | jq -r '.transitions[] | select(.status_category=="completed") | .id' | head -1) [ -n "$tid" ] && jira --yes transition "$key" --to "$tid" --resolution Done done ``` ### Weekly digest via jq ```bash jira list --jql 'assignee = currentUser() AND updated >= startOfWeek()' --max 50 --json \ | jq -r '.issues[] | "\(.key)\t\(.status)\t\(.summary)"' ``` More ready-to-run queries live in [references/jql-cookbook.md](references/jql-cookbook.md), organized by role. ## Using --json with jq ```bash jira list --project PROJ --json | jq '.issues[] | {key, status, assignee}' jira count --jql 'project = PROJ' --json | jq .count jira transitions PROJ-123 --json | jq -r '.transitions[] | "\(.id)=\(.name) -> \(.to_status)"' ``` ## Known Gotchas - **Search endpoint duality** — this CLI uses the classic `/rest/api/3/search` with offset pagination (`startAt`, `maxResults`, `total`). Atlassian's enhanced `/rest/api/3/search/jql` replaces it with an opaque `nextPageToken` (+ `isLast`), no `startAt`, no `total`, ids-only default fields, and it rejects unbounded JQL (`order by key desc` alone → 400). The classic endpoint is deprecated ("currently being removed", announced Oct 2024, removal promised after May 1 2025), so expect forced migration; mixing the two pagination models is the classic source of infinite-page-one loops. - **Pagination caps** — legacy pages default to `maxResults=50`; `total` can shrink between pages, so always tolerate empty pages instead of trusting a stale total. - **Transitions need GET first** — transition IDs (`"31"`, `"711"`) belong to one workflow/status; asking for an invalid one returns an *empty list*, not an error. Done-style screens frequently require `resolution`; missing required fields come back as `400` with `"errors": {"resolution": "..."}` naming them. - **ADF everywhere** — descriptions, comments, and environment fields take ADF JSON objects in v3 payloads; bare strings are rejected. - **Authentication** uses HTTP Basic with email + API token. CAPTCHA lockouts (repeated bad logins) block REST auth entirely; symptom header: `X-Seraph-LoginReason: AUTHENTICATION_DENIED`. Fix in the browser, not by retrying. - **Rate limits** return 429 with `Retry-After` and `RateLimit-Reason` headers; the CLI surfaces both but does not auto-retry. Writes also cap at ~20/2s per issue. - **Project keys are case-sensitive** in some contexts, though the API generally accepts either case. - **accountId, not username** — user fields accept Atlassian account IDs (GDPR migration); usernames were removed from the API. ### JQL gotchas - **`!=` excludes empty values** — `assignee != currentUser()` silently drops unassigned issues. Write `(assignee != currentUser() OR assignee IS EMPTY)`. - **AND binds tighter than OR** — `A OR B AND C` parses as `A OR (B AND C)`. Always parenthesize OR groups; without parentheses evaluation is left-to-right. - **No leading wildcards** — `summary ~ "*bug"` forces a full scan; put wildcards after the first characters. - **Filter by project first** — the biggest performance lever on large instances (official optimization guidance). - **History operators have a field whitelist** — `WAS`/`CHANGED` work only on Assignee, Fix Version, Priority, Reporter, Resolution, Status, and silently return nothing on fields without history tracking. - **Relative dates are case-sensitive** — `-1m` is minutes, `-1M` is months; day-grain expressions evaluate in each user's timezone. - **JQL has no aggregation** — no COUNT/SUM; use `jira count` (approximate-count endpoint) or dashboard gadgets. ## When to use - Any Jira Cloud interaction from the terminal: search, view, create, comment, transition - Writing, debugging, or optimizing JQL queries — full language reference included - Sprint reviews, triage sweeps, bulk status hygiene, dashboards and saved-filter design ## When not to use Do not use this skill for GitHub or GitLab issue tracking (use those platforms' own tooling such as `gh`), for Jira site administration like permission schemes or workflow editing (admin UI territory), for Confluence content, or for building server-side integrations against the Jira API (use official Atlassian SDK docs instead). ## Reference Files | File | Topic | Read when | |------|-------|-----------| | [references/rest-auth-and-search.md](references/rest-auth-and-search.md) | Basic-auth/token mechanics vs OAuth/PATs, rate-limit headers, error envelopes, legacy-vs-enhanced search pagination duality | Setting up credentials, handling 429/401s, paginating large searches, or migrating off `/search` | | [references/rest-issues-and-transitions.md](references/rest-issues-and-transitions.md) | GET/POST/PUT issue shapes, transitions GET→POST flow with screen-field requirements, ADF document model | Creating/editing issues programmatically, resolving transition failures, formatting rich text | | [references/jql-functions-catalog.md](references/jql-functions-catalog.md) | Every JQL function with supported fields/operators — date/time, user, sprint/version, custom field, JSM approvals & SLAs | Checking which operators/functions a query can use | | [references/jql-best-practices.md](references/jql-best-practices.md) | Operator precedence, performance rules, the empty-value trap, troubleshooting flows, marketplace extensions | A query is slow, wrong, or mixes AND/OR | | [references/jql-cookbook.md](references/jql-cookbook.md) | 50 ready-to-run queries organized by role (developers, scrum masters, POs/managers, power users, admins) | Building filters, automation rules, sprint reviews | | [references/jql-history-and-dates.md](references/jql-history-and-dates.md) | WAS/CHANGED predicate walkthrough, relative-date expression tables, saved-filter composition and naming conventions | History queries, date math, or designing reusable saved filters | ## Available Scripts | Script | Purpose | Invocation | |---|---|---| | `scripts/jira` | The CLI this skill drives: `me`, `list`, `view`, `projects`, `create`, `comment`, `count`, `transitions`, `transition` — all with `--json`/`--dry-run`, lazy auth, offset-pagination fetches above 50 results, parsed API error messages, and 429 Retry-After surfacing. Run it for every Jira data question above. | `scripts/jira list --project PROJ --json` | | `scripts/test_jira.py` | Offline pytest/unittest suite covering help text, argument errors, dry-run plans, pagination loops, error envelopes, and transition resolution logic — zero network. Run after modifying `scripts/jira`. | `.venv/bin/python3 -m pytest -p no:cacheprovider --strict-markers scripts/test_jira.py` | ## Prerequisites - Python 3.8+ with `requests` (stdlib otherwise); invoke as `python3 scripts/jira ...` if not executable directly - `JIRA_EMAIL` + `JIRA_API_TOKEN` exported for any non-dry-run command (token from https://id.atlassian.com/manage/api-tokens); `JIRA_SERVER` defaults to `https://your-domain.atlassian.net` - `jq` recommended for `--json` post-processing ## Limitations - Targets Jira **Cloud** REST v3; Data Center sites authenticate differently (Bearer PAT) and expose older API surfaces - The classic search endpoint this CLI uses is deprecated upstream; expect eventual forced migration to `/search/jql` token paging - Rich-text creation beyond plain paragraphs requires hand-built ADF JSON - No auto-retry on 429; loops over many writes should sleep between calls
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.