Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions .github/scripts/install-zig.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#!/usr/bin/env bash
set -euo pipefail

if [ "$#" -ne 1 ]; then
echo "usage: $0 <zig-version>" >&2
exit 1
fi

version="$1"

python_bin="${PYTHON:-python3}"
if ! command -v "$python_bin" >/dev/null 2>&1; then
python_bin="python"
fi
if ! command -v "$python_bin" >/dev/null 2>&1; then
echo "python is required to install Zig" >&2
exit 1
fi

runner_os="${RUNNER_OS:-$(uname -s)}"
runner_arch="${RUNNER_ARCH:-$(uname -m)}"

case "$runner_os" in
Linux | linux)
zig_os="linux"
;;
Darwin | macOS)
zig_os="macos"
;;
Windows | MINGW* | MSYS* | CYGWIN*)
zig_os="windows"
;;
*)
echo "unsupported runner OS: $runner_os" >&2
exit 1
;;
esac

case "$runner_arch" in
X64 | x86_64 | amd64)
zig_arch="x86_64"
;;
ARM64 | arm64 | aarch64)
zig_arch="aarch64"
;;
*)
echo "unsupported runner architecture: $runner_arch" >&2
exit 1
;;
esac

host_key="${zig_arch}-${zig_os}"
tool_root="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/nullboiler-zig"
install_dir="${tool_root}/${version}/${host_key}"
zig_bin="zig"
if [ "$zig_os" = "windows" ]; then
zig_bin="zig.exe"
fi

if [ ! -x "${install_dir}/${zig_bin}" ]; then
mkdir -p "$(dirname "$install_dir")"

zig_metadata="$(
"$python_bin" - "$version" "$host_key" <<'PY'
import json
import sys
import urllib.request

version = sys.argv[1]
host_key = sys.argv[2]

with urllib.request.urlopen("https://ziglang.org/download/index.json") as response:
data = json.load(response)

host = data.get(version, {}).get(host_key)
if not host:
raise SystemExit(f"missing Zig download metadata for version={version!r} host={host_key!r}")

archive_url = host.get("tarball") or host.get("zip")
checksum = host.get("shasum") or ""
if not archive_url:
raise SystemExit(f"missing archive URL for version={version!r} host={host_key!r}")

print(archive_url)
print(checksum)
PY
)"

archive_url="$(printf '%s\n' "$zig_metadata" | sed -n '1p')"
expected_sha="$(printf '%s\n' "$zig_metadata" | sed -n '2p')"
if [ -z "$archive_url" ]; then
echo "failed to resolve Zig download URL" >&2
exit 1
fi

archive_name="${archive_url##*/}"
archive_dir="$(mktemp -d "${RUNNER_TEMP:-${TMPDIR:-/tmp}}/zig-archive.XXXXXX")"
archive_path="${archive_dir}/${archive_name}"
extract_dir="$(mktemp -d "${RUNNER_TEMP:-${TMPDIR:-/tmp}}/zig-extract.XXXXXX")"
trap 'rm -rf "$archive_dir"; rm -rf "$extract_dir"' EXIT

curl -fsSL --retry 3 --retry-all-errors "$archive_url" -o "$archive_path"

"$python_bin" - "$archive_path" "$expected_sha" <<'PY'
import hashlib
import sys

path = sys.argv[1]
expected = sys.argv[2].strip().lower()
if not expected:
raise SystemExit(0)

