#!/usr/bin/env bash # x-sonarcloud - Fetch SonarCloud issues for LLM analysis # Copyright (c) 2025 - Licensed under MIT # # Usage: # x-sonarcloud # Auto-detect, all open issues # x-sonarcloud --pr # PR-specific issues # x-sonarcloud --branch # Branch-specific issues # x-sonarcloud --org --project-key # Explicit project # x-sonarcloud --severities BLOCKER,CRITICAL # Filter by severity # x-sonarcloud --types BUG,VULNERABILITY # Filter by type # x-sonarcloud --statuses OPEN,CONFIRMED # Filter by status # x-sonarcloud --resolved # Include resolved issues # x-sonarcloud -h|--help # Show this help # # Examples: # x-sonarcloud # All open issues in project # x-sonarcloud --pr 42 # Issues on PR #42 # x-sonarcloud --branch main # Issues on main branch # x-sonarcloud --severities BLOCKER --types BUG # Only blocker bugs # # Requirements: # - curl and jq installed # - SONAR_TOKEN environment variable set # - sonar-project.properties or .sonarlint/connectedMode.json for auto-detection set -euo pipefail # Colors for output (stderr only) readonly RED='\033[0;31m' readonly GREEN='\033[0;32m' readonly YELLOW='\033[1;33m' readonly BLUE='\033[0;34m' readonly NC='\033[0m' # No Color # API constants readonly MAX_PAGE_SIZE=500 readonly MAX_TOTAL_ISSUES=10000 # Show usage information show_usage() { sed -n '3,27p' "$0" | sed 's/^# //' | sed 's/^#//' } # Log functions log_error() { echo -e "${RED}ERROR:${NC} $1" >&2 } log_warn() { echo -e "${YELLOW}WARN:${NC} $1" >&2 } log_info() { if [[ "${INFO:-0}" == "1" ]]; then echo -e "${GREEN}INFO:${NC} $1" >&2 fi } log_debug() { if [[ "${DEBUG:-0}" == "1" ]]; then echo -e "${BLUE}DEBUG:${NC} $1" >&2 fi } # Check required dependencies check_dependencies() { local missing=0 if ! command -v curl &> /dev/null; then log_error "curl is not installed. Install it with your package manager." missing=1 fi if ! command -v jq &> /dev/null; then log_error "jq is not installed. Install it with your package manager:" log_error " https://jqlang.github.io/jq/download/" missing=1 fi if [[ "$missing" -eq 1 ]]; then exit 1 fi } # Check authentication check_auth() { if [[ -z "${SONAR_TOKEN:-}" ]]; then log_error "SONAR_TOKEN environment variable is not set." log_error "Generate a token at: https://sonarcloud.io/account/security" log_error "Then export it: export SONAR_TOKEN=your_token_here" exit 1 fi } # Detect project from sonar-project.properties detect_project_from_properties() { local props_file="sonar-project.properties" if [[ ! -f "$props_file" ]]; then return 1 fi local org key org=$(grep -E '^sonar\.organization=' "$props_file" 2> /dev/null | cut -d'=' -f2- || echo "") key=$(grep -E '^sonar\.projectKey=' "$props_file" 2> /dev/null | cut -d'=' -f2- || echo "") if [[ -n "$org" && -n "$key" ]]; then log_debug "Detected from sonar-project.properties: org=$org key=$key" echo "$org" "$key" "" return 0 fi return 1 } # Detect project from .sonarlint/connectedMode.json detect_project_from_sonarlint() { local sonarlint_file=".sonarlint/connectedMode.json" if [[ ! -f "$sonarlint_file" ]]; then return 1 fi local org key region org=$(jq -r '.sonarCloudOrganization // empty' "$sonarlint_file" 2> /dev/null || echo "") key=$(jq -r '.projectKey // empty' "$sonarlint_file" 2> /dev/null || echo "") region=$(jq -r '.region // empty' "$sonarlint_file" 2> /dev/null || echo "") if [[ -n "$org" && -n "$key" ]]; then log_debug "Detected from .sonarlint/connectedMode.json: org=$org key=$key region=$region" echo "$org" "$key" "$region" return 0 fi return 1 } # Orchestrate project detection in priority order detect_project() { local result # 1. sonar-project.properties if result=$(detect_project_from_properties); then echo "$result" return 0 fi # 2. .sonarlint/connectedMode.json if result=$(detect_project_from_sonarlint); then echo "$result" return 0 fi # No config found log_error "Could not auto-detect SonarCloud project configuration." log_error "Provide one of the following:" log_error " 1. sonar-project.properties with sonar.organization and sonar.projectKey" log_error " 2. .sonarlint/connectedMode.json with sonarCloudOrganization and projectKey" log_error " 3. CLI flags: --org --project-key " return 1 } # Get API base URL (currently same for all regions) get_base_url() { echo "https://sonarcloud.io" } # Make an authenticated SonarCloud API request sonar_api_request() { local url="$1" log_debug "API request: $url" local http_code body response response=$(curl -s -w "\n%{http_code}" \ -H "Authorization: Bearer $SONAR_TOKEN" \ "$url" 2> /dev/null) || { log_error "curl request failed for: $url" return 1 } http_code=$(echo "$response" | tail -n1) body=$(echo "$response" | sed '$d') case "$http_code" in 200) echo "$body" return 0 ;; 401) log_error "Authentication failed (HTTP 401). Check your SONAR_TOKEN." return 1 ;; 403) log_error "Access forbidden (HTTP 403). Token may lack required permissions." return 1 ;; 404) log_error "Not found (HTTP 404). Check organization and project key." return 1 ;; 429) log_error "Rate limited (HTTP 429). Wait before retrying." return 1 ;; *) log_error "API request failed with HTTP $http_code" log_debug "Response body: $body" return 1 ;; esac } # Fetch a single page of issues fetch_issues_page() { local base_url="$1" local project_key="$2" local page="$3" local pr_number="${4:-}" local branch="${5:-}" local severities="${6:-}" local types="${7:-}" local statuses="${8:-}" local resolved="${9:-}" local url="${base_url}/api/issues/search?componentKeys=${project_key}" url="${url}&p=${page}&ps=${MAX_PAGE_SIZE}" if [[ -n "$pr_number" ]]; then url="${url}&pullRequest=${pr_number}" fi if [[ -n "$branch" ]]; then url="${url}&branch=${branch}" fi if [[ -n "$severities" ]]; then url="${url}&severities=${severities}" fi if [[ -n "$types" ]]; then url="${url}&types=${types}" fi if [[ -n "$statuses" ]]; then url="${url}&statuses=${statuses}" fi if [[ -n "$resolved" ]]; then url="${url}&resolved=${resolved}" fi sonar_api_request "$url" } # Fetch all issues with pagination fetch_all_issues() { local base_url="$1" local project_key="$2" local pr_number="${3:-}" local branch="${4:-}" local severities="${5:-}" local types="${6:-}" local statuses="${7:-}" local resolved="${8:-}" local page=1 local all_issues="[]" local total=0 while true; do log_info "Fetching issues page $page..." local response response=$(fetch_issues_page "$base_url" "$project_key" "$page" \ "$pr_number" "$branch" "$severities" "$types" "$statuses" "$resolved") || return 1 local page_issues page_total page_issues=$(echo "$response" | jq '.issues // []' 2> /dev/null || echo "[]") page_total=$(echo "$response" | jq '.total // 0' 2> /dev/null || echo "0") local page_count page_count=$(echo "$page_issues" | jq 'length' 2> /dev/null || echo "0") log_debug "Page $page: $page_count issues (total available: $page_total)" # Merge into accumulated results all_issues=$(echo "$all_issues" "$page_issues" | jq -s '.[0] + .[1]' 2> /dev/null || echo "$all_issues") total=$(echo "$all_issues" | jq 'length' 2> /dev/null || echo "0") # Check if we have all issues or hit the cap if [[ "$page_count" -lt "$MAX_PAGE_SIZE" ]]; then break fi if [[ "$total" -ge "$MAX_TOTAL_ISSUES" ]]; then log_warn "Reached maximum of $MAX_TOTAL_ISSUES issues. Results may be incomplete." break fi page=$((page + 1)) done log_info "Fetched $total issues total" echo "$all_issues" } # Format issues grouped by severity then by file format_issues_by_severity() { local issues="$1" local base_url="$2" local org="$3" local project_key="$4" echo "$issues" | jq -r --arg base_url "$base_url" --arg org "$org" --arg key "$project_key" ' group_by(.severity) | sort_by(-( if .[0].severity == "BLOCKER" then 5 elif .[0].severity == "CRITICAL" then 4 elif .[0].severity == "MAJOR" then 3 elif .[0].severity == "MINOR" then 2 elif .[0].severity == "INFO" then 1 else 0 end )) | .[] | "### Severity: \(.[0].severity)\n" + ( group_by(.component) | .[] | "#### File: \(.[0].component | split(":") | if length > 1 then .[1:] | join(":") else .[0] end)\n" + ( [.[] | "##### Issue: \(.message)\n" + "- **Rule:** \(.rule)\n" + "- **Type:** \(.type)\n" + "- **Severity:** \(.severity)\n" + "- **Status:** \(.status)\n" + "- **Line:** \(.line // "N/A")\n" + "- **Effort:** \(.effort // "N/A")\n" + "- **Created:** \(.creationDate // "N/A")\n" + "- **URL:** \($base_url)/project/issues?open=\(.key)&id=\($key)\n" ] | join("\n") ) ) ' 2> /dev/null || echo "Error formatting issues." } # Format summary counts format_summary() { local issues="$1" echo "### By Severity" echo "" echo "$issues" | jq -r ' group_by(.severity) | .[] | "- **\(.[0].severity):** \(length)" ' 2> /dev/null || echo "- Error computing severity counts" echo "" echo "### By Type" echo "" echo "$issues" | jq -r ' group_by(.type) | .[] | "- **\(.[0].type):** \(length)" ' 2> /dev/null || echo "- Error computing type counts" echo "" echo "### Total" echo "" local count count=$(echo "$issues" | jq 'length' 2> /dev/null || echo "0") echo "- **Total issues:** $count" } # Format the full markdown output format_output() { local org="$1" local project_key="$2" local mode="$3" local mode_value="$4" local base_url="$5" local issues="$6" local issue_count issue_count=$(echo "$issues" | jq 'length' 2> /dev/null || echo "0") # Header and LLM instructions cat << 'EOF' # SonarCloud Issues Analysis Report ## LLM Processing Instructions You are analyzing code quality issues from SonarCloud for this project. **Your tasks:** 1. **Triage**: Review each issue and assess its real impact on the codebase 2. **Priority Assessment**: Rank issues by severity and likelihood of causing problems 3. **Code Verification**: Check the actual source code to confirm each issue is valid 4. **Root Cause Analysis**: Identify why the issue exists and what pattern caused it 5. **Implementation Plan**: Create actionable fix tasks grouped by file for efficiency 6. **False Positive Detection**: Flag issues that appear to be false positives with reasoning **Tools to use:** - `find`, `cat`, `rg` commands and available tools to examine current codebase - `git log` and `git blame` to understand code history and authorship - File system tools to verify mentioned files exist and check current state EOF # Project information cat << EOF ## Project Information - **Organization:** $org - **Project Key:** $project_key EOF case "$mode" in pr) echo "- **Mode:** Pull Request #$mode_value" echo "- **URL:** ${base_url}/project/issues?pullRequest=${mode_value}&id=${project_key}" ;; branch) echo "- **Mode:** Branch \`$mode_value\`" echo "- **URL:** ${base_url}/project/issues?branch=${mode_value}&id=${project_key}" ;; *) echo "- **Mode:** Project (all open issues)" echo "- **URL:** ${base_url}/project/issues?id=${project_key}" ;; esac echo "- **Dashboard:** ${base_url}/project/overview?id=${project_key}" # Issues section echo "" echo "## Issues ($issue_count total)" echo "" if [[ "$issue_count" -eq 0 ]]; then echo "No issues found matching the specified filters." else format_issues_by_severity "$issues" "$base_url" "$org" "$project_key" echo "" echo "## Summary" echo "" format_summary "$issues" fi # Footer cat << 'EOF' ## Next Steps for LLM Analysis 1. **Validate against current code:** - Check if mentioned files and lines still match the reported issues - Verify issues are not already fixed in the current branch - Identify false positives and explain why they are false positives 2. **Prioritize fixes:** - Address BLOCKER and CRITICAL severity issues first - Group fixes by file to minimize context switching - Consider effort estimates when planning the fix order 3. **Group by file for implementation:** - Batch changes to the same file together - Consider dependencies between fixes - Plan atomic commits per logical change group 4. **Track progress:** - Use todo lists and memory tools to track which issues are addressed - Mark false positives with clear reasoning - Verify fixes do not introduce new issues EOF } # Main pipeline: fetch and display issues fetch_and_display_issues() { local org="$1" local project_key="$2" local mode="$3" local mode_value="$4" local severities="${5:-}" local types="${6:-}" local statuses="${7:-}" local resolved="${8:-}" local base_url base_url=$(get_base_url) local pr_number="" local branch="" case "$mode" in pr) pr_number="$mode_value" ;; branch) branch="$mode_value" ;; esac log_info "Fetching SonarCloud issues for $project_key (mode: $mode)..." local issues issues=$(fetch_all_issues "$base_url" "$project_key" \ "$pr_number" "$branch" "$severities" "$types" "$statuses" "$resolved") || { log_error "Failed to fetch issues" return 1 } format_output "$org" "$project_key" "$mode" "$mode_value" "$base_url" "$issues" } # Main function main() { local org="" local project_key="" local mode="project" local mode_value="" local severities="" local types="" local statuses="OPEN,CONFIRMED,REOPENED" local resolved="false" # Parse arguments while [[ $# -gt 0 ]]; do case "$1" in -h | --help) show_usage exit 0 ;; --pr) mode="pr" mode_value="${2:?Missing PR number after --pr}" shift 2 ;; --branch) mode="branch" mode_value="${2:?Missing branch name after --branch}" shift 2 ;; --org) org="${2:?Missing organization after --org}" shift 2 ;; --project-key) project_key="${2:?Missing project key after --project-key}" shift 2 ;; --severities) severities="${2:?Missing severities after --severities}" shift 2 ;; --types) types="${2:?Missing types after --types}" shift 2 ;; --statuses) statuses="${2:?Missing statuses after --statuses}" shift 2 ;; --resolved) resolved="true" statuses="" shift ;; *) log_error "Unknown argument: $1" show_usage exit 1 ;; esac done check_dependencies check_auth # Auto-detect project if not specified via CLI if [[ -z "$org" || -z "$project_key" ]]; then local detected detected=$(detect_project) || exit 1 # shellcheck disable=SC2034 # region reserved for future per-region base URLs read -r detected_org detected_key detected_region <<< "$detected" if [[ -z "$org" ]]; then org="$detected_org" fi if [[ -z "$project_key" ]]; then project_key="$detected_key" fi fi log_debug "Organization: $org" log_debug "Project Key: $project_key" log_debug "Mode: $mode" log_debug "Severities: ${severities:-all}" log_debug "Types: ${types:-all}" log_debug "Statuses: ${statuses:-all}" log_debug "Resolved: $resolved" fetch_and_display_issues "$org" "$project_key" "$mode" "$mode_value" \ "$severities" "$types" "$statuses" "$resolved" } # Run main function with all arguments main "$@"