mermaid-cli 0.21.1

Open-source AI pair programmer with agentic capabilities. Local-first with Ollama, native tool calling, and beautiful TUI.
Documentation
name: Rust CI

on:
  push:
    branches: [ "main" ]
  # No base-branch filter on pull_request: stacked PRs (base = another feature
  # branch) must get the same gate as PRs to main — the Windows jobs
  # especially, since a local Linux gate can't catch cfg(windows) breakage.
  pull_request:
  # Beta and nightly run here rather than on every PR (see the `rust` matrix).
  schedule:
    - cron: '17 6 * * *'
  workflow_dispatch:

# One in-flight run per ref. Pushing a fixup used to leave the superseded run
# compiling to completion on every leg, producing a verdict on a commit nobody
# would read it for — while holding the runners the new run then queues behind.
concurrency:
  # `event_name` is in the key so the nightly schedule and a push to main do not
  # share a group and serialize behind each other.
  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
  # Never cancel a main-branch run: those are the ones a release is cut from.
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:
  CARGO_TERM_COLOR: always
  RUST_BACKTRACE: 1
  # ~90% of a CI leg is compilation, not testing: the Windows leg spent ~295s
  # compiling and 35s running all 2046 tests. So the compile knobs below are
  # where the wall clock is. Measured together on a cold target dir, building
  # exactly what CI builds: compile 172s -> 144s (-16%), and the target dir
  # 8473 MB -> 2616 MB (-69%), which is what rust-cache has to restore.
  #
  # Incremental compilation is pure overhead here: it exists to make the SECOND
  # build in a working tree fast, and CI never gets one. It costs time and is
  # most of the target-dir bloat.
  CARGO_INCREMENTAL: 0
  # Full debuginfo (cargo's default, `debug = 2`) is the expensive half of a
  # debug build, and on Windows the PDB write dominates linking — 12
  # integration-test binaries each link the whole lib. `line-tables-only` keeps
  # what a CI failure is actually read for: backtraces with file:line, paired
  # with the RUST_BACKTRACE above. What it drops is variable inspection, which
  # needs an interactive debugger nobody attaches to a runner.
  CARGO_PROFILE_DEV_DEBUG: line-tables-only
  CARGO_PROFILE_TEST_DEBUG: line-tables-only

