gwm-cli 1.6.0

git worktree manager — TUI + CLI, native libgit2, per-repo bootstrap
Documentation
name: ci

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

env:
  CARGO_TERM_COLOR: always
  RUSTFLAGS: -D warnings
  # Force repo-only config loading so the suite never picks up a
  # runner's user-level ~/.config/gwm/config.toml (issue #190 opt-out).
  GWM_NO_GLOBAL_CONFIG: "1"

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

jobs:
  fmt:
    name: rustfmt
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
        with:
          persist-credentials: false
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt
      - name: cargo fmt --check
        run: cargo fmt --all -- --check

  clippy:
    name: clippy
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
        with:
          persist-credentials: false
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy
      - uses: Swatinem/rust-cache@v2
      - name: cargo clippy
        run: cargo clippy --all-targets --all-features -- -D warnings

  msrv:
    name: msrv (declared floor, ${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    # Same matrix as `test`, and for the same reason: a single-platform run
    # sees neither the `[target."cfg(windows)".dependencies]` block nor the
    # `#[cfg(windows)]` code, so a Windows-only dependency raising its floor
    # (or a Windows-only use of a newer std API) would keep this job green on
    # Linux while `cargo install` breaks for those users. That is the #491 bug
    # again, one axis over.
    #
    # The clippy job catches a *std API* newer than the declared floor
    # (`clippy::incompatible_msrv`), but nothing covered a **dependency**
    # whose floor is higher than ours: `Cargo.toml` said 1.86 for a whole
    # release line while the graph needed 1.95, silently (issue #491).
    # This job installs exactly the declared floor and both resolves and
    # compiles the committed lockfile against it, which is why `--locked`
    # is load-bearing: cargo's `rust-version` gate fires at resolve time
    # for the crates that declare a floor, and the compile that follows
    # is the only thing that catches one declaring nothing at all (which
    # is what `rusqlite`'s `libsqlite3-sys` does). The toolchain is read
    # out of `Cargo.toml` rather than hardcoded so the job cannot drift
    # from the manifest either.
    env:
      # Overrides the workflow-wide `-D warnings`. A warning only an older
      # toolchain emits (or an `unknown_lints` for a lint added after it)
      # says nothing about the MSRV and must not paint this job red.
      RUSTFLAGS: ""
    steps:
      - uses: actions/checkout@v7
        with:
          persist-credentials: false
      - name: read the declared MSRV
        id: msrv
        # `windows-latest` defaults to PowerShell, which has no grep/cut.
        shell: bash
        run: |
          version=$(grep -m1 '^rust-version = ' Cargo.toml | cut -d'"' -f2)
          [ -n "$version" ] || { echo "::error::no rust-version in Cargo.toml"; exit 1; }
          echo "version=$version" >> "$GITHUB_OUTPUT"
          echo "declared MSRV: $version"
      - uses: dtolnay/rust-toolchain@master
        with:
          toolchain: ${{ steps.msrv.outputs.version }}
      - uses: Swatinem/rust-cache@v2
      - name: cargo check at the declared floor
        run: cargo check --all-targets --locked
      # `daemon` is default-on, so the check above never compiles the
      # `#[cfg(not(all(any(unix, windows), feature = "daemon")))]` arms of
      # `cmd_daemon` / `cmd_statusline`. That build is documented as supported
      # (docs/3.cli/1.reference.md), so it gets the same floor guarantee.
      - name: cargo check at the declared floor (no default features)
        run: cargo check --all-targets --locked --no-default-features

  test:
    name: test (${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    steps:
      - uses: actions/checkout@v7
        with:
          persist-credentials: false
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: configure git identity (libgit2 commit needs one)
        run: |
          git config --global user.email "ci@gwm.test"
          git config --global user.name "ci"
      - name: cargo build
        run: cargo build --verbose
      - name: cargo test
        run: cargo test --verbose

  hook-smoke:
    name: pre-commit hook smoke
    runs-on: ubuntu-latest
    # Cheap regression net for .githooks/pre-commit. Runs in parallel with
    # the heavy `test` job — no Rust toolchain needed, just shell + git.
    steps:
      - uses: actions/checkout@v7
        with:
          persist-credentials: false
      - name: shellcheck pre-commit hook
        uses: ludeeus/action-shellcheck@master
        with:
          scandir: ./.githooks
          severity: error
      - name: hook is executable
        run: test -x .githooks/pre-commit
      - name: hook short-circuits on empty index
        run: |
          scratch=$(mktemp -d)
          (
            cd "$scratch"
            git init -q
            git config user.email "ci@gwm.test"
            git config user.name "ci"
            cp "$GITHUB_WORKSPACE/.githooks/pre-commit" pre-commit
            sh pre-commit
          )
      - name: gate 2 detects .gwm.toml and skips when gwm absent
        run: |
          scratch=$(mktemp -d)
          (
            cd "$scratch"
            git init -q
            git config user.email "ci@gwm.test"
            git config user.name "ci"
            cp "$GITHUB_WORKSPACE/.githooks/pre-commit" pre-commit
            echo "[bootstrap]" > .gwm.toml
            git add .gwm.toml
            out=$(PATH="/usr/bin:/bin" sh pre-commit 2>&1)
            echo "$out"
            echo "$out" | grep -q "gwm not in PATH" \
              || (echo "FAIL: gate 2 should print 'gwm not in PATH' skip message" && exit 1)
          )

  audit:
    name: cargo audit
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
        with:
          persist-credentials: false
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: install cargo-audit
        run: cargo install cargo-audit --locked
      # `--deny warnings` makes warning-class advisories (unmaintained /
      # unsound / yanked) fail the job, not just outright vulnerabilities.
      # Plain `cargo audit` exits 0 on those, which is why RUSTSEC-2025-0068
      # (serde_yml, unsound + unmaintained) slipped past for ~9 months even
      # before accounting for `continue-on-error`. Accepted advisories go in
      # `audit.toml` (`[advisories] ignore = […]`) with a rationale so each
      # is a conscious decision (issue #340).
      - name: cargo audit
        run: cargo audit --deny warnings

  doctor:
    name: gwm doctor (advisory)
    runs-on: ubuntu-latest
    needs: test
    # Restrict to the `dev` integration branch only — `main` is meant to be
    # stable, the doctor exists to catch in-development regressions before
    # they reach a release. Without this guard the job would also run on
    # every push/PR targeting `main` (the workflow header lists both).
    if: |
      (github.event_name == 'push' && github.ref == 'refs/heads/dev') ||
      (github.event_name == 'pull_request' && github.base_ref == 'dev')
    steps:
      - uses: actions/checkout@v7
        with:
          persist-credentials: false
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: configure git identity
        run: |
          git config --global user.email "ci@gwm.test"
          git config --global user.name "ci"
      # Build the binary once with the same release profile gh actions
      # cache benefits from, then ask it to diagnose this very repo.
      # Advisory: a non-zero exit means we want eyes on the report, but
      # not a blocked merge. `lazygit` is intentionally absent on the
      # runner so a Warning here is the floor, not a regression.
      - name: cargo build
        run: cargo build --release --quiet
      - name: gwm doctor
        run: ./target/release/gwm doctor
        continue-on-error: true