Build: Add SonarCloud CI workflow (#22960)

* Add SonarCloud CI workflow

Adds a manual-dispatch GitHub Actions workflow for SonarQube Cloud
analysis (build, unit test coverage, scan). Moves file_header_template
and SA1636/SA1633 suppression from .editorconfig comments and
.globalconfig into the active .editorconfig .NET language conventions
section, removing the duplicated suppression from .globalconfig.

* Remove branch filter from pull_request trigger in SonarCloud workflow

Runs analysis on all PRs regardless of target branch.

* Adjust sonarcloud gh action based on feedback

* Add .sonarqube to .gitignore

* Attempt to split build and analysis in order to be able to run in PRs from forks

* Adjust SonarCloud workflows

* Rename SonarCloud workflows to reflect their actual purpose

* Remove sonar.coverage.exclusions

* Include .github in sonar analysis

* Include build directory in sonar analysis

* Apply sonarcloud workflow fixes from test branch

* Remove setup-dotnet step from upload workflow

* Use default branch from context instead of hardcoded main in analysis workflow

* Update checkout action to v6 in upload workflow

* Add actions: read permission to upload workflow

* Enable SCM integration in upload workflow
This commit is contained in:
Laura Neto
2026-06-02 14:49:10 +02:00
committed by GitHub
parent 4e815d7e9d
commit a86777a8f2
6 changed files with 206 additions and 13 deletions
+4 -12
View File
@@ -70,18 +70,6 @@ trim_trailing_whitespace = true
[*.less]
trim_trailing_whitespace = false
##########################################
# File Header (Uncomment to support file headers)
# https://docs.microsoft.com/visualstudio/ide/reference/add-file-header
##########################################
# [*.{cs,csx,cake,vb,vbx}]
file_header_template = Copyright (c) Umbraco.\nSee LICENSE for more details.
# SA1636: File header copyright text should match
# Justification: .editorconfig supports file headers. If this is changed to a value other than "none", a stylecop.json file will need to added to the project.
# dotnet_diagnostic.SA1636.severity = none
##########################################
# .NET Language Conventions
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions
@@ -136,6 +124,10 @@ dotnet_code_quality_unused_parameters = all:warning
dotnet_style_operator_placement_when_wrapping = end_of_line
# https://github.com/dotnet/roslyn/pull/40070
dotnet_style_prefer_simplified_interpolation = true:warning
# File header preferences
file_header_template = Copyright (c) Umbraco.\nSee LICENSE for more details.
dotnet_diagnostic.SA1633.severity = none # Suppressed until we decide to enforce it
dotnet_diagnostic.SA1636.severity = none # Suppressed since we are using StyleCop
# C# Code Style Settings
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#c-code-style-settings
+93
View File
@@ -0,0 +1,93 @@
name: "SonarQube Cloud - Analysis"
# This workflow runs without secrets so it is safe to trigger on fork PRs.
# It compiles the solution, runs unit tests, and uploads the Roslyn analysis
# output as an artifact. The upload workflow then picks it up and uploads
# results to SonarCloud using the SONAR_TOKEN secret.
on:
push:
branches:
- main
- "v*/dev"
- "v*/main"
- "release/*"
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch:
permissions:
contents: read
env:
SONAR_PROJECT_KEY: umbraco_Umbraco-CMS
SONAR_ORGANIZATION: umbraco
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
build:
name: Build and collect analysis data
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup .NET from global.json
uses: actions/setup-dotnet@v5
- name: Cache SonarQube packages
uses: actions/cache@v5
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Install tools
run: |
dotnet tool install --global dotnet-sonarscanner
dotnet tool install --global dotnet-coverage
- name: Load sonar params
env:
IS_FORK: ${{ github.event.pull_request.head.repo.fork }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
if [ "$IS_FORK" = "true" ]; then
# For fork PRs, read sonar params from the default branch.
echo "SONARQUBE_SCANNER_PARAMS=$(git show origin/$DEFAULT_BRANCH:.github/workflows/sonarcloud/sonar-params.json | jq -c .)" >> $GITHUB_ENV
else
echo "SONARQUBE_SCANNER_PARAMS=$(jq -c . .github/workflows/sonarcloud/sonar-params.json)" >> $GITHUB_ENV
fi
- name: Begin analysis
run: |
dotnet-sonarscanner begin \
/k:"$SONAR_PROJECT_KEY" \
/o:"$SONAR_ORGANIZATION"
- name: Restore
run: dotnet restore umbraco.sln
- name: Build solution
run: GITHUB_ENV=/dev/null dotnet build umbraco.sln --no-restore -clp:ErrorsOnly # prevent sonar MSBuild integration from writing malformed values to $GITHUB_ENV
- name: Run unit tests with coverage
run: |
dotnet-coverage collect \
"dotnet test tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj --no-build" \
--output TestResults/coverage.xml \
--output-format xml
- name: Upload analysis data
uses: actions/upload-artifact@v7
with:
name: analysis-data
include-hidden-files: true # required to capture the .sonarqube directory
path: |
.sonarqube/
TestResults/
retention-days: 1
+98
View File
@@ -0,0 +1,98 @@
name: "SonarQube Cloud - Upload"
# This workflow runs after the analysis workflow completes. It downloads the
# Roslyn analysis output and coverage data produced by the analysis, then uploads
# them to SonarCloud using the SONAR_TOKEN secret. Because it is triggered via
# workflow_run, it always runs in the base repository context - even for fork
# PRs - giving it safe access to secrets.
on:
workflow_run:
workflows: ["SonarQube Cloud - Analysis"]
types: [completed]
permissions:
contents: read
actions: read
env:
SONAR_PROJECT_KEY: umbraco_Umbraco-CMS
SONAR_ORGANIZATION: umbraco
jobs:
analyze:
name: Upload analysis
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success' # only run if the build succeeded
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 0
- name: Download analysis data
uses: actions/download-artifact@v8
with:
name: analysis-data
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
path: analysis-data
- name: Cache SonarQube packages
uses: actions/cache@v5
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Install sonarscanner
run: |
dotnet tool install --global dotnet-sonarscanner
- name: Set analysis context
env:
IS_FORK: ${{ github.event.workflow_run.head_repository.full_name != github.repository }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
WORKFLOW_EVENT: ${{ github.event.workflow_run.event }}
WR_PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }}
WR_PR_HEAD_REF: ${{ github.event.workflow_run.pull_requests[0].head.ref }}
WR_PR_BASE_REF: ${{ github.event.workflow_run.pull_requests[0].base.ref }}
WR_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
run: |
if [ "$IS_FORK" = "true" ]; then
echo "SONARQUBE_SCANNER_PARAMS=$(git show origin/$DEFAULT_BRANCH:.github/workflows/sonarcloud/sonar-params.json | jq -c .)" | tee -a $GITHUB_ENV # read from default branch for fork PRs
else
echo "SONARQUBE_SCANNER_PARAMS=$(jq -c . .github/workflows/sonarcloud/sonar-params.json)" | tee -a $GITHUB_ENV
fi
if [ "$WORKFLOW_EVENT" = "pull_request" ]; then
echo "PR_NUMBER=$WR_PR_NUMBER" | tee -a $GITHUB_ENV
echo "PR_HEAD_REF=$WR_PR_HEAD_REF" | tee -a $GITHUB_ENV
echo "PR_BASE_REF=$WR_PR_BASE_REF" | tee -a $GITHUB_ENV
else
echo "BRANCH_NAME=$WR_HEAD_BRANCH" | tee -a $GITHUB_ENV
fi
- name: Begin analysis
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: |
dotnet-sonarscanner begin \
/k:"$SONAR_PROJECT_KEY" \
/o:"$SONAR_ORGANIZATION" \
/d:sonar.token="$SONAR_TOKEN" \
${BRANCH_NAME:+/d:sonar.branch.name="$BRANCH_NAME"} \
${PR_NUMBER:+/d:sonar.pullrequest.key="$PR_NUMBER"} \
${PR_HEAD_REF:+/d:sonar.pullrequest.branch="$PR_HEAD_REF"} \
${PR_BASE_REF:+/d:sonar.pullrequest.base="$PR_BASE_REF"}
- name: Restore analysis data from build
run: |
cp -r analysis-data/.sonarqube/out/. .sonarqube/out/
cp -r analysis-data/.sonarqube/conf/[0-9]* .sonarqube/conf/
cp -r analysis-data/TestResults TestResults
- name: End analysis
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: dotnet-sonarscanner end /d:sonar.token="$SONAR_TOKEN"
@@ -0,0 +1,8 @@
{
"sonar.cs.vscoveragexml.reportsPaths": "TestResults/coverage.xml",
"sonar.inclusions": "src/**,templates/**,tools/**,tests/**,.github/**,build/**",
"sonar.exclusions": "**/bin/**,**/obj/**,**/node_modules/**,**/lang/*.ts,**/mocks/**,**/wwwroot/**,**/dist-cms/**,**/*.generated.cs,src/Umbraco.Web.UI/umbraco/**,src/Umbraco.Cms.Persistence.EFCore.*/Migrations/**,src/Umbraco.Web.UI.Client/src/packages/core/backend-api/**,**/.nuget/**",
"sonar.test.inclusions": "tests/**,**/*.test.ts,**/*.spec.ts",
"sonar.typescript.tsconfigPaths": "src/Umbraco.Web.UI.Client/tsconfig.json,src/Umbraco.Web.UI.Client/tsconfig.node.json,src/Umbraco.Web.UI.Login/tsconfig.json",
"sonar.scanner.skipJreProvisioning": "true"
}
+3
View File
@@ -121,3 +121,6 @@ trace.zip
/tests/Umbraco.Tests.Integration/umbraco-package-schema.json
/src/Umbraco.Cms/appsettings-schema.json
.playwright-mcp/
# SonarQube local analysis cache
.sonarqube/
-1
View File
@@ -48,7 +48,6 @@ dotnet_analyzer_diagnostic.category-StyleCop.CSharp.OrderingRules.severity = sug
dotnet_analyzer_diagnostic.category-StyleCop.CSharp.MaintainabilityRules.severity = suggestion
dotnet_analyzer_diagnostic.category-StyleCop.CSharp.LayoutRules.severity = suggestion
dotnet_diagnostic.SA1636.severity = none # SA1636: File header copyright text should match
dotnet_diagnostic.SA1101.severity = none # PrefixLocalCallsWithThis - stylecop appears to be ignoring dotnet_style_qualification_for_*
dotnet_diagnostic.SA1309.severity = none # FieldNamesMustNotBeginWithUnderscore