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 ValueWhy It's Risky
version: HEADAlways latest commit on default branch
version: mainBranch moves with every merge
version: developBranch 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

  1. Always pin versions in production — use tags or commit hashes
  2. Use variables for versions — easier to update across playbooks
  3. Use depth: 1 — shallow clones are faster and use less disk
  4. Track deployed versions — register the git result and log it
  5. Separate dev/prod config — allow main branch in dev only
RuleDescription
latest[git]Unpinned git checkout
latest[hg]Unpinned Mercurial checkout
package-latestUsing state: latest in package modules

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.