Artifact 6d343370ac09cc6826509b7c22e045fd7c8b2272504ced464c382a24588d0878:
- File
.github/workflows/ci.yml
— part of check-in
[394b4efa27]
at
2026-07-19 01:09:25
on branch trunk
— CI: sweep every seeded fuzz target, and bump checkout to v5
Two gaps the first green run exposed.
The repository carried 26 fuzz targets that CI never fuzzed. They reached CI only through their fixed-seed smoke tests inside `zig build test`, so the fuzzing investment was half-realized: the targets compiled and ran a handful of canned cases, but nothing swept them. `zig build fuzz` runs one target per invocation, so the new step loops. The target list is read from the step's own usage output rather than kept in the workflow — a target added to fuzz_tests.zig is swept automatically, with no second list to drift. Seed and rounds are fixed (1, 2000), not random: a red run has to be reproducible verbatim on a developer machine. Bare `zig build fuzz` exits non-zero because it is a usage error, so the enumeration tolerates that; every real invocation still fails the job. Verified by running the identical block under bash — 26/26 targets pass at seed 1.
src/ci_test.zig moves with it. `fuzz` leaves not_in_ci (its stated reason was that wiring a bounded CI invocation belonged to a later step — that step is this one) and a new ci_parameterized table records it as covered-in-a-loop, carrying the exact anchor that proves the invocation exists. The anchor is verified, not trusted: an entry whose anchor drifts out of the workflow fails like any other gap. That distinction is kept separate from not_in_ci on purpose — conflating 'runs, in a loop' with 'deliberately skipped' is how a step silently stops being covered while a stale excuse keeps the pin green.
actions/checkout v4 -> v5 clears the Node 20 deprecation warning the green run reported. (user: dmestas, size: 7246) [annotate] [blame] [check-ins using]
name: ci # Every push on every branch, plus pull requests. Deliberately unfiltered: the # 0.1.0 release is developed on `release-0.1`, and a default-branch-only gate # would leave the entire release unverified until the merge that is supposed to # be the safe part. on: push: pull_request: # Least privilege: the battery only reads the checkout. Without an explicit # block the job inherits whatever the repo/org default grants, which is # read/write on many configurations. permissions: contents: read # Unfiltered `push:` plus `pull_request:` means a PR branch runs the whole # battery twice per push. Superseded runs are cancelled rather than left to # finish against a commit nobody is waiting on. concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true # One gating job. A macOS leg is 0.2 material; splitting the battery across # several jobs would read as broader coverage than it is. jobs: battery: # Self-hosted (mac-mini-zit, macOS/ARM64). GitHub-hosted runners are not # available to this repository: it is private, and private-repo Actions # minutes are billed, so the first hosted run never started ("recent account # payments have failed or your spending limit needs to be increased"). The # label set is narrowed to the specific machine rather than bare # `self-hosted` so a second runner joining the account cannot silently pick # up this job on a different OS or architecture. runs-on: [self-hosted, mac-mini-zit] # An explicit ceiling, per the bounded-waits rule: every wait needs an # interval and a timeout. Without this the job inherits GitHub's 360-minute # default, so a hang — most plausibly smoke's spawned server never exiting — # turns a red gate into a six-hour silent stall instead of a failure. timeout-minutes: 20 env: # Kept in lockstep with src/ci_test.zig's `pinned_zig_version` and # `pinned_zig_shasum`, which fail `zig build test` if this file drifts. # The digest is the published one from ziglang.org/download/index.json, # so a swapped tarball is a build failure rather than a silent event. ZIG_VERSION: 0.16.0 # aarch64-macos, matching the self-hosted runner. Published digest from # ziglang.org/download/index.json. ZIG_SHASUM: b23d70deaa879b5c2d486ed3316f7eaa53e84acf6fc9cc747de152450d401489 steps: - uses: actions/checkout@v5 # smoke drives the real fossil binary — it is the only step that proves # the conformance claim against the reference implementation rather than # against our own reading of the RFCs. The runner's fossil is whatever # apt ships and will lag the local 2.28, so print it: the release notes # must name the exact version smoke passed against (readiness item S7). - name: Verify fossil run: | set -euo pipefail command -v fossil >/dev/null 2>&1 || { echo "fossil is not installed on this runner; smoke cannot run." >&2 echo "install it with: brew install fossil" >&2 exit 1 } fossil version # Installed by hand rather than through a setup action: the toolchain is # one tarball, the URL is stable, and verifying the published digest here # is both simpler and stronger than trusting a third-party action to pin # the same thing. - name: Install Zig ${{ env.ZIG_VERSION }} run: | set -euo pipefail tarball="zig-aarch64-macos-${ZIG_VERSION}.tar.xz" curl -fsSL -o "$tarball" "https://ziglang.org/download/${ZIG_VERSION}/${tarball}" # macOS ships `shasum`, not GNU `sha256sum`, so the digest is compared # explicitly rather than through `--check --strict`. Same guarantee: # a swapped tarball fails the job instead of building silently. actual="$(shasum -a 256 "$tarball" | awk '{print $1}')" if [ "$actual" != "${ZIG_SHASUM}" ]; then echo "zig tarball digest mismatch: expected ${ZIG_SHASUM}, got ${actual}" >&2 exit 1 fi tar -xf "$tarball" echo "${GITHUB_WORKSPACE}/zig-aarch64-macos-${ZIG_VERSION}" >> "$GITHUB_PATH" - name: Zig version run: zig version # `--check build.zig src` rather than `--check .`: the bare dot walks # .zig-cache/ and zig-out/, whose generated sources are not ours to # format. Naming the source roots keeps the check meaning the same thing # on a cold runner and in a warm local tree. - name: Format check run: zig fmt --check build.zig src - name: Build run: zig build # The battery runs each step explicitly. Local semantics are deliberately # unchanged: e2e staying compile-only under `zig build test` is a good # inner-loop design, so CI names the steps rather than the build graph # being rewired to fold them in. - name: Unit tests run: zig build test - name: End-to-end tests run: zig build e2e - name: Deterministic simulation run: zig build dst # The seeded fuzz corpus, bounded. Until now the targets reached CI only # through their fixed-seed smoke tests inside `zig build test`, so the # repository carried 26 fuzz targets that CI never actually fuzzed. # # `zig build fuzz` runs ONE target per invocation, so this sweeps them. # The list is read from the step's own usage output rather than kept here: # a target added to fuzz_tests.zig is fuzzed by CI automatically, with no # second list to drift out of sync. Bare `zig build fuzz` exits non-zero # (it is a usage error), hence the `|| true` on the enumeration only — # every real invocation below still fails the job. # # Seed and rounds are fixed, not random: a red CI run has to be # reproducible verbatim on a developer machine. Deeper sweeps belong to # the out-of-band runner with its own budget. - name: Seeded fuzz sweep timeout-minutes: 10 env: FUZZ_SEED: 1 FUZZ_ROUNDS: 2000 run: | set -euo pipefail targets="$( (zig build fuzz 2>&1 || true) | sed -n '/^targets:/,$p' | grep -E '^ [a-z0-9_]+$' | tr -d ' ' )" if [ -z "${targets}" ]; then echo "no fuzz targets enumerated — the usage format changed and this sweep is vacuous" >&2 exit 1 fi count=0 for target in ${targets}; do echo "fuzz: ${target} (seed ${FUZZ_SEED}, rounds ${FUZZ_ROUNDS})" zig build fuzz -- "${target}" --seed "${FUZZ_SEED}" --rounds "${FUZZ_ROUNDS}" count=$((count + 1)) done echo "fuzz sweep: ${count} targets at seed ${FUZZ_SEED}" # The tightest per-step bound in the battery, and deliberately so: smoke # spawns a server, which is the one step that can hang without producing # output. The job ceiling would eventually catch it; this catches it while # the log still reads as a failed step rather than a dead run. - name: Fossil interop smoke timeout-minutes: 5 run: zig build smoke