# NullBuilder — full documentation > Reusable GitHub Actions workflows: tests, nightlies and releases cross-compiled to 12 targets across 5 OS families. Maintainer infrastructure. --- # Start: Overview > What NullBuilder is, who it serves, and how to wire it into a Zig repo. NullBuilder is a repository of reusable GitHub Actions workflows shared by every Null-family Zig project. It is maintainer infrastructure — the mise en place of the kitchen — not something you install or run. ## Who this is for If you maintain a Zig repository in the [nullclaw org](https://github.com/nullclaw) — or a Zig project of your own with the same shape — NullBuilder gives you drop-in CI, nightly builds, and tag-driven releases from a single `uses:` line. Consumers include [NullClaw](https://claw.nullmenu.ai/), [NullHub](https://hub.nullmenu.ai/), [NullTickets](https://tickets.nullmenu.ai/) and [NullBoiler](https://boiler.nullmenu.ai/). If you just want to *use* those tools, you never touch NullBuilder. Its output is the release binaries you download. ## Three workflows | Workflow | Trigger (in your repo) | What it does | | --- | --- | --- | | `zig-ci.yml` | push / pull request | Runs tests, cross-compiles ReleaseSmall binaries on a 3-target default matrix, uploads artifacts | | `zig-nightly.yml` | schedule or manual dispatch | Builds a 12-target matrix, skips already-built commits, optionally publishes a rolling prerelease | | `zig-release.yml` | `v*` tag push | Builds the 12-target matrix, injects the version from the tag, publishes a GitHub release; optional source archive and Docker image | Every workflow builds with `-Doptimize=ReleaseSmall` and installs Zig **0.16.0** by default. CI writes a job summary with tests passed, test MaxRSS, and the resulting binary size. ## Wire it in In a caller repo's workflow file: ```yaml jobs: ci: uses: nullclaw/nullbuilder/.github/workflows/zig-ci.yml@v1 permissions: contents: read with: binary_name: nullclaw artifact_prefix: nullclaw ``` `binary_name` is the only required input — the binary your `zig build` produces under `zig-out/bin`. Everything else has a working default. The full input tables live in [Workflows](/docs/reference/workflows/), and the target matrices in [Targets](/docs/reference/targets/). The release workflow needs write permissions and inherited secrets: ```yaml jobs: release: uses: nullclaw/nullbuilder/.github/workflows/zig-release.yml@v1 permissions: contents: write packages: write secrets: inherit with: binary_name: nullclaw artifact_prefix: nullclaw publish_docker: true ``` ## Versioning NullBuilder has no tags and no GitHub releases. Callers pin to the `v1` **branch** — that is what `@v1` in the `uses:` line resolves to. Inside the workflows the story is stricter: every third-party action is pinned to a full commit SHA, and Dependabot keeps those pins current. > [!NOTE] > Pre-1.0: config and CLI may change between releases. For NullBuilder specifically that means workflow inputs and defaults may change on the `v1` branch without a tagged release. ## Status Created May 2026 and in real use across the nullclaw org's repositories. There is no license file yet, and the only documentation upstream is the README plus the self-documenting input descriptions in the workflow files themselves. The repo tests its own helpers: the nightly-dedupe and artifact-packaging logic is written in Zig and unit-tested with `zig test` in NullBuilder's own CI. --- # Reference: Workflows > Inputs, permissions and behavior of the three reusable workflows. All configuration happens through `with:` inputs on the three `workflow_call` workflows. There are no config files and no environment variables of NullBuilder's own. `binary_name` is required everywhere; every other input has a default. ## zig-ci.yml Runs tests, then a ReleaseSmall cross-build per matrix target. Requires `permissions: contents: read`. | Input | Default | Notes | | --- | --- | --- | | `binary_name` | — (required) | Binary produced under `zig-out/bin` | | `artifact_prefix` | `""` | Prefix for uploaded artifacts; falls back to `binary_name` | | `zig_version` | `"0.16.0"` | Zig version to install | | `targets_json` | 3 targets | JSON matrix; see [Targets](/docs/reference/targets/) | | `node_version` | `""` | Optional Node.js to install before custom commands | | `node_cache_dependency_path` | `""` | Enables npm caching in setup-node | | `setup_command` | `""` | Runs after tool setup, before tests | | `pre_test_command` | `""` | Runs immediately before tests | | `test_command` | `"zig build test --summary all"` | The test invocation | | `pre_build_command` | `""` | Runs immediately before the ReleaseSmall build | | `build_args` | `""` | Extra arguments appended to `zig build` | | `e2e_command` | `""` | Runs after the build, on one matrix target only | | `e2e_target` | `"linux-x86_64"` | Matrix `target` name that runs `e2e_command` | | `upload_artifacts` | `true` | Upload host binaries as CI artifacts | Hook order within each matrix job: `setup_command` → `pre_test_command` → tests → `pre_build_command` → build → `e2e_command` (matching target only). Each hook runs under `bash -euo pipefail`. The build cache covers `.zig-cache` and `~/.cache/zig`, keyed on your Zig sources, `build.zig`, `build.zig.zon` and `vendor/`. Every job appends a summary table: tests passed, test MaxRSS, and ReleaseSmall binary size in MB and bytes. ## zig-nightly.yml Builds the full 12-target matrix on a schedule. Requires `permissions: actions: read` and `contents: read` (plus `contents: write` in the caller if `publish_release` is on). | Input | Default | Notes | | --- | --- | --- | | `binary_name` | — (required) | Binary produced under `zig-out/bin` | | `artifact_prefix` | `""` | Falls back to `binary_name` | | `zig_version` | `"0.16.0"` | | | `android_api_level` | `"24"` | For Android targets | | `android_ndk_version` | `"29.0.14206865"` | For Android targets | | `targets_json` | 12 targets | See [Targets](/docs/reference/targets/) | | `node_version` / `node_cache_dependency_path` | `""` | As in CI | | `pre_build_command` | `""` | `BUILD_VERSION` is available in its environment | | `build_args` | `""` | Extra `zig build` arguments | | `retention_days` | `14` | Artifact retention | | `force` | `false` | Build even if this commit already has a successful nightly | | `dedupe_workflow_name` | `"Nightly"` | Caller workflow name used for the dedupe check | | `publish_release` | `false` | Maintain a rolling GitHub prerelease | | `release_tag` | `"nightly"` | Tag of the rolling prerelease | | `release_title` | `"Nightly"` | Title prefix of the rolling prerelease | A preflight job checks previous successful runs for the same commit and skips the build unless `force` is set. Nightly versions look like `nightly-YYYYMMDD-` and are injected via `-Dversion`. With `publish_release: true`, the workflow replaces the assets of the rolling prerelease on each run. ## zig-release.yml Triggers on `v*` tag pushes in the caller. Requires `permissions: contents: write` and `packages: write`, plus `secrets: inherit`. | Input | Default | Notes | | --- | --- | --- | | `binary_name` | — (required) | | | `artifact_prefix` | `""` | Falls back to `binary_name` | | `zig_version` | `"0.16.0"` | | | `android_api_level` / `android_ndk_version` | `"24"` / `"29.0.14206865"` | | | `targets_json` | 12 targets | Same default matrix as nightly | | `node_version` / `node_cache_dependency_path` | `""` | As in CI | | `pre_build_command` | `""` | `BUILD_VERSION` is available | | `build_args` | `""` | | | `source_archive` | `false` | Build and attach a source tarball | | `source_prepare_command` | `""` | Runs before archiving; `BUILD_VERSION` available | | `source_archive_name` | `""` | Defaults to `-source-.tar.gz` | | `source_archive_excludes` | `.git`, `.zig-cache`, `zig-out` | Newline-separated tar excludes | | `generate_release_notes` | `true` | GitHub-generated notes | | `publish_docker` | `false` | Push a multi-arch image to ghcr.io | The version is taken from the tag with the `v` stripped and injected as `-Dversion`. Release assets are named `-.bin` (or `.exe` on Windows, which additionally gets a `.zip`). If a release for the tag already exists, its assets are updated instead of failing. With `publish_docker: true`, a follow-up job builds `linux/amd64` and `linux/arm64` images with buildx + QEMU, pushes to `ghcr.io//` with the semver tag and `latest`, and uses GitHub Actions layer caching. ## Composite actions Three helpers under `.github/actions/`, referenced with the same `@v1`: | Action | Purpose | | --- | --- | | `setup-zig` | Wraps `mlugg/setup-zig@v2`; default version 0.16.0 | | `nightly-decide` | Decides whether a nightly build is needed for this commit | | `package-artifact` | Copies the binary and writes artifact metadata | `nightly-decide` and `package-artifact` are written in Zig and unit-tested with `zig test` in NullBuilder's own CI. > [!NOTE] > Pre-1.0: inputs and defaults above may change on the `v1` branch. The workflow files' own input descriptions are the source of truth. --- # Reference: Targets > Default target matrices, custom matrices, and how Android builds work. NullBuilder cross-compiles ReleaseSmall binaries from a JSON target matrix. CI defaults to three targets; nightly and release default to twelve across five OS families. ## Default CI matrix (zig-ci.yml) | target | Runner | zig_target | | --- | --- | --- | | `linux-x86_64` | ubuntu-latest | `x86_64-linux-musl` | | `macos-aarch64` | macos-latest | `aarch64-macos` | | `windows-x86_64` | windows-latest | `x86_64-windows` | ## Default nightly and release matrix | target | Runner | zig_target | Notes | | --- | --- | --- | --- | | `linux-x86_64` | ubuntu-latest | `x86_64-linux-musl` | | | `linux-aarch64` | ubuntu-latest | `aarch64-linux-musl` | | | `linux-arm32-gnu` | ubuntu-latest | `arm-linux-gnueabihf` | | | `linux-arm32-musl` | ubuntu-latest | `arm-linux-musleabihf` | | | `linux-riscv64` | ubuntu-latest | `riscv64-linux-musl` | | | `android-aarch64` | ubuntu-latest | `aarch64-linux-android` | NDK build | | `android-armv7` | ubuntu-latest | `arm-linux-androideabi` | `zig_cpu: baseline+v7a` | | `android-x86_64` | ubuntu-latest | `x86_64-linux-android` | NDK build | | `macos-aarch64` | macos-latest | `aarch64-macos` | | | `macos-x86_64` | macos-latest | `x86_64-macos` | | | `windows-x86_64` | windows-latest | `x86_64-windows` | `ext: .exe` | | `windows-aarch64` | windows-latest | `aarch64-windows` | `ext: .exe` | Linux binaries are musl-linked static builds except the arm32 `gnu` flavor. Everything is compiled with `-Doptimize=ReleaseSmall` and, for nightly and release, a `-Dversion` injected by the workflow. ## Custom matrices Pass `targets_json` to override the matrix. Each entry needs `os` (runner label), `target` (your artifact name suffix) and `zig_target` (the Zig triple); `zig_cpu` and `ext` are optional. From the README — NullHub trims CI to four targets and adds Node for its UI build: ```yaml jobs: zig: uses: nullclaw/nullbuilder/.github/workflows/zig-ci.yml@v1 permissions: contents: read with: binary_name: nullhub artifact_prefix: nullhub node_version: 22 node_cache_dependency_path: ui/package-lock.json test_command: zig build test -Dembed-ui=false -Dbuild-ui=false --summary all pre_build_command: | npm --prefix ui ci --no-audit --no-fund npm --prefix ui run build build_args: -Dbuild-ui=false e2e_command: bash tests/test_e2e.sh targets_json: >- [ {"os":"ubuntu-latest","target":"linux-x86_64","zig_target":"x86_64-linux-musl"}, {"os":"ubuntu-latest","target":"linux-aarch64","zig_target":"aarch64-linux-musl"}, {"os":"macos-latest","target":"macos-aarch64","zig_target":"aarch64-macos"}, {"os":"windows-latest","target":"windows-x86_64","zig_target":"x86_64-windows"} ] ``` > [!TIP] > `e2e_target` must match a `target` name in your matrix — the e2e command runs on that one job only (default `linux-x86_64`). ## How Android builds work No Gradle. Targets whose name starts with `android-` get, on the Ubuntu runner: 1. JDK 17 (Temurin) and the Android SDK. 2. The NDK, installed via `sdkmanager --install "ndk;"` — default `29.0.14206865`. 3. A generated libc file pointing Zig at the NDK sysroot (`include_dir`, `sys_include_dir`, `crt_dir` for the API level). The API level (default `24`) is appended to the triple — e.g. `aarch64-linux-android.24` — and the build runs as plain `zig build -Dtarget=... --libc `. Override with `android_api_level` and `android_ndk_version` on the nightly and release workflows. ## Artifact naming | Context | Name | | --- | --- | | CI artifact | `-` (contains the raw binary) | | Nightly artifact | `-nightly--`, binary named `-` plus metadata | | Release asset | `-.bin`, or `-.exe` + `.zip` on Windows | | Source archive | `-source-.tar.gz` (when `source_archive: true`) | `` is `artifact_prefix`, falling back to `binary_name`. Workflow inputs are documented in [Workflows](/docs/reference/workflows/). > [!NOTE] > Pre-1.0: the default matrices may change on the `v1` branch. Pin your own `targets_json` if you need a fixed set.