diff --git a/local/bin/x-sonarcloud b/local/bin/x-sonarcloud new file mode 100755 index 0000000..5920fae --- /dev/null +++ b/local/bin/x-sonarcloud @@ -0,0 +1,623 @@ +#!/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 "$@" diff --git a/local/bin/x-sonarcloud.md b/local/bin/x-sonarcloud.md new file mode 100644 index 0000000..ec6d8c3 --- /dev/null +++ b/local/bin/x-sonarcloud.md @@ -0,0 +1,46 @@ +# x-sonarcloud + +--- + +## Usage + +```bash +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 help +``` + +Fetches SonarCloud code quality issues via REST API and formats them as +structured markdown with LLM processing instructions for automated analysis +and triage. + +## Examples + +```bash +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 + (generate at ) +- Project auto-detection via `sonar-project.properties` or + `.sonarlint/connectedMode.json`, or explicit `--org`/`--project-key` flags + +## Environment Variables + +- `SONAR_TOKEN` — Bearer token for SonarCloud API authentication (required) +- `INFO=1` — Enable informational log messages on stderr +- `DEBUG=1` — Enable debug log messages on stderr + +