Testland
Browse all skills & agents

github-issues-bug-workflow

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.

github-issues-bug-workflow

Overview

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.

When to use

  • Filing a bug from a CI test failure on a GitHub-hosted project (consumed by bug-report-from-failure).
  • Maintaining bug label / status discipline in an open-source project where GitHub Issues is the canonical tracker.
  • Backing the duplicate-defect-finder search for GitHub-using teams.

Authoring

Authentication

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

Create an issue

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

Label conventions

Since GitHub has no first-class severity / priority field, teams adopt label prefixes:

ConventionExample labels
Severityseverity:critical, severity:high, severity:medium, severity:low, severity:trivial
Prioritypriority:p1, priority:p2, priority:p3, priority:p4, priority:p5
Lifecyclestatus:triage, status:confirmed, status:in-progress, status:in-review, status:verified, status:wontfix, status:duplicate
Defect typetype:regression, type:performance, type:security
Componentcomponent:auth, component:payments, component:ui

Adopt them consistently - the bug-report-critic checks that severity + priority labels are both present.

State transitions via PATCH

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:

CanonicalGitHub representation
Newopen + status:triage
Open / Acknowledgedopen + status:confirmed
Assignedopen + status:confirmed + assignees set
In Progressopen + status:in-progress + linked draft PR
Fixedopen + status:in-review + ready PR
Verifiedopen + status:verified
Closed (success)closed + state_reason: completed
Reopenedopen + state_reason: reopened
Deferred / Wontfixclosed + state_reason: not_planned + label status:wontfix
Rejectedclosed + state_reason: not_planned + label not-a-bug
Duplicateclosed + state_reason: duplicate + comment Duplicate of #N

Search

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.

Comments

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

Running

Idempotent bug filing

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

Projects v2 status updates

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.

gh CLI for scripts

# 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"'

Parsing results

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

CI integration

# .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-patterns

Anti-patternWhy it failsFix
Closing without state_reasonDefaults to completed - wrong for not-a-bug / duplicateAlways set state_reason explicitly
Severity / priority in title prefix"[CRITICAL]" prefixes - not searchable; not filterableUse labels
Free-form status labels per teamCross-team queries breakAdopt the canonical label vocabulary above
Search-rate-limit ignoredBulk dedupe scripts get 403sThrottle to 30 req/min unauth, 5000 authenticated
No X-GitHub-Api-Version headerFuture API changes silently break codeAlways set the version header
Plain-text body (no Markdown)Loses code-block formattingUse Markdown in body
Closing with state: closed without state_reason for "wontfix"Ambiguous closure - looks the same as a fixUse state_reason: not_planned

Limitations

  • Open / closed only. Rich lifecycle expressed via labels + Projects requires team discipline; the API doesn't enforce it.
  • No native severity / priority fields. Conventions vary across orgs - the runner is portable only if the team adopts the label vocabulary above.
  • Projects v2 is GraphQL. REST + GraphQL hybrid; engineers need both.
  • type field is new. GitHub recently added issue type; not universally supported across all clients yet.
  • Cross-repo dedupe. GitHub Issues are per-repo; cross-repo duplicate detection needs Search API with org: qualifier.

References