version: '3' tasks: security-scan: desc: Run full CI quality and security checks cmds: - task: prepare-reports - task: backend-lint - task: frontend-lint - task: unit-tests - task: frontend-e2e-tests - task: integration-tests - task: enforce-backend-coverage - task: docker-build-validate - task: k6-smoke - task: gosec-scan - task: trivy-fs-scan - task: trivy-image-scan prepare-reports: internal: true cmds: - mkdir -p reports/security reports/tests reports/tests/coverage reports/docker reports/perf backend-lint: cmds: - | set -eu for module in \ services/admin-service \ services/game-session-service \ services/gateway-service \ services/leaderboard-service \ services/question-bank-service \ services/user-service \ shared do (cd "backend/${module}" && golangci-lint run ./...) done frontend-lint: cmds: - cd frontend && yarn lint - cd frontend && yarn format:check unit-tests: cmds: - | bash -o pipefail -c ' set -eu mkdir -p reports/tests reports/tests/coverage : > reports/tests/backend-unit.log ROOT="$(pwd)" mkdir -p "${ROOT}/.cache/go-build" for module in \ services/admin-service \ services/game-session-service \ services/gateway-service \ services/leaderboard-service \ services/question-bank-service \ services/user-service \ shared do profile="${ROOT}/reports/tests/coverage/${module//\//-}-unit.out" ( cd "backend/${module}" pkgs="$(GOCACHE="${ROOT}/.cache/go-build" go list ./...)" pkgs="$(echo "${pkgs}" | grep -v '/tests$' || true)" if [ -z "${pkgs}" ]; then echo "No unit-test packages for backend/${module}" exit 0 fi GOCACHE="${ROOT}/.cache/go-build" go test -v -race -covermode=atomic -coverpkg=./... -coverprofile="${profile}" ${pkgs} ) | tee -a "${ROOT}/reports/tests/backend-unit.log" done ' - bash -o pipefail -c 'cd frontend && CI=1 yarn test | tee ../reports/tests/frontend-unit.log' frontend-e2e-tests: desc: Run Playwright critical frontend flows cmds: - | bash -o pipefail -c ' set -eu mkdir -p reports/tests cd frontend CI=1 yarn test:e2e --reporter=line | tee ../reports/tests/frontend-e2e.log ' integration-tests: cmds: - | bash -o pipefail -c ' set -eu mkdir -p reports/tests reports/tests/coverage : > reports/tests/backend-integration.log ROOT="$(pwd)" mkdir -p "${ROOT}/.cache/go-build" for module in \ services/admin-service \ services/game-session-service \ services/gateway-service \ services/leaderboard-service \ services/question-bank-service \ services/user-service do if [ ! -d "backend/${module}/tests" ]; then continue fi profile="${ROOT}/reports/tests/coverage/${module//\//-}-integration.out" ( cd "backend/${module}" GOCACHE="${ROOT}/.cache/go-build" go test -v -covermode=atomic -coverpkg=./... -coverprofile="${profile}" ./tests/... ) | tee -a "${ROOT}/reports/tests/backend-integration.log" done ' enforce-backend-coverage: desc: Aggregate backend service coverage and enforce stepwise thresholds up to 80% cmds: - | bash -o pipefail -c ' set -eu mkdir -p reports/tests reports/tests/coverage COVERAGE_DIR="reports/tests/coverage" COMBINED="${COVERAGE_DIR}/backend-combined.out" SUMMARY="reports/tests/backend-coverage-summary.txt" THRESHOLDS="${BACKEND_COVERAGE_THRESHOLDS:-60 70 80}" rm -f "${COMBINED}" echo "mode: atomic" > "${COMBINED}" merged_input="" for profile in "${COVERAGE_DIR}"/*.out; do [ -f "${profile}" ] || continue [ "${profile}" = "${COMBINED}" ] && continue case "${profile}" in *shared-unit.out) continue ;; esac merged_input="${merged_input} ${profile}" done if [ -z "${merged_input}" ]; then echo "No backend coverage profiles were generated." | tee "${SUMMARY}" exit 1 fi # Deduplicate statement blocks across unit/integration profiles by keeping max count per block. # shellcheck disable=SC2086 tail -n +2 ${merged_input} | sort -k1,1 | awk " { if (NR == 1) { prev = \$1 stmt = \$2 max = \$3 next } if (\$1 != prev) { print prev, stmt, max prev = \$1 stmt = \$2 max = \$3 next } if ((\$3 + 0) > (max + 0)) { max = \$3 } } END { if (NR > 0) { print prev, stmt, max } } " >> "${COMBINED}" total_stats="$(awk " NR > 1 { split(\$1, loc, \":\") path = loc[1] pkg = path sub(/\\/[^\\/]*$/, \"\", pkg) # Focus on service application/domain and HTTP interface logic. if (pkg !~ /^knowfoolery\\/backend\\/services\\//) { next } if (pkg !~ /\\/internal\\/(application|domain)\\// && pkg !~ /\\/internal\\/interfaces\\/http($|\\/)/) { next } total += \$2 if ((\$3 + 0) > 0) { covered += \$2 } } END { if (total == 0) { printf \"0.00 0 0\" } else { printf \"%.2f %d %d\", (covered/total)*100, covered, total } } " "${COMBINED}")" total_pct="$(echo "${total_stats}" | awk "{print \$1}")" covered_stmt="$(echo "${total_stats}" | awk "{print \$2}")" total_stmt="$(echo "${total_stats}" | awk "{print \$3}")" { echo "Backend service (application/domain/interfaces-http) coverage: ${total_pct}%" echo "Covered statements: ${covered_stmt}/${total_stmt}" echo "Threshold progression: ${THRESHOLDS}%" } | tee "${SUMMARY}" for threshold in ${THRESHOLDS}; do awk -v coverage="${total_pct}" -v threshold="${threshold}" "BEGIN { exit((coverage+0) < (threshold+0)) }" done ' docker-build-validate: cmds: - bash -o pipefail -c 'set -eu; for service in gateway game-session question-bank user leaderboard admin; do docker build -f "infrastructure/services/${service}.Dockerfile" -t "knowfoolery/${service}:ci" . | tee "reports/docker/${service}-build.log"; done' k6-smoke: desc: Run k6 smoke profile for critical gateway paths cmds: - | bash -o pipefail -c ' set -eu mkdir -p reports/perf if ! command -v k6 >/dev/null 2>&1; then echo "k6 is not installed; skipping smoke load tests." exit 0 fi if [ -z "${K6_BASE_URL:-}" ]; then echo "K6_BASE_URL is not set; skipping k6 smoke load tests." exit 0 fi k6 run \ --quiet \ --env K6_PROFILE=smoke \ --env K6_BASE_URL="${K6_BASE_URL}" \ --env K6_AUTH_TOKEN="${K6_AUTH_TOKEN:-}" \ --out json=reports/perf/k6-smoke.json \ tests/load/k6/gateway-critical.js | tee reports/perf/k6-smoke.log ' k6-load: desc: Run k6 local load profile for critical gateway paths cmds: - | bash -o pipefail -c ' set -eu mkdir -p reports/perf : "${K6_BASE_URL:?K6_BASE_URL is required}" if ! command -v k6 >/dev/null 2>&1; then echo "k6 is not installed." exit 1 fi k6 run \ --env K6_PROFILE=load \ --env K6_BASE_URL="${K6_BASE_URL}" \ --env K6_AUTH_TOKEN="${K6_AUTH_TOKEN:-}" \ --out json=reports/perf/k6-load.json \ tests/load/k6/gateway-critical.js | tee reports/perf/k6-load.log ' gosec-scan: cmds: - bash -o pipefail -c 'set -eu; mkdir -p reports/security; set +e; gosec -fmt sarif -out reports/security/gosec.sarif ./backend/services/admin-service/... ./backend/services/game-session-service/... ./backend/services/gateway-service/... ./backend/services/leaderboard-service/... ./backend/services/question-bank-service/... ./backend/services/user-service/... ./backend/shared/... 2>&1 | tee reports/security/gosec.log; status=${PIPESTATUS[0]}; set -e; if grep -q "Panic when running SSA analyzer" reports/security/gosec.log || grep -q "file requires newer Go version" reports/security/gosec.log; then echo "gosec runtime/toolchain panic detected; treating as non-blocking tool failure."; exit 0; fi; exit "${status}"' trivy-fs-scan: cmds: - trivy fs --format json --output reports/security/trivy-fs.json --severity HIGH,CRITICAL --exit-code 1 . trivy-image-scan: cmds: - | set -eu for service in gateway game-session question-bank user leaderboard admin; do trivy image \ --format json \ --output "reports/security/trivy-image-${service}.json" \ --severity HIGH,CRITICAL \ --exit-code 1 \ "knowfoolery/${service}:ci" done