worktrunk 0.46.1

A CLI for Git worktree management, designed for parallel AI agent workflows
Documentation
name: ci

# Runner versions are pinned to avoid surprise breaking changes from -latest migrations.
# The tend-renovate workflow checks weekly for updates and creates PRs.
# - windows-2022: Pinned because windows-2025 lacks D: drive (actions/runner-images#12677)
# - ubuntu-24.04, macos-15: Pinned to current -latest equivalents

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

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:
  CARGO_TERM_COLOR: always
  # CI does clean builds, so incremental compilation tracking adds I/O overhead
  # with no benefit. Especially impactful on Windows where I/O is the bottleneck.
  CARGO_INCREMENTAL: 0
  RUSTFLAGS: -C debuginfo=0
  # Authenticate lychee requests to GitHub to avoid rate limiting
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

jobs:
  lint:
    runs-on: ubuntu-24.04
    steps:
    - name: ๐Ÿ“‚ Checkout code
      uses: actions/checkout@v6

    - name: ๐Ÿ’ฐ Cache
      uses: Swatinem/rust-cache@v2

    - name: Install lychee
      uses: baptiste0928/cargo-install@v3
      with:
        crate: lychee
        version: "=0.23.0"

    - name: ๐Ÿ” Pre-commit hooks
      uses: pre-commit/action@v3.0.1

  feature-check:
    # Guards the library/CLI cleave: the `cli` feature gates clap, skim,
    # crossterm, termimad, env_logger, humantime. If anything reachable from
    # `pub mod` in src/lib.rs starts using one of those crates, library
    # consumers (e.g. worktrunk-sync) would silently regain the transitive
    # dependency graph. These checks fail before that can ship.
    runs-on: ubuntu-24.04
    steps:
    - name: ๐Ÿ“‚ Checkout code
      uses: actions/checkout@v6

    - name: ๐Ÿ’ฐ Cache
      uses: Swatinem/rust-cache@v2

    - name: Library builds without any features
      run: cargo check --lib --no-default-features

    - name: Library builds with syntax-highlighting only
      run: cargo check --lib --no-default-features --features syntax-highlighting

    - name: Binary builds without syntax-highlighting
      run: cargo check --bin wt --no-default-features --features cli

  test:
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-24.04
            name: linux
          - os: macos-15
            name: macos
          - os: windows-2022
            name: windows
    name: test (${{ matrix.name }})
    runs-on: ${{ matrix.os }}

    steps:
    - name: ๐Ÿ“‚ Checkout code
      uses: actions/checkout@v6

    - uses: ./.github/actions/test-setup

    - name: Install wt
      uses: baptiste0928/cargo-install@v3
      with:
        crate: worktrunk
        version: "=0.35.2"

    - name: "Use fast D: drive for temp files (Windows)"
      if: runner.os == 'Windows'
      shell: pwsh
      run: |
        New-Item -ItemType Directory -Force -Path "D:\tmp" | Out-Null
        echo "TEMP=D:\tmp" >> $env:GITHUB_ENV
        echo "TMP=D:\tmp" >> $env:GITHUB_ENV

    - name: ๐Ÿงช Pre-merge hooks
      run: wt hook pre-merge --yes insta doctest doc

    - name: ๐Ÿ“Š Upload test timing
      if: always()
      uses: actions/upload-artifact@v7
      with:
        name: junit-${{ matrix.name }}
        path: target/nextest/default/junit.xml

    - name: ๐Ÿ“Š Upload test results to Codecov
      if: ${{ !cancelled() && github.repository_owner == 'max-sixty' }}
      uses: codecov/codecov-action@v6.0.0
      with:
        token: ${{ secrets.CODECOV_TOKEN }}
        files: target/nextest/default/junit.xml
        report_type: test_results

  # Check if Cargo/CI files changed (for conditional jobs below)
  changes:
    runs-on: ubuntu-24.04
    permissions:
      contents: read
      pull-requests: read
    outputs:
      cargo: ${{ steps.filter.outputs.cargo }}
      docs: ${{ steps.filter.outputs.docs }}
    steps:
    - name: ๐Ÿ“‚ Checkout code
      uses: actions/checkout@v6
    - uses: dorny/paths-filter@v4
      id: filter
      with:
        filters: |
          cargo:
            - 'Cargo.toml'
            - 'Cargo.lock'
            - 'rust-toolchain.toml'
            - '.github/workflows/ci.yaml'
          docs:
            - 'docs/**'
            - 'src/cli/mod.rs'

  msrv:
    runs-on: ubuntu-24.04
    needs: changes
    if: needs.changes.outputs.cargo == 'true' || github.event_name == 'workflow_dispatch'
    steps:
    - name: ๐Ÿ“‚ Checkout code
      uses: actions/checkout@v6

    - name: ๐Ÿ’ฐ Cache
      uses: Swatinem/rust-cache@v2

    - uses: baptiste0928/cargo-install@v3
      with:
        crate: cargo-msrv
        version: "=0.19.2"

    - name: Verify minimum rust version
      run: cargo msrv verify

  check-unused-dependencies:
    runs-on: ubuntu-24.04
    needs: changes
    if: needs.changes.outputs.cargo == 'true' || github.event_name == 'workflow_dispatch'
    steps:
    - name: ๐Ÿ“‚ Checkout code
      uses: actions/checkout@v6

    # cargo-udeps requires nightly; update date periodically
    - run: rustup override set nightly-2026-03-01

    - name: ๐Ÿ’ฐ Cache
      uses: Swatinem/rust-cache@v2

    - uses: baptiste0928/cargo-install@v3
      with:
        crate: cargo-udeps
        version: "=0.1.60"

    - uses: clechasseur/rs-cargo@v5.0.3
      with:
        command: udeps
        args: --all-targets

  check-docs:
    runs-on: ubuntu-24.04
    needs: changes
    if: needs.changes.outputs.docs == 'true' || github.event_name == 'workflow_dispatch'
    steps:
    - name: ๐Ÿ“‚ Checkout code
      uses: actions/checkout@v6

    - name: Setup Zola
      uses: taiki-e/install-action@v2.75.27
      with:
        tool: zola@0.22.1

    - name: ๐Ÿ•ท๏ธ Build docs
      run: zola build
      working-directory: docs

  benchmarks:
    runs-on: ubuntu-24.04
    needs: changes
    if: needs.changes.outputs.cargo == 'true' || github.event_name == 'workflow_dispatch'
    steps:
    - name: ๐Ÿ“‚ Checkout code
      uses: actions/checkout@v6

    - name: ๐Ÿ’ฐ Cache
      uses: Swatinem/rust-cache@v2

    - name: Install shells (zsh, fish)
      uses: awalsh128/cache-apt-pkgs-action@latest
      with:
        packages: zsh fish
        version: 1.0

    - name: Install nushell
      uses: hustcer/setup-nu@v3
      with:
        version: '0.112.2'

    - name: ๐Ÿ’ฐ Rust repo cache
      uses: actions/cache@v5
      with:
        path: target/bench-repos
        key: bench-repos-rust-${{ runner.os }}

    - name: ๐Ÿ“Š Run benchmarks
      run: cargo bench

  # cargo-affected: runs only the tests whose recorded coverage overlaps the
  # diff. Advisory at this stage โ€” `test (linux/macos/windows)` still runs the
  # full suite. The signal is the runtime delta on the exact-match path and
  # whether selection misses any failures the full suite catches.
  #
  # TODO: once cargo-affected is trusted on this repo, gate the full test
  # matrix to run only on pushes to main and on PRs with a `full-tests` label
  # (or similar). PR pushes would default to affected-only. The full suite
  # could fold into the existing `nightly` workflow, which already hosts
  # slower integration-time checks. Periodic full runs remain essential โ€”
  # cargo-affected misses non-Rust inputs (`include_str!`, templates, SQL),
  # build-time inputs not in its fingerprint (build.rs, rust-toolchain.toml,
  # .cargo/config.toml), and proc-macro source edits.
  #
  # Cache strategy:
  # - `collect-affected` (push to main) saves `target/affected/` to
  #   actions/cache keyed on the main commit sha.
  # - `affected-tests` (PRs) restores the cache. Primary key is the
  #   PR/main merge-base โ€” when collect ran on that exact commit, we get a
  #   tight diff (`PR changes only`) and the smallest possible selection.
  # - Restore-keys fall back to the most recent main DB. Its `collect_sha`
  #   may be ahead of the merge-base; cargo-affected handles that by running
  #   all tests with a notice (no `Diverged` bail), so the fallback always
  #   produces a useful run, just with broader selection.
  # - Cache keys include `runner.os` (fingerprint embeds `rustc -vV`) and a
  #   manual `db-v{N}` marker. Bump the marker if cargo-affected ships an
  #   on-disk schema change; the cache is otherwise version-agnostic.
  collect-affected:
    name: collect affected coverage
    if: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch' }}
    runs-on: ubuntu-24.04
    steps:
    - name: ๐Ÿ“‚ Checkout code
      uses: actions/checkout@v6
      with:
        # `cargo affected run` later diffs PR HEAD against the sha that was
        # HEAD when collect ran. That sha must be reachable.
        fetch-depth: 0

    - uses: ./.github/actions/test-setup

    - name: Install cargo-affected
      uses: baptiste0928/cargo-install@v3
      with:
        crate: cargo-affected
        git: https://github.com/max-sixty/cargo-affected
        # No rev/branch/tag โ†’ action resolves to latest default-branch sha.

    - name: Install llvm-tools
      run: rustup component add llvm-tools

    - name: ๐Ÿ“Š Collect coverage data
      env:
        NEXTEST_NO_INPUT_HANDLER: 1
      run: cargo affected collect --features shell-integration-tests

    - name: ๐Ÿ’พ Save coverage DB
      uses: actions/cache/save@v5
      with:
        path: target/affected
        key: cargo-affected-db-v1-${{ runner.os }}-${{ github.sha }}

  affected-tests:
    name: affected tests (linux, advisory)
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-24.04
    # Advisory while we calibrate selection accuracy against the full suite.
    # The full matrix (`test (linux/macos/windows)`) is still required for
    # merge.
    continue-on-error: true
    steps:
    - name: ๐Ÿ“‚ Checkout code
      uses: actions/checkout@v6
      with:
        # Need the cached `collect_sha` reachable from HEAD for the diff;
        # see collect-affected above.
        fetch-depth: 0

    - name: Compute merge-base for cache key
      id: mb
      env:
        PR_HEAD: ${{ github.event.pull_request.head.sha }}
      run: |
        git fetch --no-tags --depth=200 origin main
        sha=$(git merge-base origin/main "$PR_HEAD")
        echo "sha=$sha" >> "$GITHUB_OUTPUT"
        echo "merge-base with origin/main: $sha"

    - name: ๐Ÿ’พ Restore coverage DB
      uses: actions/cache/restore@v5
      with:
        path: target/affected
        # Exact match: collect ran on the PR's merge-base โ†’ tight diff.
        key: cargo-affected-db-v1-${{ runner.os }}-${{ steps.mb.outputs.sha }}
        # Fallback: most recent main DB. cargo-affected runs all tests when
        # collect_sha isn't an ancestor of HEAD; with no DB at all it does
        # the same. Either way the run produces a useful signal.
        restore-keys: |
          cargo-affected-db-v1-${{ runner.os }}-

    - uses: ./.github/actions/test-setup

    - name: Install cargo-affected
      uses: baptiste0928/cargo-install@v3
      with:
        crate: cargo-affected
        git: https://github.com/max-sixty/cargo-affected
        # No rev/branch/tag โ†’ action resolves to latest default-branch sha.

    - name: ๐ŸŽฏ Run affected tests
      env:
        NEXTEST_NO_INPUT_HANDLER: 1
      run: cargo affected run --features shell-integration-tests --verbose

  code-coverage:
    runs-on: ubuntu-24.04
    steps:
    - name: ๐Ÿ“‚ Checkout code
      uses: actions/checkout@v6

    - uses: baptiste0928/cargo-install@v3
      with:
        crate: cargo-llvm-cov
        version: "=0.8.5"

    - name: ๐Ÿ’ฐ Cache
      uses: Swatinem/rust-cache@v2
      with:
        save-if: ${{ github.ref == 'refs/heads/main' }}

    - name: Install shells (zsh, fish)
      uses: awalsh128/cache-apt-pkgs-action@latest
      with:
        packages: zsh fish
        version: 1.0

    - name: Install nushell
      uses: hustcer/setup-nu@v3
      with:
        version: '0.112.2'

    # Ensure nothing remains from caching
    - run: cargo llvm-cov clean --workspace

    - name: ๐Ÿ“Š Generate coverage report
      env:
        NEXTEST_NO_INPUT_HANDLER: 1
      run: cargo llvm-cov --features shell-integration-tests --cobertura --output-path=cobertura.xml

    - name: Upload code coverage results
      uses: actions/upload-artifact@v7
      with:
        name: code-coverage-report
        path: cobertura.xml

    - name: Upload to codecov.io
      # Codecov action raises errors on forks; allow running on PRs to main repo
      if: github.repository_owner == 'max-sixty'
      uses: codecov/codecov-action@v6.0.0
      with:
        files: cobertura.xml
        fail_ci_if_error: true
        token: ${{ secrets.CODECOV_TOKEN }}