Skip to content

Commit f23fa84

Browse files
committed
ci: inline coverage badge generation
1 parent c170d47 commit f23fa84

File tree

5 files changed

+164
-23
lines changed

5 files changed

+164
-23
lines changed

.github/badges/coverage.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"schemaVersion": 1, "label": "coverage", "message": "100%", "color": "#2A9D8F"}

.github/workflows/pipeline.yml

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,27 +27,46 @@ jobs:
2727
with:
2828
failure-threshold: error
2929

30-
submit-coverage:
30+
coverage:
3131
runs-on: ubuntu-latest
32-
container:
33-
image: python:3.12-slim
32+
permissions:
33+
contents: write
34+
pages: write
35+
id-token: write
3436
steps:
3537
- uses: actions/checkout@v3
3638
- run: |
3739
apt-get update -y
3840
apt-get install -y enchant-2 hunspell-ru hunspell-es hunspell-de-de hunspell-fr hunspell-pt-pt curl
3941
pip install uv
4042
uv sync --group dev
41-
uv run pytest -n3 --cov-report=xml
42-
- name: Upload coverage reports to Codecov
43-
uses: codecov/codecov-action@v3
44-
env:
45-
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
46-
files: coverage.xml
43+
- run: uv run pytest -n3 . --cov-report=xml --cov-report=html
44+
- run: uv run python -m scripts build-coverage-badge
45+
- uses: stefanzweifel/git-auto-commit-action@v5
46+
with:
47+
commit_message: "docs: update coverage badge"
48+
file_pattern: .github/badges/coverage.json
49+
- run: mv htmlcov coverage
50+
- uses: actions/upload-pages-artifact@v3
51+
with:
52+
path: coverage
53+
54+
deploy-coverage:
55+
needs: coverage
56+
runs-on: ubuntu-latest
57+
permissions:
58+
pages: write
59+
id-token: write
60+
environment:
61+
name: github-pages
62+
url: ${{ steps.deployment.outputs.page_url }}
63+
steps:
64+
- id: deployment
65+
uses: actions/deploy-pages@v4
4766

4867
# build stage with auto-versioning based on git tags like vX.Y.Z (example: v3.1.2)
4968
build-and-publish:
50-
needs: [py-lint-and-test, docker-lint, submit-coverage]
69+
needs: [py-lint-and-test, docker-lint, coverage]
5170
runs-on: ubuntu-latest
5271
if: startsWith(github.ref, 'refs/tags/v')
5372
steps:

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Spellcheck microservice
22
[![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/xfenix/spellcheck-microservice?label=version)](https://github.com/xfenix/spellcheck-microservice/releases)
33
[![Docker Pulls](https://img.shields.io/docker/pulls/xfenix/spellcheck-microservice)](https://hub.docker.com/r/xfenix/spellcheck-microservice)
4-
[![codecov](https://codecov.io/gh/xfenix/spellcheck-microservice/graph/badge.svg?token=IyBXLeKWae)](https://codecov.io/gh/xfenix/spellcheck-microservice)
4+
[![Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/xfenix/spellcheck-microservice/main/.github/badges/coverage.json)](https://xfenix.github.io/spellcheck-microservice/coverage/)
55
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
66
<a href="https://github.com/psf/black" target="_blank"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
77
[![Imports: isort](https://img.shields.io/badge/imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://timothycrosley.github.io/isort/)

scripts/__main__.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,28 @@
22
"""Simple dockerhub readme generator."""
33

44
import argparse
5+
import contextlib
6+
import json
57
import pathlib
8+
import random
69
import re
710
import sys
11+
import time
12+
import types
813
import typing
14+
import xml.etree.ElementTree as ET
915

1016
from ._helpers import parse_last_git_tag, replace_tag_in_readme
1117
from whole_app.settings import SETTINGS
1218

1319

1420
PARENT_DIR: typing.Final = pathlib.Path(__file__).parent.parent
1521
README_PATH: typing.Final = PARENT_DIR / "README.md"
22+
COVERAGE_XML_PATH: typing.Final = pathlib.Path("coverage.xml")
23+
BADGE_JSON_PATH: typing.Final = pathlib.Path(".github/badges/coverage.json")
24+
LOW_BOUNDARY: typing.Final[float] = 60
25+
HIGH_BOUNDARY: typing.Final[float] = 80
26+
RETRY_ATTEMPTS: typing.Final[int] = 3
1627

1728

1829
def _update_dockerhub_readme() -> None:
@@ -65,6 +76,55 @@ def _update_readme() -> None:
6576
README_PATH.write_text(new_content)
6677

6778

79+
def _fetch_xml_text() -> str:
80+
for _attempt_index in range(RETRY_ATTEMPTS):
81+
with contextlib.suppress(OSError):
82+
return COVERAGE_XML_PATH.read_text()
83+
time.sleep(random.uniform(0.1, 0.3)) # noqa: S311
84+
error_message: typing.Final = f"Failed to read {COVERAGE_XML_PATH} after {RETRY_ATTEMPTS} attempts"
85+
raise OSError(error_message)
86+
87+
88+
def _persist_badge_text(badge_text: str) -> None:
89+
BADGE_JSON_PATH.parent.mkdir(parents=True, exist_ok=True)
90+
for _attempt_index in range(RETRY_ATTEMPTS):
91+
with contextlib.suppress(OSError):
92+
BADGE_JSON_PATH.write_text(badge_text)
93+
return
94+
time.sleep(random.uniform(0.1, 0.3)) # noqa: S311
95+
error_message: typing.Final = f"Failed to write {BADGE_JSON_PATH} after {RETRY_ATTEMPTS} attempts"
96+
raise OSError(error_message)
97+
98+
99+
def _build_coverage_badge() -> None:
100+
xml_source_text: typing.Final[str] = _fetch_xml_text()
101+
root_element: typing.Final[ET.Element] = ET.fromstring(xml_source_text) # noqa: S314
102+
line_rate_text: typing.Final[str | None] = root_element.attrib.get("line-rate")
103+
if line_rate_text is None:
104+
missing_attr_message: typing.Final[str] = "Missing 'line-rate' attribute in coverage report"
105+
raise KeyError(missing_attr_message)
106+
coverage_percent: typing.Final[float] = float(line_rate_text) * 100.0
107+
108+
message_text: typing.Final[str] = f"{coverage_percent:.0f}%"
109+
color_text: str
110+
if coverage_percent < LOW_BOUNDARY:
111+
color_text = "#E63946"
112+
elif coverage_percent < HIGH_BOUNDARY:
113+
color_text = "#FFB347"
114+
else:
115+
color_text = "#2A9D8F"
116+
117+
badge_mapping: typing.Final[typing.Mapping[str, typing.Any]] = types.MappingProxyType(
118+
{
119+
"schemaVersion": 1,
120+
"label": "coverage",
121+
"message": message_text,
122+
"color": color_text,
123+
},
124+
)
125+
_persist_badge_text(json.dumps(dict(badge_mapping)))
126+
127+
68128
if __name__ == "__main__":
69129
sys.path.append(str(PARENT_DIR.resolve()))
70130

@@ -76,5 +136,7 @@ def _update_readme() -> None:
76136
_update_dockerhub_readme()
77137
case "update-readme":
78138
_update_readme()
139+
case "build-coverage-badge":
140+
_build_coverage_badge()
79141
case _:
80142
print("Unknown action") # noqa: T201

0 commit comments

Comments
 (0)