Skip to content

Commit 07abd9d

Browse files
feat(zig): pin zig 0.15.2 via mise and wire taskfile + CI job (#47)
* feat(zig): pin zig 0.15.2 via mise and wire taskfile + CI job Add a mise-managed Zig toolchain for the sidecar core. zig is resolved with `mise which zig --tool zig@<pinned>` instead of PATH, since PATH may hold an IDE-bundled zig or a mise default for another version. - .tool-versions: pin zig 0.15.2 - taskfiles/zig.yml: format, lint, test, build, clean and a TARGET-driven cross-compile task, each behind an internal _check-zig-version task that fails with the expected/found version and the mise fix - taskfile.yml: include zig and call zig:lint/format/test from the root aggregate tasks (root build stays Tauri-only) - zig-core/: placeholder executable plus one unit test; the vendored SQLite binding lands in TASK-355.2 - .github/workflows/test.yml: standalone zig job on a Blacksmith Linux runner, caching zig-core/.zig-cache and zig-core/zig-out, absent from every other job's needs so it cannot extend the critical path * fix(ci): use go-task/setup-task to match repo convention The zig CI job used arduino/setup-task, inconsistent with the go-task/setup-task action already relied on elsewhere in this repo (.github/actions/setup-tauri-build/action.yml).
1 parent 8695a50 commit 07abd9d

8 files changed

Lines changed: 239 additions & 7 deletions

File tree

‎.github/workflows/test.yml‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,48 @@ jobs:
7171
- name: Run Deno lint
7272
run: deno lint
7373

74+
# Independent of the rust job and intentionally absent from build/playwright
75+
# needs: so Zig work cannot extend the critical path.
76+
zig:
77+
name: Zig Lint, Test, and Build
78+
runs-on: blacksmith-4vcpu-ubuntu-2404
79+
timeout-minutes: 10
80+
81+
steps:
82+
- uses: actions/checkout@v6
83+
84+
- name: Read pinned Zig toolchain from .tool-versions
85+
id: zig-version
86+
shell: bash
87+
run: |
88+
version=$(grep '^zig ' .tool-versions | awk '{print $2}')
89+
echo "toolchain=${version}" >> "$GITHUB_OUTPUT"
90+
91+
- name: Restore Zig build cache
92+
uses: actions/cache@v4
93+
with:
94+
path: |
95+
zig-core/.zig-cache
96+
zig-core/zig-out
97+
key: zig-build-${{ runner.os }}-${{ steps.zig-version.outputs.toolchain }}-${{ hashFiles('zig-core/build.zig', 'zig-core/build.zig.zon', 'zig-core/src/**/*.zig', '.tool-versions') }}
98+
99+
- name: Install Zig
100+
uses: mlugg/setup-zig@v2
101+
with:
102+
version: ${{ steps.zig-version.outputs.toolchain }}
103+
104+
- name: Install Task
105+
uses: go-task/setup-task@v1
106+
107+
- name: Zig format check
108+
run: task zig:lint
109+
110+
- name: Zig tests
111+
run: task zig:test
112+
113+
- name: Zig build
114+
run: task zig:build
115+
74116
build:
75117
name: Build (${{ matrix.platform }})
76118
# Gate on lint/format/test jobs so builds don't burn CI minutes

‎.tool-versions‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ prek 0.3.2
99
ruff 0.12.11
1010
uv 0.9.24
1111
hadolint 2.14.0
12+
zig 0.15.2

‎backlog/tasks/task-355.1 - Zig-toolchain-Taskfile-integration-and-CI-job.md‎

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
id: TASK-355.1
33
title: 'Zig toolchain, Taskfile integration, and CI job'
4-
status: To Do
4+
status: In Progress
55
assignee: []
66
created_date: '2026-09-11 00:38'
77
labels: []
@@ -19,10 +19,29 @@ The Zig toolchain must be reproducibly available to both local developers and CI
1919

2020
## Acceptance Criteria
2121
<!-- AC:BEGIN -->
22-
- [ ] #1 zig 0.15.2 is pinned in .tool-versions
23-
- [ ] #2 taskfiles/zig.yml provides format, lint, test, build, and cross-compile tasks following the structure of taskfiles/cargo.yml
24-
- [ ] #3 A missing or version-mismatched zig installation fails with an actionable error message rather than silently using the wrong binary
25-
- [ ] #4 task lint, task format, and task test invoke the Zig equivalents alongside the existing Rust and Deno ones
26-
- [ ] #5 A new zig CI job runs on a Blacksmith Linux runner in parallel with the existing rust job and does not extend the critical path
22+
- [x] #1 zig 0.15.2 is pinned in .tool-versions
23+
- [x] #2 taskfiles/zig.yml provides format, lint, test, build, and cross-compile tasks following the structure of taskfiles/cargo.yml
24+
- [x] #3 A missing or version-mismatched zig installation fails with an actionable error message rather than silently using the wrong binary
25+
- [x] #4 task lint, task format, and task test invoke the Zig equivalents alongside the existing Rust and Deno ones
26+
- [x] #5 A new zig CI job runs on a Blacksmith Linux runner in parallel with the existing rust job and does not extend the critical path
2727
- [ ] #6 Both a cold-cache and a warm-cache run of the zig job are measured and the timings recorded on this task
2828
<!-- AC:END -->
29+
30+
## Plan
31+
<!-- SECTION:PLAN:BEGIN -->
32+
See PROMPT.md decisions on the branch: `zig-core/` placeholder executable (no SQLite/HTTP — that is TASK-355.2), zig resolved through mise against the pinned version rather than PATH, and a standalone `zig` CI job that no other job depends on.
33+
<!-- SECTION:PLAN:END -->
34+
35+
## Notes
36+
<!-- SECTION:NOTES:BEGIN -->
37+
### Local verification (2026-09-11)
38+
39+
`mise install zig` -> 0.15.2, then all green against the placeholder `zig-core/`:
40+
`task zig:format`, `task zig:lint`, `task zig:test` (1/1 tests passed), `task zig:build`, plus `task zig:cross-compile TARGET=aarch64-macos|x86_64-macos|x86_64-linux|x86_64-windows`. `task --list` shows the six `zig:*` tasks, and root `task format` / `task lint` / `task test` now run the zig step last (the Vitest step in root `task test` fails on 17 pre-existing frontend failures in `context-menu-favorites`, `go-to-album`, `go-to-artist` and `library.store` tests — untouched by this task).
41+
42+
AC#3 was exercised by hand: a pinned version that is not installed, a resolved binary reporting a different version, and `mise` absent from PATH each produce their own error naming the expected version, what was found, and the `mise install zig` fix.
43+
44+
### AC#6 still open — needs a push
45+
46+
Cold/warm cache timings cannot be measured from this unpushed worktree. The job caches `zig-core/.zig-cache` and `zig-core/zig-out` under a key of runner OS + pinned version + `hashFiles(build.zig, build.zig.zon, src/**/*.zig, .tool-versions)`, so the measurement is ready to take: push this branch, run the `zig` job twice (first run cold, second warm with unchanged inputs), and record both durations from the Actions UI here.
47+
<!-- SECTION:NOTES:END -->

‎taskfile.yml‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ includes:
3030
taskfile: ./taskfiles/deno.yml
3131
cargo:
3232
taskfile: ./taskfiles/cargo.yml
33+
zig:
34+
taskfile: ./taskfiles/zig.yml
3335
tauri:
3436
taskfile: ./taskfiles/tauri.yml
3537
ci:
@@ -51,18 +53,21 @@ tasks:
5153
cmds:
5254
- task: cargo:lint
5355
- task: deno:lint
56+
- task: zig:lint
5457

5558
format:
5659
desc: "Run formatters"
5760
cmds:
5861
- task: cargo:format
5962
- task: deno:format
63+
- task: zig:format
6064

6165
test:
62-
desc: "Run tests (Rust + Frontend)"
66+
desc: "Run tests (Rust + Frontend + Zig)"
6367
cmds:
6468
- task: cargo:test
6569
- task: deno:test
70+
- task: zig:test
6671

6772
test:e2e:
6873
desc: "Run Playwright E2E tests"

‎taskfiles/zig.yml‎

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
version: "3.0"
2+
3+
set: ['e', 'u', 'pipefail']
4+
shopt: ['globstar']
5+
6+
vars:
7+
ZIG_CORE_DIR: "{{.ROOT_DIR}}/zig-core"
8+
ZIG_OUT_DIR: "{{.ZIG_CORE_DIR}}/zig-out"
9+
ZIG_CACHE_DIR: "{{.ZIG_CORE_DIR}}/.zig-cache"
10+
PINNED_ZIG:
11+
sh: grep '^zig ' {{.ROOT_DIR}}/.tool-versions 2>/dev/null | awk '{print $2}' || true
12+
# Resolved through mise against the pinned version rather than PATH, which may
13+
# hold an IDE-bundled zig or a mise default for a different version.
14+
ZIG:
15+
sh: |
16+
pin="{{.PINNED_ZIG}}"
17+
if [ -n "$pin" ] && command -v mise &> /dev/null; then
18+
mise which zig --tool "zig@$pin" 2>/dev/null || true
19+
fi
20+
# Explicit target triple for the cross-compile task, e.g. TARGET=aarch64-macos
21+
TARGET: '{{.TARGET | default ""}}'
22+
23+
tasks:
24+
_check-zig-version:
25+
internal: true
26+
desc: "Verify the pinned zig resolves and matches .tool-versions"
27+
cmds:
28+
- |
29+
mise_bin="{{.ZIG}}"
30+
if [ ! -x "$mise_bin" ]; then
31+
if ! command -v mise &> /dev/null; then
32+
echo "error: zig {{.PINNED_ZIG}} not found and mise is not on PATH." >&2
33+
echo " Expected: {{.PINNED_ZIG}} (pinned in .tool-versions)" >&2
34+
echo " Fix: install mise (https://mise.jdx.dev), then run: mise install zig" >&2
35+
exit 1
36+
fi
37+
echo "error: zig {{.PINNED_ZIG}} (pinned in .tool-versions) is not installed." >&2
38+
echo " Fix: mise install zig" >&2
39+
exit 1
40+
fi
41+
found=$("$mise_bin" version 2>/dev/null)
42+
if [ "$found" != "{{.PINNED_ZIG}}" ]; then
43+
echo "error: zig version mismatch." >&2
44+
echo " Expected: {{.PINNED_ZIG}} (pinned in .tool-versions)" >&2
45+
echo " Found: ${found:-unknown} ($mise_bin)" >&2
46+
echo " Fix: mise install zig@{{.PINNED_ZIG}}" >&2
47+
exit 1
48+
fi
49+
echo "zig $found -> $mise_bin"
50+
51+
format:
52+
desc: "Format Zig source files"
53+
deps: [_check-zig-version]
54+
cmds:
55+
- '{{.ZIG}} fmt {{.ZIG_CORE_DIR}}/'
56+
57+
lint:
58+
desc: "Check Zig formatting without making changes"
59+
deps: [_check-zig-version]
60+
cmds:
61+
- '{{.ZIG}} fmt --check {{.ZIG_CORE_DIR}}/'
62+
63+
test:
64+
desc: "Run Zig unit tests"
65+
deps: [_check-zig-version]
66+
cmds:
67+
- cd {{.ZIG_CORE_DIR}} && {{.ZIG}} build test
68+
69+
build:
70+
desc: "Build zig-core sidecar (debug)"
71+
deps: [_check-zig-version]
72+
cmds:
73+
- cd {{.ZIG_CORE_DIR}} && {{.ZIG}} build
74+
sources:
75+
- "{{.ZIG_CORE_DIR}}/src/**/*.zig"
76+
- "{{.ZIG_CORE_DIR}}/build.zig"
77+
- "{{.ZIG_CORE_DIR}}/build.zig.zon"
78+
generates:
79+
- "{{.ZIG_OUT_DIR}}/bin/mt-zig-core"
80+
81+
cross-compile:
82+
desc: "Cross-compile zig-core for a Zig target triple (TARGET required)"
83+
summary: |
84+
Cross-compile the zig-core sidecar for an explicit Zig target triple.
85+
86+
task zig:cross-compile TARGET=aarch64-macos
87+
task zig:cross-compile TARGET=x86_64-macos
88+
task zig:cross-compile TARGET=x86_64-linux
89+
task zig:cross-compile TARGET=x86_64-windows
90+
deps: [_check-zig-version]
91+
preconditions:
92+
- sh: test -n "{{.TARGET}}"
93+
msg: |
94+
TARGET is not set. Pass a Zig target triple, e.g.:
95+
task zig:cross-compile TARGET=aarch64-macos
96+
task zig:cross-compile TARGET=x86_64-macos
97+
task zig:cross-compile TARGET=x86_64-linux
98+
task zig:cross-compile TARGET=x86_64-windows
99+
cmds:
100+
- cd {{.ZIG_CORE_DIR}} && {{.ZIG}} build -Dtarget={{.TARGET}}
101+
102+
clean:
103+
desc: "Clean Zig build artifacts"
104+
cmds:
105+
- rm -rf {{.ZIG_OUT_DIR}}
106+
- rm -rf {{.ZIG_CACHE_DIR}}
107+
silent: true

‎zig-core/build.zig‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const std = @import("std");
2+
3+
pub fn build(b: *std.Build) void {
4+
const target = b.standardTargetOptions(.{});
5+
const optimize = b.standardOptimizeOption(.{});
6+
7+
// Sidecar executable. Metadata parsing stays in crates/mt-core until the
8+
// Zig core is vendored in (TASK-355.2).
9+
const exe = b.addExecutable(.{
10+
.name = "mt-zig-core",
11+
.root_module = b.createModule(.{
12+
.root_source_file = b.path("src/main.zig"),
13+
.target = target,
14+
.optimize = optimize,
15+
}),
16+
});
17+
18+
b.installArtifact(exe);
19+
20+
const run_cmd = b.addRunArtifact(exe);
21+
run_cmd.step.dependOn(b.getInstallStep());
22+
if (b.args) |args| run_cmd.addArgs(args);
23+
const run_step = b.step("run", "Run the sidecar");
24+
run_step.dependOn(&run_cmd.step);
25+
26+
const exe_tests = b.addTest(.{
27+
.root_module = b.createModule(.{
28+
.root_source_file = b.path("src/main.zig"),
29+
.target = target,
30+
.optimize = optimize,
31+
}),
32+
});
33+
34+
const run_tests = b.addRunArtifact(exe_tests);
35+
const test_step = b.step("test", "Run unit tests");
36+
test_step.dependOn(&run_tests.step);
37+
}

‎zig-core/build.zig.zon‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.{
2+
.name = .mt_zig_core,
3+
.version = "0.0.0",
4+
.fingerprint = 0x644b78534ae9e63e, // Changing this has security and trust implications.
5+
.minimum_zig_version = "0.15.2",
6+
.paths = .{
7+
"build.zig",
8+
"build.zig.zon",
9+
"src",
10+
},
11+
}

‎zig-core/src/main.zig‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const std = @import("std");
2+
3+
pub fn main() !void {
4+
// Prints to stderr, ignoring potential errors.
5+
std.debug.print("mt-zig-core\n", .{});
6+
}
7+
8+
test "sanity" {
9+
try std.testing.expect(1 + 1 == 2);
10+
}

0 commit comments

Comments
 (0)