diff --git a/.github/workflows/clang_tidy_analysis.yml b/.github/workflows/clang_tidy_analysis.yml new file mode 100644 index 000000000..1b0cfeb69 --- /dev/null +++ b/.github/workflows/clang_tidy_analysis.yml @@ -0,0 +1,76 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +name: Clang-Tidy Analysis + +on: + workflow_call: + outputs: + duration-seconds: + description: Runtime of the clang-tidy check in seconds. + value: ${{ jobs.clang_tidy.outputs.duration-seconds }} + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: clang_tidy_analysis-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + ANDROID_HOME: "" + ANDROID_SDK_ROOT: "" + +jobs: + clang_tidy: + runs-on: ubuntu-24.04 + outputs: + duration-seconds: ${{ steps.timer.outputs.duration-seconds }} + steps: + - name: Start timer + id: start_time + run: echo "start=$(date +%s)" >> "$GITHUB_OUTPUT" + + - name: Checkout repository + uses: actions/checkout@v6.0.2 + + - name: Free Disk Space (Ubuntu) + uses: eclipse-score/more-disk-space@v1 + with: + level: 4 + + - uses: castler/setup-bazel@8818d35864b4088fb3a12e7a3191777dc418fd69 + with: + bazelisk-cache: true + disk-cache: "clang_tidy_analysis" + disk-cache-key: "main" + repository-cache: true + cache-save: ${{ github.ref == 'refs/heads/main' }} + + - name: Allow linux-sandbox + uses: ./actions/unblock_user_namespace_for_linux_sandbox + + - name: Run clang-tidy analysis + run: | + bazel build //... --aspects //tools/lint:clang_tidy_aspect + + - name: End timer + if: ${{ always() }} + id: timer + run: | + end=$(date +%s) + start=${{ steps.start_time.outputs.start }} + duration=$((end - start)) + echo "duration-seconds=$duration" >> "$GITHUB_OUTPUT" + echo "clang-tidy duration: ${duration}s" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/codeql_analysis.yml b/.github/workflows/codeql_analysis.yml new file mode 100644 index 000000000..62d1f0b49 --- /dev/null +++ b/.github/workflows/codeql_analysis.yml @@ -0,0 +1,89 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +name: CodeQL Analysis + +on: + workflow_call: + outputs: + duration-seconds: + description: Runtime of the CodeQL check in seconds. + value: ${{ jobs.codeql.outputs.duration-seconds }} + workflow_dispatch: + +permissions: + actions: read + contents: read + security-events: write + +concurrency: + group: codeql_analysis-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + ANDROID_HOME: "" + ANDROID_SDK_ROOT: "" + +jobs: + codeql: + runs-on: ubuntu-24.04 + outputs: + duration-seconds: ${{ steps.timer.outputs.duration-seconds }} + steps: + - name: Start timer + id: start_time + run: echo "start=$(date +%s)" >> "$GITHUB_OUTPUT" + + - 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: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: c-cpp + build-mode: manual + + - uses: castler/setup-bazel@8818d35864b4088fb3a12e7a3191777dc418fd69 + with: + bazelisk-cache: true + disk-cache: "codeql_analysis" + disk-cache-key: "main" + repository-cache: true + cache-save: ${{ github.ref == 'refs/heads/main' }} + + - name: Allow linux-sandbox + uses: ./actions/unblock_user_namespace_for_linux_sandbox + + - name: Build for CodeQL extraction + run: | + bazel build //... + + - name: Analyze with CodeQL + uses: github/codeql-action/analyze@v3 + with: + category: /language:c-cpp + + - name: End timer + if: ${{ always() }} + id: timer + run: | + end=$(date +%s) + start=${{ steps.start_time.outputs.start }} + duration=$((end - start)) + echo "duration-seconds=$duration" >> "$GITHUB_OUTPUT" + echo "CodeQL duration: ${duration}s" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/coverity_analysis.yml b/.github/workflows/coverity_analysis.yml new file mode 100644 index 000000000..9675470ad --- /dev/null +++ b/.github/workflows/coverity_analysis.yml @@ -0,0 +1,78 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +name: Coverity Analysis + +on: + workflow_call: + outputs: + duration-seconds: + description: Runtime of the Coverity check in seconds. + value: ${{ jobs.coverity.outputs.duration-seconds }} + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: coverity_analysis-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + ANDROID_HOME: "" + ANDROID_SDK_ROOT: "" + +jobs: + coverity: + runs-on: ubuntu-24.04 + outputs: + duration-seconds: ${{ steps.timer.outputs.duration-seconds }} + steps: + - name: Start timer + id: start_time + run: echo "start=$(date +%s)" >> "$GITHUB_OUTPUT" + + - name: Checkout repository + uses: actions/checkout@v6.0.2 + + - name: Free Disk Space (Ubuntu) + uses: eclipse-score/more-disk-space@v1 + with: + level: 4 + + - uses: castler/setup-bazel@8818d35864b4088fb3a12e7a3191777dc418fd69 + with: + bazelisk-cache: true + disk-cache: "coverity_analysis" + disk-cache-key: "main" + repository-cache: true + cache-save: ${{ github.ref == 'refs/heads/main' }} + + - name: Allow linux-sandbox + uses: ./actions/unblock_user_namespace_for_linux_sandbox + + - name: Run Coverity scan + run: | + echo "Coverity integration placeholder" + echo "To enable: configure Coverity account and API token in secrets" + bazel build //... + + - name: End timer + if: ${{ always() }} + id: timer + run: | + end=$(date +%s) + start=${{ steps.start_time.outputs.start }} + duration=$((end - start)) + echo "duration-seconds=$duration" >> "$GITHUB_OUTPUT" + echo "Coverity duration: ${duration}s" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/hybrid_quality_demo.yml b/.github/workflows/hybrid_quality_demo.yml new file mode 100644 index 000000000..de5b75021 --- /dev/null +++ b/.github/workflows/hybrid_quality_demo.yml @@ -0,0 +1,130 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +name: Hybrid Quality Demo + +on: + workflow_dispatch: + inputs: + run_nightly_checks: + description: Run nightly quality checks in addition to fast PR checks. + required: false + type: boolean + default: true + schedule: + - cron: '0 2 * * *' + +permissions: + actions: read + contents: read + security-events: write + +concurrency: + group: hybrid_quality_demo-${{ github.ref }}-${{ github.event_name }} + cancel-in-progress: false + +jobs: + pr_checks: + name: Fast PR checks + uses: ./.github/workflows/build_and_test_host.yml + with: + run_all_configurations: false + + coverage: + name: Coverage report + if: ${{ github.event_name == 'schedule' || inputs.run_nightly_checks }} + needs: pr_checks + uses: ./.github/workflows/coverage_report.yml + + thread_sanitizer: + name: Thread sanitizer + if: ${{ github.event_name == 'schedule' || inputs.run_nightly_checks }} + needs: pr_checks + uses: ./.github/workflows/thread_sanitizer.yml + + address_sanitizer: + name: Address/UB/leak sanitizer + if: ${{ github.event_name == 'schedule' || inputs.run_nightly_checks }} + needs: pr_checks + uses: ./.github/workflows/address_undefined_behavior_leak_sanitizer.yml + + codeql: + name: CodeQL analysis + if: ${{ github.event_name == 'schedule' || inputs.run_nightly_checks }} + needs: pr_checks + uses: ./.github/workflows/codeql_analysis.yml + + clang_tidy: + name: Clang-Tidy analysis + if: ${{ github.event_name == 'schedule' || inputs.run_nightly_checks }} + needs: pr_checks + uses: ./.github/workflows/clang_tidy_analysis.yml + + coverity: + name: Coverity analysis + if: ${{ github.event_name == 'schedule' || inputs.run_nightly_checks }} + needs: pr_checks + uses: ./.github/workflows/coverity_analysis.yml + + dashboard: + name: Generate quality dashboard with timing + if: ${{ always() }} + needs: + - pr_checks + - coverage + - thread_sanitizer + - address_sanitizer + - codeql + - clang_tidy + - coverity + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v6.0.2 + + - name: Generate dashboard files with timing + env: + PR_CHECKS_RESULT: ${{ needs.pr_checks.result }} + COVERAGE_RESULT: ${{ needs.coverage.result || 'skipped' }} + THREAD_SANITIZER_RESULT: ${{ needs.thread_sanitizer.result || 'skipped' }} + ADDRESS_SANITIZER_RESULT: ${{ needs.address_sanitizer.result || 'skipped' }} + CODEQL_RESULT: ${{ needs.codeql.result || 'skipped' }} + CLANG_TIDY_RESULT: ${{ needs.clang_tidy.result || 'skipped' }} + COVERITY_RESULT: ${{ needs.coverity.result || 'skipped' }} + CODEQL_DURATION_SECONDS: ${{ needs.codeql.outputs.duration-seconds || '' }} + CLANG_TIDY_DURATION_SECONDS: ${{ needs.clang_tidy.outputs.duration-seconds || '' }} + COVERITY_DURATION_SECONDS: ${{ needs.coverity.outputs.duration-seconds || '' }} + COVERAGE_ARTIFACT_NAME: ${{ needs.coverage.outputs.artifact-name }} + REPOSITORY_NAME: ${{ github.repository }} + RUN_ID: ${{ github.run_id }} + REF_NAME: ${{ github.ref_name }} + EVENT_NAME: ${{ github.event_name }} + run: | + python3 tools/ci/generate_hybrid_quality_dashboard.py dashboard + + - name: Publish workflow summary + run: cat dashboard/summary.md >> "$GITHUB_STEP_SUMMARY" + + - name: Show timing report + if: ${{ always() }} + run: | + echo "## Quality Check Timing Report" >> "$GITHUB_STEP_SUMMARY" + cat dashboard/timing.txt >> "$GITHUB_STEP_SUMMARY" 2>/dev/null || echo "Timing data generated." >> "$GITHUB_STEP_SUMMARY" + + - name: Upload dashboard artifact + uses: actions/upload-artifact@v4 + with: + name: hybrid_quality_dashboard_${{ github.run_id }} + path: dashboard/ diff --git a/tools/ci/generate_hybrid_quality_dashboard.py b/tools/ci/generate_hybrid_quality_dashboard.py new file mode 100644 index 000000000..e5c9c1e27 --- /dev/null +++ b/tools/ci/generate_hybrid_quality_dashboard.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 + +import html +import os +import pathlib +import sys + +STATUS_LABELS = { + "success": "Passed", + "failure": "Failed", + "cancelled": "Cancelled", + "skipped": "Skipped", +} + +STATUS_COLORS = { + "success": "#1a7f37", + "failure": "#cf222e", + "cancelled": "#9a6700", + "skipped": "#6e7781", +} + + +def normalize_status(value: str) -> str: + if value in STATUS_LABELS: + return value + return "skipped" + + +def format_duration(duration_value: str) -> str: + if not duration_value: + return "-" + try: + seconds = int(duration_value) + except ValueError: + return "-" + minutes = seconds // 60 + remaining_seconds = seconds % 60 + return f"{minutes}m {remaining_seconds}s ({seconds}s)" + + +def render_markdown_row(name: str, status: str, duration: str, notes: str) -> str: + return f"| {name} | {STATUS_LABELS[status]} | {duration} | {notes} |" + + +def render_html_row(name: str, status: str, duration: str, notes: str) -> str: + safe_name = html.escape(name) + safe_notes = html.escape(notes) + safe_duration = html.escape(duration) + label = html.escape(STATUS_LABELS[status]) + color = STATUS_COLORS[status] + return ( + "" + f"{safe_name}" + f"{label}" + f"{safe_duration}" + f"{safe_notes}" + "" + ) + + +def main() -> int: + output_dir = pathlib.Path(sys.argv[1]) if len(sys.argv) > 1 else pathlib.Path("dashboard") + output_dir.mkdir(parents=True, exist_ok=True) + + run_id = os.environ.get("RUN_ID", "unknown") + repository_name = os.environ.get("REPOSITORY_NAME", "unknown") + ref_name = os.environ.get("REF_NAME", "unknown") + event_name = os.environ.get("EVENT_NAME", "unknown") + coverage_artifact_name = os.environ.get("COVERAGE_ARTIFACT_NAME", "") + + checks = [ + ( + "Fast PR checks", + normalize_status(os.environ.get("PR_CHECKS_RESULT", "skipped")), + "-", + "Build and unit tests on the default host configuration.", + ), + ( + "CodeQL analysis", + normalize_status(os.environ.get("CODEQL_RESULT", "skipped")), + format_duration(os.environ.get("CODEQL_DURATION_SECONDS", "")), + "Nightly static security analysis.", + ), + ( + "Clang-Tidy analysis", + normalize_status(os.environ.get("CLANG_TIDY_RESULT", "skipped")), + format_duration(os.environ.get("CLANG_TIDY_DURATION_SECONDS", "")), + "Clang static code analysis and linting.", + ), + ( + "Coverity analysis", + normalize_status(os.environ.get("COVERITY_RESULT", "skipped")), + format_duration(os.environ.get("COVERITY_DURATION_SECONDS", "")), + "Coverity static code analysis.", + ), + ( + "Coverage report", + normalize_status(os.environ.get("COVERAGE_RESULT", "skipped")), + "-", + "Nightly-style coverage generation." + + (f" Artifact: {coverage_artifact_name}." if coverage_artifact_name else ""), + ), + ( + "Thread sanitizer", + normalize_status(os.environ.get("THREAD_SANITIZER_RESULT", "skipped")), + "-", + "Nightly thread sanitizer run.", + ), + ( + "Address/UB/leak sanitizer", + normalize_status(os.environ.get("ADDRESS_SANITIZER_RESULT", "skipped")), + "-", + "Nightly address, undefined behavior, and leak sanitizer run.", + ), + ] + + markdown_lines = [ + "## Hybrid Quality Demo", + "", + f"- Repository: `{repository_name}`", + f"- Ref: `{ref_name}`", + f"- Event: `{event_name}`", + f"- Run: `{run_id}`", + "", + "| Check | Status | Runtime | Notes |", + "| --- | --- | --- | --- |", + ] + markdown_lines.extend( + render_markdown_row(name, status, duration, notes) for name, status, duration, notes in checks + ) + markdown_lines.extend( + [ + "", + "This demo shows the hybrid model: fast PR checks run first, and heavier quality checks (CodeQL, clang-tidy, Coverity, coverage, and sanitizers) run separately for nightly-style visibility.", + ] + ) + (output_dir / "summary.md").write_text("\n".join(markdown_lines) + "\n", encoding="utf-8") + + timing_lines = [ + "# Quality Check Timing Report", + "", + "## Measured Runtime", + "", + "| Check | Status | Runtime |", + "| --- | --- | --- |", + ] + for name, status, duration, _ in checks: + timing_lines.append(f"| {name} | {STATUS_LABELS[status]} | {duration} |") + (output_dir / "timing.txt").write_text("\n".join(timing_lines) + "\n", encoding="utf-8") + + html_rows = "\n".join( + render_html_row(name, status, duration, notes) for name, status, duration, notes in checks + ) + html_document = f""" + + + + + Hybrid Quality Demo with Timing + + + +
+
+

Hybrid Quality Demo with Timing

+

This dashboard shows measured runtime for quality checks.

+
+ + + {html_rows} +
CheckStatusRuntimeNotes
+
+
+
+ + +""" + (output_dir / "index.html").write_text(html_document, encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())