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
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
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:
pull_request:
branches:
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() }}
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.4
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.77.0
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@v1.6.0
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/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 -- --features shell-integration-tests
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@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 }}