lucida 1.1.0

Generate images and video with Google Gemini, Veo, Runway, Kling, a local ComfyUI, FLUX, Stability AI or OpenAI — a CLI and an MCP server
name: ci

# Until now the only workflow ran on a tag, so `cargo test` had never run on a
# machine other than the developer's. That is how a seed collision caused by
# macOS's 1 µs clock resolution survived five releases: the Linux box it was
# written on could not observe it, and nothing else ever ran the suite.
on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

jobs:
  test:
    # All three release platforms, because the bug that prompted this file was
    # a platform difference the code never mentioned. Windows is here for the
    # same reason even though the release job already builds it — building is
    # not running.
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    defaults:
      run:
        shell: bash
    steps:
      - uses: actions/checkout@v7

      # `--locked` throughout. The lockfile is committed, and without this flag
      # cargo is free to quietly update it to satisfy a build — so CI could go
      # green against a dependency set that is not the one in the repository, and
      # the difference would first show up as a release that does not reproduce.
      - name: Test
        run: cargo test --locked

      # The tests reach no network: every provider is answered by
      # `src/testserver.rs` on a loopback port. A run that starts talking to a
      # real API is a bug in the test, not a flaky provider.
      - name: Clippy
        run: cargo clippy --locked --all-targets -- -D warnings

      - name: Smoke test the built binary
        run: |
          cargo build --locked --release
          BIN=target/release/lucida
          if [ -f "$BIN.exe" ]; then BIN="$BIN.exe"; fi
          ./scripts/smoke.sh "$BIN"

  # Six dependencies, all of them load-bearing and one of them a TLS stack. An
  # advisory against any of those is not something to learn about from a user, and
  # nothing else in this repository would ever mention it.
  audit:
    runs-on: ubuntu-latest

    # `checks: write` because the action reports through the check-runs API, and
    # this repository's default workflow permissions are read-only. Without the
    # grant the action throws on its first call — before it has looked at a
    # single advisory — so the job is red for a reason that says nothing about
    # the dependencies.
    #
    # It ran green in August on this same file and the same action SHA
    # (69366f3), then failed on every push to `main` from 2026-09-03. Neither
    # side of the pin moved, so what changed is the repository's default token
    # permissions, outside this repository and unversioned. The grant is the fix
    # either way: a job that states the scope it needs does not depend on the
    # default.
    #
    # ⚠ A token failure and a real advisory produce the SAME red X, and both
    # finding counts ("1 vulnerabilities found!") are attached at warning level,
    # which cannot fail a job. So this being red is not evidence about the
    # lockfile: read the failure-level annotation first. Diagnosing it the other
    # way round cost a fortnight — the red was read as a four-day-old advisory,
    # which was the one thing it demonstrably was not.
    permissions:
      contents: read
      checks: write

    steps:
      - uses: actions/checkout@v7
      - uses: rustsec/audit-check@v2
        with:
          token: ${{ github.token }}

  # The installers name the release assets, which makes them one more
  # hand-written list of platform facts — and this repository has already had
  # five of those go stale at once. Running them against the live release is
  # what turns a rename into a red build rather than into somebody's first five
  # minutes with the tool.
  install:
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v7

      - name: Install with install.sh
        if: matrix.os != 'windows-latest'
        env:
          # Only to avoid the unauthenticated API rate limit, which runners
          # share by IP. No scopes are needed for public releases.
          GITHUB_TOKEN: ${{ github.token }}
          LUCIDA_INSTALL_DIR: ${{ runner.temp }}/lucida-bin
        run: |
          sh install.sh
          "$LUCIDA_INSTALL_DIR/lucida" --version

      - name: Install with install.ps1
        if: matrix.os == 'windows-latest'
        shell: pwsh
        env:
          GITHUB_TOKEN: ${{ github.token }}
          LUCIDA_INSTALL_DIR: ${{ runner.temp }}\lucida-bin
        run: |
          ./install.ps1
          & "$env:LUCIDA_INSTALL_DIR\lucida.exe" --version

      # The pin is derived, not written down. A literal `v0.7.0` here made this
      # job depend on that one release existing forever, so the check would have
      # failed for a reason that had nothing to do with the installer. That was
      # not hypothetical: on 2026-08-07 everything below v0.9.0 was pruned,
      # v0.7.0 included. The second-newest release is by definition not the
      # latest, which is the whole property under test — and with two releases
      # left, this now resolves v0.9.0. Below two, the step exits 0 saying so.
      - name: A pinned version installs that version, not the latest
        if: matrix.os != 'windows-latest'
        env:
          GITHUB_TOKEN: ${{ github.token }}
          LUCIDA_INSTALL_DIR: ${{ runner.temp }}/lucida-pinned
        run: |
          tag=$(gh api "repos/$GITHUB_REPOSITORY/releases" \
                  --jq 'map(select(.draft == false)) | .[1].tag_name // ""')
          if [ -z "$tag" ]; then
            echo "only one release exists, so there is no older version to pin against"
            exit 0
          fi
          LUCIDA_VERSION="$tag" sh install.sh
          got=$("$LUCIDA_INSTALL_DIR/lucida" --version)
          want="lucida ${tag#v}"
          case "$got" in
            "$want") echo "ok: pinned $tag and got \"$got\", not the latest" ;;
            *) echo "LUCIDA_VERSION was ignored: wanted \"$want\", got \"$got\""; exit 1 ;;
          esac