diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 898beb7bc..e1c3c084b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -17,6 +17,14 @@ on: schedule: - cron: '0 2 * * *' # Nightly at 2 AM UTC workflow_dispatch: # Allow maintainers to trigger manually when needed + workflow_call: + outputs: + artifact-name: + description: Name of the uploaded CSV artifact + value: codeql-csv-results + conclusion: + description: Job conclusion (success/failure) + value: ${{ jobs.run-codeql.result }} permissions: contents: read @@ -50,6 +58,13 @@ jobs: - name: Allow linux-sandbox uses: ./actions/unblock_user_namespace_for_linux_sandbox + - name: Stub out ARM-only dependencies unavailable in CI + run: | + mkdir -p /tmp/ape_stub + touch /tmp/ape_stub/BUILD.bazel + touch /tmp/ape_stub/WORKSPACE + echo "common --override_repository=ape+=/tmp/ape_stub" >> user.bazelrc + - name: Create CodeQL database run: | bazel run //quality/static_analysis:codeql_lint -- \ diff --git a/.github/workflows/nightly_quality.yml b/.github/workflows/nightly_quality.yml index 6fb21a2c0..2d261e26e 100644 --- a/.github/workflows/nightly_quality.yml +++ b/.github/workflows/nightly_quality.yml @@ -16,9 +16,10 @@ # Runs every night at midnight UTC: # - Coverage : full C++ test suite with gcov/lcov → HTML report # - Clang-Tidy: static analysis across all C++ targets → findings text file +# - CodeQL : MISRA C++ compliance analysis → CSV findings # -# After both jobs complete, deploy-quality-reports: -# 1. Downloads the coverage HTML artifact and the clang-tidy findings artifact +# After all jobs complete, deploy-quality-reports: +# 1. Downloads the coverage HTML artifact, clang-tidy findings, and CodeQL CSV # 2. Runs `bazel run //quality/dashboard:generate_dashboard` to produce the KPI dashboard # 3. Uploads everything as nightly-quality-reports artifact for docs.yml to deploy # @@ -26,6 +27,7 @@ # https://eclipse-score.github.io/communication/latest/quality/index.html ← dashboard # https://eclipse-score.github.io/communication/latest/quality/coverage/index.html ← lcov HTML # https://eclipse-score.github.io/communication/latest/quality/clang_tidy_findings.txt ← raw findings +# https://eclipse-score.github.io/communication/latest/quality/codeql/index.html ← CodeQL report name: Nightly Quality Jobs @@ -59,11 +61,20 @@ jobs: permissions: contents: read + # -------------------------------------------------------------------- + # Quality job 3: CodeQL + # -------------------------------------------------------------------- + run-codeql: + uses: ./.github/workflows/codeql.yml + permissions: + contents: read + security-events: write + # -------------------------------------------------------------------- # Collect results, build the dashboard, upload artifact for docs.yml # -------------------------------------------------------------------- deploy-quality-reports: - needs: [run-coverage, run-clang-tidy] + needs: [run-coverage, run-clang-tidy, run-codeql] # Always run even if individual quality jobs fail, so the dashboard # still reflects which jobs passed and which failed. if: always() @@ -88,6 +99,13 @@ jobs: id: setup uses: ./actions/unblock_user_namespace_for_linux_sandbox + - name: Stub out ARM-only dependencies unavailable in CI + run: | + mkdir -p /tmp/ape_stub + touch /tmp/ape_stub/BUILD.bazel + touch /tmp/ape_stub/WORKSPACE + echo "common --override_repository=ape+=/tmp/ape_stub" >> user.bazelrc + # ------------------------------------------------------------------ # Download Coverage artifacts (only if the upstream job succeeded) # ------------------------------------------------------------------ @@ -131,6 +149,28 @@ jobs: echo "::warning::clang_tidy_findings.txt not found in downloaded artifact; skipping copy." fi + # ------------------------------------------------------------------ + # Download CodeQL CSV artifact + # ------------------------------------------------------------------ + - name: Download CodeQL CSV artifact + if: always() + continue-on-error: true + uses: actions/download-artifact@v4 + with: + name: ${{ needs.run-codeql.outputs.artifact-name }} + path: /tmp/codeql + + - name: Copy CodeQL CSV into quality output + if: always() + run: | + mkdir -p "${GITHUB_WORKSPACE}/_quality" + if [[ -f /tmp/codeql/codeql-nightly.csv ]]; then + cp /tmp/codeql/codeql-nightly.csv \ + "${GITHUB_WORKSPACE}/_quality/codeql_findings.txt" + else + echo "::warning::codeql-nightly.csv not found; skipping copy." + fi + # ------------------------------------------------------------------ # Generate coverage KPI dashboard via generate_dashboard py_binary # ------------------------------------------------------------------ @@ -142,6 +182,7 @@ jobs: bazel run //quality/dashboard:generate_dashboard -- \ --lcov /tmp/coverage_zip/extracted/artifacts/coverage_report.dat \ --clang-tidy /tmp/clang_tidy/clang_tidy_findings.txt \ + --codeql-csv "${GITHUB_WORKSPACE}/_quality/codeql_findings.txt" \ --html "${GITHUB_WORKSPACE}/_quality/index.html" \ --github-summary diff --git a/bazel/rules/generate_quality_links.bzl b/bazel/rules/generate_quality_links.bzl index 6cde02fc4..01dcdf90a 100644 --- a/bazel/rules/generate_quality_links.bzl +++ b/bazel/rules/generate_quality_links.bzl @@ -41,6 +41,7 @@ def _generate_quality_links_impl(ctx): coverage_ref = "`Coverage report `__" dashboard_ref = "`Quality Dashboard `__" clang_tidy_ref = "`Clang-Tidy report `__" + codeql_ref = "`CodeQL findings `__" elif docs_version and docs_base_url: # versioned release — quality reports only live at latest/ latest = docs_base_url + "/latest" @@ -50,6 +51,8 @@ def _generate_quality_links_impl(ctx): "/quality/index.html>`__") clang_tidy_ref = ("`Clang-Tidy report (latest) <" + latest + "/quality/clang_tidy_findings.txt>`__") + codeql_ref = ("`CodeQL findings (latest) <" + latest + + "/quality/codeql_findings.txt>`__") else: # local build — no published reports; show the equivalent bazel command coverage_ref = ( @@ -63,10 +66,15 @@ def _generate_quality_links_impl(ctx): "*local build* — run " + "``bazel test --config=clang-tidy //...``" ) + codeql_ref = ( + "*local build* — run " + + "``bazel run //quality/static_analysis:codeql_lint``" + ) content = ( ".. |coverage_report_link| replace:: " + coverage_ref + "\n" + ".. |quality_dashboard_link| replace:: " + dashboard_ref + "\n" + - ".. |clang_tidy_report_link| replace:: " + clang_tidy_ref + "\n" + ".. |clang_tidy_report_link| replace:: " + clang_tidy_ref + "\n" + + ".. |codeql_report_link| replace:: " + codeql_ref + "\n" ) output = ctx.actions.declare_file(ctx.label.name + ".rst") diff --git a/coding-standards.yaml b/coding-standards.yaml deleted file mode 120000 index e7bcf55de..000000000 --- a/coding-standards.yaml +++ /dev/null @@ -1 +0,0 @@ -quality/static_analysis/coding-standards.yaml \ No newline at end of file diff --git a/dashboard.html b/dashboard.html new file mode 100644 index 000000000..bc71fa569 --- /dev/null +++ b/dashboard.html @@ -0,0 +1,100 @@ + + + + + +Quality Dashboard + + + +

