Skip to content

feat(salvage): total recovery for SSTs and blob files with in-place ECC autoheal #3461

feat(salvage): total recovery for SSTs and blob files with in-place ECC autoheal

feat(salvage): total recovery for SSTs and blob files with in-place ECC autoheal #3461

Workflow file for this run

# Hardening policy: every `uses:` ref is pinned to a full commit
# SHA with an inline `# vX.Y.Z` comment; every `actions/checkout`
# step sets `persist-credentials: false` unless a later step in
# the same job legitimately runs `git push`. Disabled/reference
# workflows live in `.github/disabled/`, not here, so audit
# scanners stay clean.
name: CoordiNode CI
on:
push:
branches:
- main
pull_request:
branches:
- main
# Manual re-run from any branch. Two use cases:
# (a) re-trigger a flaky cross-compile target after qemu
# hiccup — needs `full_cross_matrix: true` because the
# flaky ones (powerpc64, riscv64) live ONLY in the full
# matrix, not the PR/dispatch default;
# (b) maintainer wants a full CI cycle on a draft branch
# without opening a PR — also `full_cross_matrix: true`
# to mirror main-branch coverage.
# Without the input set (default), manual dispatch runs the
# same reduced 2-target cross matrix that PRs do (aarch64-gnu
# + i686-gnu) — useful for quick sanity-check re-runs.
# Triggering workflow_dispatch from the UI / API requires
# repository write access (not the workflow-token `actions:write`
# scope, which is a different thing). Fork PR authors don't
# have write on the upstream repo, so they can't fire this —
# full_cross_matrix has no fork-side attack surface.
workflow_dispatch:
inputs:
full_cross_matrix:
description: "Run the full 5-target cross-compile matrix (aarch64-gnu/musl, i686-gnu, powerpc64-gnu, riscv64gc-gnu) instead of the reduced 2-target subset"
type: boolean
default: false
required: false
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
jobs:
# Detect whether the change set touches code we want to validate.
# Used to gate the inner steps of `lint` and `test (stable,
# ubuntu-latest)` — both of which are REQUIRED checks in the
# branch ruleset — so their job names always surface to GitHub
# (satisfying the ruleset) while heavy work is skipped on
# docs-only / repo-meta-only PRs. Non-required downstream jobs
# (`no-std-check`, `cross-matrix`, `cross`,
# `codecov`) gate at the job level via `if:` and skip outright
# when no code changes — they aren't required so a "skipped"
# status is fine for them, but a required check that reports
# "skipped" is treated as "not yet satisfied" by the ruleset
# engine, hence the in-job conditional pattern below for the
# two required jobs.
changes:
runs-on: ubuntu-latest
# Bumped from the original 2 min — generous timeout so a slow
# `/pulls/{N}/files` page on a large PR can't run us into a
# hard wall. The job is also designed to NEVER fail (every
# path below resolves to a `code` output, even on API errors)
# so a missed deadline is the only remaining failure mode and
# 10 min gives plenty of margin.
timeout-minutes: 10
# Read-only token: the `pull_request` branch of the script
# below hits `/pulls/{N}/files`; non-PR events
# (`push` to main, `workflow_dispatch`) bypass the API
# entirely and just emit `code=true`. Either way, we never
# push and never write. Explicit `permissions:` block scopes
# the auto-provided GITHUB_TOKEN to exactly what this job
# needs, regardless of repo-default permission settings.
permissions:
contents: read
pull-requests: read
outputs:
code: ${{ steps.filter.outputs.code }}
steps:
# No `actions/checkout` step on purpose: the change predicate
# uses GitHub's REST API to list affected files via
# `/pulls/{N}/files` for the `pull_request` event. That
# avoids the `actions/checkout@v6 + fetch-depth: 0 +
# persist-credentials: false` 403 we hit on the first
# attempt, and skips the clone cost entirely for a job that
# only needs path names. Non-PR events (`push` to main,
# `workflow_dispatch`) bypass the path filter entirely and
# always emit `code=true` — see the inline rationale below.
- name: Detect code-touching changes
id: filter
# Override the runner's default `-eo pipefail` flags.
# GitHub Actions invokes the default `bash` step with
# `bash --noprofile --norc -eo pipefail {0}`, so a script
# that just adds `set -u` STILL inherits `-e` + pipefail
# from the runner. This job is a hard dependency of the
# required `lint` + `test (stable, ubuntu-latest)` checks
# (they `needs: changes`); any non-zero exit cascades to
# "required check never reports" and blocks merges, which
# is exactly the failure mode we built the safe-default
# output for. Keep `--noprofile --norc` so we still match
# the runner's clean-env defaults (no inherited dotfiles
# from a self-hosted runner with profile.d / bashrc) —
# we only drop the `-eo pipefail` half deliberately.
shell: bash --noprofile --norc {0}
env:
GH_TOKEN: ${{ github.token }}
EVENT_NAME: ${{ github.event_name }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -u
# Helper: emit a `code` output, log a reason, and exit 0
# so dependent jobs always have a valid value to consume.
emit_and_exit() {
local code_value="$1"
local reason="$2"
echo "::notice::changes job: code=$code_value ($reason)"
echo "code=$code_value" >> "$GITHUB_OUTPUT"
exit 0
}
if [ "$EVENT_NAME" = "pull_request" ]; then
# GitHub's own PR file list is the merge-base diff —
# exactly what we want, without resolving merge-base
# ourselves. Paginated to handle PRs touching >100 files
# (the API caps each page at 100, more pages auto-fetch).
# stdout / stderr are captured into SEPARATE variables:
# stdout (filename list) goes into CHANGED, stderr (any
# warnings or rate-limit messages) is preserved in
# API_ERR for the error notice. Mixing the two streams
# would let stderr noise leak into the CODE_RE predicate
# below and possibly mis-classify the run.
echo "Fetching PR #$PR_NUMBER file list via REST API..."
API_ERR_FILE=$(mktemp)
if ! CHANGED=$(gh api --paginate "repos/$REPO/pulls/$PR_NUMBER/files" --jq '.[].filename' 2>"$API_ERR_FILE"); then
# API/auth/rate-limit failure: don't cascade. Default
# to `code=true` so the real lint/test runs anyway.
# Surface the underlying error message in the notice
# so an operator debugging a flake sees what GitHub
# actually returned.
api_err=$(cat "$API_ERR_FILE" | tr '\n' ' ' | head -c 200)
rm -f "$API_ERR_FILE"
emit_and_exit "true" "REST /pulls/$PR_NUMBER/files failed (${api_err:-no stderr}), defaulting to full CI"
fi
rm -f "$API_ERR_FILE"
else
# Any non-`pull_request` event (push to main per the
# workflow trigger, plus `workflow_dispatch` for manual
# re-runs) bypasses the path filter and runs full CI.
#
# Rationale: the path-filter optimisation exists for
# developer iteration on PR branches (typical PR is
# N small commits, fast feedback matters). Merges to
# main are integration events validating what ships;
# manual workflow dispatches are operator-initiated and
# are by definition non-routine. Neither has an
# iteration loop to optimise. Skipping CI on a
# "docs-only" push to main would ALSO be structurally
# unsafe given the compare API's known 300-entry
# `.files` truncation cap — a large code-changing merge
# could be misclassified as `code=false` and silently
# skip validation.
emit_and_exit "true" "$EVENT_NAME event — always run full CI (no path filter outside PRs)"
fi
echo "Changed files:"
printf '%s\n' "$CHANGED" | sed 's/^/ /'
# Predicate: anything that affects compile / lint / test
# results counts as code. The workflow file is included so
# changes to CI logic itself always trigger the full matrix
# (avoids self-skipping a CI refactor PR). The lint/test
# config files (`.config/nextest.toml`, `.rustfmt.toml`,
# `clippy.toml`) and the in-repo test fixtures
# (`test_fixture/`) are included because changes to any of
# them can flip the lint/test outcome on otherwise-identical
# source code — skipping the real validation on a PR that
# touches only those files would defeat the purpose of the
# required-check gate.
CODE_RE='^(src/|tests/|benches/|examples/|test_fixture/|build\.rs$|Cargo\.toml$|Cargo\.lock$|tools/|\.github/workflows/coordinode-ci\.yml$|rust-toolchain(\.toml)?$|\.cargo/|\.config/nextest\.toml$|\.rustfmt\.toml$|clippy\.toml$)'
if printf '%s\n' "$CHANGED" | grep -qE "$CODE_RE"; then
emit_and_exit "true" "code paths affected — running real CI"
else
emit_and_exit "false" "no code paths affected — fast success"
fi
lint:
needs: changes
# `always() && !cancelled()` so the required `lint` check
# still runs (and emits its name to the ruleset) even if
# `changes` itself fails / is skipped — `!cancelled()` alone
# is NOT enough because GitHub's default needs-gating skips
# this job when a dependency fails. `always()` overrides that
# gating; the `!cancelled()` half lets a user-cancelled
# workflow run still report cancellation honestly. Empty
# `needs.changes.outputs.code` (changes job failed before
# emitting output) is caught below by the `!= 'false'` step
# guards, which default to running the real lint work (safe
# fallback).
if: ${{ always() && !cancelled() }}
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
# Surfaces a notice in the run log + summary so docs-only PRs
# leave an obvious trail explaining why the check passed
# without doing real work. Only fires on the explicit
# `code=false` signal — empty/unset (changes job failed)
# falls through to the real lint work below.
- name: Path-conditional skip notice
if: needs.changes.outputs.code == 'false'
run: |
echo "::notice::No code paths changed — skipping format + clippy. Required-check name still emitted as success."
- uses: actions/checkout@v7
if: needs.changes.outputs.code != 'false'
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
if: needs.changes.outputs.code != 'false'
with:
toolchain: stable
components: rustfmt, clippy
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
if: needs.changes.outputs.code != 'false'
- name: Format
if: needs.changes.outputs.code != 'false'
run: cargo fmt --all -- --check
- name: Clippy (strict)
if: needs.changes.outputs.code != 'false'
# `--all-targets` lints tests + benches too, not just the lib; without
# it, test-harness code under `--all-features` is never clippy-checked.
run: cargo clippy --all-features --all-targets -- -D warnings
- name: Clippy (default features)
if: needs.changes.outputs.code != 'false'
# The feature set a consumer gets by simply depending on the crate.
# `--all-features` does NOT cover it: code reachable only when a
# feature is OFF is never compiled there, so this configuration rotted
# unnoticed until its tests stopped compiling altogether. Lint both, or
# the one nobody builds is the one that breaks.
run: cargo clippy --all-targets -- -D warnings
test:
needs: [changes, lint]
# `always() && !cancelled()` so the matrix entries still
# emit their names to the ruleset even if `changes` or
# `lint` fails / is skipped. `!cancelled()` alone is NOT
# enough because GitHub's default needs-gating skips this
# job when a dependency fails; `always()` overrides that
# gating, the `!cancelled()` half lets a user-cancelled
# workflow run still report cancellation honestly. Empty
# `needs.changes.outputs.code` is treated as "run real
# tests" via the `!= 'false'` step guards.
#
# Matrix-aware skip (run only the required entry on docs-
# only PRs) was attempted via a job-level
# `matrix.rust == 'stable' && matrix.os == 'ubuntu-latest'`
# branch in this `if:`, but the `matrix` context is NOT
# available at job-level expression evaluation — only at
# step-level and in job names. All 6 matrix entries run on
# docs-only PRs and emit the skip notice from their own
# step-level guards; if optimising runner-minute waste is
# later required, the right shape is a dynamic matrix via
# `fromJSON(needs.changes.outputs.matrix)` (with `changes`
# emitting two pre-baked JSON values), not job-level `if:`.
if: ${{ always() && !cancelled() }}
timeout-minutes: 20
strategy:
fail-fast: true
matrix:
rust: [stable, "1.92.0"]
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
# Same path-conditional skip pattern as `lint`. ALL matrix
# entries emit their job name regardless of whether the inner
# steps ran, so `test (stable, ubuntu-latest)` — the entry
# listed in the branch ruleset as a required check — is
# always satisfied on docs-only PRs / lint-failed PRs
# without paying the full test cost.
#
# Three step-level cases for the inner work:
# - code == 'false' → skip notice (docs-only PR)
# - lint.result != 'success' → skip notice (lint broken; running tests adds noise on an already-failed PR)
# - code != 'false' AND lint.result == 'success' → real work
# The job-level `if: always() && !cancelled()` keeps the
# required check name surfacing in all three cases.
- name: Path-conditional skip notice
if: needs.changes.outputs.code == 'false'
run: |
echo "::notice::No code paths changed — skipping nextest + doc tests + tools/. Required-check name still emitted as success."
- name: Lint-failed skip notice
if: needs.changes.outputs.code != 'false' && needs.lint.result != 'success'
run: |
echo "::notice::Lint failed (result=${{ needs.lint.result }}) — skipping test work to avoid duplicate noise on an already-failed PR. Required-check name still emitted; fix the lint failure to unblock real test runs."
- uses: actions/checkout@v7
if: needs.changes.outputs.code != 'false' && needs.lint.result == 'success'
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
if: needs.changes.outputs.code != 'false' && needs.lint.result == 'success'
with:
toolchain: ${{ matrix.rust }}
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
if: needs.changes.outputs.code != 'false' && needs.lint.result == 'success'
with:
prefix-key: ${{ runner.os }}-cargo
- uses: taiki-e/install-action@1ef5c5f58e85d25baaaa1704478fdd6c2921f2b5 # nextest
if: needs.changes.outputs.code != 'false' && needs.lint.result == 'success'
- name: Run tests
if: needs.changes.outputs.code != 'false' && needs.lint.result == 'success'
# tools/db_bench is a standalone crate, not in workspace — built separately
env:
# 32 cases is plenty for CI; PROPTEST_MAX_SHRINK trims the
# shrink budget from 1000 → 100 because at this case count
# shrinking rarely exceeds 20–50 iterations and the extra
# budget is wasted CI time. See tests/common/mod.rs.
PROPTEST_CASES: "32"
PROPTEST_MAX_SHRINK: "100"
run: cargo nextest run --profile ci --all-features
- name: Run doc tests
if: needs.changes.outputs.code != 'false' && needs.lint.result == 'success'
run: cargo test --doc --features lz4
- name: Check db_bench crate
if: needs.changes.outputs.code != 'false' && needs.lint.result == 'success'
working-directory: tools/db_bench
run: cargo check --all-features
- name: Build + test sst-dump crate
if: needs.changes.outputs.code != 'false' && needs.lint.result == 'success'
# Standalone crate (not in workspace), exercise both the bin
# build and the integration smoke tests so any drift in the
# public verify::verify_sst_file API or the SST writer layout
# surfaces here instead of in production. No `--profile ci`:
# sst-dump has no .config/nextest.toml of its own and the
# parent crate's profile config is not visible from a
# non-workspace child.
working-directory: tools/sst-dump
run: cargo nextest run
fuzz-heal:
# Reproducible single-byte-bitrot fuzzer over the SST read / heal path:
# flips one bit in a corpus of SSTs (varied block size, per-KV checksum,
# columnar, compression, encryption, Page-ECC) for a fixed ~45s budget and
# asserts the read path never panics and never returns a wrong value (a
# flipped block heals via ECC or fails its checksum, never silent corruption).
# `#[ignore]`d, so it is excluded from the normal `test` job and run here with
# `--run-ignored=only`. Ubuntu-only (the invariant is platform-independent);
# not a required check.
needs: [changes, lint]
if: ${{ needs.changes.outputs.code != 'false' && needs.lint.result == 'success' }}
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: stable
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
prefix-key: ubuntu-cargo
- uses: taiki-e/install-action@1ef5c5f58e85d25baaaa1704478fdd6c2921f2b5 # nextest
- name: Run bitrot heal fuzzer
# Pin the reproducer directory explicitly so the test's dump location does
# not depend on the process working directory (robust if the crate ever
# moves into a workspace subdirectory). The dump step reads the same path.
env:
FUZZ_HEAL_REPRO_DIR: ${{ github.workspace }}
# `--retries 0` overrides the ci profile's retries: the fuzzer dumps the
# EXACT failing SST, and a retry (on a non-deterministic corpus) would
# either overwrite that dump or mask a real failure that does not replay.
# `ignored-only` is the documented selector value; the shorter `only`
# is a deprecated alias current nextest still accepts (this job ran
# fine on it) but newer releases may drop.
run: cargo nextest run --profile ci --all-features --run-ignored=ignored-only --retries 0 -E 'test(fuzz_heal_bitrot)'
- name: Dump reproducer on failure
if: failure()
# The corpus is not byte-deterministic (encrypted / timestamped SSTs), so
# the seed alone cannot replay a failure in those. The test writes the
# EXACT failing SST to `fuzz_heal_repro.sst`; surface it (base64) in the
# log so the case reproduces directly, no artifact upload needed.
env:
FUZZ_HEAL_REPRO_DIR: ${{ github.workspace }}
run: |
echo "=== fuzz_heal_repro.txt ==="
cat "$FUZZ_HEAL_REPRO_DIR/fuzz_heal_repro.txt" 2>/dev/null || echo "(no repro txt)"
echo "=== fuzz_heal_repro.sst (base64) ==="
base64 "$FUZZ_HEAL_REPRO_DIR/fuzz_heal_repro.sst" 2>/dev/null || echo "(no repro sst)"
no-std-check:
# Gates the `#![no_std]` + alloc engine path. The engine modules compile
# unconditionally; only the std default trait implementations (the system
# filesystem, the io_uring backend, the system clock) stay behind
# `#[cfg(feature = "std")]`. This job runs `cargo check --no-default-features
# --features alloc` on a true no-std-only target (the `thumbv7em-none-eabihf`
# cross-compile target has no std at all, so any leaked `std::*` import
# surfaces here even if the host toolchain has std available).
#
# This is a HARD GATE: the alloc build must stay clean. New code added to
# this crate MUST be no-std-friendly (prefer `core::*` / `alloc::*`, gate
# std-only behind `#[cfg(feature = "std")]`). A failure here means a
# `std::*` leak crept onto an engine path: fix the leak, do not relax the
# gate.
needs: [changes, lint]
if: ${{ always() && !cancelled() && needs.lint.result == 'success' && needs.changes.outputs.code != 'false' }}
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: "1.92.0"
- name: Install no-std target
# Some versions of `dtolnay/rust-toolchain@stable` silently skip the
# `targets:` input when `toolchain:` is pinned to a specific version
# rather than a channel name. Install the cross target explicitly so
# the no-std check can never break for a target-not-installed reason
# (which would mask real migration progress).
run: rustup target add thumbv7em-none-eabihf
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
prefix-key: ubuntu-cargo-no-std
- name: Check (no_std + alloc)
run: cargo check --target thumbv7em-none-eabihf --no-default-features --features alloc
# Generates the cross-compile target matrix. The full 5-target
# sweep runs in either of two cases:
# - push to main (regression coverage on every merged change)
# - workflow_dispatch with `full_cross_matrix: true` (manual
# opt-in for re-testing flaky targets or for full coverage
# on a draft branch)
# Everything else — PR builds + default manual dispatches —
# uses the reduced 2-target subset (aarch64-gnu + i686-gnu)
# that covers the two canonical pointer-width / ABI mistakes:
# 64-bit aarch64 ABI quirks and 32-bit x86 pointer-width.
# Both targets are little-endian — endianness coverage (the
# big-endian powerpc64 target) is deferred to the full sweep
# on main, alongside musl libc differences and the qemu-
# emulated riscv64 path. The reduced run is ~30% of the
# full-sweep cost and keeps PR feedback under 5 min.
cross-matrix:
# No `needs: lint` — this job only inspects github.event_name
# / github.ref / inputs to emit a JSON target list. It doesn't
# touch source, doesn't compile, doesn't run lint-sensitive
# tooling. Running it in parallel with lint shaves ~10s off
# the PR critical path: the matrix is ready when lint
# finishes, and `cross` (which `needs: [changes, lint,
# cross-matrix]`) starts immediately rather than waiting for
# serial chain.
needs: changes
# cross-matrix doesn't depend on `lint` (it's a pure
# github.event_name / inputs lookup that emits a JSON target
# list), so the `needs.lint.result` gate the other downstream
# jobs carry doesn't apply here — the matrix-generation work
# is cheap (~2 s) and independent of lint passing. The
# `always() && !cancelled()` half still applies so a `changes`
# infra-failure doesn't silently skip the matrix generator
# (which would cascade into `cross` skipping for the wrong
# reason).
if: ${{ always() && !cancelled() && needs.changes.outputs.code != 'false' }}
runs-on: ubuntu-latest
outputs:
targets: ${{ steps.set.outputs.targets }}
steps:
- id: set
env:
IS_FULL: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'workflow_dispatch' && inputs.full_cross_matrix) }}
run: |
if [ "$IS_FULL" = "true" ]; then
echo 'targets=["aarch64-unknown-linux-gnu","aarch64-unknown-linux-musl","i686-unknown-linux-gnu","powerpc64-unknown-linux-gnu","riscv64gc-unknown-linux-gnu"]' >> "$GITHUB_OUTPUT"
else
echo 'targets=["aarch64-unknown-linux-gnu","i686-unknown-linux-gnu"]' >> "$GITHUB_OUTPUT"
fi
cross:
needs: [changes, lint, cross-matrix]
if: ${{ always() && !cancelled() && needs.lint.result == 'success' && needs.changes.outputs.code != 'false' }}
timeout-minutes: 15
strategy:
fail-fast: true
matrix:
# Bracket notation REQUIRED on the needs context key here:
# the job id `cross-matrix` contains a hyphen, so dot
# notation (`needs.cross-matrix.outputs.targets`) is parsed
# by the GitHub Actions expression engine as `needs.cross`
# minus `matrix.outputs.targets`, breaking the lookup.
target: ${{ fromJSON(needs['cross-matrix'].outputs.targets) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: stable
- name: Install cross
run: cargo install cross
- name: Cross test
# proptest cases: 32 hardcoded in ProptestConfig
run: cross test -r --features lz4 --target ${{ matrix.target }}
codecov:
needs: [changes, lint]
if: ${{ always() && !cancelled() && needs.lint.result == 'success' && needs.changes.outputs.code != 'false' }}
timeout-minutes: 20
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # nightly
with:
components: llvm-tools-preview
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: taiki-e/install-action@6bd9352acd589686fbc4cff533c083c3366dd444 # cargo-llvm-cov
- uses: taiki-e/install-action@1ef5c5f58e85d25baaaa1704478fdd6c2921f2b5 # nextest
- name: Run tests (all-features) with coverage
env:
# Same proptest budget as the regular test job — without
# these, the coverage job would default to proptest's
# built-in 256 cases and dominate CI time. See
# tests/common/mod.rs.
PROPTEST_CASES: "32"
PROPTEST_MAX_SHRINK: "100"
run: cargo +nightly llvm-cov --no-report nextest --all-features
# zstd feature: run with a narrower feature set (zstd + lz4, no defaults)
# to validate the zstd backend in isolation. --all-features already enables
# zstd (zstd-pure is an alias), so this step covers the non-default path.
- name: Run tests (zstd backend) with coverage
env:
PROPTEST_CASES: "32"
PROPTEST_MAX_SHRINK: "100"
run: cargo +nightly llvm-cov --no-report nextest --no-default-features --features zstd,lz4
- run: cargo +nightly llvm-cov --no-report --doc --features lz4
- run: cargo +nightly llvm-cov report --doctests --lcov --output-path lcov.info --ignore-filename-regex='registry'
- uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: lcov.info
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}