| 1 | #!/usr/bin/env bash |
| 2 | # Fetch SonarCloud findings introduced by a specific pull request. |
| 3 | # |
| 4 | # Usage: |
| 5 | # fetch-sonar-findings.sh <pr-number> |
| 6 | # |
| 7 | # SonarCloud does NOT post per-finding inline comments on the GitHub PR -- |
| 8 | # only a QualityGate summary comment is delivered. The actual issue list |
| 9 | # lives behind /api/issues/search?pullRequest=<N> and /api/hotspots/search. |
| 10 | # This script pulls both, so the PR-reviews loop can address them. |
| 11 | # |
| 12 | # Outputs (under .local/audits/pr-reviews/pr-<N>/): |
| 13 | # sonar-issues.json -- all open issues this PR introduced |
| 14 | # sonar-hotspots.json -- all open security hotspots this PR introduced |
| 15 | # |
| 16 | # Reads SONAR_TOKEN, SONAR_HOST_URL, SONAR_PROJECT from <repo-root>/.env -- |
| 17 | # the same .env entries the sonarqube-audit skill uses. If they're missing, |
| 18 | # this script prints what's needed and exits 1. |
| 19 | # |
| 20 | # Sourcing strategy: this script is part of pr-reviews, but it leans on |
| 21 | # the sonarqube-audit skill's `_lib.sh` helpers (sq_load_env, sq_paginate) |
| 22 | # so we get a single paginator + token-masking implementation, rather |
| 23 | # than duplicating the loop and risking drift. |
| 24 | |
| 25 | set -euo pipefail |
| 26 | |
| 27 | # shellcheck source=./_lib.sh |
| 28 | # shellcheck disable=SC1091 |
| 29 | source "$(dirname "$0")/_lib.sh" |
| 30 | |
| 31 | # shellcheck disable=SC1091 |
| 32 | source "$(dirname "$0")/../../sonarqube-audit/scripts/_lib.sh" |
| 33 | |
| 34 | PR="${1:?usage: $0 <pr-number>}" |
| 35 | pr_require_numeric "${PR}" |
| 36 | |
| 37 | # sq_load_env reads <repo-root>/.env; same .env the pr-reviews scripts use. |
| 38 | sq_load_env |
| 39 | |
| 40 | DIR="$(pr_state_dir "${PR}")" |
| 41 | |
| 42 | echo -e "${PR_GRAY}[fetch-sonar] PR ${PR} -> ${DIR}${PR_NC}" >&2 |
| 43 | |
| 44 | # Build per-source path. sq_paginate streams one JSON page per line; jq -s |
| 45 | # slurps them and concatenates the array under each result key. |
| 46 | sq_paginate "/api/issues/search?componentKeys=${SONAR_PROJECT}&pullRequest=${PR}&resolved=false" \ |
| 47 | | jq -s '[.[].issues[]]' > "${DIR}/sonar-issues.json" |
| 48 | |
| 49 | sq_paginate "/api/hotspots/search?projectKey=${SONAR_PROJECT}&pullRequest=${PR}&status=TO_REVIEW" \ |
| 50 | | jq -s '[.[].hotspots[]]' > "${DIR}/sonar-hotspots.json" |
| 51 | |
| 52 | # Summary |
| 53 | n_issues="$(jq 'length' "${DIR}/sonar-issues.json")" |
| 54 | n_hotspots="$(jq 'length' "${DIR}/sonar-hotspots.json")" |
| 55 | |
| 56 | echo |
| 57 | echo "SonarCloud findings on PR ${PR}:" |
| 58 | echo " issues: ${n_issues}" |
| 59 | echo " hotspots: ${n_hotspots}" |
| 60 | echo |
| 61 | if (( n_issues > 0 )); then |
| 62 | echo "Issues by severity / rule:" |
| 63 | jq -r 'group_by(.severity + " " + .rule) | .[] | " \(.[0].severity) \(.[0].rule) x\(length)"' \ |
| 64 | "${DIR}/sonar-issues.json" |
| 65 | echo |
| 66 | echo "Issue details:" |
| 67 | jq -r '.[] | " \(.key) \(.severity) \(.rule) \(.component | sub("^[^:]+:"; ""))\(if .line then ":" + (.line | tostring) else "" end)\n \(.message)"' \ |
| 68 | "${DIR}/sonar-issues.json" |
| 69 | fi |
| 70 | if (( n_hotspots > 0 )); then |
| 71 | echo |
| 72 | echo "Hotspots:" |
| 73 | jq -r '.[] | " \(.key) \(.vulnerabilityProbability) \(.ruleKey) \(.component | sub("^[^:]+:"; ""))\(if .line then ":" + (.line | tostring) else "" end)\n \(.message)"' \ |
| 74 | "${DIR}/sonar-hotspots.json" |
| 75 | fi |