feat(sonar): Add SonarQube static analysis and code coverage setup
SonarQube Analysis / sonarqube (pull_request) Successful in 5m37s
SonarQube Analysis / sonarqube (pull_request) Successful in 5m37s
Establishes tooling to systematically analyze and address technical debt. This includes: - `scan-sonar.ps1`: An orchestration script for local SonarQube scans with coverage. - `Directory.Build.props`: Integrates SonarAnalyzer.CSharp for static analysis during build. - `coverlet.runsettings`: Configures code coverage collection using Coverlet. - `.claude/settings.local.json`: Adds permissions for AI to query SonarQube and local dev status.
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"PowerShell($url = 'https://snrqbe.bermudalamb.synology.me'; $tok = 'squ_49ed6268ca76f64072ff5d6a98ab21bdc352a3ef'; $pair = \"${tok}:\"; $b64 = [Convert]::ToBase64String\\([Text.Encoding]::ASCII.GetBytes\\($pair\\)\\); $h = @{ Authorization = \"Basic $b64\" }; $v = Invoke-WebRequest -Uri \"$url/api/server/version\" -Headers $h -SkipHttpErrorCheck; \"Version: $\\($v.Content\\)\"; $s = Invoke-WebRequest -Uri \"$url/api/system/status\" -Headers $h -SkipHttpErrorCheck; \"Status: $\\($s.Content\\)\")",
|
||||
"PowerShell($url = 'https://snrqbe.bermudalamb.synology.me'; $tok = 'squ_49ed6268ca76f64072ff5d6a98ab21bdc352a3ef'; $b64 = [Convert]::ToBase64String\\([Text.Encoding]::ASCII.GetBytes\\(\"${tok}:\"\\)\\); $h = @{ Authorization = \"Basic $b64\" }; $p = Invoke-RestMethod -Uri \"$url/api/projects/search?ps=100\" -Headers $h; \"Projects \\($\\($p.paging.total\\)\\):\"; $p.components | ForEach-Object { \" - $\\($_.key\\) [$\\($_.name\\)] visibility=$\\($_.visibility\\) lastAnalysis=$\\($_.lastAnalysisDate\\)\" })",
|
||||
"PowerShell($url='https://snrqbe.bermudalamb.synology.me'; $tok='squ_49ed6268ca76f64072ff5d6a98ab21bdc352a3ef'; $b64=[Convert]::ToBase64String\\([Text.Encoding]::ASCII.GetBytes\\(\"${tok}:\"\\)\\); $h=@{Authorization=\"Basic $b64\"}; $p='sql-utilities'; $iv=Invoke-RestMethod -Uri \"$url/api/issues/search?componentKeys=$p&resolved=false&ps=1&facets=severities,types,rules,files\" -Headers $h; \"=== Severity ===\"; \\($iv.facets | ? property -eq severities\\).values | Format-Table val, count -AutoSize; \"=== Type ===\"; \\($iv.facets | ? property -eq types\\).values | Format-Table val, count -AutoSize; \"=== Top 15 rules ===\"; \\($iv.facets | ? property -eq rules\\).values | Select-Object -First 15 | Format-Table val, count -AutoSize; \"=== Top 15 files ===\"; \\($iv.facets | ? property -eq files\\).values | Select-Object -First 15 | Format-Table val, count -AutoSize; \"Total open issues: $\\($iv.total\\)\")",
|
||||
"PowerShell($url='https://snrqbe.bermudalamb.synology.me'; $tok='squ_49ed6268ca76f64072ff5d6a98ab21bdc352a3ef'; $b64=[Convert]::ToBase64String\\([Text.Encoding]::ASCII.GetBytes\\(\"${tok}:\"\\)\\); $h=@{Authorization=\"Basic $b64\"}; $p='sql-utilities'; \"=== BLOCKER + CRITICAL \\(showing rule + file\\) ===\"; $iv=Invoke-RestMethod -Uri \"$url/api/issues/search?componentKeys=$p&resolved=false&severities=BLOCKER,CRITICAL&ps=50\" -Headers $h; $iv.issues | Group-Object rule | Sort-Object Count -Descending | ForEach-Object { \"$\\($_.Count\\) x $\\($_.Name\\)\" }; \"\"; \"=== S3776 \\(cognitive complexity\\) hotspots ===\"; $s=Invoke-RestMethod -Uri \"$url/api/issues/search?componentKeys=$p&resolved=false&rules=csharpsquid:S3776&ps=20\" -Headers $h; $s.issues | ForEach-Object { \"$\\($_.component\\) line $\\($_.line\\) - $\\($_.message\\)\" })",
|
||||
"Bash(dotnet test *)",
|
||||
"PowerShell($url='https://snrqbe.bermudalamb.synology.me'; $tok='squ_49ed6268ca76f64072ff5d6a98ab21bdc352a3ef'; $b64=[Convert]::ToBase64String\\([Text.Encoding]::ASCII.GetBytes\\(\"${tok}:\"\\)\\); $h=@{Authorization=\"Basic $b64\"}; $p='sql-utilities'; $iv=Invoke-RestMethod -Uri \"$url/api/issues/search?componentKeys=$p&resolved=false&severities=BLOCKER,CRITICAL&rules=csharpsquid:S2365,csharpsquid:S927,csharpsquid:S2696,csharpsquid:S3875,csharpsquid:S4487&ps=50\" -Headers $h; \"Total: $\\($iv.total\\)\"; \"\"; $iv.issues | Sort-Object rule, component | ForEach-Object { $f = $_.component -replace '^sql-utilities:',''; \"[$\\($_.severity\\)] $\\($_.rule\\)`n $\\($f\\):$\\($_.line\\)`n $\\($_.message\\)`n\" })",
|
||||
"Bash(git -C C:/gitea/sql-utilities status --short)",
|
||||
"Bash(git -C C:/gitea/sql-utilities log --oneline -3)",
|
||||
"Bash(git *)",
|
||||
"PowerShell(git *)",
|
||||
"Bash(dotnet build *)",
|
||||
"Bash(command -v gh)",
|
||||
"Bash(command -v tea)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<Project>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.4.0.108396">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<RunSettings>
|
||||
<DataCollectionRunSettings>
|
||||
<DataCollectors>
|
||||
<DataCollector friendlyName="XPlat code coverage">
|
||||
<Configuration>
|
||||
<Format>opencover</Format>
|
||||
<Exclude>[*.Tests]*,[*.Tests.*]*</Exclude>
|
||||
<ExcludeByFile>**/bin/**/*.cs,**/obj/**/*.cs</ExcludeByFile>
|
||||
</Configuration>
|
||||
</DataCollector>
|
||||
</DataCollectors>
|
||||
</DataCollectionRunSettings>
|
||||
</RunSettings>
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# Runs a local SonarQube scan with coverage.
|
||||
# Requires: $env:SONAR_TOKEN (and optionally $env:SONAR_HOST_URL).
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-Location $PSScriptRoot
|
||||
|
||||
if (-not $env:SONAR_TOKEN) {
|
||||
throw "Set `$env:SONAR_TOKEN before running (generate at <sonar>/account/security)."
|
||||
}
|
||||
|
||||
$sonarHost = if ($env:SONAR_HOST_URL) { $env:SONAR_HOST_URL } else { 'https://snrqbe.bermudalamb.synology.me' }
|
||||
$projectKey = 'sql-utilities'
|
||||
$solution = 'Strata.SqlTools.QueryBreakdown.sln'
|
||||
|
||||
if (-not (Get-Command dotnet-sonarscanner -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "Installing dotnet-sonarscanner..." -ForegroundColor Cyan
|
||||
dotnet tool install --global dotnet-sonarscanner
|
||||
}
|
||||
|
||||
Write-Host "Cleaning previous coverage artifacts..." -ForegroundColor Cyan
|
||||
Get-ChildItem -Path tests -Directory -Filter TestResults -Recurse -ErrorAction SilentlyContinue |
|
||||
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Write-Host "sonarscanner begin..." -ForegroundColor Cyan
|
||||
dotnet sonarscanner begin `
|
||||
/k:$projectKey `
|
||||
/d:sonar.host.url=$sonarHost `
|
||||
/d:sonar.token=$env:SONAR_TOKEN `
|
||||
/d:sonar.cs.opencover.reportsPaths="tests/**/TestResults/**/coverage.opencover.xml" `
|
||||
/d:sonar.exclusions="**/bin/**,**/obj/**" `
|
||||
/d:sonar.coverage.exclusions="tests/**,**/*.Tests/**" `
|
||||
/d:sonar.scanner.scanAll=false
|
||||
if ($LASTEXITCODE -ne 0) { throw "sonarscanner begin failed ($LASTEXITCODE)" }
|
||||
|
||||
Write-Host "dotnet build..." -ForegroundColor Cyan
|
||||
dotnet build $solution --configuration Release
|
||||
if ($LASTEXITCODE -ne 0) { throw "build failed ($LASTEXITCODE)" }
|
||||
|
||||
Write-Host "dotnet test (with coverage)..." -ForegroundColor Cyan
|
||||
dotnet test $solution `
|
||||
--configuration Release --no-build `
|
||||
--settings coverlet.runsettings `
|
||||
--collect "XPlat Code Coverage"
|
||||
# don't throw on test failures — we still want the analysis to upload
|
||||
if ($LASTEXITCODE -ne 0) { Write-Warning "some tests failed (exit $LASTEXITCODE); continuing so issues still upload" }
|
||||
|
||||
Write-Host "sonarscanner end..." -ForegroundColor Cyan
|
||||
dotnet sonarscanner end /d:sonar.token=$env:SONAR_TOKEN
|
||||
if ($LASTEXITCODE -ne 0) { throw "sonarscanner end failed ($LASTEXITCODE)" }
|
||||
|
||||
Write-Host "Done. Open $sonarHost/dashboard?id=$projectKey" -ForegroundColor Green
|
||||
Reference in New Issue
Block a user