krafka 0.26.0

A pure Rust, async-native Apache Kafka client
Documentation
name: CI

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

env:
  CARGO_TERM_COLOR: always
  RUSTFLAGS: -Dwarnings

# Every job below runs a `just` recipe rather than an inline cargo command.
# The justfile is the single source of truth for what the checks are, so
# `just ci` locally runs exactly what CI runs and the two cannot drift.
# Feature strings in particular live in one place (justfile variables) instead
# of being duplicated across YAML.
#
# `ring` and `rustls-aws-lc-rs` are additive: when both are enabled,
# `rustls-aws-lc-rs` wins (see `auth::tls::resolve_crypto_provider`), so
# `--all-features` is valid and needs no hand-maintained exclusion list.

jobs:
  fmt:
    name: Format
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt
      - uses: extractions/setup-just@v3
      - run: just fmt-check

  clippy:
    name: Clippy
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy
      - uses: Swatinem/rust-cache@v2
      - uses: extractions/setup-just@v3
      - run: just clippy

  check:
    name: Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - uses: extractions/setup-just@v3
      - run: just check

  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - uses: extractions/setup-just@v3
      - run: just test

  # `ring` is the DEFAULT backend, so it is what most downstream users compile.
  # It must be *tested*, not merely type-checked: the
  # `cfg(not(feature = "rustls-aws-lc-rs"))` arms in `auth/tls.rs` and
  # `schema_registry/http.rs` only run here.
  test-ring:
    name: Test (ring backend)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - uses: extractions/setup-just@v3
      - run: just test-ring

  # Guards the minimum viable configuration. A missing crypto backend used to
  # fail deep inside rustls; the `compile_error!` in lib.rs plus this job keep
  # that diagnostic honest, and the second command pins feature additivity.
  minimal-features:
    name: Minimal features
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - uses: extractions/setup-just@v3
      - run: just minimal-features

  # A network client's socket options, dual-stack connection racing and
  # filesystem handling are platform-sensitive. Without these jobs non-Linux
  # targets were never compiled at all.
  cross-platform:
    name: Test (${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [macos-latest, windows-latest]
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - uses: extractions/setup-just@v3
      - run: just test-cross-platform

  protocol-parity:
    name: Protocol version parity
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: extractions/setup-just@v3
      # Reads the vendored `xtask/kafka_protocol_snapshot.json`, so this job
      # needs no network and cannot flake on a GitHub API rate limit. Refresh
      # the snapshot deliberately with `just refresh-protocol-snapshot <tag>`.
      - run: just protocol-parity

  secret-debug:
    name: No secrets in Debug
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: extractions/setup-just@v3
      - run: just secret-debug

  version-check:
    name: Version consistency
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: extractions/setup-just@v3
      - run: just version-check

  test-reachability:
    name: No tests that cannot fail
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: extractions/setup-just@v3
      - run: just test-reachability

  config-reachability:
    name: No config field that cannot be set
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: extractions/setup-just@v3
      - run: just config-reachability

  protocol-reachability:
    name: No decoded field that is never read
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: extractions/setup-just@v3
      - run: just protocol-reachability

  doc:
    name: Documentation
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - uses: extractions/setup-just@v3
      - run: just doc

  # The gate behind every ```rust,compile block in README.md and
  # site/content/docs/. It was in `just ci` — and therefore in the release
  # recipe — for its whole life without ever running in a workflow, so a pull
  # request could break every documented sample and stay green. That is the
  # same shape as the SASL suite, which was runnable locally long before
  # anyone noticed it had never run in CI.
  #
  # `doc_api.py` checks that names resolve; it cannot check that a call has
  # the right shape. This compiles the snippets instead.
  # Asserts that every recipe in `just ci` has a job here, that the
  # `ci-success` aggregator needs every job, and that it carries
  # `if: always()`. Closes the class that `docs-test` and `integration-sasl`
  # were both instances of.
  ci-job-parity:
    name: CI job parity
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: extractions/setup-just@v3
      - run: just ci-job-parity

  docs-test:
    name: Documentation snippets
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - uses: extractions/setup-just@v3
      - run: just docs-test

  # Classifies API changes against the last published release. Pre-1.0 this
  # reports rather than blocks (D1 allows a minor bump to break), but the
  # output is what the CHANGELOG's Breaking section owes the reader — and
  # until now nothing checked that list was complete.
  semver:
    name: SemVer classification
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - uses: extractions/setup-just@v3
      # A prebuilt binary rather than `cargo install`, which takes minutes.
      # The check itself runs through the justfile so CI and a developer's
      # machine cannot drift (D22).
      - uses: taiki-e/install-action@v2
        with:
          tool: cargo-semver-checks
      - run: just semver-check

  supply-chain:
    name: Supply chain (cargo-deny)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: EmbarkStudios/cargo-deny-action@v2
        with:
          command: check advisories bans licenses sources

  integration:
    name: Integration Tests (Kafka ${{ matrix.kafka-version }})
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        include:
          # apache/kafka-native (GraalVM) is fast but segfaults on getpwuid in
          # some CI environments; use it only where it is known to be stable.
          - kafka-version: "3.9.0"
            kafka-image: "apache/kafka-native"
          # Kafka 4.x native images consistently segfault on GitHub Actions
          # (GraalVM Pwd.getpwuid bug); use the JVM image instead.
          #
          # 3.9 is the supported floor and 4.3 the protocol-parity target;
          # every minor between them runs so a version-specific regression
          # names the exact broker line that broke.
          - kafka-version: "4.0.0"
            kafka-image: "apache/kafka"
          - kafka-version: "4.1.0"
            kafka-image: "apache/kafka"
          - kafka-version: "4.2.0"
            kafka-image: "apache/kafka"
          - kafka-version: "4.3.0"
            kafka-image: "apache/kafka"
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - uses: extractions/setup-just@v3
      - name: Pre-pull Kafka image
        run: |
          for i in 1 2 3; do
            docker pull ${{ matrix.kafka-image }}:${{ matrix.kafka-version }} && break
            echo "Retry $i failed, waiting..."
            sleep 10
          done
      - run: just integration
        timeout-minutes: 45
        env:
          KAFKA_VERSION: ${{ matrix.kafka-version }}
          KAFKA_IMAGE: ${{ matrix.kafka-image }}

  # The SASL suite existed before this job did — it was runnable locally via
  # `just integration-sasl` but never ran in CI, so a SASL handshake
  # regression could only be caught by hand. Its harness uses the Confluent
  # image (the apache images do not ship the JAAS entrypoint hooks it needs).
  integration-sasl:
    name: Integration Tests (SASL)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - uses: extractions/setup-just@v3
      - name: Pre-pull Kafka image
        run: |
          for i in 1 2 3; do
            docker pull confluentinc/cp-kafka:7.5.0 && break
            echo "Retry $i failed, waiting..."
            sleep 10
          done
      - run: just integration-sasl
        timeout-minutes: 45

  # Redpanda speaks the Kafka wire protocol; krafka negotiates every API
  # version, so this suite pins the compatibility that negotiation is supposed
  # to buy — including the KIP-890 TV1 fallback (Redpanda has no server-side
  # TV2). `latest` rather than a pinned tag: the claim under test is "current
  # Redpanda works", and a pin would quietly narrow it to "one old Redpanda
  # once worked".
  integration-redpanda:
    name: Integration Tests (Redpanda)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - uses: extractions/setup-just@v3
      - name: Pre-pull Redpanda image
        run: |
          for i in 1 2 3; do
            docker pull redpandadata/redpanda:latest && break
            echo "Retry $i failed, waiting..."
            sleep 10
          done
      - run: just integration-redpanda
        timeout-minutes: 30

  msrv:
    name: MSRV (1.95)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@master
        with:
          toolchain: "1.95"
      - uses: Swatinem/rust-cache@v2
      # Not `just msrv`: that recipe shells out to `rustup run 1.95` so a
      # developer can check the MSRV without switching their default toolchain.
      # Here the job's toolchain already *is* 1.95, so the plain command is
      # both simpler and a genuine check that the pinned toolchain builds.
      - run: cargo check

  # ── The single required status check ────────────────────────────────────────
  #
  # Branch protection names THIS job and nothing else. Without it, every job
  # above has to be listed by hand in the repository settings, and a newly
  # added job is not required by default — it runs, it can fail, and the merge
  # button stays green until somebody remembers to go and add it.
  #
  # Two GitHub behaviours make the obvious implementation wrong, and both are
  # guarded against here:
  #
  #   1. A required check whose job never runs stays *pending* forever, so the
  #      pull request can never merge. `if: always()` makes this job run even
  #      when a dependency failed or was skipped.
  #   2. GitHub reads a *skipped* required check as **success**. So this job
  #      must not simply skip on failure — it has to run and then fail
  #      explicitly, which is what the `contains(needs.*.result, ...)` test
  #      below does. Checking `success()` would not be enough either, because
  #      a skipped dependency does not make `success()` false.
  #
  # `needs` is exhaustive on purpose. `just ci-job-parity` asserts that this
  # list matches the jobs actually defined in this file, so adding a job
  # without wiring it here fails the build rather than silently going
  # unrequired.
  ci-success:
    name: CI
    if: always()
    runs-on: ubuntu-latest
    needs:
      - fmt
      - clippy
      - check
      - test
      - test-ring
      - minimal-features
      - cross-platform
      - protocol-parity
      - secret-debug
      - version-check
      - test-reachability
      - config-reachability
      - protocol-reachability
      - ci-job-parity
      - doc
      - docs-test
      - semver
      - supply-chain
      - integration
      - integration-sasl
      - integration-redpanda
      - msrv
    steps:
      - name: Verify every job succeeded
        run: |
          echo "Results: ${{ join(needs.*.result, ', ') }}"
          if ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped') }}; then
            echo "::error::At least one CI job did not succeed."
            exit 1
          fi
          echo "All CI jobs succeeded."