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
34 changes: 33 additions & 1 deletion .github/workflows/clang_tidy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ on:
branches: [main]
merge_group:
types: [checks_requested]
workflow_call:
outputs:
artifact-name:
description: "Name of the clang-tidy findings artifact"
value: ${{ jobs.clang-tidy.outputs.artifact-name }}
errors:
description: "Total clang-tidy error count"
value: ${{ jobs.clang-tidy.outputs.errors }}
warnings:
description: "Total clang-tidy warning count"
value: ${{ jobs.clang-tidy.outputs.warnings }}
conclusion:
description: "Job conclusion: success or failure"
value: ${{ jobs.clang-tidy.outputs.conclusion }}

permissions:
contents: read
Expand All @@ -48,6 +62,11 @@ env:
jobs:
clang-tidy:
runs-on: ubuntu-24.04
outputs:
artifact-name: clang-tidy-findings
errors: ${{ steps.findings.outputs.errors }}
warnings: ${{ steps.findings.outputs.warnings }}
conclusion: ${{ steps.set-conclusion.outputs.conclusion }}

steps:
- name: Checkout repository
Expand Down Expand Up @@ -157,11 +176,24 @@ jobs:
if-no-files-found: ignore
retention-days: 7

# Clang-tidy findings are always collected and uploaded as an artifact,
# but they should only block merging when a developer is actively
# submitting code — not during nightly reporting runs.
- name: Fail check if clang-tidy errors found in changed files
if: steps.findings.outputs.errors_blocking != '0'
if: (github.event_name == 'pull_request' || github.event_name == 'push' || github.event_name == 'merge_group') && steps.findings.outputs.errors_blocking != '0'
run: |
echo "::error::Clang-tidy found ${{ steps.findings.outputs.errors_blocking }} error(s) in changed files. See the 'clang-tidy-findings' artifact for details."
echo ""
echo "=== Clang-tidy errors ==="
cat clang_tidy_errors.txt || true
exit 1

- name: Set conclusion
id: set-conclusion
if: always()
run: |
if [[ "${{ steps.run-clang-tidy.outcome }}" == "success" ]]; then
echo "conclusion=success" >> $GITHUB_OUTPUT
else
echo "conclusion=failure" >> $GITHUB_OUTPUT
fi
2 changes: 1 addition & 1 deletion .github/workflows/coverage_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ jobs:

- name: Run Unit Test with Coverage for C++
id: run-coverage
continue-on-error: true
run: |
bazel coverage //... --build_tests_only

Expand All @@ -78,6 +77,7 @@ jobs:

- name: Set conclusion
id: set-conclusion
if: always()
run: |
if [[ "${{ steps.run-coverage.outcome }}" == "success" ]]; then
echo "conclusion=success" >> $GITHUB_OUTPUT
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy_docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ jobs:
fi
echo "Documentation built successfully"
echo "Files generated:"
find docs_output -type f | head -20
find docs_output -type f | head -20 || true # head closes the pipe early, causing find to get SIGPIPE. prevents set -e from failing the step

- name: Upload docs to release
if: startsWith(github.ref, 'refs/tags/v')
Expand Down
61 changes: 34 additions & 27 deletions .github/workflows/nightly_quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ name: Nightly Quality Jobs

on:
schedule:
- cron: '0 0 * * *' # every day at midnight UTC
- cron: "0 0 * * *" # every day at midnight UTC
workflow_dispatch:

permissions:
Expand All @@ -49,11 +49,19 @@ jobs:
permissions:
contents: read

# --------------------------------------------------------------------
# Quality job 2: Clang-Tidy
# --------------------------------------------------------------------
run-clang-tidy:
uses: ./.github/workflows/clang_tidy.yml
permissions:
contents: read

# --------------------------------------------------------------------
# Collect results, build the dashboard, upload artifact for docs.yml
# --------------------------------------------------------------------
deploy-quality-reports:
needs: [run-coverage]
needs: [run-coverage, run-clang-tidy]
# Always run even if individual quality jobs fail, so the dashboard
# still reflects which jobs passed and which failed.
if: always()
Expand All @@ -75,53 +83,52 @@ jobs:
cache-save: ${{ github.ref == 'refs/heads/main' }}

- name: Allow linux-sandbox
id: setup
uses: ./actions/unblock_user_namespace_for_linux_sandbox

