From bd34fcfd2ca24403acf0e5aebad51a3e40831a84 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 14 Mar 2026 12:40:59 +0000 Subject: [PATCH 1/3] feat(ci): add Lua coverage reporting and badge pipeline --- .github/workflows/ci.yml | 57 +++++-- .luacov | 14 ++ README.md | 1 + bin/ci/build_lua_coverage_badge.sh | 229 +++++++++++++++++++++++++++++ bin/ci/run_lua_coverage.sh | 14 ++ 5 files changed, 302 insertions(+), 13 deletions(-) create mode 100644 .luacov create mode 100755 bin/ci/build_lua_coverage_badge.sh create mode 100755 bin/ci/run_lua_coverage.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29b2ea0..2376983 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -147,30 +147,61 @@ jobs: coverage-report: runs-on: ubuntu-latest - continue-on-error: true steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Setup Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + + - name: Build Lua test container + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: - python-version: "3.12" + context: . + file: docker/Dockerfile.test + tags: fairvisor-test-ci + load: true + cache-from: type=gha + cache-to: type=gha,mode=max - - name: Install E2E coverage deps - run: | - pip install -r tests/e2e/requirements.txt - pip install pytest-cov + - name: Generate Lua coverage report + run: docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" -w /work fairvisor-test-ci bin/ci/run_lua_coverage.sh - - name: Generate Python coverage report (non-blocking) - run: | - mkdir -p artifacts/coverage - pytest tests/e2e/test_health.py --cov=tests/e2e --cov-report=xml:artifacts/coverage/python-coverage.xml || true + - name: Build coverage summary, regression gate, and badge payload + env: + LUA_COVERAGE_MIN_NON_GENERATED: "82.28" + run: bin/ci/build_lua_coverage_badge.sh - name: Upload coverage artifacts if: always() uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: coverage-artifacts - path: artifacts/coverage + path: | + artifacts/coverage + artifacts/pages if-no-files-found: ignore + + - name: Upload coverage Pages artifact + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: actions/upload-pages-artifact@v3 + with: + path: artifacts/pages + + deploy-coverage-pages: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: coverage-report + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Configure Pages + uses: actions/configure-pages@v5 + + - name: Deploy coverage Pages payload + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.luacov b/.luacov new file mode 100644 index 0000000..2d98c22 --- /dev/null +++ b/.luacov @@ -0,0 +1,14 @@ +return { + include = { + "src/fairvisor/", + }, + exclude = { + "spec/", + "tests/", + "cli/", + "bin/", + "src/nginx/", + }, + statsfile = "artifacts/coverage/luacov.stats.out", + reportfile = "artifacts/coverage/luacov.report.out", +} diff --git a/README.md b/README.md index a83065e..07bc768 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ License: MPL-2.0 Latest release CI + Lua coverage GHCR image Platforms: linux/amd64 · linux/arm64 Docs diff --git a/bin/ci/build_lua_coverage_badge.sh b/bin/ci/build_lua_coverage_badge.sh new file mode 100755 index 0000000..70921f0 --- /dev/null +++ b/bin/ci/build_lua_coverage_badge.sh @@ -0,0 +1,229 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPORT_FILE="${1:-artifacts/coverage/luacov.report.out}" +COVERAGE_DIR="${COVERAGE_DIR:-artifacts/coverage}" +PAGES_DIR="${PAGES_DIR:-artifacts/pages}" +SUMMARY_FILE="${SUMMARY_FILE:-${COVERAGE_DIR}/coverage-summary.json}" +PAGES_SUMMARY_FILE="${PAGES_SUMMARY_FILE:-${PAGES_DIR}/coverage-summary.json}" +BADGE_FILE="${BADGE_FILE:-${PAGES_DIR}/coverage-badge.json}" +TOTAL_BADGE_FILE="${TOTAL_BADGE_FILE:-${PAGES_DIR}/coverage-total-badge.json}" +INDEX_FILE="${INDEX_FILE:-${PAGES_DIR}/index.html}" + +if [[ ! -f "${REPORT_FILE}" ]]; then + echo "coverage report not found: ${REPORT_FILE}" >&2 + exit 1 +fi + +mkdir -p "${COVERAGE_DIR}" "${PAGES_DIR}" + +read -r total_hits total_missed total_coverage < <( + awk ' + /^Total[[:space:]]+[0-9]+[[:space:]]+[0-9]+[[:space:]]+[0-9.]+%$/ { + gsub("%", "", $4) + print $2, $3, $4 + } + ' "${REPORT_FILE}" | tail -n 1 +) + +read -r non_generated_hits non_generated_missed non_generated_coverage < <( + awk ' + BEGIN { + hits = 0 + missed = 0 + } + /^src\/fairvisor\// && $1 !~ /^src\/fairvisor\/generated\// { + hits += $2 + missed += $3 + } + END { + total = hits + missed + if (total == 0) { + print "0 0 0.00" + } else { + printf "%d %d %.2f\n", hits, missed, (hits * 100) / total + } + } + ' "${REPORT_FILE}" +) + +if [[ -z "${total_coverage:-}" ]]; then + echo "failed to parse total coverage from ${REPORT_FILE}" >&2 + exit 1 +fi + +coverage_color() { + local coverage="$1" + + awk -v coverage="${coverage}" ' + BEGIN { + if (coverage >= 95) { + print "brightgreen" + } else if (coverage >= 90) { + print "green" + } else if (coverage >= 80) { + print "yellowgreen" + } else if (coverage >= 70) { + print "yellow" + } else if (coverage >= 60) { + print "orange" + } else { + print "red" + } + } + ' +} + +assert_minimum_coverage() { + local actual="$1" + local minimum="$2" + local label="$3" + + if [[ -z "${minimum}" ]]; then + return 0 + fi + + if awk -v actual="${actual}" -v minimum="${minimum}" 'BEGIN { exit !(actual + 1e-9 < minimum) }'; then + echo "${label} coverage regression: ${actual}% < ${minimum}%" >&2 + exit 1 + fi +} + +badge_json() { + local label="$1" + local message="$2" + local color="$3" + + cat < "${BADGE_FILE}" +badge_json "lua coverage total" "${total_coverage}%" "$(coverage_color "${total_coverage}")" > "${TOTAL_BADGE_FILE}" + +cat > "${SUMMARY_FILE}" < "${INDEX_FILE}" < + + + + + Fairvisor Lua Coverage + + + +
+

