From bacec0968f28130f3ff629904a9e1094f8880fb4 Mon Sep 17 00:00:00 2001 From: AriusII Date: Mon, 21 Sep 2026 21:29:25 +0200 Subject: [PATCH] ci: enforce Sonar quality gate coverage --- .github/workflows/pull-request-ci.yml | 2 +- .github/workflows/sonar.yml | 102 +++++++++++++----- Directory.Packages.props | 1 + eng/Tests.props | 1 + .../packages.lock.json | 21 ++++ .../packages.lock.json | 21 ++++ .../packages.lock.json | 21 ++++ .../packages.lock.json | 21 ++++ .../packages.lock.json | 21 ++++ .../packages.lock.json | 21 ++++ .../packages.lock.json | 21 ++++ 11 files changed, 226 insertions(+), 27 deletions(-) diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 6eb8aa6..c356a2b 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -40,7 +40,7 @@ jobs: if: >- vars.SONAR_CI_ENABLED == 'true' && github.event.pull_request.head.repo.full_name == github.repository - && github.actor != 'dependabot[bot]' + && github.event.pull_request.user.login != 'dependabot[bot]' uses: ./.github/workflows/sonar.yml secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 1b9a4b2..3474a27 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -11,14 +11,10 @@ on: description: SonarQube Cloud organization key. type: string default: cheatenginenet - wait-quality-gate: - description: Fail the job when the quality gate fails. - type: boolean - default: false secrets: SONAR_TOKEN: - description: SonarQube Cloud token. The analysis is skipped when it is empty. - required: false + description: SonarQube Cloud token for protected, same-repository analysis. + required: true permissions: contents: read @@ -38,22 +34,21 @@ jobs: name: Analyze if: github.event_name != 'merge_group' runs-on: windows-latest - timeout-minutes: 30 + timeout-minutes: 40 env: - SONAR_ENABLED: ${{ secrets.SONAR_TOKEN != '' }} SONAR_PROJECT_KEY: ${{ inputs.project-key }} SONAR_ORGANIZATION: ${{ inputs.organization }} - SONAR_WAIT_QUALITY_GATE: ${{ inputs.wait-quality-gate }} steps: - - name: Skip without token - if: env.SONAR_ENABLED != 'true' - run: >- - Write-Host '::notice title=Sonar analysis skipped::SONAR_TOKEN is empty. Fork and Dependabot pull requests - receive no repository secret; configure SONAR_TOKEN to enable analysis.' + - name: Require Sonar token + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + if ([string]::IsNullOrWhiteSpace($env:SONAR_TOKEN)) { + throw 'SONAR_TOKEN is required when SONAR_CI_ENABLED is true.' + } - name: Check analysis method - if: env.SONAR_ENABLED == 'true' env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: | @@ -63,8 +58,7 @@ jobs: -TimeoutSec 30 -MaximumRetryCount 2 -RetryIntervalSec 3 } catch { - Write-Host "::warning::Could not read the analysis method of $env:SONAR_PROJECT_KEY. $($_.Exception.Message)" - return + throw "Could not verify the analysis method of $env:SONAR_PROJECT_KEY. $($_.Exception.Message)" } if (@($response.settings | Where-Object { $_.key -eq 'sonar.autoscan.enabled' -and $_.value -eq 'true' })) { @@ -73,21 +67,18 @@ jobs: } - name: Checkout - if: env.SONAR_ENABLED == 'true' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - name: Set up JDK 21 - if: env.SONAR_ENABLED == 'true' uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 with: distribution: zulu java-version: '21' - name: Install pinned .NET SDK - if: env.SONAR_ENABLED == 'true' uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: global-json-file: global.json @@ -97,17 +88,14 @@ jobs: Directory.Packages.props - name: Install scanner - if: env.SONAR_ENABLED == 'true' run: >- dotnet tool install dotnet-sonarscanner --tool-path "$env:RUNNER_TEMP/sonar-scanner" --version $env:SONAR_SCANNER_VERSION - name: Restore locked dependency graph - if: env.SONAR_ENABLED == 'true' run: dotnet restore CheatEngine.Client.slnx --locked-mode - name: Begin analysis - if: env.SONAR_ENABLED == 'true' env: SONARQUBE_SCANNER_PARAMS: '{"sonar.token":"${{ secrets.SONAR_TOKEN }}"}' run: | @@ -138,8 +126,10 @@ jobs: "/k:$env:SONAR_PROJECT_KEY" "/o:$env:SONAR_ORGANIZATION" '/d:sonar.exclusions=artifacts/**,tests/CheatEngine.Client.Benchmarks/**' + '/d:sonar.cs.cobertura.reportsPaths=artifacts/sonar-test-results/*/coverage.cobertura.xml' "/d:sonar.issue.ignore.multicriteria=$(($ignoredIssues.Key) -join ',')" - "/d:sonar.qualitygate.wait=$env:SONAR_WAIT_QUALITY_GATE" + '/d:sonar.qualitygate.wait=true' + '/d:sonar.qualitygate.timeout=300' ) foreach ($issue in $ignoredIssues) { $arguments += "/d:sonar.issue.ignore.multicriteria.$($issue.Key).ruleKey=$($issue.Rule)" @@ -148,13 +138,73 @@ jobs: & "$env:RUNNER_TEMP/sonar-scanner/dotnet-sonarscanner.exe" begin @arguments - name: Build - if: env.SONAR_ENABLED == 'true' run: >- dotnet build CheatEngine.Client.slnx --configuration Release --no-restore --no-incremental --disable-build-servers --warnaserror + - name: Run tests with Microsoft Testing Platform coverage + run: | + $projects = @(Get-ChildItem -Path tests -Filter '*.Tests.csproj' -Recurse -File | Sort-Object FullName) + if ($projects.Count -eq 0) { + throw 'No *.Tests.csproj project found under tests.' + } + + foreach ($project in $projects) { + $results = Join-Path 'artifacts/sonar-test-results' $project.BaseName + $coverage = [IO.Path]::GetFullPath((Join-Path $results 'coverage.cobertura.xml')) + $options = @( + '--project', $project.FullName, + '--configuration', 'Release', + '--no-build', '--no-restore', + '--report-trx', + '--results-directory', $results, + '--coverage', + '--coverage-output', $coverage, + '--coverage-output-format', 'cobertura', + '--fail-skips', 'on' + ) + dotnet test @options + if ($LASTEXITCODE -ne 0) { + throw "Microsoft Testing Platform failed for '$($project.FullName)' with exit code $LASTEXITCODE." + } + } + + - name: Verify coverage reports + run: | + $projects = @(Get-ChildItem -Path tests -Filter '*.Tests.csproj' -Recurse -File) + $reports = @($projects | ForEach-Object { + Join-Path (Join-Path 'artifacts/sonar-test-results' $_.BaseName) 'coverage.cobertura.xml' + }) + $missingReports = @($reports | Where-Object { -not (Test-Path -LiteralPath $_ -PathType Leaf) }) + if ($missingReports.Count -gt 0) { + throw "Microsoft Testing Platform did not produce Cobertura coverage report(s): $($missingReports -join ', ')." + } + + foreach ($report in $reports) { + try { + $coverage = [xml](Get-Content -LiteralPath $report -Raw) + } + catch { + throw "Coverage report '$report' is not valid XML. $($_.Exception.Message)" + } + + if ((Get-Item -LiteralPath $report).Length -eq 0 -or $coverage.DocumentElement.Name -ne 'coverage') { + throw "Coverage report '$report' is not a Cobertura report." + } + } + + Write-Host "Verified $($reports.Count) Cobertura coverage report(s)." + - name: End analysis - if: env.SONAR_ENABLED == 'true' env: SONARQUBE_SCANNER_PARAMS: '{"sonar.token":"${{ secrets.SONAR_TOKEN }}"}' run: '& "$env:RUNNER_TEMP/sonar-scanner/dotnet-sonarscanner.exe" end' + + - name: Upload Sonar coverage reports + if: ${{ !cancelled() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: sonar-coverage + path: artifacts/sonar-test-results/*/coverage.cobertura.xml + if-no-files-found: error + retention-days: 14 diff --git a/Directory.Packages.props b/Directory.Packages.props index ab4e128..cd094fa 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,6 +19,7 @@ + diff --git a/eng/Tests.props b/eng/Tests.props index f20f859..b66e29e 100644 --- a/eng/Tests.props +++ b/eng/Tests.props @@ -14,6 +14,7 @@ + diff --git a/tests/CheatEngine.Client.Abstractions.Tests/packages.lock.json b/tests/CheatEngine.Client.Abstractions.Tests/packages.lock.json index 8c00db6..6cf7ac7 100644 --- a/tests/CheatEngine.Client.Abstractions.Tests/packages.lock.json +++ b/tests/CheatEngine.Client.Abstractions.Tests/packages.lock.json @@ -13,6 +13,17 @@ "System.IO.Hashing": "10.0.12" } }, + "Microsoft.Testing.Extensions.CodeCoverage": { + "type": "Direct", + "requested": "[18.11.2, )", + "resolved": "18.11.2", + "contentHash": "bT6awBEUR+fjPpeLAN++4qx5q0sgA9SeJt6QfijnFFPxNSr68BYsYFMQH8L8kY6vMJd3fuvFAFBht4JtWQcWtQ==", + "dependencies": { + "Microsoft.DiaSymReader": "2.2.10", + "Microsoft.Extensions.DependencyModel": "10.0.10", + "Microsoft.Testing.Platform": "2.4.0" + } + }, "Microsoft.Testing.Extensions.TrxReport": { "type": "Direct", "requested": "[2.4.1, )", @@ -52,6 +63,16 @@ "System.IO.Hashing": "10.0.12" } }, + "Microsoft.DiaSymReader": { + "type": "Transitive", + "resolved": "2.2.10", + "contentHash": "zmGsm6b2y3STDa/Of7rdkkfTDV8VuGB8aCqIkLoJIQh5tL78K3zJ8OUyFKnfsaORXmGr9iOKwDkZfUjeXi+CwA==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "rfZA1RjR021RPqSmIPovfz2aOd79TGqJ9BengbjnzIISOVwjLmuSDnhCMmiY/1c6iYvGolQ1iNGzkav0u11XEA==" + }, "Microsoft.SourceLink.Common": { "type": "Transitive", "resolved": "10.0.401", diff --git a/tests/CheatEngine.Client.Core.Tests/packages.lock.json b/tests/CheatEngine.Client.Core.Tests/packages.lock.json index d9d0a56..ac542c5 100644 --- a/tests/CheatEngine.Client.Core.Tests/packages.lock.json +++ b/tests/CheatEngine.Client.Core.Tests/packages.lock.json @@ -13,6 +13,17 @@ "System.IO.Hashing": "10.0.12" } }, + "Microsoft.Testing.Extensions.CodeCoverage": { + "type": "Direct", + "requested": "[18.11.2, )", + "resolved": "18.11.2", + "contentHash": "bT6awBEUR+fjPpeLAN++4qx5q0sgA9SeJt6QfijnFFPxNSr68BYsYFMQH8L8kY6vMJd3fuvFAFBht4JtWQcWtQ==", + "dependencies": { + "Microsoft.DiaSymReader": "2.2.10", + "Microsoft.Extensions.DependencyModel": "10.0.10", + "Microsoft.Testing.Platform": "2.4.0" + } + }, "Microsoft.Testing.Extensions.TrxReport": { "type": "Direct", "requested": "[2.4.1, )", @@ -52,6 +63,16 @@ "System.IO.Hashing": "10.0.12" } }, + "Microsoft.DiaSymReader": { + "type": "Transitive", + "resolved": "2.2.10", + "contentHash": "zmGsm6b2y3STDa/Of7rdkkfTDV8VuGB8aCqIkLoJIQh5tL78K3zJ8OUyFKnfsaORXmGr9iOKwDkZfUjeXi+CwA==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "rfZA1RjR021RPqSmIPovfz2aOd79TGqJ9BengbjnzIISOVwjLmuSDnhCMmiY/1c6iYvGolQ1iNGzkav0u11XEA==" + }, "Microsoft.SourceLink.Common": { "type": "Transitive", "resolved": "10.0.401", diff --git a/tests/CheatEngine.Client.Extensions.DependencyInjection.Tests/packages.lock.json b/tests/CheatEngine.Client.Extensions.DependencyInjection.Tests/packages.lock.json index 0f9f765..f242f60 100644 --- a/tests/CheatEngine.Client.Extensions.DependencyInjection.Tests/packages.lock.json +++ b/tests/CheatEngine.Client.Extensions.DependencyInjection.Tests/packages.lock.json @@ -13,6 +13,17 @@ "System.IO.Hashing": "10.0.12" } }, + "Microsoft.Testing.Extensions.CodeCoverage": { + "type": "Direct", + "requested": "[18.11.2, )", + "resolved": "18.11.2", + "contentHash": "bT6awBEUR+fjPpeLAN++4qx5q0sgA9SeJt6QfijnFFPxNSr68BYsYFMQH8L8kY6vMJd3fuvFAFBht4JtWQcWtQ==", + "dependencies": { + "Microsoft.DiaSymReader": "2.2.10", + "Microsoft.Extensions.DependencyModel": "10.0.10", + "Microsoft.Testing.Platform": "2.4.0" + } + }, "Microsoft.Testing.Extensions.TrxReport": { "type": "Direct", "requested": "[2.4.1, )", @@ -52,6 +63,11 @@ "System.IO.Hashing": "10.0.12" } }, + "Microsoft.DiaSymReader": { + "type": "Transitive", + "resolved": "2.2.10", + "contentHash": "zmGsm6b2y3STDa/Of7rdkkfTDV8VuGB8aCqIkLoJIQh5tL78K3zJ8OUyFKnfsaORXmGr9iOKwDkZfUjeXi+CwA==" + }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "10.0.12", @@ -61,6 +77,11 @@ "Microsoft.Extensions.Primitives": "10.0.12" } }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "rfZA1RjR021RPqSmIPovfz2aOd79TGqJ9BengbjnzIISOVwjLmuSDnhCMmiY/1c6iYvGolQ1iNGzkav0u11XEA==" + }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "10.0.12", diff --git a/tests/CheatEngine.Client.Fluent.Tests/packages.lock.json b/tests/CheatEngine.Client.Fluent.Tests/packages.lock.json index e036eed..1aaafd6 100644 --- a/tests/CheatEngine.Client.Fluent.Tests/packages.lock.json +++ b/tests/CheatEngine.Client.Fluent.Tests/packages.lock.json @@ -13,6 +13,17 @@ "System.IO.Hashing": "10.0.12" } }, + "Microsoft.Testing.Extensions.CodeCoverage": { + "type": "Direct", + "requested": "[18.11.2, )", + "resolved": "18.11.2", + "contentHash": "bT6awBEUR+fjPpeLAN++4qx5q0sgA9SeJt6QfijnFFPxNSr68BYsYFMQH8L8kY6vMJd3fuvFAFBht4JtWQcWtQ==", + "dependencies": { + "Microsoft.DiaSymReader": "2.2.10", + "Microsoft.Extensions.DependencyModel": "10.0.10", + "Microsoft.Testing.Platform": "2.4.0" + } + }, "Microsoft.Testing.Extensions.TrxReport": { "type": "Direct", "requested": "[2.4.1, )", @@ -52,6 +63,16 @@ "System.IO.Hashing": "10.0.12" } }, + "Microsoft.DiaSymReader": { + "type": "Transitive", + "resolved": "2.2.10", + "contentHash": "zmGsm6b2y3STDa/Of7rdkkfTDV8VuGB8aCqIkLoJIQh5tL78K3zJ8OUyFKnfsaORXmGr9iOKwDkZfUjeXi+CwA==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "rfZA1RjR021RPqSmIPovfz2aOd79TGqJ9BengbjnzIISOVwjLmuSDnhCMmiY/1c6iYvGolQ1iNGzkav0u11XEA==" + }, "Microsoft.SourceLink.Common": { "type": "Transitive", "resolved": "10.0.401", diff --git a/tests/CheatEngine.Client.Hosting.Tests/packages.lock.json b/tests/CheatEngine.Client.Hosting.Tests/packages.lock.json index 9610c29..60d1d51 100644 --- a/tests/CheatEngine.Client.Hosting.Tests/packages.lock.json +++ b/tests/CheatEngine.Client.Hosting.Tests/packages.lock.json @@ -13,6 +13,17 @@ "System.IO.Hashing": "10.0.12" } }, + "Microsoft.Testing.Extensions.CodeCoverage": { + "type": "Direct", + "requested": "[18.11.2, )", + "resolved": "18.11.2", + "contentHash": "bT6awBEUR+fjPpeLAN++4qx5q0sgA9SeJt6QfijnFFPxNSr68BYsYFMQH8L8kY6vMJd3fuvFAFBht4JtWQcWtQ==", + "dependencies": { + "Microsoft.DiaSymReader": "2.2.10", + "Microsoft.Extensions.DependencyModel": "10.0.10", + "Microsoft.Testing.Platform": "2.4.0" + } + }, "Microsoft.Testing.Extensions.TrxReport": { "type": "Direct", "requested": "[2.4.1, )", @@ -52,6 +63,11 @@ "System.IO.Hashing": "10.0.12" } }, + "Microsoft.DiaSymReader": { + "type": "Transitive", + "resolved": "2.2.10", + "contentHash": "zmGsm6b2y3STDa/Of7rdkkfTDV8VuGB8aCqIkLoJIQh5tL78K3zJ8OUyFKnfsaORXmGr9iOKwDkZfUjeXi+CwA==" + }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "10.0.12", @@ -61,6 +77,11 @@ "Microsoft.Extensions.Primitives": "10.0.12" } }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "rfZA1RjR021RPqSmIPovfz2aOd79TGqJ9BengbjnzIISOVwjLmuSDnhCMmiY/1c6iYvGolQ1iNGzkav0u11XEA==" + }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "10.0.12", diff --git a/tests/CheatEngine.Client.SourceGenerators.Lua.Tests/packages.lock.json b/tests/CheatEngine.Client.SourceGenerators.Lua.Tests/packages.lock.json index fa761cf..260ecf1 100644 --- a/tests/CheatEngine.Client.SourceGenerators.Lua.Tests/packages.lock.json +++ b/tests/CheatEngine.Client.SourceGenerators.Lua.Tests/packages.lock.json @@ -23,6 +23,17 @@ "System.IO.Hashing": "10.0.12" } }, + "Microsoft.Testing.Extensions.CodeCoverage": { + "type": "Direct", + "requested": "[18.11.2, )", + "resolved": "18.11.2", + "contentHash": "bT6awBEUR+fjPpeLAN++4qx5q0sgA9SeJt6QfijnFFPxNSr68BYsYFMQH8L8kY6vMJd3fuvFAFBht4JtWQcWtQ==", + "dependencies": { + "Microsoft.DiaSymReader": "2.2.10", + "Microsoft.Extensions.DependencyModel": "10.0.10", + "Microsoft.Testing.Platform": "2.4.0" + } + }, "Microsoft.Testing.Extensions.TrxReport": { "type": "Direct", "requested": "[2.4.1, )", @@ -70,6 +81,11 @@ "Microsoft.CodeAnalysis.Analyzers": "5.9.0-1.26328.17" } }, + "Microsoft.DiaSymReader": { + "type": "Transitive", + "resolved": "2.2.10", + "contentHash": "zmGsm6b2y3STDa/Of7rdkkfTDV8VuGB8aCqIkLoJIQh5tL78K3zJ8OUyFKnfsaORXmGr9iOKwDkZfUjeXi+CwA==" + }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "10.0.12", @@ -79,6 +95,11 @@ "Microsoft.Extensions.Primitives": "10.0.12" } }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "rfZA1RjR021RPqSmIPovfz2aOd79TGqJ9BengbjnzIISOVwjLmuSDnhCMmiY/1c6iYvGolQ1iNGzkav0u11XEA==" + }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "10.0.12", diff --git a/tests/CheatEngine.Client.Tests/packages.lock.json b/tests/CheatEngine.Client.Tests/packages.lock.json index e1916b1..5a92361 100644 --- a/tests/CheatEngine.Client.Tests/packages.lock.json +++ b/tests/CheatEngine.Client.Tests/packages.lock.json @@ -13,6 +13,17 @@ "System.IO.Hashing": "10.0.12" } }, + "Microsoft.Testing.Extensions.CodeCoverage": { + "type": "Direct", + "requested": "[18.11.2, )", + "resolved": "18.11.2", + "contentHash": "bT6awBEUR+fjPpeLAN++4qx5q0sgA9SeJt6QfijnFFPxNSr68BYsYFMQH8L8kY6vMJd3fuvFAFBht4JtWQcWtQ==", + "dependencies": { + "Microsoft.DiaSymReader": "2.2.10", + "Microsoft.Extensions.DependencyModel": "10.0.10", + "Microsoft.Testing.Platform": "2.4.0" + } + }, "Microsoft.Testing.Extensions.TrxReport": { "type": "Direct", "requested": "[2.4.1, )", @@ -52,6 +63,11 @@ "System.IO.Hashing": "10.0.12" } }, + "Microsoft.DiaSymReader": { + "type": "Transitive", + "resolved": "2.2.10", + "contentHash": "zmGsm6b2y3STDa/Of7rdkkfTDV8VuGB8aCqIkLoJIQh5tL78K3zJ8OUyFKnfsaORXmGr9iOKwDkZfUjeXi+CwA==" + }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "10.0.12", @@ -61,6 +77,11 @@ "Microsoft.Extensions.Primitives": "10.0.12" } }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "rfZA1RjR021RPqSmIPovfz2aOd79TGqJ9BengbjnzIISOVwjLmuSDnhCMmiY/1c6iYvGolQ1iNGzkav0u11XEA==" + }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "10.0.12",