From cf3fcca7203062e0348ff6bf83f909288c4013c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20B=C3=A4rlocher?= Date: Wed, 17 Jun 2026 23:50:57 +0200 Subject: [PATCH 1/3] refactor(ci): merge KVM molecule runner into a driver input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ci-ansible-molecule.yml and ci-ansible-molecule-kvm.yml were ~85% identical — same scenario-discovery job, checkout, python setup, dependency install, and summary. Only the QEMU/KVM apt setup and the `sg kvm` wrapper differed. Add a `driver` input (`docker` default, `qemu`) to ci-ansible-molecule.yml: the QEMU/KVM setup step is gated on `driver == 'qemu'` and the run step wraps molecule in `sg kvm` only in that case. A validation step rejects any other value. Remove ci-ansible-molecule-kvm.yml. Net -226/+72; the discovery job no longer has to be kept in sync across two files. Callers of the -kvm reusable switch to ci-ansible-molecule.yml with `driver: qemu` (only open chore branches reference it, no main caller yet). Signed-off-by: Simon Bärlocher --- .github/workflows/ci-ansible-molecule-kvm.yml | 212 ------------------ .github/workflows/ci-ansible-molecule.yml | 72 +++++- AGENTS.md | 3 +- CHANGELOG.md | 20 +- 4 files changed, 71 insertions(+), 236 deletions(-) delete mode 100644 .github/workflows/ci-ansible-molecule-kvm.yml diff --git a/.github/workflows/ci-ansible-molecule-kvm.yml b/.github/workflows/ci-ansible-molecule-kvm.yml deleted file mode 100644 index d9d7e33..0000000 --- a/.github/workflows/ci-ansible-molecule-kvm.yml +++ /dev/null @@ -1,212 +0,0 @@ ---- -name: CI - Ansible Molecule (KVM) - -# Molecule test runner for Ansible roles that need a real kernel and a -# real init system — k3s (needs modprobe / cgroups), container engines -# (dockerd), and agents that must run as a stable systemd service. These -# do not work reliably under the docker driver, so this variant uses the -# molecule-qemu driver to boot full VMs with QEMU/KVM. -# -# KVM: GitHub-hosted ubuntu-latest runners expose a writable /dev/kvm, -# so molecule-qemu auto-detects hardware acceleration (it checks vmx/svm -# + /dev/kvm in its create playbook and falls back to TCG otherwise). -# The runner user is added to the kvm group and molecule is invoked via -# `sg kvm` because group changes do not affect the current shell. -# -# Scenario discovery and the per-scenario checkout/run flow mirror -# ci-ansible-molecule.yml; only the driver dependencies and the KVM -# setup differ. Each role's molecule.yml selects `driver: molecule-qemu` -# and defines its platforms (cloud-image URL, checksum, arch, ssh user). - -on: - workflow_call: - inputs: - collection_namespace: - type: string - required: false - default: 'arillso' - description: 'Collection namespace (the part before the dot in `.`).' - collection_name: - type: string - required: true - description: 'Collection name.' - python_version: - type: string - required: false - default: '3.12' - description: 'Python version to use for pip and the molecule run.' - scenarios: - type: string - required: false - default: '' - description: | - JSON array of scenario directory names under `scenarios_root`. - When empty, the workflow discovers scenarios automatically and - builds the matrix from the directory listing (entries starting - with `.` are skipped). - scenarios_root: - type: string - required: false - default: 'extensions/molecule' - description: | - Path (relative to the collection root) where scenarios live. - Override for repos that keep scenarios under `roles//molecule`. - runs_on: - type: string - required: false - default: 'ubuntu-latest' - description: | - GitHub-hosted runner label. The standard ubuntu-latest runner - exposes /dev/kvm, so a larger/paid runner is not required. - cancel-in-progress: - type: boolean - required: false - default: true - description: | - Whether to cancel in-progress runs in the same concurrency group. - concurrency-suffix: - type: string - required: false - default: '' - description: | - Optional suffix appended to the concurrency group as `-`. - Set to separate parallel invocations of this reusable from the - same caller (e.g. per-role). - -permissions: - contents: read - -concurrency: - group: >- - ${{ github.workflow }}-${{ github.ref }}${{ - inputs.concurrency-suffix != '' && format('-{0}', inputs.concurrency-suffix) || '' - }} - cancel-in-progress: ${{ inputs.cancel-in-progress }} - -jobs: - discover: - name: Discover scenarios - runs-on: ${{ inputs.runs_on }} - outputs: - scenarios: ${{ steps.list.outputs.scenarios }} - steps: - - name: Checkout collection - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - - - name: Build scenario matrix - id: list - env: - INPUT_SCENARIOS: ${{ inputs.scenarios }} - SCENARIOS_ROOT: ${{ inputs.scenarios_root }} - run: | - set -euo pipefail - - if [ -n "$INPUT_SCENARIOS" ]; then - echo "scenarios=$(echo "$INPUT_SCENARIOS" | jq -c '.')" >> "$GITHUB_OUTPUT" - echo "::notice::Using caller-supplied scenarios: $INPUT_SCENARIOS" - exit 0 - fi - - if [ ! -d "$SCENARIOS_ROOT" ]; then - echo "::error::Scenarios root '$SCENARIOS_ROOT' does not exist" - exit 1 - fi - - mapfile -t scenarios < <( - find "$SCENARIOS_ROOT" -mindepth 1 -maxdepth 1 -type d \ - -not -name '.*' -printf '%f\n' | sort - ) - - if [ ${#scenarios[@]} -eq 0 ]; then - echo "::error::No scenarios discovered under $SCENARIOS_ROOT" - exit 1 - fi - - MATRIX=$(printf '%s\n' "${scenarios[@]}" | jq -R . | jq -sc .) - echo "scenarios=$MATRIX" >> "$GITHUB_OUTPUT" - echo "::notice::Discovered scenarios: $MATRIX" - - molecule: - name: Molecule (${{ matrix.scenario }}) - needs: discover - runs-on: ${{ inputs.runs_on }} - strategy: - fail-fast: false - matrix: - scenario: ${{ fromJson(needs.discover.outputs.scenarios) }} - steps: - - name: Checkout collection into ansible_collections// - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - path: ansible_collections/${{ inputs.collection_namespace }}/${{ inputs.collection_name }} - - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ inputs.python_version }} - cache: 'pip' - cache-dependency-path: ansible_collections/${{ inputs.collection_namespace }}/${{ inputs.collection_name }}/requirements.txt - - - name: Install QEMU/KVM and enable /dev/kvm access - run: | - set -euo pipefail - sudo apt-get update -qq - # genisoimage provides mkisofs (cloud-init seed ISO); qemu-system-x86 - # + qemu-utils are the molecule-qemu runtime dependencies. - sudo apt-get install -y -qq \ - qemu-system-x86 qemu-utils genisoimage - # The runner user is not in the kvm group by default; add it and - # confirm acceleration is usable (falls back to TCG otherwise). - sudo usermod -aG kvm "$USER" - test -e /dev/kvm && ls -l /dev/kvm - - - name: Install collection dev dependencies - working-directory: ansible_collections/${{ inputs.collection_namespace }}/${{ inputs.collection_name }} - run: | - if [ ! -f requirements.txt ]; then - echo "::error::requirements.txt missing; cannot install molecule + driver" - exit 1 - fi - pip install -r requirements.txt - - - name: Install collection runtime dependencies - working-directory: ansible_collections/${{ inputs.collection_namespace }}/${{ inputs.collection_name }} - env: - SCENARIOS_ROOT: ${{ inputs.scenarios_root }} - run: | - req="${SCENARIOS_ROOT}/.config/requirements.yml" - if [ -f "$req" ]; then - ansible-galaxy collection install -r "$req" - else - echo "::notice::No shared collection requirements at $req — skipping" - fi - - - name: Run molecule scenario - working-directory: ansible_collections/${{ inputs.collection_namespace }}/${{ inputs.collection_name }} - env: - ANSIBLE_FORCE_COLOR: '1' - PY_COLORS: '1' - MOLECULE_NO_LOG: 'false' - SCENARIO: ${{ matrix.scenario }} - SCENARIOS_ROOT: ${{ inputs.scenarios_root }} - run: | - # molecule resolves scenarios relative to the cwd that contains - # `molecule/`; SCENARIOS_ROOT is its parent. `sg kvm` activates - # the freshly added group membership for this command. - cd "$(dirname "$SCENARIOS_ROOT")" - sg kvm -c "molecule test -s '$SCENARIO'" - - - name: Summarise scenario outcome - if: always() - env: - SCENARIO: ${{ matrix.scenario }} - STATUS: ${{ job.status }} - run: | - { - echo "## Molecule scenario (KVM): \`$SCENARIO\`" - echo - echo "**Status:** $STATUS" - } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/ci-ansible-molecule.yml b/.github/workflows/ci-ansible-molecule.yml index a9dfd2f..7fea805 100644 --- a/.github/workflows/ci-ansible-molecule.yml +++ b/.github/workflows/ci-ansible-molecule.yml @@ -9,13 +9,21 @@ name: CI - Ansible Molecule # # Scenario discovery: # - If `scenarios` (JSON array) is provided, that list is used verbatim. -# - Otherwise the workflow lists `extensions/molecule/` and skips any -# directory whose name begins with `.` (e.g. `.config`). +# - Otherwise the workflow lists `scenarios_root` and skips any directory +# whose name begins with `.` (e.g. `.config`). # -# Driver: docker (containers from geerlingguy/docker-*-ansible). Roles -# that touch the kernel (sysctl write, nftables load, swap, ethtool) -# should ship a `test_sequence:` in their scenario's molecule.yml that -# stops at `syntax` — running them under docker is otherwise unreliable. +# Driver (input `driver`): +# - `docker` (default): containers from geerlingguy/docker-*-ansible. Roles +# that touch the kernel (sysctl write, nftables load, swap, ethtool) should +# ship a `test_sequence:` stopping at `syntax` in their molecule.yml — +# running them under docker is otherwise unreliable. +# - `qemu`: boots full VMs via the molecule-qemu driver for roles that need a +# real kernel and init system (k3s, container engines, systemd services). +# GitHub-hosted ubuntu-latest exposes a writable /dev/kvm, so molecule-qemu +# auto-detects hardware acceleration (falls back to TCG otherwise). The job +# installs QEMU, adds the runner user to the kvm group, and runs molecule +# via `sg kvm` (group changes do not affect the current shell). Each role's +# molecule.yml selects `driver: molecule-qemu` and defines its platforms. on: workflow_call: @@ -29,6 +37,13 @@ on: type: string required: true description: 'Collection name.' + driver: + type: string + required: false + default: 'docker' + description: | + Molecule driver: `docker` (default, fast containers) or `qemu` + (full VMs via molecule-qemu, for roles needing a real kernel/init). python_version: type: string required: false @@ -39,7 +54,7 @@ on: required: false default: '' description: | - JSON array of scenario directory names under `extensions/molecule/`. + JSON array of scenario directory names under `scenarios_root`. When empty, the workflow discovers scenarios automatically and builds the matrix from the directory listing (entries starting with `.` are skipped). @@ -50,13 +65,15 @@ on: description: | Path (relative to the collection root) where scenarios live. Defaults to the layout shipped inside published collection - tarballs; override for repos that keep scenarios outside - `extensions/`. + tarballs; override for repos that keep scenarios elsewhere. runs_on: type: string required: false default: 'ubuntu-latest' - description: 'GitHub-hosted runner label for the molecule matrix legs.' + description: | + GitHub-hosted runner label for the molecule matrix legs. The + standard ubuntu-latest runner exposes /dev/kvm, so the `qemu` + driver does not require a larger/paid runner. cancel-in-progress: type: boolean required: false @@ -93,6 +110,16 @@ jobs: outputs: scenarios: ${{ steps.list.outputs.scenarios }} steps: + - name: Validate driver input + env: + DRIVER: ${{ inputs.driver }} + run: | + set -euo pipefail + case "$DRIVER" in + docker|qemu) ;; + *) echo "::error::Invalid driver '$DRIVER' (expected 'docker' or 'qemu')"; exit 1 ;; + esac + - name: Checkout collection uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: @@ -157,6 +184,20 @@ jobs: cache: 'pip' cache-dependency-path: ansible_collections/${{ inputs.collection_namespace }}/${{ inputs.collection_name }}/requirements.txt + - name: Install QEMU/KVM and enable /dev/kvm access + if: inputs.driver == 'qemu' + run: | + set -euo pipefail + sudo apt-get update -qq + # genisoimage provides mkisofs (cloud-init seed ISO); qemu-system-x86 + # + qemu-utils are the molecule-qemu runtime dependencies. + sudo apt-get install -y -qq \ + qemu-system-x86 qemu-utils genisoimage + # The runner user is not in the kvm group by default; add it and + # confirm acceleration is usable (falls back to TCG otherwise). + sudo usermod -aG kvm "$USER" + test -e /dev/kvm && ls -l /dev/kvm + - name: Install collection dev dependencies working-directory: ansible_collections/${{ inputs.collection_namespace }}/${{ inputs.collection_name }} run: | @@ -184,6 +225,7 @@ jobs: ANSIBLE_FORCE_COLOR: '1' PY_COLORS: '1' MOLECULE_NO_LOG: 'false' + DRIVER: ${{ inputs.driver }} SCENARIO: ${{ matrix.scenario }} SCENARIOS_ROOT: ${{ inputs.scenarios_root }} run: | @@ -191,16 +233,22 @@ jobs: # `molecule/`. SCENARIOS_ROOT is the parent of that directory # (e.g. `extensions/molecule` -> cd `extensions/`). cd "$(dirname "$SCENARIOS_ROOT")" - molecule test -s "$SCENARIO" + if [ "$DRIVER" = "qemu" ]; then + # `sg kvm` activates the freshly added group membership for this command. + sg kvm -c "molecule test -s '$SCENARIO'" + else + molecule test -s "$SCENARIO" + fi - name: Summarise scenario outcome if: always() env: + DRIVER: ${{ inputs.driver }} SCENARIO: ${{ matrix.scenario }} STATUS: ${{ job.status }} run: | { - echo "## Molecule scenario: \`$SCENARIO\`" + echo "## Molecule scenario (${DRIVER}): \`$SCENARIO\`" echo echo "**Status:** $STATUS" } >> "$GITHUB_STEP_SUMMARY" diff --git a/AGENTS.md b/AGENTS.md index 6c7a7af..9cada29 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -95,8 +95,7 @@ This repository provides shared Renovate presets for consumer repositories: | Workflow | File | Description | |----------|------|-------------| | Ansible Collection CI | [ci-ansible-collection.yml](./.github/workflows/ci-ansible-collection.yml) | Linting, security scan, sanity/unit/integration tests, build | -| Ansible Molecule CI | [ci-ansible-molecule.yml](./.github/workflows/ci-ansible-molecule.yml) | Auto-discovered Molecule scenarios under `extensions/molecule/`, docker driver | -| Ansible Molecule CI (KVM) | [ci-ansible-molecule-kvm.yml](./.github/workflows/ci-ansible-molecule-kvm.yml) | Molecule scenarios in full VMs via the `molecule-qemu` driver — for roles needing a real kernel/init (k3s, container engines, systemd-service agents) | +| Ansible Molecule CI | [ci-ansible-molecule.yml](./.github/workflows/ci-ansible-molecule.yml) | Auto-discovered Molecule scenarios under `extensions/molecule/`; `driver` input selects `docker` (default) or `qemu` (full VMs via `molecule-qemu` for roles needing a real kernel/init — k3s, container engines, systemd-service agents) | | Go CI | [ci-go.yml](./.github/workflows/ci-go.yml) | golangci-lint, gofmt, go vet, go test | | Lint | [ci-lint.yml](./.github/workflows/ci-lint.yml) | MegaLinter aggregator | diff --git a/CHANGELOG.md b/CHANGELOG.md index 0234ee3..0090952 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,17 +10,17 @@ This is a rolling release - changes are deployed continuously to `main`. ### Added -- **ci-ansible-molecule-kvm.yml**: New reusable Molecule runner that boots full - VMs via the `molecule-qemu` driver instead of the docker driver, for roles - that need a real kernel and init system (k3s `modprobe`/cgroups, container - engines, agents that must run as a stable `systemd` service — under docker - the unit reports started while it has crash-looped, masking config bugs). - GitHub-hosted `ubuntu-latest` exposes a writable `/dev/kvm`, so the job - installs `qemu-system-x86`/`qemu-utils`/`genisoimage`, adds the runner to the - `kvm` group, and runs molecule via `sg kvm`; molecule-qemu auto-detects KVM +- **ci-ansible-molecule.yml**: `driver` input (`docker` default, `qemu`) to + pick the Molecule driver. `qemu` boots full VMs via the `molecule-qemu` + driver for roles that need a real kernel and init system (k3s + `modprobe`/cgroups, container engines, agents that must run as a stable + `systemd` service — under docker the unit reports started while it has + crash-looped, masking config bugs). GitHub-hosted `ubuntu-latest` exposes a + writable `/dev/kvm`, so the job installs + `qemu-system-x86`/`qemu-utils`/`genisoimage`, adds the runner to the `kvm` + group, and runs molecule via `sg kvm`; molecule-qemu auto-detects KVM acceleration and falls back to TCG. Validated end-to-end on `arillso.agent` - (alloy) and `arillso.container` (k3s). Docker-driver roles keep using - `ci-ansible-molecule.yml` + (alloy) and `arillso.container` (k3s). - **AGENTS.md**: New "Ansible Collection Conventions" section documenting the shared release workflow shape (`name`, `run-name`, `concurrency`), the Keep-a-Changelog format, the cross-collection dependency-bound matrix From 0f9fd4e64139e2973609f9cb559988df9049ef4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20B=C3=A4rlocher?= Date: Wed, 17 Jun 2026 23:56:08 +0200 Subject: [PATCH 2/3] fix(ci): make /dev/kvm probe non-fatal in molecule qemu setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under set -euo pipefail, `test -e /dev/kvm && ls -l /dev/kvm` aborted the setup step when /dev/kvm was absent, so the job never reached the TCG fallback the comment described. Log a warning instead and let molecule-qemu fall back to TCG. (Carried over from the deleted -kvm workflow.) Signed-off-by: Simon Bärlocher --- .github/workflows/ci-ansible-molecule.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-ansible-molecule.yml b/.github/workflows/ci-ansible-molecule.yml index 7fea805..d7be722 100644 --- a/.github/workflows/ci-ansible-molecule.yml +++ b/.github/workflows/ci-ansible-molecule.yml @@ -193,10 +193,15 @@ jobs: # + qemu-utils are the molecule-qemu runtime dependencies. sudo apt-get install -y -qq \ qemu-system-x86 qemu-utils genisoimage - # The runner user is not in the kvm group by default; add it and - # confirm acceleration is usable (falls back to TCG otherwise). + # The runner user is not in the kvm group by default; add it. Log + # whether acceleration is available without failing the step + # (molecule-qemu falls back to TCG when /dev/kvm is absent). sudo usermod -aG kvm "$USER" - test -e /dev/kvm && ls -l /dev/kvm + if [ -e /dev/kvm ]; then + ls -l /dev/kvm + else + echo "::warning::/dev/kvm absent — molecule-qemu will fall back to TCG" + fi - name: Install collection dev dependencies working-directory: ansible_collections/${{ inputs.collection_namespace }}/${{ inputs.collection_name }} From 93b9438997d9028157d2f7f14ef63099f2975310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20B=C3=A4rlocher?= Date: Wed, 17 Jun 2026 23:59:52 +0200 Subject: [PATCH 3/3] chore: re-trigger review after changelog/probe fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Simon Bärlocher