jobs:
  # Format checking
  fmt:
    name: Rustfmt
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
        with:
          toolchain: stable
          components: rustfmt
      - name: Check formatting
        run: cargo fmt -- --check

  # Linting with Clippy — `-D warnings` makes lint regressions fail CI.
  clippy:
    name: Clippy
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
        with:
          toolchain: stable
          components: clippy
      - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
      - name: Run clippy
        # `--workspace` so the `mermaid-runtime` crate (safety classifier,
        # approvals store) is linted too — a bare `cargo clippy` only covers the
        # root package and silently skipped it.
        run: cargo clippy --workspace --all-targets -- -D warnings

  # Build and test on multiple Rust versions and OS
  test:
    name: Test Suite
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        # A PR gets all three platforms on stable — those are the required
        # checks, and the merge waits on the slowest of them.
        #
        # Beta and nightly answer a different question ("will a future toolchain
        # break us?"), and the answer does not change between one PR and the
        # next. Running them per-PR spent 4 of 7 test jobs re-asking it, so they
        # run on the nightly schedule and on every push to main instead: the
        # same signal, arriving within a day, off the critical path.
        rust: ${{ fromJSON(github.event_name == 'pull_request' && '["stable"]' || '["stable", "beta", "nightly"]') }}
        # Nightly stays Linux-only — enough to catch a future-toolchain break
        # without tripling the cost of finding it. On a PR the list above has no
        # nightly to exclude and these are no-ops.
        exclude:
          - os: macos-latest
            rust: nightly
          - os: windows-latest
            rust: nightly
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
        with:
          toolchain: ${{ matrix.rust }}

      # Cache dependencies for faster builds
      - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
        with:
          # Cache key includes OS and Rust version
          key: ${{ matrix.os }}-${{ matrix.rust }}

      - name: Install cargo-nextest
        uses: taiki-e/install-action@6c6fd71fe4fb72c3697d269963d0e15df8adedad # v2.85.10
        with:
          tool: nextest

      # No separate `cargo build --workspace` step before this one. It looked
      # free — surely nextest reuses the artifacts — but it measured 144s cold
      # against 113s for nextest alone, because the two invocations do not want
      # the same lib: `build` produces a plain rlib and the bins, nextest wants
      # the lib again under `cfg(test)`. The integration tests reach the bins
      # through `env!("CARGO_BIN_EXE_mermaid")`, which makes cargo build them
      # here anyway, so nothing was lost by dropping the step — a compile error
      # still fails this step, just with nextest's name on it.
      - name: Build and run tests
        # nextest runs the same test binaries as `cargo test`, but applies the
        # `.config/nextest.toml` retry policy that auto-heals the documented
        # Windows cancellation flake. `--workspace` covers the mermaid-runtime
        # crate (safety classifier, approvals store). No doctests exist, so
        # nextest skipping them loses nothing.
        run: cargo nextest run --workspace

      # The insta snapshot suite now runs on every matrix leg (#296), so a
      # mismatch is reproducible locally. Still publish the `.snap.new` siblings
      # insta writes on failure: the frame a leg you don't have rendered is what
      # makes a platform-specific diff reviewable (and appliable) at all.
      - name: Upload pending snapshots
        if: failure()
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
        with:
          name: pending-snapshots-${{ matrix.os }}-${{ matrix.rust }}
          path: '**/*.snap.new'
          if-no-files-found: ignore
          retention-days: 3

      # Only check documentation on stable Linux
      - name: Check documentation
        if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest'
        run: cargo doc --no-deps --document-private-items

  # Security audit
  security:
    name: Security Audit
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      # `rustsec/audit-check` compiled cargo-audit from source on every run:
      # 221s in a single step, on a job whose actual work is reading
      # Cargo.lock against an advisory database. That made it 94% as long as
      # the Windows leg — the critical path — for no reason a prebuilt binary
      # doesn't solve. `taiki-e/install-action` downloads one (the same action
      # already used for nextest), and `cargo audit` is the same tool reading
      # the same RustSec database, so the verdict is unchanged.
      - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
        with:
          toolchain: stable
      - uses: taiki-e/install-action@6c6fd71fe4fb72c3697d269963d0e15df8adedad # v2.85.10
        with:
          tool: cargo-audit
      # Plain `cargo audit`, not `--deny warnings`: the action failed on
      # vulnerabilities and only annotated unmaintained/unsound advisories, and
      # this job is meant to run exactly as strict as it did before, not one
      # notch stricter.
      - name: Audit dependencies
        run: cargo audit

  # Every publishable crate must build ALONE, on Linux.
  #
  # `cargo build --workspace` unifies features across members, so the root
  # enabling `dbus-secret-service/vendored` made every crate's keyring build —
  # including one whose own manifest never declared it. Nothing caught that
  # until `cargo publish` verified the packaged tarball in isolation, mid-release,
  # after `mermaid-runtime` had already gone to crates.io and could not be taken
  # back (v0.21.0). A local `--dry-run` could not have caught it either: on
  # Windows keyring uses `windows-native` and never touches dbus.
  #
  # `cargo package` + building the extracted tarball reproduces exactly what
  # publish verifies, without needing the version to exist on the index — so
  # this runs on every PR instead of once per release.
  isolated-crate-build:
    name: Crates build standalone (Linux)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
        with:
          toolchain: stable
      - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
        with:
          key: isolated-crate-build
      - name: Build each crate outside the workspace
        run: |
          set -euo pipefail
          # Copy the crates somewhere with no workspace root above them. Each
          # then builds as its own root — no feature unification with
          # `mermaid-cli` — which is exactly the isolation `cargo publish`
          # applies, and exactly what hid the missing dep.
          #
          # Copying rather than `cargo package`: packaging resolves deps against
          # the crates.io index, so a crate depending on an unreleased sibling
          # version cannot be packaged before that sibling is published. That
          # made the first version of this job fail on its own fix PR. Sibling
          # path deps resolve fine here because the siblings are copied too.
          ISO="$RUNNER_TEMP/isolated"
          mkdir -p "$ISO"
          cp -r crates/. "$ISO/"
          for crate in mermaid-runtime mermaid-model; do
            echo "::group::$crate (standalone)"
            ( cd "$ISO/$crate" && cargo build --all-targets )
            echo "::endgroup::"
          done

  # Dependency-free source guards: no emoji in user-facing output, and the pure
  # `src/domain` MVU core stays free of I/O + the wall clock.
  guards:
    name: Source Guards
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - name: No emoji in source
        run: python3 .github/scripts/check_no_emoji.py
      - name: domain/ purity (no I/O, no wall clock)
        run: python3 .github/scripts/check_domain_purity.py

  # Heavier integration coverage that CI's default suite skips: the daemon
  # `#[ignore]`d tests (spawn a real mermaidd) and the deterministic, model-free
  # `mermaid self-test`. Linux-only — enough to catch daemon/self-test breakage.
  integration:
    name: Daemon + Self-Test (Linux)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
        with:
          toolchain: stable
      - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
      - name: Build binaries
        run: cargo build --workspace
      - name: Daemon integration tests (ignored)
        run: cargo test --test daemon_integration -- --ignored
      - name: Sandbox network kill-switch (ignored, Linux)
        run: cargo test --test sandbox_network -- --ignored
      - name: Sandbox filesystem confinement (ignored, Linux)
        run: cargo test --test sandbox_fs -- --ignored
      - name: Self-test (deterministic, model-free)
        run: cargo run --bin mermaid -- self-test --format json

  # The managed-search test downloads the pinned bundle, starts the real
  # Granian/SearXNG process, performs a JSON search, and verifies shutdown. Run
  # it on every supported managed-search OS; the normal Windows matrix covers
  # the actionable unsupported-backend path.
  managed-search-integration:
    name: Managed SearXNG (${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    timeout-minutes: 30
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest]
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
        with:
          toolchain: stable
      - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
        with:
          key: managed-searxng-${{ matrix.os }}
      - name: Managed SearXNG end-to-end (ignored)
        run: cargo test --lib searxng::tests::managed_searxng_end_to_end -- --ignored --exact

  # OS-sandbox enforcement on the non-Linux platforms with a real backend
  # (the Linux `integration` job above already runs these tests). macOS-only
  # for now; the Windows AppContainer port adds windows-latest. The
  # `#[ignore]`d tests include the Seatbelt profile-compile canary, so an SBPL
  # grammar change in a new macOS release fails this job loudly.
  # Real macOS verification of the clipboard FILE-REFERENCE paste path.
  #
  # This path (Finder 'Copy' on an image file, rather than copying the image
  # itself) was written against documented `public.file-url` behavior and could
  # not be exercised by its author, who has no Mac. The Linux equivalents are
  # verified by hand; a CI runner is the only Apple hardware this project has,
  # so the check lives here rather than in a paragraph nobody re-reads.
  clipboard-macos:
    name: Clipboard file-reference paste (macOS)
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
        with:
          toolchain: stable
      - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
        with:
          key: clipboard-macos
      - name: Arm the clipboard with a file reference, then probe it
        run: |
          set -uo pipefail
          DIR="$RUNNER_TEMP/mermaid-fileref"
          mkdir -p "$DIR"
          # A space in the name so the URI carries a percent-escape.
          echo 'not-a-real-png-but-the-extension-is-what-matters' > "$DIR/My Shot.png"
          # Exactly what Finder's Copy puts on the pasteboard: a file URL.
          if ! osascript -e "set the clipboard to POSIX file \"$DIR/My Shot.png\""; then
            echo "::notice::this runner cannot drive the pasteboard; skipping"
            exit 0
          fi
          INFO=$(osascript -e 'clipboard info' || true)
          echo "clipboard info: $INFO"
          case "$INFO" in
            *furl*) ;;
            *)
              echo "::notice::pasteboard did not take a file reference; skipping"
              exit 0
              ;;
          esac
          # Only now is the assertion meaningful: the pasteboard really holds a
          # file reference, so a failure here is mermaid's probe, not the runner.
          cargo test --lib manual_file_reference_paste -- --ignored --nocapture

  sandbox-integration:
    name: OS Sandbox
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [macos-latest]
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
        with:
          toolchain: stable
      - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
        with:
          key: sandbox-${{ matrix.os }}
      - name: Build binaries
        run: cargo build --workspace
      - name: Sandbox network denial (ignored)
        run: cargo test --test sandbox_network -- --ignored
      - name: Sandbox filesystem confinement (ignored)
        run: cargo test --test sandbox_fs -- --ignored
      - name: Self-test (real sandbox availability probes)
        run: cargo run --bin mermaid -- self-test --format json