log-cardio 1.1.0

Trim the fat from logs for LLM agents: lean pipeline (trim + dedup + window), direct gcloud/bq/kubectl streaming with cost guards, trace grouping, token-aware surveys, and an MCP stdio server (--mcp).
name: CI

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch: {}
  schedule:
    # weekly on main: fresh RustSec advisories between releases
    - cron: "23 4 * * 1"

jobs:
  # dependency CVE gate: Cargo.lock against the RustSec advisory DB.
  # deny-warnings=false so unmaintained-dep notices don't turn CI red;
  # only real vulnerabilities fail the check
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: rustsec/audit-check@v2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          deny-warnings: false

  # lint gate: zero warnings allowed (the codebase holds it today; this
  # keeps it from rotting)
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy
      - uses: Swatinem/rust-cache@v2
      - run: cargo clippy --all-targets -- -D warnings

  coverage:
    # coverage lives on its own named check: instrumented tests on Linux +
    # the gh-pages badge on main. Linux is covered here instead of `test` —
    # test keeps macos/windows only.
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v7
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: install cargo-llvm-cov
        uses: taiki-e/install-action@v2
        with:
          tool: cargo-llvm-cov
      - name: instrumented tests
        # src/mcp is EXCLUDED from the ratio: its stdio tests are black-box
        # (tests/mcp.rs drives the spawned binary), and llvm tooling cannot
        # attribute coverage to a child process — counting it as 0% would
        # just lie. Everything else must stay ≥80%.
        run: |
          rustup component add llvm-tools-preview
          cargo llvm-cov --lcov --ignore-filename-regex 'src/mcp' --output-path lcov.info
      - name: update gh-pages badge
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          PCT=$(awk -F: '/^LF:/{lf+=$2}/^LH:/{lh+=$2}END{printf "%.0f", 100*lh/(lf?lf:1)}' lcov.info)
          COLOR=$([ "$PCT" -ge 90 ] && echo 44cc11 || ([ "$PCT" -ge 75 ] && echo 97ca00 || echo dfb317))
          printf '<svg xmlns="http://www.w3.org/2000/svg" width="108" height="20"><rect width="108" height="20" rx="3" fill="#%s"/><text x="54" y="14" font-size="11" font-family="Verdana,sans-serif" fill="#fff" text-anchor="middle">coverage %s%%</text></svg>\n' "$COLOR" "$PCT" > coverage.svg
          URL="https://x-access-token:${GH_TOKEN}@github.com/Red-CF/log-cardio.git"
          git clone -q --depth=1 --branch=gh-pages "$URL" ghp \
            || { mkdir ghp && git -C ghp init -q -b gh-pages && git -C ghp remote add origin "$URL"; }
          mv coverage.svg ghp/coverage.svg
          git -C ghp -c user.name=Red-CF -c user.email=crojasfer31@gmail.com add coverage.svg
          git -C ghp -c user.name=Red-CF -c user.email=crojasfer31@gmail.com commit -q -m "coverage badge: $PCT%" --allow-empty
          git -C ghp push -q origin gh-pages

  test:
    # the platform contract: file/stdin plus spawning gcloud/bq behave
    # differently per OS — needs a real run on all three (Linux runs inside
    # the `coverage` job, which is instrumented)
    strategy:
      fail-fast: false
      matrix:
        os: [macos-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v7
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - run: cargo test
        # network-fetching tests are faked via PATH stubs (cfg(unix)) so
        # this works offline everywhere

  build:
    # release build is what users install — must stay clean on all OSes;
    # the binary also ships with the GitHub release (picked up by `release`)
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v7
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - run: cargo build --release
      - name: also build macOS Intel (runner is ARM)
        if: runner.os == 'macOS'
        run: |
          rustup target add x86_64-apple-darwin
          cargo build --release --target x86_64-apple-darwin
          mv target/x86_64-apple-darwin/release/cardio cardio-macOS-X64
      - name: stage named binary
        shell: bash
        run: |
          EXT=$([ "$RUNNER_OS" = "Windows" ] && echo .exe || echo "")
          mv "target/release/cardio$EXT" "cardio-$RUNNER_OS-$RUNNER_ARCH$EXT"
      - uses: actions/upload-artifact@v7
        with:
          name: cardio-${{ runner.os }}
          path: cardio-*

  version:
    # the merge gate: a PR that changes SHIPPABLE code must bump Cargo.toml
    # above what crates.io has, or the merge button stays locked. Infra PRs
    # (workflow, docs, tests, bench) are excluded: a CI fix must not force
    # a version, and a version-only bump would ship a release without
    # meaning anything.
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - name: does this PR touch shippable code?
        id: scope
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          # the PR's actual file list (shallow clone: no merge base locally)
          FILES=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" --paginate --jq '.[].filename') \
            || { echo "::error::could not list PR files — skipping the gate this way would be fail-open; retry the check"; exit 1; }
          echo "$FILES"
          if echo "$FILES" | grep -qE '^(src/|Cargo\.(toml|lock)$)'; then
            echo "code=true" >> "$GITHUB_OUTPUT"
          else
            echo "PR touches no shippable code; version gate skipped."
            echo "code=false" >> "$GITHUB_OUTPUT"
          fi
      - if: steps.scope.outputs.code == 'true'
        name: version must be new to crates.io
        run: |
          V=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)
          PUBLISHED=""
          # retry: a single bad response from the registry mustn't lock merging
          for i in 1 2 3; do
            PUBLISHED=$(curl -sfL -A "log-cardio-ci" "https://crates.io/api/v1/crates/log-cardio/versions" | jq -r --arg v "$V" '[.versions[].num | select(. == $v)] | length' 2>/dev/null) || true
            [ -n "$PUBLISHED" ] && break
            sleep 3
          done
          [ -n "$PUBLISHED" ] || { echo "::error::registry query failed after 3 tries — retry the check"; exit 1; }
          if [ "$PUBLISHED" != "0" ]; then
            echo "::error::log-cardio $V is already on crates.io — bump version in Cargo.toml before merging"
            exit 1
          fi

  release:
    # a merge to main ships the crate ONLY when the version is new to
    # crates.io: merges without a version bump (infra PRs, accumulated
    # WIP before a deliberate release) leave the crate untouched and the
    # job exits green without publishing anything
    needs: [test, build, coverage, lint, audit]
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v7
      - id: decide
        name: release only if this version is new to crates.io
        run: |
          V=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)
          KNOWN=""
          # retry: a single bad response from the registry mustn't kill the run
          for i in 1 2 3; do
            KNOWN=$(curl -sfL -A "log-cardio-ci" "https://crates.io/api/v1/crates/log-cardio/versions" | jq -r --arg v "$V" '[.versions[].num | select(. == $v)] | length' 2>/dev/null) || true
            [ -n "$KNOWN" ] && break
            sleep 3
          done
          # fail loud, not silent: a flaky registry must skip NOTHING (a
          # release that "went green" without publishing would be missed);
          # the decide step is free to re-run manually, cargo publish alone
          # guards against a true double publish
          [ -n "$KNOWN" ] || { echo "::error::registry query failed after 3 tries — re-run the release job"; exit 1; }
          if [ "$KNOWN" != "0" ]; then
            echo "Already-released version $V: no publish (no-release merge)."
            echo "release=false" >> "$GITHUB_OUTPUT"
          else
            echo "release=true" >> "$GITHUB_OUTPUT"
          fi
      - name: cargo publish
        if: steps.decide.outputs.release == 'true'
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: cargo publish
      - name: tag + GitHub release
        if: steps.decide.outputs.release == 'true'
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          V=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)
          git config user.name "Red-CF"
          git config user.email "crojasfer31@gmail.com"
          git tag "v$V"
          git push origin "v$V"
          gh release create "v$V" --generate-notes
      - uses: actions/download-artifact@v8
        if: steps.decide.outputs.release == 'true'
        with:
          path: bins
          merge-multiple: true
      - name: attach native binaries
        if: steps.decide.outputs.release == 'true'
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          V=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)
          gh release upload "v$V" bins/cardio-*

  bench:
    # INFORMATIONAL, never a gate: runs after a main push (i.e. after the
    # release landed) and on demand via workflow_dispatch. Numbers go to
    # gh-pages (badge + JSON + table), so the README's performance photo
    # refreshes WITHOUT commits to main. Runners are ubuntu-latest, so the
    # numbers are comparable run-to-run even when they differ from a local
    # build on other hardware.
    if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch'
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v7
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - run: cargo build --release
      - name: install hyperfine
        run: sudo apt-get update -qq && sudo apt-get install -y -qq hyperfine
      - name: benchmark every mode on a 1M-line synthetic log
        run: BENCH_JSON=bench.json ./bench.sh
      - name: build badge + results page
        run: |
          python3 - <<'PY'
          import json, datetime, os
          data = json.load(open("bench.json"))["results"]
          def label(cmd):
              if cmd.startswith("/bin/cat"): return "cat baseline"
              for pat, lab in [("--raw --trace", "trace (raw)"), ("--raw", "raw"),
                               ("--stats", "stats"), ("--query", "query")]:
                  if pat in cmd: return lab
              return "lean (default)"
          rows = []
          lean_ms = None
          for r in data:
              ms = r["median"] * 1000
              rows.append((label(r["command"]), ms, "s" if ms >= 1000 else "ms",
                           r["median"]))
              if label(r["command"]).startswith("lean"):
                  lean_ms = ms
          lean_txt = f"{lean_ms/1000:.2f}" if lean_ms else "n/a"
          txt = f"bench lean {lean_txt}/1M"
          w = 12 + len(txt) * 6
          open("bench.svg", "w").write(
              f'<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="20">'
              f'<rect width="{w}" height="20" rx="3" fill="#007ec6"/>'
              f'<text x="{w/2}" y="14" font-size="11" font-family="Verdana,sans-serif" '
              f'fill="#fff" text-anchor="middle">{txt}</text></svg>')
          now = datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds")
          body = "\n".join(
              f"<tr><td>{lab}</td><td>{ms/1000:.3f} s</td></tr>" for lab, ms, _, _ in rows)
          open("bench.html", "w").write(
              "<!doctype html><meta charset='utf-8'><title>cardio bench</title>"
              "<body><h1>cardio benchmark — 1M-line synthetic log</h1>"
              f"<p>runner: {os.environ.get('ImageOS','unknown')} {os.environ.get('ImageVersion','')} "
              f"| commit: {os.environ.get('GITHUB_SHA','')[:12]} | {now} | median of ≥3 runs</p>"
              "<table border='1' cellpadding='4'><tr><th>mode</th><th>median / 1M lines</th></tr>"
              f"{body}</table><p>Regenerated by CI after every release; trigger by hand with "
              "workflow_dispatch. Local numbers on your hardware: ./bench.sh</p></body>")
          PY
      - name: publish gh-pages bench results
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          URL="https://x-access-token:${GH_TOKEN}@github.com/Red-CF/log-cardio.git"
          git clone -q --depth=1 --branch=gh-pages "$URL" ghp             || { mkdir ghp && git -C ghp init -q -b gh-pages && git -C ghp remote add origin "$URL"; }
          mv bench.svg bench.html bench.json ghp/
          git -C ghp -c user.name=Red-CF -c user.email=crojasfer31@gmail.com add bench.svg bench.html bench.json
          git -C ghp -c user.name=Red-CF -c user.email=crojasfer31@gmail.com commit -q -m "bench results: ${GITHUB_SHA:0:12}" --allow-empty
          git -C ghp push -q origin gh-pages