Quality Dashboard

+

Generated: 2026-06-17 05:32 UTC

+ + + +

No coverage data available.

+ + + +

Clang-Tidy

+ +

No clang-tidy data available.

+ + + +

CodeQL

+ +
+
+
0
+ +
Errors
+
+
+
0
+ +
Warnings
+
+
+
6411
+ +
Recommendations
+
+
+
6411
+ +
Total Findings
+
+
+ + + +

Per-File Coverage

+ +

No per-file coverage data available.

+ + + + + + + + \ No newline at end of file diff --git a/docs/sphinx/quality_reports.rst b/docs/sphinx/quality_reports.rst index 1db32d185..f256042cf 100644 --- a/docs/sphinx/quality_reports.rst +++ b/docs/sphinx/quality_reports.rst @@ -19,6 +19,9 @@ nightly run of the `Nightly Quality Jobs`_ workflow. * - Clang-Tidy - Static analysis findings (errors and warnings) across all C++ targets - |clang_tidy_report_link| + * - CodeQL + - MISRA C++ compliance findings via CodeQL (codeql/misra-cpp-coding-standards) + - |codeql_report_link| |quality_dashboard_link| diff --git a/quality/dashboard/__pycache__/generate_dashboard.cpython-312.pyc b/quality/dashboard/__pycache__/generate_dashboard.cpython-312.pyc new file mode 100644 index 000000000..09c12fa4b Binary files /dev/null and b/quality/dashboard/__pycache__/generate_dashboard.cpython-312.pyc differ diff --git a/quality/dashboard/dashboard.html.j2 b/quality/dashboard/dashboard.html.j2 index a1aa1fc71..21324acdd 100644 --- a/quality/dashboard/dashboard.html.j2 +++ b/quality/dashboard/dashboard.html.j2 @@ -77,44 +77,32 @@

