1+ name : Code Analysis
2+ on :
3+ workflow_run :
4+ workflows : ["Build and Test"]
5+ types : [completed]
6+
7+ permissions :
8+ checks : write
9+ pull-requests : write
10+ actions : read
11+
12+ jobs :
13+ sonar :
14+ name : SonarQube Analysis
15+ runs-on : ubuntu-latest
16+ if : >-
17+ github.event.workflow_run.conclusion == 'success' &&
18+ (github.event.workflow_run.event == 'push' || github.event.workflow_run.event == 'pull_request')
19+ steps :
20+ - uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
21+ with :
22+ repository : ${{ github.event.workflow_run.head_repository.full_name }}
23+ ref : ${{ github.event.workflow_run.head_sha }}
24+ fetch-depth : 0
25+
26+ - name : Set up JDK 17
27+ uses : actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654
28+ with :
29+ java-version : ' 17'
30+ distribution : ' zulu'
31+ cache : ' gradle'
32+
33+ - name : Cache SonarQube packages
34+ uses : actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
35+ with :
36+ path : ~/.sonar/cache
37+ key : ${{ runner.os }}-sonar
38+ restore-keys : ${{ runner.os }}-sonar
39+
40+ - name : Setup Gradle
41+ uses : gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
42+
43+ - name : Download coverage report
44+ uses : actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
45+ with :
46+ name : coverage-report
47+ path : build/reports/kover
48+ run-id : ${{ github.event.workflow_run.id }}
49+ github-token : ${{ secrets.GITHUB_TOKEN }}
50+
51+ - name : Download test results
52+ uses : actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
53+ with :
54+ name : junit-test-results
55+ path : build/test-results
56+ run-id : ${{ github.event.workflow_run.id }}
57+ github-token : ${{ secrets.GITHUB_TOKEN }}
58+
59+ - name : Resolve PR details
60+ id : pr
61+ if : github.event.workflow_run.event == 'pull_request'
62+ env :
63+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
64+ run : |
65+ # workflow_run.pull_requests can be empty for fork PRs, fall back to API search
66+ PR_NUMBER="${{ github.event.workflow_run.pull_requests[0].number }}"
67+ if [ -z "$PR_NUMBER" ]; then
68+ PR_NUMBER=$(gh pr list --search "${{ github.event.workflow_run.head_sha }}" --state open --json number --jq '.[0].number // empty')
69+ fi
70+ if [ -z "$PR_NUMBER" ]; then
71+ echo "skip=true" >> "$GITHUB_OUTPUT"
72+ echo "Could not resolve PR number, skipping PR-specific analysis"
73+ else
74+ echo "skip=false" >> "$GITHUB_OUTPUT"
75+ echo "number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
76+ echo "branch=${{ github.event.workflow_run.head_branch }}" >> "$GITHUB_OUTPUT"
77+ echo "base=${{ github.event.workflow_run.pull_requests[0].base.ref || 'main' }}" >> "$GITHUB_OUTPUT"
78+ fi
79+
80+ - name : Run SonarQube Analysis (PR)
81+ if : github.event.workflow_run.event == 'pull_request' && steps.pr.outputs.skip != 'true'
82+ env :
83+ SONAR_TOKEN : ${{ secrets.SONAR_TOKEN }}
84+ run : >-
85+ ./gradlew sonar
86+ -Dsonar.pullrequest.key=${{ steps.pr.outputs.number }}
87+ -Dsonar.pullrequest.branch=${{ steps.pr.outputs.branch }}
88+ -Dsonar.pullrequest.base=${{ steps.pr.outputs.base }}
89+
90+ - name : Run SonarQube Analysis (push)
91+ if : github.event.workflow_run.event == 'push'
92+ env :
93+ SONAR_TOKEN : ${{ secrets.SONAR_TOKEN }}
94+ run : ./gradlew sonar
95+
96+ coverage-comment :
97+ name : Coverage PR Comment
98+ runs-on : ubuntu-latest
99+ if : >-
100+ github.event.workflow_run.conclusion == 'success' &&
101+ github.event.workflow_run.event == 'pull_request'
102+ steps :
103+ - name : Download coverage report
104+ uses : actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
105+ with :
106+ name : coverage-report
107+ path : build/reports/kover
108+ run-id : ${{ github.event.workflow_run.id }}
109+ github-token : ${{ secrets.GITHUB_TOKEN }}
110+
111+ - name : Resolve PR number
112+ id : pr
113+ env :
114+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
115+ run : |
116+ PR_NUMBER="${{ github.event.workflow_run.pull_requests[0].number }}"
117+ if [ -z "$PR_NUMBER" ]; then
118+ PR_NUMBER=$(gh pr list --search "${{ github.event.workflow_run.head_sha }}" --state open --json number --jq '.[0].number // empty')
119+ fi
120+ if [ -n "$PR_NUMBER" ]; then
121+ echo "number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
122+ else
123+ echo "Could not resolve PR number"
124+ exit 1
125+ fi
126+
127+ - name : Parse coverage and post comment
128+ env :
129+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
130+ run : |
131+ REPORT="build/reports/kover/report.xml"
132+ if [ ! -f "$REPORT" ]; then
133+ echo "Coverage report not found at $REPORT"
134+ exit 1
135+ fi
136+
137+ # Extract coverage counters from the Kover/JaCoCo XML report
138+ # Parse the top-level <counter> elements for INSTRUCTION, BRANCH, and LINE
139+ extract_coverage() {
140+ local type=$1
141+ local missed covered total pct
142+ missed=$(grep -oP "<counter type=\"$type\" missed=\"\K[0-9]+" "$REPORT" | tail -1)
143+ covered=$(grep -oP "<counter type=\"$type\" covered=\"\K[0-9]+" "$REPORT" | tail -1)
144+ if [ -n "$missed" ] && [ -n "$covered" ]; then
145+ total=$((missed + covered))
146+ if [ "$total" -gt 0 ]; then
147+ pct=$(( (covered * 10000) / total ))
148+ echo "$(( pct / 100 )).$(printf '%02d' $(( pct % 100 )))%"
149+ else
150+ echo "N/A"
151+ fi
152+ else
153+ echo "N/A"
154+ fi
155+ }
156+
157+ LINE_COV=$(extract_coverage "LINE")
158+ BRANCH_COV=$(extract_coverage "BRANCH")
159+ INSTRUCTION_COV=$(extract_coverage "INSTRUCTION")
160+
161+ BODY=$(cat <<EOF
162+ ## Code Coverage
163+
164+ | Metric | Coverage |
165+ |--------|----------|
166+ | Line | $LINE_COV |
167+ | Branch | $BRANCH_COV |
168+ | Instruction | $INSTRUCTION_COV |
169+
170+ *Updated for commit ${{ github.event.workflow_run.head_sha }}*
171+ EOF
172+ )
173+
174+ # Find existing coverage comment to update, or create a new one
175+ PR_NUMBER=${{ steps.pr.outputs.number }}
176+ EXISTING_COMMENT=$(gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
177+ --jq '.[] | select(.body | startswith("## Code Coverage")) | .id' | head -1)
178+
179+ if [ -n "$EXISTING_COMMENT" ]; then
180+ gh api "repos/${{ github.repository }}/issues/comments/${EXISTING_COMMENT}" \
181+ -X PATCH -f body="$BODY"
182+ else
183+ gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
184+ -X POST -f body="$BODY"
185+ fi
0 commit comments