Menu

SHARED CI — RELEASE INFRASTRUCTURE  ·  infra

One YAML block, twelve targets, every release.

Reusable GitHub Actions workflows for Zig projects. Add a ten-line job block per pipeline — `uses: nullclaw/nullbuilder/.github/workflows/zig-ci.yml@2b9c2f2e7bb0ac085baea1c33b4f08beaf5c7fac` pins the reviewed v1 snapshot for tests and cross-compiled binaries, with sibling workflows for nightlies and 12-target releases. Nothing to install — it all runs in your repo's own Actions.

Continue reading

releases. Nothing to install — it all runs in your repo's own Actions.

jobs:
  ci:
    # NullBuilder v1 snapshot; update this full SHA deliberately.
    uses: nullclaw/nullbuilder/.github/workflows/zig-ci.yml@2b9c2f2e7bb0ac085baea1c33b4f08beaf5c7fac
    permissions:
      contents: read
    with:
      binary_name: nullclaw
      artifact_prefix: nullclaw

Overview

The useful part, at a glance.

In plain words

What it is
A set of reusable GitHub Actions workflows and composite actions that give any Zig repo on GitHub its CI, nightly-build, and release pipelines. Your repo references a reviewed NullBuilder commit instead of copying workflow YAML; there is no binary to install and nothing runs outside GitHub Actions. The three pipelines cover per-push tests with cross-compiled artifacts, deduplicating scheduled nightlies, and tag-driven releases for 12 targets across Linux, Android, macOS, and Windows.
Where it fits
The workshop — not something you run, something that builds the rest: shared CI that gives any Zig repo twelve-target releases from one YAML block.
Why it exists — and when you need it
Why it exists
Every Zig repo in the NullClaw GitHub org (the ecosystem this site belongs to) needs the same pipeline: run the tests, cross-compile small ReleaseSmall binaries for a dozen platforms, publish a GitHub release with checksums. Copy-pasting that YAML into each repo means every bug fix and every Zig version bump has to be repeated in every copy. Keeping the pipelines in one repo that all others reference means one fix propagates everywhere, and the tricky parts — Android NDK setup without Gradle, nightly dedupe against the GitHub API — are solved once, with unit tests.
When you need it
You maintain a Zig project on GitHub and want tests plus cross-compiled release binaries for Linux, Android, macOS, and Windows without writing your own matrix workflows — you add a ten-line job block per pipeline and override inputs only where your build is unusual. It is not for you if you are not on GitHub Actions, or if your build steps go beyond what the setup/pre-test/pre-build hook inputs can express.

How it works

From zero to running.

nullbuilder is not something you install — it is a set of reusable GitHub Actions workflows that your Zig repo calls with a `uses:` line. Adoption is three small workflow files in your own repo; everything runs on GitHub's runners.

  1. Add the CI job block

    Create .github/workflows/ci.yml in your Zig repo with push and pull_request triggers and a single job that calls the shared pipeline, passing your binary's name (the file under zig-out/bin). Each run executes `zig build test --summary all`, then cross-compiles size-optimized ReleaseSmall binaries for three default targets: x86_64 Linux (musl), aarch64 macOS, and x86_64 Windows.

    # NullBuilder v1 snapshot; update this full SHA deliberately.
    uses: nullclaw/nullbuilder/.github/workflows/zig-ci.yml@2b9c2f2e7bb0ac085baea1c33b4f08beaf5c7fac
  2. Push and read the job summary

    Every CI run appends a summary table to the Actions page: parsed test counts, peak test memory (MaxRSS), and the exact ReleaseSmall binary size per target. Cross-compiled binaries are uploaded as workflow artifacts, so you can download and try a Windows build from a Linux laptop without owning either.

    git push
  3. Wire the nightly pipeline

    A second workflow file on a schedule calls zig-nightly.yml. Before building, a small unit-tested Zig program (nightly-decide) queries the GitHub workflow-runs API and skips the build if the current commit already has a successful nightly. Each built binary ships with a .sha256 checksum and a JSON provenance manifest; artifacts are kept 14 days, and an optional rolling prerelease replaces its assets on every build.

    # NullBuilder v1 snapshot; update this full SHA deliberately.
    uses: nullclaw/nullbuilder/.github/workflows/zig-nightly.yml@2b9c2f2e7bb0ac085baea1c33b4f08beaf5c7fac
  4. Tag a release

    A third workflow file triggered on v* tags calls zig-release.yml. Pushing a tag builds all 12 default targets across Linux, Android, macOS, and Windows and publishes a GitHub release: raw .bin files for unix targets, .exe plus a zip archive for Windows, optionally a source tarball and a multi-arch Docker image on ghcr.io. Re-running the workflow updates the existing release's assets instead of failing.

    git tag v0.3.0 && git push origin v0.3.0
  5. Override defaults only where needed

    If you do nothing beyond binary_name, you get Zig 0.16.0, the default target matrices, and `zig build test` — sensible defaults for a plain Zig project. Nonstandard repos override workflow inputs instead of forking: targets_json for a custom matrix, setup/pre-test/pre-build hook commands, node_version for repos that build UI assets first, publish_docker for a ghcr.io image.

    with:
      binary_name: myproject
      zig_version: "0.16.0"

