Artifact b4e7fde8ffe45298c2775d4f99f5382acba72f69b6e2d9f72d59e71d4531f60f:
- File
.github/workflows/ci.yml
— part of check-in
[861b223e3c]
at
2026-07-19 15:25:14
on branch trunk
— CI: fuzz sweep at 100 rounds per target, not 2000
The sweep could not finish inside its own 10-minute step timeout. Per-target cost varies by roughly 276x across the corpus: artifact_parse_garbage runs 100 rounds in 0.18s, while logstore_pack_same_path_round_trip takes 49s because it performs a full store pack every round. At 2000 rounds that single target needs about 16 minutes on its own.
The original figure was extrapolated from one cheap target on the assumption that per-round cost was roughly uniform. It is not, and the assumption was never measured. 100 rounds is measured: the full 27-target sweep takes about 76s.
This makes CI fuzzing a regression check — every target still runs clean on every push — rather than a bug hunt. Finding new defects needs depth, and depth belongs in an out-of-band run with its own budget, where a long sweep costs nobody a merge. (user: dmestas, size: 8155) [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 # 100, not 2000. Per-target cost varies by ~276x across the corpus — # artifact_parse_garbage runs 100 rounds in 0.18s while # logstore_pack_same_path_round_trip takes 49s, because it performs a # full store pack per round. The original 2000 was extrapolated from # one cheap target and made the sweep unable to finish inside this # step's own timeout: that single target alone would need ~16 minutes. # Measured at 100 rounds the whole sweep is ~76s, which is the budget # this number is chosen against, not a guess. # # What this buys is REGRESSION DETECTION — every target still runs # clean on every push. It is not a bug hunt: finding new defects needs # depth, and depth belongs in an out-of-band run with its own budget, # where a 30-minute sweep costs nobody a merge. FUZZ_ROUNDS: 100 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