djvu-rs 0.35.2

Read, render, convert, and create DjVu files. Pure-Rust DjVu decoder/encoder with CLI, WebAssembly, and Python bindings. DjVu to PDF, EPUB, TIFF, PNG, and text. MIT licensed, no GPL dependencies.
Documentation
name: API stability

# Enforces the compatibility contract in docs/api-compatibility.md (#695):
#   - unintended breakage of the stable public API surface (cargo-semver-checks)
#   - the documented feature combinations and targets all build
#   - the Send/Sync + panic-free contracts hold under the async feature
#
# These are kept in a dedicated workflow (not ci.yml) so the API-policy gates
# evolve independently of the core fmt/clippy/test pipeline.

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

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

env:
  CARGO_TERM_COLOR: always
  RUST_BACKTRACE: 1

jobs:
  # ── Public API breakage detection (AC2) ──────────────────────────────────────
  # Compares the PR against the latest version published on crates.io. For a
  # 0.x crate, cargo-semver-checks treats a MINOR bump as the breaking axis, so
  # this fails only on an *unintended* break of the stable surface that was not
  # accompanied by the required version bump.
  semver:
    name: Public API breakage (cargo-semver-checks)
    runs-on: ubuntu-latest
    timeout-minutes: 20
    # Blocking since v0.32.0 (#695). It ran with continue-on-error from #695
    # until then, for one reason: the error enums were exhaustive, so every
    # intended new variant reported as breakage and the gate could not tell that
    # apart from a real break. #796 marked all 34 public error enums
    # #[non_exhaustive] (docs/api-compatibility.md §1, "Error stability rule")
    # and v0.32.0 shipped them, which reset the baseline this job compares
    # against. A finding here is now an unintended break of the stable surface.
    #
    # An *intended* break is declared the way release-please reads it: a `!`
    # after the type/scope in the PR title (the squash-merge subject) or a
    # `BREAKING CHANGE:` footer in the PR body. The step below turns that
    # declaration into `--release-type major`: an explicit release type is
    # taken literally, and `major` is cargo-semver-checks' name for the
    # breaking axis, which on 0.x is the minor bump release-please makes. So a
    # declared break is checked as the bump it will produce and the job still
    # fails on any break the PR does *not* declare. Cargo.toml is not bumped by
    # hand: release-please owns the version and the manifest (#814). Do not
    # re-add continue-on-error. Adding a variant to an error enum is not a
    # break and does not trip this.
    steps:
      - uses: actions/checkout@v6

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

      - name: Cache Rust dependencies
        uses: Swatinem/rust-cache@v2
        with:
          key: semver-checks

      - name: Read the release type the change declares
        id: declared
        env:
          # The PR title on pull_request; the squash-merge subject on push.
          # Read through env, never interpolated into the script.
          SUBJECT: ${{ github.event.pull_request.title || github.event.head_commit.message }}
          BODY: ${{ github.event.pull_request.body }}
        run: |
          release_type=""
          if printf '%s\n' "$SUBJECT" | head -n 1 | grep -Eq '^[a-z]+(\([^)]*\))?!:' \
            || printf '%s\n%s\n' "$SUBJECT" "$BODY" | grep -Eq '^BREAKING[ -]CHANGE:'; then
            release_type=major
          fi
          echo "release-type=$release_type" >> "$GITHUB_OUTPUT"
          echo "declared release type: ${release_type:-none (derived from Cargo.toml)}"

      - name: cargo-semver-checks (stable public API)
        uses: obi1kenobi/cargo-semver-checks-action@v2
        with:
          # Default features = the decode-only `std` stable surface. Writer /
          # async / experimental surfaces are checked as they stabilize.
          feature-group: default-features
          package: djvu-rs
          # Empty = derive from the version number (the action skips the flag).
          release-type: ${{ steps.declared.outputs.release-type }}

  # ── Documented feature combinations & targets (AC3) ──────────────────────────
  # The human-readable companion is docs/feature-matrix.md; keep the two in sync.
  feature-matrix:
    name: Feature matrix (${{ matrix.name }})
    runs-on: ubuntu-latest
    timeout-minutes: 20
    strategy:
      fail-fast: false
      matrix:
        include:
          - name: "no_std (host)"
            flags: "--no-default-features"
          - name: "default (std, decode-only)"
            flags: ""
          - name: "jpeg"
            flags: "--features std,jpeg"
          - name: "pdf"
            flags: "--features pdf"
          - name: "epub"
            flags: "--features epub"
          - name: "cbz"
            flags: "--features cbz"
          - name: "tiff"
            flags: "--features tiff"
          - name: "cli"
            flags: "--features cli"
          - name: "async"
            flags: "--features async"
          - name: "parallel"
            flags: "--features parallel"
          - name: "mmap"
            flags: "--features mmap"
          - name: "serde"
            flags: "--features serde"
          - name: "image"
            flags: "--features image"
          - name: "kitchen-sink"
            flags: "--features cli,tiff,async,serde,image,epub,mmap,parallel"
    steps:
      - uses: actions/checkout@v6

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

      - name: Cache Rust dependencies
        uses: Swatinem/rust-cache@v2
        with:
          key: feature-matrix

      - name: cargo check ${{ matrix.flags }}
        run: cargo check ${{ matrix.flags }}

  # ── wasm32 documented feature set (AC3) ──────────────────────────────────────
  feature-matrix-wasm:
    name: Feature matrix (wasm32 ${{ matrix.name }})
    runs-on: ubuntu-latest
    timeout-minutes: 20
    strategy:
      fail-fast: false
      matrix:
        include:
          - name: "no_std"
            flags: "--no-default-features"
          - name: "wasm"
            flags: "--features wasm"
          - name: "wasm-lazy"
            flags: "--features wasm-lazy"
    steps:
      - uses: actions/checkout@v6

      - name: Install Rust stable + wasm32 target
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: wasm32-unknown-unknown

      - name: Cache Rust dependencies
        uses: Swatinem/rust-cache@v2
        with:
          key: feature-matrix-wasm

      - name: cargo check --target wasm32-unknown-unknown ${{ matrix.flags }}
        run: cargo check --target wasm32-unknown-unknown ${{ matrix.flags }}

  # ── Contract tests under the async feature (AC7 + AC8) ───────────────────────
  # The default Test (stable) job runs without `async`, so the Send/Sync
  # assertions for LazyDocument and the async panic-free surface are only
  # exercised here.
  contract-tests:
    name: Send/Sync + panic-free contracts (async)
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v6

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

      - name: Cache Rust dependencies
        uses: Swatinem/rust-cache@v2
        with:
          key: contract-tests

      - name: Send/Sync contract
        run: cargo test --test send_sync_contract --features async

      - name: Panic-free corpus + adversarial inputs
        run: cargo test --test panic_free_corpus --features cli