There’s a moment every DevOps engineer hits: a playbook works perfectly on your machine, gets merged, and then quietly breaks something in staging. Not because the logic was wrong, but because a small best-practice rule was ignored.
This is exactly where ansible-lint with GitHub Actions earns its place. Instead of relying on manual checks or tribal knowledge, you can enforce consistent standards automatically on every pull request.
Why lint Ansible in CI instead of locally?
Local linting is useful, but it’s unreliable as a gatekeeper. Not everyone runs it consistently, and different environments may produce slightly different results.
Running Ansible lint in GitHub Actions ensures:
- Every pull request is validated the same way
- Best practices are enforced automatically
- Broken patterns never reach production branches
- New contributors don’t need deep Ansible expertise to follow standards
Think of it less as a tool and more as a safety net that scales with your team.
What ansible-lint actually checks
If you haven’t used it before, ansible-lint analyzes your playbooks for:
- Deprecated modules or syntax
- Improper task formatting
- Security risks (like using shell unnecessarily)
- Idempotency issues
- Role structure problems
A common mistake developers make is assuming linting is only about formatting. In reality, it often catches logic flaws that would otherwise show up during deployment.
A minimal GitHub Actions workflow
Let’s start with a working example. This workflow runs ansible-lint on every push and pull request.
1name: Ansible Lint
2
3on:
4 push:
5 branches: [ "main" ]
6 pull_request:
7 branches: [ "main" ]
8
9jobs:
10 lint:
11 runs-on: ubuntu-latest
12
13 steps:
14 - name: Checkout repository
15 uses: actions/checkout@v4
16
17 - name: Set up Python
18 uses: actions/setup-python@v5
19 with:
20 python-version: "3.11"
21
22 - name: Install dependencies
23 run: |
24 pip install ansible ansible-lint
25
26 - name: Run ansible-lint
27 run: ansible-lint .
28That’s enough to get started. But real-world usage usually needs a bit more structure.
Making linting more realistic for real projects
Here’s where things get interesting. Most repositories aren’t just a flat directory of playbooks. You’ll often have roles, collections, and environment-specific configs.
You can fine-tune linting behavior using a configuration file.
.ansible-lint configuration
1skip_list:
2 - experimental
3 - fqcn-builtins
4
5warn_list:
6 - yaml
7
8verbosity: 1
9This lets you:
- Ignore rules that don’t fit your project
- Treat certain violations as warnings instead of failures
- Gradually introduce stricter linting
Teams often start lenient and tighten rules over time as code quality improves.
Lint only what changed (faster CI)
If your repository grows, running lint on everything can slow down pipelines. A smarter approach is to lint only changed files.
Here’s a simple tweak:
1- name: Get changed files
2 id: changed-files
3 uses: tj-actions/changed-files@v44
4
5- name: Run ansible-lint on changed files
6 run: |
7 ansible-lint ${{ steps.changed-files.outputs.all_changed_files }}
8This reduces runtime significantly, especially in large infrastructure repositories.
Failing builds vs guiding developers
Not every team wants lint failures to block merges immediately. There are two common approaches:
Strict enforcement
- Fail the pipeline on any lint error
- Best for mature teams and production-critical repos
Advisory mode
- Allow merges but show warnings
- Useful during adoption phase
You can simulate advisory mode by appending || true to the lint command, though this should be temporary.
Using pre-built GitHub Actions (optional shortcut)
If you don’t want to manage dependencies manually, you can use a prebuilt action:
1- name: Run Ansible Lint
2 uses: ansible/ansible-lint-action@v6
3This simplifies setup but gives you less control over versions and environment setup.
Common pitfalls to avoid
- Ignoring version pinning: Always pin ansible-lint and Ansible versions to avoid unexpected rule changes
- Linting without inventory context: Some checks behave differently without proper inventory
- Over-disabling rules: Skipping too many rules defeats the purpose
- Slow pipelines: Running lint across unnecessary directories can waste CI time
Where this fits in a CI/CD pipeline
Linting is just one layer. A typical pipeline for Ansible might look like:
- Lint playbooks (GitHub Actions)
- Syntax check (
ansible-playbook --syntax-check) - Dry run (
--check) - Integration tests (e.g., Molecule)
- Deployment
By placing GitHub Actions Ansible linting at the very beginning, you fail fast and save compute time downstream.
A small tweak that pays off long-term
Automating Ansible linting isn’t complicated, but it has an outsized impact. It standardizes how infrastructure code is written, reduces review overhead, and catches subtle issues early.
If your team is already using GitHub, adding this check is one of the lowest-effort, highest-return improvements you can make to your CI pipeline.