Production-Readiness Audit: Missing Infrastructure, Lint, Benchmark & Regression-Prevention Guardrails
This is a tracking issue for a repo-wide audit of infrastructure, tooling, and process (not source code). The audit compared ZigCraft against what a production game engine repo would be expected to have. Someone should break this up into focused sub-issues — each section below is roughly one sub-issue.
The repo already has unusually strong automation for a project this size: Nix flakes, opencode AI workflows, actionlint, dependabot, 4 PR templates, AI test-writer/auditor/triage loops, path-filtered CI, and 75 test files. The gaps below are the difference between "CI exists" and "CI actually prevents regressions."
Scope: lint, benchmarks, regression prevention, security, safety, observability. Out of scope: release engineering (CHANGELOG, tags, GitHub Releases) — covered separately once a release is on the roadmap.
P0 — Critical (current gates are effectively no-ops)
1. Benchmark baseline is a placeholder
docs/benchmarks/baseline.json is all zeros, with the literal note:
"Populate this file with real benchmark baselines before enabling regression gating."
The benchmark workflow runs on every dev push, "compares" against this zeroed baseline, and publishes a green commit status. No regression can ever fail.
Fix:
- Run a real low/medium/high pass on known-good reference hardware (Blacksmith runner or a fixed self-hosted box).
- Commit real numbers to
baseline.json.
- Flip
scripts/compare_benchmarks.sh to exit non-zero on regression beyond tolerance.
- Document the hardware used in
docs/benchmarks/README.md so future drift is interpretable.
2. zig fmt --check is not enforced in CI
- The format check only lives in
.githooks/pre-push, which is opt-in (./scripts/setup-hooks.sh) and bypassable with git push --no-verify.
- The hook only checks
src/, not modules/ or libs/.
- No PR-side enforcement at all.
Fix: Add a fast zig fmt --check src/ modules/ job (or step) to .github/workflows/build.yml that gates every PR.
3. Benchmarks & visual tests don't run on PRs
benchmark.yml triggers on push: dev + workflow_dispatch only.
visual-test.yml is workflow_dispatch only.
Performance and visual regressions land on dev and are detected after merge, when bisecting is harder.
Fix:
- Add
pull_request: trigger to benchmark.yml (gated by path filter or label to control cost).
- Add a label-triggered (
run-visual-test) or nightly scheduled run for visual-test.yml so it actually executes.
4. No frame-time / SLO assertion in benchmarks
Benchmarks report FPS percentiles (p1, p5, p50, p95, p99), CPU ms, GPU ms, draw calls, vertices — but nothing fails. A 50% FPS regression ships green.
Fix: Add absolute thresholds in the benchmark harness:
- p1 FPS floor per preset
- Spike guard: no single frame > N ms (e.g. 50 ms)
- Draw-call ceiling per preset
These are different from regression checks — they're absolute SLOs.
P1 — High (no safety/security net)
5. No code coverage measurement or gating
- 75 test files / ~100+ test cases, but no visibility into what's covered.
- No kcov / llvm-cov / codecov upload.
- No coverage threshold gate.
- No trend over time.
Fix: Wire kcov (Zig's best option today) into the unit-test job, upload to Codecov, add a non-blocking threshold comment first, then make it blocking once a baseline is captured.
6. No memory-safety test runs
Zig supports AddressSanitizer; Valgrind is also an option. Neither is wired into CI. For a voxel engine with manual allocators, packed structs shared with the GPU, and a job system passing chunk pointers across threads, this is high-value.
Fix: Add a sanitizer matrix job (zig build -Dsanitize=address test) running the unit suite at least nightly.
7. No Vulkan validation layer enforcement
Lavapipe is set up in the integration test, but there's no fail-on-validation-error gate. Shader misuse, wrong pipeline barriers, and binding bugs slip through silently.
Fix:
- Enable
VK_LAYER_KHRONOS_validation with validate_core=true and best-practices enabled in CI.
- Set
VK_LAYER_LOGGING=stderr and fail the integration/world-smoke steps on any validation error.
8. No security / supply-chain scanning
None of these exist today:
- SAST: CodeQL or Semgrep
- Dependency vulnerabilities: Trivy / Grype /
nix flake show audit
- Secret scanning: gitleaks / trufflehog (PR + history)
- SBOM: CycloneDX / SPDX generation
- Provenance: SLSA / sigstore signing of build artifacts
The repo already grants id-token: write to build jobs, so signing/provenance is the natural next step.
9. No SECURITY.md / CODEOWNERS / CODE_OF_CONDUCT.md
- No documented vulnerability reporting path.
- No code ownership enforcement on PRs (reviewers aren't auto-requested per path).
- No contributor code of conduct.
Fix: Add the three files. Wire CODEOWNERS to trigger mandatory reviews for hot paths (modules/engine-graphics/, modules/world-persistence/, .github/workflows/).
10. Tested in one build mode only
CI runs zig build test in Debug only. Release-only UB (the kind that bites players) is invisible.
Fix: Add a ReleaseSafe matrix job to the unit-test step. ReleaseFast is overkill for correctness but ReleaseSafe catches optimization-sensitive UB.
P2 — Medium (lint & process hygiene)
11. No shellcheck
scripts/*.sh only gets bash -n (syntax check) in workflow-validation.yml. Real issues — unquoted expansions, SC2086, SC2046 — pass silently. The repo has 30+ shell scripts driving CI.
Fix: Add shellcheck to workflow-validation.yml with a config that's strict on errors and warnings.
12. No markdown / JSON / YAML linters
README, CONTRIBUTING, AGENTS, the 4 PR templates, issue templates, and docs/**/*.md are all unchecked. baseline.json and any future config files have no schema. YAML is only parse-checked via ruby.
Fix:
markdownlint-cli2 for prose.
prettier --check for JSON/YAML.
- Consider JSON schemas for
baseline.json, labeler.yml, issue-labeler.json.
13. No conventional-commit enforcement
CONTRIBUTING.md / AGENTS.md mandate feat:/fix:/refactor:/test:/docs: but nothing enforces it on commit messages or PR titles. Commits drift from convention silently.
Fix: Add an amiquick / commitlint / PR-title-check action.
14. No DCO / CLA / commit signing
Contribution provenance is unverified. For an engine that may eventually be licensed or distributed, signed-off-by trails matter.
Fix: Add a DCO check (dco-app) as a required status check; optionally enforce GPG/SSH commit signing.
15. No nix flake check in CI
The flake could be malformed in ways that only show up outside the dev shell. Reproducibility story is undermined if the flake itself isn't validated.
Fix: Add nix flake check --no-build (or scoped variant) to workflow-validation.yml.
16. No stale issue / PR bot
Only stale branch cleanup exists (stale-branch-cleanup.yml). Issues and PRs can rot indefinitely.
Fix: Add actions/stale with sane exemption labels (pinned, roadmap, batch-*).
17. Visual regression is LLM-judged only — no deterministic golden-image diff
visual-test.yml feeds screenshots to MiniMax-M3 for verification. There's no deterministic perceptual/pixel comparison against a golden image.
Fix: Add an ImageMagick compare step (or pixelmatch, odiff) with a configurable tolerance. Use it as the primary gate, with the LLM as a secondary diagnostic. Otherwise silent visual regressions the model normalizes away will ship.
P2 — Medium (observability)
18. No crash reporting hook
No Sentry / Backtrace / minidump capture. Integration tests discard cores. In-dev crashes from testers are unattributable.
Fix: Wire a minimal crash handler (Breakpad / minidump) and an opt-in upload sink. Even just persisting minidumps as CI artifacts on integration failures would be a start.
19. No historical benchmark trend store
Each benchmark run overwrites the last. There's no Bencher / Conbench / InfluxDB / Hydra-style history, so month-over-month drift is invisible.
Fix: Push benchmark results to a trend store (Bencher is the obvious OSS pick; works well with GitHub Actions).
20. Single-platform CI
Linux + Lavapipe only. No Windows runner. No macOS runner. No MoltenVK path. Production engines test on every target platform — Lavapipe won't catch driver-specific issues that real users hit.
Fix: Add a Windows runner to build.yml (Vulkan on Windows is straightforward via SDK). macOS is harder (MoltenVK) — at least ensure it builds.
21. No automated profiling capture in CI
Tracy / Perfetto capture isn't automated. Profiling requires manual -D flags and a developer at the keyboard.
Fix: Add a nightly job that captures a Tracy frame for a fixed world and uploads it as an artifact so regressions are inspectable after the fact.
P3 — Lower (game-engine-specific guardrails)
22. No asset size guardrails
Textures and models can grow without a CI check. The repo already gitignores *_4k.* to keep 4K sources out, but there's no automated ceiling on what's committed or shipped.
Fix: Add a CI step that fails if any new texture exceeds a budget (e.g. 4 MB) or if total assets/ size grows beyond a threshold.
23. No draw-call / GPU-memory / vertex budget test
Benchmarks log draw_calls_avg, vertices_avg, GPU ms — but never assert a ceiling. A 3× draw-call regression ships green.
Fix: Add per-preset ceilings to the benchmark gate.
24. No shader compile-time / SPIR-V size regression
glslangValidator validates correctness in test, but doesn't fail on a 2× SPIR-V size growth or compile-time spike. Shader bloat creeps in.
Fix: Log SPIR-V size per shader in test, fail on >N% regression vs a baseline.
25. No fuzz / property-based tests
Worldgen has many determinism tests, but no fuzz harness. Edge cases that matter for a voxel engine — chunk coordinate boundaries, save-file corruption, malformed region files, integer overflow in light propagation — are untested.
Fix: Add fuzz harnesses for:
- Worldgen at extreme chunk coordinates
- Region file parsing with corrupted/short reads
- Light packing/unpacking round-trips
- AABB / ray-cast edge cases
26. No save-format backward-compatibility test
world-persistence has no "load a v0.1 save" golden fixture. A schema change can silently brick every existing world.
Fix: Commit a small golden save file per format version, add a test that loads it after every persistence change.
Suggested breakdown order
When this is split into sub-issues, the dependency order is roughly:
| Wave |
Items |
Why first |
| 1 |
#1 baseline.json, #2 fmt-check in CI, #11 shellcheck |
Unblock all other CI improvements; trivial wins |
| 2 |
#3 PR triggers for bench/visual, #4 SLO assertions, #17 golden-image diff |
Make existing pipelines actually fail |
| 3 |
#5 coverage, #6 sanitizers, #7 validation layers, #10 ReleaseSafe matrix |
Catch real bugs |
| 4 |
#8 security scanning, #9 SECURITY/CODEOWNERS/COC, #13 commitlint, #14 DCO |
Hardening |
| 5 |
#19 benchmark trend store, #18 crash reporting, #20 multi-platform, #21 profiling |
Observability |
| 6 |
#22–#26 engine guardrails (assets, draw calls, shaders, fuzz, save compat) |
Engine-specific |
Items #12 (markdown/json/yaml lint), #15 (nix flake check), #16 (stale bot) are independent quick wins that can land anytime.
Audit context
- Audited at: HEAD of
dev
- Scope: infrastructure / tooling / process only (not source code)
- Excluded: release engineering (CHANGELOG, tags, releases) — separate scope once shipping
- Strengths already in place: Nix flakes, actionlint, dependabot (actions), 4 PR templates, AI test-writer/auditor/triage loops, path-filtered CI, 75 test files
/cc anyone picking this up — please comment on which wave/item you're starting before opening sub-issues so we don't duplicate.
Production-Readiness Audit: Missing Infrastructure, Lint, Benchmark & Regression-Prevention Guardrails
This is a tracking issue for a repo-wide audit of infrastructure, tooling, and process (not source code). The audit compared ZigCraft against what a production game engine repo would be expected to have. Someone should break this up into focused sub-issues — each section below is roughly one sub-issue.
The repo already has unusually strong automation for a project this size: Nix flakes, opencode AI workflows, actionlint, dependabot, 4 PR templates, AI test-writer/auditor/triage loops, path-filtered CI, and 75 test files. The gaps below are the difference between "CI exists" and "CI actually prevents regressions."
Scope: lint, benchmarks, regression prevention, security, safety, observability. Out of scope: release engineering (CHANGELOG, tags, GitHub Releases) — covered separately once a release is on the roadmap.
P0 — Critical (current gates are effectively no-ops)
1. Benchmark baseline is a placeholder
docs/benchmarks/baseline.jsonis all zeros, with the literal note:The benchmark workflow runs on every
devpush, "compares" against this zeroed baseline, and publishes a green commit status. No regression can ever fail.Fix:
baseline.json.scripts/compare_benchmarks.shto exit non-zero on regression beyond tolerance.docs/benchmarks/README.mdso future drift is interpretable.2.
zig fmt --checkis not enforced in CI.githooks/pre-push, which is opt-in (./scripts/setup-hooks.sh) and bypassable withgit push --no-verify.src/, notmodules/orlibs/.Fix: Add a fast
zig fmt --check src/ modules/job (or step) to.github/workflows/build.ymlthat gates every PR.3. Benchmarks & visual tests don't run on PRs
benchmark.ymltriggers onpush: dev+workflow_dispatchonly.visual-test.ymlisworkflow_dispatchonly.Performance and visual regressions land on
devand are detected after merge, when bisecting is harder.Fix:
pull_request:trigger tobenchmark.yml(gated by path filter or label to control cost).run-visual-test) or nightly scheduled run forvisual-test.ymlso it actually executes.4. No frame-time / SLO assertion in benchmarks
Benchmarks report FPS percentiles (p1, p5, p50, p95, p99), CPU ms, GPU ms, draw calls, vertices — but nothing fails. A 50% FPS regression ships green.
Fix: Add absolute thresholds in the benchmark harness:
These are different from regression checks — they're absolute SLOs.
P1 — High (no safety/security net)
5. No code coverage measurement or gating
Fix: Wire
kcov(Zig's best option today) into the unit-test job, upload to Codecov, add a non-blocking threshold comment first, then make it blocking once a baseline is captured.6. No memory-safety test runs
Zig supports
AddressSanitizer; Valgrind is also an option. Neither is wired into CI. For a voxel engine with manual allocators, packed structs shared with the GPU, and a job system passing chunk pointers across threads, this is high-value.Fix: Add a sanitizer matrix job (
zig build -Dsanitize=address test) running the unit suite at least nightly.7. No Vulkan validation layer enforcement
Lavapipe is set up in the integration test, but there's no fail-on-validation-error gate. Shader misuse, wrong pipeline barriers, and binding bugs slip through silently.
Fix:
VK_LAYER_KHRONOS_validationwithvalidate_core=trueand best-practices enabled in CI.VK_LAYER_LOGGING=stderrand fail the integration/world-smoke steps on any validation error.8. No security / supply-chain scanning
None of these exist today:
nix flake showauditThe repo already grants
id-token: writeto build jobs, so signing/provenance is the natural next step.9. No SECURITY.md / CODEOWNERS / CODE_OF_CONDUCT.md
Fix: Add the three files. Wire CODEOWNERS to trigger mandatory reviews for hot paths (
modules/engine-graphics/,modules/world-persistence/,.github/workflows/).10. Tested in one build mode only
CI runs
zig build testin Debug only. Release-only UB (the kind that bites players) is invisible.Fix: Add a
ReleaseSafematrix job to the unit-test step.ReleaseFastis overkill for correctness butReleaseSafecatches optimization-sensitive UB.P2 — Medium (lint & process hygiene)
11. No
shellcheckscripts/*.shonly getsbash -n(syntax check) inworkflow-validation.yml. Real issues — unquoted expansions,SC2086,SC2046— pass silently. The repo has 30+ shell scripts driving CI.Fix: Add shellcheck to
workflow-validation.ymlwith a config that's strict on errors and warnings.12. No markdown / JSON / YAML linters
README, CONTRIBUTING, AGENTS, the 4 PR templates, issue templates, and
docs/**/*.mdare all unchecked.baseline.jsonand any future config files have no schema. YAML is only parse-checked via ruby.Fix:
markdownlint-cli2for prose.prettier --checkfor JSON/YAML.baseline.json,labeler.yml,issue-labeler.json.13. No conventional-commit enforcement
CONTRIBUTING.md/AGENTS.mdmandatefeat:/fix:/refactor:/test:/docs:but nothing enforces it on commit messages or PR titles. Commits drift from convention silently.Fix: Add an amiquick / commitlint / PR-title-check action.
14. No DCO / CLA / commit signing
Contribution provenance is unverified. For an engine that may eventually be licensed or distributed, signed-off-by trails matter.
Fix: Add a DCO check (
dco-app) as a required status check; optionally enforce GPG/SSH commit signing.15. No
nix flake checkin CIThe flake could be malformed in ways that only show up outside the dev shell. Reproducibility story is undermined if the flake itself isn't validated.
Fix: Add
nix flake check --no-build(or scoped variant) toworkflow-validation.yml.16. No stale issue / PR bot
Only stale branch cleanup exists (
stale-branch-cleanup.yml). Issues and PRs can rot indefinitely.Fix: Add
actions/stalewith sane exemption labels (pinned,roadmap,batch-*).17. Visual regression is LLM-judged only — no deterministic golden-image diff
visual-test.ymlfeeds screenshots to MiniMax-M3 for verification. There's no deterministic perceptual/pixel comparison against a golden image.Fix: Add an ImageMagick
comparestep (orpixelmatch,odiff) with a configurable tolerance. Use it as the primary gate, with the LLM as a secondary diagnostic. Otherwise silent visual regressions the model normalizes away will ship.P2 — Medium (observability)
18. No crash reporting hook
No Sentry / Backtrace / minidump capture. Integration tests discard cores. In-dev crashes from testers are unattributable.
Fix: Wire a minimal crash handler (Breakpad / minidump) and an opt-in upload sink. Even just persisting minidumps as CI artifacts on integration failures would be a start.
19. No historical benchmark trend store
Each benchmark run overwrites the last. There's no Bencher / Conbench / InfluxDB / Hydra-style history, so month-over-month drift is invisible.
Fix: Push benchmark results to a trend store (Bencher is the obvious OSS pick; works well with GitHub Actions).
20. Single-platform CI
Linux + Lavapipe only. No Windows runner. No macOS runner. No MoltenVK path. Production engines test on every target platform — Lavapipe won't catch driver-specific issues that real users hit.
Fix: Add a Windows runner to
build.yml(Vulkan on Windows is straightforward via SDK). macOS is harder (MoltenVK) — at least ensure it builds.21. No automated profiling capture in CI
Tracy / Perfetto capture isn't automated. Profiling requires manual
-Dflags and a developer at the keyboard.Fix: Add a nightly job that captures a Tracy frame for a fixed world and uploads it as an artifact so regressions are inspectable after the fact.
P3 — Lower (game-engine-specific guardrails)
22. No asset size guardrails
Textures and models can grow without a CI check. The repo already gitignores
*_4k.*to keep 4K sources out, but there's no automated ceiling on what's committed or shipped.Fix: Add a CI step that fails if any new texture exceeds a budget (e.g. 4 MB) or if total
assets/size grows beyond a threshold.23. No draw-call / GPU-memory / vertex budget test
Benchmarks log
draw_calls_avg,vertices_avg, GPU ms — but never assert a ceiling. A 3× draw-call regression ships green.Fix: Add per-preset ceilings to the benchmark gate.
24. No shader compile-time / SPIR-V size regression
glslangValidatorvalidates correctness intest, but doesn't fail on a 2× SPIR-V size growth or compile-time spike. Shader bloat creeps in.Fix: Log SPIR-V size per shader in
test, fail on >N% regression vs a baseline.25. No fuzz / property-based tests
Worldgen has many determinism tests, but no fuzz harness. Edge cases that matter for a voxel engine — chunk coordinate boundaries, save-file corruption, malformed region files, integer overflow in light propagation — are untested.
Fix: Add fuzz harnesses for:
26. No save-format backward-compatibility test
world-persistencehas no "load a v0.1 save" golden fixture. A schema change can silently brick every existing world.Fix: Commit a small golden save file per format version, add a test that loads it after every persistence change.
Suggested breakdown order
When this is split into sub-issues, the dependency order is roughly:
Items #12 (markdown/json/yaml lint), #15 (
nix flake check), #16 (stale bot) are independent quick wins that can land anytime.Audit context
dev/cc anyone picking this up — please comment on which wave/item you're starting before opening sub-issues so we don't duplicate.