Some checks failed
CI / skinny-install (aco) (push) Successful in 45s
CI / skinny-install (api) (push) Successful in 29s
CI / skinny-install (bcda) (push) Successful in 25s
CI / skinny-install (bib) (push) Successful in 23s
CI / skinny-install (bls) (push) Successful in 20s
CI / skinny-install (ccw) (push) Successful in 36s
CI / skinny-install (cli) (push) Successful in 27s
CI / skinny-install (cms) (push) Successful in 24s
CI / skinny-install (conf) (push) Successful in 27s
CI / skinny-install (pfs) (push) Successful in 25s
CI / skinny-install (rex) (push) Successful in 25s
CI / lint-test (push) Successful in 6m2s
Infra CI / notebooks (push) Successful in 7s
Infra CI / zotero (push) Failing after 6s
Infra CI / docs (push) Successful in 33s
Infra CI / api (push) Successful in 6s
Infra CI / mc (push) Successful in 7s
Deploy / build-scan-report (push) Has been cancelled
- Fix all 72 ruff lint errors (unused imports, unused variables, E402) - Format all 14 unformatted dev/scripts files - Move generated artifacts to assets/ (dag.html, pfs.html) - Remove duplicate root coverage.svg (already in assets/icons/) - Update .dockerignore for infra/ tree layout - Update .gitignore: add .env.bak, mirrors/, htmlcov/ - Fix stale path refs in coverage_badge.py, woodpecker backend, test_network_isolation.sh, docs custom.css - Add .gitkeep to empty dirs (infra/polaris, cloud/*/terraform) - Delete 12 stale local branches, 10 stale remote branches
150 lines
4.4 KiB
Python
150 lines
4.4 KiB
Python
"""Generate a coverage badge SVG and optionally post status to Gitea.
|
|
|
|
Usage:
|
|
# Generate badge SVG to stdout from piped pytest-cov output:
|
|
uv run pytest --cov=src ... | uv run python dev/scripts/coverage_badge.py
|
|
|
|
# Post coverage as a Gitea commit status (reads GITEA_TOKEN, GITEA_URL,
|
|
# CI_COMMIT_SHA from environment):
|
|
uv run python dev/scripts/coverage_badge.py --post pytest.out
|
|
|
|
# Update assets/icons/coverage.svg in the repo via Gitea API:
|
|
uv run python dev/scripts/coverage_badge.py --upload coverage.svg
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
TEMPLATE = """\
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="116" height="20">
|
|
<linearGradient id="b" x2="0" y2="100%">
|
|
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
|
<stop offset="1" stop-opacity=".1"/>
|
|
</linearGradient>
|
|
<clipPath id="a">
|
|
<rect width="116" height="20" rx="3" fill="#fff"/>
|
|
</clipPath>
|
|
<g clip-path="url(#a)">
|
|
<path fill="#555" d="M0 0h65v20H0z"/>
|
|
<path fill="{color}" d="M65 0h51v20H65z"/>
|
|
<path fill="url(#b)" d="M0 0h116v20H0z"/>
|
|
</g>
|
|
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
|
|
<text x="32.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
|
|
<text x="32.5" y="14">coverage</text>
|
|
<text x="90" y="15" fill="#010101" fill-opacity=".3">{pct}%</text>
|
|
<text x="90" y="14">{pct}%</text>
|
|
</g>
|
|
</svg>"""
|
|
|
|
FILE_PATH = "assets/icons/coverage.svg"
|
|
|
|
|
|
def color_for(pct: int) -> str:
|
|
if pct >= 95:
|
|
return "#4c1"
|
|
if pct >= 80:
|
|
return "#a3c51c"
|
|
if pct >= 60:
|
|
return "#dfb317"
|
|
return "#e05d44"
|
|
|
|
|
|
def extract_pct(text: str) -> int:
|
|
m = re.search(r"^TOTAL\s+\d+\s+\d+\s+(\d+)%", text, re.MULTILINE)
|
|
if not m:
|
|
print("ERROR: could not find coverage percentage in input", file=sys.stderr)
|
|
sys.exit(1)
|
|
return int(m.group(1))
|
|
|
|
|
|
def _gitea_env() -> tuple[str, str]:
|
|
return os.environ["GITEA_TOKEN"], os.environ["GITEA_URL"]
|
|
|
|
|
|
def post_status(pct: int) -> None:
|
|
token, url = _gitea_env()
|
|
sha = os.environ["CI_COMMIT_SHA"]
|
|
endpoint = f"{url}/api/v1/repos/homelab/stack/statuses/{sha}"
|
|
body = json.dumps(
|
|
{
|
|
"state": "success",
|
|
"description": f"{pct}% coverage",
|
|
"context": "coverage",
|
|
}
|
|
).encode()
|
|
req = urllib.request.Request(
|
|
endpoint,
|
|
data=body,
|
|
headers={
|
|
"Authorization": f"token {token}",
|
|
"Content-Type": "application/json",
|
|
},
|
|
method="POST",
|
|
)
|
|
with urllib.request.urlopen(req) as resp:
|
|
print(f"Posted coverage status: {pct}% (HTTP {resp.status})")
|
|
|
|
|
|
def upload_badge(svg_path: str) -> None:
|
|
"""Update assets/icons/coverage.svg in the repo via Gitea contents API."""
|
|
token, url = _gitea_env()
|
|
endpoint = f"{url}/api/v1/repos/homelab/stack/contents/{FILE_PATH}"
|
|
svg_data = open(svg_path, "rb").read()
|
|
content_b64 = base64.b64encode(svg_data).decode()
|
|
|
|
# Get current file SHA (needed for update)
|
|
sha = None
|
|
try:
|
|
req = urllib.request.Request(
|
|
endpoint,
|
|
headers={"Authorization": f"token {token}"},
|
|
)
|
|
with urllib.request.urlopen(req) as resp:
|
|
sha = json.loads(resp.read())["sha"]
|
|
except urllib.error.HTTPError:
|
|
pass
|
|
|
|
body = {"content": content_b64, "message": "update coverage badge [skip ci]"}
|
|
if sha:
|
|
body["sha"] = sha
|
|
|
|
method = "PUT" if sha else "POST"
|
|
req = urllib.request.Request(
|
|
endpoint,
|
|
data=json.dumps(body).encode(),
|
|
headers={
|
|
"Authorization": f"token {token}",
|
|
"Content-Type": "application/json",
|
|
},
|
|
method=method,
|
|
)
|
|
with urllib.request.urlopen(req) as resp:
|
|
print(f"Updated {FILE_PATH} in repo (HTTP {resp.status})")
|
|
|
|
|
|
def main() -> None:
|
|
if "--post" in sys.argv:
|
|
path = sys.argv[sys.argv.index("--post") + 1]
|
|
text = open(path).read()
|
|
pct = extract_pct(text)
|
|
post_status(pct)
|
|
elif "--upload" in sys.argv:
|
|
svg_path = sys.argv[sys.argv.index("--upload") + 1]
|
|
upload_badge(svg_path)
|
|
else:
|
|
text = sys.stdin.read()
|
|
pct = extract_pct(text)
|
|
print(TEMPLATE.format(pct=pct, color=color_for(pct)))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|