From 89dc22ac23c5f9734548ac6b964f2c3f9767e17c Mon Sep 17 00:00:00 2001 From: Travis Lyons Date: Fri, 7 Aug 2026 18:41:47 +0000 Subject: [PATCH] Configure SonarQube analysis in CI --- .github/scripts/resolve-sonar-project-key.sh | 77 ++++++++++++++++++++ .github/workflows/build.yml | 9 ++- build.gradle | 4 +- 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 .github/scripts/resolve-sonar-project-key.sh diff --git a/.github/scripts/resolve-sonar-project-key.sh b/.github/scripts/resolve-sonar-project-key.sh new file mode 100644 index 0000000..eaca0f0 --- /dev/null +++ b/.github/scripts/resolve-sonar-project-key.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# +# Prints this repository's SonarQube project key to stdout. +# +# Projects imported through the SonarQube GitHub App are assigned a generated +# key of the form "__". The uuid cannot be derived from any +# GitHub context, so the key is looked up through the SonarQube API instead of +# being assumed from a naming convention. +# +# On any problem this prints nothing and exits 0. Callers must treat an empty +# result as "skip analysis": letting the scanner run with a guessed key would +# silently create a second, unbound project instead of reporting an error. +set -uo pipefail + +log() { printf '%s\n' "$*" >&2; } + +if [ -z "${SONAR_HOST_URL:-}" ]; then + log "SONAR_HOST_URL is not set." + exit 0 +fi + +if [ -z "${SONAR_TOKEN:-}" ]; then + log "SONAR_TOKEN is not set." + exit 0 +fi + +case "$SONAR_HOST_URL" in + http://*|https://*) ;; + *) + log "SONAR_HOST_URL must include a scheme, e.g. https://sonarqube.example.com" + exit 0 + ;; +esac + +HOST="${SONAR_HOST_URL%/}" +OWNER="${GITHUB_REPOSITORY%%/*}" +REPO="${GITHUB_REPOSITORY#*/}" +PREFIX="${OWNER}_${REPO}" + +RESPONSE="$(curl -sS -u "${SONAR_TOKEN}:" --get \ + --data-urlencode "q=${REPO}" \ + --data-urlencode "qualifiers=TRK" \ + --data-urlencode "ps=500" \ + "${HOST}/api/components/search")" || { + log "Could not query ${HOST}/api/components/search." + exit 0 + } + +if printf '%s' "$RESPONSE" | jq -e 'has("errors")' >/dev/null 2>&1; then + log "SonarQube returned an error: $(printf '%s' "$RESPONSE" | jq -r '[.errors[].msg] | join("; ")')" + exit 0 +fi + +# Preference order: exact "_", then "__" as created +# by the GitHub App import, then a project whose display name is the repo name, +# then a bare "" key. +KEY="$(printf '%s' "$RESPONSE" | jq -r --arg prefix "$PREFIX" --arg repo "$REPO" ' + [.components[]?] as $c + | ( + [ $c[] | select(.key == $prefix) | .key ] + + [ $c[] + | select((.key | startswith($prefix + "_")) + and ((.key | length) == (($prefix | length) + 37))) + | .key ] + + [ $c[] | select(.name == $repo) | .key ] + + [ $c[] | select(.key == $repo) | .key ] + ) + | first // empty +' 2>/dev/null)" + +if [ -z "$KEY" ]; then + log "No SonarQube project matching '${REPO}' was found. Has it been imported?" + exit 0 +fi + +log "Resolved SonarQube project key: ${KEY}" +printf '%s\n' "$KEY" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1413c06..215764d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,4 +46,11 @@ jobs: env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} - run: ./gradlew sonar --info + run: | + SONAR_PROJECT_KEY="$(bash .github/scripts/resolve-sonar-project-key.sh)" + if [ -z "$SONAR_PROJECT_KEY" ]; then + echo "Skipping analysis: no bound SonarQube project resolved." + exit 0 + fi + export SONAR_PROJECT_KEY + ./gradlew sonar --info diff --git a/build.gradle b/build.gradle index 7b92be2..e892f93 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,9 @@ plugins { sonar { properties { - property "sonar.projectKey", "deprecated-java-api-demo-2" + // The SonarQube GitHub App gives imported projects a generated key, so CI + // looks the real key up and passes it in through SONAR_PROJECT_KEY. + property "sonar.projectKey", System.getenv("SONAR_PROJECT_KEY") ?: "deprecated-java-api-demo-2" property "sonar.projectName", "deprecated-java-api-demo-2" } }