feat: comprehensive issue management with auto-triage, cleanup, and stale bot
Auto-triage workflow (issue-triage.yml): - Classifies new issues using Claude Haiku (single turn, low cost) - Sets GitHub issue types (Bug/Feature/Task) via GraphQL API - Applies scope, priority, and size labels automatically - Detects potential duplicates by comparing against open issues - Guides support questions to Discord/Discussions - Flags feature suggestions for discussion conversion - Requests reproduction steps for unconfirmed bugs Stale issue workflow (issue-stale.yml): - Marks issues stale after 90 days of inactivity - Auto-closes after 14 more days - Exempts critical, high priority, and bounty issues Issue template updates: - Remove all "type: *" labels from templates (replaced by issue types) - Auto-triage handles classification for all templates uniformly One-time scripts (scripts/): - issue-cleanup-labels.sh: deletes obsolete type labels and typos - issue-backfill-triage.sh: triages existing 154 unlabeled issues https://claude.ai/code/session_012t1J7hF5Th74qF9Fcn8h3h
This commit is contained in:
Executable
+182
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-time script to triage existing unlabeled issues using Claude.
|
||||
# Run locally with authenticated `gh` and `claude` (Claude Code CLI):
|
||||
#
|
||||
# bash scripts/issue-backfill-triage.sh [--dry-run] [--limit N]
|
||||
#
|
||||
# Prerequisites:
|
||||
# - gh CLI authenticated with repo write access
|
||||
# - Claude Code CLI installed (npm install -g @anthropic-ai/claude-code)
|
||||
# - ANTHROPIC_API_KEY set in environment
|
||||
#
|
||||
# What it does:
|
||||
# 1. Fetches all open issues with no labels
|
||||
# 2. For each, calls Claude to classify it
|
||||
# 3. Sets the issue type via GraphQL
|
||||
# 4. Applies scope/priority/size labels
|
||||
# 5. Leaves a triage comment
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO="twentyhq/twenty"
|
||||
DRY_RUN=false
|
||||
LIMIT=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--limit) LIMIT=$2; shift 2 ;;
|
||||
*) echo "Unknown option: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "=== Backfill Triage for $REPO ==="
|
||||
echo "Dry run: $DRY_RUN"
|
||||
[ "$LIMIT" -gt 0 ] && echo "Limit: $LIMIT issues"
|
||||
|
||||
# Fetch issue types
|
||||
echo "Fetching issue types..."
|
||||
ISSUE_TYPES=$(gh api graphql \
|
||||
-H "GraphQL-Features: issue_types" \
|
||||
-f query='{
|
||||
repository(owner: "twentyhq", name: "twenty") {
|
||||
issueTypes(first: 20) {
|
||||
nodes { id name }
|
||||
}
|
||||
}
|
||||
}' --jq '.data.repository.issueTypes.nodes')
|
||||
echo "Available types: $(echo "$ISSUE_TYPES" | jq -r '.[].name' | tr '\n' ', ')"
|
||||
|
||||
# Fetch all open issues for duplicate-check context
|
||||
echo "Fetching open issues for context..."
|
||||
ALL_ISSUES=$(gh issue list -R "$REPO" --state open --limit 200 --json number,title --jq '.[] | "#\(.number): \(.title)"')
|
||||
|
||||
# Fetch unlabeled issues
|
||||
echo "Fetching unlabeled open issues..."
|
||||
UNLABELED=$(gh api --paginate "repos/$REPO/issues?state=open&per_page=100" \
|
||||
--jq '.[] | select(.labels | length == 0) | select(.pull_request == null) | "\(.number)\t\(.title)"')
|
||||
|
||||
COUNT=$(echo "$UNLABELED" | grep -c . || true)
|
||||
echo "Found $COUNT unlabeled issues"
|
||||
[ "$COUNT" -eq 0 ] && { echo "Nothing to do!"; exit 0; }
|
||||
|
||||
PROCESSED=0
|
||||
|
||||
while IFS=$'\t' read -r number title; do
|
||||
[ -z "$number" ] && continue
|
||||
PROCESSED=$((PROCESSED + 1))
|
||||
[ "$LIMIT" -gt 0 ] && [ "$PROCESSED" -gt "$LIMIT" ] && break
|
||||
|
||||
echo ""
|
||||
echo "--- Processing #$number: $title ($PROCESSED/$COUNT) ---"
|
||||
|
||||
# Fetch full issue body
|
||||
BODY=$(gh issue view "$number" -R "$REPO" --json body --jq '.body' 2>/dev/null || echo "")
|
||||
|
||||
# Call Claude for classification
|
||||
PROMPT="You are an issue triager for Twenty CRM. Classify this issue. Respond with ONLY valid JSON.
|
||||
|
||||
Issue #$number
|
||||
Title: $title
|
||||
Body:
|
||||
$BODY
|
||||
|
||||
Recent open issues (for duplicate check):
|
||||
$ALL_ISSUES
|
||||
|
||||
{
|
||||
\"issue_type\": \"<Bug|Feature|Task|null>\",
|
||||
\"scope\": \"<front|backend|back+front|infra|self-host|null>\",
|
||||
\"priority\": \"<critical|high|medium|low>\",
|
||||
\"size\": \"<short|medium|long>\",
|
||||
\"is_support_question\": <true|false>,
|
||||
\"needs_reproduction\": <true|false>,
|
||||
\"good_first_issue\": <true|false>,
|
||||
\"duplicate_of\": <number|null>,
|
||||
\"summary\": \"<one sentence>\"
|
||||
}"
|
||||
|
||||
RESULT=$(echo "$PROMPT" | claude --model haiku --print 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$RESULT" ]; then
|
||||
echo " Claude returned empty, skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Extract JSON
|
||||
JSON=$(echo "$RESULT" | python3 -c "
|
||||
import sys, json, re
|
||||
text = sys.stdin.read()
|
||||
match = re.search(r'\{[\s\S]*\}', text)
|
||||
if match:
|
||||
obj = json.loads(match.group())
|
||||
print(json.dumps(obj))
|
||||
else:
|
||||
sys.exit(1)
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$JSON" ]; then
|
||||
echo " Could not parse JSON from Claude response, skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo " Classification: $(echo "$JSON" | jq -c '{type: .issue_type, scope: .scope, prio: .priority}')"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo " [DRY RUN] Would apply: $JSON"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Set issue type
|
||||
ISSUE_TYPE=$(echo "$JSON" | jq -r '.issue_type // empty')
|
||||
if [ -n "$ISSUE_TYPE" ] && [ "$ISSUE_TYPE" != "null" ]; then
|
||||
TYPE_ID=$(echo "$ISSUE_TYPES" | jq -r --arg name "$ISSUE_TYPE" '.[] | select(.name == $name) | .id')
|
||||
if [ -n "$TYPE_ID" ]; then
|
||||
ISSUE_NODE_ID=$(gh api graphql -f query="{
|
||||
repository(owner: \"twentyhq\", name: \"twenty\") {
|
||||
issue(number: $number) { id }
|
||||
}
|
||||
}" --jq '.data.repository.issue.id')
|
||||
|
||||
gh api graphql \
|
||||
-H "GraphQL-Features: issue_types" \
|
||||
-f query="mutation {
|
||||
updateIssue(input: {id: \"$ISSUE_NODE_ID\", issueTypeId: \"$TYPE_ID\"}) {
|
||||
issue { number }
|
||||
}
|
||||
}" >/dev/null 2>&1 && echo " Set type: $ISSUE_TYPE" || echo " Failed to set type"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build and apply labels
|
||||
LABELS=()
|
||||
SCOPE=$(echo "$JSON" | jq -r '.scope // empty')
|
||||
PRIO=$(echo "$JSON" | jq -r '.priority // empty')
|
||||
SIZE=$(echo "$JSON" | jq -r '.size // empty')
|
||||
IS_SUPPORT=$(echo "$JSON" | jq -r '.is_support_question // false')
|
||||
NEEDS_REPRO=$(echo "$JSON" | jq -r '.needs_reproduction // false')
|
||||
GOOD_FIRST=$(echo "$JSON" | jq -r '.good_first_issue // false')
|
||||
|
||||
[ -n "$SCOPE" ] && [ "$SCOPE" != "null" ] && LABELS+=("scope: $SCOPE")
|
||||
[ -n "$PRIO" ] && [ "$PRIO" != "null" ] && LABELS+=("prio: $PRIO")
|
||||
[ -n "$SIZE" ] && [ "$SIZE" != "null" ] && LABELS+=("size: $SIZE")
|
||||
[ "$IS_SUPPORT" = "true" ] && LABELS+=("support")
|
||||
[ "$NEEDS_REPRO" = "true" ] && LABELS+=("needs: reproduction")
|
||||
[ "$GOOD_FIRST" = "true" ] && LABELS+=("good first issue")
|
||||
|
||||
if [ ${#LABELS[@]} -gt 0 ]; then
|
||||
LABEL_ARGS=""
|
||||
for l in "${LABELS[@]}"; do
|
||||
LABEL_ARGS="$LABEL_ARGS --add-label \"$l\""
|
||||
done
|
||||
eval "gh issue edit $number -R $REPO $LABEL_ARGS" 2>/dev/null && \
|
||||
echo " Applied labels: ${LABELS[*]}" || echo " Failed to apply labels"
|
||||
fi
|
||||
|
||||
# Brief pause to avoid rate limits
|
||||
sleep 1
|
||||
|
||||
done <<< "$UNLABELED"
|
||||
|
||||
echo ""
|
||||
echo "=== Backfill complete! Processed $PROCESSED issues ==="
|
||||
Executable
+82
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-time script to clean up issue labels.
|
||||
# Run locally with an authenticated `gh` CLI:
|
||||
# bash scripts/issue-cleanup-labels.sh
|
||||
#
|
||||
# What it does:
|
||||
# 1. Deletes typo / stale / obsolete labels
|
||||
# 2. Renames labels that are miscategorized
|
||||
# 3. Removes all "type: *" labels (replaced by GitHub issue types)
|
||||
#
|
||||
# Safe to run multiple times (idempotent).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO="twentyhq/twenty"
|
||||
|
||||
echo "=== Issue Label Cleanup for $REPO ==="
|
||||
|
||||
# Labels to delete outright (typos, stale, or obsolete)
|
||||
DELETE_LABELS=(
|
||||
"hihg"
|
||||
"for exa"
|
||||
"HACKTOBERFEST_2025"
|
||||
"type: bug"
|
||||
"type: chore"
|
||||
"type: feature"
|
||||
"type: design improvement"
|
||||
"type: documentation"
|
||||
"type: marketing"
|
||||
"type: SELF_HOST"
|
||||
)
|
||||
|
||||
for label in "${DELETE_LABELS[@]}"; do
|
||||
echo "Deleting label: '$label'"
|
||||
gh label delete "$label" -R "$REPO" --yes 2>/dev/null || echo " (not found or already deleted)"
|
||||
done
|
||||
|
||||
# Create new labels that the triage workflow needs
|
||||
echo ""
|
||||
echo "=== Creating new labels ==="
|
||||
|
||||
declare -A NEW_LABELS=(
|
||||
["scope: self-host"]="bfd4f2:Issues specific to self-hosted deployments"
|
||||
["needs: reproduction"]="fbca04:Bug reports that lack steps to reproduce"
|
||||
["support"]="0075ca:Support questions / troubleshooting"
|
||||
["size: medium"]="c2e0c6:Medium-sized task"
|
||||
)
|
||||
|
||||
for label in "${!NEW_LABELS[@]}"; do
|
||||
IFS=':' read -r color desc <<< "${NEW_LABELS[$label]}"
|
||||
echo "Creating label: '$label' ($desc)"
|
||||
gh label create "$label" -R "$REPO" --color "$color" --description "$desc" 2>/dev/null || echo " (already exists)"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Removing 'type: *' labels from existing issues ==="
|
||||
|
||||
# For each type label, find issues that have it and remove it
|
||||
TYPE_LABELS=(
|
||||
"type: bug"
|
||||
"type: chore"
|
||||
"type: feature"
|
||||
"type: design improvement"
|
||||
"type: documentation"
|
||||
"type: marketing"
|
||||
"type: SELF_HOST"
|
||||
)
|
||||
|
||||
for label in "${TYPE_LABELS[@]}"; do
|
||||
issues=$(gh issue list -R "$REPO" --state all --label "$label" --json number --jq '.[].number' 2>/dev/null || true)
|
||||
if [ -n "$issues" ]; then
|
||||
echo "Removing '$label' from issues: $issues"
|
||||
for num in $issues; do
|
||||
gh issue edit "$num" -R "$REPO" --remove-label "$label" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Done! ==="
|
||||
echo "Remaining labels:"
|
||||
gh label list -R "$REPO" --limit 100
|
||||
Reference in New Issue
Block a user