From 86d92efd2b8635ef5eb2e8763fe2a1c758afc43a Mon Sep 17 00:00:00 2001 From: shankar0123 Date: Thu, 30 Apr 2026 20:39:30 +0000 Subject: [PATCH] =?UTF-8?q?ci-pipeline-cleanup=20Phase=202:=20coverage=20t?= =?UTF-8?q?hresholds=20=E2=86=92=20YAML=20manifest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundle: ci-pipeline-cleanup, Phase 2 / frozen decision 0.3. Move 9 hardcoded coverage thresholds from inline bash to a YAML manifest at .github/coverage-thresholds.yml. The load-bearing per-package context (Bundle reference, HEAD measurement, gap rationale) survives in the YAML's `why:` field instead of in inline bash comments. Adding a new gated package: one YAML entry instead of ~30 lines of bash + 50 lines of comment. Coverage check logic extracted to scripts/check-coverage-thresholds.sh so the operator can run the same check locally: bash scripts/check-coverage-thresholds.sh ci.yml dropped 557 → 417 lines (-140, total Phase 1+2: -1071, -72% from baseline 1488). Same 9 floors, same fail-on-miss semantics — pure relocation: internal/service: 70 (was: 70) internal/api/handler: 75 (was: 75) internal/domain: 40 (was: 40) internal/api/middleware: 30 (was: 30) internal/crypto: 88 (was: 88) internal/connector/issuer/local: 86 (was: 86) internal/connector/issuer/acme: 80 (was: 80) internal/connector/issuer/stepca: 80 (was: 80) internal/mcp: 85 (was: 85) Sandbox verification: - ci.yml YAML-parses cleanly - coverage-thresholds.yml YAML-parses cleanly with all 9 entries - scripts/check-coverage-thresholds.sh extracts the (pkg, floor) table correctly from the YAML --- .github/coverage-thresholds.yml | 78 ++++++++++++++ .github/workflows/ci.yml | 152 ++------------------------- scripts/check-coverage-thresholds.sh | 61 +++++++++++ 3 files changed, 145 insertions(+), 146 deletions(-) create mode 100644 .github/coverage-thresholds.yml create mode 100755 scripts/check-coverage-thresholds.sh diff --git a/.github/coverage-thresholds.yml b/.github/coverage-thresholds.yml new file mode 100644 index 0000000..734e1d7 --- /dev/null +++ b/.github/coverage-thresholds.yml @@ -0,0 +1,78 @@ +# Coverage floors per gated package. +# +# Each entry: floor: , why: . +# Adding a new gated package: one entry here; CI's `Check Coverage Thresholds` +# step auto-picks up. Lowering a floor REQUIRES corresponding code-side test +# work — never lower the gate to make CI green. +# +# Per ci-pipeline-cleanup bundle Phase 2 / frozen decision 0.3. + +internal/service: + floor: 70 + why: | + Bundle R-CI-extended raise (post-Bundle-N.C-extended): service + 55 → 70. HEAD 73.4% (3pp margin). Prescribed Bundle R target + was 80; held lower to avoid false-positives on single low- + coverage files dragging the global per-file-average down. + +internal/api/handler: + floor: 75 + why: | + Bundle R-CI-extended raise: handler 60 → 75. HEAD 79.8% (4pp + margin). Prescribed Bundle R target was 80; held lower for + same reason as service layer. + +internal/domain: + floor: 40 + why: | + Domain layer is mostly type definitions + validators; 40% is + the load-bearing-paths floor. + +internal/api/middleware: + floor: 30 + why: | + Middleware coverage is per-handler-test-driven. 30% is the + floor that catches the wired-up middleware paths; the + unwired paths (alternative auth providers not currently + enabled) sit below. + +internal/crypto: + floor: 88 + why: | + Bundle R closure CI checkpoint #3: crypto floor lifted 85 → 88. + Post-Bundle-Q package-scoped coverage at HEAD: 88.2%. The + remaining ~12% gap is platform-failure branches (rand.Reader / + aes.NewCipher) that require interface seams the production + code doesn't use; closing them is tracked as R-CI-extended, + not Bundle R scope. + +internal/connector/issuer/local: + floor: 86 + why: | + Bundle R closure CI checkpoint #3: local-issuer floor lifted + 85 → 86. Post-Bundle-Q package-scoped coverage at HEAD: 86.7%. + The prescribed Bundle R target was 92, but reaching it + requires interface seams for crypto/x509 signing-error + branches — tracked as R-CI-extended. + +internal/connector/issuer/acme: + floor: 80 + why: | + Bundle R-CI-extended threshold raise (post-Bundle-J-extended): + ACME 50 → 80. The Pebble-style mock + per-CA failure tests + lift package-scoped ACME to 85.4%; gate at 80 with 5pp margin + to absorb the global-run per-file-average dip. + +internal/connector/issuer/stepca: + floor: 80 + why: | + Bundle L.B / Coverage-Audit C-005 — StepCA failure-mode + JWE + round-trip tests lift package from 52.1% to 90.4% (per-package + run). Floor at 80 with margin. + +internal/mcp: + floor: 85 + why: | + Bundle K / Coverage-Audit C-002 — MCP per-tool dispatch via + in-memory transport lifts package from 28.0% to 93.1% (per- + package run). Floor at 85. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 477d396..0703749 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,152 +86,12 @@ jobs: go test ./internal/service/... ./internal/api/handler/... ./internal/api/middleware/... ./internal/integration/... ./internal/connector/issuer/... ./internal/connector/target/... ./internal/connector/notifier/... ./internal/connector/discovery/... ./internal/crypto/... ./internal/mcp/... ./internal/cli/... ./internal/domain/... ./internal/validation/... ./internal/tlsprobe/... -count=1 -cover -coverprofile=coverage.out - name: Check Coverage Thresholds - run: | - # Extract per-package coverage from test output - echo "=== Coverage Report ===" - go tool cover -func=coverage.out | tail -1 - - # Check service layer coverage (target: 60%+) - SERVICE_COV=$(go tool cover -func=coverage.out | grep 'internal/service' | awk '{print $NF}' | sed 's/%//' | awk '{sum+=$1; n++} END {if(n>0) printf "%.1f", sum/n; else print "0"}') - echo "Service layer coverage: ${SERVICE_COV}%" - - # Check handler layer coverage (target: 60%+) - HANDLER_COV=$(go tool cover -func=coverage.out | grep 'internal/api/handler' | awk '{print $NF}' | sed 's/%//' | awk '{sum+=$1; n++} END {if(n>0) printf "%.1f", sum/n; else print "0"}') - echo "Handler layer coverage: ${HANDLER_COV}%" - - # Check domain layer coverage (target: 40%+) - DOMAIN_COV=$(go tool cover -func=coverage.out | grep 'internal/domain' | awk '{print $NF}' | sed 's/%//' | awk '{sum+=$1; n++} END {if(n>0) printf "%.1f", sum/n; else print "0"}') - echo "Domain layer coverage: ${DOMAIN_COV}%" - - # Check middleware layer coverage (target: 50%+) - MIDDLEWARE_COV=$(go tool cover -func=coverage.out | grep 'internal/api/middleware' | awk '{print $NF}' | sed 's/%//' | awk '{sum+=$1; n++} END {if(n>0) printf "%.1f", sum/n; else print "0"}') - echo "Middleware layer coverage: ${MIDDLEWARE_COV}%" - - # Check crypto package coverage (target: 85%+) - # M-8 rationale: encryption primitives are a security-critical gate. - # v2 format, key-derivation, fallback, and fail-closed sentinel paths - # all need exhaustive coverage to avoid silent regressions (CWE-916 / CWE-329). - CRYPTO_COV=$(go tool cover -func=coverage.out | grep 'internal/crypto' | awk '{print $NF}' | sed 's/%//' | awk '{sum+=$1; n++} END {if(n>0) printf "%.1f", sum/n; else print "0"}') - echo "Crypto package coverage: ${CRYPTO_COV}%" - - # Bundle-7 / Audit H-005 — extended crypto-cluster gates per CLAUDE.md. - # internal/pkcs7/ is at 100% at HEAD (encoder-only, exhaustively tested - # via Bundle-4 fuzz targets + unit tests). internal/connector/issuer/local/ - # is at 68.3% at HEAD; H-010 tracks the gap and will lift this floor - # to 85% once the missing CSR-validation + CA-cert-loading tests land. - PKCS7_COV=$(go tool cover -func=coverage.out | grep 'internal/pkcs7' | awk '{print $NF}' | sed 's/%//' | awk '{sum+=$1; n++} END {if(n>0) printf "%.1f", sum/n; else print "0"}') - echo "PKCS7 package coverage: ${PKCS7_COV}%" - - LOCAL_ISSUER_COV=$(go tool cover -func=coverage.out | grep 'internal/connector/issuer/local' | awk '{print $NF}' | sed 's/%//' | awk '{sum+=$1; n++} END {if(n>0) printf "%.1f", sum/n; else print "0"}') - echo "Local-issuer coverage: ${LOCAL_ISSUER_COV}%" - - # Bundle-J / Coverage-Audit C-001 (partial-closed) — ACME failure-mode - # batch lifts internal/connector/issuer/acme from 41.8% to ~55.6% - # (per-package package-scoped run). The global per-file average can - # come in lower because this awk pattern divides by file count - # rather than weighting by line count, but with the failure-mode - # tests landed every file in the package has at least 50% coverage. - # Floor set at 50 to accommodate the global-run arithmetic; bumps - # to 85 when Bundle J-extended (Pebble-style mock) lands and the - # IssueCertificate / solveAuthorizations* flows are exercisable. - ACME_COV=$(go tool cover -func=coverage.out | grep 'internal/connector/issuer/acme' | awk '{print $NF}' | sed 's/%//' | awk '{sum+=$1; n++} END {if(n>0) printf "%.1f", sum/n; else print "0"}') - echo "ACME issuer coverage: ${ACME_COV}%" - - # Bundle-L.B / Coverage-Audit C-005 — StepCA failure-mode + JWE - # round-trip tests lift internal/connector/issuer/stepca from - # 52.1% to 90.4% (per-package run). Floor at 80 with margin. - STEPCA_COV=$(go tool cover -func=coverage.out | grep 'internal/connector/issuer/stepca' | awk '{print $NF}' | sed 's/%//' | awk '{sum+=$1; n++} END {if(n>0) printf "%.1f", sum/n; else print "0"}') - echo "StepCA issuer coverage: ${STEPCA_COV}%" - - # Bundle-K / Coverage-Audit C-002 — MCP per-tool dispatch via - # in-memory transport lifts internal/mcp from 28.0% to 93.1% - # (per-package run). Floor at 85. - MCP_COV=$(go tool cover -func=coverage.out | grep 'internal/mcp/' | awk '{print $NF}' | sed 's/%//' | awk '{sum+=$1; n++} END {if(n>0) printf "%.1f", sum/n; else print "0"}') - echo "MCP coverage: ${MCP_COV}%" - - # Fail if thresholds not met. - # Bundle R-CI-extended raises (post-Bundle-N.C-extended): - # service 55 -> 70 (HEAD 73.4%; 3pp margin); handler 60 -> 75 - # (HEAD 79.8%; 4pp margin). Prescribed Bundle R target was 80; - # held lower to avoid false-positives on single low-coverage - # files dragging the global per-file-average down. - if [ "$(echo "$SERVICE_COV < 70" | bc -l)" -eq 1 ]; then - echo "::error::Service layer coverage ${SERVICE_COV}% is below 70% (Bundle R-CI-extended floor — add tests, do not lower the gate)" - exit 1 - fi - if [ "$(echo "$HANDLER_COV < 75" | bc -l)" -eq 1 ]; then - echo "::error::Handler layer coverage ${HANDLER_COV}% is below 75% (Bundle R-CI-extended floor — add tests, do not lower the gate)" - exit 1 - fi - if [ "$(echo "$DOMAIN_COV < 40" | bc -l)" -eq 1 ]; then - echo "::error::Domain layer coverage ${DOMAIN_COV}% is below 40% threshold" - exit 1 - fi - if [ "$(echo "$MIDDLEWARE_COV < 30" | bc -l)" -eq 1 ]; then - echo "::error::Middleware layer coverage ${MIDDLEWARE_COV}% is below 30% threshold" - exit 1 - fi - # Bundle R / Coverage Audit Closure — CI threshold raise checkpoint #3. - # Crypto package floor lifted 85 → 88. Post-Bundle-Q package-scoped - # coverage at HEAD: 88.2% (Bundle Q's gopter property tests don't add - # production-code coverage — they exercise the same paths via - # generative inputs). The remaining ~12% gap is platform-failure - # branches (rand.Reader / aes.NewCipher) that require interface seams - # the production code doesn't use; closing them is tracked as - # R-CI-extended, not Bundle R scope. - if [ "$(echo "$CRYPTO_COV < 88" | bc -l)" -eq 1 ]; then - echo "::error::Crypto package coverage ${CRYPTO_COV}% is below 88% (Bundle R closure floor — add tests, do not lower the gate)" - exit 1 - fi - # Bundle-7 / H-005: pkcs7 coverage is INFORMATIONAL only in this run. - # The global `go test -cover ./...` invocation in CI doesn't exercise - # internal/pkcs7's tests (they're primarily Fuzz* targets that - # require an explicit `-fuzz` invocation, plus encoder helpers - # exercised transitively). The deep-scan workflow runs - # `go test -cover ./internal/pkcs7/...` directly and confirmed 100% - # at Bundle-7 close — that's the load-bearing measurement. Keeping - # the global-run number visible here for trend-watching but not - # gating because 0% is a measurement artifact, not a regression. - echo "PKCS7 package coverage (global run, informational): ${PKCS7_COV}%" - # Bundle-9 / H-010 closure: local-issuer HARD gate at 85%. The - # transitional 60% floor (Bundle-7) was an explicit promise in the - # CI config that H-010 would raise it once CSR-validation + CA- - # cert-loading + key-rotation + key-encoding pin tests landed. - # Bundle-9 ships those tests (bundle9_coverage_test.go) and lifts - # the package-scoped run to ~86.7%; the global run averages a few - # points lower (per-function arithmetic), so the gate is set to 85 - # with the live `go test -cover` number being the source of truth. - # If this gate trips, the fix is to add tests, NOT to lower the - # floor — every percentage point under 85 is a regression on the - # H-010 closure invariant. - # Bundle R / Coverage Audit Closure — CI threshold raise checkpoint #3. - # Local-issuer floor lifted 85 → 86. Post-Bundle-Q package-scoped - # coverage at HEAD: 86.7%. The prescribed Bundle R target was - # 92, but reaching it requires interface seams for crypto/x509 - # signing-error branches — tracked as R-CI-extended. - if [ "$(echo "$LOCAL_ISSUER_COV < 86" | bc -l)" -eq 1 ]; then - echo "::error::Local-issuer coverage ${LOCAL_ISSUER_COV}% is below 86% (Bundle R closure floor — add tests, do not lower the gate)" - exit 1 - fi - # Bundle R-CI-extended threshold raise (post-Bundle-J-extended): - # ACME 50 -> 80. The Pebble-style mock + per-CA failure tests - # lift package-scoped ACME to 85.4%; gate at 80 with 5pp margin - # to absorb the global-run per-file-average dip. The prescribed - # Bundle R target was 85; held at 80 to avoid false-positives - # on single low-coverage files. - if [ "$(echo "$ACME_COV < 80" | bc -l)" -eq 1 ]; then - echo "::error::ACME issuer coverage ${ACME_COV}% is below 80% (Bundle R-CI-extended floor — add tests, do not lower the gate)" - exit 1 - fi - if [ "$(echo "$STEPCA_COV < 80" | bc -l)" -eq 1 ]; then - echo "::error::StepCA issuer coverage ${STEPCA_COV}% is below 80% (Bundle L.B closure floor — add tests, do not lower the gate)" - exit 1 - fi - if [ "$(echo "$MCP_COV < 85" | bc -l)" -eq 1 ]; then - echo "::error::MCP coverage ${MCP_COV}% is below 85% (Bundle K closure floor — add tests, do not lower the gate)" - exit 1 - fi - echo "Coverage thresholds passed!" + # ci-pipeline-cleanup Phase 2: per-package floors moved to + # .github/coverage-thresholds.yml. Each entry has `floor:` + + # `why:` (load-bearing context). Logic in + # scripts/check-coverage-thresholds.sh — operator runs the same + # script locally via `make verify`-equivalent loop. + run: bash scripts/check-coverage-thresholds.sh - name: Upload Coverage Report uses: actions/upload-artifact@v4 diff --git a/scripts/check-coverage-thresholds.sh b/scripts/check-coverage-thresholds.sh new file mode 100755 index 0000000..dcb6408 --- /dev/null +++ b/scripts/check-coverage-thresholds.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# scripts/check-coverage-thresholds.sh +# +# Enforces per-package coverage floors declared in +# .github/coverage-thresholds.yml against the live coverage.out. +# +# Per ci-pipeline-cleanup bundle Phase 2 / frozen decision 0.3. +# Adding a new gated package: one entry in the YAML — this script +# auto-picks it up. Lowering a floor REQUIRES corresponding code-side +# test work — never lower the gate to make CI green. + +set -e + +if [ ! -f coverage.out ]; then + echo "::error::coverage.out not found — run 'go test -cover -coverprofile=coverage.out' first" + exit 1 +fi + +if [ ! -f .github/coverage-thresholds.yml ]; then + echo "::error::.github/coverage-thresholds.yml not found" + exit 1 +fi + +echo "=== Coverage Report ===" +go tool cover -func=coverage.out | tail -1 +echo "" + +# Extract the pkg → floor table from the YAML. +python3 - <<'PY' > /tmp/cov-thresholds.tsv +import yaml +d = yaml.safe_load(open('.github/coverage-thresholds.yml')) +for pkg, entry in d.items(): + print(f"{pkg}\t{entry['floor']}") +PY + +fail=0 +while IFS=$'\t' read -r pkg floor; do + cov=$(go tool cover -func=coverage.out \ + | grep "$pkg" \ + | awk '{print $NF}' \ + | sed 's/%//' \ + | awk '{sum+=$1; n++} END {if(n>0) printf "%.1f", sum/n; else print "0"}') + printf "%-50s %5s%% (floor: %s%%)\n" "$pkg" "$cov" "$floor" + if [ "$(echo "$cov < $floor" | bc -l)" -eq 1 ]; then + # Pull the why: text out of the YAML for this package. + why=$(python3 -c " +import yaml, sys +d = yaml.safe_load(open('.github/coverage-thresholds.yml')) +print(d.get(sys.argv[1], {}).get('why', '').strip()) +" "$pkg") + echo "::error::$pkg coverage $cov% is below floor $floor%" + echo "Why this floor exists:" + echo "$why" | sed 's/^/ /' + echo "Add tests; do not lower the gate." + fail=1 + fi +done < /tmp/cov-thresholds.tsv + +[ $fail -eq 0 ] || exit 1 +echo "" +echo "All coverage thresholds passed."