Fairvisor Lua Coverage

+

Coverage is generated by luacov in CI. The badge in README uses the non-generated metric so autogenerated tables do not mask regressions in handwritten runtime code.

+
+
+
Total coverage
+
${total_coverage}%
+
${total_hits} hits / ${total_missed} missed
+
+
+
Non-generated coverage
+
${non_generated_coverage}%
+
${non_generated_hits} hits / ${non_generated_missed} missed
+
+
+

Generated at ${updated_at} for commit ${commit_sha}.

+

coverage-badge.json | coverage-total-badge.json | coverage-summary.json

+
+ + +EOF + +echo "total coverage: ${total_coverage}%" +echo "non-generated coverage: ${non_generated_coverage}%" diff --git a/bin/ci/run_lua_coverage.sh b/bin/ci/run_lua_coverage.sh new file mode 100755 index 0000000..7dcfbb5 --- /dev/null +++ b/bin/ci/run_lua_coverage.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +ARTIFACTS_DIR="${ARTIFACTS_DIR:-artifacts/coverage}" + +mkdir -p "${ARTIFACTS_DIR}" +rm -f "${ARTIFACTS_DIR}/luacov.stats.out" \ + "${ARTIFACTS_DIR}/luacov.report.out" + +echo "[coverage] busted unit + integration with luacov" +busted --coverage spec/unit/ spec/integration/ + +echo "[coverage] luacov text report" +luacov From 815c978b89688c1e047a43f07053ebb281cf8277 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 14 Mar 2026 12:52:59 +0000 Subject: [PATCH 2/3] fix(ci): address coverage review feedback --- .github/workflows/ci.yml | 6 +++--- bin/ci/build_lua_coverage_badge.sh | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2376983..9a67af4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -184,7 +184,7 @@ jobs: - name: Upload coverage Pages artifact if: github.event_name == 'push' && github.ref == 'refs/heads/main' - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 with: path: artifacts/pages @@ -200,8 +200,8 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} steps: - name: Configure Pages - uses: actions/configure-pages@v5 + uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5 - name: Deploy coverage Pages payload id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 diff --git a/bin/ci/build_lua_coverage_badge.sh b/bin/ci/build_lua_coverage_badge.sh index 70921f0..3ec8d1c 100755 --- a/bin/ci/build_lua_coverage_badge.sh +++ b/bin/ci/build_lua_coverage_badge.sh @@ -24,7 +24,7 @@ read -r total_hits total_missed total_coverage < <( print $2, $3, $4 } ' "${REPORT_FILE}" | tail -n 1 -) +) || true read -r non_generated_hits non_generated_missed non_generated_coverage < <( awk ' @@ -45,7 +45,7 @@ read -r non_generated_hits non_generated_missed non_generated_coverage < <( } } ' "${REPORT_FILE}" -) +) || true if [[ -z "${total_coverage:-}" ]]; then echo "failed to parse total coverage from ${REPORT_FILE}" >&2 From 73fcfd3afd038c3cf69064d91404d585e76418ba Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 14 Mar 2026 14:32:40 +0000 Subject: [PATCH 3/3] feat(ci): gate coverage against published main baseline --- .github/workflows/ci.yml | 2 -- bin/ci/build_lua_coverage_badge.sh | 46 +++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a67af4..a7249c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -168,8 +168,6 @@ jobs: run: docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" -w /work fairvisor-test-ci bin/ci/run_lua_coverage.sh - name: Build coverage summary, regression gate, and badge payload - env: - LUA_COVERAGE_MIN_NON_GENERATED: "82.28" run: bin/ci/build_lua_coverage_badge.sh - name: Upload coverage artifacts diff --git a/bin/ci/build_lua_coverage_badge.sh b/bin/ci/build_lua_coverage_badge.sh index 3ec8d1c..ebab38f 100755 --- a/bin/ci/build_lua_coverage_badge.sh +++ b/bin/ci/build_lua_coverage_badge.sh @@ -9,6 +9,7 @@ PAGES_SUMMARY_FILE="${PAGES_SUMMARY_FILE:-${PAGES_DIR}/coverage-summary.json}" BADGE_FILE="${BADGE_FILE:-${PAGES_DIR}/coverage-badge.json}" TOTAL_BADGE_FILE="${TOTAL_BADGE_FILE:-${PAGES_DIR}/coverage-total-badge.json}" INDEX_FILE="${INDEX_FILE:-${PAGES_DIR}/index.html}" +BASELINE_URL="${BASELINE_URL:-https://fairvisor.github.io/edge/coverage-summary.json}" if [[ ! -f "${REPORT_FILE}" ]]; then echo "coverage report not found: ${REPORT_FILE}" >&2 @@ -74,6 +75,44 @@ coverage_color() { ' } +resolve_baseline_coverage() { + if [[ -n "${LUA_COVERAGE_MIN_NON_GENERATED:-}" ]]; then + printf '%s\n' "${LUA_COVERAGE_MIN_NON_GENERATED}" + return 0 + fi + + if [[ "${GITHUB_EVENT_NAME:-}" != "pull_request" ]]; then + return 0 + fi + + local payload + if ! payload="$(curl --fail --silent --show-error --location "${BASELINE_URL}" 2>/dev/null)"; then + echo "baseline not found, skipping regression gate for bootstrap run: ${BASELINE_URL}" >&2 + return 0 + fi + + local baseline + if ! baseline="$( + printf '%s' "${payload}" | python3 -c ' +import json +import sys + +try: + data = json.load(sys.stdin) + value = data["non_generated"]["coverage"] +except Exception: + raise SystemExit(1) + +print(value) +' + )"; then + echo "baseline could not be parsed, skipping regression gate: ${BASELINE_URL}" >&2 + return 0 + fi + + printf '%s\n' "${baseline}" +} + assert_minimum_coverage() { local actual="$1" local minimum="$2" @@ -113,8 +152,10 @@ if [[ -n "${GITHUB_SERVER_URL:-}" && -n "${GITHUB_REPOSITORY:-}" && -n "${GITHUB run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" fi +baseline_non_generated_coverage="$(resolve_baseline_coverage)" + assert_minimum_coverage "${total_coverage}" "${LUA_COVERAGE_MIN_TOTAL:-}" "total" -assert_minimum_coverage "${non_generated_coverage}" "${LUA_COVERAGE_MIN_NON_GENERATED:-}" "non-generated" +assert_minimum_coverage "${non_generated_coverage}" "${baseline_non_generated_coverage:-}" "non-generated" badge_json "lua coverage" "${non_generated_coverage}%" "$(coverage_color "${non_generated_coverage}")" > "${BADGE_FILE}" badge_json "lua coverage total" "${total_coverage}%" "$(coverage_color "${total_coverage}")" > "${TOTAL_BADGE_FILE}" @@ -227,3 +268,6 @@ EOF echo "total coverage: ${total_coverage}%" echo "non-generated coverage: ${non_generated_coverage}%" +if [[ -n "${baseline_non_generated_coverage:-}" ]]; then + echo "baseline non-generated coverage: ${baseline_non_generated_coverage}%" +fi