From 8f11f5f3286634683feb824dbe20ee6b0919457c Mon Sep 17 00:00:00 2001 From: santidev21 Date: Wed, 30 Sep 2026 21:01:58 -0500 Subject: [PATCH 1/2] ci(sonarcloud): add SonarCloud analysis workflow with Quality Gate on PRs - New .github/workflows/sonarcloud.yml: dotnet-sonarscanner begin/build/ test/end on push to main, pull requests and manual dispatch. - sonar.qualitygate.wait=true on pull_request events only: the Sonar way gate evaluates new code, so a red build means the PR introduced issues. The first push to main is the baseline (everything counts as new) and must not block CI on pre-existing debt. - The analysis build passes TreatWarningsAsErrors=false: SonarScanner injects its own Roslyn analyzers for that build only. The zero-warnings gate is unchanged in ci.yml, and the solution still has no SonarAnalyzer package reference by design. - PR parameters are passed explicitly because the scanner does not auto-detect GitHub Actions. - Job is skipped for Dependabot and fork PRs (no repository secrets). - coverlet.runsettings: emit opencover alongside cobertura so SonarCloud imports coverage (check-coverage.mjs still reads cobertura only). - .sonarqube/ added to .gitignore (scanner working directory). --- .github/workflows/sonarcloud.yml | 100 +++++++++++++++++++++++++++++++ .gitignore | 3 + coverlet.runsettings | 2 +- 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/sonarcloud.yml diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml new file mode 100644 index 0000000..26da2e7 --- /dev/null +++ b/.github/workflows/sonarcloud.yml @@ -0,0 +1,100 @@ +name: SonarCloud + +# SonarCloud analysis (Clean as You Code): the "Sonar way" Quality Gate is +# evaluated on NEW CODE only. +# +# Design notes: +# - `sonar.qualitygate.wait=true` is set for pull_request events ONLY: the gate +# then fails this job when new code introduces issues. On the very first push +# to main the whole codebase counts as new code (baseline), so blocking there +# would fail on pre-existing debt by design. +# - The scanner does not auto-detect GitHub Actions, so PR parameters are passed +# explicitly below. +# - The analysis build passes TreatWarningsAsErrors=false: SonarScanner injects +# its own Roslyn analyzers for the duration of this build, and their rule +# warnings must not fail it. The zero-warnings gate itself is NOT weakened: +# it still runs in ci.yml (build-and-test job), which does not run the scanner. +# The SonarAnalyzer NuGet package remains out of the solution by design. +# - Skipped for Dependabot and fork PRs: repository secrets are not available to +# those events, so the job would fail without a token. +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + sonarcloud: + name: SonarCloud analysis + runs-on: ubuntu-latest + if: >- + github.actor != 'dependabot[bot]' && + (github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name == github.repository) + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # Full history: SonarCloud needs blame data to decide which lines are + # "new code" for the Quality Gate. + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Install SonarScanner + run: dotnet tool install --global dotnet-sonarscanner + + - name: Restore + run: dotnet restore MyBudget.sln + + - name: Begin analysis + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + PR_KEY: ${{ github.event.pullrequest.number }} + PR_BRANCH: ${{ github.head_ref }} + PR_BASE: ${{ github.base_ref }} + run: | + args=( + /k:santidev21_MyBudgetBot + /o:santidev21 + "/d:sonar.token=$SONAR_TOKEN" + "/d:sonar.cs.opencover.reportsPaths=**/coverage.opencover.xml" + "/d:sonar.cs.vstest.reportsPaths=**/TestResults/*.trx" + ) + if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then + args+=( + "/d:sonar.pullrequest.key=$PR_KEY" + "/d:sonar.pullrequest.branch=$PR_BRANCH" + "/d:sonar.pullrequest.base=$PR_BASE" + /d:sonar.qualitygate.wait=true + ) + fi + dotnet sonarscanner begin "${args[@]}" + + - name: Build + # See the header note: scoped to this analysis build only. + run: dotnet build MyBudget.sln --no-restore --configuration Release -p:TreatWarningsAsErrors=false + + # Integration tests start a real PostgreSQL container via Testcontainers; + # Docker is available on the GitHub-hosted runner (same as in ci.yml). + - name: Test + run: > + dotnet test MyBudget.sln + --no-build + --configuration Release + --logger "trx;LogFileName=results.trx" + --collect:"XPlat Code Coverage" + --settings coverlet.runsettings + + - name: End analysis + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: dotnet sonarscanner end "/d:sonar.token=$SONAR_TOKEN" diff --git a/.gitignore b/.gitignore index 965c57b..1be9ec2 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ Thumbs.db # CodeGraph local index .codegraph/ + +# SonarScanner for .NET working directory +.sonarqube/ diff --git a/coverlet.runsettings b/coverlet.runsettings index d1b5a1e..8aafab8 100644 --- a/coverlet.runsettings +++ b/coverlet.runsettings @@ -8,7 +8,7 @@ - cobertura + cobertura,opencover [MyBudget.*.Tests]* GeneratedCodeAttribute,CompilerGeneratedAttribute **/Persistence/Migrations/*.cs From bf543a3ed557e1609fc7db6c6c8ae771a400b6db Mon Sep 17 00:00:00 2001 From: santidev21 Date: Wed, 30 Sep 2026 21:46:20 -0500 Subject: [PATCH 2/2] fix(sonarcloud): correct GitHub context path for the PR number github.event.pullrequest.number does not exist (the pull_request payload key uses an underscore); the empty value made the scanner reject sonar.pullrequest.key and abort the analysis. --- .github/workflows/sonarcloud.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 26da2e7..09c5d4a 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -58,7 +58,7 @@ jobs: - name: Begin analysis env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - PR_KEY: ${{ github.event.pullrequest.number }} + PR_KEY: ${{ github.event.pull_request.number }} PR_BRANCH: ${{ github.head_ref }} PR_BASE: ${{ github.base_ref }} run: |