Capabilities

What NullBuilder does.

One CI block

zig-ci.yml runs your test suite, then cross-compiles size-optimized (ReleaseSmall) binaries for a configurable target matrix — x86_64 Linux (musl), aarch64 macOS, and x86_64 Windows by default. Every job appends a summary table: tests passed, peak test memory, and the exact binary size per target.

Twelve-target releases

Push a v* tag and zig-release.yml builds binaries for 12 targets across Linux, Android, macOS, and Windows, then publishes a GitHub release: raw .bin files for unix targets, .exe plus a zip for Windows, an optional source tarball, and an optional multi-arch Docker image on ghcr.io. Re-running updates the release's assets instead of failing.

Nightlies that dedupe

zig-nightly.yml asks the GitHub API whether the current commit already has a successful nightly and skips the build if so — the decision is made by a small unit-tested Zig program, not a shell one-liner. Artifacts are kept 14 days, and an optional rolling prerelease gets its assets replaced on every build.

Checksums and manifests

Every nightly binary ships with a .sha256 checksum file and a JSON manifest recording the commit, run URL, target, and build time. General-purpose third-party actions are pinned to full commit SHAs with weekly Dependabot updates. The remaining upstream gap is internal: all three bundled composite helpers are still loaded through NullBuilder's mutable v1 branch.

Android without Gradle

aarch64, armv7 and x86_64 Android binaries cross-compiled with plain Zig plus an NDK sysroot libc file. No Java build system involved.

Hooks for odd builds

Nonstandard build? Pass your commands as workflow inputs: setup, pre-test, and pre-build hooks, a custom test command and build flags, one end-to-end test pinned to a target of your choice, and optional Node.js with npm caching for repos that build UI assets first. Hook commands reach bash through environment variables, never templated into the workflow YAML.

Use it for

Where it earns its place.

Ship a Zig CLI for 12 platforms from one tag push

You maintain a Zig command-line tool and users keep asking for an ARM Linux build, then a Windows one, then macOS. You add one release workflow file that calls zig-release.yml on v* tags. Every tag push now publishes a GitHub release with binaries for Linux (including riscv64 and two arm32 flavors), Android, macOS, and Windows — and if the release job is re-run, it updates the assets on the existing release rather than erroring out.

git tag v1.2.0
git push origin v1.2.0
Nightly builds that skip themselves when nothing changed

You want fresh binaries every night without burning runner minutes on quiet weeks. zig-nightly.yml runs a preflight where the nightly-decide program checks the GitHub workflow-runs API for a prior successful nightly of the same commit and prints should_build=false if it finds one. When a build does happen, each binary ships with a SHA-256 checksum file and a manifest-<target>.json recording the commit, run URL, target, and build time — enough provenance to answer 'which commit is this binary from' months later.

# NullBuilder v1 snapshot; update this full SHA deliberately.
uses: nullclaw/nullbuilder/.github/workflows/zig-nightly.yml@2b9c2f2e7bb0ac085baea1c33b4f08beaf5c7fac
with:
  retention_days: 14
Android binaries without touching Gradle

Your tool should run on an Android device over adb, but you do not want a Java build system in a Zig repo. The release workflow installs the NDK via sdkmanager, generates a Zig --libc file pointing at the NDK sysroot, and cross-compiles aarch64, armv7, and x86_64 Android binaries with the target suffixed by API level (aarch64-linux-android.24 by default, configurable via android_api_level).

with:
  android_api_level: "24"
A Zig binary that embeds a web UI

Your project builds frontend assets with npm before `zig build` can embed them. Instead of forking the workflow, you pass inputs: node_version to get Node.js with npm caching, setup_command to install dependencies, and pre_build_command to compile the assets — with BUILD_VERSION exported so the UI can display it. Hook commands reach bash through environment variables with `bash -euo pipefail`, never templated into the workflow YAML.

with:
  node_version: "22"
  pre_build_command: npm run build
What's inside Counted in the source, not the brochure. 25 listed

3 reusable workflows

