From cbfc316313f53c7a6cee0e9a6dcb70f8b838fa21 Mon Sep 17 00:00:00 2001 From: Dries Samyn <5557551+driessamyn@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:08:29 +0000 Subject: [PATCH] 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. --- .github/workflows/analysis.yml | 185 +++++++++++++++++++++++++++ .github/workflows/build-and-test.yml | 20 +-- 2 files changed, 186 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/analysis.yml diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml new file mode 100644 index 00000000..d9841483 --- /dev/null +++ b/.github/workflows/analysis.yml @@ -0,0 +1,185 @@ +name: Code Analysis +on: + workflow_run: + workflows: ["Build and Test"] + types: [completed] + +permissions: + checks: write + pull-requests: write + actions: read + +jobs: + sonar: + name: SonarQube Analysis + runs-on: ubuntu-latest + if: >- + github.event.workflow_run.conclusion == 'success' && + (github.event.workflow_run.event == 'push' || github.event.workflow_run.event == 'pull_request') + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + repository: ${{ github.event.workflow_run.head_repository.full_name }} + ref: ${{ github.event.workflow_run.head_sha }} + fetch-depth: 0 + + - name: Set up JDK 17 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 + with: + java-version: '17' + distribution: 'zulu' + cache: 'gradle' + + - name: Cache SonarQube packages + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + + - name: Download coverage report + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: coverage-report + path: build/reports/kover + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Download test results + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: junit-test-results + path: build/test-results + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Resolve PR details + id: pr + if: github.event.workflow_run.event == 'pull_request' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # workflow_run.pull_requests can be empty for fork PRs, fall back to API search + PR_NUMBER="${{ github.event.workflow_run.pull_requests[0].number }}" + if [ -z "$PR_NUMBER" ]; then + PR_NUMBER=$(gh pr list --search "${{ github.event.workflow_run.head_sha }}" --state open --json number --jq '.[0].number // empty') + fi + if [ -z "$PR_NUMBER" ]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "Could not resolve PR number, skipping PR-specific analysis" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "number=$PR_NUMBER" >> "$GITHUB_OUTPUT" + echo "branch=${{ github.event.workflow_run.head_branch }}" >> "$GITHUB_OUTPUT" + echo "base=${{ github.event.workflow_run.pull_requests[0].base.ref || 'main' }}" >> "$GITHUB_OUTPUT" + fi + + - name: Run SonarQube Analysis (PR) + if: github.event.workflow_run.event == 'pull_request' && steps.pr.outputs.skip != 'true' + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: >- + ./gradlew sonar + -Dsonar.pullrequest.key=${{ steps.pr.outputs.number }} + -Dsonar.pullrequest.branch=${{ steps.pr.outputs.branch }} + -Dsonar.pullrequest.base=${{ steps.pr.outputs.base }} + + - name: Run SonarQube Analysis (push) + if: github.event.workflow_run.event == 'push' + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: ./gradlew sonar + + coverage-comment: + name: Coverage PR Comment + runs-on: ubuntu-latest + if: >- + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'pull_request' + steps: + - name: Download coverage report + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: coverage-report + path: build/reports/kover + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Resolve PR number + id: pr + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_NUMBER="${{ github.event.workflow_run.pull_requests[0].number }}" + if [ -z "$PR_NUMBER" ]; then + PR_NUMBER=$(gh pr list --search "${{ github.event.workflow_run.head_sha }}" --state open --json number --jq '.[0].number // empty') + fi + if [ -n "$PR_NUMBER" ]; then + echo "number=$PR_NUMBER" >> "$GITHUB_OUTPUT" + else + echo "Could not resolve PR number" + exit 1 + fi + + - name: Parse coverage and post comment + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + REPORT="build/reports/kover/report.xml" + if [ ! -f "$REPORT" ]; then + echo "Coverage report not found at $REPORT" + exit 1 + fi + + # Extract coverage counters from the Kover/JaCoCo XML report + # Parse the top-level elements for INSTRUCTION, BRANCH, and LINE + extract_coverage() { + local type=$1 + local missed covered total pct + missed=$(grep -oP "