Introduction
Ansible-lint rule 401 (latest[git]) flags Git module tasks that use unpinned versions like HEAD or branch names. These create non-reproducible deployments — the same playbook run on different days may check out completely different code.
This guide explains why the rule exists, shows correct patterns, and covers legitimate cases where you need to suppress it.
The Error
$ ansible-lint playbook.yml
WARNING Listing 1 violation(s) that are fatal
latest[git]: Result of the command may vary on subsequent runs.
playbook.yml:5 Task/Handler: Clone application repo
Rule Violation Summary
count tag profile rule associated tags
1 latest[git] safety idempotency
Failed: 1 failure(s), 0 warning(s) on 1 files.
Root Cause
The rule triggers when the ansible.builtin.git module uses values that resolve to different commits over time:
| Problematic Value | Why It's Risky |
|---|---|
version: HEAD | Always latest commit on default branch |
version: main | Branch moves with every merge |
version: develop | Branch changes constantly |
| (no version specified) | Defaults to HEAD |
Problematic Code Examples
---
- name: Deploy application
hosts: webservers
tasks:
# BAD: HEAD changes constantly
- name: Clone repo (unpinned)
ansible.builtin.git:
repo: "https://github.com/org/app.git"
dest: /opt/app
version: HEAD
# BAD: branch name moves with every commit
- name: Clone repo (branch)
ansible.builtin.git:
repo: "https://github.com/org/app.git"
dest: /opt/app
version: main
# BAD: no version at all (defaults to HEAD)
- name: Clone repo (implicit HEAD)
ansible.builtin.git:
repo: "https://github.com/org/app.git"
dest: /opt/app
Correct Code
Pin to a Specific Commit Hash (Most Secure)
---
- name: Deploy application (pinned)
hosts: webservers
tasks:
- name: Clone repo at specific commit
ansible.builtin.git:
repo: "https://github.com/org/app.git"
dest: /opt/app
version: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"
Pin to a Release Tag
---
- name: Deploy tagged release
hosts: webservers
tasks:
- name: Clone repo at release tag
ansible.builtin.git:
repo: "https://github.com/org/app.git"
dest: /opt/app
version: "v2.1.0"
Use a Variable for the Version
---
- name: Deploy with configurable version
hosts: webservers
vars:
app_version: "v2.1.0" # Easy to update in one place
tasks:
- name: Clone app at specified version
ansible.builtin.git:
repo: "https://github.com/org/app.git"
dest: /opt/app
version: "{{ app_version }}"
Suppressing the Rule (When Intentional)
Sometimes you genuinely want the latest code — for example, in development environments or CI pipelines:
Inline Suppression
- name: Clone latest for development
ansible.builtin.git:
repo: "https://github.com/org/app.git"
dest: /opt/app
version: main
# noqa: latest[git]
In .ansible-lint Configuration
---
# .ansible-lint
skip_list:
- latest[git] # We handle pinning via CI variables
Per-Environment Approach
---
- name: Deploy application
hosts: webservers
vars:
# Production: pinned tag; Dev: branch
app_version: "{{ 'main' if env == 'dev' else 'v2.1.0' }}"
tasks:
- name: Clone application
ansible.builtin.git:
repo: "https://github.com/org/app.git"
dest: /opt/app
version: "{{ app_version }}"
Complete Deployment Playbook
---
- name: Reproducible application deployment
hosts: webservers
become: true
vars:
app_repo: "https://github.com/org/myapp.git"
app_version: "v3.2.1"
app_dest: /opt/myapp
app_user: appservice
tasks:
- name: Ensure git is installed
ansible.builtin.package:
name: git
state: present
- name: Clone application at pinned version
ansible.builtin.git:
repo: "{{ app_repo }}"
dest: "{{ app_dest }}"
version: "{{ app_version }}"
force: true
depth: 1 # Shallow clone for faster deploys
register: git_result
notify: Restart application
- name: Set ownership
ansible.builtin.file:
path: "{{ app_dest }}"
owner: "{{ app_user }}"
group: "{{ app_user }}"
recurse: true
when: git_result.changed
- name: Install dependencies
ansible.builtin.command:
cmd: pip install -r requirements.txt
chdir: "{{ app_dest }}"
when: git_result.changed
changed_when: true
- name: Display deployed version
ansible.builtin.debug:
msg: "Deployed {{ app_version }} (commit: {{ git_result.after }})"
handlers:
- name: Restart application
ansible.builtin.systemd:
name: myapp
state: restarted
Best Practices
- Always pin versions in production — use tags or commit hashes
- Use variables for versions — easier to update across playbooks
- Use
depth: 1— shallow clones are faster and use less disk - Track deployed versions — register the git result and log it
- Separate dev/prod config — allow
mainbranch in dev only
Related Rules
| Rule | Description |
|---|---|
latest[git] | Unpinned git checkout |
latest[hg] | Unpinned Mercurial checkout |
package-latest | Using state: latest in package modules |
Related Articles
- Ansible-lint Guide — Complete linting setup
- Ansible Error 205: playbook-extension — File extension lint rule
- Ansible delegate_to — Task delegation patterns
- Ansible Git Module — Full git module guide
Conclusion
Rule 401 (latest[git]) enforces reproducible deployments by requiring pinned Git versions. Always use commit hashes or release tags in production playbooks. Reserve branch names for development environments, and suppress the rule explicitly with # noqa: latest[git] when the behavior is intentional.