Build an AI GitHub Issue Triage Bot with Claude API
Automatically label, prioritize, and route GitHub issues using Claude API. Save your team hours of manual triage every week with this Python bot.
GitHub issue triage is repetitive work that eats engineering time. This bot reads new issues, classifies them (bug/feature/question/docs), assigns severity (P1/P2/P3), suggests which team should own it, and adds labels โ automatically.
What It Does
- Reads new GitHub issues via webhook
- Sends issue title + body to Claude API
- Gets back: category, severity, team, suggested response
- Applies labels via GitHub API
- Posts a triage comment on the issue
Setup
pip install anthropic PyGithub flask python-dotenv# .env
ANTHROPIC_API_KEY=sk-ant-...
GITHUB_TOKEN=ghp_...
GITHUB_REPO=owner/repo-name
WEBHOOK_SECRET=your-webhook-secretThe Triage Bot
#!/usr/bin/env python3
"""
AI GitHub Issue Triage Bot using Claude API
"""
import hmac
import hashlib
import json
import os
from flask import Flask, request, jsonify, abort
from anthropic import Anthropic
from github import Github
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__)
claude = Anthropic()
gh = Github(os.environ["GITHUB_TOKEN"])
repo = gh.get_repo(os.environ["GITHUB_REPO"])
TRIAGE_PROMPT = """You are an expert software engineer performing issue triage for a DevOps/Kubernetes project.
Analyze this GitHub issue and provide structured triage data.
Issue Title: {title}
Issue Body: {body}
Existing Labels: {labels}
Respond ONLY with valid JSON in this exact format:
{{
"category": "bug|feature|question|documentation|enhancement|security",
"severity": "P1|P2|P3|P4",
"severity_reason": "one sentence explaining why",
"component": "kubernetes|docker|terraform|ci-cd|aws|monitoring|other",
"teams": ["platform", "sre", "backend", "frontend"],
"labels_to_add": ["list", "of", "github", "label", "names"],
"needs_more_info": true|false,
"suggested_response": "A helpful first response to post on the issue (2-3 sentences, friendly tone)",
"triage_notes": "Internal notes for the team (1-2 sentences)"
}}
Severity guide:
- P1: Production outage, security vulnerability, data loss risk
- P2: Major feature broken, significant user impact
- P3: Minor feature issue, workaround exists
- P4: Enhancement request, nice to have, cosmetic issues"""
def verify_webhook_signature(payload: bytes, signature: str) -> bool:
"""Verify GitHub webhook signature."""
secret = os.environ.get("WEBHOOK_SECRET", "").encode()
if not secret:
return True # Skip verification if no secret set
expected = "sha256=" + hmac.new(secret, payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
def triage_issue(title: str, body: str, existing_labels: list) -> dict:
"""Send issue to Claude for triage analysis."""
prompt = TRIAGE_PROMPT.format(
title=title,
body=body[:3000], # limit body to avoid token overflow
labels=", ".join(existing_labels) if existing_labels else "none"
)
message = claude.messages.create(
model="claude-haiku-4-5-20251001", # fast and cheap for triage
max_tokens=1000,
messages=[{"role": "user", "content": prompt}]
)
response_text = message.content[0].text.strip()
# Extract JSON
start = response_text.find("{")
end = response_text.rfind("}") + 1
return json.loads(response_text[start:end])
def apply_triage(issue_number: int, triage: dict):
"""Apply triage results to GitHub issue."""
issue = repo.get_issue(issue_number)
# Add labels
labels_to_add = triage.get("labels_to_add", [])
severity_label = triage.get("severity", "P3").lower()
category_label = triage.get("category", "question")
all_labels = labels_to_add + [severity_label, category_label]
for label_name in all_labels:
try:
label = repo.get_label(label_name)
issue.add_to_labels(label)
except Exception:
# Label doesn't exist, skip
pass
# Post triage comment
comment = format_triage_comment(triage)
issue.create_comment(comment)
# Assign to team if P1
if triage.get("severity") == "P1":
# Add critical/urgent label
try:
issue.add_to_labels(repo.get_label("urgent"))
except Exception:
pass
def format_triage_comment(triage: dict) -> str:
severity_emoji = {
"P1": "๐ด",
"P2": "๐ ",
"P3": "๐ก",
"P4": "๐ข"
}
category_emoji = {
"bug": "๐",
"feature": "โจ",
"question": "โ",
"documentation": "๐",
"security": "๐",
"enhancement": "โก"
}
severity = triage.get("severity", "P3")
category = triage.get("category", "question")
return f"""## ๐ค Automated Triage
{category_emoji.get(category, "๐")} **Category:** {category.capitalize()}
{severity_emoji.get(severity, "๐ก")} **Severity:** {severity} โ {triage.get("severity_reason", "")}
๐ท๏ธ **Component:** {triage.get("component", "other")}
๐ฅ **Suggested Team:** {", ".join(triage.get("teams", ["platform"]))}
---
{triage.get("suggested_response", "Thank you for filing this issue! We will review it shortly.")}
{"โ ๏ธ **Note:** Additional information may be needed to reproduce/address this issue." if triage.get("needs_more_info") else ""}
---
*Triaged by AI ยท Review and override labels as needed*"""
@app.route("/webhook", methods=["POST"])
def github_webhook():
# Verify signature
signature = request.headers.get("X-Hub-Signature-256", "")
if not verify_webhook_signature(request.data, signature):
abort(401, "Invalid signature")
event = request.headers.get("X-GitHub-Event")
payload = request.json
# Only handle issue opened events
if event != "issues" or payload.get("action") != "opened":
return jsonify({"status": "ignored"})
issue = payload["issue"]
issue_number = issue["number"]
title = issue["title"]
body = issue.get("body", "") or ""
existing_labels = [l["name"] for l in issue.get("labels", [])]
print(f"Triaging issue #{issue_number}: {title}")
try:
triage = triage_issue(title, body, existing_labels)
apply_triage(issue_number, triage)
print(f"Triage applied: {triage['severity']} {triage['category']}")
return jsonify({"status": "triaged", "result": triage})
except Exception as e:
print(f"Triage failed: {e}")
return jsonify({"status": "error", "message": str(e)}), 500
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, debug=False)Deploy on Kubernetes
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: issue-triage-bot
namespace: tools
spec:
replicas: 1
template:
spec:
containers:
- name: bot
image: ghcr.io/yourorg/issue-triage-bot:latest
ports:
- containerPort: 8080
envFrom:
- secretRef:
name: triage-bot-secrets
resources:
requests:
memory: "128Mi"
cpu: "100m"
---
apiVersion: v1
kind: Service
metadata:
name: issue-triage-bot
namespace: tools
spec:
selector:
app: issue-triage-bot
ports:
- port: 80
targetPort: 8080Set Up the GitHub Webhook
- Go to your repo โ Settings โ Webhooks โ Add webhook
- Payload URL:
https://your-bot.domain.com/webhook - Content type:
application/json - Secret: same as
WEBHOOK_SECRET - Events: select "Issues" only
GitHub Actions Version (No Server Needed)
If you don't want to run a server, use GitHub Actions instead:
# .github/workflows/triage-issues.yml
name: AI Issue Triage
on:
issues:
types: [opened]
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: pip install anthropic PyGithub
- name: Run triage
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
GITHUB_REPO: ${{ github.repository }}
run: python triage_action.pyThe GitHub Actions version runs per-issue with no server needed โ perfect for smaller repositories.
Cost estimate: Using claude-haiku-4-5-20251001, each issue triage costs ~$0.001. For 1000 issues/month, that's $1. Extremely affordable.
Today I Fixed
Short real fixes from production โ posted daily
Stay ahead of the curve
Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam โ just practical engineering content.
Related Articles
Build an AI-Powered CI/CD Pipeline Failure Analyzer with LangChain
Build a tool that automatically reads CI/CD failure logs, uses LangChain + Claude to diagnose the root cause, and posts a clear explanation with fix suggestions to your PR.
Build an AI GitHub PR Review Bot with Claude API (2026)
Build a GitHub Actions workflow that automatically reviews every pull request using Claude AI โ catches bugs, security issues, and bad patterns before human review.
Build an AI-Powered Terraform Drift Detection System
Terraform drift happens silently. Here's how to build an automated drift detector using Terraform plan + Claude API that alerts your team and explains exactly what changed.