worktrunk 0.37.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

  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

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

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

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

    - name: Install shells (zsh, fish) - Ubuntu
      if: runner.os == 'Linux'
      uses: awalsh128/cache-apt-pkgs-action@latest
      with:
        packages: zsh fish
        version: 1.0

    - name: Install shells (fish) - macOS
      if: runner.os == 'macOS'
      run: |
        brew install fish
        # bash and zsh are pre-installed on macOS

    - name: Install nushell
      if: runner.os != 'Windows'
      uses: hustcer/setup-nu@v3
      with:
        version: '*'

    - name: Install shells - Windows
      if: runner.os == 'Windows'
      run: |
        # Git Bash is pre-installed on GitHub Actions Windows runners
        # Fish, zsh, and nushell are not commonly available on native Windows (require WSL/MSYS2)
        # CI will test with bash only on Windows (tests skip unavailable shells)
        echo "Using Git Bash for Windows tests"
      shell: pwsh

    - 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.10
      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: '*'

    - 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

  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: '*'

    # 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 }}