# ------------------------------------------------------------------
# Determine job conclusions (needs.*.result = success|failure|skipped|cancelled)
# ------------------------------------------------------------------
- name: Resolve job conclusions
id: conclusions
run: |
map_conclusion() {
case "$1" in
success) echo "success" ;;
failure) echo "failure" ;;
*) echo "skipped" ;;
esac
}
echo "coverage=$(map_conclusion '${{ needs.run-coverage.result }}')" >> $GITHUB_OUTPUT

# ------------------------------------------------------------------
# Download artifacts (only if the upstream job succeeded)
# Download Coverage artifacts (only if the upstream job succeeded)
# ------------------------------------------------------------------
- name: Download coverage artifact
if: needs.run-coverage.result == 'success'
id: download-coverage
if: needs.run-coverage.outputs.conclusion == 'success'
uses: actions/download-artifact@v4
with:
name: ${{ needs.run-coverage.outputs.artifact-name }}
path: /tmp/coverage_zip

# ------------------------------------------------------------------
# Extract coverage artifact to a known location for the dashboard generator
# ------------------------------------------------------------------
- name: Extract coverage HTML report
if: needs.run-coverage.result == 'success'
if: needs.run-coverage.outputs.conclusion == 'success' && steps.download-coverage.outcome == 'success'
run: |
bash quality/scripts/extract_coverage_artifact.sh \
/tmp/coverage_zip \
"${GITHUB_WORKSPACE}/_quality/coverage"

# ------------------------------------------------------------------
# Generate coverage KPI dashboard via generate_dashboard py_binary
# Download clang-tidy findings artifact
# ------------------------------------------------------------------
- name: Download clang-tidy findings
if: needs.run-clang-tidy.outputs.conclusion == 'success'
uses: actions/download-artifact@v4
with:
name: ${{ needs.run-clang-tidy.outputs.artifact-name }}
path: /tmp/clang_tidy

# ------------------------------------------------------------------
# Generate coverage KPI dashboard via generate_dashboard py_binary
# ------------------------------------------------------------------
- name: Generate quality dashboard
if: always() && steps.conclusions.outcome == 'success'
if: always() && steps.setup.outcome == 'success'
run: |
# The LCOV .dat file is inside the extracted coverage zip at a known
# path; pass it unconditionally — generate_dashboard handles the
# case where the file is absent (returns empty coverage data).
# Pass LCOV data and clang-tidy findings, generate_dashboard handles
# absent files gracefully (shows N/A for that metric).
bazel run //quality/dashboard:generate_dashboard -- \
--lcov /tmp/coverage_zip/extracted/artifacts/coverage_report.dat \
--html "${GITHUB_WORKSPACE}/_quality/index.html" \
--clang-tidy /tmp/clang_tidy/clang_tidy_findings.txt \
--html _quality/index.html \
--github-summary

