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
12 changes: 12 additions & 0 deletions .github/workflows/_codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,16 @@ 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 \
--target //score/message_passing //score/mw/com

- 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
Expand All @@ -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
24 changes: 18 additions & 6 deletions .github/workflows/nightly_quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
# ------------------------------------------------------------------
Expand All @@ -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.
Expand Down
28 changes: 26 additions & 2 deletions bazel/rules/generate_quality_links.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,12 @@ def _generate_quality_links_impl(ctx):
dashboard_ref = "`Quality Dashboard <quality/index.html>`__"
clang_tidy_ref = "`Clang-Tidy report <quality/clang_tidy_findings.txt>`__"
codeql_ref = "`CodeQL findings <quality/codeql_findings.txt>`__"
codeql_integrity_ref = "`Database integrity <quality/codeql/database_integrity_report.md>`__"
codeql_deviations_ref = "`Deviations <quality/codeql/deviations_report.md>`__"
codeql_compliance_ref = "`Compliance summary <quality/codeql/guideline_compliance_summary.md>`__"
codeql_recat_ref = "`Recategorizations <quality/codeql/guideline_recategorizations_report.md>`__"
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 = (
Expand All @@ -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 = (
Expand All @@ -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")
Expand Down
6 changes: 5 additions & 1 deletion docs/sphinx/quality_reports.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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|

Expand Down
5 changes: 5 additions & 0 deletions quality/quality.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<output-dir>/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/`:
Expand Down
2 changes: 2 additions & 0 deletions quality/scripts/bundle_quality_reports.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions quality/static_analysis/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ py_binary(
main = "codeql_lint.py",
tags = ["local"],
target_compatible_with = ["@platforms//os:linux"],
deps = ["@rules_python//python/runfiles"],
)

filegroup(
Expand Down
100 changes: 59 additions & 41 deletions quality/static_analysis/codeql_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Comment on lines -101 to -103

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we removing this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removes the old reports before creating new ones. I first thought the new reports would replace the old ones, but they don't always do that. Some old files can still stay there and get included. Keeping this cleanup makes sure only the new reports are used.


result = subprocess.run(
[analysis_report_path,
Expand All @@ -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():
Expand All @@ -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)
Expand All @@ -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):
Expand Down
Loading