Introduction

Automated testing is essential for maintaining quality in Ansible collections. GitHub Actions provides native CI/CD that runs lint checks, sanity tests, unit tests, and integration tests on every pull request — catching issues before they reach main. This article covers the complete workflow setup using the ansible-network/github_actions reusable workflows, custom test configurations, and best practices for collection CI/CD.

Quick Start: Minimal Workflow

Create .github/workflows/test.yml in your collection repository:

name: CI
concurrency:
  group: ${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

on:
  pull_request:
    branches: [main]
  workflow_dispatch:
  schedule:
    - cron: '0 6 * * 1'  # Weekly Monday 6 AM

jobs:
  ansible-lint:
    uses: ansible-network/github_actions/.github/workflows/ansible-lint.yml@main
  changelog:
    uses: ansible-network/github_actions/.github/workflows/changelog.yml@main
  sanity:
    uses: ansible-network/github_actions/.github/workflows/sanity.yml@main
  unit-galaxy:
    uses: ansible-network/github_actions/.github/workflows/unit_galaxy.yml@main
  integration:
    uses: ansible-network/github_actions/.github/workflows/integration_simple.yml@main

  all_green:
    if: ${{ always() && (github.event_name != 'schedule') }}
    needs:
      - changelog
      - sanity
      - unit-galaxy
      - integration
    runs-on: ubuntu-latest
    steps:
      - run: >-
          python -c "assert set([
          '${{ needs.changelog.result }}',
          '${{ needs.sanity.result }}',
          '${{ needs.unit-galaxy.result }}',
          '${{ needs.integration.result }}'
          ]) == {'success'}"

Understanding Each Job

ansible-lint

Runs ansible-lint against the collection to catch:

  • YAML syntax issues
  • Deprecated module usage
  • Missing FQCN (Fully Qualified Collection Names)
  • Task naming violations
  • Jinja2 best practices

changelog

Validates changelog fragments exist for PRs. Ansible collections use changelogs/fragments/ with YAML files:

# changelogs/fragments/fix-timeout-handling.yml
bugfixes:
  - Fix timeout handling in my_module (https://github.com/org/repo/pull/42).

sanity

Runs ansible-test sanity which checks:

  • Python import validation
  • Documentation formatting
  • GPL license headers
  • PEP 8 compliance
  • YAML syntax
  • Proper module documentation
  • Return value documentation

unit-galaxy

Runs unit tests from tests/unit/ using ansible-test units:

# tests/unit/plugins/modules/test_my_module.py
from unittest.mock import patch
from ansible_collections.my_ns.my_col.plugins.modules import my_module

class TestMyModule:
    def test_create_resource(self):
        set_module_args({"name": "test", "state": "present"})
        with patch.object(my_module, "create_resource") as mock_create:
            mock_create.return_value = {"id": "123"}
            result = my_module.main()
            assert result["changed"] is True

integration

Runs integration tests from tests/integration/targets/:

tests/integration/targets/
├── my_module/
│   ├── tasks/
│   │   └── main.yml
│   └── defaults/
│       └── main.yml
# tests/integration/targets/my_module/tasks/main.yml
---
- name: Create resource
  my_ns.my_col.my_module:
    name: test_resource
    state: present
  register: result

- name: Verify creation
  ansible.builtin.assert:
    that:
      - result.changed
      - result.resource.name == 'test_resource'

all_green

A gate job that ensures all other jobs passed before the PR can be merged. Uses Python assertion to check all results equal 'success'.

Workflow Configuration Deep Dive

Concurrency Control

concurrency:
  group: ${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

This ensures:

  • Only one workflow runs per PR branch at a time
  • New pushes cancel the previous run (saves runner minutes)
  • Manual/scheduled runs each get their own group

Trigger Events

on:
  pull_request:
    branches: [main]        # Test every PR to main
  workflow_dispatch:          # Manual trigger from GitHub UI
  schedule:
    - cron: '0 6 * * 1'     # Weekly: catch upstream breakage
  push:
    branches: [main]         # Optional: test after merge

Path Filtering (Save Runner Time)

on:
  pull_request:
    branches: [main]
    paths:
      - 'plugins/**'
      - 'tests/**'
      - 'meta/**'
      - '.github/workflows/**'
    paths-ignore:
      - '**.md'
      - 'docs/**'
      - 'changelogs/**'

Custom Workflows

Run Against Multiple Ansible Versions

jobs:
  sanity:
    strategy:
      matrix:
        ansible-version:
          - stable-2.16
          - stable-2.17
          - devel
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install ansible-core
        run: pip install https://github.com/ansible/ansible/archive/${{ matrix.ansible-version }}.tar.gz

      - name: Run sanity tests
        run: ansible-test sanity --color -v --docker

Run ansible-lint Locally First

  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run ansible-lint
        uses: ansible/ansible-lint@main
        with:
          args: ""

Integration Tests with Docker Services

  integration:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: test
          POSTGRES_DB: testdb
        ports:
          - 5432:5432
    steps:
      - uses: actions/checkout@v4

      - name: Install dependencies
        run: |
          pip install ansible-core psycopg2-binary
          ansible-galaxy collection install community.postgresql

      - name: Run integration tests
        run: ansible-test integration --color -v my_module
        env:
          POSTGRESQL_HOST: localhost
          POSTGRESQL_PASSWORD: test

Collection Directory Structure

my_namespace/my_collection/
├── .github/
│   └── workflows/
│       └── test.yml
├── changelogs/
│   ├── changelog.yaml
│   └── fragments/
├── docs/
├── galaxy.yml
├── meta/
│   └── runtime.yml
├── plugins/
│   ├── modules/
│   ├── module_utils/
│   ├── inventory/
│   └── filter/
├── roles/
├── tests/
│   ├── integration/
│   │   └── targets/
│   ├── unit/
│   │   └── plugins/
│   └── sanity/
└── README.md

Required Files for CI

galaxy.yml

namespace: my_namespace
name: my_collection
version: 1.0.0
readme: README.md
authors:
  - Your Name <email@example.com>
description: My Ansible collection
license_file: LICENSE
tags:
  - networking
  - cloud
dependencies:
  ansible.netcommon: ">=5.0.0"

meta/runtime.yml

requires_ansible: ">=2.15.0"
plugin_routing:
  modules:
    old_module:
      redirect: my_namespace.my_collection.new_module

.ansible-lint

profile: production
skip_list:
  - yaml[line-length]

Branch Protection Rules

Configure GitHub branch protection to require CI:

  1. Settings → Branches → Add Rule
  2. Branch pattern: main
  3. Enable Require status checks to pass
  4. Select required checks: all_green
  5. Enable Require branches to be up to date

Troubleshooting

Sanity Test Failures

# Run locally to debug
ansible-test sanity --color -v --docker
ansible-test sanity --color -v --test pep8
ansible-test sanity --color -v --test import

Integration Test Timeouts

# Increase timeout for slow tests
  integration:
    uses: ansible-network/github_actions/.github/workflows/integration_simple.yml@main
    with:
      timeout-minutes: 60

Changelog Missing

# Create a fragment before submitting PR
cat > changelogs/fragments/my-fix.yml << 'EOF'
bugfixes:
  - Fix connection timeout in my_module.
EOF

Best Practices

  1. Run tests locally first — ansible-test sanity --docker before pushing
  2. Use matrix testing — test against multiple ansible-core versions
  3. Pin reusable workflow versions — use @v1.0.0 tags not @main
  4. Add path filters — skip CI for docs-only changes
  5. Keep integration tests fast — use mocks where possible
  6. Require all_green — enforce via branch protection rules
  7. Add changelog fragments — every PR should include one
  8. Schedule weekly runs — catch upstream ansible-core breakage early

Conclusion

GitHub Actions provides everything you need for Ansible collection CI/CD. Use the ansible-network/github_actions reusable workflows for quick setup, or build custom workflows with matrix testing across multiple ansible-core versions. The all_green gate pattern ensures no PR merges without passing lint, sanity, unit, and integration tests. Combine with branch protection rules for a reliable, automated quality gate.