echo "Dashboard generated. Contents of _quality/:"
Expand All @@ -132,7 +139,7 @@ jobs:
# pick up and deploy as part of the unified Sphinx site.
# ------------------------------------------------------------------
- name: Upload quality reports artifact
if: always() && steps.conclusions.outcome == 'success'
if: always() && steps.setup.outcome == 'success'
uses: actions/upload-artifact@v4
with:
name: nightly-quality-reports
Expand Down
11 changes: 9 additions & 2 deletions bazel/rules/generate_quality_links.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,16 @@ def _generate_quality_links_impl(ctx):
# quality reports are published alongside the latest/ docs
coverage_ref = "`Coverage report <quality/coverage/index.html>`__"
dashboard_ref = "`Quality Dashboard <quality/index.html>`__"
clang_tidy_ref = "`Clang-Tidy report <quality/clang_tidy_findings.txt>`__"
elif docs_version and docs_base_url:
# versioned release — quality reports only live at latest/
latest = docs_base_url + "/latest"
coverage_ref = ("`Coverage report (latest) <" + latest +
"/quality/coverage/index.html>`__")
dashboard_ref = ("`Quality Dashboard (latest) <" + latest +
"/quality/index.html>`__")
clang_tidy_ref = ("`Clang-Tidy report (latest) <" + latest +
"/quality/clang_tidy_findings.txt>`__")
else:
# local build — no published reports; show the equivalent bazel command
coverage_ref = (
Expand All @@ -56,10 +59,14 @@ def _generate_quality_links_impl(ctx):
dashboard_ref = (
"*local build* — dashboard only available on GitHub Pages"
)

clang_tidy_ref = (
"*local build* — run " +
"``bazel test --config=clang-tidy //...``"
)
content = (
".. |coverage_report_link| replace:: " + coverage_ref + "\n" +
".. |quality_dashboard_link| replace:: " + dashboard_ref + "\n"
".. |quality_dashboard_link| replace:: " + dashboard_ref + "\n" +
".. |clang_tidy_report_link| replace:: " + clang_tidy_ref + "\n"
)

output = ctx.actions.declare_file(ctx.label.name + ".rst")
Expand Down
3 changes: 3 additions & 0 deletions docs/sphinx/quality_reports.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ nightly run of the `Nightly Quality Jobs`_ workflow.
* - Coverage
- Line, function, and branch coverage from C++ unit tests (gcov/lcov)
- |coverage_report_link|
* - Clang-Tidy
- Static analysis findings (errors and warnings) across all C++ targets
- |clang_tidy_report_link|

|quality_dashboard_link|

Expand Down
37 changes: 34 additions & 3 deletions quality/dashboard/dashboard.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Coverage Dashboard</title>
<title>Quality Dashboard</title>
<style>
:root { --bg:#0d1117; --surface:#161b22; --border:#30363d; --text:#e6edf3; --muted:#8b949e; --accent:#58a6ff; }
* { box-sizing:border-box; margin:0; padding:0; }
Expand Down Expand Up @@ -36,7 +36,7 @@
</style>
</head>
<body>
<h1>Coverage Dashboard</h1>
<h1>Quality Dashboard</h1>
<p class="meta">Generated: {{ timestamp }}</p>

{# ── Summary cards ── #}
Expand All @@ -54,6 +54,29 @@
<p class="muted" style="margin-bottom:1.5rem">No coverage data available.</p>
{% endif %}

{# ── Clang-Tidy summary cards ── #}
<h2>Clang-Tidy</h2>
{% if clang_tidy %}
<div class="cards">
<div class="card">
<div class="num" style="color:{% if clang_tidy.errors == 0 %}#27ae60{% else %}#e74c3c{% endif %}">{{ clang_tidy.errors }}</div>
{% if prev and prev.ct_errors is not none %}<div class="delta">{{ delta(clang_tidy.errors, prev.ct_errors, false) }}</div>{% endif %}
<div class="label">Errors</div>
</div>
<div class="card">
<div class="num" style="color:{% if clang_tidy.warnings == 0 %}#27ae60{% else %}#e67e22{% endif %}">{{ clang_tidy.warnings }}</div>
{% if prev and prev.ct_warnings is not none %}<div class="delta">{{ delta(clang_tidy.warnings, prev.ct_warnings, false) }}</div>{% endif %}
<div class="label">Warnings</div>
</div>
<div class="card">
<div class="num" style="color:var(--muted)">{{ clang_tidy.total }}</div>
<div class="label">Total Findings</div>
</div>
</div>
{% else %}
<p class="muted" style="margin-bottom:1.5rem">No clang-tidy data available.</p>
{% endif %}

{# ── Per-file coverage table ── #}
<h2>Per-File Coverage</h2>
{% if cov_files %}
Expand Down Expand Up @@ -126,7 +149,7 @@
<h2>Run History</h2>
<table>
<thead>
<tr><th>Date</th><th>Line Cov</th><th>Function Cov</th><th>Branch Cov</th></tr>
<tr><th>Date</th><th>Line Cov</th><th>Function Cov</th><th>Branch Cov</th><th>CT Errors</th><th>CT Warnings</th></tr>
</thead>
<tbody>
{% for snap in history|reverse %}
Expand All @@ -143,6 +166,14 @@
{% else %}N/A{% endif %}
</td>
{% endfor %}
{% for key, higher_better in [('ct_errors', false), ('ct_warnings', false)] %}
<td>
{% if snap[key] is not none %}
<span style="color:{% if snap[key] == 0 %}#27ae60{% else %}#e74c3c{% endif %}">{{ snap[key] }}</span>
{% if ps and ps[key] is not none %} {{ delta(snap[key], ps[key], higher_better) }}{% endif %}
{% else %}N/A{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
Expand Down
Loading
Loading