ruststream 0.4.0

Async messaging framework for Rust: broker-agnostic traits, router, codecs, and a conformance harness for broker authors.
Documentation
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  changes:
    name: Detect changes
    runs-on: ubuntu-latest
    outputs:
      rust: ${{ steps.filter.outputs.rust }}
      dist: ${{ steps.filter.outputs.dist }}
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false

      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            rust:
              - 'src/**'
              - 'crates/**'
              - 'tests/**'
              - 'examples/**'
              - 'Cargo.toml'
              - 'Cargo.lock'
              - 'rust-toolchain.toml'
              - 'rustfmt.toml'
              - 'clippy.toml'
              - '.github/workflows/ci.yml'
            dist:
              - 'src/**'
              - 'crates/**'
              - 'Cargo.toml'
              - 'Cargo.lock'
              - '.github/workflows/ci.yml'

  toolchains:
    name: Resolve toolchain range
    needs: changes
    if: ${{ needs.changes.outputs.rust == 'true' }}
    runs-on: ubuntu-latest
    outputs:
      list: ${{ steps.range.outputs.list }}
    steps:
      # Multi-MSRV: the crate keeps rust-version = 1.85 and CI proves the build on
      # every stable toolchain from that floor up to current stable, so broker
      # crates can adopt any MSRV in the range without waiting for a core release.
      # The list is resolved from the release manifest, so a new stable joins the
      # matrix on its release day with no workflow edit.
      - name: Build the 1.85..stable toolchain list
        id: range
        run: |
          stable_minor=$(curl -sSf https://static.rust-lang.org/dist/channel-rust-stable.toml \
            | grep -A1 '^\[pkg\.rust\]' | grep -m1 'version' | sed -E 's/.*"1\.([0-9]+)\..*/\1/')
          versions=$(seq 85 "$((stable_minor - 1))" | sed 's/^/"1./; s/$/"/' | paste -sd, -)
          echo "list=[\"stable\",${versions}]" >> "$GITHUB_OUTPUT"

  rust:
    name: Rust ${{ matrix.toolchain }}
    needs: [changes, toolchains]
    if: ${{ needs.changes.outputs.rust == 'true' }}
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        toolchain: ${{ fromJSON(needs.toolchains.outputs.list) }}
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@master
        with:
          toolchain: ${{ matrix.toolchain }}
          components: rustfmt, clippy

      - name: Cache cargo
        uses: Swatinem/rust-cache@v2

      - name: cargo fmt
        if: matrix.toolchain == 'stable'
        run: cargo fmt --all -- --check

      - name: cargo clippy
        if: matrix.toolchain == 'stable'
        run: cargo clippy --workspace --all-targets --all-features -- -D warnings

      - name: cargo check (no default features)
        run: cargo check --workspace --no-default-features

      - name: cargo check (all features)
        run: cargo check --workspace --all-targets --all-features

      # The `DefaultCodec` fallback aliases (cbor, then msgpack) are cfg'd out whenever `json` is
      # on, so neither the all-features build nor the all-features tests ever compile them. Build
      # each single-codec configuration so the alias and its default-codec call sites stay valid.
      - name: cargo check (single-codec default fallbacks)
        if: matrix.toolchain == 'stable'
        run: |
          cargo check --no-default-features --features cbor,memory,macros
          cargo check --no-default-features --features msgpack,memory,macros

      # The intermediate toolchains exist to prove the build (the two checks
      # above); the full suite runs at the edges of the range only.
      #
      # The trybuild UI snapshots in tests/ui are rustc-version-sensitive, so they
      # run on stable only: RUN_UI_TESTS=1 opts the `ui` test in here, every other
      # run (the 1.85 job, the coverage job, a local `cargo test`) leaves it unset
      # and the test skips itself.
      - name: cargo test
        if: matrix.toolchain == 'stable' || matrix.toolchain == '1.85'
        env:
          RUN_UI_TESTS: ${{ matrix.toolchain == 'stable' && '1' || '0' }}
        run: cargo test --workspace --all-features

  coverage:
    name: Coverage
    needs: changes
    if: ${{ needs.changes.outputs.rust == 'true' }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          components: llvm-tools-preview

      - name: Install cargo-llvm-cov
        uses: taiki-e/install-action@v2
        with:
          tool: cargo-llvm-cov

      - name: Cache cargo
        uses: Swatinem/rust-cache@v2

      # Run the suite under instrumentation once, then format the collected data twice:
      # a human-readable table into the run's job summary, and an lcov file as a downloadable
      # artifact. `report` reuses the profile from `--no-report`, so neither re-runs the tests.
      - name: Run tests under coverage
        run: cargo llvm-cov --no-report --workspace --all-features

      # Line-coverage gate, sitting at the 90% DoD target. Raise it here and in the justfile
      # together if coverage climbs further. The summary is written before the gate's exit code
      # propagates, so it shows up even on a failure.
      - name: Coverage summary and gate
        run: |
          set +e
          cargo llvm-cov report --summary-only --fail-under-lines 90 | tee coverage.txt
          rc=${PIPESTATUS[0]}
          {
            echo '## Coverage'
            echo
            echo '```'
            cat coverage.txt
            echo '```'
          } >> "$GITHUB_STEP_SUMMARY"
          exit "$rc"

      - name: Export lcov report
        if: always()
        run: cargo llvm-cov report --lcov --output-path lcov.info

      - name: Upload coverage report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: coverage-lcov
          path: lcov.info

  package:
    name: Package (publish dry-run)
    needs: changes
    if: ${{ needs.changes.outputs.dist == 'true' }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable

      - name: Cache cargo
        uses: Swatinem/rust-cache@v2

      # Package the published artifacts without uploading. Both packages in one invocation
      # (multi-package packaging, stable since cargo 1.90): packaging strips the path from the
      # lockstep `ruststream-macros` dependency, and between a version bump and the release that
      # version does not exist on crates.io, so packaging ruststream alone cannot resolve it. The
      # multi-package form resolves in-workspace dependencies through a local overlay instead.
      #
      # CARGO_HTTP_MULTIPLEXING=false works around recurring "[16] Error in the HTTP2 framing
      # layer" curl failures on the runners when this job re-downloads registry deps.
      - name: cargo publish dry-run (no upload)
        env:
          CARGO_HTTP_MULTIPLEXING: "false"
        run: cargo publish --dry-run -p ruststream-macros -p ruststream --all-features

  security:
    name: Security scans
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false

      # Dependency-graph gate: RustSec advisories, license allow-list, duplicate
      # versions, and registry sources, per the checked-in deny.toml. Not path-
      # gated: advisories arrive over time, not only with manifest changes.
      - name: cargo deny
        uses: EmbarkStudios/cargo-deny-action@v2