Claude
Skills
Sign in
Back

triage

Included with Lifetime
$97 forever

Triage FreshService ticket and create GitHub issue

General

What this skill does


# FreshService Ticket Triage

You are a support engineer who triages bug reports from FreshService and creates well-structured GitHub issues. You extract all relevant information from FreshService tickets and automatically create comprehensive issues for development teams.

**Ticket ID:** $ARGUMENTS

## Workflow

### Phase 1: Configuration Validation & Ticket Fetch

Validate credentials, fetch the ticket, and format the data:

```bash
# Validate and sanitize ticket ID
TICKET_ID="$ARGUMENTS"
TICKET_ID="${TICKET_ID//[^0-9]/}"  # Remove all non-numeric characters

if [ -z "$TICKET_ID" ] || ! [[ "$TICKET_ID" =~ ^[0-9]+$ ]]; then
  echo "Invalid ticket ID"
  echo "Usage: /triage <ticket-id>"
  exit 1
fi

# Load FreshService credentials (check multiple locations)
# Priority: env vars (shell profile) > ~/.config/psd-productivity/.env > ~/.claude/freshservice.env
if [ -n "$FRESHSERVICE_API_KEY" ] && [ -n "$FRESHSERVICE_DOMAIN" ]; then
  echo "Using FreshService credentials from environment..."
elif [ -f ~/.config/psd-productivity/.env ]; then
  echo "Loading from ~/.config/psd-productivity/.env..."
  set -a
  source ~/.config/psd-productivity/.env
  set +a
elif [ -f ~/.claude/freshservice.env ]; then
  echo "Loading from ~/.claude/freshservice.env..."
  source ~/.claude/freshservice.env
else
  echo "FreshService configuration not found!"
  echo ""
  echo "Set credentials using one of these methods:"
  echo ""
  echo "  Option A - Shell profile (~/.zshrc):"
  echo "    export FRESHSERVICE_API_KEY=your_api_key_here"
  echo "    export FRESHSERVICE_DOMAIN=psd401"
  echo ""
  echo "  Option B - Config file (~/.config/psd-productivity/.env):"
  echo "    mkdir -p ~/.config/psd-productivity"
  echo "    Add: FRESHSERVICE_API_KEY=your_api_key_here"
  echo "    Add: FRESHSERVICE_DOMAIN=psd401"
  echo ""
  exit 1
fi

# Validate required variables
if [ -z "$FRESHSERVICE_API_KEY" ] || [ -z "$FRESHSERVICE_DOMAIN" ]; then
  echo "Missing required environment variables!"
  echo "Required: FRESHSERVICE_API_KEY, FRESHSERVICE_DOMAIN"
  exit 1
fi

# Validate domain format (alphanumeric and hyphens only, prevents SSRF)
if ! [[ "$FRESHSERVICE_DOMAIN" =~ ^[a-zA-Z0-9-]+$ ]]; then
  echo "Invalid FRESHSERVICE_DOMAIN format"
  echo "Domain must contain only alphanumeric characters and hyphens"
  echo "Example: 'psd401' (not 'psd401.freshservice.com')"
  exit 1
fi

# Validate API key format (basic sanity check)
if [ ${#FRESHSERVICE_API_KEY} -lt 20 ]; then
  echo "Warning: API key appears too short. Please verify your configuration."
fi

echo "Configuration validated"
echo "Domain: $FRESHSERVICE_DOMAIN"
echo ""

# API configuration
API_BASE_URL="https://${FRESHSERVICE_DOMAIN}.freshservice.com/api/v2"
TICKET_ENDPOINT="${API_BASE_URL}/tickets/${TICKET_ID}"

# Temporary files for API responses
TICKET_JSON="/tmp/fs-ticket-${TICKET_ID}.json"
CONVERSATIONS_JSON="/tmp/fs-conversations-${TICKET_ID}.json"

# Cleanup function
cleanup() {
  rm -f "$TICKET_JSON" "$CONVERSATIONS_JSON"
}
trap cleanup EXIT

echo "=== Fetching FreshService Ticket #${TICKET_ID} ==="
echo ""

# Function to make API request with retry logic
api_request() {
  local url="$1"
  local output_file="$2"
  local max_retries=3
  local retry_delay=2
  local attempt=1

  while [ $attempt -le $max_retries ]; do
    # Make request and capture HTTP status code
    http_code=$(curl -s -w "%{http_code}" -u "${FRESHSERVICE_API_KEY}:X" \
         -H "Content-Type: application/json" \
         -X GET "$url" \
         -o "$output_file" \
         --max-time 30)

    # Check for rate limiting (HTTP 429)
    if [ "$http_code" = "429" ]; then
      echo "Error: Rate limit exceeded. Please wait before retrying."
      echo "FreshService API has rate limits (typically 1000 requests/hour)."
      return 1
    fi

    # Success (HTTP 200)
    if [ "$http_code" = "200" ]; then
      return 0
    fi

    # Unauthorized (HTTP 401)
    if [ "$http_code" = "401" ]; then
      echo "Error: Authentication failed. Please check your API key."
      return 1
    fi

    # Not found (HTTP 404)
    if [ "$http_code" = "404" ]; then
      echo "Error: Ticket not found. Please verify the ticket ID."
      return 1
    fi

    # Retry on server errors (5xx)
    if [ $attempt -lt $max_retries ]; then
      echo "Warning: API request failed with HTTP $http_code (attempt $attempt/$max_retries), retrying in ${retry_delay}s..."
      sleep $retry_delay
      retry_delay=$((retry_delay * 2))  # Exponential backoff
    fi
    attempt=$((attempt + 1))
  done

  echo "Error: API request failed after $max_retries attempts (last HTTP code: $http_code)"
  return 1
}

# Fetch ticket with embedded fields
echo "Fetching ticket #${TICKET_ID}..."
if ! api_request "${TICKET_ENDPOINT}?include=requester,stats" "$TICKET_JSON"; then
  echo ""
  echo "Failed to retrieve ticket from FreshService"
  echo "Please verify:"
  echo "  - Ticket ID $TICKET_ID exists"
  echo "  - API key is valid"
  echo "  - Domain is correct ($FRESHSERVICE_DOMAIN)"
  exit 1
fi

# Fetch conversations (comments)
echo "Fetching ticket conversations..."
if ! api_request "${TICKET_ENDPOINT}/conversations" "$CONVERSATIONS_JSON"; then
  echo "Warning: Failed to fetch conversations, continuing without them..."
  echo '{"conversations":[]}' > "$CONVERSATIONS_JSON"
fi

echo "Ticket retrieved successfully"
echo ""

# Check if jq is available for JSON parsing
if ! command -v jq &> /dev/null; then
  echo "Warning: jq not found, using basic parsing"
  echo "Install jq for full functionality: brew install jq (macOS) or apt-get install jq (Linux)"
  JQ_AVAILABLE=false
else
  JQ_AVAILABLE=true
fi

# Extract ticket fields
if [ "$JQ_AVAILABLE" = true ]; then
  SUBJECT=$(jq -r '.ticket.subject // "No subject"' "$TICKET_JSON")
  DESCRIPTION=$(jq -r '.ticket.description_text // .ticket.description // "No description"' "$TICKET_JSON")
  PRIORITY=$(jq -r '.ticket.priority // 0' "$TICKET_JSON")
  STATUS=$(jq -r '.ticket.status // 0' "$TICKET_JSON")
  CREATED_AT=$(jq -r '.ticket.created_at // "Unknown"' "$TICKET_JSON")
  REQUESTER_NAME=$(jq -r '.ticket.requester.name // "Unknown"' "$TICKET_JSON" 2>/dev/null || echo "Unknown")
  CATEGORY=$(jq -r '.ticket.category // "Uncategorized"' "$TICKET_JSON")
  URGENCY=$(jq -r '.ticket.urgency // 0' "$TICKET_JSON")

  # Extract custom fields if present
  CUSTOM_FIELDS=$(jq -r '.ticket.custom_fields // {}' "$TICKET_JSON")

  # Extract attachments if present
  ATTACHMENTS=$(jq -r '.ticket.attachments // []' "$TICKET_JSON")
  HAS_ATTACHMENTS=$(echo "$ATTACHMENTS" | jq '. | length > 0')

  # Extract conversations
  CONVERSATIONS=$(jq -r '.conversations // []' "$CONVERSATIONS_JSON")
  CONVERSATION_COUNT=$(echo "$CONVERSATIONS" | jq '. | length')
else
  # Fallback to basic grep/sed parsing
  SUBJECT=$(grep -o '"subject":"[^"]*"' "$TICKET_JSON" | head -1 | sed 's/"subject":"//;s/"$//' || echo "No subject")
  DESCRIPTION=$(grep -o '"description_text":"[^"]*"' "$TICKET_JSON" | head -1 | sed 's/"description_text":"//;s/"$//' || echo "No description")
  PRIORITY="0"
  STATUS="0"
  CREATED_AT="Unknown"
  REQUESTER_NAME="Unknown"
  CATEGORY="Unknown"
  URGENCY="0"
  HAS_ATTACHMENTS="false"
  CONVERSATION_COUNT="0"
fi

# Map priority codes to human-readable strings
case "$PRIORITY" in
  1) PRIORITY_STR="Low" ;;
  2) PRIORITY_STR="Medium" ;;
  3) PRIORITY_STR="High" ;;
  4) PRIORITY_STR="Urgent" ;;
  *) PRIORITY_STR="Unknown" ;;
esac

# Map urgency codes
case "$URGENCY" in
  1) URGENCY_STR="Low" ;;
  2) URGENCY_STR="Medium" ;;
  3) URGENCY_STR="High" ;;
  *) URGENCY_STR="Unknown" ;;
esac

# Map status codes
case "$STATUS" in
  2) STATUS_STR="Open" ;;
  3) STATUS_STR="Pending" ;;
  4) STATUS_STR="Resolved" ;;
  5) STATUS_STR="Closed" ;;
  *) STATUS_STR="Unknown" ;;
esac

# Format the issue description
ISSUE_DESCRIPTION="Bug report from FreshService Ticket #${TICKET_ID}

## Summary
${SUBJECT}

## Description
${DESCRIPTION}

## Ticket Information
- **FreshService Ticket**: #${TICKET_ID}
- **Status**: ${STA

Related in General