diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml
index be1c23a..2ddff43 100644
--- a/.github/workflows/integration-test.yml
+++ b/.github/workflows/integration-test.yml
@@ -8,7 +8,8 @@ on:
permissions:
contents: read
- pull-requests: read
+ pull-requests: write
+ checks: write
jobs:
test-coverage-mode:
@@ -35,8 +36,8 @@ jobs:
coverage-files: coverage.xml
compare-branch: origin/main
fail-under: '0'
- post-comment: 'false'
- create-annotations: 'false'
+ post-comment: 'true'
+ create-annotations: 'true'
create-badge: 'true'
test-quality-mode:
diff --git a/README.md b/README.md
index fb013d5..245ffeb 100644
--- a/README.md
+++ b/README.md
@@ -60,89 +60,87 @@ Works with **any language** that can produce Cobertura XML, lcov, or JaCoCo cove
## What You Get
-### PR Comment (auto-posted, updates on re-run)
+### PR Comment — Passing
-> ## :white_check_mark: Diff Coverage: 82.0%
+>
+>
+>
>
-> > :heavy_check_mark: Meets threshold of 80%
+> `████████████████░░░░` **82.0%** on changed lines
+> |
>
-> | Metric | Value |
-> |--------|------:|
-> | **Coverage on diff lines** | **82.0%** |
-> | Lines changed | 50 |
-> | Lines uncovered | 9 |
-> | Files changed | 3 |
+> **50** lines changed **9** uncovered **3** files
>
>
-> File breakdown (3 files)
+> 📂 3 files changed
+>
>
-> | File | Coverage | Uncovered Lines |
-> |------|:--------:|:---------------:|
-> | `src/bar.py` | 60.0% | 5, 6, 7, 8, 15, 22 |
-> | `src/foo.py` | 85.0% | 13, 27, 42 |
-> | `src/baz.py` | 100.0% | |
+> | | File | Coverage | Uncovered Lines |
+> |:---:|:---|---:|:---|
+> | 🔴 | `src/bar.py` | 60.0% | 5, 6, 7, 8, 15, 22 |
+> | 🟡 | `src/foo.py` | 85.0% | 13, 27, 42 |
+> | 🟢 | `src/baz.py` | 100.0% | — |
>
>
>
-> ---
-> Posted by diff-cover-action
+> 🛡️ diff-cover-action
-### PR Comment -- Below Threshold
+### PR Comment — Failing
-> ## :red_circle: Diff Coverage: 45.0%
+>
+>
+>
>
-> > :x: Below threshold of 80% -- needs 35.0% more coverage
+> `█████████░░░░░░░░░░░` **45.0%** on changed lines
+> |
>
-> | Metric | Value |
-> |--------|------:|
-> | **Coverage on diff lines** | **45.0%** |
-> | Lines changed | 120 |
-> | Lines uncovered | 66 |
-> | Files changed | 8 |
+> > **80%** required — missing **35.0%** more coverage
+>
+> **120** lines changed **66** uncovered **8** files
>
>
-> File breakdown (8 files)
+> 📂 8 files changed
+>
>
-> | File | Coverage | Uncovered Lines |
-> |------|:--------:|:---------------:|
-> | `src/payments/stripe.py` | 0.0% | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 (+12 more) |
-> | `src/auth/login.py` | 25.0% | 15, 16, 17, 30, 31, 32 |
-> | `src/api/routes.py` | 40.0% | 22, 23, 55, 56, 57 |
-> | `src/models/user.py` | 50.0% | 8, 9, 44 |
-> | `src/utils/cache.py` | 60.0% | 18, 19 |
-> | `src/services/email.py` | 70.0% | 33 |
-> | `src/config.py` | 80.0% | 5 |
-> | `src/middleware.py` | 100.0% | |
+> | | File | Coverage | Uncovered Lines |
+> |:---:|:---|---:|:---|
+> | 🔴 | `src/payments/stripe.py` | 0.0% | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 (+12 more) |
+> | 🔴 | `src/auth/login.py` | 25.0% | 15, 16, 17, 30, 31, 32 |
+> | 🔴 | `src/api/routes.py` | 40.0% | 22, 23, 55, 56, 57 |
+> | 🔴 | `src/models/user.py` | 50.0% | 8, 9, 44 |
+> | 🔴 | `src/utils/cache.py` | 60.0% | 18, 19 |
+> | 🟡 | `src/services/email.py` | 70.0% | 33 |
+> | 🟡 | `src/config.py` | 80.0% | 5 |
+> | 🟢 | `src/middleware.py` | 100.0% | — |
>
>
>
-> ---
-> Posted by diff-cover-action
+> 🛡️ diff-cover-action
### Diff Quality PR Comment
-> ## :large_orange_diamond: Diff Quality: 75.0%
+>
+>
+>
+>
+> `███████████████░░░░░` **75.0%** on changed lines
+> |
>
-> > :x: Below threshold of 90%
+> > **90%** required — missing **15.0%** more quality coverage
>
-> | Metric | Value |
-> |--------|------:|
-> | **Quality on diff lines** | **75.0%** |
-> | Lines changed | 20 |
-> | Lines with violations | 4 |
-> | Files changed | 1 |
+> **20** lines changed **4** violations **1** file
>
>
-> File breakdown (1 file)
+> 📂 1 file changed
+>
>
-> | File | Quality | Violation Lines |
-> |------|:-------:|:---------------:|
-> | `src/module.py` | 75.0% | 10, 11, 12, 30 |
+> | | File | Quality | Violation Lines |
+> |:---:|:---|---:|:---|
+> | 🟡 | `src/module.py` | 75.0% | 10, 11, 12, 30 |
>
>
>
-> ---
-> Posted by diff-cover-action
+> 🛡️ diff-cover-action
### Inline Annotations (appear directly on the PR diff)
diff --git a/src/comment.py b/src/comment.py
index 6c366fc..642203a 100644
--- a/src/comment.py
+++ b/src/comment.py
@@ -100,6 +100,37 @@ def _find_existing_comment(
return None
+def _progress_bar(percent: float, width: int = 20) -> str:
+ """Generate a Unicode progress bar. Example: ████████████████░░░░ for 80%."""
+ filled = round(percent / 100 * width)
+ filled = max(0, min(width, filled))
+ return "\u2588" * filled + "\u2591" * (width - filled)
+
+
+def _status_icon(percent: float) -> str:
+ """Return a colored circle emoji based on coverage percentage."""
+ if percent >= 90:
+ return "\U0001f7e2" # 🟢
+ if percent >= 70:
+ return "\U0001f7e1" # 🟡
+ return "\U0001f534" # 🔴
+
+
+def _badge_color(percent: float) -> str:
+ """Map a coverage percentage to a shields.io color name."""
+ if percent >= 90:
+ return "brightgreen"
+ if percent >= 80:
+ return "green"
+ if percent >= 70:
+ return "yellowgreen"
+ if percent >= 60:
+ return "yellow"
+ if percent >= 40:
+ return "orange"
+ return "red"
+
+
def _render_comment_body(
*,
report: Report,
@@ -115,24 +146,19 @@ def _render_comment_body(
autoescape=False,
keep_trailing_newline=True,
)
+ env.filters["progress_bar"] = _progress_bar
+ env.filters["status_icon"] = _status_icon
+ env.filters["badge_color"] = _badge_color
template_name = f"comment_{mode}.md.j2"
template = env.get_template(template_name)
- if report.total_percent_covered >= 90:
- icon = ":white_check_mark:"
- elif report.total_percent_covered >= 70:
- icon = ":large_orange_diamond:"
- else:
- icon = ":red_circle:"
-
return template.render(
report=report,
mode=mode,
fail_under=fail_under,
threshold_met=threshold_met,
identifier=identifier,
- icon=icon,
md_report_content=md_report_content,
)
diff --git a/src/outputs.py b/src/outputs.py
index fb33bb5..2054234 100644
--- a/src/outputs.py
+++ b/src/outputs.py
@@ -48,6 +48,37 @@ def write_outputs(
_set_output("exit-code", str(exit_code))
+def _progress_bar(percent: float, width: int = 20) -> str:
+ """Generate a Unicode progress bar."""
+ filled = round(percent / 100 * width)
+ filled = max(0, min(width, filled))
+ return "\u2588" * filled + "\u2591" * (width - filled)
+
+
+def _status_icon(percent: float) -> str:
+ """Return a colored circle emoji based on coverage percentage."""
+ if percent >= 90:
+ return "\U0001f7e2" # 🟢
+ if percent >= 70:
+ return "\U0001f7e1" # 🟡
+ return "\U0001f534" # 🔴
+
+
+def _badge_color(percent: float) -> str:
+ """Map a coverage percentage to a shields.io color name."""
+ if percent >= 90:
+ return "brightgreen"
+ if percent >= 80:
+ return "green"
+ if percent >= 70:
+ return "yellowgreen"
+ if percent >= 60:
+ return "yellow"
+ if percent >= 40:
+ return "orange"
+ return "red"
+
+
def write_step_summary(
*,
report: Report,
@@ -61,20 +92,15 @@ def write_step_summary(
autoescape=False,
keep_trailing_newline=True,
)
+ env.filters["progress_bar"] = _progress_bar
+ env.filters["status_icon"] = _status_icon
+ env.filters["badge_color"] = _badge_color
template = env.get_template("step_summary.md.j2")
- if report.total_percent_covered >= 90:
- icon = "white_check_mark"
- elif report.total_percent_covered >= 70:
- icon = "large_orange_diamond"
- else:
- icon = "red_circle"
-
content = template.render(
report=report,
mode=mode,
fail_under=fail_under,
threshold_met=threshold_met,
- icon=icon,
)
_append_to_github_file("GITHUB_STEP_SUMMARY", content)
diff --git a/templates/comment_coverage.md.j2 b/templates/comment_coverage.md.j2
index c72f554..98d618d 100644
--- a/templates/comment_coverage.md.j2
+++ b/templates/comment_coverage.md.j2
@@ -1,28 +1,34 @@
-## {{ icon }} Diff Coverage: {{ "%.1f" | format(report.total_percent_covered) }}%
+{% set pct = "%.1f" | format(report.total_percent_covered) %}
+{% set color = report.total_percent_covered | badge_color %}
+
+
{% if fail_under > 0 %}
{% if threshold_met %}
-> :heavy_check_mark: Meets threshold of {{ "%.0f" | format(fail_under) }}%
+
{% else %}
-> :x: Below threshold of {{ "%.0f" | format(fail_under) }}% — needs {{ "%.1f" | format(fail_under - report.total_percent_covered) }}% more coverage
+
{% endif %}
{% endif %}
-| Metric | Value |
-|--------|------:|
-| **Coverage on diff lines** | **{{ "%.1f" | format(report.total_percent_covered) }}%** |
-| Lines changed | {{ report.total_num_lines }} |
-| Lines uncovered | {{ report.total_num_violations }} |
-| Files changed | {{ report.files | length }} |
+`{{ report.total_percent_covered | progress_bar }}` **{{ pct }}%** on changed lines
+ |
+
+{% if fail_under > 0 and not threshold_met %}
+> **{{ "%.0f" | format(fail_under) }}%** required — missing **{{ "%.1f" | format(fail_under - report.total_percent_covered) }}%** more coverage
+{% endif %}
+
+**{{ report.total_num_lines }}** lines changed **{{ report.total_num_violations }}** uncovered **{{ report.files | length }}** file{{ "s" if report.files | length != 1 else "" }}
{% if report.files %}
-File breakdown ({{ report.files | length }} file{{ "s" if report.files | length != 1 else "" }})
+ 📂 {{ report.files | length }} file{{ "s" if report.files | length != 1 else "" }} changed
+
-| File | Coverage | Uncovered Lines |
-|------|:--------:|:---------------:|
+| | File | Coverage | Uncovered Lines |
+|:---:|:---|---:|:---|
{% for f in report.files | sort(attribute='percent_covered') %}
-| `{{ f.path }}` | {{ "%.1f" | format(f.percent_covered) }}% | {{ f.violation_lines[:10] | join(', ') }}{% if f.violation_lines | length > 10 %} (+{{ f.violation_lines | length - 10 }} more){% endif %} |
+| {{ f.percent_covered | status_icon }} | `{{ f.path }}` | {{ "%.1f" | format(f.percent_covered) }}% | {{ f.violation_lines[:10] | join(', ') if f.violation_lines else '—' }}{% if f.violation_lines | length > 10 %} (+{{ f.violation_lines | length - 10 }} more){% endif %} |
{% endfor %}
@@ -30,12 +36,12 @@
{% if md_report_content %}
-Full diff-cover report
+ 📋 Full diff-cover report
+
{{ md_report_content }}
{% endif %}
----
-Posted by diff-cover-action
+🛡️ diff-cover-action
diff --git a/templates/comment_quality.md.j2 b/templates/comment_quality.md.j2
index 0ecab87..db12d32 100644
--- a/templates/comment_quality.md.j2
+++ b/templates/comment_quality.md.j2
@@ -1,28 +1,34 @@
-## {{ icon }} Diff Quality: {{ "%.1f" | format(report.total_percent_covered) }}%
+{% set pct = "%.1f" | format(report.total_percent_covered) %}
+{% set color = report.total_percent_covered | badge_color %}
+
+
{% if fail_under > 0 %}
{% if threshold_met %}
-> :heavy_check_mark: Meets threshold of {{ "%.0f" | format(fail_under) }}%
+
{% else %}
-> :x: Below threshold of {{ "%.0f" | format(fail_under) }}% — needs {{ "%.1f" | format(fail_under - report.total_percent_covered) }}% more quality coverage
+
{% endif %}
{% endif %}
-| Metric | Value |
-|--------|------:|
-| **Quality on diff lines** | **{{ "%.1f" | format(report.total_percent_covered) }}%** |
-| Lines changed | {{ report.total_num_lines }} |
-| Lines with violations | {{ report.total_num_violations }} |
-| Files changed | {{ report.files | length }} |
+`{{ report.total_percent_covered | progress_bar }}` **{{ pct }}%** on changed lines
+ |
+
+{% if fail_under > 0 and not threshold_met %}
+> **{{ "%.0f" | format(fail_under) }}%** required — missing **{{ "%.1f" | format(fail_under - report.total_percent_covered) }}%** more quality coverage
+{% endif %}
+
+**{{ report.total_num_lines }}** lines changed **{{ report.total_num_violations }}** violations **{{ report.files | length }}** file{{ "s" if report.files | length != 1 else "" }}
{% if report.files %}
-File breakdown ({{ report.files | length }} file{{ "s" if report.files | length != 1 else "" }})
+ 📂 {{ report.files | length }} file{{ "s" if report.files | length != 1 else "" }} changed
+
-| File | Quality | Violation Lines |
-|------|:-------:|:---------------:|
+| | File | Quality | Violation Lines |
+|:---:|:---|---:|:---|
{% for f in report.files | sort(attribute='percent_covered') %}
-| `{{ f.path }}` | {{ "%.1f" | format(f.percent_covered) }}% | {{ f.violation_lines[:10] | join(', ') }}{% if f.violation_lines | length > 10 %} (+{{ f.violation_lines | length - 10 }} more){% endif %} |
+| {{ f.percent_covered | status_icon }} | `{{ f.path }}` | {{ "%.1f" | format(f.percent_covered) }}% | {{ f.violation_lines[:10] | join(', ') if f.violation_lines else '—' }}{% if f.violation_lines | length > 10 %} (+{{ f.violation_lines | length - 10 }} more){% endif %} |
{% endfor %}
@@ -30,12 +36,12 @@
{% if md_report_content %}
-Full diff-quality report
+ 📋 Full diff-quality report
+
{{ md_report_content }}
{% endif %}
----
-Posted by diff-cover-action
+🛡️ diff-cover-action
diff --git a/templates/step_summary.md.j2 b/templates/step_summary.md.j2
index 0c44b46..5dc8e08 100644
--- a/templates/step_summary.md.j2
+++ b/templates/step_summary.md.j2
@@ -1,27 +1,28 @@
-## :{{ icon }}: Diff {{ "Coverage" if mode == "coverage" else "Quality" }}: {{ "%.1f" | format(report.total_percent_covered) }}%
+{% set pct = "%.1f" | format(report.total_percent_covered) %}
+{% set color = report.total_percent_covered | badge_color %}
+{% set label = "diff_coverage" if mode == "coverage" else "diff_quality" %}
+
{% if fail_under > 0 %}
{% if threshold_met %}
-> :heavy_check_mark: Meets threshold of {{ "%.0f" | format(fail_under) }}%
+
{% else %}
-> :x: Below threshold of {{ "%.0f" | format(fail_under) }}%
+
{% endif %}
{% endif %}
-| Metric | Value |
-|--------|------:|
-| **{{ "Coverage" if mode == "coverage" else "Quality" }} on diff lines** | **{{ "%.1f" | format(report.total_percent_covered) }}%** |
-| Lines changed | {{ report.total_num_lines }} |
-| {{ "Lines uncovered" if mode == "coverage" else "Lines with violations" }} | {{ report.total_num_violations }} |
-| Files changed | {{ report.files | length }} |
+`{{ report.total_percent_covered | progress_bar }}` **{{ pct }}%** on changed lines
+
+**{{ report.total_num_lines }}** lines changed **{{ report.total_num_violations }}** {{ "uncovered" if mode == "coverage" else "violations" }} **{{ report.files | length }}** file{{ "s" if report.files | length != 1 else "" }}
{% if report.files %}
-File breakdown
+ 📂 File details
+
-| File | {{ "Coverage" if mode == "coverage" else "Quality" }} |
-|------|:--------:|
+| | File | {{ "Coverage" if mode == "coverage" else "Quality" }} |
+|:---:|:---|---:|
{% for f in report.files | sort(attribute='percent_covered') %}
-| `{{ f.path }}` | {{ "%.1f" | format(f.percent_covered) }}% |
+| {{ f.percent_covered | status_icon }} | `{{ f.path }}` | {{ "%.1f" | format(f.percent_covered) }}% |
{% endfor %}
diff --git a/tests/test_comment.py b/tests/test_comment.py
index b21ccb0..63758e5 100644
--- a/tests/test_comment.py
+++ b/tests/test_comment.py
@@ -95,7 +95,7 @@ def test_render_comment_body() -> None:
)
assert "" in body
assert "90.0%" in body
- assert "Meets threshold" in body
+ assert "threshold-passed-success" in body
assert "src/foo.py" in body
@@ -116,7 +116,7 @@ def test_render_comment_body_below_threshold() -> None:
identifier="test",
md_report_content="",
)
- assert "Below threshold" in body
+ assert "threshold-failed-critical" in body
@responses.activate