diff --git a/.github/workflows/_codeql.yml b/.github/workflows/_codeql.yml index 527365f9e..e5937b54f 100644 --- a/.github/workflows/_codeql.yml +++ b/.github/workflows/_codeql.yml @@ -53,6 +53,8 @@ jobs: if: inputs.fetch-only != 'true' id: run-codeql run: | + set -euo pipefail + bazel run //quality/static_analysis:codeql_lint -- \ --output-dir /tmp/codeql-results \ --output-prefix codeql-nightly \ @@ -60,6 +62,7 @@ jobs: - name: Upload SARIF to GitHub Code Scanning if: inputs.fetch-only != 'true' && always() && steps.run-codeql.outcome == 'success' + continue-on-error: true uses: github/codeql-action/upload-sarif@v4 with: sarif_file: /tmp/codeql-results/codeql-nightly.sarif @@ -72,3 +75,12 @@ jobs: name: codeql-csv-results path: /tmp/codeql-results/codeql-nightly.csv retention-days: 30 + + - name: Upload MISRA compliance reports + if: inputs.fetch-only != 'true' && always() + uses: actions/upload-artifact@v4 + with: + name: codeql-misra-reports + path: /tmp/codeql-results/analysis_reports/ + retention-days: 30 + if-no-files-found: warn diff --git a/.github/workflows/nightly_quality.yml b/.github/workflows/nightly_quality.yml index 0d399ea6e..3b786f3ff 100644 --- a/.github/workflows/nightly_quality.yml +++ b/.github/workflows/nightly_quality.yml @@ -27,7 +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 +# https://eclipse-score.github.io/communication/latest/quality/codeql/index.html ← CodeQL reports name: Nightly Quality Jobs @@ -164,10 +164,25 @@ jobs: 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 + # ------------------------------------------------------------------ + # Download CodeQL MISRA compliance reports artifact + # ------------------------------------------------------------------ + - name: Download CodeQL MISRA reports artifact + if: always() + continue-on-error: true + uses: actions/download-artifact@v4 + with: + name: codeql-misra-reports + path: /tmp/codeql_reports + + - name: Copy CodeQL MISRA reports into quality output + if: always() + run: | + mkdir -p "${GITHUB_WORKSPACE}/_quality/codeql" + find /tmp/codeql_reports -name '*.md' -type f -exec cp {} "${GITHUB_WORKSPACE}/_quality/codeql/" \; + # ------------------------------------------------------------------ # Generate coverage KPI dashboard via generate_dashboard py_binary # ------------------------------------------------------------------ @@ -183,9 +198,6 @@ jobs: --html "${GITHUB_WORKSPACE}/_quality/index.html" \ --github-summary - echo "Dashboard generated. Contents of _quality/:" - find "${GITHUB_WORKSPACE}/_quality" -type f | sort - # ------------------------------------------------------------------ # Upload all quality reports as a single artifact for docs.yml to # pick up and deploy as part of the unified Sphinx site. diff --git a/bazel/rules/generate_quality_links.bzl b/bazel/rules/generate_quality_links.bzl index 0213cc44e..46c7029be 100644 --- a/bazel/rules/generate_quality_links.bzl +++ b/bazel/rules/generate_quality_links.bzl @@ -60,8 +60,12 @@ def _generate_quality_links_impl(ctx): dashboard_ref = "`Quality Dashboard `__" clang_tidy_ref = "`Clang-Tidy report `__" codeql_ref = "`CodeQL findings `__" + codeql_integrity_ref = "`Database integrity `__" + codeql_deviations_ref = "`Deviations `__" + codeql_compliance_ref = "`Compliance summary `__" + codeql_recat_ref = "`Recategorizations `__" elif docs_version and docs_base_url: - # versioned release — quality reports generally live at latest/ + # versioned release — quality reports only live at latest/ latest = docs_base_url + "/latest" if release_coverage_asset_url: coverage_ref = ( @@ -77,6 +81,14 @@ def _generate_quality_links_impl(ctx): "/quality/clang_tidy_findings.txt>`__") codeql_ref = ("`CodeQL findings (latest) <" + latest + "/quality/codeql_findings.txt>`__") + codeql_integrity_ref = ("`Database integrity (latest) <" + latest + + "/quality/codeql/database_integrity_report.md>`__") + codeql_deviations_ref = ("`Deviations (latest) <" + latest + + "/quality/codeql/deviations_report.md>`__") + codeql_compliance_ref = ("`Compliance summary (latest) <" + latest + + "/quality/codeql/guideline_compliance_summary.md>`__") + codeql_recat_ref = ("`Recategorizations (latest) <" + latest + + "/quality/codeql/guideline_recategorizations_report.md>`__") else: # local build — no published reports; show the equivalent bazel command coverage_ref = ( @@ -94,12 +106,24 @@ def _generate_quality_links_impl(ctx): "*local build* — run " + "``bazel run //quality/static_analysis:codeql_lint``" ) + codeql_report_hint = ( + "*local build* — run " + + "``bazel run //quality/static_analysis:codeql_lint``" + ) + codeql_integrity_ref = codeql_report_hint + codeql_deviations_ref = codeql_report_hint + codeql_compliance_ref = codeql_report_hint + codeql_recat_ref = codeql_report_hint content = ( ":orphan:\n\n" + ".. |coverage_report_link| replace:: " + coverage_ref + "\n" + ".. |quality_dashboard_link| replace:: " + dashboard_ref + "\n" + ".. |clang_tidy_report_link| replace:: " + clang_tidy_ref + "\n" + - ".. |codeql_report_link| replace:: " + codeql_ref + "\n" + ".. |codeql_report_link| replace:: " + codeql_ref + "\n" + + ".. |codeql_integrity_report_link| replace:: " + codeql_integrity_ref + "\n" + + ".. |codeql_deviations_report_link| replace:: " + codeql_deviations_ref + "\n" + + ".. |codeql_compliance_report_link| replace:: " + codeql_compliance_ref + "\n" + + ".. |codeql_recat_report_link| replace:: " + codeql_recat_ref + "\n" ) output = ctx.actions.declare_file(ctx.label.name + ".rst") diff --git a/docs/sphinx/quality_reports.rst b/docs/sphinx/quality_reports.rst index e0152783c..0922561af 100644 --- a/docs/sphinx/quality_reports.rst +++ b/docs/sphinx/quality_reports.rst @@ -35,7 +35,11 @@ nightly run of the `Nightly Quality Jobs`_ workflow. - |clang_tidy_report_link| * - CodeQL - MISRA C++ compliance findings via CodeQL (codeql/misra-cpp-coding-standards) - - |codeql_report_link| + - | |codeql_report_link| + | |codeql_integrity_report_link| + | |codeql_deviations_report_link| + | |codeql_compliance_report_link| + | |codeql_recat_report_link| |quality_dashboard_link| diff --git a/quality/quality.md b/quality/quality.md index 57e7edba6..2d820abd7 100644 --- a/quality/quality.md +++ b/quality/quality.md @@ -104,6 +104,11 @@ This single command automatically: 2. Generates SARIF file (JSON with 274+ findings) 3. Generates 4 Markdown reports from SARIF + database +The markdown reports are written to `bazel-out/analysis_reports/` for local runs. +When `--output-dir` is used, they are written to `/analysis_reports/` instead. +In CI, the nightly workflow copies that directory into `/tmp/codeql-results/analysis_reports/` +before publishing the artifact. + #### Generated Reports The following markdown files are automatically created in `bazel-out/analysis_reports/`: diff --git a/quality/scripts/bundle_quality_reports.sh b/quality/scripts/bundle_quality_reports.sh index c6e83f301..5e9bb308a 100644 --- a/quality/scripts/bundle_quality_reports.sh +++ b/quality/scripts/bundle_quality_reports.sh @@ -29,8 +29,10 @@ EVENT_NAME="${1:?event_name required}" WORKFLOW_RUN_ID="${2:-}" REPOSITORY="${3:?repository required}" PUBLISH_DIR="${4:-publish/latest/quality}" +CODEQL_REPORTS_DIR="${PUBLISH_DIR}/codeql" mkdir -p "${PUBLISH_DIR}" +mkdir -p "${CODEQL_REPORTS_DIR}" if [[ "${EVENT_NAME}" == "workflow_run" ]]; then # Triggered by nightly — download the fresh artifact directly. diff --git a/quality/static_analysis/BUILD b/quality/static_analysis/BUILD index 229c73acc..cf40ab76a 100644 --- a/quality/static_analysis/BUILD +++ b/quality/static_analysis/BUILD @@ -27,6 +27,7 @@ py_binary( main = "codeql_lint.py", tags = ["local"], target_compatible_with = ["@platforms//os:linux"], + deps = ["@rules_python//python/runfiles"], ) filegroup( diff --git a/quality/static_analysis/codeql_lint.py b/quality/static_analysis/codeql_lint.py index b27e86636..f46b70d34 100644 --- a/quality/static_analysis/codeql_lint.py +++ b/quality/static_analysis/codeql_lint.py @@ -17,12 +17,35 @@ import subprocess import datetime import shutil -import time -import glob TMP_PATH_FOR_DATABASES = "/var/tmp/codeql_databases" + +def _find_pack_root(): + """Locate the MISRA C++ query pack root (cpp/common/src) via Bazel runfiles. + + The codeql_coding_standards repo's cpp/** sources are already declared as a + `data` dependency of @codeql_coding_standards//:analysis_report, which is in + turn a `data` dependency of this py_binary. That means Bazel places them in + our own runfiles tree, so we can resolve the pack root the same way + everywhere (locally and in CI) without any manual filesystem searching. + """ + from python.runfiles import Runfiles + + runfiles = Runfiles.Create() + anchor = runfiles.Rlocation("codeql_coding_standards/cpp/common/src/qlpack.yml") + if not anchor or not os.path.exists(anchor): + raise RuntimeError("Unable to locate CodeQL pack root (cpp/common/src)") + return os.path.dirname(anchor) + + +def _install_query_pack(code_ql_path): + """Resolve the MISRA C++ query pack's own dependencies (like `npm install`).""" + pack_root = _find_pack_root() + subprocess.run(f"{code_ql_path} pack install {pack_root}", shell=True, check=True) + + def create_database(code_ql_path, config_path, target, source_root, database_path): """Create the CodeQL database: init, build with tracing, finalize.""" subprocess.run( @@ -50,7 +73,15 @@ def create_database(code_ql_path, config_path, target, source_root, database_pat subprocess.run(f"{code_ql_path} database finalize -j=0 -- {database_path}", shell=True, check=True) -def analyze_database(code_ql_path, database_path, source_root, analysis_report_path=None, query_spec=None, output_prefix="codeql", output_dir=None): +def analyze_database( + code_ql_path, + database_path, + source_root, + analysis_report_path=None, + query_spec=None, + output_prefix="codeql", + output_dir=None, +): """Run CodeQL analysis and generate MISRA C++ compliance reports.""" output_base = output_dir or _get_bazel_info(source_root).get('output_path') os.makedirs(output_base, exist_ok=True) @@ -78,29 +109,21 @@ def analyze_database(code_ql_path, database_path, source_root, analysis_report_p try: # Make analysis_report executable and run it os.chmod(analysis_report_path, 0o755) - print(f" Using database: {database_path}") - print(f" Using SARIF: {sarif_path}") - print(f" Output directory: {output_base}") - # Prepare environment with CodeQL binary path + # Remove existing reports directory if it exists + reports_output_dir = os.path.join(output_base, "analysis_reports") + if os.path.exists(reports_output_dir): + shutil.rmtree(reports_output_dir) + + # Prepare environment with CodeQL binary path so analysis_report can find 'codeql' command env = os.environ.copy() - # codeql_cli is a symlink to the actual codeql binary - # Resolve the symlink to get the real directory - try: - real_codeql_path = os.path.realpath(code_ql_path) - codeql_bin_dir = os.path.dirname(real_codeql_path) - except: - codeql_bin_dir = os.path.dirname(os.path.abspath(code_ql_path)) - - # Prepend codeql directory to PATH + codeql_bin_dir = os.path.dirname(os.path.realpath(code_ql_path)) + print(f" Resolved CodeQL bin dir: {codeql_bin_dir}") + print(f" CodeQL bin dir exists: {os.path.isdir(codeql_bin_dir)}") env["PATH"] = f"{codeql_bin_dir}:{env.get('PATH', '')}" + print(f" PATH for analysis_report: {env['PATH']}") # analysis_report expects positional args: database-dir sarif-file output-dir - reports_output_dir = os.path.join(output_base, "analysis_reports") - - # Remove existing reports directory if it exists - if os.path.exists(reports_output_dir): - shutil.rmtree(reports_output_dir) result = subprocess.run( [analysis_report_path, @@ -109,25 +132,18 @@ def analyze_database(code_ql_path, database_path, source_root, analysis_report_p reports_output_dir], capture_output=True, text=True, env=env) - # Check if reports were generated even if there was an error - # (analysis_report writes files before failing on some post-processing) - if os.path.exists(reports_output_dir): - report_files = glob.glob(os.path.join(reports_output_dir, "*")) - if report_files: - print(f"✓ Reports generated successfully") - print(f"\n Generated Report Files:") - for f in sorted(report_files): - file_size = os.path.getsize(f) / 1024 # KB - print(f" ✓ {os.path.basename(f)} ({file_size:.1f} KB)") - else: - # If no files, raise the error - result.check_returncode() - else: - result.check_returncode() - except subprocess.CalledProcessError as e: - print(f"⚠️ Report generation warning: {e.stderr if e.stderr else e}") - else: - print(" Report generation skipped (analysis_report tool not available)") + # Always show subprocess output for diagnostics + if result.stdout: + print(f" [analysis_report stdout]: {result.stdout.strip()}") + + if result.stderr: + print(f" [analysis_report stderr]: {result.stderr.strip()}") + if result.returncode != 0: + print(f" analysis_report exited with code {result.returncode}") + # Don't raise exception - allow workflow to continue + + except Exception as e: + print(f"Report generation exception: {e}") def main(): @@ -150,6 +166,9 @@ def main(): # Make codeql_path absolute codeql_path = os.path.abspath(args.codeql_path) if args.codeql_path else None + if codeql_path: + _install_query_pack(codeql_path) + if args.phase == "create-database": os.makedirs(os.path.dirname(args.database_path), exist_ok=True) create_database(codeql_path, args.config_path, target, source_root, args.database_path) @@ -175,7 +194,6 @@ def main(): analysis_report_path=args.analysis_report_path, query_spec=args.query_spec, output_prefix=args.output_prefix, output_dir=args.output_dir) - print(f" Use this database for future report generation") def _get_action_env_extension(codeql_env):