From qa-defect-management
Author and run GitHub Issues bug workflows via REST API v2022-11-28 - issue creation, state changes (open / closed with state_reason), label-based severity/priority classification, comment attachment, and Projects v2 status-column updates via GraphQL. Covers POST /repos/{owner}/{repo}/issues, PATCH for state_reason transitions (completed / not_planned / duplicate / reopened), label conventions for the impoverished GitHub state model, and the gh CLI for scripted workflows. Use when programmatically managing GitHub Issues bug lifecycle - GitHub's binary open/closed model requires label + Projects discipline.
How this skill is triggered — by the user, by Claude, or both
Slash command
/qa-defect-management:github-issues-bug-workflowThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
GitHub Issues has only **two states**: open and closed. This is
GitHub Issues has only two states: open and closed. This is
intentionally minimalist. To express the canonical defect
lifecycle
(bug-lifecycle-reference)
teams supplement Issues with labels (severity, priority,
status) and optionally Projects v2 (status columns).
This skill wraps the GitHub Issues REST API v2022-11-28 (per docs.github.com/en/rest/issues/issues) for create / update / close / reopen / search, and notes the Projects v2 GraphQL augmentation when richer state is needed.
bug-report-from-failure).duplicate-defect-finder
search for GitHub-using teams.Per GitHub REST API docs:
export GITHUB_TOKEN="ghp_..." # personal access token, classic or fine-grained
export GITHUB_REPO="owner/repo"
import requests, os
HEADERS = {
"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2026-03-10",
}
BASE = f"https://api.github.com/repos/{os.environ['GITHUB_REPO']}"
The X-GitHub-Api-Version header is recommended per the API
docs to lock the response shape.
Alternative: the gh CLI handles auth via the user's stored
credentials:
gh issue create --title "..." --body "..." --label bug,severity:high
POST /repos/{owner}/{repo}/issues per the API docs:
def create_bug(title, body, severity, priority, labels=None):
payload = {
"title": title,
"body": body,
"labels": (labels or []) + [
"bug",
f"severity:{severity}",
f"priority:{priority}",
],
}
r = requests.post(f"{BASE}/issues", json=payload, headers=HEADERS)
r.raise_for_status()
return r.json()
Required parameter is title. Optional: body, assignees,
milestone, labels, type (recently added for issue types).
Since GitHub has no first-class severity / priority field, teams adopt label prefixes:
| Convention | Example labels |
|---|---|
| Severity | severity:critical, severity:high, severity:medium, severity:low, severity:trivial |
| Priority | priority:p1, priority:p2, priority:p3, priority:p4, priority:p5 |
| Lifecycle | status:triage, status:confirmed, status:in-progress, status:in-review, status:verified, status:wontfix, status:duplicate |
| Defect type | type:regression, type:performance, type:security |
| Component | component:auth, component:payments, component:ui |
Adopt them consistently - the
bug-report-critic checks
that severity + priority labels are both present.
PATCH /repos/{owner}/{repo}/issues/{issue_number}. The
state_reason parameter (per API docs) takes
completed | not_planned | reopened | duplicate:
def close(issue_number, reason="completed"):
"""reason: completed | not_planned | duplicate"""
r = requests.patch(
f"{BASE}/issues/{issue_number}",
json={"state": "closed", "state_reason": reason},
headers=HEADERS,
)
r.raise_for_status()
return r.json()
def reopen(issue_number):
r = requests.patch(
f"{BASE}/issues/{issue_number}",
json={"state": "open", "state_reason": "reopened"},
headers=HEADERS,
)
r.raise_for_status()
return r.json()
Map canonical lifecycle states via labels + close-reason:
| Canonical | GitHub representation |
|---|---|
| New | open + status:triage |
| Open / Acknowledged | open + status:confirmed |
| Assigned | open + status:confirmed + assignees set |
| In Progress | open + status:in-progress + linked draft PR |
| Fixed | open + status:in-review + ready PR |
| Verified | open + status:verified |
| Closed (success) | closed + state_reason: completed |
| Reopened | open + state_reason: reopened |
| Deferred / Wontfix | closed + state_reason: not_planned + label status:wontfix |
| Rejected | closed + state_reason: not_planned + label not-a-bug |
| Duplicate | closed + state_reason: duplicate + comment Duplicate of #N |
GET /repos/{owner}/{repo}/issues supports filter via query
parameters; for richer search use the search endpoint:
def search_issues(q):
r = requests.get(
"https://api.github.com/search/issues",
params={"q": f"repo:{os.environ['GITHUB_REPO']} {q}"},
headers=HEADERS,
)
r.raise_for_status()
return r.json()["items"]
dupes = search_issues(
f'type:issue is:open label:bug "{title_safe}" in:title,body'
)
GitHub search has a 30-request-per-minute unauthenticated / higher authenticated rate limit.
POST /repos/{owner}/{repo}/issues/{issue_number}/comments:
def add_comment(issue_number, body):
r = requests.post(
f"{BASE}/issues/{issue_number}/comments",
json={"body": body}, headers=HEADERS)
r.raise_for_status()
return r.json()
def create_or_attach(title, body):
dupes = search_issues(f'is:open label:bug "{title}" in:title')
if dupes:
add_comment(dupes[0]["number"], f"Recurred: {body[:500]}")
return dupes[0]["number"]
issue = create_bug(title, body, severity="medium", priority="p3")
return issue["number"]
For richer state (e.g., a Kanban with custom columns), Projects v2 requires GraphQL - the REST API doesn't reach Projects v2:
PROJECTS_MUTATION = """
mutation MoveItem($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
updateProjectV2ItemFieldValue(
input: { projectId: $projectId, itemId: $itemId,
fieldId: $fieldId, value: { singleSelectOptionId: $optionId } }
) { projectV2Item { id } }
}
"""
# Discovery of projectId, itemId, fieldId, optionId via the matching queries.
Per docs.github.com/en/issues/planning-and-tracking-with-projects.
# Create
gh issue create \
--title "Checkout fails for promo X" \
--body-file failure.md \
--label bug,severity:high,priority:p2
# Close with reason
gh issue close 1234 --reason completed
gh issue close 1234 --reason "not planned"
# Search
gh issue list --search 'is:open label:bug "checkout fails"'
Create response includes number (per-repo), html_url
(permalink), node_id (GraphQL ID for Projects v2 cross-ref).
Search response includes items array (issues + PRs), total_count,
incomplete_results (set to true on partial results due to
rate limit).
# .github/workflows/test.yml
- name: Run tests
id: tests
run: pytest --junitxml=results.xml
continue-on-error: true
- name: File issue on test failure
if: steps.tests.outcome == 'failure'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPO: ${{ github.repository }}
run: python scripts/file-github-bug.py results.xml
Use the auto-provided GITHUB_TOKEN for in-repo automation; for
cross-repo, use a fine-grained PAT.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Closing without state_reason | Defaults to completed - wrong for not-a-bug / duplicate | Always set state_reason explicitly |
| Severity / priority in title prefix | "[CRITICAL]" prefixes - not searchable; not filterable | Use labels |
| Free-form status labels per team | Cross-team queries break | Adopt the canonical label vocabulary above |
| Search-rate-limit ignored | Bulk dedupe scripts get 403s | Throttle to 30 req/min unauth, 5000 authenticated |
No X-GitHub-Api-Version header | Future API changes silently break code | Always set the version header |
| Plain-text body (no Markdown) | Loses code-block formatting | Use Markdown in body |
Closing with state: closed without state_reason for "wontfix" | Ambiguous closure - looks the same as a fix | Use state_reason: not_planned |
type field is new. GitHub recently added issue type;
not universally supported across all clients yet.org: qualifier.gh CLI manual -
cli.github.com/manual/gh_issue.bug-lifecycle-reference,
severity-vs-priority-reference.jira-bug-workflow-runner,
linear-bug-workflow-runner.bug-report-from-failure,
duplicate-defect-finder.Guides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.
npx claudepluginhub testland/qa --plugin qa-defect-management