Reusable GitHub Actions workflow for continuous code quality and security analysis using SonarCloud.
SonarCloud provides static analysis for:
- Code Quality: Bugs, code smells, technical debt
- Security: Vulnerabilities, security hotspots (OWASP Top 10)
- Test Coverage: Track and trend coverage over time
- Code Duplication: Identify duplicated code blocks
- Maintainability: Technical debt and complexity metrics
- Visit sonarcloud.io and sign in with GitHub
- Import your repository
- Note your organization and project key (format:
org_repo-name) - Generate token at sonarcloud.io/account/security
- Add
SONAR_TOKENto repository secrets
# .github/workflows/sonarcloud.yml
name: SonarCloud
on:
push:
branches: [main, develop]
pull_request:
types: [opened, synchronize, reopened]
jobs:
sonarcloud:
uses: ByronWilliamsCPA/.github/.github/workflows/python-sonarcloud.yml@d5cf99101d4150ae5832d154cb42993705a09e31 # v7.0.1
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
sonar-organization: 'your-org'
sonar-project-key: 'your-org_your-repo'| Input | Type | Description |
|---|---|---|
sonar-organization |
string | SonarCloud organization name |
sonar-project-key |
string | Project key (format: org_repo-name) |
| Input | Type | Default | Description |
|---|---|---|---|
python-version |
string | '3.12' | Single Python version used to build the project and run the analysis; feeds actions/setup-python |
sonar-python-version |
string | '' | Python versions the source supports, comma-separated (3.11,3.12,3.13). Sets sonar.python.version. Falls back to python-version when empty. See Declaring supported source versions |
source-directory |
string | 'src' | Source code directory |
coverage-paths |
string | 'coverage.xml' | Coverage report paths (comma-separated) |
coverage-exclusions |
string | See below | Paths to exclude from coverage |
test-exclusions |
string | '' | Paths to exclude from analysis |
extra-dependencies |
string | 'dev' | Additional uv sync extras |
pytest-args |
string | '' | Additional pytest arguments |
fail-on-quality-gate |
boolean | false | Fail workflow if quality gate fails |
skip-if-no-token |
boolean | true | Skip gracefully if SONAR_TOKEN missing |
timeout-minutes |
number | 15 | Job timeout |
additional-sonar-args |
string | '' | Extra SonarQube scanner arguments |
coverage-artifact-retention |
number | 7 | Days to retain artifacts |
Default Coverage Exclusions:
**/tests/**,**/validation/**,**/benchmarks/**,**/scripts/**
| Secret | Description |
|---|---|
SONAR_TOKEN |
SonarCloud authentication token (optional if skip-if-no-token: true) |
python-version and sonar-python-version answer two different questions. Keep them
separate:
| Input | Question it answers | Cardinality |
|---|---|---|
python-version |
Which interpreter builds the project and runs the scan? | Exactly one (actions/setup-python rejects a list) |
sonar-python-version |
Which versions must the source code remain valid on? | One or more, comma-separated |
Set sonar-python-version whenever your requires-python range is wider than the single
version used to build:
with:
python-version: '3.12' # builds and scans on 3.12
sonar-python-version: '3.11,3.12,3.13' # source must stay valid on all threeWhy it matters. SonarPython gates version-specific rules on every declared version,
not the highest one. Declaring only the build version tells the analyzer the project is
3.12-only, so it raises 3.12+ syntax rules (PEP 695 type aliases, python:S6794 and
python:S6796) against code that still has to run on 3.11. Adding 3.11 to the list
correctly silences them.
The inverse also holds, and it loses findings silently. Because the gate requires all
declared versions to meet a rule's threshold, declaring versions below your real floor
suppresses rules you actually want. A project whose requires-python is >=3.12 that
declares 3.9,3.12 gets no 3.10+, 3.11+, or 3.12+ rules at all, with no warning. Declare
the range that matches requires-python and nothing wider.
Format. Comma-separated MAJOR.MINOR values. Whitespace around the commas and at the
ends is tolerated and stripped, so both 3.11,3.12 and SonarSource's own documented
3.11, 3.12 spacing work. Anything else fails the job with an explicit message rather than
reaching the scanner as a malformed argument: a patch version (3.12.1), a range operator
(>=3.11), a trailing comma, or whitespace inside a version (3 .11), which is rejected
rather than silently normalized into a different version.
When the input is empty. The fallback derives sonar.python.version from
python-version instead, and it is deliberately lenient there, because python-version
feeds actions/setup-python and its accepted syntax is much wider than a Sonar version
list. The leading MAJOR.MINOR is extracted, so 3.12.1 resolves to 3.12, >=3.11 to
3.11, and pypy3.10 to 3.10. All of these are legal python-version values and none of
them should fail an analysis. Only a value with no derivable minor version (3.x, pypy,
or empty) fails, and the message points at sonar-python-version rather than guessing a
version you never declared. The strict list validation above applies to
sonar-python-version alone, since that input means a Sonar version list and nothing else.
name: Code Quality
on:
push:
branches: [main]
pull_request:
jobs:
sonarcloud:
uses: ByronWilliamsCPA/.github/.github/workflows/python-sonarcloud.yml@d5cf99101d4150ae5832d154cb42993705a09e31 # v7.0.1
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
sonar-organization: 'acme-corp'
sonar-project-key: 'acme-corp_my-project'jobs:
sonarcloud:
uses: ByronWilliamsCPA/.github/.github/workflows/python-sonarcloud.yml@d5cf99101d4150ae5832d154cb42993705a09e31 # v7.0.1
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
sonar-organization: 'acme-corp'
sonar-project-key: 'acme-corp_my-project'
source-directory: 'app' # Custom source path
coverage-paths: 'coverage.xml,coverage-integration.xml'jobs:
sonarcloud:
uses: ByronWilliamsCPA/.github/.github/workflows/python-sonarcloud.yml@d5cf99101d4150ae5832d154cb42993705a09e31 # v7.0.1
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
sonar-organization: 'acme-corp'
sonar-project-key: 'acme-corp_my-project'
coverage-paths: 'coverage.xml,integration-coverage.xml,e2e-coverage.xml'jobs:
sonarcloud:
uses: ByronWilliamsCPA/.github/.github/workflows/python-sonarcloud.yml@d5cf99101d4150ae5832d154cb42993705a09e31 # v7.0.1
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
sonar-organization: 'acme-corp'
sonar-project-key: 'acme-corp_my-project'
fail-on-quality-gate: true # Block merge on failurejobs:
sonarcloud:
uses: ByronWilliamsCPA/.github/.github/workflows/python-sonarcloud.yml@d5cf99101d4150ae5832d154cb42993705a09e31 # v7.0.1
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
sonar-organization: 'acme-corp'
sonar-project-key: 'acme-corp_my-project'
coverage-exclusions: '**/tests/**,**/migrations/**,**/fixtures/**'
test-exclusions: '**/integration_tests/**'jobs:
sonarcloud:
uses: ByronWilliamsCPA/.github/.github/workflows/python-sonarcloud.yml@d5cf99101d4150ae5832d154cb42993705a09e31 # v7.0.1
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
sonar-organization: 'acme-corp'
sonar-project-key: 'acme-corp_my-project'
additional-sonar-args: >
-Dsonar.projectVersion=${{ github.ref_name }}
-Dsonar.links.homepage=https://example.com
-Dsonar.links.ci=${{ github.server_url }}/${{ github.repository }}/actionsFor advanced configuration, create sonar-project.properties in repository root:
# Project identification
sonar.organization=acme-corp
sonar.projectKey=acme-corp_my-project
sonar.projectName=My Project
sonar.projectVersion=1.0.0
# Source configuration
sonar.sources=src
sonar.tests=tests
# Every version the source must stay valid on, not just the build version.
sonar.python.version=3.11,3.12,3.13
# Coverage configuration
sonar.python.coverage.reportPaths=coverage.xml
sonar.coverage.exclusions=**/tests/**,**/migrations/**
# Code analysis
sonar.exclusions=**/generated/**
sonar.test.exclusions=**/integration_tests/**
# Additional settings
sonar.sourceEncoding=UTF-8Note: Workflow inputs override properties file settings. The workflow always passes
-Dsonar.python.version on the command line, so a sonar.python.version key in this file
is ignored; set the sonar-python-version input instead.
Configure quality gates in SonarCloud dashboard:
| Metric | Threshold | Type |
|---|---|---|
| Coverage on New Code | ≥ 80% | New Code |
| Duplicated Lines on New Code | ≤ 3% | New Code |
| Maintainability Rating on New Code | ≤ A | New Code |
| Reliability Rating on New Code | ≤ A | New Code |
| Security Rating on New Code | ≤ A | New Code |
| Security Hotspots Reviewed | 100% | Overall |
- Go to SonarCloud dashboard
- Organization Settings → Quality Gates
- Create new gate or modify existing
- Set conditions for Overall Code and New Code
- Assign to project
SonarCloud automatically decorates pull requests with:
- Overall Status: Quality gate pass/fail
- New Issues: Bugs, vulnerabilities, code smells
- Coverage Impact: Coverage change on PR
- Inline Comments: Issues annotated in code
SonarCloud Quality Gate: Passed ✅
3 new issues:
- 1 Bug
- 2 Code Smells
Coverage: 85.2% (+2.1%)
View details: https://sonarcloud.io/dashboard?id=acme-corp_my-project&pullRequest=42
Error: Workflow skipped
Solution:
- Generate token at sonarcloud.io/account/security
- Add to repository: Settings → Secrets → Actions
- Name:
SONAR_TOKEN
Alternative: Set skip-if-no-token: false to require token
Warning: "Coverage report not found"
Causes:
- Tests failed to run
- Coverage not generated in XML format
- Incorrect
coverage-pathsconfiguration
Solution:
# Ensure pytest generates coverage.xml
pytest --cov=src --cov-report=xml:coverage.xmlError: "Quality Gate failed"
Review:
- Visit SonarCloud dashboard
- Check New Code tab for issues
- Address bugs, vulnerabilities, code smells
- Improve coverage if below threshold
Warning: "Shallow clone detected"
Solution: Workflow automatically uses fetch-depth: 0
Error: Job timeout after 15 minutes
Solution:
with:
timeout-minutes: 30 # Increase for large projectsname: CI and Quality
on: [push, pull_request]
jobs:
ci:
uses: ByronWilliamsCPA/.github/.github/workflows/python-ci.yml@d5cf99101d4150ae5832d154cb42993705a09e31 # v7.0.1
sonarcloud:
needs: ci
uses: ByronWilliamsCPA/.github/.github/workflows/python-sonarcloud.yml@d5cf99101d4150ae5832d154cb42993705a09e31 # v7.0.1
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
sonar-organization: 'acme-corp'
sonar-project-key: 'acme-corp_my-project'jobs:
security:
uses: ByronWilliamsCPA/.github/.github/workflows/python-security-analysis.yml@d5cf99101d4150ae5832d154cb42993705a09e31 # v7.0.1
sonarcloud:
uses: ByronWilliamsCPA/.github/.github/workflows/python-sonarcloud.yml@d5cf99101d4150ae5832d154cb42993705a09e31 # v7.0.1
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
sonar-organization: 'acme-corp'
sonar-project-key: 'acme-corp_my-project'| Metric | Description |
|---|---|
| Bugs | Issues that represent coding errors |
| Code Smells | Maintainability issues |
| Technical Debt | Estimated time to fix all code smells |
| Duplications | Percentage of duplicated code |
| Cognitive Complexity | How difficult code is to understand |
| Metric | Description |
|---|---|
| Vulnerabilities | Security issues (OWASP Top 10) |
| Security Hotspots | Code requiring security review |
| Security Rating | A (best) to E (worst) |
| Metric | Description |
|---|---|
| Line Coverage | % of lines executed by tests |
| Branch Coverage | % of branches executed by tests |
| Coverage on New Code | Coverage of code added in PR |
Set realistic thresholds for your project maturity level.
Prioritize quality on new code additions over fixing legacy issues.
Manually review all security hotspots flagged by SonarCloud.
Track technical debt trend and allocate time for remediation.
Target ≥80% coverage on new code to prevent regressions.
Exclude generated code, migrations, and test fixtures from analysis.
- Python Security Analysis - Bandit, OSV-Scanner
- Python CI - Testing and linting
- Qlty Coverage - Alternative coverage tracking
See Also:
- USAGE_EXAMPLES.md - Complete workflow examples
- examples/ - Ready-to-use configurations