From 227754e9b1b1d323fbac71ed2fdbcfdf238dd589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Mon, 4 Aug 2025 21:12:53 +0200 Subject: [PATCH 1/3] feat: add ansible task testing infrastructure based on Docker and pytest This complements the existing AMI tests in testinfra by providing a faster feedback loops for Ansible development without requiring a full VM. We are also using testinfra to validate that the Ansible tasks have the desired effect. It is based on Docker, it can be run locally (e.g. macOS) or in CI. Note that this approach is not intended to replace the AMI tests, but rather to provide a more efficient way to test Ansible tasks during development. You can run the tests using `nix run -L .\#ansible-test` --- .envrc.recommended | 3 - .github/actionlint.yaml | 1 + .github/actionlint.yml | 4 -- .github/workflows/ansible-tests.yml | 98 ++++++++++++++++++++++++++++ ansible/tasks/files | 1 + ansible/tests/conftest.py | 84 ++++++++++++++++++++++++ ansible/tests/nginx.yaml | 14 ++++ ansible/tests/test_nginx.py | 11 ++++ nix/hooks.nix | 1 + nix/packages/ansible-test.nix | 23 +++++++ nix/packages/default.nix | 5 ++ nix/packages/docker-ansible-test.nix | 21 ++++++ nix/packages/docker-ubuntu.nix | 57 ++++++++++++++++ 13 files changed, 316 insertions(+), 7 deletions(-) delete mode 100644 .envrc.recommended delete mode 100644 .github/actionlint.yml create mode 100644 .github/workflows/ansible-tests.yml create mode 120000 ansible/tasks/files create mode 100644 ansible/tests/conftest.py create mode 100644 ansible/tests/nginx.yaml create mode 100644 ansible/tests/test_nginx.py create mode 100644 nix/packages/ansible-test.nix create mode 100644 nix/packages/docker-ansible-test.nix create mode 100644 nix/packages/docker-ubuntu.nix diff --git a/.envrc.recommended b/.envrc.recommended deleted file mode 100644 index a7aaf82ac0..0000000000 --- a/.envrc.recommended +++ /dev/null @@ -1,3 +0,0 @@ -watch_file nix/devShells.nix - -use flake diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index f238fb7913..4e21e72c37 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -3,6 +3,7 @@ self-hosted-runner: - aarch64-darwin - aarch64-linux - blacksmith-32vcpu-ubuntu-2404 + - blacksmith-16vcpu-ubuntu-2404 - blacksmith-2vcpu-ubuntu-2404 - blacksmith-2vcpu-ubuntu-2404-arm - blacksmith-4vcpu-ubuntu-2404 diff --git a/.github/actionlint.yml b/.github/actionlint.yml deleted file mode 100644 index eaf4d7d50d..0000000000 --- a/.github/actionlint.yml +++ /dev/null @@ -1,4 +0,0 @@ -self-hosted-runner: - labels: - - blacksmith-2vcpu-ubuntu-2404-arm - - blacksmith-4vcpu-ubuntu-2404 diff --git a/.github/workflows/ansible-tests.yml b/.github/workflows/ansible-tests.yml new file mode 100644 index 0000000000..b88985596e --- /dev/null +++ b/.github/workflows/ansible-tests.yml @@ -0,0 +1,98 @@ +name: Ansible Test Image CI + +on: + push: + branches: + - develop + pull_request: + workflow_dispatch: + +permissions: + contents: read + id-token: write + +jobs: + build-and-push: + if: github.event_name == 'push' && github.ref == 'refs/heads/develop' + strategy: + matrix: + arch: [amd64, arm64] + runs-on: ${{ matrix.arch == 'amd64' && 'blacksmith-16vcpu-ubuntu-2404' || 'blacksmith-16vcpu-ubuntu-2404-arm' }} + steps: + - name: Checkout Repo + uses: supabase/postgres/.github/actions/shared-checkout@HEAD + + - name: Install Nix + uses: ./.github/actions/nix-install-ephemeral + with: + push-to-cache: true + env: + DEV_AWS_ROLE: ${{ secrets.DEV_AWS_ROLE }} + NIX_SIGN_SECRET_KEY: ${{ secrets.NIX_SIGN_SECRET_KEY }} + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build Docker image with Nix + run: | + echo "Building ansible-test Docker image for ${{ matrix.arch }}..." + IMAGE_PATH=$(nix build .#docker-ansible-test --print-out-paths) + echo "IMAGE_PATH=$IMAGE_PATH" >> "$GITHUB_ENV" + + - name: Load and push Docker image + run: | + echo "Loading Docker image..." + docker load < "$IMAGE_PATH" + docker tag supabase/ansible-test:latest supabase/ansible-test:latest-${{ matrix.arch }} + docker push supabase/ansible-test:latest-${{ matrix.arch }} + + create-manifest: + if: github.event_name == 'push' && github.ref == 'refs/heads/develop' + needs: build-and-push + runs-on: 'blacksmith-4vcpu-ubuntu-2404' + steps: + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Create and push multi-arch manifest + run: | + docker manifest create supabase/ansible-test:latest \ + supabase/ansible-test:latest-amd64 \ + supabase/ansible-test:latest-arm64 + docker manifest push supabase/ansible-test:latest + + run-ansible-tests: + if: ${{ !failure() && !cancelled() }} + needs: create-manifest + runs-on: 'blacksmith-16vcpu-ubuntu-2404' + steps: + - name: Checkout Repo + uses: supabase/postgres/.github/actions/shared-checkout@HEAD + + - name: Install Nix + uses: ./.github/actions/nix-install-ephemeral + with: + push-to-cache: true + env: + DEV_AWS_ROLE: ${{ secrets.DEV_AWS_ROLE }} + NIX_SIGN_SECRET_KEY: ${{ secrets.NIX_SIGN_SECRET_KEY }} + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Run Ansible tests + env: + PY_COLORS: '1' + ANSIBLE_FORCE_COLOR: '1' + run: | + docker pull supabase/ansible-test:latest & + nix run .#ansible-test diff --git a/ansible/tasks/files b/ansible/tasks/files new file mode 120000 index 0000000000..feb122881c --- /dev/null +++ b/ansible/tasks/files @@ -0,0 +1 @@ +../files \ No newline at end of file diff --git a/ansible/tests/conftest.py b/ansible/tests/conftest.py new file mode 100644 index 0000000000..72ff94466b --- /dev/null +++ b/ansible/tests/conftest.py @@ -0,0 +1,84 @@ +import pytest +import subprocess +import testinfra +from rich.console import Console + +console = Console() + + +def pytest_addoption(parser): + parser.addoption( + "--flake-dir", + action="store", + help="Directory containing the current flake", + ) + + parser.addoption( + "--docker-image", + action="store", + help="Docker image and tag to use for testing", + ) + + +@pytest.fixture(scope="module") +def host(request): + flake_dir = request.config.getoption("--flake-dir") + if not flake_dir: + pytest.fail("--flake-dir option is required") + docker_image = request.config.getoption("--docker-image") + if not docker_image: + pytest.fail("--docker-image option is required") + docker_id = ( + subprocess.check_output( + [ + "docker", + "run", + "--privileged", + "--cap-add", + "SYS_ADMIN", + "--security-opt", + "seccomp=unconfined", + "--cgroup-parent=docker.slice", + "--cgroupns", + "private", + "-v", + f"{flake_dir}:/flake", + "-d", + docker_image, + ] + ) + .decode() + .strip() + ) + yield testinfra.get_host("docker://" + docker_id) + subprocess.check_call(["docker", "rm", "-f", docker_id], stdout=subprocess.DEVNULL) + + +@pytest.fixture(scope="module") +def run_ansible_playbook(host): + def _run_playbook(playbook_name, verbose=False): + cmd = [ + "ANSIBLE_HOST_KEY_CHECKING=False", + "ansible-playbook", + "--connection=local", + ] + if verbose: + cmd.append("-vvv") + cmd.extend( + [ + "-i", + "localhost,", + "--extra-vars", + "@/flake/ansible/vars.yml", + f"/flake/ansible/tests/{playbook_name}", + ] + ) + result = host.run(" ".join(cmd)) + if result.failed: + console.log(result.stdout) + console.log(result.stderr) + pytest.fail( + f"Ansible playbook {playbook_name} failed with return code {result.rc}" + ) + + return _run_playbook diff --git a/ansible/tests/nginx.yaml b/ansible/tests/nginx.yaml new file mode 100644 index 0000000000..720e79679e --- /dev/null +++ b/ansible/tests/nginx.yaml @@ -0,0 +1,14 @@ +--- +- hosts: localhost + tasks: + - name: Install dependencies + apt: + pkg: + - build-essential + update_cache: yes + - import_tasks: ../tasks/setup-nginx.yml + - name: Start Nginx service + service: + name: nginx + state: started + enabled: yes diff --git a/ansible/tests/test_nginx.py b/ansible/tests/test_nginx.py new file mode 100644 index 0000000000..ec68e82a92 --- /dev/null +++ b/ansible/tests/test_nginx.py @@ -0,0 +1,11 @@ +import pytest + + +@pytest.fixture(scope="module", autouse=True) +def run_ansible(run_ansible_playbook): + run_ansible_playbook("nginx.yaml") + + +def test_nginx_service(host): + assert host.service("nginx.service").is_valid + assert host.service("nginx.service").is_running diff --git a/nix/hooks.nix b/nix/hooks.nix index 9b78b8a5d7..74ea506c93 100644 --- a/nix/hooks.nix +++ b/nix/hooks.nix @@ -2,6 +2,7 @@ let ghWorkflows = builtins.attrNames (builtins.readDir ../.github/workflows); lintedWorkflows = [ + "ansible-test.yml" "nix-eval.yml" "nix-build.yml" "testinfra-ami-build.yml" diff --git a/nix/packages/ansible-test.nix b/nix/packages/ansible-test.nix new file mode 100644 index 0000000000..6aaec68b37 --- /dev/null +++ b/nix/packages/ansible-test.nix @@ -0,0 +1,23 @@ +{ self, pkgs }: +pkgs.writeShellApplication { + name = "ansible-test"; + runtimeInputs = with pkgs; [ + (python3.withPackages ( + ps: with ps; [ + requests + pytest + pytest-testinfra + pytest-xdist + rich + ] + )) + ]; + text = '' + echo "Running Ansible tests..." + FLAKE_DIR=${self} + pytest -x -p no:cacheprovider -s -v $FLAKE_DIR/ansible/tests --flake-dir=$FLAKE_DIR --docker-image=supabase/ansible-test:latest "$@" + ''; + meta = { + description = "Ansible test runner"; + }; +} diff --git a/nix/packages/default.nix b/nix/packages/default.nix index d49211073f..5553af6211 100644 --- a/nix/packages/default.nix +++ b/nix/packages/default.nix @@ -38,6 +38,7 @@ { build-ami = pkgs.callPackage ./build-ami.nix { packer = self'.packages.packer; }; build-test-ami = pkgs.callPackage ./build-test-ami.nix { packer = self'.packages.packer; }; + ansible-test = pkgs.callPackage ./ansible-test.nix { inherit self; }; cleanup-ami = pkgs.callPackage ./cleanup-ami.nix { }; dbmate-tool = pkgs.callPackage ./dbmate-tool.nix { inherit (self.supabase) defaults; }; docker-image-inputs = pkgs.callPackage ./docker-image-inputs.nix { @@ -46,6 +47,10 @@ psql_orioledb-17_slim = self'.packages."psql_orioledb-17_slim/bin"; supabase-groonga = self'.packages.supabase-groonga; }; + docker-ansible-test = pkgs.callPackage ./docker-ansible-test.nix { + inherit (self'.packages) docker-image-ubuntu; + }; + docker-image-ubuntu = pkgs.callPackage ./docker-ubuntu.nix { }; docs = pkgs.callPackage ./docs.nix { }; pgbouncer = pkgs.callPackage ../pgbouncer.nix { }; github-matrix = pkgs.callPackage ./github-matrix { diff --git a/nix/packages/docker-ansible-test.nix b/nix/packages/docker-ansible-test.nix new file mode 100644 index 0000000000..15a1d23205 --- /dev/null +++ b/nix/packages/docker-ansible-test.nix @@ -0,0 +1,21 @@ +{ + pkgs, + lib, + docker-image-ubuntu, +}: +let + tools = [ pkgs.ansible ]; +in +pkgs.dockerTools.buildLayeredImage { + name = "supabase/ansible-test"; + tag = "latest"; + maxLayers = 30; + fromImage = docker-image-ubuntu; + compressor = "zstd"; + config = { + Env = [ + "PATH=${lib.makeBinPath tools}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ]; + Cmd = [ "/lib/systemd/systemd" ]; + }; +} diff --git a/nix/packages/docker-ubuntu.nix b/nix/packages/docker-ubuntu.nix new file mode 100644 index 0000000000..c492380f93 --- /dev/null +++ b/nix/packages/docker-ubuntu.nix @@ -0,0 +1,57 @@ +{ + runCommand, + dockerTools, + xz, + buildEnv, + stdenv, +}: +let + ubuntu-cloudimg = + let + + cloudImg = + if stdenv.hostPlatform.system == "x86_64-linux" then + builtins.fetchurl { + url = "https://cloud-images.ubuntu.com/releases/noble/release-20251026/ubuntu-24.04-server-cloudimg-amd64-root.tar.xz"; + sha256 = "0y3d55f5qy7bxm3mfmnxzpmwp88d7iiszc57z5b9npc6xgwi28np"; + } + else + builtins.fetchurl { + url = "https://cloud-images.ubuntu.com/releases/noble/release-20251026/ubuntu-24.04-server-cloudimg-arm64-root.tar.xz"; + sha256 = "1l4l0llfffspzgnmwhax0fcnjn8ih8n4azhfaghng2hh1xvr4a17"; + }; + in + runCommand "ubuntu-cloudimg" { nativeBuildInputs = [ xz ]; } '' + mkdir -p $out + tar --exclude='dev/*' \ + --exclude='etc/systemd/system/network-online.target.wants/systemd-networkd-wait-online.service' \ + --exclude='etc/systemd/system/multi-user.target.wants/systemd-resolved.service' \ + --exclude='usr/lib/systemd/system/tpm-udev.service' \ + --exclude='usr/lib/systemd/system/systemd-remount-fs.service' \ + --exclude='usr/lib/systemd/system/systemd-resolved.service' \ + --exclude='usr/lib/systemd/system/proc-sys-fs-binfmt_misc.automount' \ + --exclude='usr/lib/systemd/system/sys-kernel-*' \ + --exclude='var/lib/apt/lists/*' \ + -xJf ${cloudImg} -C $out + rm -f $out/bin $out/lib $out/lib64 $out/sbin + mkdir -p $out/run/systemd && echo 'docker' > $out/run/systemd/container + mkdir $out/var/lib/apt/lists/partial + ''; +in +dockerTools.buildImage { + name = "ubuntu-cloudimg"; + tag = "24.04"; + created = "now"; + extraCommands = '' + ln -s usr/bin + ln -s usr/lib + ln -s usr/lib64 + ln -s usr/sbin + ''; + copyToRoot = buildEnv { + name = "image-root"; + pathsToLink = [ "/" ]; + paths = [ ubuntu-cloudimg ]; + }; + config.Cmd = [ "/lib/systemd/systemd" ]; +} From 1aa4df53a76fff4d697e7abfb7cd077e097073d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Tue, 11 Nov 2025 15:14:18 +0100 Subject: [PATCH 2/3] feat: add ansible-lint validation for test playbooks Configure pre-commit hook to run ansible-lint on test playbooks and their dependencies. Since test playbooks include tasks from existing task files, ansible-lint automatically validates those dependencies as well. --- .ansible-lint.yml | 44 ++++++++++++++++++++++ ansible/tasks/setup-nginx.yml | 70 ++++++++++++++++++----------------- ansible/tests/nginx.yaml | 26 +++++++------ ansible/vars.yml | 2 +- nix/hooks.nix | 9 +++++ 5 files changed, 105 insertions(+), 46 deletions(-) create mode 100644 .ansible-lint.yml diff --git a/.ansible-lint.yml b/.ansible-lint.yml new file mode 100644 index 0000000000..512c3751c4 --- /dev/null +++ b/.ansible-lint.yml @@ -0,0 +1,44 @@ +--- +profile: production + +# exclude_paths included in this file are parsed relative to this file's location +# and not relative to the CWD of execution. CLI arguments passed to the --exclude +# option are parsed relative to the CWD of execution. +exclude_paths: + - .cache/ # implicit unless exclude_paths is defined in config + - .github/ + - ansible/files/ + - ansible/manifest-playbook.yml + - ansible/playbook.yml + - ansible/tasks/ + - audit-specs/ + - nix/mkdocs.yml + +use_default_rules: true +enable_list: + - args + - empty-string-compare + - no-log-password + - no-same-owner +warn_list: + - experimental +skip_list: + - name[casing] + - name[prefix] + - yaml[line-length] + - var-naming[no-role-prefix] + +# Offline mode disables installation of requirements.yml +offline: false + +# Make the output more readable +parseable: true + +# Define required Ansible's variables to satisfy syntax check +# extra_vars: + +# List of additional kind:pattern to be added at the top of the default +# match list, first match determines the file kind. +kinds: + - tasks: "ansible/tasks/*.yml" + - vars: "ansible/vars.yml" diff --git a/ansible/tasks/setup-nginx.yml b/ansible/tasks/setup-nginx.yml index 1f10ceec26..58986419ac 100644 --- a/ansible/tasks/setup-nginx.yml +++ b/ansible/tasks/setup-nginx.yml @@ -1,10 +1,11 @@ -- name: nginx - system user +--- +- name: Nginx - system user ansible.builtin.user: - name: 'nginx' - state: 'present' + name: nginx + state: present # Kong installation steps from http://archive.vn/3HRQx -- name: nginx - system dependencies +- name: Nginx - system dependencies ansible.builtin.apt: pkg: - libpcre3-dev @@ -12,67 +13,70 @@ - openssl - zlib1g-dev -- name: nginx - download source +- name: Nginx - download source ansible.builtin.get_url: checksum: "{{ nginx_release_checksum }}" - dest: '/tmp/nginx-{{ nginx_release }}.tar.gz' - url: "https://nginx.org/download/nginx-{{ nginx_release }}.tar.gz" + dest: /tmp/nginx-{{ nginx_release }}.tar.gz + url: https://nginx.org/download/nginx-{{ nginx_release }}.tar.gz + mode: '0640' -- name: nginx - unpack archive +- name: Nginx - unpack archive ansible.builtin.unarchive: - dest: '/tmp' + dest: /tmp remote_src: true - src: "/tmp/nginx-{{ nginx_release }}.tar.gz" + src: /tmp/nginx-{{ nginx_release }}.tar.gz -- name: nginx - configure +- name: Nginx - configure ansible.builtin.command: argv: - - ./configure - - --prefix=/usr/local/nginx - - --conf-path=/etc/nginx/nginx.conf - - --with-http_ssl_module - - --with-http_realip_module + - ./configure + - --prefix=/usr/local/nginx + - --conf-path=/etc/nginx/nginx.conf + - --with-http_ssl_module + - --with-http_realip_module - --with-threads + creates: /tmp/nginx-{{ nginx_release }}/Makefile args: - chdir: "/tmp/nginx-{{ nginx_release }}" + chdir: /tmp/nginx-{{ nginx_release }} become: true -- name: nginx - build and install +- name: Nginx - build and install community.general.make: - chdir: "/tmp/nginx-{{ nginx_release }}" + chdir: /tmp/nginx-{{ nginx_release }} jobs: "{{ parallel_jobs | default(omit) }}" target: "{{ make_target }}" become: true loop: - - 'build' - - 'install' + - build + - install loop_control: - loop_var: 'make_target' + loop_var: make_target -- name: nginx - hand over ownership of /etc/nginx and /usr/local/nginx to user nginx +- name: Nginx - hand over ownership of /etc/nginx and /usr/local/nginx to user nginx ansible.builtin.file: - owner: 'nginx' + owner: nginx path: "{{ nginx_dir_item }}" recurse: true loop: - /etc/nginx - /usr/local/nginx loop_control: - loop_var: 'nginx_dir_item' + loop_var: nginx_dir_item # [warn] ulimit is currently set to "1024". For better performance set it to at least # "4096" using "ulimit -n" -- name: nginx - bump up ulimit +- name: Nginx - bump up ulimit community.general.pam_limits: - domain: 'nginx' - limit_item: 'nofile' - limit_type: 'soft' - value: '4096' + domain: nginx + limit_item: nofile + limit_type: soft + value: "4096" -- name: nginx - create service file +- name: Nginx - create service file ansible.builtin.template: - dest: '/etc/systemd/system/nginx.service' - src: 'files/nginx.service.j2' + dest: /etc/systemd/system/nginx.service + src: files/nginx.service.j2 + mode: '0644' # Keep it dormant for the timebeing diff --git a/ansible/tests/nginx.yaml b/ansible/tests/nginx.yaml index 720e79679e..0054819c17 100644 --- a/ansible/tests/nginx.yaml +++ b/ansible/tests/nginx.yaml @@ -1,14 +1,16 @@ --- -- hosts: localhost +- name: Setup Nginx Server + hosts: localhost tasks: - - name: Install dependencies - apt: - pkg: - - build-essential - update_cache: yes - - import_tasks: ../tasks/setup-nginx.yml - - name: Start Nginx service - service: - name: nginx - state: started - enabled: yes + - name: Install dependencies + ansible.builtin.apt: + pkg: + - build-essential + update_cache: true + - name: Setup Nginx using existing task file + ansible.builtin.import_tasks: ../tasks/setup-nginx.yml + - name: Start Nginx service + ansible.builtin.service: + name: nginx + state: started + enabled: true diff --git a/ansible/vars.yml b/ansible/vars.yml index a7d2d7a276..f99eec68d4 100644 --- a/ansible/vars.yml +++ b/ansible/vars.yml @@ -18,7 +18,7 @@ postgres_release: pgbouncer_release: 1.25.1 pgbouncer_release_checksum: sha256:6e566ae92fe3ef7f6a1b9e26d6049f7d7ca39c40e29e7b38f6d5500ae15d8465 -# The checksum can be found under "Assets", in the GitHub release page for each version. +# The checksum can be found under "Assets", in the GitHub release page for each version. # The binaries used are: ubuntu-aarch64 and linux-static. # https://github.com/PostgREST/postgrest/releases postgrest_release: 14.5 diff --git a/nix/hooks.nix b/nix/hooks.nix index 74ea506c93..f0d5bc5d71 100644 --- a/nix/hooks.nix +++ b/nix/hooks.nix @@ -25,6 +25,15 @@ in verbose = true; }; + ansible-lint = { + enable = true; + verbose = true; + settings = { + configPath = "${../.ansible-lint.yml}"; + subdir = "ansible/tests"; + }; + }; + treefmt = { enable = true; package = config.treefmt.build.wrapper; From 6f1fa1b456c0bf14628d90e9eef50e050de81a83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Mon, 9 Feb 2026 20:20:37 +0100 Subject: [PATCH 3/3] feat: run ansible tests in parallel with pytest-xdist --- nix/packages/ansible-test.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/packages/ansible-test.nix b/nix/packages/ansible-test.nix index 6aaec68b37..58c6a86a66 100644 --- a/nix/packages/ansible-test.nix +++ b/nix/packages/ansible-test.nix @@ -15,7 +15,7 @@ pkgs.writeShellApplication { text = '' echo "Running Ansible tests..." FLAKE_DIR=${self} - pytest -x -p no:cacheprovider -s -v $FLAKE_DIR/ansible/tests --flake-dir=$FLAKE_DIR --docker-image=supabase/ansible-test:latest "$@" + pytest -p no:cacheprovider -v -n auto $FLAKE_DIR/ansible/tests --flake-dir=$FLAKE_DIR --docker-image=supabase/ansible-test:latest "$@" ''; meta = { description = "Ansible test runner";