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