Skip to content

Commit cbfc316

Browse files
committed
fix: split CI to run Sonar analysis via workflow_run
Fork PRs cannot access repository secrets, causing the SonarQube analysis step to fail. Split the build workflow so that secret-dependent steps (Sonar, coverage PR comment) run in a separate workflow_run- triggered workflow that executes in the base repo context.
1 parent 452b33d commit cbfc316

2 files changed

Lines changed: 186 additions & 19 deletions

File tree

.github/workflows/analysis.yml

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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

.github/workflows/build-and-test.yml

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,11 @@ jobs:
3131
distribution: 'zulu'
3232
cache: 'gradle'
3333

34-
- name: Cache SonarQube packages
35-
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
36-
with:
37-
path: ~/.sonar/cache
38-
key: ${{ runner.os }}-sonar
39-
restore-keys: ${{ runner.os }}-sonar
40-
4134
- name: Setup Gradle
4235
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
4336

4437
- name: Build & Test with Gradle
45-
env:
46-
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
47-
run: ./gradlew check sonar
38+
run: ./gradlew check
4839

4940
- name: Upload Test Report
5041
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
@@ -69,15 +60,6 @@ jobs:
6960
files: |
7061
**/build/test-results/*/TEST-*.xml
7162
72-
- name: Add coverage report to PR
73-
if: github.event_name == 'pull_request' || github.event_name == 'push'
74-
id: kover
75-
uses: mi-kas/kover-report@5f58465b6f395c8fa3adc2665e27250bad87de50
76-
with:
77-
path: |
78-
build/reports/kover/report.xml
79-
title: Code Coverage
80-
update-comment: true
8163
integration-test:
8264
name: Integration tests
8365
runs-on: ubuntu-latest

0 commit comments

Comments
 (0)