Artifact 44658d565ebc7ab6d7c30b134788846fcb8278c8306241e48ad763588f9c3dd6:
- File
src/ci_test.zig
— part of check-in
[fa78377389]
at
2026-09-18 12:31:12
on branch trunk
— soak: a nightly deep DST and fuzz run
scripts/soak.sh passed the optimize flag this Zig rejects, so it has been unrunnable since the toolchain moved; it takes --release=safe now. A scheduled workflow runs it at 03:00 UTC on the x64 runner with the script's nightly budgets, overridable from the dispatch form, keeps the log and any reproduce line as an artifact, and is pinned to the ci toolchain by ci_test like the other workflows. (user: dmestas, size: 33642) [annotate] [blame] [check-ins using]
//! Structural pins over the GitHub Actions workflow that gates this repository. //! //! The pipeline itself is the real test — a green run against a real runner is //! the only thing that proves fossil installs, Zig builds, and smoke can spawn //! a server. What a green run cannot prove is that the workflow still invokes //! everything it is supposed to invoke: a build step added to build.zig months //! from now is silently *not* run by CI, and CI stays green while covering //! less. That is the failure this module exists to catch, and it is a //! repository-file fact, so it is pinned the same way src/packaging_test.zig //! pins the shipped distribution: embed the file, assert on its text, let //! `zig build test` be the thing that notices. //! //! The workflow arrives through build.zig's `build_unit_test_module` as the //! anonymous import `ci_workflow`, deliberately on the test-only module: the //! exported `zit` module must never depend on a repo-root file outside //! `build.zig.zon`'s `.paths`, or a consumer fetching the package fails before //! compiling a line. `.github` is allowlisted in packaging_test.zig's //! `test_only_roots` for exactly that reason. //! //! That allowlist is a *packaging* exemption only. The workflow file still has //! to be version-controlled, or a clean checkout fails to build on the embed — //! and because it lives under a dot-directory, bare `fossil extras` never //! reports it as untracked (`fossil extras --dotfiles` does). That near-miss is //! why src/packaging_test.zig now pins tracking separately from packaging. //! //! These are text assertions, not a YAML parse. A pinned command sitting inside //! a YAML comment would satisfy them. That trade is taken knowingly: a parser //! for one file is more machinery than the risk warrants, and the workflow is //! short enough that a human reads it whole. What the pins buy is the //! *negative space* — the reviewed-exception list below — which no amount of //! reading catches reliably. const std = @import("std"); const ci_workflow = @embedFile("ci_workflow"); const release_workflow = @embedFile("release_workflow"); const soak_workflow = @embedFile("soak_workflow"); const build_source = @embedFile("build_source"); fn expect_contains(haystack: []const u8, needle: []const u8, where: []const u8) !void { if (std.mem.indexOf(u8, haystack, needle) != null) return; std.debug.print("\nci pin: {s} does not contain \"{s}\"\n", .{ where, needle }); return error.CiPinMissing; } /// Counts non-overlapping occurrences of `needle`. Used for the single-job pin: /// "one gating job" is a claim about how many `runs-on:` keys exist, which a /// containment check cannot make. fn count_occurrences(haystack: []const u8, needle: []const u8) usize { if (needle.len == 0) return 0; var seen: usize = 0; var scan: usize = 0; while (std.mem.indexOfPos(u8, haystack, scan, needle)) |hit| { seen += 1; scan = hit + needle.len; } return seen; } /// The exact Zig toolchain the release is built and tested with. Pinned as a /// constant here and asserted against the workflow so the two cannot drift: /// a runner silently upgrading Zig turns "0.1.0 builds on 0.16.0" into an /// unverified claim. const pinned_zig_version = "0.16.0"; /// The oldest fossil the conformance claim is stated against. Not a /// formality: `smoke` is the only lane that runs a real fossil against zit, so /// this number is what "conforms to fossil" means for a given release, and the /// runners must agree on it or the gate's verdict is unattributable. const pinned_fossil_minimum = "2.28"; /// Published SHA-256 digests of the three pinned Zig toolchain tarballs /// (ziglang.org/download/index.json, 2026-07-20). The workflow matrix verifies /// each native target against its own digest; pinning all three here prevents a /// platform leg from silently accepting a swapped or mismatched tarball. const pinned_zig_shasums = [_][]const u8{ "b23d70deaa879b5c2d486ed3316f7eaa53e84acf6fc9cc747de152450d401489", "70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00", "ea4b09bfb22ec6f6c6ceac57ab63efb6b46e17ab08d21f69f3a48b38e1534f17", }; /// The command that turns the digests above from recorded values into /// enforced ones. Pinned separately because the two are independently /// deletable: dropping this line while leaving `ZIG_SHASUM:` in the `env:` /// block would leave a digest sitting in the file that nothing checks — the /// same "a value appearing in the file is not the value being used" trap the /// battery's whole-line anchoring guards against. /// Anchored to the whole checking command, not the bare tool name: `sha256sum /// --check --strict` appearing anywhere satisfies a loose substring while the /// value actually fed to it is something other than the pinned `ZIG_SHASUM` — /// a digest check that verifies the wrong digest. Naming the env var and the /// tarball on the same line is the reverse assertion: the digest this file /// pins is provably the digest the workflow checks. const pinned_digest_check = "if [ \"$actual\" != \"${ZIG_SHASUM}\" ]; then"; /// The release battery, in the order the workflow runs it — asserted, not just /// described: the pin below requires each entry to appear later in the file than /// the one before it. /// /// Each entry is a whole `run:` line, `run: ` prefix and trailing newline /// included. Anchoring matters more than it looks: as a bare substring /// `zig build` is contained in `zig build test`, `zig build e2e`, `zig build /// dst` and `zig build smoke`, so deleting the bare-build step would have left /// the pin passing while claiming coverage it no longer had. /// /// `zig fmt --check build.zig src` rather than `--check .`: the bare dot walks /// `.zig-cache/` and `zig-out/`, whose generated sources are not ours to /// format and whose presence differs between a cold runner and a warm local /// tree. Naming the two real source roots keeps the check meaning the same /// thing in both places. const battery = [_][]const u8{ "run: zig fmt --check build.zig src\n", "run: zig build\n", "run: zig build test\n", "run: zig build e2e\n", "run: zig build dst\n", "run: zig build smoke\n", "run: zig build muscle\n", }; /// Every step build.zig declares. Asserted as an exact set, in neither /// direction forgiving: a step added to build.zig and a step deleted from it /// both fail here, which is the point — the scan below is only meaningful if it /// is looking at the step list this file was written against. /// /// An earlier revision guarded the scan with `checked < 8`, a floor sitting /// exactly at the current count. It fired on any legitimate step removal, said /// nothing about *which* step went missing, and would have been satisfied by a /// step being renamed. Naming the set costs the same edit and reports the /// actual difference. const declared_steps = [_][]const u8{ "test", "gate", "run", "e2e", "git-import-e2e", "git-export-e2e", "fuzz", "dst", "bench", "import-git-bench", "smoke", "muscle", "ux", "cov", "git-import-refs", "git-import-cli", "cross", }; /// Steps declared in build.zig that CI deliberately does not run, each with the /// reason it is out of scope. This list is the point of the whole module: the /// pin below fails when build.zig grows a step that is neither invoked by CI /// nor named here, which forces the "should CI run this?" decision at the /// moment the step is written instead of never. /// Steps CI runs, but not as a bare `run: zig build <step>` line — the sweep /// below drives `fuzz` one target per invocation inside a loop, so the plain /// whole-line invocation the forward pin looks for never appears. Each entry /// carries the exact anchor that proves the step really is invoked, so this is /// an alternate proof of coverage, NOT an exemption from it: an entry whose /// anchor is absent fails exactly like a missing invocation. Keeping it /// separate from `not_in_ci` matters — conflating "runs, in a loop" with /// "deliberately skipped" is how a step silently stops being covered while a /// stale excuse keeps the pin green. const ci_parameterized = [_]struct { step: []const u8, anchor: []const u8, how: []const u8 }{ .{ .step = "fuzz", .anchor = "zig build fuzz -- \"${target}\" --seed \"${FUZZ_SEED}\" --rounds \"${FUZZ_ROUNDS}\"\n", .how = "swept over every target enumerated from the step's own usage output, at a fixed seed and round budget", }, }; const not_in_ci = [_]struct { step: []const u8, why: []const u8 }{ .{ .step = "run", .why = "developer convenience wrapper around the installed exe" }, .{ .step = "gate", .why = "aggregate of lanes CI already invokes one by one; running it too would rebuild and rerun the same work while reporting one failure where CI currently reports which lane broke. Note this means `gate` itself is never exercised in CI -- if a lane is ever added to `gate` WITHOUT a matching `run:` line below, it will be gated locally and nowhere else" }, .{ .step = "bench", .why = "measures throughput; a shared runner's timings are noise, and rule 7 forbids claims not backed by a real measurement" }, .{ .step = "import-git-bench", .why = "bounded scaling qualification workload, deliberately out-of-band like the other benchmark" }, .{ .step = "cov", .why = "out-of-band by design (needs kcov installed); never gates a commit" }, .{ .step = "ux", .why = "the muscle session CI already runs, with every step's bytes kept under zig-out/ux for the reading page; the transcripts are a developer artifact, not a check" }, .{ .step = "git-import-refs", .why = "`zig build test` runs this test binary; the dedicated step is only a focused developer alias" }, .{ .step = "git-import-cli", .why = "`zig build test` runs this test binary; the dedicated step is only a focused developer alias" }, .{ .step = "cross", .why = "CI compiles the real thing on all three targets it ships for, so cross-compiling them again would prove nothing it has not already proved. The step exists for the DEVELOPER: macOS links libc always, so a call demanding libc builds on a mac, passes every local lane, and refuses on both Linux runners. `gate` depends on it, which is where it is meant to fire -- before the push, not after" }, }; test "the workflow triggers on every branch push and on pull requests, and not on a tag push" { // Anchored as the whole trigger block rather than as three loose // substrings: bare `on:` is contained in `runs-on:`, so it asserted nothing // about the triggers at all. Tag pushes are excluded on purpose: a release // tag lands on a commit whose branch push already ran this battery, and // running it again queued the release build behind a second battery on // the same single-slot runners. The release workflow gates the tag. // Both filters, because GitHub runs a `push` trigger naming only a tag // filter for tag pushes alone: `tags-ignore` by itself ran nothing for // two commits, and this text pin could not see it — a trigger change is // proven by a run appearing, never by this test alone. try expect_contains(ci_workflow, "on:\n push:\n branches: ['**']\n tags-ignore: ['**']\n pull_request:\n", ".github/workflows/ci.yml"); // `branches: ['**']` is every branch — the unfiltered intent: a release // branch is gated too, and a workflow that fired only for the default // branch would leave a release unverified until the merge. Any narrower // `branches:` form is refused. const every_branch = "branches: ['**']"; var rest: []const u8 = ci_workflow; while (std.mem.indexOf(u8, rest, "branches:")) |i| { if (!std.mem.startsWith(u8, rest[i..], every_branch)) { std.debug.print("\nci pin: .github/workflows/ci.yml filters triggers by branch; " ++ "a release branch must be gated too, so only `branches: ['**']` is allowed\n", .{}); return error.CiPinMissing; } rest = rest[i + every_branch.len ..]; } } test "the workflow pins every native Zig toolchain by version and digest" { // Target-specific digests live in the matrix; the environment assignment // must select the current leg's value rather than restating one platform. try expect_contains(ci_workflow, "ZIG_VERSION: " ++ pinned_zig_version ++ "\n", ".github/workflows/ci.yml"); try expect_contains(ci_workflow, "ZIG_SHASUM: ${{ matrix.zig_shasum }}\n", ".github/workflows/ci.yml"); try expect_contains(ci_workflow, "zig_shasum: " ++ pinned_zig_shasums[0] ++ "\n", ".github/workflows/ci.yml"); try expect_contains(ci_workflow, "zig_shasum: " ++ pinned_zig_shasums[1] ++ "\n", ".github/workflows/ci.yml"); try expect_contains(ci_workflow, "zig_shasum: " ++ pinned_zig_shasums[2] ++ "\n", ".github/workflows/ci.yml"); // The digest is only a supply-chain guarantee if something checks it. See // `pinned_digest_check`. try expect_contains(ci_workflow, pinned_digest_check, ".github/workflows/ci.yml"); } test "the workflow bounds how long a run may hang" { // The bounded-waits rule applied to CI itself. GitHub's default is 360 // minutes, which is indistinguishable from "no timeout" in practice: a // hung smoke server would hold the gate open for six hours without ever // reporting failure. // // So the values are pinned, not merely the key. Asserting that // `timeout-minutes:` appears twice is satisfied by `timeout-minutes: 360` // in both places — the default restated, certifying neither an interval nor // a bound. Each pin is a whole line with its YAML indentation, the way the // `permissions:\n contents: read\n` pin is: four spaces is the job // ceiling, eight is a step bound, so the indentation is what makes "the // job has a ceiling *and* smoke has a tighter one" a claim rather than a // count of keys. try expect_contains(ci_workflow, "\n timeout-minutes: 20\n", ".github/workflows/ci.yml"); // smoke spawns a server: it is the one step that can hang without producing // output, so it carries the tightest bound in the battery. try expect_contains(ci_workflow, "\n timeout-minutes: 5\n", ".github/workflows/ci.yml"); } test "the workflow takes the least privilege it can and cancels superseded runs" { // The battery only reads the checkout. Without an explicit block the job // inherits the repo/org default, which is read/write on many // configurations — a wider token than any step here has a use for. try expect_contains(ci_workflow, "permissions:\n contents: read\n", ".github/workflows/ci.yml"); // Unfiltered `push:` plus `pull_request:` doubles up on PR branches; a // superseded run should not keep burning a runner. Anchored to the whole // line with its `concurrency:` indentation, like the `permissions:` pin // above: the bare substring is satisfied by the phrase in a comment, or by // `cancel-in-progress: true` nested under something that does not govern // this workflow's runs. try expect_contains(ci_workflow, "\n cancel-in-progress: true\n", ".github/workflows/ci.yml"); } test "the workflow installs fossil and records the version it got" { // smoke drives the real `fossil` binary; without it the conformance claim // the release rests on is not exercised at all. Anchored to the install // command rather than to the bare word "fossil": that word appears in this // file's own explanatory comments, so the loose pin stayed green even with // the entire install step deleted. // The self-hosted runners are developer machines with fossil already // installed, so the workflow verifies rather than installs — but it // must still HARD-FAIL when fossil is absent, or smoke would be skipped and // the conformance claim would quietly stop being exercised. Pinned to the // guard, not to the bare word "fossil": that word appears in this file's // own comments, so a loose pin stayed green with the whole step deleted. try expect_contains(ci_workflow, "command -v fossil >/dev/null 2>&1 || {", ".github/workflows/ci.yml"); // The release notes have to name the exact version smoke passed against // (readiness item S7), which is only knowable if the run prints it — so // pin the whole command line, which a prose mention of "fossil version" // cannot satisfy. try expect_contains(ci_workflow, "\n fossil version\n", ".github/workflows/ci.yml"); // Printing alone was not enough. The runners drifted onto different // fossils, and because `smoke` is the only lane that runs a real fossil // against zit, the conformance claim silently meant a different thing on // each: a regression against the newer could pass on the older, and // newer-only behaviour reddened the older for something that was not a // defect. `fossil sqlite3` quoting its output on 2.26 but not 2.28 cost // three CI round trips to find, one of which shipped a fix that fixed // nothing. Pinned to the comparison itself rather than to the minimum // alone, so deleting the gate and leaving the number behind fails here. try expect_contains(ci_workflow, "minimum=\"" ++ pinned_fossil_minimum ++ "\"\n", ".github/workflows/ci.yml"); try expect_contains( ci_workflow, "if [ \"$(printf '%s\\n%s\\n' \"$minimum\" \"$version\" | sort -V | head -1)\" != \"$minimum\" ]; then", ".github/workflows/ci.yml", ); } test "the workflow runs the whole release battery, in order" { // `previous` starts unset rather than at 0 so the first entry is not // required to sit at a strictly positive offset — the comparison below is // strict, and a battery entry legitimately could begin at byte 0. var previous: ?usize = null; for (battery) |command| { const at = std.mem.indexOf(u8, ci_workflow, command) orelse { std.debug.print("\nci pin: .github/workflows/ci.yml has no line \"{s}\"", .{command}); return error.CiPinMissing; }; // Each battery command runs exactly once. Ordering alone cannot make // this claim: duplicating a step leaves every entry's *first* offset // unchanged, so the comparison below stays satisfied while the battery // silently burns a slow step twice. const runs = count_occurrences(ci_workflow, command); if (runs != 1) { std.debug.print("\nci pin: .github/workflows/ci.yml runs \"{s}\" {d} times; " ++ "each battery command is run exactly once\n", .{ command, runs }); return error.CiPinMissing; } // Ordering is part of the claim: format check before build before // tests, so a run fails at the cheapest step that can catch the fault // rather than after the slow ones have burned runner minutes. // Strict, not `<`: with the exactly-once pin above, equal offsets are // unreachable, so the strictness is a statement of intent rather than a // live guard. Note the remaining softness — `indexOf` finds the *first* // occurrence, so a command named in a comment above its real step is // what gets ordered. // That is the text-assertion trade this module's header declares. if (previous) |before| if (at <= before) { std.debug.print("\nci pin: .github/workflows/ci.yml runs \"{s}\" out of order; " ++ "the battery in src/ci_test.zig is ordered cheapest-failure-first\n", .{command}); return error.CiPinMissing; }; previous = at; } } test "the workflow enumerates the complete fuzz registry through list mode" { // A failed bare fuzz invocation is summarized by `zig build`; on Linux its // leading target names can be truncated before the workflow parses them. // Successful list mode makes complete enumeration part of the CLI contract. try expect_contains( ci_workflow, "targets=\"$(zig build fuzz -- --list)\"\n", ".github/workflows/ci.yml", ); } test "the workflow is one matrix job gated on explicit native runners" { // One matrix job runs the identical battery on every native release target. // Its conjunctive labels prevent a leg from falling onto an arbitrary // self-hosted runner, while each matrix entry names one dedicated machine. try expect_contains( ci_workflow, "\n runs-on: [self-hosted, \"${{ matrix.os }}\", \"${{ matrix.arch }}\", \"${{ matrix.runner }}\"]\n", ".github/workflows/ci.yml", ); try expect_contains(ci_workflow, "runner: mac-mini-zit\n", ".github/workflows/ci.yml"); try expect_contains(ci_workflow, "runner: linux-x64-zit\n", ".github/workflows/ci.yml"); try expect_contains(ci_workflow, "runner: linux-arm64-zit\n", ".github/workflows/ci.yml"); const jobs = count_occurrences(ci_workflow, "runs-on:"); if (jobs != 1) { std.debug.print("\nci pin: .github/workflows/ci.yml declares {d} jobs (`runs-on:` keys); " ++ "0.1.1 gates all native targets through exactly one matrix job\n", .{jobs}); return error.CiPinMissing; } } test "the soak workflow pins the same toolchain as ci and runs the soak script nightly" { // The nightly soak builds with the CI toolchain, or its findings would // be findings about a different compiler. The digest is the x86_64 one. try expect_contains(soak_workflow, "ZIG_VERSION: " ++ pinned_zig_version ++ "\n", ".github/workflows/soak.yml"); try expect_contains(soak_workflow, "ZIG_SHASUM: " ++ pinned_zig_shasums[1] ++ "\n", ".github/workflows/soak.yml"); try expect_contains(soak_workflow, " schedule:\n - cron: ", ".github/workflows/soak.yml"); try expect_contains(soak_workflow, "bash scripts/soak.sh\n", ".github/workflows/soak.yml"); } test "the release workflow builds every artifact from an explicit baseline target" { try expect_contains( release_workflow, "run: zig build --release=fast -Dtarget=\"${ZIG_TARGET}\"\n", ".github/workflows/release.yml", ); try expect_contains(release_workflow, "zig_target: aarch64-macos\n", ".github/workflows/release.yml"); try expect_contains(release_workflow, "zig_target: x86_64-linux\n", ".github/workflows/release.yml"); try expect_contains(release_workflow, "zig_target: aarch64-linux\n", ".github/workflows/release.yml"); } test "the release workflow builds from the exact Fossil release tag" { try expect_contains( release_workflow, "tag=\"version-${version}\"\n", ".github/workflows/release.yml", ); try expect_contains( release_workflow, "fossil open \"${repo}\" \"${tag}\"\n", ".github/workflows/release.yml", ); } test "the release workflow qualifies packaged Linux binaries below the build hosts" { try expect_contains( release_workflow, "uses: actions/download-artifact@v4\n", ".github/workflows/release.yml", ); try expect_contains(release_workflow, "runs-on: ubuntu-24.04\n", ".github/workflows/release.yml"); try expect_contains( release_workflow, "qemu-x86_64 -cpu Nehalem \"${binary}\" version\n", ".github/workflows/release.yml", ); try expect_contains( release_workflow, "qemu-aarch64 -cpu cortex-a53 \"${binary}\" version\n", ".github/workflows/release.yml", ); } test "every build.zig step is either run by CI or a reviewed exception" { // The negative-space pin. Scans build.zig for `b.step("<name>"` / // `env.b.step("<name>"` declarations and requires each name to be // accounted for. Text matching, as elsewhere in this file: a step whose // name is computed rather than a literal is invisible here, which is // stated rather than assumed. build.zig declares them all literally today. const marker = ".step(\""; var seen = [_]bool{false} ** declared_steps.len; var scan: usize = 0; while (std.mem.indexOfPos(u8, build_source, scan, marker)) |hit| { const start = hit + marker.len; const end = std.mem.indexOfScalarPos(u8, build_source, start, '"') orelse { // Unreachable while build.zig compiles, but failing loudly beats a // `break` that would silently truncate the scan. std.debug.print("\nci pin: build.zig has an unterminated .step(\"...) literal " ++ "at byte offset {d}; the scan cannot continue\n", .{start}); return error.CiPinMissing; }; scan = end + 1; const name = build_source[start..end]; for (declared_steps, &seen) |expected, *hit_before| { if (std.mem.eql(u8, expected, name)) { hit_before.* = true; break; } } else { std.debug.print("\nci pin: build.zig declares step \"{s}\", which src/ci_test.zig's " ++ "`declared_steps` does not list. Add it there (and decide whether CI runs it) " ++ "so the scan keeps describing the real step set.\n", .{name}); return error.CiPinMissing; } // The invocation is matched as a whole `run:` line, not as a substring: // `zig build e2e` contains `zig build e`, so an unanchored match would // let a step named "e" read as covered by the e2e leg. var invocation_buf: [128]u8 = undefined; const invocation = std.fmt.bufPrint(&invocation_buf, "run: zig build {s}\n", .{name}) catch { std.debug.print("\nci pin: build.zig declares a step name longer than {d} bytes " ++ "(\"{s}\"); widen invocation_buf\n", .{ invocation_buf.len, name }); return error.CiPinMissing; }; if (std.mem.indexOf(u8, ci_workflow, invocation) != null) continue; // Covered, but invoked in a loop rather than as a bare `run:` line. // The anchor is verified, not merely trusted: an entry naming a step // whose anchor has drifted out of the workflow is a step CI stopped // running, and it must fail like any other gap. var parameterized_covered = false; for (ci_parameterized) |parameterized| { if (!std.mem.eql(u8, parameterized.step, name)) continue; if (std.mem.indexOf(u8, ci_workflow, parameterized.anchor) == null) { std.debug.print("\nci pin: src/ci_test.zig records step \"{s}\" as run by CI " ++ "({s}), but .github/workflows/ci.yml no longer contains its invocation. " ++ "Either restore the sweep or move the step to `not_in_ci` with a reason.\n", .{ name, parameterized.how }); return error.CiPinMissing; } parameterized_covered = true; break; } if (parameterized_covered) continue; for (not_in_ci) |exception| { if (std.mem.eql(u8, exception.step, name)) break; } else { std.debug.print("\nci pin: build.zig declares step \"{s}\", which " ++ ".github/workflows/ci.yml never runs (`zig build {s}`) and src/ci_test.zig's " ++ "`not_in_ci` list does not excuse. Either add it to the workflow or " ++ "record why CI skips it.\n", .{ name, name }); return error.CiPinMissing; } } // The other half of the exact-set assertion, and the vacuity guard: a scan // that matched nothing would otherwise pass having checked nothing. for (declared_steps, seen) |expected, found| { if (found) continue; std.debug.print("\nci pin: src/ci_test.zig expects build.zig to declare step \"{s}\", " ++ "but the scan for `.step(\"...)` did not find it — the step was removed or " ++ "renamed, or the scan pattern has drifted from the source\n", .{expected}); return error.CiPinMissing; } } test "the reviewed not-in-CI exceptions are real build.zig steps that CI does not run" { // The other direction: an exception for a step that no longer exists is // dead permission. Left unchecked, `not_in_ci` accumulates entries that // quietly excuse nothing while looking like they excuse something. for (not_in_ci) |exception| { var declaration_buf: [128]u8 = undefined; const declaration = std.fmt.bufPrint(&declaration_buf, ".step(\"{s}\"", .{exception.step}) catch { std.debug.print("\nci pin: not_in_ci names a step longer than {d} bytes " ++ "(\"{s}\"); widen declaration_buf\n", .{ declaration_buf.len, exception.step }); return error.CiPinMissing; }; if (std.mem.indexOf(u8, build_source, declaration) == null) { std.debug.print("\nci pin: src/ci_test.zig excuses step \"{s}\" from CI (\"{s}\"), " ++ "but build.zig no longer declares it; drop the stale exception\n", .{ exception.step, exception.why }); return error.CiPinMissing; } // And the negative space itself, which is this module's declared // purpose and was the one direction nothing asserted: an excused step // must actually be absent from the workflow. Without this, adding // `zig build bench` to CI would silently falsify that entry's stated // reason — "a shared runner's timings are noise, and rule 7 forbids // claims not backed by a real measurement" — with no test failing and // the excuse left in the file reading as though it still held. // // Matched as a whole `run:` line for the same reason the forward // direction is: `zig build e2e` contains `zig build e`. var invocation_buf: [128]u8 = undefined; const invocation = std.fmt.bufPrint(&invocation_buf, "run: zig build {s}\n", .{exception.step}) catch { std.debug.print("\nci pin: not_in_ci names a step longer than {d} bytes " ++ "(\"{s}\"); widen invocation_buf\n", .{ invocation_buf.len, exception.step }); return error.CiPinMissing; }; if (std.mem.indexOf(u8, ci_workflow, invocation) == null) continue; std.debug.print("\nci pin: .github/workflows/ci.yml runs \"{s}\", but src/ci_test.zig's " ++ "`not_in_ci` excuses it from CI (\"{s}\"). Either drop the exception or drop the " ++ "step from the workflow — the recorded reason no longer describes what CI does.\n", .{ exception.step, exception.why }); return error.CiPinMissing; } } test "the shipped release mode is stated, not left to whoever packages" { // THIS PIN USED TO REQUIRE ReleaseSafe, on the grounds that src/'s thousand- // odd `std.debug.assert` calls are the mechanism by which a detected // inconsistency becomes a crash instead of a durable write, and that // ReleaseFast compiles every one to nothing. The project has since decided // to ship ReleaseFast anyway: `zit pack` runs ~25x faster there, and the // write path's answer to the lost assertions is `release_assert_test.zig`, // which forbids an assert from standing in for validation of untrusted // input in the first place. That guard does not depend on the build mode, // which is exactly why it can carry this. // // What survives the reversal is the reason this was ever a pin rather than // a comment: `standardOptimizeOption(.{})` with NO preference leaves the // choice to whoever packages, and that failure is silent in both directions // — nothing warns at build time, and nothing differs at run time until the // behaviour someone assumed turns out not to be the behaviour they got. The // hazard is an UNSTATED mode, not a particular one. So this now pins that a // preference is stated at all, and that it is the one the project ships. // // It pins the DEFAULT, not the ceiling: `--release=safe` still selects the // checked build for anyone who wants it. const preference = ".preferred_optimize_mode = .ReleaseFast"; if (std.mem.indexOf(u8, build_source, preference) == null) { std.debug.print("\nci pin: build.zig no longer states `{s}` in its " ++ "standardOptimizeOption call. Without a stated preference the release " ++ "mode is whatever the packager's flags happen to select, and nothing " ++ "reports the difference until behaviour diverges from what was assumed. " ++ "Restore it, or if the shipped mode has genuinely changed, update this " ++ "pin in the same commit so the decision is visible in history.\n", .{preference}); return error.CiPinMissing; } // Negative space: the preference is worthless if the call it sits in is not // the one that produces the top-level `optimize`. Pin the pairing, not just // the substring — a stray mention in a comment must not satisfy this. const call = "standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseFast })"; if (std.mem.indexOf(u8, build_source, call) == null) { std.debug.print("\nci pin: build.zig mentions the ReleaseFast preference but not as " ++ "`{s}`. The preference only binds when it is passed to the call that yields the " ++ "top-level optimize mode.\n", .{call}); return error.CiPinMissing; } }