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
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
name: CI
on:
push:
branches:
tags:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
KACHE_LOG: kache=debug
jobs:
check:
name: Check (${{ matrix.os }})
runs-on: ${{ matrix.runner }}
# Job-level cap so a hang in checkout/mise/build is killed in minutes, not
# the 6h GitHub default — the step-level timeouts below only guard their own
# steps. (`just ci` itself is additionally capped at 30m.)
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
include:
- os: Linux
runner: ubuntu-latest
# `check` runs the full `just ci` (docker buildx + helm + tarpaulin)
# and stays Linux-only. macOS gets a dedicated, lighter `test-macos`
# job below — see that job for the rationale.
steps:
- uses: actions/checkout@v6
# mise installs everything in mise.toml: rust (with rustfmt +
# clippy via the rust tool-options block), just, helm, sccache,
# cargo-llvm-cov.
- uses: jdx/mise-action@v4
with:
# Pin the mise CLI. v2026.6.10 (2026-06-14) regressed binary
# resolution ("mise ERROR cannot find binary path"), reddening every
# run after its release — including unchanged code that passed hours
# earlier. mise.toml already pins every *tool* for exactly this
# reason; the CLI was the one unpinned surface. Bump deliberately once
# upstream fixes it.
version: 2026.6.9
cache: false
- uses: docker/setup-buildx-action@v4
- uses: kunobi-ninja/kache-action@v1
with:
github-cache: "true"
cache-executables: "false"
- name: Clean bootstrap cache artifacts
run: cargo clean
- name: Run repo verification
# kache-action exports the installed release version; self-checks must
# compile with Cargo.toml's version from this PR.
run: env -u KACHE_VERSION just ci
timeout-minutes: 30
- name: Check crates.io package metadata
run: |
version="$(cargo pkgid -p kache-core | sed 's/.*#//')"
cargo package -p kache-core --locked
# Poll the sparse index (index.crates.io), not `cargo search`: the
# search index is eventually-consistent and lagged on the 0.4.0 cut.
if curl -sf -A kache-ci-metadata-check "https://index.crates.io/ka/ch/kache-core" | grep -q "\"vers\":\"$version\""; then
cargo package -p kache --locked
else
echo "kache-core $version is not on crates.io yet; skipping kache package verification"
fi
env:
RUSTC_WRAPPER: ""
- name: Check coverage threshold (35%)
# Enforced on PRs too, not just push: in a trunk-based model a PR that
# drops coverage must fail before merge, not after on the main push.
run: |
python3 -c "
import json, sys
data = json.load(open('tmp/llvm-cov/coverage.json'))
coverage = data['data'][0]['totals']['lines']['percent']
threshold = 35.0
print(f'Coverage: {coverage:.1f}%')
if coverage < threshold:
print(f'FAIL: below threshold {threshold}%')
sys.exit(1)
print(f'OK: >= {threshold}%')
"
- name: Upload HTML coverage report
uses: actions/upload-artifact@v7
# Skip on tag/release runs: the shared release workflow downloads *all*
# run artifacts and `gh release upload artifacts/*` chokes on directory
# artifacts like this one. Coverage HTML is only useful on PR/branch runs.
if: always() && github.ref_type != 'tag'
with:
name: coverage-html
path: tmp/llvm-cov/html
retention-days: 14
# Fast, hermetic version gate (no nix / no cargo / no network). Runs on every
# push & PR in internal mode (the shipping manifests must agree), and on a v*
# tag additionally asserts the tag matches the manifest BEFORE the release job
# builds anything. It is a `release` need (below), so a drifted tag — e.g. the
# v0.5.0-rc.* tags that were cut against a 0.4.1 manifest — fails here in
# seconds and never reaches the irreversible binary build / GitHub Release.
# The old "Check Nix package version matches Cargo" step lived in nix-package
# but compared two values both derived from Cargo.toml (tautological); this
# gate replaces it. The publish floor re-runs the script directly (not by
# matching a job name), so no job-name coupling is required.
version-consistency:
name: Version consistency
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- name: Manifests agree (and match the tag on a v* tag)
# Pass the ref via a quoted env var (not `${{ }}` interpolated into the
# script) — a tag name can legally contain shell metacharacters.
env:
REF_TYPE: ${{ github.ref_type }}
REF_NAME: ${{ github.ref_name }}
run: |
if [ "$REF_TYPE" = "tag" ]; then
./scripts/check-version-consistency.sh "$REF_NAME"
else
./scripts/check-version-consistency.sh
fi
nix-package:
name: Nix package
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v6
- uses: DeterminateSystems/nix-installer-action@v22
- name: Check flake evaluation
run: nix flake check --no-build
- name: Build flake package
run: nix build .#kache --no-link --print-build-logs
# --- E2E smoke test ---
# Builds kache, configures as RUSTC_WRAPPER, cold-builds a fixture project,
# cleans, warm-builds, and verifies cache hits work end-to-end.
e2e:
name: E2E smoke (${{ matrix.os }})
runs-on: ${{ matrix.runner }}
# Per-arm cap. The self-hosted macOS/Windows runners are persistent and have
# hung in `checkout` on stale git refs (#208 run hung ~51m before manual
# cancel); without a job timeout the default is 6h. Normal runs are <10m.
timeout-minutes: 30
# All three arms are blocking. Windows was non-blocking while its port
# landed (the cache store #196, the `.sh` fallback wrappers #197, the
# rust-c-ffi cc-rs path #198, the harness `.exe`/probe/OUT_DIR fixes
# #201/#202); now that every fixture passes on Windows it gates like
# Linux/macOS. The publish gates (`require-ci-green.sh`) also await
# "E2E smoke (Windows)".
# Run every step through bash on all three OSes. On Windows the bash comes
# from Git for Windows (already present — checkout needs git), so the shared
# step bodies below run identically everywhere. `exe_suffix` (matrix) is the
# only build-output difference: Windows binaries are `.exe`.
defaults:
run:
shell: bash
strategy:
fail-fast: false
matrix:
include:
- os: Linux
runner: ubuntu-latest
exe_suffix: ""
# Use the self-hosted Apple Silicon runners only inside
# kunobi-ninja and not for fork PRs; otherwise fall back to a
# GitHub-hosted runner (a fork cannot reach our runners, so a
# hardcoded label would queue forever). NOTE: this is a
# convenience fallback, not a security control — for a `pull_request`
# the fork's own copy of this file runs, so a fork can override it.
# Fork PRs are gated by the repo's "require approval" Actions
# setting, which is the actual boundary.
- os: macOS
runner: ${{ (github.repository_owner == 'kunobi-ninja' && github.event.pull_request.head.repo.fork != true) && fromJSON('["self-hosted", "macOS", "ARM64"]') || 'macos-latest' }}
exe_suffix: ""
# Self-hosted Windows runner shared with this repo (kunobi org,
# group `kunobi`, label `kunobi-windows`). No fork-fallback expression
# like macOS: it's an internal runner and the Windows e2e is internal
# (#77); a fork PR simply can't reach it. Blocking like the other arms.
- os: Windows
runner: ${{ fromJSON('["self-hosted", "Windows", "X64", "kunobi-windows"]') }}
exe_suffix: ".exe"
steps:
- uses: actions/checkout@v6
# Checkout has hung on the persistent self-hosted runners' stale git
# state (#208); a checkout never legitimately runs this long, so fail
# fast rather than waiting out the whole job timeout.
timeout-minutes: 10
# Fail fast with an explicit checklist if the self-hosted Windows runner
# is missing a build/fixture tool, instead of an opaque failure deep in
# the cargo build or a fixture's `make`. Mirrors the verification step in
# ans-runners/tasks/setup_windows.yml — keep the two tool lists in sync.
# cc/c++/make -> C/C++ fixtures (harness sets CC="$KACHE cc")
# nasm -> ring's x86_64 `.asm` (the rustls crypto provider).
# perl/cmake are no longer needed: they were aws-lc-sys
# build deps, and aws-lc-sys was dropped in favour of ring.
# MSVC link.exe presence is proven by a successful `cargo build`.
- name: Verify Windows build toolchain
if: runner.os == 'Windows'
run: |
missing=
for t in cc c++ make nasm; do
if ! command -v "$t" >/dev/null 2>&1; then
echo "MISSING on runner PATH: $t — provision it (see ans-runners/tasks/setup_windows.yml)"
missing=1
fi
done
[ -z "$missing" ] || exit 1
echo "all build/fixture tools present"
# Point tool state at the per-run temp dir before installing. All the
# self-hosted runners (macOS AND Windows) are persistent: shared rustup /
# mise state goes stale and makes `mise --force rust` try to remove a
# locked toolchain dir (`Access is denied (os error 5)` on Windows), and
# `rustup target add` against a shared toolchain poisons every later job.
# Keep Rust + mise installs disposable per job so one run can't break the
# next. Runs everywhere via `shell: bash` (Git Bash on Windows).
- name: Isolate tool state
run: |
mkdir -p \
"$RUNNER_TEMP/rustup" \
"$RUNNER_TEMP/cargo" \
"$RUNNER_TEMP/mise-data/bin" \
"$RUNNER_TEMP/mise-data/shims" \
"$RUNNER_TEMP/mise-cache" \
"$RUNNER_TEMP/mise-state"
rm -rf "$RUNNER_TEMP/mise"
# Fresh, per-(instance,run) scratch for mise-action's download/extract,
# exported as MISE_DL_TMPDIR for the install step below. On Windows
# mise-action extracts with chocolatey `unzip.exe`, which prompts
# interactively on overwrite and hangs forever in CI (no stdin); and
# RUNNER_TEMP on the self-hosted runners is persistent (the service
# account's %TEMP% on Windows, possibly shared between instances), so
# leftovers from earlier runs collide. A unique dir can't collide;
# scope it AND the sweep to a sanitized $RUNNER_NAME so the sweep only
# ever removes THIS instance's dirs, never a sibling's in-use dir on a
# shared Temp. (Windows ignores $TMPDIR, so the install step also sets
# %TEMP%/%TMP% to this path.)
safe_runner=$(printf '%s' "$RUNNER_NAME" | tr -c 'A-Za-z0-9_.-' '_')
rm -rf "$RUNNER_TEMP"/mise-dl-"$safe_runner"-* 2>/dev/null || true
mise_dl="$RUNNER_TEMP/mise-dl-$safe_runner-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
mkdir -p "$mise_dl"
echo "MISE_DL_TMPDIR=$mise_dl" >> "$GITHUB_ENV"
echo "RUSTUP_HOME=$RUNNER_TEMP/rustup" >> "$GITHUB_ENV"
echo "CARGO_HOME=$RUNNER_TEMP/cargo" >> "$GITHUB_ENV"
echo "MISE_DATA_DIR=$RUNNER_TEMP/mise-data" >> "$GITHUB_ENV"
echo "MISE_CACHE_DIR=$RUNNER_TEMP/mise-cache" >> "$GITHUB_ENV"
echo "MISE_STATE_DIR=$RUNNER_TEMP/mise-state" >> "$GITHUB_ENV"
# `cache: false` so the self-hosted runner's mise state never
# round-trips through GHA cache (it accumulates broken shims).
# The state dirs above are fresh per job, so there are no old
# shims to repair. Keep `reshim` off: the self-hosted macOS
# runner has failed inside `mise reshim -f` before any build ran.
# mise installs rust + sccache (the latter backs the
# `rust-sccache` fixture / KACHE_FALLBACK check). Tool names
# must match the mise.toml key — sccache is registered under
# the github backend, so the full `github:mozilla/sccache`
# identifier is required.
- name: Install tools via mise
uses: jdx/mise-action@v4
env:
# jdx/mise-action extracts into `$TMPDIR/mise` before moving
# the binary. On the shared macOS host, parallel runner
# instances share the default /var/folders/.../T path and can
# race each other. Scope TMPDIR to the action only so Cargo
# builds keep their normal environment.
# Unix uses $TMPDIR; Windows uses %TEMP%/%TMP%. Set all three so the
# download/extract lands in our per-run dir on every platform.
TMPDIR: ${{ env.MISE_DL_TMPDIR }}
TEMP: ${{ env.MISE_DL_TMPDIR }}
TMP: ${{ env.MISE_DL_TMPDIR }}
with:
# Pin mise CLI — see the bootstrap job's note (v2026.6.10 regression).
version: 2026.6.9
install_args: --force rust github:mozilla/sccache
cache: false
reshim: false
# mise installs the toolchain via rustup under RUSTUP_HOME, but on a
# runner with a pre-existing rustup it leaves no cargo proxy on PATH —
# so the stale system Rust (Homebrew 1.94) would win. Prepend the
# toolchain's own bin dir (the real 1.95 binaries) to PATH instead.
# macOS-runner workaround (see "Isolate tool state"): skipped on Windows,
# where mise-action puts cargo on PATH directly and there is no competing
# system Rust to shadow it.
- name: Put the Rust toolchain first on PATH
if: runner.os != 'Windows'
run: |
bin_dir=$(echo "$RUSTUP_HOME"/toolchains/*/bin)
if [ ! -x "$bin_dir/cargo" ]; then
echo "cargo not found under $RUSTUP_HOME/toolchains/*/bin"
ls -la "$RUSTUP_HOME/toolchains" 2>/dev/null || true
exit 1
fi
"$bin_dir/rustc" --version
echo "$bin_dir" >> "$GITHUB_PATH"
# The `rust-c-ffi` fixture builds with the windows-gnu target on Windows
# (its C half is MinGW/GNU — see that fixture's `[windows]` override); the
# MinGW linker is already provisioned. Add BOTH targets: on the
# mise-managed toolchain the host (msvc) std is bundled, but once
# `rustup target add` runs, rustup resolves std from its per-target dirs —
# so the host build breaks ("can't find crate for std") unless msvc is
# installed there too.
- name: Add Rust targets (msvc host + windows-gnu for rust-c-ffi)
if: runner.os == 'Windows'
run: rustup target add x86_64-pc-windows-msvc x86_64-pc-windows-gnu
- name: Build kache + e2e harness (release)
run: |
rustc --version && cargo --version
cargo build --release -p kache && cargo build --release -p kache-e2e
env:
RUSTC_WRAPPER: ""
- name: Run e2e harness (gate scenarios)
# Single Rust harness drives selected e2e scenarios from scenarios/
# through cold → warm → noop, applies per-fixture assertions
# against `kache report --format json`, and writes a single
# results.json. Replaces the previous per-language bash scripts.
run: |
mkdir -p tmp/e2e
# Bounded retry to absorb transient self-hosted-runner / Windows
# file-system flakes (e.g. a relocate-phase build hiccup). Each
# attempt is a full clean re-run, so cache assertions stay sound.
# A real failure fails all attempts; retries are logged as warnings
# so a genuinely-intermittent issue stays visible, not hidden.
n=0
until ./target/release/kache-scenario${{ matrix.exe_suffix }} \
--kache ./target/release/kache${{ matrix.exe_suffix }} \
--scenarios ./scenarios \
--select suite:e2e \
--select tier:gate \
--out tmp/e2e/results.json
do
n=$((n + 1))
if [ "$n" -ge 3 ]; then
echo "::error::e2e harness failed after $n attempts"
exit 1
fi
echo "::warning::e2e harness attempt $n failed; retrying (transient-flake mitigation)"
sleep 15
done
- name: Run e2e negative-control (falsifiability check)
# Reruns every fixture with kache disabled (KACHE_DISABLED=1)
# and asserts each non-exempt result FLIPS to a failure — a
# cache-dependent fixture that still passes with kache off has
# a vacuous test that does not actually exercise caching.
# Independent signal; `always()` so it runs even when the
# harness run above failed, as long as the build produced the
# binaries.
if: always() && hashFiles(format('target/release/kache-scenario{0}', matrix.exe_suffix)) != ''
run: |
./target/release/kache-scenario${{ matrix.exe_suffix }} \
--kache ./target/release/kache${{ matrix.exe_suffix }} \
--scenarios ./scenarios \
--select suite:e2e \
--select tier:gate \
--negative-control \
--out tmp/e2e/negative-control.json
- name: Check the sccache fallback caches an rlib
# The `rust-sccache` fixture (run by the harness above) proves
# the executable passthrough composes with sccache; sccache
# does not cache `bin` crates, so this step covers the path
# that does — a library compile kache passes through to
# sccache, asserting the rebuild is an sccache cache hit.
# Independent signal; `always()` so it runs even if the
# harness step above failed.
if: always() && hashFiles(format('target/release/kache{0}', matrix.exe_suffix)) != ''
run: |
# Bounded retry, same rationale as the harness step above.
n=0
until ./scripts/sccache-fallback-check.sh ./target/release/kache${{ matrix.exe_suffix }}
do
n=$((n + 1))
if [ "$n" -ge 3 ]; then
echo "::error::sccache fallback check failed after $n attempts"
exit 1
fi
echo "::warning::sccache fallback check attempt $n failed; retrying (transient-flake mitigation)"
sleep 15
done
- name: Show aggregated e2e results
if: always()
run: |
if [ -f tmp/e2e/results.json ]; then
echo "--- e2e results.json ---"
cat tmp/e2e/results.json
else
echo "no results.json produced"
fi
- name: Upload e2e results artifact
# Per-arm artifact name (#203): with upload-artifact@v4+ an artifact
# name is unique per run, so all three matrix arms uploading the same
# `e2e-results` collide — `gh run download -n e2e-results` then returns
# an arbitrary arm's results and the others are lost (this masked the
# real Windows results.json while debugging #196). `${{ matrix.os }}`
# keeps each arm's results.json + negative-control.json separate.
# The path moved to `tmp/e2e/` per the repo-wide scratch-under-tmp
# convention (see .gitignore comment).
#
# Skipped on tag/release runs for the same reason as the coverage
# artifact: the shared release workflow's `gh release upload
# artifacts/*` would try to upload this directory as a release asset.
if: always() && github.ref_type != 'tag'
uses: actions/upload-artifact@v7
with:
name: e2e-results-${{ matrix.os }}
path: tmp/e2e/
retention-days: 14
# --- macOS test suite ---
# The Linux `check` job runs the full `just ci` (coverage + docker + helm);
# replicating that on macOS isn't worth it. Instead this job runs a focused
# `cargo test` pass on a self-hosted Apple Silicon runner so OS-specific
# regressions — e.g. the daemon's Unix-socket EOF behaviour, which once hung
# the suite on macOS while passing on Linux — are caught before merge.
# Blocking: macOS is a first-class supported platform.
test-macos:
name: Test (macOS)
# Self-hosted Apple Silicon inside kunobi-ninja (non-fork); GitHub-hosted
# otherwise so forks can still run this job. Convenience only — see the
# matching note on the `e2e` job; fork PRs are gated by the repo's
# "require approval" Actions setting, not by this expression.
runs-on: ${{ (github.repository_owner == 'kunobi-ninja' && github.event.pull_request.head.repo.fork != true) && fromJSON('["self-hosted", "macOS", "ARM64"]') || 'macos-latest' }}
# Job-level cap on the persistent self-hosted macOS runner (cargo test is
# additionally capped at 30m); catches checkout/mise hangs.
timeout-minutes: 40
steps:
- uses: actions/checkout@v6
# See the e2e job: checkout can hang on this persistent runner's stale
# git state, so cap it well under the job timeout.
timeout-minutes: 10
# This persistent self-hosted runner has no usable shared toolchain:
# its ~/.rustup is corrupted and its Homebrew Rust is stale (1.94,
# below the pinned 1.95). Keep Rust and mise install/cache/state under
# the per-run temp dir so setup is isolated from shared runner drift.
- name: Isolate tool state
run: |
mkdir -p \
"$RUNNER_TEMP/rustup" \
"$RUNNER_TEMP/cargo" \
"$RUNNER_TEMP/mise-data/bin" \
"$RUNNER_TEMP/mise-data/shims" \
"$RUNNER_TEMP/mise-cache" \
"$RUNNER_TEMP/mise-state"
rm -rf "$RUNNER_TEMP/mise"
# Fresh, per-(instance,run) scratch for mise-action's download/extract,
# exported as MISE_DL_TMPDIR for the install step below. On Windows
# mise-action extracts with chocolatey `unzip.exe`, which prompts
# interactively on overwrite and hangs forever in CI (no stdin); and
# RUNNER_TEMP on the self-hosted runners is persistent (the service
# account's %TEMP% on Windows, possibly shared between instances), so
# leftovers from earlier runs collide. A unique dir can't collide;
# scope it AND the sweep to a sanitized $RUNNER_NAME so the sweep only
# ever removes THIS instance's dirs, never a sibling's in-use dir on a
# shared Temp. (Windows ignores $TMPDIR, so the install step also sets
# %TEMP%/%TMP% to this path.)
safe_runner=$(printf '%s' "$RUNNER_NAME" | tr -c 'A-Za-z0-9_.-' '_')
rm -rf "$RUNNER_TEMP"/mise-dl-"$safe_runner"-* 2>/dev/null || true
mise_dl="$RUNNER_TEMP/mise-dl-$safe_runner-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
mkdir -p "$mise_dl"
echo "MISE_DL_TMPDIR=$mise_dl" >> "$GITHUB_ENV"
echo "RUSTUP_HOME=$RUNNER_TEMP/rustup" >> "$GITHUB_ENV"
echo "CARGO_HOME=$RUNNER_TEMP/cargo" >> "$GITHUB_ENV"
echo "MISE_DATA_DIR=$RUNNER_TEMP/mise-data" >> "$GITHUB_ENV"
echo "MISE_CACHE_DIR=$RUNNER_TEMP/mise-cache" >> "$GITHUB_ENV"
echo "MISE_STATE_DIR=$RUNNER_TEMP/mise-state" >> "$GITHUB_ENV"
# Same setup as the `e2e` job — see that block for the
# `cache: false` + `reshim: false` rationale.
- name: Install Rust via mise
uses: jdx/mise-action@v4
env:
# See the e2e job: isolate mise's download/extract scratch
# without changing TMPDIR for the later Cargo test process.
# Unix uses $TMPDIR; Windows uses %TEMP%/%TMP%. Set all three so the
# download/extract lands in our per-run dir on every platform.
TMPDIR: ${{ env.MISE_DL_TMPDIR }}
TEMP: ${{ env.MISE_DL_TMPDIR }}
TMP: ${{ env.MISE_DL_TMPDIR }}
with:
# Pin mise CLI — see the bootstrap job's note (v2026.6.10 regression).
version: 2026.6.9
install_args: --force rust
cache: false
reshim: false
# mise installs the toolchain via rustup under RUSTUP_HOME, but on a
# runner with a pre-existing rustup it leaves no cargo proxy on PATH —
# so the stale system Rust (Homebrew 1.94) would win. Prepend the
# toolchain's own bin dir (the real 1.95 binaries) to PATH instead.
- name: Put the Rust toolchain first on PATH
run: |
bin_dir=$(echo "$RUSTUP_HOME"/toolchains/*/bin)
if [ ! -x "$bin_dir/cargo" ]; then
echo "cargo not found under $RUSTUP_HOME/toolchains/*/bin"
ls -la "$RUSTUP_HOME/toolchains" 2>/dev/null || true
exit 1
fi
"$bin_dir/rustc" --version
echo "$bin_dir" >> "$GITHUB_PATH"
- name: cargo test (workspace)
run: |
rustc --version && cargo --version
cargo test --workspace
timeout-minutes: 30
env:
RUSTC_WRAPPER: ""
# --- Release (only on v* tags) ---
release:
# This `if:` and the `version-consistency` job's `if: github.ref_type ==
# 'tag'` must stay paired: both skip together on non-tag pushes, so a
# *skipped* gate never resolves as success-equivalent and silently un-gates
# the release. If you relax this `if:`, relax the gate's too (or drop the
# gate's `if:` so it always runs).
if: startsWith(github.ref, 'refs/tags/v')
# Gate the release (and, transitively, the crates publish it triggers) on
# the full validation suite — `version-consistency` rejects a tag that
# disagrees with the manifest BEFORE any binary builds; `e2e` includes the
# (blocking) Windows arm, so a tag won't release unless Windows e2e is green.
needs:
uses: zondax/_workflows/.github/workflows/_release-rust.yml@v10
with:
binary_name: kache
# Both Windows targets (x64 + arm64) ship as `-pc-windows-msvc`, cross-built
# on the Linux runner via cargo-xwin: clang-cl + lld-link against the MSVC
# CRT and Windows SDK that `xwin` downloads. TLS uses the `ring` crypto
# provider (no aws-lc-sys — its cmake build hung cross-compiling to arm64),
# so the runner needs `nasm` to assemble ring's x86_64 `.asm` (provisioned
# by _release-rust.yml). No native Windows runner is needed here
# (`runner_windows` left unset); the kunobi-windows runner is still used by
# the e2e job above.
#
# `aarch64-pc-windows-msvc` requires the cargo-xwin clang shim added in
# _workflows@v10 (rewrites ring's `.S` asm `/imsvc` flags → `-isystem` so the
# GNU clang driver accepts them). Without that shim the arm64 build fails at
# ring's asm step. blake3 also uses its `pure` feature on Windows (see
# Cargo.toml) since its arm64 NEON C doesn't cross-compile.
targets: '["x86_64-unknown-linux-musl", "aarch64-unknown-linux-musl", "aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"]'
runner_macos: '["self-hosted", "macOS", "ARM64"]'
extra_build_env: |
KACHE_VERSION=${{ github.ref_name }}
permissions:
contents: write