Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 42 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,30 +147,59 @@ 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
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@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1
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@983d7736d9b0ae728b81ab479565c72886d7745b # v5

- name: Deploy coverage Pages payload
id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
14 changes: 14 additions & 0 deletions .luacov
Original file line number Diff line number Diff line change
@@ -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",
}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<a href="https://github.com/fairvisor/edge/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MPL--2.0-blue" alt="License: MPL-2.0"></a>
<a href="https://github.com/fairvisor/edge/releases"><img src="https://img.shields.io/github/v/release/fairvisor/edge" alt="Latest release"></a>
<a href="https://github.com/fairvisor/-edge/actions"><img src="https://img.shields.io/github/actions/workflow/status/fairvisor/edge/ci.yml?label=CI" alt="CI"></a>
<a href="https://fairvisor.github.io/edge/"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Ffairvisor.github.io%2Fedge%2Fcoverage-badge.json" alt="Lua coverage"></a>
<a href="https://github.com/fairvisor/edge/pkgs/container/fairvisor-edge"><img src="https://img.shields.io/badge/ghcr.io-fairvisor--edge-blue?logo=docker" alt="GHCR image"></a>
<img src="https://img.shields.io/badge/platform-linux%2Famd64%20·%20linux%2Farm64-lightgrey" alt="Platforms: linux/amd64 · linux/arm64">
<a href="https://docs.fairvisor.com/docs/quickstart/"><img src="https://img.shields.io/badge/docs-quickstart-informational" alt="Docs"></a>
Expand Down
273 changes: 273 additions & 0 deletions bin/ci/build_lua_coverage_badge.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
#!/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}"
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
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
) || true

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}"
) || true

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"
}
}
'
}

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"
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 <<EOF
{
"schemaVersion": 1,
"label": "${label}",
"message": "${message}",
"color": "${color}"
}
EOF
}

updated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
commit_sha="${GITHUB_SHA:-local}"
repo_name="${GITHUB_REPOSITORY:-fairvisor/edge}"
run_url=""

if [[ -n "${GITHUB_SERVER_URL:-}" && -n "${GITHUB_REPOSITORY:-}" && -n "${GITHUB_RUN_ID:-}" ]]; then
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}" "${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}"

cat > "${SUMMARY_FILE}" <<EOF
{
"generated_at": "${updated_at}",
"repository": "${repo_name}",
"commit": "${commit_sha}",
"run_url": "${run_url}",
"total": {
"hits": ${total_hits},
"missed": ${total_missed},
"coverage": ${total_coverage}
},
"non_generated": {
"hits": ${non_generated_hits},
"missed": ${non_generated_missed},
"coverage": ${non_generated_coverage}
}
}
EOF

cp "${SUMMARY_FILE}" "${PAGES_SUMMARY_FILE}"

cat > "${INDEX_FILE}" <<EOF
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fairvisor Lua Coverage</title>
<style>
:root {
color-scheme: light;
--bg: #f6f7f3;
--panel: #ffffff;
--text: #182016;
--muted: #5b6657;
--line: #d8ddd2;
--accent: #1e7a4f;
}
body {
margin: 0;
background: linear-gradient(180deg, #eef4ea 0%, var(--bg) 100%);
color: var(--text);
font: 16px/1.5 "Georgia", "Times New Roman", serif;
}
main {
max-width: 820px;
margin: 0 auto;
padding: 48px 20px 64px;
}
h1 {
margin: 0 0 12px;
font-size: 2.4rem;
}
p {
margin: 0 0 18px;
color: var(--muted);
}
.grid {
display: grid;
gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
margin: 28px 0;
}
.card {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 18px;
padding: 20px;
box-shadow: 0 12px 30px rgba(24, 32, 22, 0.06);
}
.metric {
font-size: 2rem;
color: var(--accent);
}
code {
font-family: "SFMono-Regular", "Consolas", monospace;
font-size: 0.95em;
}
a {
color: var(--accent);
}
</style>
</head>
<body>
<main>
<h1>Fairvisor Lua Coverage</h1>
<p>Coverage is generated by <code>luacov</code> in CI. The badge in README uses the non-generated metric so autogenerated tables do not mask regressions in handwritten runtime code.</p>
<div class="grid">
<section class="card">
<div>Total coverage</div>
<div class="metric">${total_coverage}%</div>
<div>${total_hits} hits / ${total_missed} missed</div>
</section>
<section class="card">
<div>Non-generated coverage</div>
<div class="metric">${non_generated_coverage}%</div>
<div>${non_generated_hits} hits / ${non_generated_missed} missed</div>
</section>
</div>
<p>Generated at <code>${updated_at}</code> for commit <code>${commit_sha}</code>.</p>
<p><a href="coverage-badge.json">coverage-badge.json</a> | <a href="coverage-total-badge.json">coverage-total-badge.json</a> | <a href="coverage-summary.json">coverage-summary.json</a></p>
</main>
</body>
</html>
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
14 changes: 14 additions & 0 deletions bin/ci/run_lua_coverage.sh
Original file line number Diff line number Diff line change
@@ -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
Loading