Created
October 31, 2025 07:02
-
-
Save RizkiHerdaID/9b4b5c1f9f6c8b91bc084b68033cc5e3 to your computer and use it in GitHub Desktop.
JIRA Helper Script for GitHub Copilot Integration
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Jira Helper Configuration | |
| # Your Jira instance URL (e.g., https://yourcompany.atlassian.net) | |
| JIRA_BASE_URL= | |
| # Your Jira email address | |
| JIRA_EMAIL= | |
| JIRA_USER_EMAIL= | |
| # Your Jira API token (generate from: https://id.atlassian.com/manage-profile/security/api-tokens) | |
| JIRA_API_TOKEN= | |
| # Optional: Project key (default will search all projects) | |
| JIRA_PROJECT_KEY= | |
| # Optional: Maximum results to fetch from Jira API (default: 10) | |
| JIRA_MAX_RESULTS=10 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # JIRA Helper Script for GitHub Copilot Integration | |
| # Provides convenient retrieval of JIRA issue details in text or JSON. | |
| # Enhanced help output, version flag, and graceful help display without env file present. | |
| set -euo pipefail | |
| IFS=$'\n\t' | |
| VERSION="1.4.0" | |
| # Detect script directory for relative .env | |
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| # Pre-scan arguments to allow help / version without having a valid env file | |
| HELP_ONLY=0 | |
| for _raw_arg in "$@"; do | |
| case "$_raw_arg" in | |
| help|--help|-h|--version) HELP_ONLY=1; break ;; | |
| esac | |
| done | |
| # Load environment variables (allow override via JIRA_ENV_FILE) | |
| ENV_FILE=${JIRA_ENV_FILE:-"${SCRIPT_DIR}/.env.jira"} | |
| if [[ -f "$ENV_FILE" ]]; then | |
| # shellcheck disable=SC1090 | |
| source "$ENV_FILE" | |
| else | |
| if [[ "$HELP_ONLY" -ne 1 ]]; then | |
| echo "β Error: JIRA env file not found at $ENV_FILE" >&2 | |
| exit 2 | |
| fi | |
| fi | |
| # Basic dependency validation | |
| for dep in curl jq; do | |
| if ! command -v "$dep" >/dev/null 2>&1; then | |
| echo "β Missing dependency: $dep (please install before continuing)" >&2 | |
| exit 3 | |
| fi | |
| done | |
| # Validate required env vars | |
| missing_env=() | |
| for v in JIRA_BASE_URL JIRA_EMAIL JIRA_API_TOKEN; do | |
| if [[ -z "${!v:-}" ]]; then | |
| missing_env+=("$v") | |
| fi | |
| done | |
| if (( ${#missing_env[@]} )); then | |
| echo "β Missing required environment variables: ${missing_env[*]}" >&2 | |
| exit 4 | |
| fi | |
| # Flag to control whether we print full description (no truncation) | |
| FULL_DESCRIPTION=${FULL_DESCRIPTION:-0} | |
| FAIL_ON_MISSING=${FAIL_ON_MISSING:-0} | |
| # Output format (text or json) for AI-friendly structured consumption. | |
| # Can be set via env OUTPUT_FORMAT=json or flag --json | |
| OUTPUT_FORMAT=${OUTPUT_FORMAT:-text} | |
| # Output file (if set, write final output there) (still supported for get) | |
| OUT_FILE=${OUT_FILE:-""} | |
| # Caching configuration | |
| JIRA_CACHE_DIR=${JIRA_CACHE_DIR:-"${SCRIPT_DIR}/.jira_cache"} | |
| CACHE_TTL_SECONDS=${CACHE_TTL_SECONDS:-300} # default 5m | |
| mkdir -p "$JIRA_CACHE_DIR" 2>/dev/null || true | |
| # Search configuration | |
| SEARCH_MAX_RESULTS=${SEARCH_MAX_RESULTS:-50} | |
| SEARCH_START_AT=${SEARCH_START_AT:-0} | |
| # Bulk operations configuration | |
| BULK_MAX_ISSUES=${BULK_MAX_ISSUES:-20} # Max issues for get-many | |
| DEFAULT_FIELDS=${DEFAULT_FIELDS:-"key,summary,status,assignee,reporter,issuetype,priority,created,updated,project,labels,components,fixVersions"} | |
| # Output format options: text, json, compact, detailed, ai-summary | |
| OUTPUT_FORMAT_MODE=${OUTPUT_FORMAT_MODE:-"detailed"} | |
| # Smart caching configuration - different TTLs for different data types | |
| CACHE_TTL_METADATA=${CACHE_TTL_METADATA:-86400} # 24 hours for projects, statuses, etc. | |
| CACHE_TTL_ISSUES=${CACHE_TTL_ISSUES:-300} # 5 minutes for issues (default) | |
| CACHE_TTL_SEARCH=${CACHE_TTL_SEARCH:-600} # 10 minutes for search results | |
| # Rate limiting configuration | |
| RATE_LIMIT_REQUESTS=${RATE_LIMIT_REQUESTS:-30} # Max requests per minute | |
| RATE_LIMIT_WINDOW=${RATE_LIMIT_WINDOW:-60} # Time window in seconds | |
| RATE_LIMIT_FILE=${RATE_LIMIT_FILE:-"${SCRIPT_DIR}/.jira_rate_limit"} | |
| cache_path() { # key | |
| local key="$1" | |
| echo "$JIRA_CACHE_DIR/${key//\//_}.json" | |
| } | |
| cache_get() { # key | |
| local key="$1"; local path | |
| path="$(cache_path "$key")" | |
| [[ -f "$path" ]] || return 1 | |
| local now epoch | |
| now=$(date +%s) | |
| epoch=$(stat -c %Y "$path" 2>/dev/null || echo 0) | |
| if (( now - epoch > CACHE_TTL_SECONDS )); then | |
| return 1 | |
| fi | |
| cat "$path" | |
| } | |
| cache_set() { # key json | |
| local key="$1"; local data="$2"; local path | |
| path="$(cache_path "$key")" | |
| printf '%s' "$data" > "$path" | |
| } | |
| # Smart cache functions with different TTLs based on data type | |
| cache_get_smart() { # key data_type | |
| local key="$1"; local data_type="${2:-issue}"; local path | |
| path="$(cache_path "$key")" | |
| [[ -f "$path" ]] || return 1 | |
| local now epoch ttl | |
| now=$(date +%s) | |
| epoch=$(stat -c %Y "$path" 2>/dev/null || echo 0) | |
| case "$data_type" in | |
| metadata) ttl=$CACHE_TTL_METADATA ;; | |
| search) ttl=$CACHE_TTL_SEARCH ;; | |
| *) ttl=$CACHE_TTL_ISSUES ;; | |
| esac | |
| if (( now - epoch > ttl )); then | |
| return 1 | |
| fi | |
| cat "$path" | |
| } | |
| cache_set_smart() { # key json data_type | |
| local key="$1"; local data="$2"; local data_type="${3:-issue}"; local path | |
| path="$(cache_path "$key")" | |
| printf '%s' "$data" > "$path" | |
| } | |
| # Rate limiting functions | |
| check_rate_limit() { | |
| local now=$(date +%s) | |
| local rate_file="$RATE_LIMIT_FILE" | |
| # Create rate limit file if it doesn't exist | |
| if [[ ! -f "$rate_file" ]]; then | |
| echo "0 $now" > "$rate_file" | |
| return 0 | |
| fi | |
| # Read current count and window start | |
| local count window_start | |
| read -r count window_start < "$rate_file" 2>/dev/null || { echo "0 $now" > "$rate_file"; return 0; } | |
| # Reset if window expired | |
| if (( now - window_start > RATE_LIMIT_WINDOW )); then | |
| echo "1 $now" > "$rate_file" | |
| return 0 | |
| fi | |
| # Check if limit exceeded | |
| if (( count >= RATE_LIMIT_REQUESTS )); then | |
| local wait_time=$((RATE_LIMIT_WINDOW - (now - window_start))) | |
| echo "β³ Rate limit reached ($RATE_LIMIT_REQUESTS requests/$RATE_LIMIT_WINDOW seconds). Wait ${wait_time}s or use cache." >&2 | |
| return 1 | |
| fi | |
| # Increment counter | |
| echo "$((count + 1)) $window_start" > "$rate_file" | |
| return 0 | |
| } | |
| # Unified curl helper with error & smart caching (GET only when no body) | |
| http_get_json() { # key path (relative) queryString(optional) data_type(optional) | |
| local key="$1"; shift | |
| local endpoint="$1"; shift || true | |
| local qs="${1:-}"; shift || true # optional query string already encoded | |
| local data_type="${1:-issue}" # optional data type for smart caching | |
| local cache_key | |
| cache_key="GET_${endpoint//\//_}_${qs}" | |
| # Try smart cache first | |
| if [[ "$CACHE_TTL_SECONDS" -gt 0 ]]; then | |
| if cached=$(cache_get_smart "$cache_key" "$data_type" 2>/dev/null); then | |
| echo "$cached" | |
| return 0 | |
| fi | |
| fi | |
| # Check rate limit before making request | |
| if ! check_rate_limit; then | |
| return 6 # Rate limit exceeded | |
| fi | |
| local url="$JIRA_BASE_URL$endpoint"; | |
| [[ -n "$qs" ]] && url+="?$qs" | |
| local resp | |
| resp=$(curl -s -w '\n%{http_code}' -u "${JIRA_EMAIL}:${JIRA_API_TOKEN}" -H 'Accept: application/json' "$url") | |
| local body status | |
| status="${resp##*$'\n'}" | |
| body="${resp%$'\n'$status}" | |
| if [[ "$status" != 2* ]]; then | |
| echo "{""error"": ""HTTP $status"", ""cache_key"": ""$cache_key""}" >&2 | |
| echo "$body" | jq '.errorMessages? // .' >&2 || true | |
| return 5 | |
| fi | |
| if (( CACHE_TTL_SECONDS > 0 )); then | |
| cache_set_smart "$cache_key" "$body" "$data_type" | |
| fi | |
| echo "$body" | |
| } | |
| # Unified curl helper for POST with JSON body (optionally cached) | |
| http_post_json() { # key endpoint json_body | |
| local key="$1"; shift | |
| local endpoint="$1"; shift | |
| local body="$1" | |
| local cache_key="POST_${endpoint//\//_}_${key}" | |
| # Only cache if TTL > 0 | |
| if [[ "$CACHE_TTL_SECONDS" -gt 0 ]]; then | |
| if cached=$(cache_get "$cache_key" 2>/dev/null); then | |
| echo "$cached" | |
| return 0 | |
| fi | |
| fi | |
| local url="$JIRA_BASE_URL$endpoint" | |
| local resp | |
| resp=$(curl -s -w '\n%{http_code}' -u "${JIRA_EMAIL}:${JIRA_API_TOKEN}" -H 'Accept: application/json' -H 'Content-Type: application/json' -X POST -d "$body" "$url") | |
| local body_out status | |
| status="${resp##*$'\n'}" | |
| body_out="${resp%$'\n'$status}" | |
| if [[ "$status" != 2* ]]; then | |
| echo "{\"error\": \"HTTP $status\", \"cache_key\": \"$cache_key\"}" >&2 | |
| echo "$body_out" | jq '.errorMessages? // .' >&2 || true | |
| return 5 | |
| fi | |
| if (( CACHE_TTL_SECONDS > 0 )); then | |
| cache_set "$cache_key" "$body_out" | |
| fi | |
| echo "$body_out" | |
| } | |
| # Function to display help | |
| show_help() { | |
| echo "π« JIRA Helper Script (version ${VERSION})" | |
| echo "=========================================" | |
| echo "Usage: $0 [global-flags] <command> [args]" | |
| echo "" | |
| echo "Commands:" | |
| echo " get <ISSUE> Fetch detailed issue (summary, description, comments, worklog, history)" | |
| echo " get-full <ISSUE> Same as 'get' but forces full (untruncated) description" | |
| echo " get-many ISSUE1 ISSUE2 ... Fetch multiple issues efficiently (max: $BULK_MAX_ISSUES)" | |
| echo " get-batch --file <file> Read issue keys from file and fetch them" | |
| echo " search '<JQL>' Search issues using JQL (JIRA Query Language)" | |
| echo " my-issues [MAX] Get your assigned unresolved issues (default: 50)" | |
| echo " project <KEY> [STATUS] [MAX] Get issues for project, optionally filtered by status" | |
| echo " recent [DAYS] [MAX] Get recently updated issues (default: 7 days, 50 results)" | |
| echo "" | |
| echo "Discovery & Metadata:" | |
| echo " projects List all accessible projects" | |
| echo " project-info <KEY> Get detailed project information" | |
| echo " statuses List all available issue statuses" | |
| echo " priorities List all available priorities" | |
| echo " issue-types List all available issue types" | |
| echo "" | |
| echo " help Show this help (alias: --help, -h)" | |
| echo "" | |
| echo "Global Flags:" | |
| echo " --json Output machine-readable JSON (same as env OUTPUT_FORMAT=json)" | |
| echo " --full Force full description output (or set FULL_DESCRIPTION=1)" | |
| echo " --fields <list> Comma-separated list of fields to fetch (reduces API load)" | |
| echo " --format <mode> Output format: detailed|compact|ai-summary (default: detailed)" | |
| echo " --out <file> Tee final output to <file>" | |
| echo " --cache-ttl <sec> Override cache TTL seconds (CACHE_TTL_SECONDS). 0 disables cache" | |
| echo " --no-cache Shortcut for --cache-ttl 0" | |
| echo " --fail-on-missing Exit non-zero (7) if issue not found / inaccessible" | |
| echo " --version Print version and exit" | |
| echo " --help | -h Show this help and exit" | |
| echo "" | |
| echo "Environment Variables (override defaults without editing script):" | |
| printf '%-20s %s\n' \ | |
| 'JIRA_BASE_URL' '(required) e.g. https://your-domain.atlassian.net' \ | |
| 'JIRA_EMAIL' '(required) Atlassian account email' \ | |
| 'JIRA_API_TOKEN' '(required) API token (never printed)' \ | |
| 'JIRA_ENV_FILE' "Env file path (default: ${SCRIPT_DIR}/.env.jira-mcp)" \ | |
| 'OUTPUT_FORMAT' 'text | json (default: text)' \ | |
| 'FULL_DESCRIPTION' '0 | 1 (default: 0)' \ | |
| 'CACHE_TTL_SECONDS' 'Cache lifetime (default: 300)' \ | |
| 'JIRA_CACHE_DIR' "Cache dir (default: ${SCRIPT_DIR}/.jira_cache)" \ | |
| 'FAIL_ON_MISSING' '0 | 1 (default: 0)' \ | |
| 'SEARCH_MAX_RESULTS' 'Default max results for search (default: 50)' \ | |
| 'SEARCH_START_AT' 'Default pagination start (default: 0)' \ | |
| 'BULK_MAX_ISSUES' 'Max issues for bulk operations (default: 20)' \ | |
| 'DEFAULT_FIELDS' 'Default field list for API calls' \ | |
| 'OUTPUT_FORMAT_MODE' 'detailed|compact|ai-summary (default: detailed)' \ | |
| 'CACHE_TTL_METADATA' 'Cache lifetime for metadata (default: 86400s/24h)' \ | |
| 'CACHE_TTL_SEARCH' 'Cache lifetime for searches (default: 600s/10m)' \ | |
| 'RATE_LIMIT_REQUESTS' 'Max API requests per minute (default: 30)' \ | |
| 'RATE_LIMIT_WINDOW' 'Rate limit time window (default: 60s)' | |
| echo "" | |
| echo "Runtime (current / effective):" | |
| echo " Env file: ${ENV_FILE:-'(unset)'}" | |
| echo " Output format: ${OUTPUT_FORMAT:-text}" | |
| echo " Full desc: ${FULL_DESCRIPTION:-0}" | |
| echo " Cache TTL: ${CACHE_TTL_SECONDS:-300}s" | |
| echo " Cache dir: ${JIRA_CACHE_DIR:-'(unset)'}" | |
| echo " Fail on missing: ${FAIL_ON_MISSING:-0}" | |
| echo " Search max: ${SEARCH_MAX_RESULTS:-50}" | |
| echo " Search start: ${SEARCH_START_AT:-0}" | |
| echo " Bulk max: ${BULK_MAX_ISSUES:-20}" | |
| echo " Format mode: ${OUTPUT_FORMAT_MODE:-detailed}" | |
| echo " Selected fields: ${SELECTED_FIELDS:-$DEFAULT_FIELDS}" | |
| echo " Metadata cache: ${CACHE_TTL_METADATA:-86400}s" | |
| echo " Search cache: ${CACHE_TTL_SEARCH:-600}s" | |
| echo " Rate limit: ${RATE_LIMIT_REQUESTS:-30} req/${RATE_LIMIT_WINDOW:-60}s" | |
| echo "" | |
| echo "Exit Codes:" | |
| printf ' %-3s %s\n' \ | |
| 0 'Success' \ | |
| 1 'Generic usage / unknown command' \ | |
| 2 'Env file missing (unless help/version)' \ | |
| 3 'Missing dependency (curl / jq)' \ | |
| 4 'Missing required env vars' \ | |
| 5 'HTTP error from JIRA API' \ | |
| 6 'Rate limit exceeded' \ | |
| 7 'Issue not found (with --fail-on-missing)' \ | |
| 11 'Missing file for --out' \ | |
| 12 'Missing seconds for --cache-ttl' \ | |
| 13 'Missing fields for --fields' \ | |
| 14 'Missing format for --format' \ | |
| 90 'Temporary file or jq processing failure' | |
| echo "" | |
| echo "JSON Output Contract:" | |
| echo " get/get-full: { key, summary, status, assignee, reporter, issueType, priority, created, updated," | |
| echo " project:{id,key,name}, labels[], components[], fixVersions[], description:{plain,adf}," | |
| echo " comments[], worklog[], history[], url }" | |
| echo " search/*: { returned, isLast, nextPageToken, issues:[{key, summary, status, assignee," | |
| echo " reporter, issueType, priority, created, updated, project:{id,key,name}, labels[]," | |
| echo " components[], fixVersions[], url}] }" | |
| echo "" | |
| echo "Examples:" | |
| echo " $0 get BRAVO-581" | |
| echo " $0 get-many BRAVO-581 BRAVO-582 BRAVO-583" | |
| echo " $0 get-batch --file issues.txt" | |
| echo " $0 search 'project = BRAVO AND status = \"In Progress\"'" | |
| echo " $0 --fields key,summary,status --format compact my-issues" | |
| echo " $0 --json --format ai-summary recent 1" | |
| echo " $0 projects" | |
| echo " $0 --format compact project-info BRAVO" | |
| echo " $0 --json statuses | jq 'map(.name)'" | |
| echo " $0 priorities" | |
| echo " $0 issue-types" | |
| echo " CACHE_TTL_METADATA=3600 $0 projects # Cache for 1 hour" | |
| echo "" | |
| echo "Tips:" | |
| echo " β’ Use caching for repeated queries; disable for fresh data during triage." | |
| echo " β’ JSON mode is stable for automated toolingβfield names are fixed." | |
| echo " β’ Set FULL_DESCRIPTION=1 to avoid truncation without changing scripts using 'get'." | |
| } | |
| show_version() { echo "jira_helper.sh version ${VERSION}"; } | |
| # Function to format issue details with comprehensive information | |
| format_issue() { | |
| local response="$1" | |
| local issue_key=$(echo "$response" | jq -r '.key') | |
| echo "π« Key: $issue_key" | |
| echo "π Summary: $(echo "$response" | jq -r '.fields.summary')" | |
| echo "π Status: $(echo "$response" | jq -r '.fields.status.name')" | |
| echo "π€ Assignee: $(echo "$response" | jq -r '.fields.assignee.displayName // "Unassigned"')" | |
| echo "π€ Reporter: $(echo "$response" | jq -r '.fields.reporter.displayName')" | |
| echo "π·οΈ Issue Type: $(echo "$response" | jq -r '.fields.issuetype.name')" | |
| echo "β‘ Priority: $(echo "$response" | jq -r '.fields.priority.name')" | |
| echo "π Created: $(echo "$response" | jq -r '.fields.created')" | |
| echo "π Updated: $(echo "$response" | jq -r '.fields.updated')" | |
| echo "π Project: $(echo "$response" | jq -r '.fields.project.name') ($(echo "$response" | jq -r '.fields.project.key'))" | |
| echo "π Project ID: $(echo "$response" | jq -r '.fields.project.id')" | |
| # Show labels if any | |
| local labels=$(echo "$response" | jq -r '.fields.labels[]?' | tr '\n' ', ' | sed 's/,$//') | |
| if [ ! -z "$labels" ]; then | |
| echo "π·οΈ Labels: $labels" | |
| fi | |
| # Show components if any | |
| local components=$(echo "$response" | jq -r '.fields.components[]?.name?' | tr '\n' ', ' | sed 's/,$//') | |
| if [ ! -z "$components" ]; then | |
| echo "π§© Components: $components" | |
| fi | |
| # Show fix versions if any | |
| local fix_versions=$(echo "$response" | jq -r '.fields.fixVersions[]?.name?' | tr '\n' ', ' | sed 's/,$//') | |
| if [ ! -z "$fix_versions" ]; then | |
| echo "π§ Fix Versions: $fix_versions" | |
| fi | |
| echo "" | |
| echo "π Description:" | |
| if [ "$FULL_DESCRIPTION" = "1" ]; then | |
| # Collect all text nodes from the Atlassian Document Format description | |
| local full_desc=$(echo "$response" | jq -r '.fields.description // empty | .content[]? | .content[]? | select(.text != null) | .text') | |
| if [ -z "$full_desc" ]; then | |
| echo "No description available" | |
| else | |
| echo "$full_desc" | sed 's/^[[:space:]]*$//' | |
| fi | |
| else | |
| # Default (backward compatible) truncated output | |
| echo "$(echo "$response" | jq -r '.fields.description.content[]?.content[]?.text // "No description available"' | head -5)" | |
| fi | |
| echo "" | |
| # Get and display comments | |
| echo "π¬ Comments:" | |
| get_issue_comments "$issue_key" | |
| echo "" | |
| # Get and display worklog | |
| echo "β° Work Log:" | |
| get_issue_worklog "$issue_key" | |
| echo "" | |
| # Get and display recent history | |
| echo "π Recent History:" | |
| get_issue_history "$issue_key" | |
| echo "" | |
| echo "π Link: ${JIRA_BASE_URL}/browse/$issue_key" | |
| echo "----------------------------------------" | |
| } | |
| # Fetch-only helpers (return raw JSON without printing) for JSON aggregation | |
| fetch_issue_comments() { | |
| local issue_key="$1" | |
| http_get_json "comments_$issue_key" "/rest/api/3/issue/$issue_key/comment" "orderBy=created" "issue" | |
| } | |
| fetch_issue_worklog() { | |
| local issue_key="$1" | |
| http_get_json "worklog_$issue_key" "/rest/api/3/issue/$issue_key/worklog" "" "issue" | |
| } | |
| fetch_issue_history() { | |
| local issue_key="$1" | |
| http_get_json "history_$issue_key" "/rest/api/3/issue/$issue_key/changelog" "" "issue" | |
| } | |
| # Produce combined JSON for a single issue (issue + comments + worklog + history) | |
| output_json_issue() { | |
| local issue_json="$1" | |
| local comments_json="$2" | |
| local worklog_json="$3" | |
| local history_json="$4" | |
| # Use temporary files + slurpfile to avoid hitting ARG_MAX with very large JSON payloads | |
| # (Observed: /usr/bin/jq: Argument list too long). This keeps invocation args small. | |
| local tmp_issue tmp_comments tmp_worklog tmp_history | |
| tmp_issue=$(mktemp) || { echo "Failed to create temp file" >&2; return 90; } | |
| tmp_comments=$(mktemp) || { echo "Failed to create temp file" >&2; return 90; } | |
| tmp_worklog=$(mktemp) || { echo "Failed to create temp file" >&2; return 90; } | |
| tmp_history=$(mktemp) || { echo "Failed to create temp file" >&2; return 90; } | |
| printf '%s' "$issue_json" > "$tmp_issue" | |
| printf '%s' "$comments_json" > "$tmp_comments" | |
| printf '%s' "$worklog_json" > "$tmp_worklog" | |
| printf '%s' "$history_json" > "$tmp_history" | |
| jq -n --arg baseUrl "${JIRA_BASE_URL}" \ | |
| --slurpfile issue "$tmp_issue" \ | |
| --slurpfile comments "$tmp_comments" \ | |
| --slurpfile worklog "$tmp_worklog" \ | |
| --slurpfile history "$tmp_history" ' | |
| { | |
| key: $issue[0].key, | |
| summary: $issue[0].fields.summary, | |
| status: $issue[0].fields.status.name, | |
| assignee: ($issue[0].fields.assignee.displayName // "Unassigned"), | |
| reporter: ($issue[0].fields.reporter.displayName // null), | |
| issueType: $issue[0].fields.issuetype.name, | |
| priority: $issue[0].fields.priority.name, | |
| created: $issue[0].fields.created, | |
| updated: $issue[0].fields.updated, | |
| project: { | |
| id: $issue[0].fields.project.id, | |
| key: $issue[0].fields.project.key, | |
| name: $issue[0].fields.project.name | |
| }, | |
| labels: ($issue[0].fields.labels // []), | |
| components: ($issue[0].fields.components // [] | map(.name)), | |
| fixVersions: ($issue[0].fields.fixVersions // [] | map(.name)), | |
| description: { | |
| plain: (($issue[0].fields.description // {content: []}) | [.content[]? | .content[]? | select(.text != null) | .text] | join("\n")), | |
| adf: ($issue[0].fields.description // null) | |
| }, | |
| comments: ( ($comments[0].comments // []) | map({ | |
| id, created, updated, author: (.author.displayName // null), | |
| bodyPlain: ([.body.content[]? | .content[]? | select(.text!=null) | .text] | join("\n")) | |
| }) ), | |
| worklog: ( ($worklog[0].worklogs // []) | map({ | |
| id, started, timeSpent, timeSpentSeconds, author: (.author.displayName // null), | |
| comment: ([.comment.content[]? | .content[]? | select(.text!=null) | .text] | join("\n")) | |
| }) ), | |
| history: ( ($history[0].values // []) | map({ | |
| id, created, author: (.author.displayName // null), | |
| items: ( .items // [] | map({ field, from, fromString, to, toString })) | |
| }) ), | |
| url: ($baseUrl + "/browse/" + $issue[0].key) | |
| } | |
| ' || { echo "jq processing failed" >&2; } | |
| # Cleanup temp files | |
| rm -f "$tmp_issue" "$tmp_comments" "$tmp_worklog" "$tmp_history" 2>/dev/null || true | |
| } | |
| # Function to get issue comments | |
| get_issue_comments() { | |
| local issue_key="$1" | |
| local comments_response | |
| comments_response=$(fetch_issue_comments "$issue_key" || echo '{}') | |
| if echo "$comments_response" | jq -e '.errorMessages' > /dev/null 2>&1; then | |
| echo " β Error retrieving comments" | |
| return | |
| fi | |
| local comment_count=$(echo "$comments_response" | jq -r '.total // 0') | |
| if [ "$comment_count" -eq 0 ]; then | |
| echo " π No comments" | |
| return | |
| fi | |
| echo " π Total Comments: $comment_count" | |
| echo "$comments_response" | jq -r '.comments[]? | " π€ \(.author.displayName) (\(.created | split("T")[0])): \(.body.content[]?.content[]?.text // "No content")"' | head -10 | |
| } | |
| # Function to get issue worklog | |
| get_issue_worklog() { | |
| local issue_key="$1" | |
| local worklog_response | |
| worklog_response=$(fetch_issue_worklog "$issue_key" || echo '{}') | |
| if echo "$worklog_response" | jq -e '.errorMessages' > /dev/null 2>&1; then | |
| echo " β Error retrieving worklog" | |
| return | |
| fi | |
| local worklog_count=$(echo "$worklog_response" | jq -r '.total // 0') | |
| if [ "$worklog_count" -eq 0 ]; then | |
| echo " π No work logged" | |
| return | |
| fi | |
| echo " π Total Work Entries: $worklog_count" | |
| echo "$worklog_response" | jq -r '.worklogs[]? | " β±οΈ \(.author.displayName) - \(.timeSpent) on \(.started | split("T")[0]): \(.comment.content[]?.content[]?.text // "No comment")"' | head -10 | |
| } | |
| # Function to get issue history | |
| get_issue_history() { | |
| local issue_key="$1" | |
| local changelog_response | |
| changelog_response=$(fetch_issue_history "$issue_key" || echo '{}') | |
| if echo "$changelog_response" | jq -e '.errorMessages' > /dev/null 2>&1; then | |
| echo " β Error retrieving history" | |
| return | |
| fi | |
| local history_count=$(echo "$changelog_response" | jq -r '.total // 0') | |
| if [ "$history_count" -eq 0 ]; then | |
| echo " π No history available" | |
| return | |
| fi | |
| echo " π Total History Entries: $history_count (showing latest 100)" | |
| echo "$changelog_response" | jq -r '.values[]? | " π \(.author.displayName) (\(.created | split("T")[0])): \(.items[]? | "\(.field): \(.fromString // "null") β \(.toString // "null")")"' | head -15 | |
| } | |
| # Function to format issue summary (for search results - less verbose) | |
| format_issue_summary() { | |
| local response="$1" | |
| local issue_key=$(echo "$response" | jq -r '.key') | |
| case "$OUTPUT_FORMAT_MODE" in | |
| compact) | |
| echo "$issue_key: $(echo "$response" | jq -r '.fields.summary // "No summary"') [$(echo "$response" | jq -r '.fields.status.name // "Unknown"')]" | |
| ;; | |
| ai-summary) | |
| # Minimal format optimized for AI processing - one line per issue | |
| local summary=$(echo "$response" | jq -r '.fields.summary // "No summary"') | |
| local status=$(echo "$response" | jq -r '.fields.status.name // "Unknown"') | |
| local assignee=$(echo "$response" | jq -r '.fields.assignee.displayName // "Unassigned"') | |
| local priority=$(echo "$response" | jq -r '.fields.priority.name // "Unknown"') | |
| echo "$issue_key|$summary|$status|$assignee|$priority" | |
| ;; | |
| *) | |
| # Default detailed format | |
| echo "π« $issue_key: $(echo "$response" | jq -r '.fields.summary // "No summary"')" | |
| if echo "$response" | jq -e '.fields.status' > /dev/null 2>&1; then | |
| echo " π Status: $(echo "$response" | jq -r '.fields.status.name') | π€ Assignee: $(echo "$response" | jq -r '.fields.assignee.displayName // "Unassigned"')" | |
| fi | |
| if echo "$response" | jq -e '.fields.priority' > /dev/null 2>&1; then | |
| echo " β‘ Priority: $(echo "$response" | jq -r '.fields.priority.name') | π Updated: $(echo "$response" | jq -r '.fields.updated | split("T")[0] // "Unknown"')" | |
| fi | |
| echo " π ${JIRA_BASE_URL}/browse/$issue_key" | |
| echo "" | |
| ;; | |
| esac | |
| } | |
| # Function to format JSON output based on available fields and format mode | |
| format_json_issue() { | |
| local response="$1" | |
| local base_url="$2" | |
| case "$OUTPUT_FORMAT_MODE" in | |
| compact) | |
| echo "$response" | jq --arg baseUrl "$base_url" '{ | |
| key, | |
| summary: (.fields.summary // null), | |
| status: (.fields.status.name // null), | |
| url: ($baseUrl + "/browse/" + .key) | |
| }' | |
| ;; | |
| ai-summary) | |
| echo "$response" | jq --arg baseUrl "$base_url" '{ | |
| key, | |
| summary: (.fields.summary // null), | |
| status: (.fields.status.name // null), | |
| assignee: (.fields.assignee.displayName // null), | |
| priority: (.fields.priority.name // null), | |
| updated: (.fields.updated // null) | |
| }' | |
| ;; | |
| *) | |
| # Detailed format - include all available fields | |
| echo "$response" | jq --arg baseUrl "$base_url" '{ | |
| key, | |
| summary: (.fields.summary // null), | |
| status: (.fields.status.name // null), | |
| assignee: (.fields.assignee.displayName // null), | |
| reporter: (.fields.reporter.displayName // null), | |
| issueType: (.fields.issuetype.name // null), | |
| priority: (.fields.priority.name // null), | |
| created: (.fields.created // null), | |
| updated: (.fields.updated // null), | |
| project: (if .fields.project then { | |
| id: .fields.project.id, | |
| key: .fields.project.key, | |
| name: .fields.project.name | |
| } else null end), | |
| labels: (.fields.labels // []), | |
| components: (.fields.components // [] | map(.name)), | |
| fixVersions: (.fields.fixVersions // [] | map(.name)), | |
| url: ($baseUrl + "/browse/" + .key) | |
| }' | |
| ;; | |
| esac | |
| } | |
| # Function to perform JQL search | |
| search_issues() { | |
| local jql="$1" | |
| local max_results="${2:-$SEARCH_MAX_RESULTS}" | |
| local start_at="${3:-$SEARCH_START_AT}" | |
| if [ -z "$jql" ]; then | |
| echo "β Error: Please provide a JQL query" | |
| echo "Usage: $0 search '<JQL_QUERY>'" | |
| echo "Example: $0 search 'project = BRAVO AND status = \"In Progress\"'" | |
| exit 1 | |
| fi | |
| if [ "$OUTPUT_FORMAT" = "text" ]; then | |
| echo "π Searching JIRA with JQL: $jql" | |
| echo "=========================================" | |
| fi | |
| # URL encode the JQL query | |
| local encoded_jql=$(printf '%s' "$jql" | jq -sRr @uri) | |
| local query_string="jql=${encoded_jql}&maxResults=${max_results}&startAt=${start_at}&fields=${SELECTED_FIELDS}" | |
| # Create a shorter cache key to avoid filesystem limits | |
| local cache_key_hash=$(echo "$jql$max_results$start_at" | sha256sum | cut -d' ' -f1 | head -c 16) | |
| local search_response | |
| if ! search_response=$(http_get_json "search_$cache_key_hash" "/rest/api/3/search/jql" "$query_string" "search"); then | |
| echo "β Search failed" >&2 | |
| return 5 | |
| fi | |
| if echo "$search_response" | jq -e '.errorMessages' > /dev/null 2>&1; then | |
| echo "β Search error: $(echo "$search_response" | jq -r '.errorMessages[]?' | head -1)" >&2 | |
| return 5 | |
| fi | |
| local returned=$(echo "$search_response" | jq -r '.issues | length') | |
| local is_last=$(echo "$search_response" | jq -r '.isLast // true') | |
| if [ "$OUTPUT_FORMAT" = "json" ]; then | |
| # Output structured JSON for AI consumption with format-specific optimization | |
| local formatted_issues="" | |
| if [ "$OUTPUT_FORMAT_MODE" = "ai-summary" ] || [ "$OUTPUT_FORMAT_MODE" = "compact" ]; then | |
| # Use optimized formatting for AI consumption | |
| formatted_issues=$(echo "$search_response" | jq -c '.issues[]' | while IFS= read -r issue; do | |
| format_json_issue "$issue" "${JIRA_BASE_URL}" | |
| done | jq -s '.') | |
| else | |
| # Use detailed formatting | |
| formatted_issues=$(echo "$search_response" | jq -c '.issues[]' | while IFS= read -r issue; do | |
| format_json_issue "$issue" "${JIRA_BASE_URL}" | |
| done | jq -s '.') | |
| fi | |
| echo "$search_response" | jq --argjson issues "$formatted_issues" '{ | |
| returned: (.issues | length), | |
| isLast: .isLast, | |
| nextPageToken: .nextPageToken, | |
| issues: $issues | |
| }' | |
| else | |
| if [ "$returned" -eq 0 ]; then | |
| echo "π No issues found matching the query" | |
| return 0 | |
| fi | |
| echo "β Found $returned issue(s):" | |
| echo "" | |
| echo "$search_response" | jq -c '.issues[]' | while IFS= read -r issue; do | |
| format_issue_summary "$issue" | |
| done | |
| if [ "$is_last" != "true" ]; then | |
| echo "π Showing $returned results (more pages available)" | |
| fi | |
| fi | |
| } | |
| # Function to get current user's assigned issues | |
| get_my_issues() { | |
| local jql="assignee = currentUser() AND resolution = Unresolved ORDER BY updated DESC" | |
| if [ "$OUTPUT_FORMAT" = "text" ]; then | |
| echo "π Getting your assigned issues..." | |
| echo "=========================================" | |
| fi | |
| search_issues "$jql" "${1:-$SEARCH_MAX_RESULTS}" | |
| } | |
| # Function to get issues for a specific project | |
| get_project_issues() { | |
| local project_key="$1" | |
| local status_filter="${2:-}" | |
| if [ -z "$project_key" ]; then | |
| echo "β Error: Please provide a project key" | |
| echo "Usage: $0 project PROJECT_KEY [STATUS]" | |
| echo "Example: $0 project BRAVO" | |
| echo "Example: $0 project BRAVO \"In Progress\"" | |
| exit 1 | |
| fi | |
| local jql="project = \"$project_key\"" | |
| if [ -n "$status_filter" ]; then | |
| jql="$jql AND status = \"$status_filter\"" | |
| fi | |
| jql="$jql ORDER BY updated DESC" | |
| if [ "$OUTPUT_FORMAT" = "text" ]; then | |
| echo "π Getting issues for project $project_key..." | |
| if [ -n "$status_filter" ]; then | |
| echo " Status filter: $status_filter" | |
| fi | |
| echo "=========================================" | |
| fi | |
| search_issues "$jql" "${3:-$SEARCH_MAX_RESULTS}" | |
| } | |
| # Function to get recently updated issues | |
| get_recent_issues() { | |
| local days="${1:-7}" | |
| local jql="updated >= -${days}d ORDER BY updated DESC" | |
| if [ "$OUTPUT_FORMAT" = "text" ]; then | |
| echo "π Getting issues updated in the last $days days..." | |
| echo "=========================================" | |
| fi | |
| search_issues "$jql" "${2:-$SEARCH_MAX_RESULTS}" | |
| } | |
| # Function to get multiple issues efficiently using JQL IN clause | |
| get_many_issues() { | |
| local issue_keys=("$@") | |
| if [ ${#issue_keys[@]} -eq 0 ]; then | |
| echo "β Error: Please provide at least one issue key" | |
| echo "Usage: $0 get-many ISSUE1 [ISSUE2] [ISSUE3] ..." | |
| echo "Example: $0 get-many BRAVO-581 BRAVO-582 BRAVO-583" | |
| exit 1 | |
| fi | |
| if [ ${#issue_keys[@]} -gt $BULK_MAX_ISSUES ]; then | |
| echo "β Error: Too many issues (${#issue_keys[@]}). Maximum allowed: $BULK_MAX_ISSUES" | |
| echo "Use get-batch for larger lists or increase BULK_MAX_ISSUES" | |
| exit 1 | |
| fi | |
| if [ "$OUTPUT_FORMAT" = "text" ]; then | |
| echo "π Fetching ${#issue_keys[@]} issues: ${issue_keys[*]}" | |
| echo "=========================================" | |
| fi | |
| # Build JQL query using IN clause for efficiency | |
| local jql_keys="" | |
| for key in "${issue_keys[@]}"; do | |
| if [ -n "$jql_keys" ]; then | |
| jql_keys="$jql_keys,$key" | |
| else | |
| jql_keys="$key" | |
| fi | |
| done | |
| local jql="key IN ($jql_keys) ORDER BY key" | |
| search_issues "$jql" "${#issue_keys[@]}" | |
| } | |
| # Function to get issues from a batch file | |
| get_batch_issues() { | |
| local batch_file="" | |
| local max_results="$BULK_MAX_ISSUES" | |
| # Parse arguments | |
| while [[ $# -gt 0 ]]; do | |
| case $1 in | |
| --file|-f) | |
| batch_file="$2" | |
| shift 2 | |
| ;; | |
| --max|-m) | |
| max_results="$2" | |
| shift 2 | |
| ;; | |
| *) | |
| echo "β Unknown argument: $1" | |
| echo "Usage: $0 get-batch --file <file> [--max <count>]" | |
| exit 1 | |
| ;; | |
| esac | |
| done | |
| if [ -z "$batch_file" ]; then | |
| echo "β Error: Please provide a batch file" | |
| echo "Usage: $0 get-batch --file <file> [--max <count>]" | |
| echo "Example: $0 get-batch --file issues.txt" | |
| exit 1 | |
| fi | |
| if [ ! -f "$batch_file" ]; then | |
| echo "β Error: Batch file not found: $batch_file" | |
| exit 1 | |
| fi | |
| # Read issue keys from file (one per line, ignore empty lines and comments) | |
| local issue_keys=() | |
| while IFS= read -r line || [ -n "$line" ]; do | |
| # Skip empty lines and comments | |
| if [[ -n "$line" && ! "$line" =~ ^[[:space:]]*# ]]; then | |
| # Trim whitespace | |
| line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') | |
| if [[ -n "$line" ]]; then | |
| issue_keys+=("$line") | |
| fi | |
| fi | |
| done < "$batch_file" | |
| if [ ${#issue_keys[@]} -eq 0 ]; then | |
| echo "β Error: No valid issue keys found in $batch_file" | |
| exit 1 | |
| fi | |
| if [ ${#issue_keys[@]} -gt "$max_results" ]; then | |
| echo "β οΈ Warning: Found ${#issue_keys[@]} issues, limiting to first $max_results" | |
| issue_keys=("${issue_keys[@]:0:$max_results}") | |
| fi | |
| if [ "$OUTPUT_FORMAT" = "text" ]; then | |
| echo "π Processing batch file: $batch_file" | |
| echo "π Fetching ${#issue_keys[@]} issues..." | |
| echo "=========================================" | |
| fi | |
| get_many_issues "${issue_keys[@]}" | |
| } | |
| # Function to list all accessible projects | |
| list_projects() { | |
| if [ "$OUTPUT_FORMAT" = "text" ]; then | |
| echo "π Listing accessible projects..." | |
| echo "=========================================" | |
| fi | |
| local projects_response | |
| if ! projects_response=$(http_get_json "projects_list" "/rest/api/3/project" "" "metadata"); then | |
| echo "β Failed to fetch projects" >&2 | |
| return 5 | |
| fi | |
| if echo "$projects_response" | jq -e '.errorMessages' > /dev/null 2>&1; then | |
| echo "β Error fetching projects: $(echo "$projects_response" | jq -r '.errorMessages[]?' | head -1)" >&2 | |
| return 5 | |
| fi | |
| local project_count=$(echo "$projects_response" | jq -r 'length') | |
| if [ "$OUTPUT_FORMAT" = "json" ]; then | |
| case "$OUTPUT_FORMAT_MODE" in | |
| compact) | |
| echo "$projects_response" | jq 'map({key, name, projectTypeKey})' | |
| ;; | |
| ai-summary) | |
| echo "$projects_response" | jq 'map({key, name})' | |
| ;; | |
| *) | |
| echo "$projects_response" | jq 'map({ | |
| id, key, name, description, | |
| projectTypeKey, style, lead: .lead.displayName, | |
| url: .self | |
| })' | |
| ;; | |
| esac | |
| else | |
| if [ "$project_count" -eq 0 ]; then | |
| echo "π No accessible projects found" | |
| return 0 | |
| fi | |
| echo "β Found $project_count accessible project(s):" | |
| echo "" | |
| case "$OUTPUT_FORMAT_MODE" in | |
| compact) | |
| echo "$projects_response" | jq -r '.[] | "\(.key): \(.name)"' | |
| ;; | |
| ai-summary) | |
| echo "$projects_response" | jq -r '.[] | "\(.key)|\(.name)"' | |
| ;; | |
| *) | |
| echo "$projects_response" | jq -r '.[] | "ποΈ \(.key): \(.name)\n π Type: \(.projectTypeKey) | π€ Lead: \(.lead.displayName // "Unknown")\n π \(.self)\n"' | |
| ;; | |
| esac | |
| fi | |
| } | |
| # Function to get detailed project information | |
| get_project_info() { | |
| local project_key="$1" | |
| if [ -z "$project_key" ]; then | |
| echo "β Error: Please provide a project key" | |
| echo "Usage: $0 project-info PROJECT_KEY" | |
| echo "Example: $0 project-info BRAVO" | |
| exit 1 | |
| fi | |
| if [ "$OUTPUT_FORMAT" = "text" ]; then | |
| echo "π Getting project information for $project_key..." | |
| echo "=========================================" | |
| fi | |
| local project_response | |
| if ! project_response=$(http_get_json "project_info_$project_key" "/rest/api/3/project/$project_key" "" "metadata"); then | |
| echo "β Failed to fetch project info for $project_key" >&2 | |
| return 5 | |
| fi | |
| if echo "$project_response" | jq -e '.errorMessages' > /dev/null 2>&1; then | |
| echo "β Project not found or inaccessible: $project_key" >&2 | |
| return 5 | |
| fi | |
| if [ "$OUTPUT_FORMAT" = "json" ]; then | |
| case "$OUTPUT_FORMAT_MODE" in | |
| compact) | |
| echo "$project_response" | jq '{key, name, projectTypeKey, issueTypes: [.issueTypes[].name]}' | |
| ;; | |
| ai-summary) | |
| echo "$project_response" | jq '{key, name, lead: .lead.displayName}' | |
| ;; | |
| *) | |
| echo "$project_response" | jq '{ | |
| id, key, name, description, | |
| projectTypeKey, style, | |
| lead: .lead.displayName, | |
| components: [.components[]?.name], | |
| versions: [.versions[]?.name], | |
| issueTypes: [.issueTypes[]?.name], | |
| url: .self | |
| }' | |
| ;; | |
| esac | |
| else | |
| echo "β Project found!" | |
| echo "" | |
| echo "ποΈ Key: $(echo "$project_response" | jq -r '.key')" | |
| echo "π Name: $(echo "$project_response" | jq -r '.name')" | |
| echo "π Description: $(echo "$project_response" | jq -r '.description // "No description"')" | |
| echo "π·οΈ Type: $(echo "$project_response" | jq -r '.projectTypeKey')" | |
| echo "π€ Lead: $(echo "$project_response" | jq -r '.lead.displayName // "Unknown"')" | |
| local components=$(echo "$project_response" | jq -r '.components[]?.name?' | tr '\n' ', ' | sed 's/,$//') | |
| if [ ! -z "$components" ]; then | |
| echo "π§© Components: $components" | |
| fi | |
| local versions=$(echo "$project_response" | jq -r '.versions[]?.name?' | tr '\n' ', ' | sed 's/,$//') | |
| if [ ! -z "$versions" ]; then | |
| echo "π Versions: $versions" | |
| fi | |
| local issue_types=$(echo "$project_response" | jq -r '.issueTypes[]?.name?' | tr '\n' ', ' | sed 's/,$//') | |
| if [ ! -z "$issue_types" ]; then | |
| echo "π Issue Types: $issue_types" | |
| fi | |
| echo "" | |
| echo "π Link: $(echo "$project_response" | jq -r '.self')" | |
| echo "----------------------------------------" | |
| fi | |
| } | |
| # Function to get system statuses | |
| get_statuses() { | |
| if [ "$OUTPUT_FORMAT" = "text" ]; then | |
| echo "π Getting issue statuses..." | |
| echo "=========================================" | |
| fi | |
| local statuses_response | |
| if ! statuses_response=$(http_get_json "statuses_list" "/rest/api/3/status" "" "metadata"); then | |
| echo "β Failed to fetch statuses" >&2 | |
| return 5 | |
| fi | |
| local status_count=$(echo "$statuses_response" | jq -r 'length') | |
| if [ "$OUTPUT_FORMAT" = "json" ]; then | |
| case "$OUTPUT_FORMAT_MODE" in | |
| compact|ai-summary) | |
| echo "$statuses_response" | jq 'map({id, name})' | |
| ;; | |
| *) | |
| echo "$statuses_response" | jq 'map({id, name, description, statusCategory: .statusCategory.name})' | |
| ;; | |
| esac | |
| else | |
| echo "β Found $status_count statuses:" | |
| echo "" | |
| case "$OUTPUT_FORMAT_MODE" in | |
| compact|ai-summary) | |
| echo "$statuses_response" | jq -r '.[] | "\(.name)"' | |
| ;; | |
| *) | |
| echo "$statuses_response" | jq -r '.[] | "π \(.name) (\(.statusCategory.name // "Unknown"))\n π \(.description // "No description")\n"' | |
| ;; | |
| esac | |
| fi | |
| } | |
| # Function to get system priorities | |
| get_priorities() { | |
| if [ "$OUTPUT_FORMAT" = "text" ]; then | |
| echo "π Getting issue priorities..." | |
| echo "=========================================" | |
| fi | |
| local priorities_response | |
| if ! priorities_response=$(http_get_json "priorities_list" "/rest/api/3/priority" "" "metadata"); then | |
| echo "β Failed to fetch priorities" >&2 | |
| return 5 | |
| fi | |
| local priority_count=$(echo "$priorities_response" | jq -r 'length') | |
| if [ "$OUTPUT_FORMAT" = "json" ]; then | |
| case "$OUTPUT_FORMAT_MODE" in | |
| compact|ai-summary) | |
| echo "$priorities_response" | jq 'map({id, name})' | |
| ;; | |
| *) | |
| echo "$priorities_response" | jq 'map({id, name, description})' | |
| ;; | |
| esac | |
| else | |
| echo "β Found $priority_count priorities:" | |
| echo "" | |
| case "$OUTPUT_FORMAT_MODE" in | |
| compact|ai-summary) | |
| echo "$priorities_response" | jq -r '.[] | "\(.name)"' | |
| ;; | |
| *) | |
| echo "$priorities_response" | jq -r '.[] | "β‘ \(.name)\n π \(.description // "No description")\n"' | |
| ;; | |
| esac | |
| fi | |
| } | |
| # Function to get issue types | |
| get_issue_types() { | |
| if [ "$OUTPUT_FORMAT" = "text" ]; then | |
| echo "π Getting issue types..." | |
| echo "=========================================" | |
| fi | |
| local types_response | |
| if ! types_response=$(http_get_json "issue_types_list" "/rest/api/3/issuetype" "" "metadata"); then | |
| echo "β Failed to fetch issue types" >&2 | |
| return 5 | |
| fi | |
| local type_count=$(echo "$types_response" | jq -r 'length') | |
| if [ "$OUTPUT_FORMAT" = "json" ]; then | |
| case "$OUTPUT_FORMAT_MODE" in | |
| compact|ai-summary) | |
| echo "$types_response" | jq 'map({id, name})' | |
| ;; | |
| *) | |
| echo "$types_response" | jq 'map({id, name, description, subtask})' | |
| ;; | |
| esac | |
| else | |
| echo "β Found $type_count issue types:" | |
| echo "" | |
| case "$OUTPUT_FORMAT_MODE" in | |
| compact|ai-summary) | |
| echo "$types_response" | jq -r '.[] | "\(.name)"' | |
| ;; | |
| *) | |
| echo "$types_response" | jq -r '.[] | "π \(.name) \(if .subtask then "(Subtask)" else "" end)\n π \(.description // "No description")\n"' | |
| ;; | |
| esac | |
| fi | |
| } | |
| # Function to get a specific ticket | |
| get_ticket() { | |
| local ticket_id="$1" | |
| if [ -z "$ticket_id" ]; then | |
| echo "β Error: Please provide a ticket ID" | |
| echo "Usage: $0 get TICKET-ID" | |
| exit 1 | |
| fi | |
| if [ "$OUTPUT_FORMAT" = "text" ]; then | |
| echo "π Querying JIRA ticket $ticket_id..." | |
| echo "=========================================" | |
| fi | |
| local raw | |
| if ! raw=$(http_get_json "issue_$ticket_id" "/rest/api/3/issue/$ticket_id" "expand=changelog" "issue"); then | |
| if [[ "$FAIL_ON_MISSING" == 1 ]]; then | |
| echo "β Ticket not found or inaccessible: $ticket_id" >&2 | |
| exit 7 | |
| fi | |
| raw='{}' | |
| fi | |
| response="$raw" | |
| if echo "$response" | jq -e '.errorMessages' > /dev/null 2>&1 || [[ $(echo "$response" | jq -r '.key // empty') == "" ]]; then | |
| if [[ "$FAIL_ON_MISSING" == 1 ]]; then | |
| echo "β Ticket not found or inaccessible: $ticket_id" >&2 | |
| exit 7 | |
| fi | |
| echo "β οΈ Ticket not found or inaccessible (empty response used)" >&2 | |
| return 0 | |
| fi | |
| if [ "$OUTPUT_FORMAT" = "json" ]; then | |
| local comments_json worklog_json history_json | |
| comments_json=$(fetch_issue_comments "$ticket_id" || echo '{}') | |
| worklog_json=$(fetch_issue_worklog "$ticket_id" || echo '{}') | |
| history_json=$(fetch_issue_history "$ticket_id" || echo '{}') | |
| output_json_issue "$response" "$comments_json" "$worklog_json" "$history_json" | |
| else | |
| echo "β Ticket found!" | |
| echo "" | |
| format_issue "$response" | |
| fi | |
| } | |
| # (Search functionality removed: search_tickets, search_tickets_migrated, get_my_issues) | |
| # Parse global flags (restored) | |
| SELECTED_FIELDS="$DEFAULT_FIELDS" | |
| new_args=() | |
| while (( "$#" )); do | |
| case "$1" in | |
| --json) OUTPUT_FORMAT=json; shift ;; | |
| --full) FULL_DESCRIPTION=1; shift ;; | |
| --out) OUT_FILE="${2:-}"; shift 2 || { echo "Missing file for --out" >&2; exit 11; } ;; | |
| --cache-ttl) CACHE_TTL_SECONDS="${2:-}"; shift 2 || { echo "Missing seconds for --cache-ttl" >&2; exit 12; } ;; | |
| --no-cache) CACHE_TTL_SECONDS=0; shift ;; | |
| --fail-on-missing) FAIL_ON_MISSING=1; shift ;; | |
| --fields) SELECTED_FIELDS="${2:-}"; shift 2 || { echo "Missing fields for --fields" >&2; exit 13; } ;; | |
| --format) OUTPUT_FORMAT_MODE="${2:-}"; shift 2 || { echo "Missing format for --format" >&2; exit 14; } ;; | |
| --help|-h) show_help; exit 0 ;; | |
| --version) show_version; exit 0 ;; | |
| --) shift; break ;; | |
| *) new_args+=("$1"); shift ;; | |
| esac | |
| done | |
| set -- "${new_args[@]}" "$@" | |
| # Main script logic | |
| run_command() { | |
| local cmd="${1:-}"; shift || true | |
| case "$cmd" in | |
| get) get_ticket "${1:-}" ;; | |
| get-full) FULL_DESCRIPTION=1 get_ticket "${1:-}" ;; | |
| get-many) get_many_issues "$@" ;; | |
| get-batch) get_batch_issues "$@" ;; | |
| search) search_issues "${1:-}" "${2:-}" "${3:-}" ;; | |
| my-issues) get_my_issues "${1:-}" ;; | |
| project) get_project_issues "${1:-}" "${2:-}" "${3:-}" ;; | |
| recent) get_recent_issues "${1:-}" "${2:-}" ;; | |
| projects) list_projects ;; | |
| project-info) get_project_info "${1:-}" ;; | |
| statuses) get_statuses ;; | |
| priorities) get_priorities ;; | |
| issue-types) get_issue_types ;; | |
| help|""|-h|--help) show_help ;; | |
| *) | |
| echo "β Unknown command: $cmd" >&2 | |
| show_help | |
| exit 1 ;; | |
| esac | |
| } | |
| execute_main() { | |
| if [[ -n "$OUT_FILE" ]]; then | |
| run_command "$@" | tee "$OUT_FILE" | |
| else | |
| run_command "$@" | |
| fi | |
| } | |
| execute_main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment