Skip to content

Feat/sonarqube

Feat/sonarqube #9

Workflow file for this run

name: SonarCloud Analysis
on:
push:
branches: [main, master, develop]
pull_request:
branches: [main, master, develop]
workflow_dispatch:
jobs:
sonarcloud:
name: Build, Test & Sonar Scan
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
# =====================================================
# BACKEND (Python / FastAPI) – Tests + Coverage
# =====================================================
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install backend dependencies
working-directory: project/backend
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install "pytest-cov>=5,<7" "coverage>=7"
- name: Run backend tests with coverage
working-directory: project
run: |
python -m pytest \
--cov=backend \
--cov-report=xml:backend/coverage.xml \
--cov-report=term-missing \
backend/tests/
continue-on-error: true
# =====================================================
# FRONTEND (Next.js / TypeScript) – Tests + Coverage
# =====================================================
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: project/frontend/package-lock.json
- name: Install frontend dependencies
working-directory: project/frontend
run: npm ci
- name: Run frontend tests with coverage
working-directory: project/frontend
run: npm run test:coverage --if-present
continue-on-error: true
# =====================================================
# SONARCLOUD SCAN
# =====================================================
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@v3
with:
projectBaseDir: project
args: >
-Dsonar.projectKey=GalacticCodeGambit_LazyCook
-Dsonar.organization=galacticcodegambit
-Dproject.settings=sonar-project.properties
-Dsonar.python.coverage.reportPaths=backend/coverage.xml
-Dsonar.javascript.lcov.reportPaths=frontend/coverage/lcov.info
-Dsonar.typescript.lcov.reportPaths=frontend/coverage/lcov.info
-Dsonar.python.version=3.10
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: SonarCloud Quality Gate check
id: sonar-qg
uses: SonarSource/sonarqube-quality-gate-action@master
timeout-minutes: 5
continue-on-error: true
with:
scanMetadataReportFile: project/.scannerwork/report-task.txt
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
# =====================================================
# METRIKEN ALS MARKDOWN-SUMMARY IN GITHUB AUSGEBEN
# =====================================================
- name: Render SonarCloud metrics to Job Summary
if: always()
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST: https://sonarcloud.io
QG_STATUS: ${{ steps.sonar-qg.outputs.quality-gate-status }}
run: |
set +e
PROJECT_KEY=$(grep -E '^\s*sonar.projectKey\s*=' project/sonar-project.properties \
| head -1 | cut -d'=' -f2 | tr -d ' \r\n')
METRICS="alert_status,bugs,vulnerabilities,code_smells,coverage,line_coverage,branch_coverage,duplicated_lines_density,duplicated_blocks,duplicated_files,complexity,cognitive_complexity,ncloc,sqale_rating,reliability_rating,security_rating,sqale_index"
API_URL="${SONAR_HOST}/api/measures/component?component=${PROJECT_KEY}&metricKeys=${METRICS}"
HTTP_CODE=$(curl -s -o /tmp/sonar.json -w "%{http_code}" \
-u "${SONAR_TOKEN}:" "${API_URL}")
RESP=$(cat /tmp/sonar.json)
HAS_MEASURES=$(echo "${RESP}" | jq -r 'if (.component? and .component.measures?) then "yes" else "no" end' 2>/dev/null)
SUMMARY_FILE="${GITHUB_STEP_SUMMARY}"
if [ "${HTTP_CODE}" != "200" ] || [ "${HAS_MEASURES}" != "yes" ]; then
{
echo "## SonarCloud Analyse"
echo ""
echo ":warning: Konnte die Metriken nicht abrufen."
echo ""
echo "- **HTTP Status:** ${HTTP_CODE}"
echo "- **Project Key:** ${PROJECT_KEY}"
echo "- **Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
} >> "$SUMMARY_FILE"
exit 0
fi
get() {
echo "${RESP}" | jq -r --arg k "$1" \
'if (.component? and .component.measures?) then
((.component.measures[] | select(.metric==$k) | .value) // "-")
else "-" end'
}
get_sub() {
local sub_resp
sub_resp=$(curl -s -u "${SONAR_TOKEN}:" -G \
--data-urlencode "component=${PROJECT_KEY}:$1" \
--data-urlencode "metricKeys=$2" \
"${SONAR_HOST}/api/measures/component")
echo "${sub_resp}" | jq -r --arg k "$2" \
'if (.component? and .component.measures?) then
((.component.measures[] | select(.metric==$k) | .value) // "-")
else "-" end' 2>/dev/null || echo "-"
}
rating() {
case "$(get "$1")" in
1.0) echo "A" ;; 2.0) echo "B" ;; 3.0) echo "C" ;;
4.0) echo "D" ;; 5.0) echo "E" ;; *) echo "-" ;;
esac
}
DEBT_MIN=$(get sqale_index)
if [[ "${DEBT_MIN}" =~ ^[0-9]+$ ]]; then
DEBT_H=$((DEBT_MIN / 60)); DEBT_REST=$((DEBT_MIN % 60))
DEBT_FMT="${DEBT_H}h ${DEBT_REST}min"
else
DEBT_FMT="-"
fi
if [ "${QG_STATUS}" = "PASSED" ] || [ "$(get alert_status)" = "OK" ]; then
QG_ICON="PASSED"; QG_EMOJI=":white_check_mark:"
else
QG_ICON="FAILED"; QG_EMOJI=":x:"
fi
{
echo "## SonarCloud Analyse – LazyCook"
echo ""
echo "**Quality Gate:** ${QG_EMOJI} **${QG_ICON}**"
echo "**Dashboard:** [${SONAR_HOST}/project/overview?id=${PROJECT_KEY}](${SONAR_HOST}/project/overview?id=${PROJECT_KEY})"
echo ""
echo "### Coverage"
echo ""
echo "| Komponente | Coverage | Line Coverage | Branch Coverage | LoC |"
echo "|---|---|---|---|---|"
echo "| **Gesamt** | $(get coverage)% | $(get line_coverage)% | $(get branch_coverage)% | $(get ncloc) |"
echo "| Backend (Python) | $(get_sub backend coverage)% | $(get_sub backend line_coverage)% | $(get_sub backend branch_coverage)% | $(get_sub backend ncloc) |"
echo "| Frontend (TS/JS/CSS) | $(get_sub frontend/app coverage)% | $(get_sub frontend/app line_coverage)% | $(get_sub frontend/app branch_coverage)% | $(get_sub frontend/app ncloc) |"
echo ""
echo "### Duplikate & Komplexität"
echo ""
echo "| Bereich | Metrik | Wert |"
echo "|---|---|---|"
echo "| Duplikate | Duplizierte Zeilen | $(get duplicated_lines_density)% |"
echo "| Duplikate | Duplizierte Blöcke | $(get duplicated_blocks) |"
echo "| Duplikate | Betroffene Dateien | $(get duplicated_files) |"
echo "| Komplexität | Zyklomatische Komplexität | $(get complexity) |"
echo "| Komplexität | Cognitive Complexity | $(get cognitive_complexity) |"
echo ""
echo "### Alle Dateien mit Duplikaten"
echo ""
} >> "$SUMMARY_FILE"
TREE_URL="${SONAR_HOST}/api/measures/component_tree?component=${PROJECT_KEY}&metricKeys=duplicated_blocks,duplicated_lines,duplicated_lines_density,ncloc&qualifiers=FIL&ps=500&s=metric&metricSort=duplicated_blocks&asc=false"
TREE_RESP=$(curl -s -u "${SONAR_TOKEN}:" "${TREE_URL}")
FILES_WITH_DUPS=$(echo "${TREE_RESP}" | jq '[.components[]? | select((.measures[]? | select(.metric=="duplicated_blocks") | .value | tonumber) > 0)] | length' 2>/dev/null || echo "0")
{
if [ "${FILES_WITH_DUPS}" = "0" ] || [ -z "${FILES_WITH_DUPS}" ]; then
echo "_Keine Datei hat aktuell Duplikate._"
echo ""
else
echo "_Insgesamt **${FILES_WITH_DUPS}** Dateien mit mindestens einem Duplikat-Block._"
echo ""
echo "| Datei | Blöcke | Dupl. Zeilen | Anteil | LoC |"
echo "|---|---:|---:|---:|---:|"
echo "${TREE_RESP}" | jq -r '
.components[]?
| (.path // .key) as $p
| ((.measures[]? | select(.metric=="duplicated_blocks") | .value) // "0") as $b
| ((.measures[]? | select(.metric=="duplicated_lines") | .value) // "0") as $l
| ((.measures[]? | select(.metric=="duplicated_lines_density") | .value) // "0") as $pct
| ((.measures[]? | select(.metric=="ncloc") | .value) // "-") as $n
| select(($b | tonumber) > 0)
| "| " + $p + " | " + $b + " | " + $l + " | " + $pct + "% | " + $n + " |"
'
echo ""
fi
echo "[Volle Duplikat-Ansicht in SonarCloud öffnen](${SONAR_HOST}/component_measures?id=${PROJECT_KEY}&metric=duplicated_lines_density&view=list)"
echo ""
echo "### Ratings & Issues"
echo ""
echo "| Bewertung | Wert | Anzahl |"
echo "|---|---|---|"
echo "| Reliability Rating | $(rating reliability_rating) | $(get bugs) Bugs |"
echo "| Security Rating | $(rating security_rating) | $(get vulnerabilities) Vulnerabilities |"
echo "| Maintainability Rating | $(rating sqale_rating) | $(get code_smells) Code Smells |"
echo "| Technische Schuld | ${DEBT_FMT} | – |"
} >> "$SUMMARY_FILE"
- name: Fail job on Quality Gate failure
if: steps.sonar-qg.outputs.quality-gate-status == 'FAILED'
run: |
echo "::error::SonarCloud Quality Gate ist FAILED – siehe Job Summary."
exit 1