This is an automated email from the ASF dual-hosted git repository. hubcio pushed a commit to branch perf/rust-pre-merge-coverage in repository https://gitbox.apache.org/repos/asf/iggy.git
commit affe6e098bbb39ee6333e61e259a81e396abab33 Author: Hubert Gruszecki <[email protected]> AuthorDate: Wed Oct 14 16:57:50 2026 +0200 perf(ci): avoid redundant Rust coverage builds Rust shards and BDD jobs rebuilt tests, while coverage settings and Python's native-library environment prevented dependency reuse. Own Rust checks, the shared test build, and parallel shards in one reusable workflow. Reuse it for PRs and full master coverage while preserving affected-task selection and test failure propagation. Instrument BDD tests during image builds and reuse Python's prebuilt server. Collect Rust coverage only from tests and BDD, with matching master reports and two-hour post-merge timeouts. Align master cache warmers with the SDK jobs. Keep compiler errors visible and avoid hashing raw coverage files. Remove npm and Bazel caching and shorten shared setup action names. --- .config/nextest.toml | 3 +- .github/actions/cpp-bazel/pre-merge/action.yml | 8 +- .github/actions/csharp-dotnet/pre-merge/action.yml | 6 +- .github/actions/go/pre-merge/action.yml | 6 +- .github/actions/java-gradle/post-merge/action.yml | 4 +- .github/actions/java-gradle/pre-merge/action.yml | 8 +- .github/actions/node-npm/pre-merge/action.yml | 7 +- .github/actions/php/pre-merge/action.yml | 4 +- .../actions/python-maturin/pre-merge/action.yml | 50 ++-- .github/actions/rust/post-merge/action.yml | 4 +- .github/actions/rust/pre-merge/action.yml | 255 +++++++++++---------- .github/actions/swift/pre-merge/action.yml | 2 +- .github/actions/utils/server-start/action.yml | 13 +- .../{setup-cpp-with-cache => setup-cpp}/action.yml | 9 +- .../{setup-go-with-cache => setup-go}/action.yml | 4 +- .../action.yml | 2 +- .../action.yml | 29 +-- .../action.yml | 86 ++++--- .../utils/validate-third-party-licenses/action.yml | 5 +- .github/config/components.yml | 6 +- .github/workflows/_build_python_wheels.yml | 12 +- .github/workflows/_build_rust_artifacts.yml | 9 +- .github/workflows/_common.yml | 1 + .github/workflows/_detect.yml | 19 +- .github/workflows/_publish_rust_crates.yml | 4 +- .github/workflows/_rust.yml | 196 ++++++++++++++++ .github/workflows/_test.yml | 76 +----- .github/workflows/_test_bdd.yml | 43 +++- .github/workflows/_test_examples.yml | 63 +++-- .github/workflows/coverage-baseline.yml | 191 ++++++--------- .github/workflows/pr-skill-review-run.yml | 2 + .github/workflows/pre-merge.yml | 32 +-- .github/workflows/publish.yml | 2 +- bdd/docker-compose.cluster.yml | 3 + bdd/docker-compose.coverage.yml | 21 +- bdd/docker-compose.server.yml | 2 + bdd/docker-compose.yml | 2 + bdd/rust/Dockerfile | 40 +++- codecov.yml | 3 + scripts/ci/coverage-baseline-affected.sh | 2 +- scripts/ci/storage-compat.sh | 4 + scripts/run-bdd-tests.sh | 3 +- 42 files changed, 698 insertions(+), 543 deletions(-) diff --git a/.config/nextest.toml b/.config/nextest.toml index bd0f105e5..1acb8e17f 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -17,8 +17,7 @@ # Floor for the flags CI and scripts/ci/storage-compat.sh pass: --no-tests # (0.9.75), --run-ignored only (0.9.76) and --ignore-default-filter (0.9.77). -# nextest refuses to run below it, and setup-rust-with-cache replaces a -# cache-restored binary that fails `cargo nextest show-config version`. +# nextest refuses to run below it; setup-rust installs a pinned version above this floor. nextest-version = { required = "0.9.77" } [[profile.default.overrides]] diff --git a/.github/actions/cpp-bazel/pre-merge/action.yml b/.github/actions/cpp-bazel/pre-merge/action.yml index b21f3eb2e..eaa15c9c6 100644 --- a/.github/actions/cpp-bazel/pre-merge/action.yml +++ b/.github/actions/cpp-bazel/pre-merge/action.yml @@ -26,12 +26,12 @@ inputs: runs: using: "composite" steps: - - name: Setup Bazel with cache + - name: Setup Bazel if: inputs.task != 'lint' - uses: ./.github/actions/utils/setup-cpp-with-cache + uses: ./.github/actions/utils/setup-cpp - - name: Setup Rust with cache - uses: ./.github/actions/utils/setup-rust-with-cache + - name: Setup Rust + uses: ./.github/actions/utils/setup-rust - name: Install clang-format if: inputs.task == 'lint' diff --git a/.github/actions/csharp-dotnet/pre-merge/action.yml b/.github/actions/csharp-dotnet/pre-merge/action.yml index d68f930c5..d0471cd79 100644 --- a/.github/actions/csharp-dotnet/pre-merge/action.yml +++ b/.github/actions/csharp-dotnet/pre-merge/action.yml @@ -33,9 +33,9 @@ runs: 8.0.x 10.0.x - - name: Setup Rust with cache - if: inputs.task == 'test' || startsWith(inputs.task, 'e2e') - uses: ./.github/actions/utils/setup-rust-with-cache + - name: Setup Rust + if: startsWith(inputs.task, 'e2e') + uses: ./.github/actions/utils/setup-rust - name: Restore dependencies run: | diff --git a/.github/actions/go/pre-merge/action.yml b/.github/actions/go/pre-merge/action.yml index b06f680a5..30a4a0010 100644 --- a/.github/actions/go/pre-merge/action.yml +++ b/.github/actions/go/pre-merge/action.yml @@ -27,7 +27,7 @@ runs: using: "composite" steps: - name: Setup Go - uses: ./.github/actions/utils/setup-go-with-cache + uses: ./.github/actions/utils/setup-go - name: Format Check shell: bash @@ -117,9 +117,9 @@ runs: # Build the module go build -v ./... - - name: Setup Rust with cache for e2e + - name: Setup Rust for e2e if: inputs.task == 'e2e' - uses: ./.github/actions/utils/setup-rust-with-cache + uses: ./.github/actions/utils/setup-rust # The Go SDK speaks the VSR wire protocol, so every e2e lane runs against # a locally built VSR server rather than a container of the legacy one. diff --git a/.github/actions/java-gradle/post-merge/action.yml b/.github/actions/java-gradle/post-merge/action.yml index e4e1927ea..79b7cf73e 100644 --- a/.github/actions/java-gradle/post-merge/action.yml +++ b/.github/actions/java-gradle/post-merge/action.yml @@ -30,8 +30,8 @@ inputs: runs: using: "composite" steps: - - name: Setup Java with cache - uses: ./.github/actions/utils/setup-java-with-cache + - name: Setup Java + uses: ./.github/actions/utils/setup-java - name: Build for publishing shell: bash diff --git a/.github/actions/java-gradle/pre-merge/action.yml b/.github/actions/java-gradle/pre-merge/action.yml index db377bd43..a6047a619 100644 --- a/.github/actions/java-gradle/pre-merge/action.yml +++ b/.github/actions/java-gradle/pre-merge/action.yml @@ -26,8 +26,8 @@ inputs: runs: using: "composite" steps: - - name: Setup Java with cache - uses: ./.github/actions/utils/setup-java-with-cache + - name: Setup Java + uses: ./.github/actions/utils/setup-java with: gradle-cache-disabled: "true" @@ -76,9 +76,9 @@ runs: # Exit with build exit code exit $BUILD_EXIT_CODE - - name: Setup Rust with cache + - name: Setup Rust if: inputs.task == 'test' - uses: ./.github/actions/utils/setup-rust-with-cache + uses: ./.github/actions/utils/setup-rust - name: Start Iggy server if: inputs.task == 'test' diff --git a/.github/actions/node-npm/pre-merge/action.yml b/.github/actions/node-npm/pre-merge/action.yml index 97771a05b..4214810f1 100644 --- a/.github/actions/node-npm/pre-merge/action.yml +++ b/.github/actions/node-npm/pre-merge/action.yml @@ -31,16 +31,15 @@ runs: with: node-version: "23" registry-url: "https://registry.npmjs.org" - cache: "npm" - cache-dependency-path: foreign/node/package-lock.json + package-manager-cache: false - name: Pin npm to 11 to match Dependabot lockfile format run: npm install -g npm@11 shell: bash - - name: Setup Rust with cache + - name: Setup Rust if: inputs.task == 'e2e' - uses: ./.github/actions/utils/setup-rust-with-cache + uses: ./.github/actions/utils/setup-rust - name: Install netcat if: inputs.task == 'e2e' diff --git a/.github/actions/php/pre-merge/action.yml b/.github/actions/php/pre-merge/action.yml index f69b57657..6386086a0 100644 --- a/.github/actions/php/pre-merge/action.yml +++ b/.github/actions/php/pre-merge/action.yml @@ -58,8 +58,8 @@ runs: "${php_config}" --version "${php_bin}" "${composer_bin}" --version - - name: Setup Rust with cache - uses: ./.github/actions/utils/setup-rust-with-cache + - name: Setup Rust + uses: ./.github/actions/utils/setup-rust with: shared-key: dev save-cache: "false" diff --git a/.github/actions/python-maturin/pre-merge/action.yml b/.github/actions/python-maturin/pre-merge/action.yml index dcb69b146..3233c3a75 100644 --- a/.github/actions/python-maturin/pre-merge/action.yml +++ b/.github/actions/python-maturin/pre-merge/action.yml @@ -26,14 +26,37 @@ inputs: runs: using: "composite" steps: + - name: Setup Rust + uses: ./.github/actions/utils/setup-rust + + # setup-python changes PKG_CONFIG_PATH and invalidates cached native server dependencies. + - name: Build server Docker image for TLS tests + if: inputs.task == 'test' + run: | + # test_tls.py spawns the server in a container; build it from the + # same binaries the plain-TCP tests run against. + cargo build --locked --bin iggy-server --bin iggy + docker build \ + -f core/server/Dockerfile \ + --target runtime-prebuilt \ + --build-arg PREBUILT_IGGY_SERVER=target/debug/iggy-server \ + --build-arg PREBUILT_IGGY_CLI=target/debug/iggy \ + -t iggy-server:local . + shell: bash + + - name: Start Iggy server + if: inputs.task == 'test' + id: iggy + uses: ./.github/actions/utils/server-start + with: + mode: bin + bin: target/debug/iggy-server + - name: Setup Python uses: actions/setup-python@v6 with: python-version: "3.11" - - name: Setup Rust with cache - uses: ./.github/actions/utils/setup-rust-with-cache - - name: Install cargo-llvm-cov if: inputs.task == 'test' uses: taiki-e/install-action@v2 @@ -147,27 +170,6 @@ runs: uv run --no-sync maturin develop shell: bash - - name: Build server Docker image for TLS tests - if: inputs.task == 'test' - run: | - # test_tls.py spawns the server in a container; build it from the - # same binaries the plain-TCP tests run against. - cargo build --locked --bin iggy-server --bin iggy - docker build \ - -f core/server/Dockerfile \ - --target runtime-prebuilt \ - --build-arg PREBUILT_IGGY_SERVER=target/debug/iggy-server \ - --build-arg PREBUILT_IGGY_CLI=target/debug/iggy \ - -t iggy-server:local . - shell: bash - - - name: Start Iggy server - if: inputs.task == 'test' - id: iggy - uses: ./.github/actions/utils/server-start - with: - cargo-bin: iggy-server - - name: Run Python integration tests if: inputs.task == 'test' run: | diff --git a/.github/actions/rust/post-merge/action.yml b/.github/actions/rust/post-merge/action.yml index caec167ee..13868253d 100644 --- a/.github/actions/rust/post-merge/action.yml +++ b/.github/actions/rust/post-merge/action.yml @@ -34,8 +34,8 @@ inputs: runs: using: "composite" steps: - - name: Setup Rust with cache - uses: ./.github/actions/utils/setup-rust-with-cache + - name: Setup Rust + uses: ./.github/actions/utils/setup-rust - name: Validate package env: diff --git a/.github/actions/rust/pre-merge/action.yml b/.github/actions/rust/pre-merge/action.yml index de49148fd..34d6c5d3c 100644 --- a/.github/actions/rust/pre-merge/action.yml +++ b/.github/actions/rust/pre-merge/action.yml @@ -20,22 +20,41 @@ description: Rust pre-merge testing and linting github iggy actions inputs: task: - description: "Task to run (check, check-msrv, fmt, clippy, clippy-macos-aarch64, sort, machete, doctest, verify-publish, test-1, test-2, test-3, test-storage-compat, miri)" + description: "Task to run (check, check-msrv, fmt, clippy, clippy-macos-aarch64, sort, machete, doctest, verify-publish, test-build, test, test-storage-compat, miri)" required: true component: description: "Component name (for context)" required: false default: "" + test-scope: + description: "Test selection: affected or workspace" + required: false + default: affected + partition: + description: "One-based nextest partition index for the test task" + required: false + partitions: + description: "Total nextest partitions for the test task" + required: false + save-cache: + description: "Save build dependencies for subsequent PRs" + required: false + default: "false" + free-disk-space-aggressive: + description: "Use deep disk cleanup for full workspace coverage builds" + required: false + default: "false" runs: using: "composite" steps: - - name: Setup Rust with cache - uses: ./.github/actions/utils/setup-rust-with-cache + - name: Setup Rust + uses: ./.github/actions/utils/setup-rust with: # Only test jobs execute nextest. Avoid downloading it in lint, metadata, # and cross-build jobs. - install-nextest: ${{ startsWith(inputs.task, 'test-') }} + install-nextest: ${{ inputs.task == 'test' || startsWith(inputs.task, 'test-') }} + coverage: ${{ inputs.task == 'test-build' || inputs.task == 'test' }} # These tasks do not compile workspace crates and need no native # libraries. All compiling tasks keep the existing dependency setup. install-system-dependencies: ${{ inputs.task != 'fmt' && inputs.task != 'sort' && inputs.task != 'machete' }} @@ -48,14 +67,14 @@ runs: # the warm `~/.cargo` restore is a free win, and its 1.95 target # artifacts rebuild regardless since the rustc hash differs. shared-key: ${{ inputs.task == 'miri' && 'miri' || 'dev' }} - # fmt/sort/machete never build into target/, so the multi-GB cache - # restore is pure overhead. Compiling legs (check/clippy/doctest/test-*) - # keep it; a cold cache there recompiles the whole dep tree. - read-cache: ${{ (inputs.task == 'fmt' || inputs.task == 'sort' || inputs.task == 'machete') && 'false' || 'true' }} + # Shards download the build artifact. Metadata-only tasks need no target cache. + read-cache: ${{ (inputs.task == 'fmt' || inputs.task == 'sort' || inputs.task == 'machete' || inputs.task == 'test') && 'false' || 'true' }} + save-cache: ${{ inputs.save-cache }} # Light legs fit in the runner's ~89 GiB default headroom; skip the # ~20-45s reclaim. Disk-heavy legs (coverage build + testcontainers - # images on test-*, cross-builds, miri, verify-publish) keep it. + # images on test jobs, cross-builds, miri, verify-publish) keep it. free-disk-space: ${{ (inputs.task == 'fmt' || inputs.task == 'sort' || inputs.task == 'clippy' || inputs.task == 'check' || inputs.task == 'check-msrv' || inputs.task == 'machete' || inputs.task == 'doctest') && 'false' || 'true' }} + free-disk-space-aggressive: ${{ inputs.free-disk-space-aggressive }} - name: Install cargo-sort if: inputs.task == 'sort' @@ -84,7 +103,7 @@ runs: # Cargo-Rail release it is qualified with (0.30.1 for v10.1.1). - name: Plan affected Rust work id: rail - if: startsWith(inputs.task, 'test-') && inputs.task != 'test-storage-compat' + if: inputs.task == 'test-build' && inputs.test-scope == 'affected' continue-on-error: true uses: loadingalias/cargo-rail-action@409962f05d90a78aae995ef6b1c8466010c0a460 # v10.1.1 @@ -149,45 +168,22 @@ runs: shell: bash - name: Install dependencies for Rust tests - if: startsWith(inputs.task, 'test-') && runner.os == 'Linux' + if: (inputs.task == 'test' || inputs.task == 'test-storage-compat') && runner.os == 'Linux' run: | ./scripts/ci/apt-install.sh musl-tools gnome-keyring keyutils dbus-x11 libsecret-tools rm -f "$HOME"/.local/share/keyrings/* shell: bash - - name: Install cargo-llvm-cov - if: startsWith(inputs.task, 'test-') && inputs.task != 'test-storage-compat' - uses: taiki-e/install-action@v2 - with: - tool: cargo-llvm-cov - - - name: Build and test with coverage - # test-storage-compat drives its own nextest invocation from - # scripts/ci/storage-compat.sh. Without this exclusion it would also run - # here without partition arguments, i.e. a fourth *unpartitioned* copy - # of the whole suite on top of test-1/2/3. - if: startsWith(inputs.task, 'test-') && inputs.task != 'test-storage-compat' + - name: Build coverage test archive + if: inputs.task == 'test-build' env: PLAN_FILE: ${{ steps.rail.outputs.plan-file }} PLAN_STATUS: ${{ steps.rail.outcome }} - TASK: ${{ inputs.task }} + TEST_SELECTION: ${{ inputs.test-scope }} run: | set -euo pipefail - - # Parse partition index from task name (test-1 -> hash:1/3, test-2 -> hash:2/3, ...). - # TEST_PARTITIONS must match the number of test-N tasks in - # .github/config/components.yml. Cluster bootstrap makes each test - # CPU-heavy, so partitions stay small. - # The regex is numeric-only on purpose: non-numeric test-* tasks - # (test-storage-compat) opt out of partitioning rather than producing - # an out-of-range hash:N/3. - TEST_PARTITIONS=3 - PARTITION_ARGS=() - if [[ "$TASK" =~ ^test-([0-9]+)$ ]]; then - PARTITION_INDEX="${BASH_REMATCH[1]}" - PARTITION_ARGS=(--partition "hash:${PARTITION_INDEX}/${TEST_PARTITIONS}") - echo "::notice::Running test partition ${PARTITION_INDEX}/${TEST_PARTITIONS}" - fi + ARTIFACT_DIR="$RUNNER_TEMP/rust-test-build" + mkdir -p "$ARTIFACT_DIR" # Read only validated selectors. Argument and package selectors are # NUL-delimited, hence `mapfile -d ''`. Any planner or selector failure @@ -195,12 +191,18 @@ runs: # target-args emits `--test` pairs only when every selected target of # every selected package is an integration test, so it never drops a # package or its unit tests from a mixed selection. - SELECTOR_DIR="${RUNNER_TEMP:?RUNNER_TEMP is required}/cargo-rail-selectors-${TASK}" + SELECTOR_DIR="${RUNNER_TEMP:?RUNNER_TEMP is required}/cargo-rail-selectors" mkdir -p "$SELECTOR_DIR" TEST_CARGO_ARGS=() TEST_TARGET_ARGS=() TEST_PACKAGES=() - if [[ "$PLAN_STATUS" == "success" ]] && + if [[ "$TEST_SELECTION" == "workspace" ]]; then + TEST_SCOPE=workspace + RUNTIME_SCOPE=workspace + elif [[ "$TEST_SELECTION" != "affected" ]]; then + echo "::error::Invalid test scope: $TEST_SELECTION" + exit 1 + elif [[ "$PLAN_STATUS" == "success" ]] && TEST_SCOPE=$(cargo-rail-action plan cargo-scope "$PLAN_FILE" cargo.test) && RUNTIME_SCOPE=$(cargo-rail-action plan cargo-scope "$PLAN_FILE" runtime-artifacts) && cargo-rail-action plan cargo-args "$PLAN_FILE" cargo.test > "$SELECTOR_DIR/test-args" && @@ -216,61 +218,44 @@ runs: echo "::warning::Cargo-Rail plan unavailable or invalid; running the full workspace" fi + printf '%s\n' "$TEST_SCOPE" > "$ARTIFACT_DIR/scope" + git rev-parse HEAD > "$ARTIFACT_DIR/revision" if [[ "$TEST_SCOPE" == "skipped" ]]; then echo "::notice::Cargo-Rail reports no affected Rust tests" exit 0 fi - # Runtime prerequisites (spawned binaries, dlopened connector plugins) - # are built alongside the tested packages. A package in both lists is - # built once. + # Only spawned binaries and dlopened plugins need a separate dev build. + # nextest archive compiles the selected tests with their dev-dependency features. BUILD_ARGS=() - if [[ "$TEST_SCOPE" == "packages" && "$RUNTIME_SCOPE" != "workspace" ]]; then - mapfile -d '' -t BUILD_PACKAGES < <(sort -zu "$SELECTOR_DIR/test-packages" "$SELECTOR_DIR/runtime-packages") - for package in "${BUILD_PACKAGES[@]}"; do + if [[ "$RUNTIME_SCOPE" == "packages" ]]; then + mapfile -d '' -t RUNTIME_PACKAGES < "$SELECTOR_DIR/runtime-packages" + for package in "${RUNTIME_PACKAGES[@]}"; do BUILD_ARGS+=(-p "$package") done - BUILD_SUMMARY="${#BUILD_PACKAGES[@]} packages" - else - BUILD_SUMMARY="full workspace" fi - if [[ "$TEST_SCOPE" == "packages" ]]; then - TEST_SUMMARY="${#TEST_PACKAGES[@]} packages" - else - TEST_SUMMARY="full workspace" - fi - echo "::notice::Cargo-Rail: testing ${TEST_SUMMARY}, building ${BUILD_SUMMARY}" - - source <(cargo llvm-cov show-env --export-prefix) - - # Doris 4.0.3's start_be.sh hard-`exit 1`s unless vm.max_map_count >= 2000000. - # This kernel param can only be set on the host (no container can raise it), - # so we raise it here. Everything else about booting Doris — image, heap/mem - # caps, port mappings, BE-alive wait — lives in the testcontainers fixture - # at core/integration/tests/connectors/fixtures/doris/container.rs, so - # `cargo test` and CI follow exactly the same path. - if [[ "$RUNNER_OS" == "Linux" ]]; then - sudo sysctl -w vm.max_map_count=2000000 || true + : > "$ARTIFACT_DIR/build.jsonl" + : > "$ARTIFACT_DIR/runtime-files" + if [[ "$RUNTIME_SCOPE" != "skipped" ]]; then + cargo build --locked "${BUILD_ARGS[@]}" --message-format=json-render-diagnostics > "$ARTIFACT_DIR/build.jsonl" + jq -r 'select(.reason == "compiler-artifact") | + (select(.target.kind | index("bin")) | .executable // empty), + (select(.target.crate_types | index("cdylib")) | .filenames[] | select(endswith(".so")))' \ + "$ARTIFACT_DIR/build.jsonl" | sort -u | \ + while IFS= read -r artifact; do + realpath --relative-to="$GITHUB_WORKSPACE" "$artifact" + done > "$ARTIFACT_DIR/runtime-files" fi - - bins_start=$(date +%s) - cargo build --locked "${BUILD_ARGS[@]}" - bins_end=$(date +%s) - bins_duration=$((bins_end - bins_start)) - echo "::notice::Binaries and libraries built in ${bins_duration}s ($(date -ud @${bins_duration} +'%M:%S'))" - - compile_start=$(date +%s) - cargo test --locked --no-run "${TEST_CARGO_ARGS[@]}" "${TEST_TARGET_ARGS[@]}" - compile_end=$(date +%s) - compile_duration=$((compile_end - compile_start)) - echo "::notice::Tests compiled in ${compile_duration}s ($(date -ud @${compile_duration} +'%M:%S'))" + tar --zstd -cf "$ARTIFACT_DIR/runtime.tar.zst" -T "$ARTIFACT_DIR/runtime-files" + rm "$ARTIFACT_DIR/build.jsonl" "$ARTIFACT_DIR/runtime-files" # api_handler_tests, version_firewall_tests, and server_e2e_tests need gitignored - # wire fixtures. Generate when iggy-gateway-kafka is in the Cargo-Rail test scope, or (on - # a full-workspace run) when gateways/** changed vs the PR base — avoid building - # kafka-message-gen / kafka-protocol when the gateway was not touched. + # wire fixtures. Master always generates them. On PRs, use the selected + # packages or the gateway diff to avoid building fixture tools unnecessarily. NEEDS_KAFKA_FIXTURES=false - if [[ "$TEST_SCOPE" == "packages" ]]; then + if [[ "$TEST_SELECTION" == "workspace" ]]; then + NEEDS_KAFKA_FIXTURES=true + elif [[ "$TEST_SCOPE" == "packages" ]]; then for package in "${TEST_PACKAGES[@]}"; do if [[ "$package" == "iggy-gateway-kafka" ]]; then NEEDS_KAFKA_FIXTURES=true @@ -313,9 +298,44 @@ runs: echo "::warning::git diff against the PR base failed (base: ${BASE_SHA:-none, HEAD is not a merge commit}; ${DIFF_OUTPUT:-}); generating Kafka fixtures unconditionally as a fail-safe" fi fi + printf '%s\n' "$NEEDS_KAFKA_FIXTURES" > "$ARTIFACT_DIR/kafka-fixtures" if [[ "$NEEDS_KAFKA_FIXTURES" == true ]]; then ./gateways/kafka/scripts/ci-wire-fixtures.sh generate - trap './gateways/kafka/scripts/ci-wire-fixtures.sh cleanup' EXIT + tar --zstd -cf "$ARTIFACT_DIR/kafka-fixtures.tar.zst" gateways/kafka/tools/kafka-tool/kafka_messages + fi + + cargo nextest archive --locked --profile ci \ + "${TEST_CARGO_ARGS[@]}" "${TEST_TARGET_ARGS[@]}" \ + --archive-file "$ARTIFACT_DIR/nextest.tar.zst" + shell: bash + + - name: Run coverage test partition + if: inputs.task == 'test' + env: + TEST_PARTITION: ${{ inputs.partition }} + TEST_PARTITIONS: ${{ inputs.partitions }} + run: | + set -euo pipefail + if [[ ! "$TEST_PARTITION" =~ ^[1-9][0-9]*$ || ! "$TEST_PARTITIONS" =~ ^[1-9][0-9]*$ ]] || + (( TEST_PARTITION > TEST_PARTITIONS )); then + echo "::error::Invalid test partition: $TEST_PARTITION/$TEST_PARTITIONS" + exit 1 + fi + ARTIFACT_DIR="$RUNNER_TEMP/rust-test-build" + artifact_revision=$(cat "$ARTIFACT_DIR/revision") + checkout_revision=$(git rev-parse HEAD) + if [[ "$artifact_revision" != "$checkout_revision" ]]; then + echo "::error::Coverage archive revision $artifact_revision does not match checkout $checkout_revision" + exit 1 + fi + TEST_SCOPE=$(cat "$ARTIFACT_DIR/scope") + if [[ "$TEST_SCOPE" == "skipped" ]]; then + echo "::notice::Cargo-Rail reports no affected Rust tests" + exit 0 + fi + tar --zstd -xf "$ARTIFACT_DIR/runtime.tar.zst" + if [[ "$(cat "$ARTIFACT_DIR/kafka-fixtures")" == true ]]; then + tar --zstd -xf "$ARTIFACT_DIR/kafka-fixtures.tar.zst" # Fixtures are expected to exist from here on; a missing one now means # generation silently failed, not a legitimate local-dev skip. export KAFKA_FIXTURES_REQUIRED=1 @@ -349,52 +369,23 @@ runs: # Previously this ran before `cargo build`, leaving a 7+ minute idle # window that triggered org.freedesktop.Secret.Error.IsLocked ~10% of runs. if [[ "$RUNNER_OS" == "Linux" ]]; then - eval $(dbus-launch --sh-syntax) + eval "$(dbus-launch --sh-syntax)" export DBUS_SESSION_BUS_ADDRESS - eval $(echo -n "test" | gnome-keyring-daemon --unlock --components=secrets) + eval "$(echo -n "test" | gnome-keyring-daemon --unlock --components=secrets)" echo -n "warmup" | secret-tool store --label="ci-warmup" ci-test warmup fi - test_start=$(date +%s) - if command -v cargo-nextest &> /dev/null; then - if [[ "$TEST_SCOPE" == "packages" ]]; then - # Every test target in the `bdd` crate is `required-features = - # ["bdd"]`, so the `cargo test --no-run` above builds no binaries - # for it. A PR touching only that crate then produces a valid - # package scope that matches zero tests, and nextest's default - # `--no-tests=fail` turns that into exit 4 on a green PR. - cargo nextest run --locked --no-fail-fast --no-tests=warn --profile ci \ - "${PARTITION_ARGS[@]}" "${TEST_CARGO_ARGS[@]}" "${TEST_TARGET_ARGS[@]}" - else - cargo nextest run --locked --no-fail-fast --profile ci "${PARTITION_ARGS[@]}" - fi - else - if [[ ${#PARTITION_ARGS[@]} -gt 0 ]]; then - echo "::error::cargo-nextest not found, falling back to cargo test without partitioning (all tests will run on every partition)" - fi - cargo test --locked --no-fail-fast "${TEST_CARGO_ARGS[@]}" "${TEST_TARGET_ARGS[@]}" + # Doris requires this host setting before its testcontainers fixture starts. + sudo sysctl -w vm.max_map_count=2000000 || true + NO_TESTS=fail + if [[ "$TEST_SCOPE" == "packages" ]]; then + # BDD targets require features, so a BDD-only scope can contain no runnable tests. + NO_TESTS=warn fi - test_end=$(date +%s) - test_duration=$((test_end - test_start)) - echo "::notice::Tests executed in ${test_duration}s ($(date -ud @${test_duration} +'%M:%S'))" - - build_duration=$((bins_duration + compile_duration)) - total_duration=$((build_duration + test_duration)) - echo "" - echo "=========================================" - echo "Cargo-Rail scope (test): ${TEST_SUMMARY}" - echo "Cargo-Rail scope (build): ${BUILD_SUMMARY}" - echo "All targets build: ${bins_duration}s ($(date -ud @${bins_duration} +'%M:%S'))" - echo "Tests compile: ${compile_duration}s ($(date -ud @${compile_duration} +'%M:%S'))" - echo "Tests execute: ${test_duration}s ($(date -ud @${test_duration} +'%M:%S'))" - echo "-----------------------------------------" - echo "Total build: ${build_duration}s ($(date -ud @${build_duration} +'%M:%S'))" - echo "Total time: ${total_duration}s ($(date -ud @${total_duration} +'%M:%S'))" - echo "=========================================" - - cargo llvm-cov report --codecov --output-path codecov.json - echo "Coverage report generated: codecov.json" - ls -la codecov.json + cargo nextest run --archive-file "$ARTIFACT_DIR/nextest.tar.zst" \ + --extract-to "$GITHUB_WORKSPACE" --extract-overwrite --workspace-remap "$GITHUB_WORKSPACE" \ + --no-fail-fast --no-tests="$NO_TESTS" --profile ci \ + --partition "hash:${TEST_PARTITION}/${TEST_PARTITIONS}" shell: bash # On-disk format backwards compatibility. The script builds an iggy-server @@ -478,7 +469,19 @@ runs: run: | target/debug/iggy-server --fresh > server-boot.log 2>&1 & SERVER_PID=$! - trap 'kill "$SERVER_PID" 2>/dev/null || true' EXIT + stop_server() { + local stop_timeout=30 + kill "$SERVER_PID" 2>/dev/null || true + for ((elapsed = 0; elapsed < stop_timeout; elapsed++)); do + if ! kill -0 "$SERVER_PID" 2>/dev/null; then + break + fi + sleep 1 + done + kill -KILL "$SERVER_PID" 2>/dev/null || true + wait "$SERVER_PID" || true + } + trap stop_server EXIT for _ in $(seq 1 30); do if ! kill -0 "$SERVER_PID" 2>/dev/null; then diff --git a/.github/actions/swift/pre-merge/action.yml b/.github/actions/swift/pre-merge/action.yml index f21a11903..8623a6a53 100644 --- a/.github/actions/swift/pre-merge/action.yml +++ b/.github/actions/swift/pre-merge/action.yml @@ -80,7 +80,7 @@ runs: # the lane skips the disk cleanup, the apt install, and the cache restore. - name: Setup Rust for the golden vectors if: inputs.task == 'golden' - uses: ./.github/actions/utils/setup-rust-with-cache + uses: ./.github/actions/utils/setup-rust with: free-disk-space: "false" read-cache: "false" diff --git a/.github/actions/utils/server-start/action.yml b/.github/actions/utils/server-start/action.yml index 664582f7e..6b76e9cc5 100644 --- a/.github/actions/utils/server-start/action.yml +++ b/.github/actions/utils/server-start/action.yml @@ -112,14 +112,13 @@ runs: else OUT="target/debug/$NAME" fi - if [[ ! -x "$OUT" ]]; then - echo "Building $NAME with cargo ($PROFILE)…" - CARGO_ARGS=(build --locked --bin "$NAME") - if [[ "$PROFILE" == "release" ]]; then - CARGO_ARGS+=(--release) - fi - cargo "${CARGO_ARGS[@]}" + # An existing executable may belong to a different checkout or feature set. + echo "Building $NAME with cargo ($PROFILE)..." + CARGO_ARGS=(build --locked --bin "$NAME") + if [[ "$PROFILE" == "release" ]]; then + CARGO_ARGS+=(--release) fi + cargo "${CARGO_ARGS[@]}" BIN_PATH="$OUT" else BIN_PATH="${{ inputs.bin }}" diff --git a/.github/actions/utils/setup-cpp-with-cache/action.yml b/.github/actions/utils/setup-cpp/action.yml similarity index 82% rename from .github/actions/utils/setup-cpp-with-cache/action.yml rename to .github/actions/utils/setup-cpp/action.yml index 1ffb584bf..9f3a43bfd 100644 --- a/.github/actions/utils/setup-cpp-with-cache/action.yml +++ b/.github/actions/utils/setup-cpp/action.yml @@ -15,18 +15,15 @@ # specific language governing permissions and limitations # under the License. -name: setup-cpp-with-cache -description: Setup Bazel toolchain and caching (bazelisk + repository + disk cache) +name: setup-cpp +description: Setup Bazel toolchain runs: using: "composite" steps: - - name: Setup Bazel with cache + - name: Setup Bazel uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # v0.19.0 with: - bazelisk-cache: true - disk-cache: true - repository-cache: true module-root: foreign/cpp - name: Verify Bazel installation diff --git a/.github/actions/utils/setup-go-with-cache/action.yml b/.github/actions/utils/setup-go/action.yml similarity index 98% rename from .github/actions/utils/setup-go-with-cache/action.yml rename to .github/actions/utils/setup-go/action.yml index 10f9c2a7e..ae896bbf9 100644 --- a/.github/actions/utils/setup-go-with-cache/action.yml +++ b/.github/actions/utils/setup-go/action.yml @@ -16,10 +16,10 @@ # under the License. # NOTE: Go builds are fast, so caching is disabled by default. -# The read-cache/save-cache inputs exist for parity with setup-rust-with-cache +# The read-cache/save-cache inputs exist for parity with setup-rust # but both default to "false". Enable only if Go build times become a bottleneck. -name: setup-go-with-cache +name: setup-go description: Setup Go toolchain and optional caching (go mod cache + golangci-lint cache) inputs: go-version: diff --git a/.github/actions/utils/setup-java-with-cache/action.yml b/.github/actions/utils/setup-java/action.yml similarity index 98% rename from .github/actions/utils/setup-java-with-cache/action.yml rename to .github/actions/utils/setup-java/action.yml index 2a757b24e..e9f60cd28 100644 --- a/.github/actions/utils/setup-java-with-cache/action.yml +++ b/.github/actions/utils/setup-java/action.yml @@ -18,7 +18,7 @@ # NOTE: this action sets up the Java toolchain with optional Gradle caching. # Gradle itself is provided via the wrapper (gradlew) in each project. -name: setup-java-with-cache +name: setup-java description: Setup Java toolchain with optional Gradle caching (uses gradlew wrapper) inputs: java-version: diff --git a/.github/actions/utils/setup-node-with-cache/action.yml b/.github/actions/utils/setup-node/action.yml similarity index 68% rename from .github/actions/utils/setup-node-with-cache/action.yml rename to .github/actions/utils/setup-node/action.yml index a46b199e4..4c4767694 100644 --- a/.github/actions/utils/setup-node-with-cache/action.yml +++ b/.github/actions/utils/setup-node/action.yml @@ -15,24 +15,13 @@ # specific language governing permissions and limitations # under the License. -# NOTE: this action sets up the Node.js toolchain + npm cache for dependencies, -# it is a convenience wrapper, so that we can use it in all workflows. - -name: setup-node-with-cache -description: Setup Node.js toolchain and comprehensive caching (npm cache for dependencies) +name: setup-node +description: Setup Node.js toolchain and install dependencies inputs: node-version: description: "Node.js version to use" required: false default: "23" - enabled: - description: "Whether to enable caching" - required: false - default: "true" - cache-dependency-path: - description: "Path to lock file for npm caching" - required: false - default: "package-lock.json" workspace: description: "Workspace directory" required: false @@ -44,25 +33,13 @@ runs: uses: actions/setup-node@v6 with: node-version: ${{ inputs.node-version }} - cache: "npm" - cache-dependency-path: ${{ inputs.cache-dependency-path }} + package-manager-cache: false - name: Pin npm to 11 to match Dependabot lockfile format run: npm install -g npm@11 shell: bash - - name: Setup npm cache - if: inputs.enabled == 'true' - uses: actions/cache@v5 - with: - path: ~/.npm - key: npm-${{ runner.os }}-${{ hashFiles(inputs.cache-dependency-path) }} - restore-keys: | - npm-${{ runner.os }}- - - - name: Install dependencies - if: inputs.enabled == 'true' run: | echo "📦 Installing Node.js dependencies..." if [ "${{ inputs.workspace }}" != "." ]; then diff --git a/.github/actions/utils/setup-rust-with-cache/action.yml b/.github/actions/utils/setup-rust/action.yml similarity index 74% rename from .github/actions/utils/setup-rust-with-cache/action.yml rename to .github/actions/utils/setup-rust/action.yml index 023483300..c4f0a2026 100644 --- a/.github/actions/utils/setup-rust-with-cache/action.yml +++ b/.github/actions/utils/setup-rust/action.yml @@ -15,10 +15,14 @@ # specific language governing permissions and limitations # under the License. -name: setup-rust-with-cache +name: setup-rust description: Setup Rust toolchain with Swatinem/rust-cache inputs: + coverage: + description: "Instrument workspace crates for coverage before compiling" + required: false + default: "false" install-nextest: description: "Whether to install cargo-nextest" required: false @@ -88,15 +92,35 @@ runs: rustup-init -y --no-modify-path --default-toolchain none fi rustup show + # rust-cache hashes every installed toolchain, including unused runner defaults. + active_toolchain=$(rustup show active-toolchain) + active_toolchain=${active_toolchain%% *} + installed_toolchains=$(rustup toolchain list --quiet) + while read -r toolchain; do + if [[ "$toolchain" != "$active_toolchain" ]]; then + rustup toolchain uninstall "$toolchain" + fi + done <<< "$installed_toolchains" echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" shell: bash + - name: Configure Cargo for CI + run: | + echo "CARGO_INCREMENTAL=0" >> "$GITHUB_ENV" + echo "CARGO_PROFILE_DEV_DEBUG=0" >> "$GITHUB_ENV" + echo "CARGO_PROFILE_TEST_DEBUG=0" >> "$GITHUB_ENV" + echo "CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse" >> "$GITHUB_ENV" + shell: bash + + # llvm-cov instruments workspace crates, which rust-cache removes before saving. + # External dependencies can share the cache with check, lint and SDK server builds. - name: Setup Rust cache id: rust-cache if: inputs.read-cache == 'true' uses: Swatinem/rust-cache@v2 with: shared-key: ${{ inputs.shared-key }} + cache-workspace-crates: "false" save-if: ${{ inputs.save-cache == 'true' }} - name: Cache status @@ -123,42 +147,32 @@ runs: shell: bash - name: Install cargo-nextest - if: runner.os == 'Linux' && inputs.install-nextest == 'true' - run: | - # A binary restored from the cargo cache can predate the floor in - # .config/nextest.toml. `show-config version` exits non-zero below it, - # and on a nextest too old to know the subcommand at all, so both fall - # through to a fresh install instead of aborting the lane later. - if command -v cargo-nextest &> /dev/null && cargo nextest show-config version; then - echo "cargo-nextest already installed" - exit 0 - fi - - ARCH="$(uname -m)" - case "$ARCH" in - x86_64) NEXTEST_PLATFORM="linux" ;; - aarch64) NEXTEST_PLATFORM="linux-arm" ;; - *) - echo "Unsupported architecture: $ARCH, skipping nextest" - exit 0 - ;; - esac + if: inputs.install-nextest == 'true' + uses: taiki-e/install-action@v2 + with: + tool: [email protected] - CARGO_BIN="${CARGO_HOME:-$HOME/.cargo}/bin" - tarball="$(mktemp)" - # Staged to a file because curl --retry cannot rewind a pipe. - curl -LsSf --retry 5 --retry-all-errors -o "$tarball" "https://get.nexte.st/latest/${NEXTEST_PLATFORM}" - tar xzf "$tarball" -C "$CARGO_BIN" - rm -f "$tarball" - cargo nextest --version - shell: bash - continue-on-error: true + - name: Install cargo-llvm-cov + if: inputs.coverage == 'true' + uses: taiki-e/install-action@v2 + with: + tool: [email protected] - - name: Configure Cargo for CI - if: inputs.read-cache == 'true' + - name: Enable coverage for subsequent builds and tests + if: inputs.coverage == 'true' run: | - echo "CARGO_INCREMENTAL=0" >> "$GITHUB_ENV" - echo "CARGO_PROFILE_DEV_DEBUG=0" >> "$GITHUB_ENV" - echo "CARGO_PROFILE_TEST_DEBUG=0" >> "$GITHUB_ENV" - echo "CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse" >> "$GITHUB_ENV" + set -euo pipefail + rustup component add llvm-tools-preview + coverage_env=$(cargo llvm-cov show-env --sh) + eval "$coverage_env" + if [[ "${{ inputs.read-cache }}" == true ]]; then + cargo llvm-cov clean --workspace + fi + while IFS= read -r line; do + if [[ "$line" =~ ^export\ ([A-Za-z_][A-Za-z0-9_]*)= ]]; then + name=${BASH_REMATCH[1]} + delimiter="COVERAGE_ENV_${RANDOM}_${RANDOM}" + printf '%s<<%s\n%s\n%s\n' "$name" "$delimiter" "${!name}" "$delimiter" >> "$GITHUB_ENV" + fi + done <<< "$coverage_env" shell: bash diff --git a/.github/actions/utils/validate-third-party-licenses/action.yml b/.github/actions/utils/validate-third-party-licenses/action.yml index 772da695d..ca853e53b 100644 --- a/.github/actions/utils/validate-third-party-licenses/action.yml +++ b/.github/actions/utils/validate-third-party-licenses/action.yml @@ -37,9 +37,10 @@ runs: uses: actions/setup-node@v6 with: node-version: ${{ inputs.node-version }} + package-manager-cache: false - - name: Setup Rust with cache - uses: ./.github/actions/utils/setup-rust-with-cache + - name: Setup Rust + uses: ./.github/actions/utils/setup-rust with: read-cache: "false" diff --git a/.github/config/components.yml b/.github/config/components.yml index b8c5dd036..4714969fe 100644 --- a/.github/config/components.yml +++ b/.github/config/components.yml @@ -144,7 +144,7 @@ components: # existing deployments, which no other check covers. Matching is literal # glob with no crate-graph expansion, so every crate has to be spelled out: # `core/server/**` does not cover `core/server_common/**`. - # `breaking:storage` on the PR skips it (see .github/workflows/_test.yml). + # `breaking:storage` on the PR skips it (see .github/workflows/_rust.yml). rust-storage-compat: depends_on: - "rust-workspace" @@ -212,9 +212,7 @@ components: - "sort" - "doctest" - "machete" - - "test-1" - - "test-2" - - "test-3" + - "test" - "build-aarch64-gnu" - "build-aarch64-musl" - "build-macos-aarch64" diff --git a/.github/workflows/_build_python_wheels.yml b/.github/workflows/_build_python_wheels.yml index 1942a1ba1..af8e43c57 100644 --- a/.github/workflows/_build_python_wheels.yml +++ b/.github/workflows/_build_python_wheels.yml @@ -91,8 +91,8 @@ jobs: id: interpreters run: ./scripts/ci/wheel-interpreters.sh >> "$GITHUB_OUTPUT" - - name: Setup Rust with cache - uses: ./.github/actions/utils/setup-rust-with-cache + - name: Setup Rust + uses: ./.github/actions/utils/setup-rust - name: Generate third-party license manifest run: | @@ -166,8 +166,8 @@ jobs: with: python-version: ${{ steps.interpreters.outputs.python_versions }} - - name: Setup Rust with cache - uses: ./.github/actions/utils/setup-rust-with-cache + - name: Setup Rust + uses: ./.github/actions/utils/setup-rust - name: Generate third-party license manifest run: | @@ -240,8 +240,8 @@ jobs: python-version: ${{ matrix.python-version }} architecture: x64 - - name: Setup Rust with cache - uses: ./.github/actions/utils/setup-rust-with-cache + - name: Setup Rust + uses: ./.github/actions/utils/setup-rust with: shared-key: python-wheels-windows diff --git a/.github/workflows/_build_rust_artifacts.yml b/.github/workflows/_build_rust_artifacts.yml index 5d8715f63..df77bba9c 100644 --- a/.github/workflows/_build_rust_artifacts.yml +++ b/.github/workflows/_build_rust_artifacts.yml @@ -102,8 +102,8 @@ jobs: if: matrix.libc == 'musl' run: ./scripts/ci/apt-install.sh musl-tools - - name: Setup Rust with cache - uses: ./.github/actions/utils/setup-rust-with-cache + - name: Setup Rust + uses: ./.github/actions/utils/setup-rust - name: Add Rust target run: rustup target add ${{ matrix.target }} @@ -112,6 +112,7 @@ jobs: uses: actions/setup-node@v7 with: node-version: "22" + package-manager-cache: false - name: Build Web UI static files run: npm --prefix web ci && npm --prefix web run build:static @@ -213,8 +214,8 @@ jobs: /tmp/copy-latest-from-master.sh save .github scripts /tmp/copy-latest-from-master.sh apply - - name: Setup Rust with cache - uses: ./.github/actions/utils/setup-rust-with-cache + - name: Setup Rust + uses: ./.github/actions/utils/setup-rust - name: Add Rust target run: rustup target add ${{ matrix.target }} diff --git a/.github/workflows/_common.yml b/.github/workflows/_common.yml index 56459044a..e4e247cdc 100644 --- a/.github/workflows/_common.yml +++ b/.github/workflows/_common.yml @@ -151,6 +151,7 @@ jobs: uses: actions/setup-node@v7 with: node-version: "23" + package-manager-cache: false - name: Install markdownlint-cli run: npm install -g markdownlint-cli diff --git a/.github/workflows/_detect.yml b/.github/workflows/_detect.yml index 38902b859..8cd417c1c 100644 --- a/.github/workflows/_detect.yml +++ b/.github/workflows/_detect.yml @@ -23,9 +23,9 @@ on: third_party_licenses: description: "Whether third-party license inputs changed" value: ${{ jobs.detect.outputs.third_party_licenses }} - rust_matrix: - description: "Matrix for Rust components" - value: ${{ jobs.detect.outputs.rust_matrix }} + rust_plan: + description: "Selected Rust checks and tests; empty when Rust is unaffected" + value: ${{ jobs.detect.outputs.rust_plan }} python_matrix: description: "Matrix for Python SDK" value: ${{ jobs.detect.outputs.python_matrix }} @@ -69,7 +69,7 @@ jobs: timeout-minutes: 10 outputs: third_party_licenses: ${{ github.event_name != 'pull_request' || steps.changed.outputs.third_party_licenses == 'true' }} - rust_matrix: ${{ steps.mk.outputs.rust_matrix }} + rust_plan: ${{ steps.mk.outputs.rust_plan }} python_matrix: ${{ steps.mk.outputs.python_matrix }} php_matrix: ${{ steps.mk.outputs.php_matrix }} node_matrix: ${{ steps.mk.outputs.node_matrix }} @@ -111,7 +111,7 @@ jobs: - 'scripts/ci/render-node-licenses.mjs' - 'scripts/ci/apt-install.sh' - '.github/actions/utils/validate-third-party-licenses/**' - - '.github/actions/utils/setup-rust-with-cache/**' + - '.github/actions/utils/setup-rust/**' - '.github/actions/utils/free-disk-space/**' - '.github/workflows/_detect.yml' - '.github/workflows/_common.yml' @@ -290,7 +290,7 @@ jobs: const entries = cfg.tasks.map(task => ({ component: name, task })); taskedComponents.push({ name, tasks: cfg.tasks, trigger }); - if (name === 'rust') groups.rust.push(...entries); + if (name === 'rust' || name.startsWith('rust-')) groups.rust.push(...entries); else if (name === 'sdk-python') groups.python.push(...entries); else if (name === 'sdk-php') groups.php.push(...entries); else if (name === 'sdk-node') groups.node.push(...entries); @@ -354,7 +354,7 @@ jobs: for (const [name, cfg] of Object.entries(components)) { if (!cfg.tasks || cfg.tasks.length === 0) continue; const entries = cfg.tasks.map(task => ({ component: name, task })); - if (name === 'rust') groups.rust.push(...entries); + if (name === 'rust' || name.startsWith('rust-')) groups.rust.push(...entries); else if (name === 'sdk-python') groups.python.push(...entries); else if (name === 'sdk-php') groups.php.push(...entries); else if (name === 'sdk-node') groups.node.push(...entries); @@ -426,7 +426,10 @@ jobs: require('fs').appendFileSync(process.env.GITHUB_OUTPUT, `${output}\n`); }; - setOutput('rust_matrix', JSON.stringify(matrix(groups.rust))); + setOutput('rust_plan', groups.rust.length ? JSON.stringify({ + checks: groups.rust.filter(entry => entry.task !== 'test'), + tests: groups.rust.some(entry => entry.task === 'test'), + }) : ''); setOutput('python_matrix', JSON.stringify(matrix(groups.python))); setOutput('php_matrix', JSON.stringify(matrix(groups.php))); setOutput('node_matrix', JSON.stringify(matrix(groups.node))); diff --git a/.github/workflows/_publish_rust_crates.yml b/.github/workflows/_publish_rust_crates.yml index ae2a8d739..2f268231d 100644 --- a/.github/workflows/_publish_rust_crates.yml +++ b/.github/workflows/_publish_rust_crates.yml @@ -112,8 +112,8 @@ jobs: /tmp/copy-latest-from-master.sh apply - - name: Setup Rust with cache - uses: ./.github/actions/utils/setup-rust-with-cache + - name: Setup Rust + uses: ./.github/actions/utils/setup-rust # Normalize the inbound commit to a canonical full SHA. Caller (publish.yml) # already does this; a direct workflow_call caller must pass an explicit diff --git a/.github/workflows/_rust.yml b/.github/workflows/_rust.yml new file mode 100644 index 000000000..7d5458b72 --- /dev/null +++ b/.github/workflows/_rust.yml @@ -0,0 +1,196 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: _rust +on: + workflow_call: + inputs: + plan: + description: "JSON with checks (component/task entries) and tests (boolean)" + type: string + required: true + test-scope: + description: "Test selection: affected or workspace" + type: string + default: affected + timeout-minutes: + type: number + default: 60 + save-cache: + description: "Save dependencies from the test build for subsequent PRs" + type: boolean + default: false + build-metadata: + description: "Embed CI build metadata in the server binaries" + type: boolean + default: false + secrets: + CODECOV_TOKEN: + required: false + +permissions: + contents: read + issues: read # live breaking:storage label lookup + +env: + IGGY_CI_BUILD: ${{ inputs.build-metadata }} + +jobs: + checks: + name: ${{ matrix.component }}/${{ matrix.task }} + if: ${{ fromJSON(inputs.plan).checks[0] != null }} + strategy: + fail-fast: true + matrix: + include: ${{ fromJSON(inputs.plan).checks }} + runs-on: ${{ + (matrix.task == 'build-aarch64-gnu' || matrix.task == 'build-aarch64-musl') && 'ubuntu-26.04-arm' || + (matrix.task == 'build-macos-aarch64' || matrix.task == 'clippy-macos-aarch64') && 'macos-14' || + matrix.task == 'build-windows-sdk' && 'windows-latest' || + 'ubuntu-26.04' + }} + timeout-minutes: ${{ inputs.timeout-minutes }} + steps: + - uses: actions/[email protected] + + # Re-runs replay the original event labels. Read live labels so applying + # breaking:storage followed by a re-run can skip an intentional format break. + - name: Resolve breaking:storage escape hatch + id: storage_hatch + if: matrix.task == 'test-storage-compat' && github.event.pull_request.number + uses: actions/github-script@v9 + with: + script: | + let skip = false; + try { + const labels = await github.paginate( + github.rest.issues.listLabelsOnIssue, + { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + per_page: 100, + }, + ); + skip = labels.some(l => l.name === 'breaking:storage'); + } catch (e) { + core.warning(`listLabelsOnIssue failed (${e.status ?? e.code ?? 'network'}), running the check`); + } + core.info(`breaking:storage escape hatch: skip=${skip}`); + core.setOutput('skip', String(skip)); + + - name: Run Rust check + if: steps.storage_hatch.outputs.skip != 'true' + uses: ./.github/actions/rust/pre-merge + with: + task: ${{ matrix.task }} + component: ${{ matrix.component }} + + - name: Upload reports + if: always() + uses: actions/upload-artifact@v7 + with: + name: ${{ matrix.component }}-${{ matrix.task }}-reports-${{ github.run_id }}-${{ github.run_attempt }} + path: reports/** + if-no-files-found: ignore + retention-days: 7 + + test-build: + name: Build test artifacts + if: ${{ fromJSON(inputs.plan).tests }} + runs-on: ubuntu-26.04 + timeout-minutes: ${{ inputs.timeout-minutes }} + steps: + - uses: actions/[email protected] + + - name: Build coverage test archive + uses: ./.github/actions/rust/pre-merge + with: + task: test-build + test-scope: ${{ inputs.test-scope }} + save-cache: ${{ inputs.save-cache }} + free-disk-space-aggressive: ${{ inputs.test-scope == 'workspace' }} + + - name: Upload test artifacts + uses: actions/upload-artifact@v7 + with: + name: rust-test-build-${{ github.run_id }} + path: ${{ runner.temp }}/rust-test-build/ + compression-level: 0 + if-no-files-found: error + overwrite: true + retention-days: 7 + + tests: + name: Test shard ${{ matrix.partition }}/${{ strategy.job-total }} + needs: test-build + strategy: + fail-fast: false + matrix: + partition: [1, 2, 3] + runs-on: ubuntu-26.04 + timeout-minutes: ${{ inputs.timeout-minutes }} + steps: + - uses: actions/[email protected] + + - name: Download test artifacts + uses: actions/download-artifact@v8 + with: + name: rust-test-build-${{ github.run_id }} + path: ${{ runner.temp }}/rust-test-build + + - name: Run test shard + uses: ./.github/actions/rust/pre-merge + with: + task: test + partition: ${{ matrix.partition }} + partitions: ${{ strategy.job-total }} + + - name: Generate coverage report + if: always() && !cancelled() + run: | + if compgen -G 'target/*.profraw' > /dev/null; then + cargo llvm-cov report --codecov --output-path codecov.json + else + echo "::notice::No coverage profiles found; skipping report" + fi + shell: bash + + - name: Upload coverage to Codecov + if: always() && !cancelled() && hashFiles('codecov.json') != '' + uses: codecov/[email protected] + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: codecov.json + disable_search: true + flags: rust + fail_ci_if_error: false + verbose: true + override_pr: ${{ github.event.pull_request.number }} + + - name: Upload reports + if: always() + uses: actions/upload-artifact@v7 + with: + name: rust-test-${{ matrix.partition }}-reports-${{ github.run_id }}-${{ github.run_attempt }} + path: | + reports/** + target/llvm-cov/** + coverage.lcov + codecov.json + if-no-files-found: ignore + retention-days: 7 diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index 7fb5ff909..0d24d4da2 100644 --- a/.github/workflows/_test.yml +++ b/.github/workflows/_test.yml @@ -33,7 +33,6 @@ on: permissions: contents: read - issues: read # listLabelsOnIssue, for the breaking:storage escape hatch security-events: write pull-requests: write @@ -41,10 +40,7 @@ jobs: run: # Select runner based on build target runs-on: ${{ - (inputs.task == 'build-aarch64-gnu' || inputs.task == 'build-aarch64-musl') && 'ubuntu-26.04-arm' || - (inputs.task == 'build-macos-aarch64' || inputs.task == 'clippy-macos-aarch64') && 'macos-14' || inputs.task == 'build-macos' && 'macos-15' || - inputs.task == 'build-windows-sdk' && 'windows-latest' || 'ubuntu-26.04' }} timeout-minutes: 60 @@ -56,75 +52,6 @@ jobs: if: inputs.component == 'noop' run: echo "No changes detected, skipping tests" - # Rust - # `breaking:storage` is the documented escape hatch for a deliberate - # on-disk format change, which the compat check cannot pass by - # construction. The label has to be read live: pre-merge.yml does not - # subscribe to `labeled` (pr-triage-apply.yml writes S-* labels, so every - # triage write would re-run the whole gate), and a re-run replays the - # webhook-frozen `pull_request.labels` from the last push. The operator - # flow is "check fails, apply label, re-run", which the frozen payload - # cannot see. Paginated so a PR with more than 100 labels still resolves. - - name: Resolve breaking:storage escape hatch - id: storage_hatch - if: >- - startsWith(inputs.component, 'rust') && - inputs.task == 'test-storage-compat' && - github.event.pull_request.number - uses: actions/github-script@v9 - with: - script: | - let skip = false; - try { - const labels = await github.paginate( - github.rest.issues.listLabelsOnIssue, - { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - per_page: 100, - }, - ); - skip = labels.some(l => l.name === 'breaking:storage'); - } catch (e) { - // Fail toward running. A rate limit or a narrowed token must not - // wave an on-disk format break through as a green check. - core.warning(`listLabelsOnIssue failed (${e.status ?? e.code ?? 'network'}), running the check`); - } - core.info(`breaking:storage escape hatch: skip=${skip}`); - core.setOutput('skip', String(skip)); - - - name: Run Rust task - # Skipping the step (not the job) keeps the leg green so the pre-merge - # status roll-up stays reportable instead of pending. - if: >- - startsWith(inputs.component, 'rust') && - steps.storage_hatch.outputs.skip != 'true' - uses: ./.github/actions/rust/pre-merge - with: - task: ${{ inputs.task }} - component: ${{ inputs.component }} - - - name: Upload coverage to Codecov - # test-storage-compat runs its own nextest invocation without llvm-cov, - # so it emits no codecov.json. Uploading an empty report under the - # `rust` flag would read as a coverage drop on the PR. A test task that - # Cargo-Rail found no affected tests for writes no report either. - if: >- - startsWith(inputs.component, 'rust') && - startsWith(inputs.task, 'test-') && - inputs.task != 'test-storage-compat' && - hashFiles('codecov.json') != '' - uses: codecov/[email protected] - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: codecov.json - disable_search: true - flags: rust - fail_ci_if_error: false - verbose: true - override_pr: ${{ github.event.pull_request.number }} - # Python SDK - name: Set up Docker Buildx for Python if: inputs.component == 'sdk-python' && inputs.task == 'test' @@ -283,8 +210,7 @@ jobs: uses: actions/setup-node@v7 with: node-version: "24" - cache: npm - cache-dependency-path: web/package-lock.json + package-manager-cache: false - name: Run Web UI task if: inputs.component == 'web-ui' diff --git a/.github/workflows/_test_bdd.yml b/.github/workflows/_test_bdd.yml index 945f58054..0e713c05d 100644 --- a/.github/workflows/_test_bdd.yml +++ b/.github/workflows/_test_bdd.yml @@ -27,6 +27,14 @@ on: type: string required: true description: "Task to run (e.g., 'bdd-rust', 'bdd-python', etc.)" + timeout-minutes: + type: number + default: 60 + description: "Job timeout in minutes" + + secrets: + CODECOV_TOKEN: + required: false permissions: contents: read @@ -35,7 +43,7 @@ permissions: jobs: run: runs-on: ubuntu-26.04 - timeout-minutes: 60 + timeout-minutes: ${{ inputs.timeout-minutes }} steps: - name: Checkout code uses: actions/[email protected] @@ -44,9 +52,11 @@ jobs: if: inputs.component == 'noop' run: echo "No changes detected, skipping tests" - - name: Setup Rust with cache for BDD + - name: Setup Rust for BDD if: startsWith(inputs.component, 'bdd-') && startsWith(inputs.task, 'bdd-') - uses: ./.github/actions/utils/setup-rust-with-cache + uses: ./.github/actions/utils/setup-rust + with: + coverage: ${{ inputs.task == 'bdd-rust' }} - name: Build server for BDD tests if: startsWith(inputs.component, 'bdd-') && startsWith(inputs.task, 'bdd-') @@ -88,7 +98,11 @@ jobs: export IGGY_CLI_PATH="target/debug/iggy" export IGGY_ROOT_USERNAME="iggy" export IGGY_ROOT_PASSWORD="iggy" - ./scripts/run-bdd-tests.sh "$SDK_NAME" + COVERAGE_ARGS=() + if [[ "$SDK_NAME" == rust ]]; then + COVERAGE_ARGS=(--coverage) + fi + ./scripts/run-bdd-tests.sh "${COVERAGE_ARGS[@]}" "$SDK_NAME" - name: Clean up Docker resources (BDD) if: always() && startsWith(inputs.component, 'bdd-') && startsWith(inputs.task, 'bdd-') @@ -96,6 +110,26 @@ jobs: ./scripts/run-bdd-tests.sh clean || true docker system prune -f || true + - name: Generate Rust coverage report + if: always() && !cancelled() && inputs.task == 'bdd-rust' + run: | + if compgen -G 'target/*.profraw' > /dev/null; then + cargo llvm-cov report --codecov --output-path codecov.json + else + echo "::notice::No coverage profiles found; skipping report" + fi + + - name: Upload Rust coverage to Codecov + if: always() && !cancelled() && inputs.task == 'bdd-rust' && hashFiles('codecov.json', 'reports/rust-bdd-coverage.lcov') != '' + uses: codecov/[email protected] + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: codecov.json,reports/rust-bdd-coverage.lcov + disable_search: true + flags: rust-bdd + fail_ci_if_error: false + override_pr: ${{ github.event.pull_request.number }} + - name: Upload reports if: always() uses: actions/upload-artifact@v7 @@ -105,5 +139,6 @@ jobs: reports/** target/llvm-cov/** coverage.lcov + codecov.json if-no-files-found: ignore retention-days: 7 diff --git a/.github/workflows/_test_examples.yml b/.github/workflows/_test_examples.yml index 024961914..6b3de5130 100644 --- a/.github/workflows/_test_examples.yml +++ b/.github/workflows/_test_examples.yml @@ -27,6 +27,7 @@ on: type: string required: true description: "Task to run (e.g., 'examples-rust', 'examples-go', 'examples-node')" + permissions: contents: read security-events: write @@ -43,28 +44,17 @@ jobs: if: inputs.component == 'noop' run: echo "No changes detected, skipping tests" - - name: Setup Rust with cache for examples + - name: Setup Rust for examples if: startsWith(inputs.component, 'examples-') - uses: ./.github/actions/utils/setup-rust-with-cache + uses: ./.github/actions/utils/setup-rust - - name: Setup Node with cache for examples + - name: Setup Node for examples if: startsWith(inputs.component, 'examples-') && inputs.task == 'examples-node' - uses: ./.github/actions/utils/setup-node-with-cache + uses: ./.github/actions/utils/setup-node with: node-version: "23" - cache-dependency-path: examples/node/package-lock.json workspace: examples/node - - name: Setup Python - if: startsWith(inputs.component, 'examples-') && inputs.task == 'examples-python' - uses: actions/[email protected] - with: - python-version: "3.11" - - - name: Setup uv - if: startsWith(inputs.component, 'examples-') && inputs.task == 'examples-python' - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 - - name: Install PHP build dependencies if: startsWith(inputs.component, 'examples-') && inputs.task == 'examples-php' shell: bash @@ -91,22 +81,15 @@ jobs: "${php_bin}" --version "${php_config}" --version - - name: Cache uv - if: startsWith(inputs.component, 'examples-') && inputs.task == 'examples-python' - uses: actions/[email protected] - with: - path: ~/.cache/uv - key: uv-${{ runner.os }}-${{ hashFiles('examples/python/uv.lock', 'foreign/python/uv.lock', 'bdd/python/uv.lock') }} - - - name: Setup Java with cache for examples + - name: Setup Java for examples if: startsWith(inputs.component, 'examples-') && inputs.task == 'examples-java' - uses: ./.github/actions/utils/setup-java-with-cache + uses: ./.github/actions/utils/setup-java with: gradle-cache-disabled: "true" - name: Setup Go for examples if: startsWith(inputs.component, 'examples-') && inputs.task == 'examples-go' - uses: ./.github/actions/utils/setup-go-with-cache + uses: ./.github/actions/utils/setup-go with: download-deps: "false" @@ -130,12 +113,11 @@ jobs: # against the VSR server. SERVER_BIN="iggy-server" echo "Building ${SERVER_BIN}..." - cargo build --locked --bin "${SERVER_BIN}" - + BUILD_ARGS=(--bin "$SERVER_BIN") if [[ "${{ inputs.task }}" == "examples-rust" ]]; then - echo "Building additional binaries for Rust examples..." - cargo build --locked --bin iggy --examples + BUILD_ARGS+=(--bin iggy --examples) fi + cargo build --locked "${BUILD_ARGS[@]}" # Verify server binary was built (needed by all examples) echo "Verifying server binary:" @@ -160,6 +142,24 @@ jobs: fi fi + # Keep Python's PKG_CONFIG_PATH out of the server build above. + - name: Setup Python + if: startsWith(inputs.component, 'examples-') && inputs.task == 'examples-python' + uses: actions/[email protected] + with: + python-version: "3.11" + + - name: Setup uv + if: startsWith(inputs.component, 'examples-') && inputs.task == 'examples-python' + uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 + + - name: Cache uv + if: startsWith(inputs.component, 'examples-') && inputs.task == 'examples-python' + uses: actions/[email protected] + with: + path: ~/.cache/uv + key: uv-${{ runner.os }}-${{ hashFiles('examples/python/uv.lock', 'foreign/python/uv.lock', 'bdd/python/uv.lock') }} + - name: Build PHP extension for examples if: startsWith(inputs.component, 'examples-') && inputs.task == 'examples-php' shell: bash @@ -218,9 +218,6 @@ jobs: uses: actions/upload-artifact@v7 with: name: ${{ inputs.component }}-${{ inputs.task }}-reports-${{ github.run_id }}-${{ github.run_attempt }} - path: | - reports/** - target/llvm-cov/** - coverage.lcov + path: reports/** if-no-files-found: ignore retention-days: 7 diff --git a/.github/workflows/coverage-baseline.yml b/.github/workflows/coverage-baseline.yml index 5cd48bef4..78962fc16 100644 --- a/.github/workflows/coverage-baseline.yml +++ b/.github/workflows/coverage-baseline.yml @@ -83,87 +83,48 @@ jobs: name: Rust coverage baseline needs: detect if: needs.detect.outputs.rust == 'true' - runs-on: ubuntu-26.04 - timeout-minutes: 45 - steps: - - uses: actions/[email protected] - - - name: Install system dependencies - run: | - ./scripts/ci/apt-install.sh gnome-keyring keyutils dbus-x11 libsecret-tools - rm -f "$HOME"/.local/share/keyrings/* - - - name: Setup Rust with cache - uses: ./.github/actions/utils/setup-rust-with-cache - with: - install-nextest: "true" - # Also warms the GitHub Actions build cache for subsequent PR builds - save-cache: "true" - # llvm-cov instrumentation roughly doubles object sizes; the light - # cleanup leaves too little headroom (build hit "No space left on - # device" at ~34 MB free). - free-disk-space-aggressive: "true" - - - name: Install cargo-llvm-cov - uses: taiki-e/[email protected] - with: - tool: cargo-llvm-cov - - # Doris 4.0.3's start_be.sh hard-`exit 1`s unless vm.max_map_count >= 2000000. - # Host-only kernel param (no container can raise it). Mirrors the same step in - # .github/actions/rust/pre-merge/action.yml; without it every Doris connector - # integration test panics at fixture setup. - - name: Raise vm.max_map_count for Doris - run: sudo sysctl -w vm.max_map_count=2000000 || true - shell: bash - - - name: Build and test with coverage - run: | - source <(cargo llvm-cov show-env --export-prefix) - cargo build --locked - cargo test --locked --no-run - - # Start D-Bus and unlock keyring right before test execution to avoid - # gnome-keyring auto-locking the collection during the build phase. - # Previously this ran before `cargo build`, leaving a 7+ minute idle - # window that triggered org.freedesktop.Secret.Error.IsLocked ~10% of runs. - eval $(dbus-launch --sh-syntax) - export DBUS_SESSION_BUS_ADDRESS - eval $(echo -n "test" | gnome-keyring-daemon --unlock --components=secrets) - echo -n "warmup" | secret-tool store --label="ci-warmup" ci-test warmup - - cargo nextest run --locked --no-fail-fast --profile ci - shell: bash - - - name: Generate coverage report - run: | - source <(cargo llvm-cov show-env --export-prefix) - cargo llvm-cov report --codecov --output-path codecov.json - shell: bash - - - name: Upload to Codecov - uses: codecov/[email protected] - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: codecov.json - disable_search: true - flags: rust - fail_ci_if_error: false + permissions: + contents: read + issues: read + uses: ./.github/workflows/_rust.yml + with: + plan: '{"checks":[],"tests":true}' + test-scope: workspace + timeout-minutes: 120 + save-cache: true + build-metadata: true + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + rust-bdd-coverage: + name: Rust BDD coverage baseline + needs: detect + if: needs.detect.outputs.rust == 'true' + permissions: + contents: read + security-events: write + uses: ./.github/workflows/_test_bdd.yml + with: + component: bdd-rust + task: bdd-rust + timeout-minutes: 120 + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} java-coverage: name: Java coverage baseline needs: detect if: needs.detect.outputs.java == 'true' runs-on: ubuntu-26.04 - timeout-minutes: 30 + timeout-minutes: 120 steps: - uses: actions/[email protected] - - name: Setup Java with cache - uses: ./.github/actions/utils/setup-java-with-cache + - name: Setup Java + uses: ./.github/actions/utils/setup-java - - name: Setup Rust with cache - uses: ./.github/actions/utils/setup-rust-with-cache + - name: Setup Rust + uses: ./.github/actions/utils/setup-rust with: save-cache: "false" @@ -200,7 +161,7 @@ jobs: needs: detect if: needs.detect.outputs.csharp == 'true' runs-on: ubuntu-26.04 - timeout-minutes: 30 + timeout-minutes: 120 strategy: fail-fast: false matrix: @@ -215,8 +176,8 @@ jobs: 8.0.x 10.0.x - - name: Setup Rust with cache - uses: ./.github/actions/utils/setup-rust-with-cache + - name: Setup Rust + uses: ./.github/actions/utils/setup-rust with: save-cache: "false" @@ -285,20 +246,41 @@ jobs: needs: detect if: needs.detect.outputs.python == 'true' runs-on: ubuntu-26.04 - timeout-minutes: 30 + timeout-minutes: 120 steps: - uses: actions/[email protected] + - name: Setup Rust + uses: ./.github/actions/utils/setup-rust + with: + save-cache: "false" + + # setup-python changes PKG_CONFIG_PATH and invalidates cached native server dependencies. + - name: Build server Docker image for TLS tests + run: | + # test_tls.py spawns the server in a container; build it from the + # same binaries the plain-TCP tests run against. + cargo build --locked --bin iggy-server --bin iggy + docker build \ + -f core/server/Dockerfile \ + --target runtime-prebuilt \ + --build-arg PREBUILT_IGGY_SERVER=target/debug/iggy-server \ + --build-arg PREBUILT_IGGY_CLI=target/debug/iggy \ + -t iggy-server:local . + shell: bash + + - name: Start Iggy server + id: iggy + uses: ./.github/actions/utils/server-start + with: + mode: bin + bin: target/debug/iggy-server + - name: Setup Python uses: actions/[email protected] with: python-version: "3.11" - - name: Setup Rust with cache - uses: ./.github/actions/utils/setup-rust-with-cache - with: - save-cache: "false" - - name: Install cargo-llvm-cov uses: taiki-e/[email protected] with: @@ -326,25 +308,6 @@ jobs: uv run --no-sync maturin develop shell: bash - - name: Build server Docker image for TLS tests - run: | - # test_tls.py spawns the server in a container; build it from the - # same binaries the plain-TCP tests run against. - cargo build --locked --bin iggy-server --bin iggy - docker build \ - -f core/server/Dockerfile \ - --target runtime-prebuilt \ - --build-arg PREBUILT_IGGY_SERVER=target/debug/iggy-server \ - --build-arg PREBUILT_IGGY_CLI=target/debug/iggy \ - -t iggy-server:local . - shell: bash - - - name: Start Iggy server - id: iggy - uses: ./.github/actions/utils/server-start - with: - cargo-bin: iggy-server - - name: Run tests run: | cd foreign/python @@ -409,7 +372,7 @@ jobs: needs: detect if: needs.detect.outputs.php == 'true' runs-on: ubuntu-26.04 - timeout-minutes: 30 + timeout-minutes: 120 steps: - uses: actions/[email protected] @@ -432,7 +395,7 @@ jobs: needs: detect if: needs.detect.outputs.node == 'true' runs-on: ubuntu-26.04 - timeout-minutes: 30 + timeout-minutes: 120 steps: - uses: actions/[email protected] @@ -440,14 +403,13 @@ jobs: uses: actions/setup-node@v7 with: node-version: "23" - cache: "npm" - cache-dependency-path: foreign/node/package-lock.json + package-manager-cache: false - name: Pin npm to 11 to match Dependabot lockfile format run: npm install -g npm@11 - - name: Setup Rust with cache - uses: ./.github/actions/utils/setup-rust-with-cache + - name: Setup Rust + uses: ./.github/actions/utils/setup-rust with: save-cache: "false" @@ -499,15 +461,15 @@ jobs: needs: detect if: needs.detect.outputs.go == 'true' runs-on: ubuntu-26.04 - timeout-minutes: 30 + timeout-minutes: 120 steps: - uses: actions/[email protected] - name: Setup Go - uses: ./.github/actions/utils/setup-go-with-cache + uses: ./.github/actions/utils/setup-go - - name: Setup Rust with cache - uses: ./.github/actions/utils/setup-rust-with-cache + - name: Setup Rust + uses: ./.github/actions/utils/setup-rust with: save-cache: "false" @@ -587,7 +549,7 @@ jobs: needs: detect if: needs.detect.outputs.cpp == 'true' runs-on: ubuntu-26.04 - timeout-minutes: 45 + timeout-minutes: 120 steps: - uses: actions/[email protected] @@ -608,17 +570,14 @@ jobs: warm-cache-macos: name: Warm macOS cache needs: detect - # Gated on the rust key: the cache goes stale only when Rust sources - # change, and the Swatinem key is Cargo.lock-based, so skipping the save - # on non-Rust pushes leaves the previous cache valid. if: needs.detect.outputs.rust == 'true' runs-on: macos-14 - timeout-minutes: 30 + timeout-minutes: 120 steps: - uses: actions/[email protected] - - name: Setup Rust with cache - uses: ./.github/actions/utils/setup-rust-with-cache + - name: Setup Rust + uses: ./.github/actions/utils/setup-rust with: save-cache: "true" diff --git a/.github/workflows/pr-skill-review-run.yml b/.github/workflows/pr-skill-review-run.yml index 28d48c327..96286c04a 100644 --- a/.github/workflows/pr-skill-review-run.yml +++ b/.github/workflows/pr-skill-review-run.yml @@ -231,6 +231,8 @@ jobs: - name: Setup Node if: steps.parse.outputs.status == 'ok' uses: actions/setup-node@v7 + with: + package-manager-cache: false - name: Install the agent if: steps.parse.outputs.status == 'ok' diff --git a/.github/workflows/pre-merge.yml b/.github/workflows/pre-merge.yml index 24c0017f1..bcb96cf60 100644 --- a/.github/workflows/pre-merge.yml +++ b/.github/workflows/pre-merge.yml @@ -55,16 +55,12 @@ jobs: # Rust components test-rust: - name: Rust • ${{ matrix.component }}/${{ matrix.task }} + name: Rust needs: detect - if: ${{ fromJson(needs.detect.outputs.rust_matrix).include[0].component != 'noop' }} - strategy: - fail-fast: true - matrix: ${{ fromJson(needs.detect.outputs.rust_matrix) }} - uses: ./.github/workflows/_test.yml + if: needs.detect.outputs.rust_plan != '' + uses: ./.github/workflows/_rust.yml with: - component: ${{ matrix.component }} - task: ${{ matrix.task }} + plan: ${{ needs.detect.outputs.rust_plan }} secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} @@ -211,6 +207,8 @@ jobs: with: component: ${{ matrix.component }} task: ${{ matrix.task }} + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} # Examples Tests test-examples: @@ -237,10 +235,11 @@ jobs: uses: actions/github-script@v9 with: script: | - const jobs = await github.rest.actions.listJobsForWorkflowRun({ + const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, { owner: context.repo.owner, repo: context.repo.repo, - run_id: context.runId + run_id: context.runId, + per_page: 100 }); const jobTimes = {}; @@ -263,7 +262,7 @@ jobs: // Log job names for debugging console.log('Job names found:'); - for (const job of jobs.data.jobs) { + for (const job of jobs) { console.log(` - ${job.name}: ${job.status} (conclusion: ${job.conclusion || 'N/A'})`); jobStatuses[job.name] = job.conclusion || job.status; @@ -305,7 +304,6 @@ jobs: }; // Set outputs for each component - const rust = findJobInfo('Rust •'); const python = findJobInfo('Python •'); const php = findJobInfo('PHP •'); const node = findJobInfo('Node •'); @@ -327,7 +325,13 @@ jobs: : { time: null, status: 'skipped' }; // Output formatted durations - core.setOutput('rust_time', formatJobDuration(rust)); + const rustJobs = jobs.filter(job => job.name.startsWith('Rust /') && + job.conclusion !== 'skipped' && job.started_at && job.completed_at); + const rustDuration = rustJobs.length ? formatDuration( + Math.max(...rustJobs.map(job => new Date(job.completed_at).getTime())) - + Math.min(...rustJobs.map(job => new Date(job.started_at).getTime())) + ) : '-'; + core.setOutput('rust_time', rustDuration); core.setOutput('python_time', formatJobDuration(python)); core.setOutput('php_time', formatJobDuration(php)); core.setOutput('node_time', formatJobDuration(node)); @@ -345,7 +349,7 @@ jobs: // Calculate total time - find the earliest job start time let earliestStart = null; let latestEnd = null; - for (const job of jobs.data.jobs) { + for (const job of jobs) { if (job.started_at) { const start = new Date(job.started_at); if (!earliestStart || start < earliestStart) { diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 425f2b91e..15705dfaa 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1040,7 +1040,7 @@ jobs: # need the toolchain. - name: Setup Rust toolchain (if needed) if: matrix.type == 'python' - uses: ./.github/actions/utils/setup-rust-with-cache + uses: ./.github/actions/utils/setup-rust - name: Debug matrix run: echo '${{ toJson(matrix) }}' diff --git a/bdd/docker-compose.cluster.yml b/bdd/docker-compose.cluster.yml index fb9b02954..0ccc3a676 100644 --- a/bdd/docker-compose.cluster.yml +++ b/bdd/docker-compose.cluster.yml @@ -52,6 +52,7 @@ x-cluster-node: &cluster-node hard: -1 x-cluster-topology: &cluster-topology + LLVM_PROFILE_FILE: ${BDD_SERVER_PROFILE_FILE:-} IGGY_ROOT_USERNAME: iggy IGGY_ROOT_PASSWORD: iggy IGGY_CLUSTER_ENABLED: "true" @@ -116,6 +117,7 @@ services: ipv4_address: 172.28.0.101 volumes: - iggy_leader_data:/app/local_data_leader + - ../target:/profiles iggy-follower: <<: *cluster-node @@ -142,6 +144,7 @@ services: ipv4_address: 172.28.0.102 volumes: - iggy_follower_data:/app/local_data_follower + - ../target:/profiles rust-bdd: <<: *cluster-bdd-deps diff --git a/bdd/docker-compose.coverage.yml b/bdd/docker-compose.coverage.yml index 79ea67d3b..e1c58fea8 100644 --- a/bdd/docker-compose.coverage.yml +++ b/bdd/docker-compose.coverage.yml @@ -23,20 +23,25 @@ services: rust-bdd: + build: + args: + COVERAGE: "true" volumes: - ../reports:/reports command: - - sh + - bash + - -euo + - pipefail - -c - | - rustup component add llvm-tools-preview \ - && cargo install cargo-llvm-cov \ - && case "$$BDD_FEATURE" in - basic_messaging) cargo llvm-cov test -p bdd --features "$$BDD_RUST_FEATURES" --test basic_messaging --lcov --output-path /reports/rust-bdd-coverage.lcov ;; - leader_redirection) cargo llvm-cov test -p bdd --features "$$BDD_RUST_FEATURES" --test leader_redirection --lcov --output-path /reports/rust-bdd-coverage.lcov ;; - stream_crud) cargo llvm-cov test -p bdd --features "$$BDD_RUST_FEATURES" --test stream_crud --lcov --output-path /reports/rust-bdd-coverage.lcov ;; - *) cargo llvm-cov test -p bdd --features "$$BDD_RUST_FEATURES" --lcov --output-path /reports/rust-bdd-coverage.lcov ;; + coverage_env=$$(cargo llvm-cov show-env --sh --remap-path-prefix) + eval "$$coverage_env" + trap 'cargo llvm-cov report --lcov --output-path /reports/rust-bdd-coverage.lcov' EXIT + TEST_ARGS=() + case "$$BDD_FEATURE" in + basic_messaging|leader_redirection|raw_command|stream_crud) TEST_ARGS=(--test "$$BDD_FEATURE") ;; esac + cargo test --locked -p bdd --features "$$BDD_RUST_FEATURES" "$${TEST_ARGS[@]}" python-bdd: volumes: diff --git a/bdd/docker-compose.server.yml b/bdd/docker-compose.server.yml index 88868ace3..dcda9539e 100644 --- a/bdd/docker-compose.server.yml +++ b/bdd/docker-compose.server.yml @@ -60,6 +60,7 @@ services: retries: 30 start_period: 2s environment: + - LLVM_PROFILE_FILE=${BDD_SERVER_PROFILE_FILE:-} - RUST_LOG=info - IGGY_ROOT_USERNAME=iggy - IGGY_ROOT_PASSWORD=iggy @@ -71,6 +72,7 @@ services: - IGGY_WEBSOCKET_ADDRESS=0.0.0.0:8070 volumes: - iggy_data:/app/local_data + - ../target:/profiles networks: - iggy-bdd-network diff --git a/bdd/docker-compose.yml b/bdd/docker-compose.yml index 01dee9292..37a460ca8 100644 --- a/bdd/docker-compose.yml +++ b/bdd/docker-compose.yml @@ -21,6 +21,8 @@ services: build: context: .. dockerfile: bdd/rust/Dockerfile + args: + BDD_RUST_FEATURES: ${BDD_RUST_FEATURES:-bdd} environment: - IGGY_ROOT_USERNAME=iggy - IGGY_ROOT_PASSWORD=iggy diff --git a/bdd/rust/Dockerfile b/bdd/rust/Dockerfile index ff967433d..1ffa83ea2 100644 --- a/bdd/rust/Dockerfile +++ b/bdd/rust/Dockerfile @@ -18,22 +18,44 @@ ARG RUST_VERSION=1.99 FROM rust:${RUST_VERSION}-slim-trixie +ARG COVERAGE=false +ARG BDD_RUST_FEATURES=bdd +ARG LLVM_COV_VERSION=0.9.1 + +ENV RUSTFLAGS="-C linker=gcc" \ + CARGO_INCREMENTAL=0 \ + CARGO_PROFILE_DEV_DEBUG=0 \ + CARGO_PROFILE_TEST_DEBUG=0 + +SHELL ["/bin/bash", "-euo", "pipefail", "-c"] WORKDIR /app RUN apt-get update && apt-get install -y \ + ca-certificates \ + curl \ libhwloc-dev \ libudev-dev \ pkg-config \ && rm -rf /var/lib/apt/lists/* -# Copy everything -COPY . . +RUN if [[ "$COVERAGE" == true ]]; then \ + rustup component add llvm-tools-preview; \ + host=$(rustc -vV | sed -n 's/^host: //p'); \ + curl -fsSL --retry 5 --retry-all-errors \ + "https://github.com/taiki-e/cargo-llvm-cov/releases/download/v${LLVM_COV_VERSION}/cargo-llvm-cov-${host}.tar.gz" \ + -o /tmp/llvm-cov.tar.gz; \ + tar -xzf /tmp/llvm-cov.tar.gz -C /usr/local/cargo/bin; \ + rm /tmp/llvm-cov.tar.gz; \ + fi -# Create features directory for BDD feature files -RUN mkdir -p /app/features - -# Build tests -RUN cargo test --no-run -p bdd +COPY . . +# The shared Docker context includes host server binaries for other images. +# Their coverage mappings belong in the host report, not the BDD report. +RUN rm -rf target && mkdir -p /app/features +RUN if [[ "$COVERAGE" == true ]]; then \ + coverage_env=$(cargo llvm-cov show-env --sh --remap-path-prefix); \ + eval "$coverage_env"; \ + fi; \ + cargo test --locked --no-run -p bdd --features "$BDD_RUST_FEATURES" -# Default command will be overridden by docker-compose -CMD ["cargo", "test", "-p", "bdd", "--features", "bdd"] +CMD ["cargo", "test", "--locked", "-p", "bdd", "--features", "bdd"] diff --git a/codecov.yml b/codecov.yml index 2d927b241..f33f0de4e 100644 --- a/codecov.yml +++ b/codecov.yml @@ -55,6 +55,9 @@ flag_management: - name: rust paths: - core/ + - name: rust-bdd + paths: + - core/ - name: java paths: - foreign/java/ diff --git a/scripts/ci/coverage-baseline-affected.sh b/scripts/ci/coverage-baseline-affected.sh index aa1187d8f..3b0f96af8 100755 --- a/scripts/ci/coverage-baseline-affected.sh +++ b/scripts/ci/coverage-baseline-affected.sh @@ -75,7 +75,7 @@ changed() { for gate in "${GATES[@]}"; do case "$gate" in # .config holds the nextest and Cargo-Rail policy every Rust test run reads. - rust) paths=(core gateways Cargo.toml Cargo.lock rust-toolchain.toml .cargo .config) ;; + rust) paths=(core gateways bdd/rust bdd/scenarios ':(glob)bdd/docker-compose*.yml' examples/rust Cargo.toml Cargo.lock rust-toolchain.toml .cargo .config) ;; java) paths=(foreign/java) ;; csharp) paths=(foreign/csharp) ;; python) paths=(foreign/python) ;; diff --git a/scripts/ci/storage-compat.sh b/scripts/ci/storage-compat.sh index edfabe02c..296893268 100755 --- a/scripts/ci/storage-compat.sh +++ b/scripts/ci/storage-compat.sh @@ -243,6 +243,10 @@ else BASELINE_TARGET_DIR="${WORKTREE_DIR}/target" ( cd "${WORKTREE_DIR}" + # The reference revision must not contribute counters to HEAD's coverage report. + if [[ "${__CARGO_LLVM_COV_RUSTC_WRAPPER:-}" == 1 ]]; then + export RUSTC_WRAPPER="${__CARGO_LLVM_COV_RUSTC_WRAPPER_PRE_EXISTING:-}" + fi CARGO_TARGET_DIR="${BASELINE_TARGET_DIR}" cargo build --locked -p server --bin iggy-server ) diff --git a/scripts/run-bdd-tests.sh b/scripts/run-bdd-tests.sh index acc6f913b..aadd6e2e9 100755 --- a/scripts/run-bdd-tests.sh +++ b/scripts/run-bdd-tests.sh @@ -78,7 +78,8 @@ case "$FEATURE" in esac if [ "$COVERAGE" = "1" ]; then COMPOSE_FILES+=(-f docker-compose.coverage.yml) - mkdir -p ../reports + mkdir -p ../reports ../target + export BDD_SERVER_PROFILE_FILE="/profiles/bdd-server-%h-%p-%m.profraw" fi cleanup(){
