Files, transitions, and searches bugs in the team's tracker - Jira, Linear, GitHub Issues, or Azure DevOps - through one tracker-agnostic workflow: authenticate, dedupe-search before creating, create with severity/priority classification, transition lifecycle states, and wire idempotent CI-driven filing from test failures. Jira Cloud REST API v3 is worked in full in the body (ADF descriptions, runtime transition lookup, JQL triage and duplicate queries, dry-run bulk transitions); Linear's GraphQL API (issueCreate/issueUpdate, workflowStates resolved by type, the 0-4 priority enum), GitHub Issues REST (open/closed + state_reason, label-based severity/priority), and Azure DevOps Work Item Tracking (JSON Patch, WIQL, process-template states) each have a deep reference. Use when programmatically managing the bug lifecycle on any of the four trackers: creating from CI failures, triaging queues, transitioning states, or dedupe-searching.
94
96%
Does it follow best practices?
Impact
94%
1.01xAverage score across 10 eval scenarios
Low
Low-risk findings worth noting
Every mainstream tracker exposes the same four core operations - create, transition, search, and update/comment - behind a different API shape. This skill runs the bug workflow tracker-agnostically, with Jira Cloud REST API v3 worked in full below and per-platform deep dives in references:
| Tracker | API shape | Lifecycle model | Deep dive |
|---|---|---|---|
| Jira Cloud | REST v3, ADF rich text | Configurable workflow engine; look up transition IDs at runtime | Worked below + references/jira.md |
| Linear | GraphQL only | Per-team WorkflowState objects; resolve by type, not display name | references/linear.md |
| GitHub Issues | REST + Projects v2 GraphQL | Two states (open/closed) + state_reason; severity/priority via labels | references/github-issues.md |
| Azure DevOps | WIT REST 7.1, JSON Patch | Process-template states (Agile: New/Active/Resolved/Closed); WIQL search | references/azuredevops.md |
The tracker-agnostic rules that hold on all four platforms:
severity-vs-priority-reference).bug-report-template, qa-bug-repro).Jira's workflow engine maps cleanly to the canonical defect lifecycle (see
the lifecycle reference in severity-vs-priority-reference), but every
project's actual workflow is configurable, so the runner looks up transition
IDs at runtime rather than hard-coding them. All calls per
developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/.
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>"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. 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 uses labels for portability; discovering
and submitting the custom field is in references/jira.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'
)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. 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.
The same workflow shape on the other three platforms, each with its own auth, create, transition, search, worked example, anti-patterns, and CI wiring:
issueCreate / issueUpdate mutations; per-team workflowStates
resolved by lifecycle type (never display name); priority enum where
1 = Urgent; personal-key vs OAuth Bearer header difference.state_reason
transitions (completed / not_planned / duplicate / reopened); Projects v2
GraphQL and the gh CLI.application/json-patch+json); WIQL
triage / dedupe queries; process-template state names; optimistic
concurrency via test /rev; PR / build artifact links; az boards CLI.| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Hard-coding transition IDs / state names | Workflow or process-template updates break the runner silently | Discover at runtime (Jira GET /transitions, Linear workflowStates by type, ADO process template) |
Plain-text description in Jira | 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 a custom Severity field or severity-* labels |
| Storing the API token in code | Token leak | Use environment variables / secret stores |
| Polling metadata endpoints 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) and ADO severity availability vary; discover at
deploy time.text ~, WIQL CONTAINS WORDS, and GitHub
search all interpolate user text - escape quotes and reserved characters.severity-vs-priority-reference (classification,
lifecycle states, taxonomy).bug-report-template
(qa-bug-repro).test-management-sync (in the
qa-test-reporting plugin) - different scope (test-result posting; not bug
workflow).