From c69720518a1ba2597dc6f7d4ad041a1669a56553 Mon Sep 17 00:00:00 2001 From: komalmahale Date: Fri, 15 May 2026 11:51:46 +0200 Subject: [PATCH] ci: add hybrid quality workflows and main-push cache seeding --- .github/workflows/clang_tidy_analysis.yml | 89 +++++++++ .github/workflows/coverage_report.yml | 35 +++- .github/workflows/hybrid_quality_demo.yml | 118 ++++++++++++ tools/ci/generate_hybrid_quality_dashboard.py | 180 ++++++++++++++++++ 4 files changed, 420 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/clang_tidy_analysis.yml create mode 100644 .github/workflows/hybrid_quality_demo.yml create mode 100644 tools/ci/generate_hybrid_quality_dashboard.py diff --git a/.github/workflows/clang_tidy_analysis.yml b/.github/workflows/clang_tidy_analysis.yml new file mode 100644 index 000000000..247e742db --- /dev/null +++ b/.github/workflows/clang_tidy_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: 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: Configure QNX credentials + if: ${{ secrets.SCORE_QNX_USER != '' && secrets.SCORE_QNX_PASSWORD != '' }} + env: + SCORE_QNX_USER: ${{ secrets.SCORE_QNX_USER }} + SCORE_QNX_PASSWORD: ${{ secrets.SCORE_QNX_PASSWORD }} + run: | + umask 077 + { + echo "machine qnx.com" + echo " login ${SCORE_QNX_USER}" + echo " password ${SCORE_QNX_PASSWORD}" + } > "$HOME/.netrc" + + - name: Allow linux-sandbox + uses: ./actions/unblock_user_namespace_for_linux_sandbox + + - name: Run clang-tidy analysis + run: | + bazel --credential_helper= build //... --aspects=//:tools/lint/linters.bzl%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/coverage_report.yml b/.github/workflows/coverage_report.yml index 9d36e5cfd..f25216a59 100644 --- a/.github/workflows/coverage_report.yml +++ b/.github/workflows/coverage_report.yml @@ -18,6 +18,9 @@ on: artifact-name: description: 'Name of the coverage report artifact' value: ${{ jobs.coverage_report.outputs.artifact-name }} + duration-seconds: + description: Runtime of the coverage check in seconds. + value: ${{ jobs.coverage_report.outputs.duration-seconds }} permissions: contents: read @@ -33,8 +36,13 @@ jobs: runs-on: ubuntu-24.04 outputs: artifact-name: ${{ steps.set-artifact-name.outputs.artifact-name }} + 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 @@ -54,14 +62,27 @@ jobs: bazelisk-cache: true disk-cache: "coverage_report" repository-cache: true - cache-save: ${{ github.event_name == 'merge_group' }} + cache-save: ${{ github.ref == 'refs/heads/main' }} + + - name: Configure QNX credentials + if: ${{ secrets.SCORE_QNX_USER != '' && secrets.SCORE_QNX_PASSWORD != '' }} + env: + SCORE_QNX_USER: ${{ secrets.SCORE_QNX_USER }} + SCORE_QNX_PASSWORD: ${{ secrets.SCORE_QNX_PASSWORD }} + run: | + umask 077 + { + echo "machine qnx.com" + echo " login ${SCORE_QNX_USER}" + echo " password ${SCORE_QNX_PASSWORD}" + } > "$HOME/.netrc" - name: Allow linux-sandbox uses: ./actions/unblock_user_namespace_for_linux_sandbox - name: Run Unit Test with Coverage for C++ run: | - bazel coverage //... --build_tests_only + bazel --credential_helper= coverage //... --build_tests_only - name: Generate HTML Coverage Report # FIXME: "--ignore-errors category,inconsistent" is a workaround to cope with gcov messing up hit counts because of internal data races @@ -93,4 +114,14 @@ jobs: name: ${{ steps.set-artifact-name.outputs.artifact-name }} path: ${{ github.event.repository.name }}_coverage_report_${{ github.sha }}.zip + - 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 "Coverage 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..bc86ab4c2 --- /dev/null +++ b/.github/workflows/hybrid_quality_demo.yml @@ -0,0 +1,118 @@ +# ******************************************************************************* +# 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: + push: + branches: [main] + pull_request: + types: [opened, reopened, synchronize] + 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: write + contents: read + +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 == 'push' || github.event_name == 'pull_request' || github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.run_nightly_checks == 'true') }} + needs: pr_checks + uses: ./.github/workflows/coverage_report.yml + secrets: inherit + + thread_sanitizer: + name: Thread sanitizer + if: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.run_nightly_checks == 'true') }} + needs: pr_checks + uses: ./.github/workflows/thread_sanitizer.yml + + address_sanitizer: + name: Address/UB/leak sanitizer + if: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.run_nightly_checks == 'true') }} + needs: pr_checks + uses: ./.github/workflows/address_undefined_behavior_leak_sanitizer.yml + + clang_tidy: + name: Clang-Tidy analysis + if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.run_nightly_checks == 'true') }} + needs: pr_checks + uses: ./.github/workflows/clang_tidy_analysis.yml + secrets: inherit + + dashboard: + name: Generate quality dashboard with timing + if: ${{ always() }} + needs: + - pr_checks + - coverage + - thread_sanitizer + - address_sanitizer + - clang_tidy + 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' }} + CLANG_TIDY_RESULT: ${{ needs.clang_tidy.result || 'skipped' }} + CLANG_TIDY_DURATION_SECONDS: ${{ needs.clang_tidy.outputs.duration-seconds || '' }} + COVERAGE_DURATION_SECONDS: ${{ needs.coverage.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..729bbf34f --- /dev/null +++ b/tools/ci/generate_hybrid_quality_dashboard.py @@ -0,0 +1,180 @@ +#!/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.", + ), + ( + "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.", + ), + ( + "Coverage report", + normalize_status(os.environ.get("COVERAGE_RESULT", "skipped")), + format_duration(os.environ.get("COVERAGE_DURATION_SECONDS", "")), + "Code coverage analysis and reporting." + + (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 (clang-tidy and coverage) run on demand or nightly for 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())