worktrunk 0.50.0

A CLI for Git worktree management, designed for parallel AI agent workflows
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
name: ci

# Runner versions are pinned to avoid surprise breaking changes from -latest migrations.
# The tend-weekly workflow checks for updates and opens bump PRs (see
# .claude/skills/running-tend/SKILL.md โ†’ "Weekly Maintenance: CI Pin Bumps").
# - 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.24.2"

    - 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.49.0"

    - 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

    # Anything tests leave behind in the working tree is a leak we want to see
    # before it lands. `target/` is gitignored, so a normal run is clean โ€”
    # untracked files here mean a test wrote outside its sandbox (the
    # `default_*.profraw` stray that prompted this guard is the canonical case).
    - name: ๐Ÿงน Verify clean working tree
      shell: bash
      run: |
        if [ -n "$(git status --porcelain)" ]; then
          echo "::error::tests left files behind in the working tree:"
          git status --porcelain
          exit 1
        fi

    - 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() }}
      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.3"

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

  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.77.5
      with:
        tool: zola@0.22.1

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

  # 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/coverage.db`
  #   to actions/cache keyed on the main commit sha. We cache only the DB,
  #   not the parent dir โ€” cargo-affected drops its profraw staging dir
  #   at the end of every successful collect, but caching the path
  #   explicitly keeps the contract obvious and ~10 GB of profile bundles
  #   from leaking into the cache if that cleanup ever regresses.
  # - `collect-affected` also restores the most recent prior main DB before
  #   collecting, so the new DB accumulates rows for up to FINGERPRINT_KEEP
  #   (=10) recent main-tip env_fingerprints (LRU-evicted in `Db::gc`).
  #   PRs whose manifests match any of those fingerprints get exact-match
  #   selection instead of the all-or-nothing single-fingerprint cache.
  #   Don't "simplify" by removing the restore โ€” it's load-bearing.
  # - `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`
  #   is typically a sibling of the PR's HEAD, not a strict ancestor.
  #   cargo-affected uses any sha still in the repo as a diff anchor, so
  #   the fallback drives normal selection โ€” over-includes tests touched
  #   by main commits between the merge-base and collect_sha (correct
  #   superset), but never widens to "run everything" unless the cache is
  #   missing entirely.
  # - 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: ๐Ÿ’พ Restore prior coverage DB
      uses: actions/cache/restore@v5
      with:
        path: target/affected/coverage.db
        # Primary key matches the current sha (no-op on first push of this
        # commit; on re-runs of the same sha, lets us skip rebuilding from
        # scratch).
        key: cargo-affected-db-v1-${{ runner.os }}-${{ github.sha }}
        # Fall back to any prior main DB. cargo-affected preserves rows for
        # up to FINGERPRINT_KEEP (=10) distinct fingerprints in one DB,
        # evicting LRU on each collect. By feeding a prior DB into the new
        # collect, we accumulate fingerprint snapshots across main commits โ€”
        # PRs whose manifests match any of the last ~10 main fingerprints
        # get exact-match selection, instead of the all-or-nothing
        # single-fingerprint cache.
        restore-keys: |
          cargo-affected-db-v1-${{ runner.os }}-

    - 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:
        # Only the SQLite DB. `target/affected/profraw-<PID>/` holds raw
        # profile bundles (~5 GB on this repo) that aren't needed past the
        # current collect โ€” caching them blows past the 10 GB repo cache
        # cap and forces eviction of every prior cache.
        path: target/affected/coverage.db
        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"

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

    # Restore AFTER test-setup so we land on top of rust-cache's tar
    # extraction. rust-cache's restore step calls `cleanTargetDir` on a
    # partial cache hit (`full match: false`), which deletes file children
    # of `target/affected/` โ€” including a `coverage.db` we'd just dropped
    # there. Order: rust-cache populates `target/` first; we then drop the
    # DB on top, where nothing else touches it.
    - name: ๐Ÿ’พ Restore coverage DB
      uses: actions/cache/restore@v5
      with:
        path: target/affected/coverage.db
        # 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
        # the cached `collect_sha` is missing from the repo (rebased and
        # pruned, beyond a shallow clone boundary). Sibling shas โ€” including
        # PR-vs-main-tip โ€” drive normal selection.
        restore-keys: |
          cargo-affected-db-v1-${{ runner.os }}-

    - 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 --report-json target/affected/report.json -- --features shell-integration-tests

    # Report writes BEFORE nextest runs, so it survives test failures โ€”
    # uploading on `!cancelled()` makes it the most useful diagnostic
    # when tests fail (cache state, fingerprint divergence, selection
    # reasons). Schema documented at:
    # https://github.com/max-sixty/cargo-affected/blob/main/docs/report-json.md
    - name: ๐Ÿ“Š Upload cargo-affected report
      if: ${{ !cancelled() }}
      uses: actions/upload-artifact@v7
      with:
        name: cargo-affected-report
        path: target/affected/report.json

  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.6"

    - 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@v1.6.0
      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
      uses: codecov/codecov-action@v6.0.0
      with:
        files: cobertura.xml
        # Soft-fail on fork PRs: secrets aren't available there, so we rely on
        # tokenless upload, which can hiccup. Keep hard-fail on the main repo.
        fail_ci_if_error: ${{ github.repository_owner == 'max-sixty' }}
        token: ${{ secrets.CODECOV_TOKEN }}