Build-warnings gate (HISS-10)
HISS-10 makes every compiler warning fatal. A CI lane that compiles with its toolchain's
defaults prints its warnings and still passes: one CI run of an adopter repository printed
compiler warnings in 39 of its 103 jobs, about 71,600 in each MSVC job, and every job stayed
green. The build-warnings gate fails such a lane. It reads each workflow under
.github/workflows, finds every command that compiles C, C++, Rust or Go code, and requires the
warnings-as-errors form that toolchain documents.
The gate is adopt.AuditBuildWarnings in
internal/adopt/build_warnings_audit.go,
over the measurement forge.MeasureBuildWarnings in
internal/forge/build_warnings.go.
praetorctl audit and the MCP standards_audit both run it.
Quick start
Give every build lane its toolchain's form:
| Toolchain | Lane the gate reads | Passes with |
|---|---|---|
gcc, clang, icx, icpx (any target prefix, version suffix, or the -posix/-win32 suffix of Debian's MinGW-w64 names) |
a command that compiles a source file or runs with -c |
-Werror on the command, not undone by a later -Wno-error. -Werror=<warning> covers that warning only and does not count (GCC). |
MSVC cl, clang-cl |
the same | /WX (or -WX) on the command or in the CL or _CL_ variable, not undone by a later /WX- (MSVC); clang-cl also takes -Werror, undone by -Wno-error |
| CMake | a configure run (cmake -S . -B build, cmake --preset) |
-DCMAKE_COMPILE_WARNING_AS_ERROR=ON (any CMake true constant, CMake 3.24 or later) without --compile-no-warning-as-error (CMake), or -Werror (or /WX) in the flags of every language the repository's sources need: CMAKE_C_FLAGS for .c files, CMAKE_CXX_FLAGS for .C, .cc, .cpp, .cxx and .c++ files, and both when it holds neither, since project() enables C and C++ by default. Each is read from -D on the command, else from CFLAGS or CXXFLAGS, which initialize it (CMAKE_<LANG>_FLAGS) |
| Meson | meson setup |
--werror or -Dwerror=true, in any case as Meson reads a boolean (Meson) |
Cargo, cross, cargo llvm-cov |
build, check, test, run, bench, clippy, rustc, nextest, llvm-cov (not its report, show-env or clean) |
CARGO_BUILD_WARNINGS: deny (Cargo 1.97 or later) without a --cap-lints allow in those flags; or -D warnings in the flags rustc receives for every target: the first set of CARGO_ENCODED_RUSTFLAGS, RUSTFLAGS, CARGO_TARGET_<TRIPLE>_RUSTFLAGS and CARGO_BUILD_RUSTFLAGS, after the [lints] levels below, and for cargo clippy and cargo rustc what follows -- (Cargo, Clippy) |
Cargo [lints] |
every crate of the root Cargo.toml's workspace |
warnings = "deny" (or "forbid", or { level = "deny", priority = -1 }) in each crate's [lints.rust], or in [workspace.lints.rust] with [lints] workspace = true in each member (lints, workspace lints). Read only for a cargo command run in the repository root without --manifest-path |
| rustc | a command that compiles a .rs file |
-D warnings on the command |
| Go | go build, test and install of the repository |
a step of any workflow that runs go vet or go test -vet=all: the Go compiler reports no warnings, and go vet exits non-zero on a finding (cmd/vet). In a module with cgo files (import "C"), also -Werror in CGO_CFLAGS (or CGO_CPPFLAGS), and in CGO_CXXFLAGS when a cgo package holds C++ files, on the build step; CGO_ENABLED=0 compiles no C |
-D warnings may also be written -Dwarnings, --deny warnings, --deny=warnings or with
-F/--forbid; a later -W warnings or -A warnings undoes a deny, never a forbid. Cargo
passes the [lints] levels before the flags of its flag source, so RUSTFLAGS: -W warnings
undoes a [lints] deny. A --cap-lints allow or --cap-lints warn turns every deny and forbid
back into a warning; rustc reads the first --cap-lints it is given (measured with rustc 1.98.1).
CARGO_BUILD_WARNINGS: deny fails the build on any warning rustc reports, so --cap-lints warn
keeps it fatal while --cap-lints allow, which hides every warning, does not (measured with
cargo 1.98.1); flags the workflow does not show do not undo it. A build for the runner's host
takes CARGO_TARGET_<TRIPLE>_RUSTFLAGS only when the host is that triple, which the workflow does
not show: with no CARGO_ENCODED_RUSTFLAGS or RUSTFLAGS set, any such variable leaves the flags
undecided, so the -D warnings form fails even when CARGO_BUILD_RUSTFLAGS also denies warnings.
Name the target with --target or CARGO_BUILD_TARGET, or set RUSTFLAGS.
env:
RUSTFLAGS: -D warnings
jobs:
native:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: cmake -S . -B build -DCMAKE_COMPILE_WARNING_AS_ERROR=ON
- run: cmake --build build
- run: cargo test --locked
- run: go vet ./...
Praetor's scaffolded CI workflows pass the gate: the Rust workflow
(templates/rust/ci-rust.yml.tmpl) sets RUSTFLAGS: -D warnings on its job and the Go workflow
runs go vet ./... (TestAuditBuildWarningsPassesTheScaffoldedWorkflows).
What the gate reads
- Commands of
run:steps. A script is cut into commands at;,&&,||,|,&and line ends, and the parentheses of a subshell ((cd build && cmake ..)) are dropped. Quotes are read with their nesting (hiss.QuoteStepper), so a command substitution inside a double-quoted word may hold quotes of its own (TOKEN="$(gh api 'x' --jq '.y')"); a quote that is never closed still fails the measurement. Words before the program that run nothing of their own (if,then,do,time,nice,timeout,env,sudo,exec,ccache,sccache) are passed over with their options. Anecho cargo buildor a command in a comment builds nothing. A program path is cut at/or\on every host, soC:\msys64\mingw64\bin\gcc.exereads asgccon Linux too. A path whose directory alone holds a variable, an expression or a command substitution ($HOME/.cargo/bin/cargo,${{ github.workspace }}/bin/meson,$(go env GOROOT)/bin/go) is read by its base name the same way. - Variables. A variable is read from the command's own assignments
(
RUSTFLAGS="-D warnings" cargo build), anexportearlier in the script, then the step's, the job's and the workflow'senv:, in that order. A word that is one whole reference ($CMAKE_ARGS,${CMAKE_ARGS}) expands to its value. Anenv:that is one expression, such as${{ fromJSON(vars.CI_ENV) }}, decides nothing the file shows, so the lane fails. - Wrappers and the environment.
sudoruns its program in a reset environment (env_reset, on by default in sudoers), soRUSTFLAGSfromenv:reachessudo cargo buildonly withsudo -Eor--preserve-env=RUSTFLAGS;env -iempties the environment andenv -u NAMEremoves one variable. - Compilers given as variables.
$CC -c a.creadsCCfrom the environment and judges the compiler it names. A program the workflow shows no value for, such as${{ matrix.cc }}, an unset$CCor$(which gcc), is a lane that fails as unread when its arguments name a C or C++ source file: name the compiler on the command or set the variable inenv:. A bare-cor/cnames no source, so"$PYTHON" -c 'print(1)'or$SHELL -c 'echo hi'is no lane. - Configure before build.
cmake --buildandmeson compile,testorinstallare lanes only in a job where no earlierrun:step configures: what configured that tree cannot be read, so the lane fails and says so. - Go. Every Go lane of every workflow needs one binding step that runs
go vetorgo test -vet=all.go installof amodule@versioninstalls a tool, not the repository, andgo runcompiles a program only to execute it, such as the API gatetools/apicompat/gate/main.goadoption writes; neither is a lane. - Repository sources. For CMake's flags form and Go's cgo rule the gate reads which files the
repository holds: the files git reports as its own (tracked, or untracked and not ignored),
every file outside a work tree. A Go file counts when its path is one the go command builds:
not a test file, not under
testdata, and under no directory starting with.or_. Build constraints are not read, so a//go:build ignorefile that imports"C"still makes the module a cgo module. - Binding. A step or job with
continue-on-errorpasses whatever its compiler reports, so its lane fails, Go lanes included, and ago vetstep with it does not count. A job or step whoseif:is the literalfalseis not read.
What the gate does not read
The gate reads the flags a run: step passes. It does not run a build, and it does not read:
- what a
maketarget, a script (./build.sh,bash -c), an action (uses:) or a reusable workflow of another repository compiles; - a preset's
cacheVariables(CMakePresets.json), a Meson machine file ordefault_options,CMakeLists.txt,.cargo/config.toml(build.rustflags,target.<cfg>.rustflags,build.warnings),cargo --config, orgo env -wsettings; - the flags of languages other than C and C++ in CMake's flags form (
CMAKE_CUDA_FLAGSand the like):CMAKE_COMPILE_WARNING_AS_ERRORcovers every language; #cgo CFLAGSdirectives in Go files, which come afterCGO_CFLAGSand can undo it, and Fortran files in cgo packages;- build scripts and proc macros under
--target: Cargo then passesRUSTFLAGSand its other flag sources to the target's code only, so[lints]orCARGO_BUILD_WARNINGScovers them; - an assignment in PowerShell or cmd syntax, or a variable a step writes to
$GITHUB_ENV; - whether the script lets the failure through (
|| true,set +e); - which warnings the lane enables (
-Wall,-Wextra,warning_level):-Werrormakes the enabled ones fatal; - other compilers, such as
nvcc, and which analyzers a golangci-lint configuration runs.
A lane whose form lives in one of these places fails until the form is on the step, or until an
exception declares it. A lane the gate never sees passes unread:
gap/build-failure-caught.yml
and
gap/make-hides-the-compiler.yml
record two such shapes in the HISS-20 coverage table, .config/hiss/coverage.yaml.
A lane that cannot build with warnings as errors at all, such as one compiling a vendored tree whose warnings are not yours to fix, needs a gate that reads the warnings a build prints instead of its flags. That log-scan gate is tracked in #825; until it lands, declare such a lane in the exceptions list below.
Exceptions
A lane that cannot build with warnings as errors yet, such as one compiling a vendored tree,
takes an entry in the top-level exceptions list of .standards.yaml:
exceptions:
- rule: "HISS-10"
path: ".github/workflows/ci.yml"
reason: "vendored codec still warns under MSVC; upstream fix pending"
expires: "2026-12-31"
path names one workflow file directly in .github/workflows; the entry excuses every failing
lane of that workflow. The audit prints each excused lane under the entry's reason and expiry and
passes. An expired entry fails like a missing one. An entry whose workflow has no failing lane,
or that is left when no workflow builds anything, is stale and fails until it is removed. The
list's general rules, one line of reason and an expiry within the category interval (A: stated, at most 90 days; B: 3 days, C: 10 days, D: 120 days; uncategorized: 90 days), are in
clang-tidy coverage: Exceptions.
Reading the output
A failure names each lane, what it lacks and the form to add, then the way out:
[FAIL] Build warnings (HISS-10): .github/workflows/ci.yml:14: job native, step "cmake -S . -B build": CMake builds without warnings as errors: the configure command defines no CMAKE_COMPILE_WARNING_AS_ERROR. Add: configure with -DCMAKE_COMPILE_WARNING_AS_ERROR=ON (CMake 3.24 or later) and without --compile-no-warning-as-error.
[FAIL] Build warnings (HISS-10): no live exceptions entry declares the lanes above; build each with its toolchain's warnings-as-errors form (docs/guides/build-warnings.md), or, for a lane that cannot yet, declare its workflow in the exceptions list of .standards.yaml (rule HISS-10, path .github/workflows/<workflow>, a reason, and an expiry at most 90 days ahead).
A pass counts the lanes by toolchain, lists the excused ones, and states what was read:
[PASS] Build warnings (HISS-10): 4 build lanes fail on a warning (CMake 1, Cargo 1, Go 2).
Read from the run: steps of the workflow files under .github/workflows; what a make target, a script, an action, a preset or a build file sets was not read, and no build was run.
A repository whose workflows build none of these languages skips the gate:
[SKIP] Build warnings (HISS-10) not checked: no run: step under .github/workflows builds C, C++, Rust or Go code, so no lane needs warnings as errors.
Tests
internal/forge/build_warnings_test.go: each toolchain's form, each lane without it, the Go vet rule, and the commands that are no lane.internal/forge/build_warnings_sources_test.go: the cgo rule, CMake's flags per language, and the Cargo[lints]level;internal/forge/build_warnings_shell_test.go: subshells, wrappers, compilers given as variables and host-neutral program names.internal/flavor/cargo_lints_test.go: the[lints]level of a root package and of a workspace's members.internal/adopt/build_warnings_audit_test.go: the gate per language, exceptions, the skip, the scaffolded workflows, and the HISS-10 fixture replay.cmd/standardsctl/audit_build_warnings_test.goandTestServerAuditRunsTheBuildWarningsGateincmd/standards-mcp/audit_policy_test.go: the CLI and MCP audits run the gate.