Air Build Rules And CLI Flag Notes


Air is a Go live reload tool that tends to sit in the boring part of a developer workflow: watch files, rebuild, restart, repeat. The recent activity is worth reading because it changes how Air handles command line lists, build bypass rules, and a couple of small footguns that matter in script driven service and pipeline work.

The most interesting change is the addition of build.rules. The stated behavior is narrow: run commands on change without rebuilding. That sounds small, but it is exactly the kind of control that matters when a Go service is only one process inside a larger local workflow.

For data teams, the useful case is adjacent work. A schema file changes and you want to regenerate types. A SQL query changes and you want to run a linter. A fixture changes and you want to refresh generated test data. None of that should always mean rebuilding the application binary. Splitting those commands from the normal build loop reduces noise and makes watch based automation less blunt.

The files touched for this feature also show where the project drew the boundary. runner/config.go and air_example.toml pick up the config shape. runner/rule.go and runner/rule_test.go carry the rule logic. runner/engine.go is involved because rules have to sit in the event loop, not as a separate afterthought.

The tradeoff is predictable. More routing inside a watch loop means more places where a changed file may match the wrong action. Operators should keep rules boring at first. Match specific paths. Keep commands fast. Treat this as a way to make the rebuild path smaller, not as a general task runner.

Air also picked up repeatable slice flags on the command line, followed by documentation for repeatable list arguments. This is mostly ergonomics, but it removes a common script wart.

Before a change like this, tools often force callers to pack list values into one argument. That works for humans typing one command. It is worse for generated commands, CI wrappers, container entrypoints, and shell scripts that compose lists from separate variables. Repeatable list flags let callers append values in the same shape they are produced.

The top changed files back that up. runner/flag.go and runner/flag_test.go changed around flag parsing. README.md changed twice in the window, which matters because command line behavior is only useful when the documented form matches the parser.

A script can now be written in a more composable shape:

air --build.exclude_dir tmp --build.exclude_dir vendor

That is easier to generate than a single packed argument, and it is easier to inspect in CI logs. The behavior also lines up with how many platform tools expose repeated selectors, paths, and include lists.

The watcher path got a fix for a case where a transient empty read could defeat exclude_unchanged. That landed in the empty read fix, with runner/util.go touched three times in the activity window and runner/util_test.go touched once.

This is the kind of issue that looks minor until it sits under a noisy workspace. Editors, code generators, sync tools, and test runners can all create short lived file states. If the watcher sees an empty read at the wrong moment, it can make a decision from a false view of the file. For exclude_unchanged, that means the tool may treat a file as meaningfully changed when it is not, or lose the benefit of avoiding unchanged work.

The practical effect is fewer spurious rebuild decisions in environments where files are written in stages. That matters for Go services with generated code, but it also matters for data engineering repos where local runs may update fixtures, manifests, temporary output, or generated clients during one edit cycle.

There is still a limit here. A watcher cannot make every editor or network filesystem atomic. This fix should be read as hardening around a known read edge case, not a promise that every file event stream becomes clean.

One bug fix is aimed at users who run Air without a config file. The build.bin fix makes Air honor --build.bin when no config file exists.

That behavior matters because configless startup is often how Air enters a repo. Someone tries it in a service directory, or a CI job wraps it for a short lived check, or a container image uses command line flags instead of mounting a TOML file. In those cases, --build.bin is not a cosmetic option. It controls which binary path the runner should execute.

The fix is also a reminder that command line precedence only helps if the empty config path follows the same merge rules as the configured path. When a tool has both defaults and file based config, missing files should not silently move flags into a second class path.

For teams standardizing local developer containers, this is the change to care about. It reduces the need to commit a minimal Air config just to keep the binary name stable.

Watch how build.rules evolves after real users put it under busy watch trees. The useful version is predictable path matching and clear ordering with the normal build command.

Check wrappers that pass list values to Air. If they still pack values manually, repeated flags may simplify those scripts and make log output clearer.

For source builds, verify Go 1.26.0 availability in local and CI images before taking the latest master branch. The Hugo update to v0.164.0 also changes the documentation build graph and several indirect packages, so dependency scanners should be expected to report a different graph.