diff --git a/.bazelrc b/.bazelrc index 2505e4402..18700e92c 100644 --- a/.bazelrc +++ b/.bazelrc @@ -131,3 +131,6 @@ try-import %workspace%/.bazelrc.ai_checker # Enable user-defined configs try-import %workspace%/user.bazelrc + +# Import Clippy linting configuration for Rust +import %workspace%/quality/static_analysis/clippy.bazelrc diff --git a/.github/actions/setup-bazel-lint/action.yml b/.github/actions/setup-bazel-lint/action.yml new file mode 100644 index 000000000..dcb97b395 --- /dev/null +++ b/.github/actions/setup-bazel-lint/action.yml @@ -0,0 +1,62 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +# Composite action: Setup Bazel for Lint +# +# Shared setup used by lint/analysis workflows (Clippy, Clang-Tidy, CodeQL, etc.) +# to eliminate duplicated steps. +# +# Runs in order: +# 1. Free Disk Space — ensures enough space before Bazel downloads artifacts +# 2. Setup Bazel — configures Bazel with caching +# 3. Allow linux-sandbox — unblocks user namespaces needed by Bazel sandbox +# +# The calling workflow is responsible for checking out the repository before +# invoking this action. This separation allows workflows to insert their own +# steps (e.g. change-detection) between checkout and the Bazel setup. + +name: Setup Bazel for Lint +description: > + Frees disk space, configures Bazel with shared caching options, and unblocks + the linux sandbox. Shared by lint/analysis workflows (Clippy, Clang-Tidy, + CodeQL, etc.) to avoid duplicating the same setup steps. + The caller must check out the repository before using this action. + +inputs: + disk-cache: + description: Bazel disk-cache key (e.g. "clippy", "clang_tidy") + required: true + cache-save: + description: > + Save the Bazel cache to the remote store. + Should be true only for pushes to main to avoid cache poisoning. + required: false + default: 'false' + +runs: + using: composite + steps: + - name: Free Disk Space (Ubuntu) + uses: eclipse-score/more-disk-space@v1 + with: + level: 4 + - name: Setup Bazel + uses: castler/setup-bazel@cache-optimized + with: + bazelisk-cache: true + disk-cache: ${{ inputs.disk-cache }} + repository-cache: true + cache-optimized: true + cache-save: ${{ inputs.cache-save }} + - name: Allow linux-sandbox + uses: ./actions/unblock_user_namespace_for_linux_sandbox diff --git a/.github/workflows/clang_tidy.yml b/.github/workflows/clang_tidy.yml index 3ae73016c..c0121d47e 100644 --- a/.github/workflows/clang_tidy.yml +++ b/.github/workflows/clang_tidy.yml @@ -72,23 +72,13 @@ jobs: 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 - - - uses: castler/setup-bazel@cache-optimized + fetch-depth: 0 # Full history so git-diff against origin/ works + - name: Setup lint environment + uses: ./.github/actions/setup-bazel-lint with: - bazelisk-cache: true - disk-cache: "clang_tidy" - repository-cache: true - cache-optimized: true + disk-cache: clang_tidy cache-save: ${{ github.ref == 'refs/heads/main' }} - - - 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 findings and upload the artifact diff --git a/.github/workflows/clippy_lint.yml b/.github/workflows/clippy_lint.yml new file mode 100644 index 000000000..c21c51263 --- /dev/null +++ b/.github/workflows/clippy_lint.yml @@ -0,0 +1,262 @@ +# ******************************************************************************* +# 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: Clippy PR check +# +# Runs Clippy on every pull_request, push to main, and merge_group. +# - Fails if Clippy errors or warnings are found in Rust files changed by the PR. +# - The same changed-files scope is applied for both pull_request and +# merge_group (gate) so that a passing PR check guarantees a passing gate. +# - Writes a job summary with error/warning counts. +# - Uploads clippy log as a downloadable artifact. +# +# Cache is saved only on pushes to main. + +name: Clippy Check + +on: + pull_request: + types: [opened, reopened, synchronize] + push: + branches: [main] + merge_group: + types: [checks_requested] + workflow_call: + outputs: + artifact-name: + description: "Name of the clippy findings artifact" + value: ${{ jobs.clippy.outputs.artifact-name }} + errors: + description: "Total clippy error count" + value: ${{ jobs.clippy.outputs.errors }} + warnings: + description: "Total clippy warning count" + value: ${{ jobs.clippy.outputs.warnings }} + conclusion: + description: "Job conclusion: success or failure" + value: ${{ jobs.clippy.outputs.conclusion }} + +permissions: + contents: read + +concurrency: + group: clippy-pr-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +env: + ANDROID_HOME: "" + ANDROID_SDK_ROOT: "" + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + clippy: + runs-on: ubuntu-24.04 + permissions: + contents: read + actions: write # Needed to delete old bazel caches + outputs: + artifact-name: clippy-findings + errors: ${{ steps.findings.outputs.errors }} + warnings: ${{ steps.findings.outputs.warnings }} + conclusion: ${{ steps.set-conclusion.outputs.conclusion }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6.0.2 + with: + fetch-depth: 0 # Full history so git-diff against origin/ works + - name: Check for relevant file changes + id: check-changes + env: + EVENT_NAME: ${{ github.event_name }} + BASE_REF: ${{ github.base_ref }} + run: | + should_run='true' # default: always run + + # For PRs and merge groups, skip when no Rust or workflow files changed. + # For push/workflow_call we always run so the job never gets stuck. + if [[ "$EVENT_NAME" == "pull_request" || "$EVENT_NAME" == "merge_group" ]]; then + CHANGED_FILES=$(git diff --name-only "origin/${BASE_REF}...HEAD" 2>/dev/null || true) + CHANGED_RUST=$(echo "$CHANGED_FILES" | grep -E '\.rs$' || true) + CHANGED_WORKFLOW=$(echo "$CHANGED_FILES" | grep -E '^\.github/workflows/clippy_lint\.yml$' || true) + + if [[ -z "$CHANGED_RUST" && -z "$CHANGED_WORKFLOW" ]]; then + should_run='false' + echo "No Rust or workflow files changed — reporting success without running Clippy." + else + [[ -n "$CHANGED_RUST" ]] && echo "Rust file changes detected." + [[ -n "$CHANGED_WORKFLOW" ]] && echo "Workflow file changes detected." + fi + fi + + echo "should-run=${should_run}" >> $GITHUB_OUTPUT + + - name: Setup lint environment + if: steps.check-changes.outputs.should-run == 'true' + uses: ./.github/actions/setup-bazel-lint + with: + disk-cache: clippy + cache-save: ${{ github.ref == 'refs/heads/main' }} + - name: Run Clippy via Bazel + id: run-clippy + if: steps.check-changes.outputs.should-run == 'true' + # continue-on-error so we can collect findings and upload the artifact + # even when the Bazel invocation exits non-zero. + continue-on-error: true + run: | + bazel build --config=clippy //score/... \ + 2>&1 | tee clippy_raw.log + - name: Collect findings + id: findings + if: steps.check-changes.outputs.should-run == 'true' + run: | + # Verify clippy_raw.log exists + if [[ ! -f clippy_raw.log ]]; then + echo "::error::clippy_raw.log not found - build step may have failed" + exit 1 + fi + + # Check if this was a real build failure due to compilation errors + BUILD_FAILED=false + if grep -q "ERROR: Build did NOT complete successfully" clippy_raw.log; then + BUILD_FAILED=true + echo "::error::Build failed due to compilation errors" + fi + + # Extract clippy errors and warnings + ERRORS=$(grep -A 1 -E "^error(\[|:)" clippy_raw.log 2>/dev/null \ + | grep -c -E "^\s+--> .*\.rs:" || true) + WARNINGS=$(grep -A 1 -E "^warning(\[|:)" clippy_raw.log 2>/dev/null \ + | grep -c -E "^\s+--> .*\.rs:" || true) + ERRORS=${ERRORS:-0} + WARNINGS=${WARNINGS:-0} + TOTAL=$((ERRORS + WARNINGS)) + + echo "Grep results: errors=${ERRORS}, warnings=${WARNINGS}, total=${TOTAL}" + + # Scope blocking issues to Rust files changed in this PR / merge-group + # commit set. The same logic is applied for all event types + # (pull_request, merge_group, push to main) so that a check passing on + # a PR is guaranteed to also pass at gate. + # + # GITHUB_BASE_REF is set by GitHub for both pull_request and + # merge_group events (it is the name of the target branch, e.g. main). + CHANGED_RUST=$(git diff --name-only "origin/${GITHUB_BASE_REF}...HEAD" 2>/dev/null \ + | grep -E '\.rs$' || true) + + echo "Changed Rust files:" + echo "${CHANGED_RUST}" + + if [[ -z "$CHANGED_RUST" ]]; then + ERRORS_BLOCKING=0 + WARNINGS_BLOCKING=0 + echo "No Rust files changed - skipping changed-file check" + else + PATTERN=$(echo "$CHANGED_RUST" | tr '\n' '|' | sed 's/|$//') + echo "File pattern: ${PATTERN}" + # This filters out non-Clippy warnings like "dot utility not found" and summary lines + ERRORS_BLOCKING=$(grep -A 1 -E "^error(\[|:)" clippy_raw.log 2>/dev/null \ + | grep -c -E "^\s+--> (${PATTERN}):" || true) + ERRORS_BLOCKING=${ERRORS_BLOCKING:-0} + WARNINGS_BLOCKING=$(grep -A 1 -E "^warning(\[|:)" clippy_raw.log 2>/dev/null \ + | grep -c -E "^\s+--> (${PATTERN}):" || true) + WARNINGS_BLOCKING=${WARNINGS_BLOCKING:-0} + echo "Blocking errors in changed files: ${ERRORS_BLOCKING}" + echo "Blocking warnings in changed files: ${WARNINGS_BLOCKING}" + fi + + BLOCKING_TOTAL=$((ERRORS_BLOCKING + WARNINGS_BLOCKING)) + + echo "errors=${ERRORS}" >> $GITHUB_OUTPUT + echo "warnings=${WARNINGS}" >> $GITHUB_OUTPUT + echo "total=${TOTAL}" >> $GITHUB_OUTPUT + echo "errors_blocking=${ERRORS_BLOCKING}" >> $GITHUB_OUTPUT + echo "warnings_blocking=${WARNINGS_BLOCKING}" >> $GITHUB_OUTPUT + echo "blocking_total=${BLOCKING_TOTAL}" >> $GITHUB_OUTPUT + echo "build_failed=${BUILD_FAILED}" >> $GITHUB_OUTPUT + + # Fail immediately if this was a build failure due to compilation errors + if [[ "${BUILD_FAILED}" == "true" ]]; then + echo "::error::Build failed due to compilation errors" + echo "## :x: Build Failed" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "The build failed due to compilation errors." >> "$GITHUB_STEP_SUMMARY" + echo "Check the build log for details." >> "$GITHUB_STEP_SUMMARY" + exit 1 + fi + + # Job summary + { + echo "## Clippy Results" + echo "" + echo "| Metric | Count |" + echo "|--------|------:|" + echo "| :x: Errors | **${ERRORS}** |" + echo "| :warning: Warnings | **${WARNINGS}** |" + echo "| Total | **${TOTAL}** |" + echo "" + if [[ "${BLOCKING_TOTAL}" -gt 0 ]]; then + echo "### :x: Check Failed" + echo "" + echo "Found **${ERRORS_BLOCKING} error(s)** and **${WARNINGS_BLOCKING} warning(s)** in changed files." + echo "" + echo "#### Issues in Changed Files" + echo '```' + # Show warning/error message + file location for changed files only + grep -E "^\s+--> (${PATTERN}):" clippy_raw.log 2>/dev/null | while IFS= read -r line; do + # Get the line number of this --> line + line_num=$(grep -n -F "$line" clippy_raw.log | head -1 | cut -d: -f1) + if [[ -n "$line_num" ]]; then + # Show the warning line (1-2 lines before) and the --> line + sed -n "$((line_num-2)),$((line_num))p" clippy_raw.log + echo "" + fi + done | head -100 || echo "See artifact for details" + echo '```' + echo "" + echo "> Download the **clippy-findings** artifact for full details." + else + echo "> :white_check_mark: **Check passed** — no clippy issues in changed files." + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload clippy log + if: always() + uses: actions/upload-artifact@v4 + with: + name: clippy-findings + path: clippy_raw.log + if-no-files-found: ignore + retention-days: 7 + + - name: Fail check if clippy issues found in changed files + if: steps.check-changes.outputs.should-run == 'true' && (github.event_name == 'pull_request' || github.event_name == 'push' || github.event_name == 'merge_group') && steps.findings.outputs.blocking_total != '0' + run: | + echo "::error::Clippy found ${{ steps.findings.outputs.errors_blocking }} error(s) and ${{ steps.findings.outputs.warnings_blocking }} warning(s) in changed files. See the 'clippy-findings' artifact for details." + echo "" + echo "=== Clippy findings ===" + cat clippy_raw.log || true + exit 1 + + - name: Set conclusion + id: set-conclusion + if: always() + run: | + # When check-changes skipped the build (no relevant files changed), + # run-clippy.outcome is 'skipped' — treat that as success. + OUTCOME="${{ steps.run-clippy.outcome }}" + if [[ "${OUTCOME}" == "success" || "${OUTCOME}" == "skipped" ]]; then + echo "conclusion=success" >> $GITHUB_OUTPUT + else + echo "conclusion=failure" >> $GITHUB_OUTPUT + fi diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 898beb7bc..dfe38e4f4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -39,16 +39,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Free Disk Space (Ubuntu) - uses: eclipse-score/more-disk-space@v1 + - name: Setup build environment + uses: ./.github/actions/setup-bazel-lint with: - level: 4 - - - name: Setup Bazel - uses: castler/setup-bazel@cache-optimized - - - name: Allow linux-sandbox - uses: ./actions/unblock_user_namespace_for_linux_sandbox + disk-cache: codeql + cache-save: 'true' # Nightly run — no cache poisoning risk from PRs - name: Create CodeQL database run: | diff --git a/MODULE.bazel b/MODULE.bazel index db223f162..ebd1e351f 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -24,6 +24,8 @@ bazel_dep(name = "rules_cc", version = "0.2.17") bazel_dep(name = "rules_python", version = "1.8.5") bazel_dep(name = "rules_rust", version = "0.68.1-score") +bazel_dep(name = "score_rust_policies", version = "0.0.5", dev_dependency = True) + # Cannot be dev-dependency due to being required in "load" statements bazel_dep(name = "score_qnx_unit_tests", version = "0.1.0") bazel_dep(name = "score_baselibs", version = "0.2.7") diff --git a/quality/static_analysis/clippy.bazelrc b/quality/static_analysis/clippy.bazelrc new file mode 100644 index 000000000..ee13188cf --- /dev/null +++ b/quality/static_analysis/clippy.bazelrc @@ -0,0 +1,18 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +# Clippy configuration for Rust linting +# Run Clippy on all Rust targets with: bazel build --config=clippy //... +build:clippy --aspects=@score_rust_policies//clippy:linters.bzl%clippy_strict +build:clippy --output_groups=+rules_lint_human +build:clippy --@aspect_rules_lint//lint:fail_on_violation=true