bug-tracker-workflow · git:20260813.bc28491 · 2026-08-13 · sha256 71e12b6d7f49c763

bug-tracker-workflow git:20260813.bc28491A

Immutable. This exact content never changes and is served at /api/v1/blob/71e12b6d7f49c763.

---
name: bug-tracker-workflow
description: "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."
---

# bug-tracker-workflow

## Overview

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](references/jira.md) |
| **Linear** | GraphQL only | Per-team `WorkflowState` objects; resolve by `type`, not display name | [references/linear.md](references/linear.md) |
| **GitHub Issues** | REST + Projects v2 GraphQL | Two states (open/closed) + `state_reason`; severity/priority via labels | [references/github-issues.md](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](references/azuredevops.md) |

The tracker-agnostic rules that hold on all four platforms:

- **Dedupe before create.** CI bug filing must not duplicate when the same
  failure recurs: search open bugs by title/summary first, comment on the
  existing bug on a hit, and fail closed (skip the create, surface the
  error) if the search itself errors.
- **Severity and priority are different axes** - keep both fields and score
  them independently (`severity-vs-priority-reference`).
- **Never hard-code lifecycle identifiers.** Jira transition IDs, Linear
  state names, and ADO state strings are all tenant/team/process-specific -
  discover them at runtime.
- **Dry-run bulk operations.** Bulk transitions are not trivially
  reversible; log the plan and verify counts before applying.
- **Secrets in env vars / secret stores**, never in code.

## When to use

- Filing a bug from a CI test failure (fed by the from-CI-failure workflow
  in `bug-report-template`, qa-bug-repro).
- Bulk-transitioning bugs after a release.
- Building a triage script that pulls new defects and applies severity /
  priority based on labels.
- Backing a duplicate-defect search backend.

## Worked primary - Jira Cloud REST API v3

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/](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/).

### Authentication

Jira Cloud REST API v3 uses HTTP Basic auth with an API token:

```bash
export JIRA_BASE="https://your-tenant.atlassian.net"
export JIRA_EMAIL="you@company.com"
export JIRA_TOKEN="<api-token-from-id.atlassian.com>"
```

```python
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"]
```

### Create a bug

`POST /rest/api/3/issue`. The `description` must be Atlassian Document
Format (ADF), not plain text.

```python
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](references/jira.md).

### Look up and apply a transition

Workflow transitions are project-specific. Look up the available transitions
then apply by transition ID:

```python
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.

### Search via JQL

`POST /rest/api/3/search/jql` returns issues matching a JQL query. Useful
for duplicate detection and triage queues.

```python
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'
)
```

### Idempotent bug creation

```python
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.

### Bulk transition after release

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:

```python
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.

### CI integration

Auto-file a bug from a test failure:

```yaml
# .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.xml
```

Where `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.

## Other trackers

The same workflow shape on the other three platforms, each with its own
auth, create, transition, search, worked example, anti-patterns, and CI
wiring:

- **Linear** - [references/linear.md](references/linear.md): GraphQL-only
  API; `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.
- **GitHub Issues** - [references/github-issues.md](references/github-issues.md):
  two states only; the canonical-lifecycle-to-labels mapping; `state_reason`
  transitions (completed / not_planned / duplicate / reopened); Projects v2
  GraphQL and the gh CLI.
- **Azure DevOps** - [references/azuredevops.md](references/azuredevops.md):
  JSON Patch work-item mutations (`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-patterns

| 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 |

## Limitations

- **Workflow is per-project / per-team / per-process.** A transition or
  state name in one project may not exist in another - handle "not found"
  gracefully on every platform.
- **Custom fields are tenant-specific.** Jira field IDs
  (`customfield_10039`) and ADO severity availability vary; discover at
  deploy time.
- **Rich text differs per platform.** Jira needs full ADF; Linear takes
  Markdown; GitHub takes Markdown; ADO takes HTML. Test code-block / table
  rendering per platform.
- **Rate limits everywhere.** Jira per-minute, Linear ~1500 req/15 min,
  GitHub search 30 req/min unauthenticated, ADO per-user throttling - bulk
  operations need throttling and retry-with-backoff.
- **Query injection.** JQL `text ~`, WIQL `CONTAINS WORDS`, and GitHub
  search all interpolate user text - escape quotes and reserved characters.

## References

- Jira Cloud REST API v3 issues -
  [developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/).
- Jira Cloud authentication -
  developer.atlassian.com/cloud/jira/platform/basic-auth-for-rest-apis.
- ADF spec - developer.atlassian.com/cloud/jira/platform/apis/document/structure.
- JQL syntax - confluence.atlassian.com/jiracoreserver/advanced-searching.
- Per-platform deep dives: [references/jira.md](references/jira.md),
  [references/linear.md](references/linear.md),
  [references/github-issues.md](references/github-issues.md),
  [references/azuredevops.md](references/azuredevops.md) - each carries its
  own platform-doc citations.
- Sibling reference: `severity-vs-priority-reference` (classification,
  lifecycle states, taxonomy).
- Fed by: the from-CI-failure spec workflow in `bug-report-template`
  (qa-bug-repro).
- Sibling-plugin neighbour: `test-management-sync` (in the
  qa-test-reporting plugin) - different scope (test-result posting; not bug
  workflow).