digest = hashlib.sha256()
with open(path, "rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)

actual = digest.hexdigest().lower()
if actual != expected:
raise SystemExit(f"checksum mismatch for {path}: expected {expected}, got {actual}")
PY

"$python_bin" - "$archive_path" "$extract_dir" <<'PY'
import pathlib
import sys
import tarfile
import zipfile

archive = pathlib.Path(sys.argv[1])
destination = pathlib.Path(sys.argv[2])
destination.mkdir(parents=True, exist_ok=True)

def ensure_within_destination(relative_name: str) -> None:
target = (destination / relative_name).resolve()
if destination.resolve() not in target.parents and target != destination.resolve():
raise SystemExit(f"archive entry escapes destination: {relative_name}")

if archive.suffix == ".zip":
with zipfile.ZipFile(archive) as handle:
for member in handle.namelist():
ensure_within_destination(member)
handle.extractall(destination)
else:
with tarfile.open(archive, "r:*") as handle:
for member in handle.getnames():
ensure_within_destination(member)
handle.extractall(destination)
PY

extracted_dir="$(find "$extract_dir" -mindepth 1 -maxdepth 1 -type d | head -n 1)"
if [ -z "$extracted_dir" ]; then
echo "failed to extract Zig archive: $archive_url" >&2
exit 1
fi

rm -rf "$install_dir"
mv "$extracted_dir" "$install_dir"
fi

if [ -n "${GITHUB_PATH:-}" ]; then
printf '%s\n' "$install_dir" >> "$GITHUB_PATH"
else
echo "GITHUB_PATH is not set; add this directory to PATH manually: $install_dir" >&2
fi

"${install_dir}/${zig_bin}" version
24 changes: 14 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
name: CI

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
ZIG_VERSION: "0.16.0"

on:
push:
branches: [main]
Expand Down Expand Up @@ -28,18 +32,18 @@ jobs:
zig_target: x86_64-windows

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Install Zig 0.15.2
uses: mlugg/setup-zig@v2
with:
version: 0.15.2
- name: Install Zig 0.16.0
run: bash .github/scripts/install-zig.sh "${ZIG_VERSION}"

- name: Cache .zig-cache
uses: actions/cache@v4
- name: Cache Zig build outputs
uses: actions/cache@v5
with:
path: .zig-cache
key: zig-${{ matrix.target }}-${{ hashFiles('src/**/*.zig', 'build.zig') }}
path: |
.zig-cache
~/.cache/zig
key: zig-${{ matrix.target }}-${{ hashFiles('src/**/*.zig', 'build.zig', 'build.zig.zon', 'deps/**') }}
restore-keys: zig-${{ matrix.target }}-

- name: Run tests
Expand Down Expand Up @@ -79,7 +83,7 @@ jobs:

- name: Upload binary
if: success()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: nullboiler-${{ matrix.target }}
path: zig-out/bin/nullboiler${{ runner.os == 'Windows' && '.exe' || '' }}
28 changes: 15 additions & 13 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
name: Release

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
ZIG_VERSION: "0.16.0"

on:
push:
tags: ['v*']
Expand Down Expand Up @@ -44,18 +48,16 @@ jobs:
ext: ".exe"

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Install Zig 0.15.2
uses: mlugg/setup-zig@v2
with:
version: 0.15.2
- name: Install Zig 0.16.0
run: bash .github/scripts/install-zig.sh "${ZIG_VERSION}"

- name: Build ReleaseSmall
run: zig build -Doptimize=ReleaseSmall ${{ matrix.zig_target && format('-Dtarget={0}', matrix.zig_target) || '' }}

- name: Upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: nullboiler-${{ matrix.target }}
path: zig-out/bin/nullboiler${{ matrix.ext }}
Expand All @@ -67,7 +69,7 @@ jobs:
contents: write

steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v8

- name: Rename binaries
run: |
Expand Down Expand Up @@ -100,32 +102,32 @@ jobs:
packages: write

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4

- name: Log in to ghcr.io
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{raw}}
type=raw,value=latest

- name: Build and push
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Graph-based workflow orchestrator with unified state model for NullClaw AI bot a

## Tech Stack

