Jira Cloud bug workflow runner using the REST API v3: issue creation with an ADF description, runtime transition lookup and apply, JQL search for triage queues and duplicate detection, severity/priority field updates, label-based classification (severity/priority/regression), and idempotent CI-driven filing from JUnit XML test failures. Use when the target tracker is Jira Cloud and the task involves Jira lifecycle states (create, triage, transition, close). Distinct from a platform-agnostic event-driven CI defect filer, and from linear-bug-workflow-runner / github-issues-bug-workflow for other trackers.
79
99%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Jira's workflow engine maps cleanly to the canonical defect lifecycle
(bug-lifecycle-reference)
but every project's actual workflow is configurable, so the
runner has to look up transition IDs at runtime rather than
hard-code them.
This skill wraps the Jira Cloud REST API v3 (per developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/) for the four core operations: create, transition, update, and search.
bug-report-from-failure).Verified → Closed after deployment).Per Atlassian docs, Jira Cloud REST API v3 uses HTTP Basic auth with an API token:
export JIRA_BASE="https://your-tenant.atlassian.net"
export JIRA_EMAIL="you@company.com"
export JIRA_TOKEN="<api-token-from-id.atlassian.com>"
export JIRA_AUTH=$(echo -n "$JIRA_EMAIL:$JIRA_TOKEN" | base64)import requests, base64, os
auth = base64.b64encode(
f"{os.environ['JIRA_EMAIL']}:{os.environ['JIRA_TOKEN']}".encode()
).decode()
HEADERS = {
"Authorization": f"Basic {auth}",
"Accept": "application/json",
"Content-Type": "application/json",
}
BASE = os.environ["JIRA_BASE"]POST /rest/api/3/issue per the API group docs. The description
must be Atlassian Document Format (ADF), not plain text.
def create_bug(project_key, summary, description_text, severity, priority, labels):
payload = {
"fields": {
"project": {"key": project_key},
"summary": summary,
"description": {
"type": "doc",
"version": 1,
"content": [{
"type": "paragraph",
"content": [{"type": "text", "text": description_text}],
}],
},
"issuetype": {"name": "Bug"},
"priority": {"name": priority}, # e.g. "High"
"labels": labels + [f"severity-{severity}"],
}
}
r = requests.post(f"{BASE}/rest/api/3/issue", json=payload, headers=HEADERS)
r.raise_for_status()
return r.json()["key"]Note: severity is typically a custom field - most tenants
either define a custom Severity field (customfield_XXXXX) or
use labels (severity-critical). The example above uses labels
for portability; discovering and submitting the custom field is in
references/jira-rest-api.md.
Workflow transitions are project-specific. Look up the available transitions then apply by transition ID:
def get_transitions(issue_key):
r = requests.get(f"{BASE}/rest/api/3/issue/{issue_key}/transitions",
headers=HEADERS)
r.raise_for_status()
return r.json()["transitions"]
def transition(issue_key, target_state_name):
transitions = get_transitions(issue_key)
match = next((t for t in transitions if t["name"] == target_state_name), None)
if not match:
raise ValueError(f"No transition named {target_state_name}; "
f"available: {[t['name'] for t in transitions]}")
r = requests.post(
f"{BASE}/rest/api/3/issue/{issue_key}/transitions",
json={"transition": {"id": match["id"]}},
headers=HEADERS,
)
r.raise_for_status()The POST /rest/api/3/issue/{key}/transitions body shape is
{"transition": {"id": "<id>"}} per the API group docs.
POST /rest/api/3/search/jql returns issues matching a JQL query.
Useful for duplicate detection and triage queues.
def search_jql(jql, max_results=50):
r = requests.post(
f"{BASE}/rest/api/3/search/jql",
json={"jql": jql, "fields": ["summary", "status", "priority"],
"maxResults": max_results},
headers=HEADERS,
)
r.raise_for_status()
return r.json()["issues"]
# Triage queue:
triage = search_jql(
'project = ENG AND issuetype = Bug AND status = "New" ORDER BY created ASC'
)
# Duplicate-candidate search:
dupes = search_jql(
f'project = ENG AND text ~ "{summary_safe}" AND issuetype = Bug'
)CI bug filing must not duplicate if the same failure recurs. Pair with upstream duplicate detection, but also defensively search before creating:
def create_or_attach(project, summary, body):
existing = search_jql(
f'project = {project} AND summary ~ "\\"{summary}\\"" '
f'AND statusCategory != Done',
max_results=5,
)
if existing:
# Attach a comment to the existing bug instead of duplicating
key = existing[0]["key"]
add_comment(key, f"Recurred at {timestamp()}: {body[:500]}")
return key
return create_bug(project, summary, body, "Medium", "Medium",
labels=["auto-filed", "ci-failure"])Verify: the statusCategory != Done search must run and return 0 open matches
before create_bug fires. If it returns a hit, comment on that key instead of
creating; if the search itself errors, fail closed (skip the create and surface
the error) rather than filing a possible duplicate.
Dry-run first: a mis-scoped JQL can push hundreds of issues into the wrong
state, and a transition is not trivially reversible (bulk transitions without dry-run in Anti-patterns). Gate the apply behind a flag:
DRY_RUN = True # flip to False only after reviewing the logged plan
verified = search_jql(
'project = ENG AND status = Verified AND fixVersion = "2026.05.20"',
max_results=1000,
)
for issue in verified:
if DRY_RUN:
print(f"[dry-run] {issue['key']}: Verified -> Close Issue")
continue
transition(issue["key"], "Close Issue")Verify: assert the dry-run count and keys match the issue set you intended to
close before flipping DRY_RUN to False; if they do not, fix the JQL and
re-run the dry run. transition already raises ValueError when the named
transition is absent for an issue's workflow, so a workflow mismatch fails loud
rather than silently skipping.
Auto-file a bug from a test failure:
# .github/workflows/test.yml (excerpt)
- name: Run tests
id: tests
run: pytest --junitxml=results.xml
continue-on-error: true
- name: File Jira bug on failure
if: steps.tests.outcome == 'failure'
env:
JIRA_BASE: ${{ secrets.JIRA_BASE }}
JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }}
JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }}
run: python scripts/file-jira-bug.py results.xmlWhere file-jira-bug.py parses the JUnit XML, extracts the
failure, deduplicates, and creates / comments per the helpers
above.
Verify: assert the create call returned HTTP 2xx and a non-empty issue key
before the step reports success. On 400, the description was likely plain
text instead of ADF or a required field is missing - fix the payload and
re-run. On 429 (rate limit), back off and retry rather than failing the
build. If it still fails, leave the test result red so the filing gap stays
visible instead of being swallowed.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Hard-coding transition IDs | Workflow updates break the runner silently | Look up transitions per call via GET /transitions |
Plain-text description | API returns 400 - Jira v3 requires ADF | Wrap as {"type": "doc", "version": 1, "content": [...]} |
| No deduplication before create | Each retry of a flaky test creates a new bug | Search by summary first; comment on existing |
Severity as built-in priority | Conflates two axes (severity-vs-priority-reference) | Use custom Severity field or severity-* labels |
| Storing the API token in code | Token leak | Use environment variables / secret stores |
Polling /transitions on every call | Rate-limited | Cache per workflow scheme, refresh on 4xx |
| Bulk transitions without dry-run | Cannot easily reverse if wrong state | Always run in dry-run mode first; log all changes |
customfield_10039)
vary; discover at deploy time.text ~ "user input" accepts JQL operators - always escape quotes and reserved characters.bug-lifecycle-reference,
severity-vs-priority-reference.linear-bug-workflow-runner,
github-issues-bug-workflow.bug-report-from-failure.testrail-integration (in the qa-test-reporting plugin) - different scope (test-result posting; not bug workflow).