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% +>
+> 82.0% +>  passed > -> > :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% +>
+> 45.0% +>  failed > -> > :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% +>  failed +> +> `███████████████░░░░░` **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 %} +
+{{ pct }}% {% if fail_under > 0 %} {% if threshold_met %} -> :heavy_check_mark: Meets threshold of {{ "%.0f" | format(fail_under) }}% + passed {% else %} -> :x: Below threshold of {{ "%.0f" | format(fail_under) }}% — needs {{ "%.1f" | format(fail_under - report.total_percent_covered) }}% more coverage + failed {% 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 %} +
+{{ pct }}% {% if fail_under > 0 %} {% if threshold_met %} -> :heavy_check_mark: Meets threshold of {{ "%.0f" | format(fail_under) }}% + passed {% else %} -> :x: Below threshold of {{ "%.0f" | format(fail_under) }}% — needs {{ "%.1f" | format(fail_under - report.total_percent_covered) }}% more quality coverage + failed {% 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" %} +{{ pct }}% {% if fail_under > 0 %} {% if threshold_met %} -> :heavy_check_mark: Meets threshold of {{ "%.0f" | format(fail_under) }}% + passed {% else %} -> :x: Below threshold of {{ "%.0f" | format(fail_under) }}% + failed {% 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