From 1b83a68fc6ce708b8b19c3e731eadaf52d0e162f Mon Sep 17 00:00:00 2001 From: Konrad Ferdinand Heimel Date: Thu, 6 Aug 2026 16:20:08 +0200 Subject: [PATCH] :construction_worker: ci(sonar): enforce the SonarCloud quality gate in CI OD-3 set continue-on-error on the sonarqube job so an ERROR quality gate could not veto the very lanes (E4-S01, E4-S02) that were remediating it, with an explicit instruction to revisit once the gate went green. It went OK on 2026-08-06 after Automatic Analysis was disabled and CI analysis actually ran (new_duplicated_lines_density 4.6 -> 0.0, new_coverage 93.6). Two independent holes had to close, since either alone is a false sense of safety: - continue-on-error swallowed hard scanner failures. It hid an exit 3 ("running CI analysis while Automatic Analysis is enabled") for a full day while the workflow reported success. - the scan was fire-and-forget. Without sonar.qualitygate.wait the job went green the moment the report was uploaded, so a gate flipping to ERROR would never have failed anything. An earlier revision of this commit removed only the first and claimed the gate was blocking; that claim was false. Also wires hack/test/*.sh into task check via a new guards task. Ten config regression guards existed but nothing ever ran them, so "guarded by" was aspirational across the whole repo. Fork PRs receive no secrets, so the scan step self-skips via the SONAR_TOKEN gate rather than failing. The real cost is that CI now hard-couples to sonarcloud.io availability for in-repo branches; that is the intended trade. --- .github/workflows/ci.yaml | 12 +++-- Taskfile.yml | 19 +++++++- hack/test/sonar_blocking_gate_test.sh | 66 +++++++++++++++++++++++++++ sonar-project.properties | 5 ++ 4 files changed, 96 insertions(+), 6 deletions(-) create mode 100755 hack/test/sonar_blocking_gate_test.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3e17809..0ec0b4e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -170,16 +170,18 @@ jobs: - name: task changelog:verify run: task changelog:verify - # SonarCloud: needs repo secret SONAR_TOKEN. Automatic Analysis must stay off - # (mutually exclusive with CI analysis + Go coverage). Non-blocking per OD-3 — - # quality gate is ERROR until E4-S02 clears duplication; continue-on-error so - # that does not block the remediating lanes. + # SonarCloud: needs repo secret SONAR_TOKEN. Automatic Analysis must stay OFF — + # it is mutually exclusive with CI analysis (the scanner exits 3) and it ignores + # sonar-project.properties, so the E4-S02 test exclusions silently would not + # apply. Blocking as of 2026-08-06 (OD-3 revisit): the continue-on-error escape + # hatch is gone AND sonar.qualitygate.wait=true is set, so both a scanner + # failure and an ERROR quality gate now fail this job. Fork PRs receive no + # secrets, so the scan step self-skips rather than failing. sonarqube: name: sonarqube runs-on: ubuntu-latest timeout-minutes: 20 needs: [check] - continue-on-error: true steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 diff --git a/Taskfile.yml b/Taskfile.yml index a17e67b..99ef467 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -103,6 +103,22 @@ tasks: cmds: - bash hack/pinned-actions.sh + guards: + desc: Run the hack/test/*.sh config regression guards + cmds: + - | + fail=0 + for t in hack/test/*.sh; do + if bash "$t" >/dev/null 2>&1; then + echo "ok: $t" + else + echo "FAIL: $t" >&2 + bash "$t" || true + fail=1 + fi + done + exit "$fail" + tools:git-cliff: desc: Download pinned git-cliff binary ({{.GIT_CLIFF_VERSION}}) status: @@ -135,7 +151,7 @@ tasks: - bash hack/verify-changelog.sh check: - desc: Local gate — fmt, lint, arch, coverage≥90%, gitleaks, build, REWE-trace, pinned-actions + desc: Local gate — fmt, lint, arch, coverage≥90%, gitleaks, build, REWE-trace, pinned-actions, guards cmds: - task: fmt-check - task: vet @@ -146,6 +162,7 @@ tasks: - task: build - task: rewe-trace - task: pinned-actions + - task: guards docs:install: desc: Install the locked MkDocs toolchain into .venv-docs diff --git a/hack/test/sonar_blocking_gate_test.sh b/hack/test/sonar_blocking_gate_test.sh new file mode 100755 index 0000000..8260d91 --- /dev/null +++ b/hack/test/sonar_blocking_gate_test.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Assert the SonarCloud job is a BLOCKING gate (OD-3 revisit, 2026-08-06). +# +# OD-3 made `sonarqube` non-blocking so the quality gate could not veto the very +# lanes (E4-S01/E4-S02) that were fixing it. That gate went OK once Automatic +# Analysis was disabled and CI analysis actually ran, so the escape hatch came +# out. Blocking here means BOTH halves, because either alone is a false sense of +# safety: +# +# 1. no `continue-on-error` — a scanner failure fails the job. Without this a +# hard exit 3 ("running CI analysis while Automatic Analysis is enabled") +# was swallowed for a day while the workflow reported success. +# 2. `sonar.qualitygate.wait=true` — the scanner polls for the gate verdict +# instead of fire-and-forget. Without this the job goes green the moment the +# report is uploaded, so a gate that flips to ERROR is never noticed. +# +# Run: bash hack/test/sonar_blocking_gate_test.sh +set -euo pipefail + +root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +wf="${root}/.github/workflows/ci.yaml" +props="${root}/sonar-project.properties" + +fail=0 + +# 1. No job or step in ci.yaml may opt out of failing the workflow. Deliberately +# whole-file: nothing in this workflow has a legitimate use for it today. +# Matches bare, quoted and expression forms. +if grep -Eq "^[[:space:]]*continue-on-error:[[:space:]]*(\"?true\"?|'true'|\\\$\\{\\{)" "$wf"; then + echo "FAIL: continue-on-error opt-out still present in ci.yaml" >&2 + grep -nE '^[[:space:]]*continue-on-error:' "$wf" >&2 + fail=1 +else + echo "ok: no continue-on-error opt-out in ci.yaml" +fi + +# 2. The sonarqube job must exist and keep needing `check` — the scan consumes +# coverage.out via the artifact that `check` uploads. +if grep -Eq '^[[:space:]]{2}sonarqube:' "$wf"; then + echo "ok: sonarqube job present" +else + echo "FAIL: sonarqube job missing from ci.yaml" >&2 + fail=1 +fi + +if grep -Eq '^[[:space:]]*needs:[[:space:]]*\[[[:space:]]*check[[:space:]]*\]' "$wf"; then + echo "ok: sonarqube needs [check] (coverage artifact producer)" +else + echo "FAIL: sonarqube no longer declares needs: [check]" >&2 + fail=1 +fi + +# 3. The scanner must wait for the quality gate verdict, not merely upload. +if grep -Eq '^sonar\.qualitygate\.wait[[:space:]]*=[[:space:]]*true' "$props"; then + echo "ok: sonar.qualitygate.wait=true" +else + echo "FAIL: sonar.qualitygate.wait=true missing from sonar-project.properties" >&2 + echo " without it the job passes on upload and never sees an ERROR gate" >&2 + fail=1 +fi + +if [[ "${fail}" -ne 0 ]]; then + echo "sonar_blocking_gate_test: RED" >&2 + exit 1 +fi +echo "sonar_blocking_gate_test: GREEN" diff --git a/sonar-project.properties b/sonar-project.properties index b6fc894..5409a15 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -13,6 +13,11 @@ sonar.tests=. sonar.test.inclusions=**/*_test.go sonar.go.coverage.reportPaths=coverage.out +# Make the CI job actually ENFORCE the gate rather than fire-and-forget: without +# this the scanner uploads and exits 0, so a quality gate that flips to ERROR +# leaves the workflow green. Guarded by hack/test/sonar_blocking_gate_test.sh. +sonar.qualitygate.wait=true + # Belt-and-suspenders with source exclusion above (cpd.exclusions alone was # insufficient when tests were dual-indexed as sources). sonar.cpd.exclusions=**/*_test.go