Artifact 7b817b4a7c230e3f351a112a83c0cad98db79add99264a07549cab8d499120f9:
- File
.github/workflows/ci.yml
— part of check-in
[be97df0398]
at
2026-07-20 21:26:25
on branch trunk
— Release zit 0.1.1
Harden native Linux filesystem behavior, deterministic repository iteration, update and undo transitions, Fossil smoke-process containment, and the three-platform CI matrix. (user: dmestas, size: 8177) [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 release gate across the native macOS/ARM64 and Linux x86-64/ARM64 # platforms. Each label set is intentionally precise: Linux legs remain queued # until the corresponding self-hosted runner is registered; they never fall # back to a hosted or arbitrary self-hosted machine. jobs: battery: strategy: fail-fast: false matrix: include: - os: macOS arch: ARM64 runner: mac-mini-zit zig_target: aarch64-macos zig_shasum: b23d70deaa879b5c2d486ed3316f7eaa53e84acf6fc9cc747de152450d401489 - os: Linux arch: X64 runner: linux-x64-zit zig_target: x86_64-linux zig_shasum: 70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00 - os: Linux arch: ARM64 runner: linux-arm64-zit zig_target: aarch64-linux zig_shasum: ea4b09bfb22ec6f6c6ceac57ab63efb6b46e17ab08d21f69f3a48b38e1534f17 runs-on: [self-hosted, "${{ matrix.os }}", "${{ matrix.arch }}", "${{ matrix.runner }}"] # 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_shasums`, which fail `zig build test` if this file drifts. # These are the target-specific published digests from # ziglang.org/download/index.json. ZIG_VERSION: 0.16.0 ZIG_TARGET: ${{ matrix.zig_target }} ZIG_SHASUM: ${{ matrix.zig_shasum }} 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. Fossil is provisioned by each # self-hosted runner image, so print the actual version used by the gate. - 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 "provision fossil in this self-hosted runner image." >&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. `sha256sum` is standard on Linux; macOS supplies # `shasum`, so select the available native checksum tool. - name: Install Zig ${{ env.ZIG_VERSION }} run: | set -euo pipefail tarball="zig-${ZIG_TARGET}-${ZIG_VERSION}.tar.xz" curl -fsSL -o "$tarball" "https://ziglang.org/download/${ZIG_VERSION}/${tarball}" if command -v sha256sum >/dev/null 2>&1; then actual="$(sha256sum "$tarball" | awk '{print $1}')" else actual="$(shasum -a 256 "$tarball" | awk '{print $1}')" fi 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-${ZIG_TARGET}-${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. The targets also reach `zig build test` # through fixed-seed smoke cases, but this step exercises every registered # target through the real fuzz runner. # # `zig build fuzz` runs ONE target per invocation, so this sweeps them. # `--list` exits successfully and writes the complete registration table; # unlike scraping a failed bare invocation, its output is not truncated by # Zig's build-failure summary on Linux. A target added to fuzz_tests.zig is # therefore fuzzed automatically, with no second list to drift out of sync. # # 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 -- --list)" 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