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 || echo swallows 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: astl
pre-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.
Summary
The
ansible-lintpre-commit hook takes about 13 seconds on every commit thattouches a YAML file. I maintain ansible-static-lint
(
astl), a Go reimplementation of ansible-lint's static rules that aims forbyte-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-lintresolves underansible==12.3.0),full-tree invocation (240 files):
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 runadds its own environment resolution on top.Findings parity
On the current tree, with
.ansible-lintand.yamllinthonoured(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-lintconfig 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:
uv run --with ansible-lint ansible-lint .. With anexplicit
.argument, ansible-lint only examines what its kind globs matchat 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
.makesthe CI gate actually lint the tree.
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
pre-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-lintand.yamllintfiles, including exclude_paths, skip_list, enable_list, profileand warn_list, so no configuration change is needed.
Happy to open a PR if there is interest.