- **Language**: Zig 0.15.2
- **Language**: Zig 0.16.0
- **Database**: SQLite (vendored in `deps/sqlite/`), WAL mode
- **Protocol**: HTTP/1.1 REST API with JSON payloads
- **Dispatch**: HTTP (webhook/api_chat/openai_chat/a2a), MQTT, Redis Streams
Expand Down
22 changes: 20 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,33 @@
# Build on the native builder architecture and cross-compile with Zig.
FROM --platform=$BUILDPLATFORM alpine:3.23 AS builder

RUN apk add --no-cache zig musl-dev
ARG ZIG_VERSION=0.16.0

RUN apk add --no-cache bash curl musl-dev python3 tar xz

COPY .github/scripts/install-zig.sh /tmp/install-zig.sh
RUN set -eu; \
export GITHUB_PATH=/tmp/zig-path; \
export RUNNER_OS=Linux; \
case "$(uname -m)" in \
x86_64) export RUNNER_ARCH=X64 ;; \
aarch64|arm64) export RUNNER_ARCH=ARM64 ;; \
*) echo "Unsupported host arch: $(uname -m)" >&2; exit 1 ;; \
esac; \
bash /tmp/install-zig.sh "${ZIG_VERSION}"; \
zig_dir="$(cat /tmp/zig-path)"; \
ln -sf "${zig_dir}/zig" /usr/local/bin/zig; \
zig version

WORKDIR /app
COPY build.zig build.zig.zon ./
COPY src/ src/
COPY deps/ deps/

ARG TARGETARCH
RUN set -eu; \
RUN --mount=type=cache,target=/root/.cache/zig \
--mount=type=cache,target=/app/.zig-cache \
set -eu; \
arch="${TARGETARCH:-}"; \
if [ -z "${arch}" ]; then \
case "$(uname -m)" in \
Expand Down
12 changes: 6 additions & 6 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ pub fn build(b: *std.Build) void {
.optimize = optimize,
}),
});
exe.linkLibrary(sqlite3_lib);
exe.linkLibrary(hiredis_lib);
exe.linkLibrary(mosquitto_lib);
exe.root_module.linkLibrary(sqlite3_lib);
exe.root_module.linkLibrary(hiredis_lib);
exe.root_module.linkLibrary(mosquitto_lib);
b.installArtifact(exe);

const run_cmd = b.addRunArtifact(exe);
Expand All @@ -50,9 +50,9 @@ pub fn build(b: *std.Build) void {
.optimize = optimize,
}),
});
exe_unit_tests.linkLibrary(sqlite3_lib);
exe_unit_tests.linkLibrary(hiredis_lib);
exe_unit_tests.linkLibrary(mosquitto_lib);
exe_unit_tests.root_module.linkLibrary(sqlite3_lib);
exe_unit_tests.root_module.linkLibrary(hiredis_lib);
exe_unit_tests.root_module.linkLibrary(mosquitto_lib);
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_exe_unit_tests.step);
Expand Down
2 changes: 1 addition & 1 deletion build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
.name = .nullboiler,
.fingerprint = 0xa2fe02b8f872821f,
.version = "2026.3.2",
.minimum_zig_version = "0.15.2",
.minimum_zig_version = "0.16.0",
.dependencies = .{
.sqlite3 = .{
.path = "deps/sqlite",
Expand Down
3 changes: 2 additions & 1 deletion deps/hiredis/build.zig.zon
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
.{
.name = .hiredis,
.fingerprint = 0xb7c9a464c4191c3c,
.version = "1.2.0",
.minimum_zig_version = "0.15.2",
.minimum_zig_version = "0.16.0",
.paths = .{
"build.zig",
"build.zig.zon",
Expand Down
3 changes: 2 additions & 1 deletion deps/mosquitto/build.zig.zon
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
.{
.name = .mosquitto,
.fingerprint = 0xedaa031912870053,
.version = "2.0.18",
.minimum_zig_version = "0.15.2",
.minimum_zig_version = "0.16.0",
.paths = .{
"build.zig",
"build.zig.zon",
Expand Down
Loading