All three are `on: workflow_call` — callers pin a reviewed full commit SHA and never copy the YAML. The published pin is the v1 snapshot at `2b9c2f2e7bb0ac085baea1c33b4f08beaf5c7fac`.

  • Zig CI (zig-ci.yml)
  • Zig Release (zig-release.yml)
  • Zig Nightly (zig-nightly.yml)

3 composite actions

Nightly Decide and Package Artifact are small Zig 0.16 programs run via `zig run` — the dedupe decision and the checksum/manifest writer are unit-tested code, not shell one-liners.

  • Setup Zig
  • Nightly Decide
  • Package Artifact

12 default release targets

Four OS families from one tag push; Android is cross-compiled with plain Zig plus an NDK sysroot libc file — no Gradle.

  • Linux x86_64
  • Linux aarch64
  • Linux arm32 (gnu)
  • Linux arm32 (musl)
  • Linux riscv64
  • Android aarch64
  • Android armv7
  • Android x86_64
  • macOS aarch64
  • macOS x86_64
  • Windows x86_64
  • Windows aarch64

3 default CI targets

A faster per-push subset of the release matrix, overridable via the targets_json input.

  • Linux x86_64 (musl)
  • macOS aarch64
  • Windows x86_64

4 release asset shapes

Nightly binaries additionally get a .sha256 checksum file and a per-target JSON build manifest.

  • Raw .bin (unix targets)
  • .exe plus zip archive (Windows targets)
  • Source tar.gz (optional)
  • Multi-arch Docker image on ghcr.io (optional)

Quickstart

Up and running.

Full walkthrough in the docs .

There is nothing to install. In any Zig project on GitHub, add one workflow file that calls the shared pipeline at a reviewed commit; update the pin deliberately when you adopt upstream changes:

# in your project repo
mkdir -p .github/workflows

# one job block wires up tests + cross-compiled ReleaseSmall builds
cat > .github/workflows/ci.yml <<'EOF'
name: CI
on: [push, pull_request]
jobs:
  ci:
    # NullBuilder v1 snapshot; update this full SHA deliberately.
    uses: nullclaw/nullbuilder/.github/workflows/zig-ci.yml@2b9c2f2e7bb0ac085baea1c33b4f08beaf5c7fac
    permissions:
      contents: read
    with:
      binary_name: myproject   # your binary under zig-out/bin
EOF

git add .github/workflows/ci.yml
git commit -m "ci: adopt nullbuilder shared workflows"
git push

Common questions

Questions, answered.

Can I download and run nullbuilder itself?

No — the repo contains no build.zig and no src/ directory, so there is nothing to compile or install. It exists to be referenced: your repo's workflow files pin a reviewed NullBuilder commit, and everything executes inside your own repo's GitHub Actions runs.

Why do the examples pin a commit instead of the v1 branch?

The repo currently has zero releases and zero tags, and v1 is a mutable branch. A full commit SHA makes the reviewed workflow revision explicit and prevents that caller reference from changing underneath you. The examples pin `2b9c2f2e7bb0ac085baea1c33b4f08beaf5c7fac`; review upstream changes before updating it.

Is the complete action chain pinned to commit SHAs?

Not yet. General-purpose third-party actions are pinned to full 40-character SHAs with version comments and weekly Dependabot updates. At the reviewed snapshot, setup-zig runs NullBuilder's bundled install-zig.sh and verifies the Zig archive against ziglang.org metadata. The remaining gap is that every reusable workflow loads setup-zig, nightly-decide, and package-artifact through NullBuilder's mutable v1 branch, so the internal helper implementations can still move independently of a caller's pinned workflow file.

My project doesn't build with a plain `zig build` — can I still use it?

Usually, yes, through workflow inputs rather than forking: setup_command, pre_test_command, a replaceable test_command, pre_build_command, custom build_args, and an e2e_command pinned to a target of your choice (linux-x86_64 by default). If your build needs something the hooks cannot express, you are outside what the shared workflows cover.

Does this work outside GitHub?

No. All three pipelines are GitHub Actions `workflow_call` workflows, releases are GitHub releases, the nightly dedupe queries the GitHub API, and the optional Docker publish targets ghcr.io. On GitLab or a self-hosted CI system, none of it applies.

What provenance do the built binaries carry?

Nightly binaries each get a .sha256 checksum file and a manifest-<target>.json recording the commit, run ID, run URL, target triple, version, and build timestamp — written by a small unit-tested Zig program. It is checksum-and-manifest provenance, not a signed attestation: there is no Sigstore or SLSA machinery involved.

Not an end-user tool — the repo contains no build.zig and ships no binary of its own. No releases or tags yet: `v1` is a mutable branch, so every published caller example pins the reviewed v1 snapshot at commit `2b9c2f2e7bb0ac085baea1c33b4f08beaf5c7fac`.