Proposal: use ansible-static-lint in the pre-commit hook (526x faster, identical findings)
Summary
The ansible-lint pre-commit hook takes about 13 seconds on every commit that
touches a YAML file. I maintain ansible-static-lint
(astl), a Go reimplementation of ansible-lint's static rules that aims for
byte-identical output, and I benchmarked both tools on this repository
(HEAD 20e22a8). I would like to propose using it in the pre-commit hook,
while keeping ansible-lint in CI for full coverage.
Measurements
Same tree, same machine, hyperfine, 5 runs, warmup 1. ansible-lint 25.8.2
(the version uv run --with ansible-lint resolves under ansible==12.3.0),
full-tree invocation (240 files):
| tool | mean time |
|---|---|
| ansible-lint (no-arg form, 240 files) | 13.128 s ± 0.043 |
| astl | 25.0 ms ± 0.3 |
That is a 526x speedup, and it turns the hook from a noticeable pause into
something below the perception threshold. The 13 s figure is a floor for the
real hook, since uv run adds its own environment resolution on top.
Findings parity
On the current tree, with .ansible-lint and .yamllint honoured
(exclude_paths, skip_list, enable_list, mock_modules), both tools report
zero findings on the full 240-file set. To verify that the double zero is
real agreement and not a discovery gap, I also ran both tools with the
.ansible-lint config removed: 270 findings match byte for byte
(name[missing], var-naming, name[casing], role-name, no-handler, key-order,
no-changed-when). The remaining differences are rules astl deliberately does
not port (fqcn, syntax-check, args, jinja, no-free-form) and a handful of
lines that differ between ansible-lint 25.8.2 and 26.8.0 themselves.
Scope caveat, stated plainly: astl covers ansible-lint's static rules only. It does not run syntax-check or the fqcn/args/jinja families. That is why the proposal is to swap the pre-commit hook, where latency matters on every commit, and keep ansible-lint in CI as the authoritative gate.
A note on the current lint setup
While measuring, I noticed the current gates may not be checking what they appear to:
- The CI step runs
uv run --with ansible-lint ansible-lint .. With an explicit.argument, ansible-lint only examines what its kind globs match at the top level: 6 files, of which 3 are the lint configs themselves. Your own green CI log shows it ("Passed: 0 failure(s), 0 warning(s) on 6 files"). The no-argument form discovers 240 files. Dropping the.makes the CI gate actually lint the tree. - The pre-commit entry is
bash -c 'uv run ansible-lint --force-color || echo "..."', so the|| echoswallows the exit code and the hook can never fail a commit.
The tree is clean either way (I verified the 240-file run is green), so nothing is currently slipping through, but both gates would report success even if it were not.
Proposed change
- repo: https://github.com/arhuman/ansible-static-lint
rev: v0.3.0
hooks:
- id: astlpre-commit installs astl in an isolated environment and caches it; with
pre-commit 3.0 or newer, neither astl nor Go needs to be installed
beforehand. The first run bootstraps Go and compiles once, subsequent runs
use the cached binary. astl reads the repository's .ansible-lint and
.yamllint files, including exclude_paths, skip_list, enable_list, profile
and warn_list, so no configuration change is needed.
Happy to open a PR if there is interest.
Source: trailofbits/algo