SonarQube scan #401
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: SonarQube scan | |
| # Runs after main CI succeeds or on manual dispatch. | |
| # | |
| # The workflow consumes the coverage-report artifact from the same CI run | |
| # that triggered it. It intentionally does not run directly on push: | |
| # push-time scans can start before CI uploads coverage.xml and then report | |
| # a partial fallback coverage number for main. | |
| on: # zizmor: ignore[dangerous-triggers] | |
| workflow_run: | |
| workflows: ["CI"] | |
| branches: [main] | |
| types: [completed] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| actions: read | |
| concurrency: | |
| group: sonar-scan-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| scan: | |
| name: SonarQube scan | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| # Run after main CI succeeds or on manual dispatch. | |
| # SONAR_HOST_URL must be configured as a repo variable. | |
| if: >- | |
| ${{ vars.SONAR_HOST_URL != '' && | |
| (github.event_name == 'workflow_dispatch' || | |
| (github.event_name == 'workflow_run' && | |
| github.event.workflow_run.conclusion == 'success' && | |
| github.event.workflow_run.head_branch == 'main')) }} | |
| steps: | |
| - name: Harden runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| disable-sudo: true | |
| - name: Checkout | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| # Sonar needs full git history for accurate blame. | |
| fetch-depth: 0 | |
| ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }} | |
| persist-credentials: false | |
| # Manual dispatch has no upstream run id, so resolve the most recent | |
| # successful main CI run and pull its coverage-report artifact. | |
| - name: Resolve latest successful CI run (workflow_dispatch) | |
| if: github.event_name == 'workflow_dispatch' | |
| id: ci_run | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| run_id=$(gh run list \ | |
| --repo "${{ github.repository }}" \ | |
| --workflow ci.yml \ | |
| --branch main \ | |
| --status success \ | |
| --limit 1 \ | |
| --json databaseId \ | |
| --jq '.[0].databaseId') | |
| if [ -n "$run_id" ]; then | |
| echo "Found successful CI run: $run_id" | |
| echo "run_id=$run_id" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "::warning::No successful CI run found on main; scanning without coverage" | |
| fi | |
| - name: Download coverage artifact (workflow_run) | |
| if: github.event_name == 'workflow_run' | |
| continue-on-error: true # cancelled CI runs may not have reached the upload step | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: coverage-report | |
| run-id: ${{ github.event.workflow_run.id }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Download coverage artifact (workflow_dispatch) | |
| if: github.event_name == 'workflow_dispatch' && steps.ci_run.outputs.run_id != '' | |
| continue-on-error: true # manual bootstrap may not have a usable artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: coverage-report | |
| run-id: ${{ steps.ci_run.outputs.run_id }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Verify coverage.xml present | |
| id: coverage_check | |
| run: | | |
| if [ ! -f coverage.xml ] || [ ! -s coverage.xml ]; then | |
| echo "::warning::coverage.xml not found or empty; will run thin coverage fallback" | |
| echo "missing=true" >> "$GITHUB_OUTPUT" | |
| else | |
| size=$(wc -c < coverage.xml) | |
| echo "coverage.xml found ($size bytes)" | |
| echo "missing=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Set up uv for fallback | |
| if: github.event_name == 'workflow_dispatch' && steps.coverage_check.outputs.missing == 'true' | |
| uses: ./.github/actions/bootstrap | |
| # Thin coverage fallback is manual-dispatch only. Automatic main | |
| # scans without a CI coverage artifact skip the Sonar upload instead | |
| # of sending a partial fallback coverage report. | |
| - name: Thin coverage fallback | |
| if: github.event_name == 'workflow_dispatch' && steps.coverage_check.outputs.missing == 'true' | |
| timeout-minutes: 10 | |
| continue-on-error: true # manual bootstrap should not fail before the scan step | |
| run: | | |
| set +e | |
| uv sync --group dev --no-progress 2>&1 | tail -20 || true | |
| uv run --no-progress python -m coverage run -m pytest \ | |
| tests/unit/observability \ | |
| tests/unit/cli/doctor \ | |
| tests/unit/telemetry \ | |
| -q --no-header \ | |
| --timeout=60 2>&1 | tail -40 || true | |
| uv run --no-progress python -m coverage xml -o coverage.xml || true | |
| if [ -f coverage.xml ] && [ -s coverage.xml ]; then | |
| echo "Thin coverage fallback produced coverage.xml ($(wc -c < coverage.xml) bytes)" | |
| else | |
| echo "::warning::Thin coverage fallback did not produce coverage.xml; Sonar scan will be skipped" | |
| fi | |
| - name: Verify scan coverage input | |
| id: scan_coverage | |
| run: | | |
| if [ -f coverage.xml ] && [ -s coverage.xml ]; then | |
| echo "available=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "::warning::No usable coverage.xml available; skipping Sonar scan" | |
| echo "available=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: SonarQube scan | |
| if: steps.scan_coverage.outputs.available == 'true' | |
| timeout-minutes: 10 | |
| uses: SonarSource/sonarqube-scan-action@713881670b6b3676cda39549040e2d88c70d582e # v8.2.0 | |
| env: | |
| SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} | |
| SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }} | |
| with: | |
| # `sonar.projectKey` and `sonar.projectName` come from | |
| # `sonar-project.properties`. Keep source, test, exclusion, | |
| # and coverage-exclusion scope in that file so local and CI | |
| # scans use the same analysis boundary. | |
| args: > | |
| -Dsonar.python.version=3.12,3.13 | |
| -Dsonar.python.coverage.reportPaths=coverage.xml | |
| -Dsonar.scm.revision=${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }} | |
| -Dsonar.ws.timeout=600 |