diff --git a/.github/workflows/address_undefined_behavior_leak_sanitizer.yml b/.github/workflows/address_undefined_behavior_leak_sanitizer.yml index bc8d717f7..b71d8a7fb 100644 --- a/.github/workflows/address_undefined_behavior_leak_sanitizer.yml +++ b/.github/workflows/address_undefined_behavior_leak_sanitizer.yml @@ -30,6 +30,7 @@ concurrency: env: ANDROID_HOME: "" ANDROID_SDK_ROOT: "" + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: build_and_test_asan_ubsan_lsan: runs-on: ubuntu-24.04 diff --git a/.github/workflows/automated_release.yml b/.github/workflows/automated_release.yml index 51302ed28..2d87afa46 100644 --- a/.github/workflows/automated_release.yml +++ b/.github/workflows/automated_release.yml @@ -27,6 +27,7 @@ on: env: ANDROID_HOME: "" ANDROID_SDK_ROOT: "" + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: create-draft-release: diff --git a/.github/workflows/build_and_test_host.yml b/.github/workflows/build_and_test_host.yml index f948d0363..c7caaaae1 100644 --- a/.github/workflows/build_and_test_host.yml +++ b/.github/workflows/build_and_test_host.yml @@ -35,6 +35,7 @@ concurrency: env: ANDROID_HOME: "" ANDROID_SDK_ROOT: "" + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: prepare_build_and_test_host_matrix: runs-on: ubuntu-24.04 diff --git a/.github/workflows/build_and_test_qnx.yml b/.github/workflows/build_and_test_qnx.yml index 39845a0b5..77933c524 100644 --- a/.github/workflows/build_and_test_qnx.yml +++ b/.github/workflows/build_and_test_qnx.yml @@ -39,6 +39,7 @@ env: LICENSE_DIR: "/opt/score_qnx/license" ANDROID_HOME: "" ANDROID_SDK_ROOT: "" + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: precheck: runs-on: ubuntu-24.04 diff --git a/.github/workflows/clang_tidy.yml b/.github/workflows/clang_tidy.yml new file mode 100644 index 000000000..e67ce6b5d --- /dev/null +++ b/.github/workflows/clang_tidy.yml @@ -0,0 +1,179 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# Reusable workflow: run clang-tidy via Bazel and upload the report as an artifact. +# +# Called by nightly_quality.yml. Uses the clang-tidy Bazel config defined in +# quality/static_analysis/static_analysis.bazelrc: +# bazel test --config=clang-tidy //... + +name: Clang-Tidy + +on: + workflow_call: + outputs: + artifact-name: + description: "Name of the clang-tidy report artifact" + value: ${{ jobs.clang-tidy.outputs.artifact-name }} + conclusion: + description: "Job conclusion: success or failure" + value: ${{ jobs.clang-tidy.outputs.conclusion }} + +permissions: + contents: read + +env: + ANDROID_HOME: "" + ANDROID_SDK_ROOT: "" + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + clang-tidy: + runs-on: ubuntu-24.04 + outputs: + artifact-name: ${{ steps.set-artifact-name.outputs.artifact-name }} + conclusion: ${{ steps.set-conclusion.outputs.conclusion }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6.0.2 + + - name: Free Disk Space (Ubuntu) + uses: eclipse-score/more-disk-space@v1 + with: + level: 4 + + - name: Setup Bazel with shared caching + uses: bazel-contrib/setup-bazel@0.18.0 + with: + bazelisk-cache: true + disk-cache: "clang_tidy" + repository-cache: true + cache-save: ${{ github.event_name == 'schedule' }} + + - name: Allow linux-sandbox + uses: ./actions/unblock_user_namespace_for_linux_sandbox + + - name: Run clang-tidy via Bazel + id: run-clang-tidy + # Continue on error so we can collect and upload the report even on findings + continue-on-error: true + run: | + bazel test --config=clang-tidy //... \ + 2>&1 | tee clang_tidy_raw.log + + - name: Collect clang-tidy reports + run: | + mkdir -p clang_tidy_report + + # aspect_rules_lint v2 writes {label}.AspectRulesLintClangTidy.out per target. + # -L is required because bazel-bin is a symlink. + find -L bazel-bin -name "*.AspectRulesLintClangTidy.out" 2>/dev/null \ + | xargs -I{} cp {} clang_tidy_report/ 2>/dev/null || true + + # Also save the raw log + cp clang_tidy_raw.log clang_tidy_report/ + + # Count findings from the .out files (verbose=False means findings are only + # in the per-file .out files, not in the bazel terminal output) + FINDINGS=$(cat clang_tidy_report/*.AspectRulesLintClangTidy.out 2>/dev/null \ + | grep -c "warning:\|error:" || true) + echo "Total clang-tidy findings: ${FINDINGS}" + echo "findings=${FINDINGS}" >> $GITHUB_OUTPUT + + # Generate a simple HTML summary + python3 - << 'PYEOF' + import re, pathlib, html, datetime, glob + + # Read all .out files (findings are per source file, not in bazel terminal output) + findings = [] + for out_file in glob.glob("clang_tidy_report/*.AspectRulesLintClangTidy.out"): + log = pathlib.Path(out_file).read_text(errors="replace") + for m in re.finditer( + r"^(.*?):(\d+):\d+:\s+(warning|error):\s+(.+?)(?:\s+\[.+?\])?$", + log, re.MULTILINE): + findings.append(m.groups()) + + rows = "" + for path, line, severity, msg in findings: + sev_cls = "error" if severity == "error" else "warning" + rows += ( + f"
| Location | Severity | Message |
|---|---|---|
| No findings. | ||
| Location | Severity | Rule | Message |
|---|---|---|---|
| No SARIF findings. See codeql_raw.log for details. | |||
Redirecting to latest documentation...
+ + + REDIRECT + + # Prevent Jekyll from processing the site + touch _deploy/.nojekyll + + # Generate versions.json for the pydata-sphinx-theme version switcher + python3 docs/sphinx/utils/update_versions_json.py \ + --owner "${{ github.repository_owner }}" \ + --repo "${{ github.event.repository.name }}" \ + --base-url "${BASE_URL}" \ + --output _deploy/versions.json + + # ------------------------------------------------------------------ + # Bundle quality reports into the deploy so docs and quality reports + # are published together in one gh-pages push. + # - Triggered by workflow_run (nightly): download the fresh artifact. + # - All other triggers: restore the existing reports from gh-pages so + # they are preserved and not wiped by this deploy. + # ------------------------------------------------------------------ + QUALITY_DEST="_deploy/${DEST_DIR}/quality" + mkdir -p "${QUALITY_DEST}" + + if [[ "${{ github.event_name }}" == "workflow_run" ]]; then + echo "Triggered by nightly quality run — quality artifact will be downloaded in the next step." + else + echo "Restoring existing quality reports from gh-pages..." + git fetch origin gh-pages 2>/dev/null || true + git show origin/gh-pages:latest/quality 2>/dev/null \ + && git checkout origin/gh-pages -- latest/quality 2>/dev/null \ + && cp -r latest/quality/. "${QUALITY_DEST}/" \ + || echo "No existing quality reports found — skipping restore." + fi + + # ------------------------------------------------------------------ + # Download quality reports artifact (only when triggered by nightly) + # ------------------------------------------------------------------ + - name: Download quality reports from nightly run + if: github.event_name == 'workflow_run' + uses: actions/download-artifact@v4 + with: + name: nightly-quality-reports + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + path: _deploy/${{ steps.vars.outputs.dest_dir }}/quality + + # ------------------------------------------------------------------ + # Deploy to gh-pages branch (keep_files preserves other versions) + # ------------------------------------------------------------------ + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./_deploy + keep_files: true diff --git a/.github/workflows/nightly_quality.yml b/.github/workflows/nightly_quality.yml new file mode 100644 index 000000000..12f4a9a8a --- /dev/null +++ b/.github/workflows/nightly_quality.yml @@ -0,0 +1,185 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# Workflow: Run nightly quality jobs and publish results to GitHub Pages. +# +# Runs every night at midnight UTC. Three quality jobs run in parallel: +# - Coverage : full C++ test suite with gcov/lcov → HTML report +# - CodeQL : static security/MISRA analysis → HTML report +# - Clang-Tidy : C++ static analysis → HTML report +# +# After all three complete (pass or fail), a final deploy-quality-reports job: +# 1. Downloads all three HTML artifacts +# 2. Runs generate_dashboard.py to produce the dashboard index page +# 3. Deploys everything to https://eclipse-score.github.io/communication/latest/quality/index.html on the gh-pages branch +# +# Deployed URL structure: +# 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/codeql/index.html ← CodeQL HTML +# https://eclipse-score.github.io/communication/latest/quality/clang_tidy/index.html ← Clang-Tidy HTML + +name: Nightly Quality Jobs + +on: + schedule: + - cron: '0 0 * * *' # every day at midnight UTC + workflow_dispatch: + +permissions: + contents: write + +env: + ANDROID_HOME: "" + ANDROID_SDK_ROOT: "" + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + # -------------------------------------------------------------------- + # Quality job 1: Coverage + # -------------------------------------------------------------------- + run-coverage: + uses: ./.github/workflows/coverage_report.yml + permissions: + contents: read + + # -------------------------------------------------------------------- + # Quality job 2: CodeQL + # -------------------------------------------------------------------- + run-codeql: + uses: ./.github/workflows/codeql.yml + permissions: + contents: read + security-events: write + + # -------------------------------------------------------------------- + # Quality job 3: Clang-Tidy + # -------------------------------------------------------------------- + run-clang-tidy: + uses: ./.github/workflows/clang_tidy.yml + permissions: + contents: read + + # -------------------------------------------------------------------- + # Collect all results, build the dashboard, deploy to GitHub Pages + # -------------------------------------------------------------------- + deploy-quality-reports: + needs: [run-coverage, run-codeql, run-clang-tidy] + # Always run even if individual quality jobs fail, so the dashboard + # still reflects which jobs passed and which failed. + if: always() + runs-on: ubuntu-24.04 + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v6.0.2 + + # ------------------------------------------------------------------ + # 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 + echo "codeql=$(map_conclusion '${{ needs.run-codeql.result }}')" >> $GITHUB_OUTPUT + echo "clang_tidy=$(map_conclusion '${{ needs.run-clang-tidy.result }}')" >> $GITHUB_OUTPUT + + # ------------------------------------------------------------------ + # Download artifacts (continue-on-error: artifacts may not exist if job failed) + # ------------------------------------------------------------------ + - name: Download coverage artifact + if: needs.run-coverage.result == 'success' + continue-on-error: true + uses: actions/download-artifact@v4 + with: + name: ${{ needs.run-coverage.outputs.artifact-name }} + path: /tmp/coverage_zip + + - name: Extract coverage HTML report + if: needs.run-coverage.result == 'success' + continue-on-error: true + run: | + cd /tmp/coverage_zip + unzip *.zip -d extracted + mkdir -p ${GITHUB_WORKSPACE}/_quality/coverage + cp -r /tmp/coverage_zip/extracted/artifacts/cpp_coverage/. \ + ${GITHUB_WORKSPACE}/_quality/coverage/ + + - name: Download CodeQL artifact + if: needs.run-codeql.result == 'success' + continue-on-error: true + uses: actions/download-artifact@v4 + with: + name: ${{ needs.run-codeql.outputs.artifact-name }} + path: _quality/codeql + + - name: Download Clang-Tidy artifact + if: needs.run-clang-tidy.result == 'success' + continue-on-error: true + uses: actions/download-artifact@v4 + with: + name: ${{ needs.run-clang-tidy.outputs.artifact-name }} + path: _quality/clang_tidy + + # ------------------------------------------------------------------ + # Generate dashboard index page via generate_dashboard.py + # ------------------------------------------------------------------ + + # Install Jinja2 (already in requirements_lock.txt; just ensure it is + # available on the runner before running the dashboard script). + - name: Install dashboard dependencies + if: always() && steps.conclusions.outcome == 'success' + run: pip install jinja2 markupsafe + + - name: Generate quality dashboard + if: always() && steps.conclusions.outcome == 'success' + run: | + # The LCOV .dat file is inside the extracted coverage zip at a known + # path; pass it unconditionally — generate_dashboard.py handles the + # case where the file is absent (returns empty coverage data). + python3 quality/dashboard/generate_dashboard.py \ + --sarif-dir _quality/codeql \ + --clang-tidy-dir _quality/clang_tidy \ + --lcov /tmp/coverage_zip/extracted/artifacts/coverage_report.dat \ + --html _quality/index.html \ + --github-summary + + echo "Dashboard generated. Contents of _quality/:" + find _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. + # ------------------------------------------------------------------ + - name: Upload quality reports artifact + if: always() && steps.conclusions.outcome == 'success' + uses: actions/upload-artifact@v4 + with: + name: nightly-quality-reports + path: _quality/ + retention-days: 7 + + - name: Print dashboard URL + run: | + OWNER="${{ github.repository_owner }}" + REPO="${{ github.event.repository.name }}" + echo "::notice::Quality reports will be published at https://${OWNER}.github.io/${REPO}/latest/quality/index.html once docs.yml completes." diff --git a/.github/workflows/thread_sanitizer.yml b/.github/workflows/thread_sanitizer.yml index 52f70e843..bfd10ae0a 100644 --- a/.github/workflows/thread_sanitizer.yml +++ b/.github/workflows/thread_sanitizer.yml @@ -29,6 +29,7 @@ concurrency: env: ANDROID_HOME: "" ANDROID_SDK_ROOT: "" + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: build_and_test_tsan: runs-on: ubuntu-24.04 diff --git a/docs/sphinx/BUILD b/docs/sphinx/BUILD index b0267a098..c3264f4af 100644 --- a/docs/sphinx/BUILD +++ b/docs/sphinx/BUILD @@ -56,6 +56,7 @@ sphinx_module( "index.rst", "introduction.rst", "message_passing.rst", + "quality_reports.rst", "safety_reports.rst", ":doxygen_xml", ":generate_api_rst", diff --git a/docs/sphinx/conf.py b/docs/sphinx/conf.py index 0808c1bbc..e952a165f 100644 --- a/docs/sphinx/conf.py +++ b/docs/sphinx/conf.py @@ -20,7 +20,6 @@ import os import sys import warnings -from pathlib import Path # -- Project information -- project = 'Eclipse S-CORE' @@ -28,11 +27,10 @@ author = 'Eclipse Foundation' release = '1.0.0' -# GitHub Pages base URL (update org/repo as needed) -GITHUB_PAGES_URL = os.environ.get( - 'DOCS_BASE_URL', - 'https://eclipse-score.github.io/communication' -) +# Version and base URL injected by the docs.yml GitHub Actions workflow. +# Falls back to sensible defaults for local builds. +docs_version = os.environ.get('DOCS_VERSION', 'latest') +docs_base_url = os.environ.get('DOCS_BASE_URL', '').rstrip('/') # -- General configuration --- extensions = [ @@ -75,6 +73,10 @@ # -- Options for HTML output -- html_theme = 'pydata_sphinx_theme' +# Canonical base URL for this version (helps search engines and the switcher) +if docs_base_url: + html_baseurl = f"{docs_base_url}/{docs_version}/" + # Professional theme configuration inspired by modern open-source projects html_theme_options = { # Navigation settings @@ -89,6 +91,12 @@ 'navbar_center': ['navbar-nav'], 'navbar_end': ['version-switcher', 'navbar-icon-links', 'theme-switcher'], + # Version switcher — reads versions.json from the GitHub Pages root + 'switcher': { + 'json_url': f"{docs_base_url}/versions.json" if docs_base_url else '/versions.json', + 'version_match': docs_version, + }, + # Search configuration 'search_bar_text': 'Search documentation...', @@ -113,11 +121,6 @@ } ], - # Version switcher configuration - 'switcher': { - 'json_url': f'{GITHUB_PAGES_URL}/switcher.json', - 'version_match': os.environ.get('DOCS_VERSION', 'latest'), - }, } # Add custom styling diff --git a/docs/sphinx/index.rst b/docs/sphinx/index.rst index 1d6e7ba66..8efdafaae 100644 --- a/docs/sphinx/index.rst +++ b/docs/sphinx/index.rst @@ -27,6 +27,12 @@ including the LoLa (Low Latency) implementation and Message Passing library. safety_reports +.. toctree:: + :maxdepth: 1 + :caption: Quality Reports: + + quality_reports + .. Note: safety_reports.rst contains links to pre-built HTML reports from external targets. About This Documentation diff --git a/docs/sphinx/quality_reports.rst b/docs/sphinx/quality_reports.rst new file mode 100644 index 000000000..8c772f706 --- /dev/null +++ b/docs/sphinx/quality_reports.rst @@ -0,0 +1,26 @@ +Quality Reports +=============== + +Nightly quality job results are built and published to GitHub Pages after each +nightly run of the `Nightly Quality Jobs`_ workflow. + +.. list-table:: + :widths: 20 45 35 + :header-rows: 1 + + * - Job + - Description + - Report + * - Coverage + - Line and branch coverage from C++ unit tests (gcov/lcov) + - `Coverage report| Severity ↕ | Rule ↕ | Message ↕ | Location ↕ |
|---|---|---|---|
| {{ r.severity|upper }} | +{{ r.name }} | +{{ r.message }} | +{{ r.path }}{% if r.start_line %}:{{ r.start_line }}{% endif %} | +
No CodeQL findings — clean code!
+ {% endif %} +| Severity ↕ | Check ↕ | Message ↕ | Location ↕ |
|---|---|---|---|
| {{ r.severity|upper }} | +{{ r.check }} | +{{ r.message }} | +{{ r.path }}{% if r.line %}:{{ r.line }}{% endif %} | +
No Clang-Tidy warnings — clean code!
+ {% endif %} +| File | Lines ↕ | 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 }} | +
No coverage data.
Run bazel coverage //:calculator_test
{{ history|length }} run(s) recorded. Trends appear after 2+ runs.
+ {% else %} +| Date | CodeQL | Clang-Tidy | Line Cov | Func Cov | Branch Cov |
|---|---|---|---|---|---|
| {{ snap.date }}{% if is_latest %} now{% endif %} | +{{ snap.codeql if snap.codeql is not none else 'N/A' }}{% if ps and snap.codeql is not none and ps.codeql is not none %} {{ delta(snap.codeql, ps.codeql, false) }}{% endif %} | +{{ snap.clang_tidy if snap.clang_tidy is not none else 'N/A' }}{% if ps and snap.clang_tidy is not none and ps.clang_tidy is not none %} {{ delta(snap.clang_tidy, ps.clang_tidy, false) }}{% endif %} | +{% if snap.line_cov is not none %}{{ '%.1f'|format(snap.line_cov) }}%{% if ps and ps.line_cov is not none %} {{ delta(snap.line_cov, ps.line_cov, true) }}{% endif %}{% else %}N/A{% endif %} | +{% if snap.func_cov is not none %}{{ '%.1f'|format(snap.func_cov) }}%{% if ps and ps.func_cov is not none %} {{ delta(snap.func_cov, ps.func_cov, true) }}{% endif %}{% else %}N/A{% endif %} | +{% if snap.branch_cov is not none %}{{ '%.1f'|format(snap.branch_cov) }}%{% if ps and ps.branch_cov is not none %} {{ delta(snap.branch_cov, ps.branch_cov, true) }}{% endif %}{% else %}N/A{% endif %} | +