No clang-tidy data available.

{% endif %} -{# ── Per-file coverage table ── #} -

Per-File Coverage

-{% if cov_files %} - - - - - - - - - - - - {% for f in cov_files %} - - - - - - - - {% endfor %} - -
FileLines ↕Functions ↕Branches ↕Lines (hit/total)
{{ f.file|basename }} -
- {{ '%.1f'|format(f.line_pct) }}% -
-
- {{ '%.1f'|format(f.func_pct) }}% -
-
- {{ '%.1f'|format(f.branch_pct) }}% -
{{ f.lh }}/{{ f.lf }}
+{# ── CodeQL summary cards ── #} +

CodeQL

+{% if codeql %} +
+
+
{{ codeql.errors + codeql.recommendations }}
+ {% if prev and prev.codeql_errors is not none %}
{{ delta(codeql.errors + codeql.recommendations, prev.codeql_errors, false) }}
{% endif %} +
Errors
+
+
+
{{ codeql.warnings }}
+ {% if prev and prev.codeql_warnings is not none %}
{{ delta(codeql.warnings, prev.codeql_warnings, false) }}
{% endif %} +
Warnings
+
+
+
{{ codeql.total }}
+ {% if prev and prev.codeql_total is not none %}
{{ delta(codeql.total, prev.codeql_total, false) }}
{% endif %} +
Total Findings
+
+
{% else %} -

No per-file coverage data available.

+

No CodeQL data available.

{% endif %} + + {# ── KPI Trends ── #} {% if history|length >= 2 %}

Coverage Trend

@@ -149,7 +137,7 @@

Run History

- + {% for snap in history|reverse %} @@ -174,21 +162,19 @@ {% else %}N/A{% endif %} {% endfor %} + {% for key, higher_better in [('codeql_errors', false), ('codeql_warnings', false), ('codeql_total', false)] %} + + {% endfor %} {% endfor %}
DateLine CovFunction CovBranch CovCT ErrorsCT Warnings
DateLine CovFunction CovBranch CovCT ErrorsCT WarningsCQ ErrorsCQ WarningsCQ Total
+ {% if snap[key] is not none %} + {{ snap[key] }} + {% if ps and ps[key] is not none %} {{ delta(snap[key], ps[key], higher_better) }}{% endif %} + {% else %}N/A{% endif %} +
{% endif %} - diff --git a/quality/dashboard/generate_dashboard.py b/quality/dashboard/generate_dashboard.py index 17825c10b..de44d7274 100644 --- a/quality/dashboard/generate_dashboard.py +++ b/quality/dashboard/generate_dashboard.py @@ -25,6 +25,7 @@ """ import argparse +import csv import json import os import pathlib @@ -133,6 +134,63 @@ def load_clang_tidy(path: pathlib.Path) -> dict | None: warnings = len([l for l in text.splitlines() if "warning:" in l]) return {"errors": errors, "warnings": warnings, "total": errors + warnings} + +def load_codeql_csv(path: pathlib.Path) -> dict | None: + """Return {errors, warnings, recommendations, total, findings} from a CodeQL CSV results file.""" + if not path or not path.is_file(): + return None + errors = warnings = recommendations = 0 + findings = [] + severity_counts = {} # For debugging + try: + with path.open(encoding="utf-8", errors="replace", newline="") as fh: + reader = csv.DictReader(fh) + if reader.fieldnames: + print(f"CodeQL CSV columns: {reader.fieldnames}", file=sys.stderr) + for row in reader: + # Try multiple severity column name variations + severity = (row.get("severity") or row.get("Severity") or row.get("level") or row.get("Level") or "").lower().strip() + + # Track severity values for debugging + raw_severity = severity + severity_counts[raw_severity] = severity_counts.get(raw_severity, 0) + 1 + + # Categorize + if severity == "error" or severity == "fail": + errors += 1 + severity = "error" + elif severity == "warning" or severity == "warn": + warnings += 1 + severity = "warning" + else: + # Treat everything else as recommendation (including empty or unknown) + recommendations += 1 + severity = "recommendation" + + findings.append({ + "severity": severity, + "name": row.get("name") or row.get("Name") or row.get("rule_id") or row.get("Rule") or "", + "message": row.get("message") or row.get("Message") or row.get("description") or row.get("Description") or "", + "path": row.get("path") or row.get("Path") or row.get("file") or row.get("File") or "", + "line": row.get("start:line") or row.get("Line") or row.get("line_number") or "", + }) + except (OSError, csv.Error) as e: + print(f"Error parsing CodeQL CSV: {e}", file=sys.stderr) + return None + + # Debug: show what severity values we found + if severity_counts: + print(f"CodeQL severity distribution: {dict(sorted(severity_counts.items(), key=lambda x: x[1], reverse=True))}", file=sys.stderr) + + return { + "loaded": True, + "errors": errors, + "warnings": warnings, + "recommendations": recommendations, + "total": errors + warnings + recommendations, + "findings": findings, + } + def load_history(path: pathlib.Path) -> list[dict]: if not path or not path.exists(): return [] @@ -151,7 +209,7 @@ def save_history(path: pathlib.Path, history: list[dict]) -> None: # ── HTML rendering ──────────────────────────────────────────────────────────── -def render_dashboard(cov_summary, cov_files, clang_tidy, history, timestamp) -> str: +def render_dashboard(cov_summary, cov_files, clang_tidy, codeql, history, timestamp) -> str: env = Environment(loader=FileSystemLoader(str(_TEMPLATE_DIR)), autoescape=True) env.globals["cov_colour"] = _cov_colour env.globals["delta"] = _delta_badge @@ -163,6 +221,7 @@ def render_dashboard(cov_summary, cov_files, clang_tidy, history, timestamp) -> cov=cov_summary or None, cov_files=cov_files, clang_tidy=clang_tidy, + codeql=codeql, history=history, prev=history[-2] if len(history) >= 2 else None, ) @@ -170,7 +229,7 @@ def render_dashboard(cov_summary, cov_files, clang_tidy, history, timestamp) -> # ── GitHub Actions step summary ─────────────────────────────────────────────── -def write_github_summary(cov_summary, clang_tidy, history, summary_path) -> None: +def write_github_summary(cov_summary, clang_tidy, codeql, history, summary_path) -> None: lines = ["## Quality Dashboard\n"] lines.append("### Coverage\n") @@ -198,6 +257,20 @@ def write_github_summary(cov_summary, clang_tidy, history, summary_path) -> None else: lines.append("Clang-tidy data not available.\n") + lines.append("\n### CodeQL (MISRA C++)\n") + if codeql: + err_icon = ":x:" if codeql["errors"] > 0 else ":white_check_mark:" + lines += [ + "| Metric | Count |", + "|--------|------:|", + f"| {err_icon} Errors | **{codeql['errors']}** |", + f"| :warning: Warnings | **{codeql['warnings']}** |", + f"| :information_source: Recommendations | **{codeql['recommendations']}** |", + f"| Total | **{codeql['total']}** |", + ] + else: + lines.append("CodeQL data not available.\n") + if len(history) >= 2: prev, curr = history[-2], history[-1] lines += [ @@ -211,6 +284,9 @@ def write_github_summary(cov_summary, clang_tidy, history, summary_path) -> None ("Branch coverage %", "branch_cov", True), ("Clang-Tidy errors", "ct_errors", False), ("Clang-Tidy warnings", "ct_warnings", False), + ("CodeQL errors", "codeql_errors", False), + ("CodeQL warnings", "codeql_warnings", False), + ("CodeQL total", "codeql_total", False), ]: pv, cv = prev.get(key), curr.get(key) if pv is not None and cv is not None: @@ -240,6 +316,11 @@ def main() -> int: dest="clang_tidy", help="Path to clang-tidy findings text file", ) + parser.add_argument( + "--codeql-csv", default="", + dest="codeql_csv", + help="Path to CodeQL CSV results file", + ) parser.add_argument( "--html", default="dashboard.html", help="Output HTML dashboard path", @@ -256,6 +337,7 @@ def main() -> int: lcov_path = pathlib.Path(args.lcov) if args.lcov else pathlib.Path("") ct_path = pathlib.Path(args.clang_tidy) if args.clang_tidy else pathlib.Path("") + codeql_path = pathlib.Path(args.codeql_csv) if args.codeql_csv else pathlib.Path("") html_path = pathlib.Path(args.html) hist_path = pathlib.Path(args.history) if args.history else None @@ -263,22 +345,27 @@ def main() -> int: cov_summary, cov_files = load_lcov(lcov_path) clang_tidy = load_clang_tidy(ct_path) + codeql = load_codeql_csv(codeql_path) timestamp = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d %H:%M UTC") history = load_history(hist_path) if hist_path else [] history.append({ - "date": timestamp, - "line_cov": cov_summary.get("line_pct") if cov_summary else None, - "func_cov": cov_summary.get("func_pct") if cov_summary else None, - "branch_cov": cov_summary.get("branch_pct") if cov_summary else None, - "ct_errors": clang_tidy["errors"] if clang_tidy else None, - "ct_warnings": clang_tidy["warnings"] if clang_tidy else None, + "date": timestamp, + "line_cov": cov_summary.get("line_pct") if cov_summary else None, + "func_cov": cov_summary.get("func_pct") if cov_summary else None, + "branch_cov": cov_summary.get("branch_pct") if cov_summary else None, + "ct_errors": clang_tidy["errors"] if clang_tidy else None, + "ct_warnings": clang_tidy["warnings"] if clang_tidy else None, + "codeql_errors": codeql["errors"] if codeql else None, + "codeql_warnings": codeql["warnings"] if codeql else None, + "codeql_recommendations": codeql["recommendations"] if codeql else None, + "codeql_total": codeql["total"] if codeql else None, }) if hist_path: save_history(hist_path, history) html_path.write_text( - render_dashboard(cov_summary, cov_files, clang_tidy, history, timestamp), + render_dashboard(cov_summary, cov_files, clang_tidy, codeql, history, timestamp), encoding="utf-8", ) @@ -293,10 +380,14 @@ def main() -> int: print(f" Clang-Tidy errors: {clang_tidy['errors']} warnings: {clang_tidy['warnings']}") else: print(" Clang-Tidy: N/A") + if codeql: + print(f" CodeQL errors: {codeql['errors']} warnings: {codeql['warnings']} recommendations: {codeql['recommendations']}") + else: + print(" CodeQL: N/A") if args.github_summary: write_github_summary( - cov_summary, clang_tidy, history, + cov_summary, clang_tidy, codeql, history, os.environ.get("GITHUB_STEP_SUMMARY", "/dev/null"), )