diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 13c314dfa7bb2375ae42acaa4f24c35208b3eed2..0c642985c19f8567153643c8e107b16f8939f73c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -77,7 +77,6 @@ ci-authorized:
 include:
   - .gitlab-ci/build.yml
   - .gitlab-ci/lint.yml
-  - .gitlab-ci/shellcheck.yml
   - .gitlab-ci/terraform.yml
   - .gitlab-ci/packet.yml
   - .gitlab-ci/vagrant.yml
diff --git a/.gitlab-ci/lint.yml b/.gitlab-ci/lint.yml
index 2f96fec5fa2d2c1a95a667993e78f4cf30cd981d..00f381f4ee4b11c3021d7a69db5db436a21b5cf4 100644
--- a/.gitlab-ci/lint.yml
+++ b/.gitlab-ci/lint.yml
@@ -1,13 +1,24 @@
 ---
-yamllint:
-  extends: .job
-  stage: unit-tests
-  tags: [light]
-  variables:
-    LANG: C.UTF-8
-  script:
-    - yamllint --strict .
-  except: ['triggers', 'master']
+generate-pre-commit:
+  image: 'mikefarah/yq@sha256:bcb889a1f9bdb0613c8a054542d02360c2b1b35521041be3e1bd8fbd0534d411'
+  stage: build
+  before_script: []
+  script:
+    - >
+      yq -r < .pre-commit-config.yaml '.repos[].hooks[].id' |
+      sed 's/^/      - /' |
+      cat .gitlab-ci/pre-commit-dynamic-stub.yml - > pre-commit-generated.yml
+  artifacts:
+    paths:
+      - pre-commit-generated.yml
+
+run-pre-commit:
+  stage: unit-tests
+  trigger:
+    include:
+      - artifact: pre-commit-generated.yml
+        job: generate-pre-commit
+    strategy: depend
 
 vagrant-validate:
   extends: .job
@@ -19,108 +30,11 @@ vagrant-validate:
     - ./tests/scripts/vagrant-validate.sh
   except: ['triggers', 'master']
 
-ansible-lint:
-  extends: .job
-  stage: unit-tests
-  tags: [light]
-  script:
-    - ansible-lint -v
-  except: ['triggers', 'master']
-
-jinja-syntax-check:
-  extends: .job
-  stage: unit-tests
-  tags: [light]
-  script:
-    - "find -name '*.j2' -exec tests/scripts/check-templates.py {} +"
-  except: ['triggers', 'master']
-
-syntax-check:
-  extends: .job
-  stage: unit-tests
-  tags: [light]
-  variables:
-    ANSIBLE_INVENTORY: inventory/local-tests.cfg
-    ANSIBLE_REMOTE_USER: root
-    ANSIBLE_BECOME: "true"
-    ANSIBLE_BECOME_USER: root
-    ANSIBLE_VERBOSITY: "3"
-  script:
-    - ansible-playbook --syntax-check cluster.yml
-    - ansible-playbook --syntax-check playbooks/cluster.yml
-    - ansible-playbook --syntax-check upgrade-cluster.yml
-    - ansible-playbook --syntax-check playbooks/upgrade_cluster.yml
-    - ansible-playbook --syntax-check reset.yml
-    - ansible-playbook --syntax-check playbooks/reset.yml
-    - ansible-playbook --syntax-check extra_playbooks/upgrade-only-k8s.yml
-  except: ['triggers', 'master']
-
-collection-build-install-sanity-check:
-  extends: .job
-  stage: unit-tests
-  tags: [light]
-  variables:
-    ANSIBLE_COLLECTIONS_PATH: "./ansible_collections"
-  script:
-    - ansible-galaxy collection build
-    - ansible-galaxy collection install kubernetes_sigs-kubespray-$(grep "^version:" galaxy.yml | awk '{print $2}').tar.gz
-    - ansible-galaxy collection list $(egrep -i '(name:\s+|namespace:\s+)' galaxy.yml | awk '{print $2}' | tr '\n' '.' | sed 's|\.$||g') | grep "^kubernetes_sigs.kubespray"
-    - test -f ansible_collections/kubernetes_sigs/kubespray/playbooks/cluster.yml
-    - test -f ansible_collections/kubernetes_sigs/kubespray/playbooks/reset.yml
-  except: ['triggers', 'master']
-
-tox-inventory-builder:
-  stage: unit-tests
-  tags: [light]
-  extends: .job
-  before_script:
-    - ./tests/scripts/rebase.sh
-  script:
-    - pip3 install tox
-    - cd contrib/inventory_builder && tox
-  except: ['triggers', 'master']
-
-markdownlint:
-  stage: unit-tests
-  tags: [light]
-  image: node
-  before_script:
-    - npm install -g markdownlint-cli@0.22.0
-  script:
-    - markdownlint $(find . -name '*.md' | grep -vF './.git') --ignore docs/_sidebar.md --ignore contrib/dind/README.md
-
-generate-sidebar:
-  extends: .job
-  stage: unit-tests
-  tags: [light]
-  script:
-    - scripts/gen_docs_sidebar.sh
-    - git diff --exit-code
-
-check-readme-versions:
-  stage: unit-tests
-  tags: [light]
-  image: python:3
-  script:
-    - tests/scripts/check_readme_versions.sh
 
+# TODO: convert to pre-commit hook
 check-galaxy-version:
   stage: unit-tests
   tags: [light]
   image: python:3
   script:
     - tests/scripts/check_galaxy_version.sh
-
-check-typo:
-  stage: unit-tests
-  tags: [light]
-  image: python:3
-  script:
-    - tests/scripts/check_typo.sh
-
-ci-matrix:
-  stage: unit-tests
-  tags: [light]
-  image: python:3
-  script:
-    - tests/scripts/md-table/test.sh
diff --git a/.gitlab-ci/pre-commit-dynamic-stub.yml b/.gitlab-ci/pre-commit-dynamic-stub.yml
new file mode 100644
index 0000000000000000000000000000000000000000..55f81b513a21ffa987f003b09574f6931ae9e132
--- /dev/null
+++ b/.gitlab-ci/pre-commit-dynamic-stub.yml
@@ -0,0 +1,17 @@
+---
+# stub pipeline for dynamic generation
+pre-commit:
+  tags:
+  - light
+  image: 'ghcr.io/pre-commit-ci/runner-image@sha256:aaf2c7b38b22286f2d381c11673bec571c28f61dd086d11b43a1c9444a813cef'
+  variables:
+    PRE_COMMIT_HOME: /pre-commit-cache
+  script:
+  - pre-commit run -a $HOOK_ID
+  cache:
+    key: pre-commit-$HOOK_ID
+    paths:
+    - /pre-commit-cache
+  parallel:
+    matrix:
+    - HOOK_ID:
diff --git a/.gitlab-ci/shellcheck.yml b/.gitlab-ci/shellcheck.yml
deleted file mode 100644
index 307e121c5c43c4479962582fed42aa716fdcf4e0..0000000000000000000000000000000000000000
--- a/.gitlab-ci/shellcheck.yml
+++ /dev/null
@@ -1,16 +0,0 @@
----
-shellcheck:
-  extends: .job
-  stage: unit-tests
-  tags: [light]
-  variables:
-    SHELLCHECK_VERSION: v0.7.1
-  before_script:
-    - ./tests/scripts/rebase.sh
-    - curl --silent --location "https://github.com/koalaman/shellcheck/releases/download/"${SHELLCHECK_VERSION}"/shellcheck-"${SHELLCHECK_VERSION}".linux.x86_64.tar.xz" | tar -xJv
-    - cp shellcheck-"${SHELLCHECK_VERSION}"/shellcheck /usr/bin/
-    - shellcheck --version
-  script:
-    # Run shellcheck for all *.sh
-    - find . -name '*.sh' -not -path './.git/*' | xargs shellcheck --severity error
-  except: ['triggers', 'master']
diff --git a/.markdownlint.yaml b/.markdownlint.yaml
deleted file mode 100644
index 8ece4c7613e9e19b4172126332e5900243f323b4..0000000000000000000000000000000000000000
--- a/.markdownlint.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
----
-MD013: false
-MD029: false
diff --git a/.md_style.rb b/.md_style.rb
new file mode 100644
index 0000000000000000000000000000000000000000..73adf8ae47ec942e426cdaf1664661bfdc9562c8
--- /dev/null
+++ b/.md_style.rb
@@ -0,0 +1,4 @@
+all
+exclude_rule 'MD013'
+exclude_rule 'MD029'
+rule 'MD007', :indent => 2
diff --git a/.mdlrc b/.mdlrc
new file mode 100644
index 0000000000000000000000000000000000000000..8ca55a8cee51d24e4330edad412bb5116d525ac4
--- /dev/null
+++ b/.mdlrc
@@ -0,0 +1 @@
+style "#{File.dirname(__FILE__)}/.md_style.rb"
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 337b484f19934e69db61c4e1325ad6a7a6620867..5bba7e7eac91866282ce5ff7009964647fa4a94d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,7 +1,7 @@
 ---
 repos:
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v3.4.0
+    rev: v4.6.0
     hooks:
       - id: check-added-large-files
       - id: check-case-conflict
@@ -15,47 +15,59 @@ repos:
       - id: trailing-whitespace
 
   - repo: https://github.com/adrienverge/yamllint.git
-    rev: v1.27.1
+    rev: v1.35.1
     hooks:
       - id: yamllint
         args: [--strict]
 
   - repo: https://github.com/markdownlint/markdownlint
-    rev: v0.11.0
+    rev: v0.12.0
     hooks:
       - id: markdownlint
-        args: [-r, "~MD013,~MD029"]
-        exclude: "^.git"
+        exclude: "^.github|(^docs/_sidebar\\.md$)"
 
-  - repo: https://github.com/jumanjihouse/pre-commit-hooks
-    rev: 3.0.0
+  - repo: https://github.com/shellcheck-py/shellcheck-py
+    rev: v0.10.0.1
     hooks:
       - id: shellcheck
-        args: [--severity, "error"]
+        args: ["--severity=error"]
         exclude: "^.git"
         files: "\\.sh$"
 
-  - repo: local
+  - repo: https://github.com/ansible/ansible-lint
+    rev: v24.5.0
     hooks:
       - id: ansible-lint
-        name: ansible-lint
-        entry: ansible-lint -v
-        language: python
-        pass_filenames: false
         additional_dependencies:
-          - .[community]
+          - ansible==9.5.1
+          - jsonschema==4.22.0
+          - jmespath==1.0.1
+          - netaddr==1.2.1
 
+  - repo: https://github.com/VannTen/misspell
+    # Waiting on https://github.com/golangci/misspell/pull/19 to get merged
+    rev: 8592a4e
+    hooks:
+      - id: misspell
+        exclude: "OWNERS_ALIASES$"
+
+  - repo: local
+    hooks:
       - id: ansible-syntax-check
         name: ansible-syntax-check
         entry: env ANSIBLE_INVENTORY=inventory/local-tests.cfg ANSIBLE_REMOTE_USER=root ANSIBLE_BECOME="true" ANSIBLE_BECOME_USER=root ANSIBLE_VERBOSITY="3" ansible-playbook --syntax-check
         language: python
         files: "^cluster.yml|^upgrade-cluster.yml|^reset.yml|^extra_playbooks/upgrade-only-k8s.yml"
+        additional_dependencies:
+          - ansible==9.5.1
 
       - id: tox-inventory-builder
         name: tox-inventory-builder
         entry: bash -c "cd contrib/inventory_builder && tox"
         language: python
         pass_filenames: false
+        additional_dependencies:
+          - tox==4.15.0
 
       - id: check-readme-versions
         name: check-readme-versions
@@ -63,6 +75,14 @@ repos:
         language: script
         pass_filenames: false
 
+      - id: collection-build-install
+        name: Build and install kubernetes-sigs.kubespray Ansible collection
+        language: python
+        additional_dependencies:
+          - ansible-core>=2.16.4
+        entry: tests/scripts/collection-build-install.sh
+        pass_filenames: false
+
       - id: generate-docs-sidebar
         name: generate-docs-sidebar
         entry: scripts/gen_docs_sidebar.sh
@@ -71,9 +91,13 @@ repos:
 
       - id: ci-matrix
         name: ci-matrix
-        entry: tests/scripts/md-table/test.sh
-        language: script
+        entry: tests/scripts/md-table/main.py
+        language: python
         pass_filenames: false
+        additional_dependencies:
+          - jinja2
+          - pathlib
+          - pyaml
 
       - id: jinja-syntax-check
         name: jinja-syntax-check
@@ -82,4 +106,4 @@ repos:
         types:
           - jinja
         additional_dependencies:
-          - Jinja2
+          - jinja2
diff --git a/contrib/terraform/nifcloud/README.md b/contrib/terraform/nifcloud/README.md
index 8c46df402f52bb9044d6ab20591736a8b29b0bb8..a6dcf014855f44b917124b003ab8937ef5269c97 100644
--- a/contrib/terraform/nifcloud/README.md
+++ b/contrib/terraform/nifcloud/README.md
@@ -72,6 +72,7 @@ The setup looks like following
 
   ```bash
   ./generate-inventory.sh > sample-inventory/inventory.ini
+  ```
 
 * Export Variables:
 
diff --git a/contrib/terraform/upcloud/cluster-settings.tfvars b/contrib/terraform/upcloud/cluster-settings.tfvars
index 45a374900f7a1a03dc2b8c3ecb3ee56372550d5e..eae1551e221d6c7fdbc1714b97c19cb679fbd849 100644
--- a/contrib/terraform/upcloud/cluster-settings.tfvars
+++ b/contrib/terraform/upcloud/cluster-settings.tfvars
@@ -146,4 +146,4 @@ server_groups = {
   #   ]
   #   anti_affinity_policy = "yes"
   # }
-}
\ No newline at end of file
+}
diff --git a/contrib/terraform/upcloud/modules/kubernetes-cluster/main.tf b/contrib/terraform/upcloud/modules/kubernetes-cluster/main.tf
index 2adeb44678f87d27e93c5609490f38c462168807..9639bdeae87ee81d0ded5930ea4c1bac209db96d 100644
--- a/contrib/terraform/upcloud/modules/kubernetes-cluster/main.tf
+++ b/contrib/terraform/upcloud/modules/kubernetes-cluster/main.tf
@@ -558,4 +558,4 @@ resource "upcloud_server_group" "server_groups" {
   anti_affinity_policy = each.value.anti_affinity_policy
   labels               = {}
   members              = [for server in each.value.servers : merge(upcloud_server.master, upcloud_server.worker)[server].id]
-}
\ No newline at end of file
+}
diff --git a/contrib/terraform/upcloud/modules/kubernetes-cluster/variables.tf b/contrib/terraform/upcloud/modules/kubernetes-cluster/variables.tf
index ad2cc70f020c9dea5f46e84dbee4b7b5c150fdf4..87e5e5370635cdf1b2cbbd729b35a31417a4d8be 100644
--- a/contrib/terraform/upcloud/modules/kubernetes-cluster/variables.tf
+++ b/contrib/terraform/upcloud/modules/kubernetes-cluster/variables.tf
@@ -106,4 +106,4 @@ variable "server_groups" {
     anti_affinity_policy = string
     servers              = list(string)
   }))
-}
\ No newline at end of file
+}
diff --git a/contrib/terraform/upcloud/sample-inventory/cluster.tfvars b/contrib/terraform/upcloud/sample-inventory/cluster.tfvars
index 58536674f68fed9aab63e2dcdf3827d7124068a4..d1546004bccde3b3165c1c5a3a79a0bdbcc8e66d 100644
--- a/contrib/terraform/upcloud/sample-inventory/cluster.tfvars
+++ b/contrib/terraform/upcloud/sample-inventory/cluster.tfvars
@@ -146,4 +146,4 @@ server_groups = {
   #   ]
   #   anti_affinity_policy = "yes"
   # }
-}
\ No newline at end of file
+}
diff --git a/docs/cloud_providers/openstack.md b/docs/cloud_providers/openstack.md
index 6f53da53444bbebdc864a393cf8ab1637a357a2a..1506be370c470f98b8d825f970a35de077b2d9c5 100644
--- a/docs/cloud_providers/openstack.md
+++ b/docs/cloud_providers/openstack.md
@@ -1,4 +1,3 @@
-
 # OpenStack
 
 ## Known compatible public clouds
diff --git a/docs/operations/recover-control-plane.md b/docs/operations/recover-control-plane.md
index 7cda08afb2bf2ffe765c6e7338ff4cd1323f4d37..d54aa13f58a58de473516267328c640a40b2416c 100644
--- a/docs/operations/recover-control-plane.md
+++ b/docs/operations/recover-control-plane.md
@@ -1,4 +1,3 @@
-
 # Recovering the control plane
 
 To recover from broken nodes in the control plane use the "recover\-control\-plane.yml" playbook.
@@ -8,7 +7,6 @@ Examples of what broken means in this context:
 * One or more bare metal node(s) suffer from unrecoverable hardware failure
 * One or more node(s) fail during patching or upgrading
 * Etcd database corruption
-  
 * Other node related failures leaving your control plane degraded or nonfunctional
 
 __Note that you need at least one functional node to be able to recover using this method.__
diff --git a/inventory/sample/group_vars/etcd.yml b/inventory/sample/group_vars/etcd.yml
index 68beeb62b2f28ad5ae64abea71edfc5e80ab91d9..66bbc0d48886703628a153209d28155771ee12a1 100644
--- a/inventory/sample/group_vars/etcd.yml
+++ b/inventory/sample/group_vars/etcd.yml
@@ -32,4 +32,4 @@
 # etcd_experimental_enable_distributed_tracing: false
 # etcd_experimental_distributed_tracing_sample_rate: 100
 # etcd_experimental_distributed_tracing_address: "localhost:4317"
-# etcd_experimental_distributed_tracing_service_name: etcd
\ No newline at end of file
+# etcd_experimental_distributed_tracing_service_name: etcd
diff --git a/requirements.txt b/requirements.txt
index 7a982b04d014c0492d478eb9b39c4aab5af6d03e..f4dba014c97cbf49bf876509fce9c3a967f815ff 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,9 +2,9 @@ ansible==9.6.0
 cryptography==42.0.7
 jinja2==3.1.4
 jmespath==1.0.1
+jsonschema==4.22.0
 MarkupSafe==2.1.5
 netaddr==1.2.1
 pbr==6.0.0
 ruamel.yaml==0.18.6
 ruamel.yaml.clib==0.2.8
-jsonschema==4.22.0
diff --git a/roles/container-engine/containerd/defaults/main.yml b/roles/container-engine/containerd/defaults/main.yml
index a6b24843c8b20874f002fe2464d4561fbffe6b9c..291e96e347f44e8960d914159dfc9c8967cf994e 100644
--- a/roles/container-engine/containerd/defaults/main.yml
+++ b/roles/container-engine/containerd/defaults/main.yml
@@ -116,4 +116,4 @@ containerd_tracing_enabled: false
 containerd_tracing_endpoint: "0.0.0.0:4317"
 containerd_tracing_protocol: "grpc"
 containerd_tracing_sampling_ratio: 1.0
-containerd_tracing_service_name: "containerd"
\ No newline at end of file
+containerd_tracing_service_name: "containerd"
diff --git a/roles/container-engine/containerd/templates/config.toml.j2 b/roles/container-engine/containerd/templates/config.toml.j2
index 8a1ee8aa53c7988d33353317089df79070a49699..fea6f7f10db676502adb12a1c810b3a5cc3f9df8 100644
--- a/roles/container-engine/containerd/templates/config.toml.j2
+++ b/roles/container-engine/containerd/templates/config.toml.j2
@@ -107,4 +107,3 @@ oom_score = {{ containerd_oom_score }}
     sampling_ratio = {{ containerd_tracing_sampling_ratio }}
     service_name = "{{ containerd_tracing_service_name }}"
 {% endif %}
-
diff --git a/roles/etcd/defaults/main.yml b/roles/etcd/defaults/main.yml
index 814caed8ebbde0f9e1832f4b75d02eebd0d289a1..fee6903df5ff2b7ca5b73ed5c396bd07d36555a7 100644
--- a/roles/etcd/defaults/main.yml
+++ b/roles/etcd/defaults/main.yml
@@ -124,4 +124,4 @@ unsafe_show_logs: false
 etcd_experimental_enable_distributed_tracing: false
 etcd_experimental_distributed_tracing_sample_rate: 100
 etcd_experimental_distributed_tracing_address: "localhost:4317"
-etcd_experimental_distributed_tracing_service_name: etcd
\ No newline at end of file
+etcd_experimental_distributed_tracing_service_name: etcd
diff --git a/roles/kubernetes-apps/csi_driver/gcp_pd/templates/gcp-pd-csi-controller.yml.j2 b/roles/kubernetes-apps/csi_driver/gcp_pd/templates/gcp-pd-csi-controller.yml.j2
index 61157d8fc60ed9f3980efdb8a4aa74a5face071f..6bdaff677266c8147f0eb7ff8a72f265c018c0e0 100644
--- a/roles/kubernetes-apps/csi_driver/gcp_pd/templates/gcp-pd-csi-controller.yml.j2
+++ b/roles/kubernetes-apps/csi_driver/gcp_pd/templates/gcp-pd-csi-controller.yml.j2
@@ -162,4 +162,4 @@ metadata:
   name: pd.csi.storage.gke.io
 spec:
   attachRequired: true
-  podInfoOnMount: false
\ No newline at end of file
+  podInfoOnMount: false
diff --git a/roles/kubernetes-apps/csi_driver/gcp_pd/templates/gcp-pd-csi-node.yml.j2 b/roles/kubernetes-apps/csi_driver/gcp_pd/templates/gcp-pd-csi-node.yml.j2
index 9aad6206936bd435931200385f22bbf1fec93b38..2992d7ff9a296ca49ef09c449908c97fc017e3f5 100644
--- a/roles/kubernetes-apps/csi_driver/gcp_pd/templates/gcp-pd-csi-node.yml.j2
+++ b/roles/kubernetes-apps/csi_driver/gcp_pd/templates/gcp-pd-csi-node.yml.j2
@@ -109,4 +109,4 @@ spec:
       # See "special case". This will tolerate everything. Node component should
       # be scheduled on all nodes.
       tolerations:
-      - operator: Exists
\ No newline at end of file
+      - operator: Exists
diff --git a/roles/kubernetes-apps/csi_driver/gcp_pd/templates/gcp-pd-csi-sc-regional.yml.j2 b/roles/kubernetes-apps/csi_driver/gcp_pd/templates/gcp-pd-csi-sc-regional.yml.j2
index 57a8675e451855042b9e890eec603701aac74884..fa2e5a81f1f99cf76a00597a41082f8d7d32dba1 100644
--- a/roles/kubernetes-apps/csi_driver/gcp_pd/templates/gcp-pd-csi-sc-regional.yml.j2
+++ b/roles/kubernetes-apps/csi_driver/gcp_pd/templates/gcp-pd-csi-sc-regional.yml.j2
@@ -6,4 +6,4 @@ provisioner: pd.csi.storage.gke.io
 parameters:
   type: pd-balanced
   replication-type: regional-pd
-volumeBindingMode: WaitForFirstConsumer
\ No newline at end of file
+volumeBindingMode: WaitForFirstConsumer
diff --git a/roles/kubernetes-apps/csi_driver/gcp_pd/templates/gcp-pd-csi-sc-zonal.yml.j2 b/roles/kubernetes-apps/csi_driver/gcp_pd/templates/gcp-pd-csi-sc-zonal.yml.j2
index e9bedaf83c52077d7d552c50055230ae5aa32cdc..dc530162573d66a781306cd5eb520b55edcbbed8 100644
--- a/roles/kubernetes-apps/csi_driver/gcp_pd/templates/gcp-pd-csi-sc-zonal.yml.j2
+++ b/roles/kubernetes-apps/csi_driver/gcp_pd/templates/gcp-pd-csi-sc-zonal.yml.j2
@@ -5,4 +5,4 @@ metadata:
 provisioner: pd.csi.storage.gke.io
 parameters:
   type: pd-balanced
-volumeBindingMode: WaitForFirstConsumer
\ No newline at end of file
+volumeBindingMode: WaitForFirstConsumer
diff --git a/roles/kubernetes-apps/csi_driver/vsphere/templates/vsphere-csi-controller-config.yml.j2 b/roles/kubernetes-apps/csi_driver/vsphere/templates/vsphere-csi-controller-config.yml.j2
index fb52d107e47d87812249f2005be37b01f9e43cdd..274889604c4bb4ccf3bd462546c9b594b878ce26 100644
--- a/roles/kubernetes-apps/csi_driver/vsphere/templates/vsphere-csi-controller-config.yml.j2
+++ b/roles/kubernetes-apps/csi_driver/vsphere/templates/vsphere-csi-controller-config.yml.j2
@@ -18,7 +18,7 @@ data:
   "max-pvscsi-targets-per-vm": "true"
   "multi-vcenter-csi-topology": "true"
   "csi-internal-generated-cluster-id": "true"
-  "listview-tasks": "true" 
+  "listview-tasks": "true"
 {% if vsphere_csi_controller is version('v2.7.0', '>=') %}
   "improved-csi-idempotency": "true"
   "improved-volume-topology": "true"
diff --git a/roles/kubernetes-apps/external_cloud_controller/huaweicloud/templates/external-huawei-cloud-controller-manager-role-bindings.yml.j2 b/roles/kubernetes-apps/external_cloud_controller/huaweicloud/templates/external-huawei-cloud-controller-manager-role-bindings.yml.j2
index 3c893f3faffc80606972dcfe3f9646c60e5cc263..65dfefaa4df14199590709f7fbcb3c34a87f9aee 100644
--- a/roles/kubernetes-apps/external_cloud_controller/huaweicloud/templates/external-huawei-cloud-controller-manager-role-bindings.yml.j2
+++ b/roles/kubernetes-apps/external_cloud_controller/huaweicloud/templates/external-huawei-cloud-controller-manager-role-bindings.yml.j2
@@ -9,4 +9,4 @@ roleRef:
 subjects:
   - kind: ServiceAccount
     name: cloud-controller-manager
-    namespace: kube-system
\ No newline at end of file
+    namespace: kube-system
diff --git a/roles/kubernetes-apps/external_cloud_controller/huaweicloud/templates/external-huawei-cloud-controller-manager-roles.yml.j2 b/roles/kubernetes-apps/external_cloud_controller/huaweicloud/templates/external-huawei-cloud-controller-manager-roles.yml.j2
index d2710e960b59161f2052ea7c57b3169d9bf9169d..ccb7c0bfa7ea78373012bb00da8cbff35fc4ac55 100644
--- a/roles/kubernetes-apps/external_cloud_controller/huaweicloud/templates/external-huawei-cloud-controller-manager-roles.yml.j2
+++ b/roles/kubernetes-apps/external_cloud_controller/huaweicloud/templates/external-huawei-cloud-controller-manager-roles.yml.j2
@@ -110,4 +110,4 @@ rules:
       - list
       - watch
     apiGroups:
-      - discovery.k8s.io
\ No newline at end of file
+      - discovery.k8s.io
diff --git a/roles/kubernetes-apps/external_provisioner/local_path_provisioner/templates/local-path-storage-cm.yml.j2 b/roles/kubernetes-apps/external_provisioner/local_path_provisioner/templates/local-path-storage-cm.yml.j2
index 9cd7fd3ff3b26661922cb1636ecfa5713b695131..a0b0bc1238fa23236f383dd0861eff41780cf011 100644
--- a/roles/kubernetes-apps/external_provisioner/local_path_provisioner/templates/local-path-storage-cm.yml.j2
+++ b/roles/kubernetes-apps/external_provisioner/local_path_provisioner/templates/local-path-storage-cm.yml.j2
@@ -32,4 +32,3 @@ data:
       - name: helper-pod
         image: "{{ local_path_provisioner_helper_image_repo }}:{{ local_path_provisioner_helper_image_tag }}"
         imagePullPolicy: IfNotPresent
-
diff --git a/roles/kubernetes-apps/external_provisioner/local_path_provisioner/templates/local-path-storage-cr.yml.j2 b/roles/kubernetes-apps/external_provisioner/local_path_provisioner/templates/local-path-storage-cr.yml.j2
index 299db6eba861ea75dd8805bf1f691e2a9f11244d..2b53ba72d6e030d79bc35e0145f66f0de004cf30 100644
--- a/roles/kubernetes-apps/external_provisioner/local_path_provisioner/templates/local-path-storage-cr.yml.j2
+++ b/roles/kubernetes-apps/external_provisioner/local_path_provisioner/templates/local-path-storage-cr.yml.j2
@@ -15,4 +15,4 @@ rules:
     verbs: [ "create", "patch" ]
   - apiGroups: [ "storage.k8s.io" ]
     resources: [ "storageclasses" ]
-    verbs: [ "get", "list", "watch" ]
\ No newline at end of file
+    verbs: [ "get", "list", "watch" ]
diff --git a/roles/kubernetes-apps/metallb/defaults/main.yml b/roles/kubernetes-apps/metallb/defaults/main.yml
index 02f4e3cae944567b15d6d5f8e6a41658fa8181f1..c83b293d9f25d5a890be396ece5ebbdc23be09d1 100644
--- a/roles/kubernetes-apps/metallb/defaults/main.yml
+++ b/roles/kubernetes-apps/metallb/defaults/main.yml
@@ -13,4 +13,4 @@ metallb_speaker_tolerations:
     key: node-role.kubernetes.io/control-plane
     operator: Exists
 metallb_controller_tolerations: []
-metallb_loadbalancer_class: ""
\ No newline at end of file
+metallb_loadbalancer_class: ""
diff --git a/roles/kubernetes-apps/node_feature_discovery/templates/nfd-rolebinding.yaml.j2 b/roles/kubernetes-apps/node_feature_discovery/templates/nfd-rolebinding.yaml.j2
index 5493087619c1996f8665f4842a0ec835b33c9216..04a5786967a0c6991db5a7c4d0af3826178d2652 100644
--- a/roles/kubernetes-apps/node_feature_discovery/templates/nfd-rolebinding.yaml.j2
+++ b/roles/kubernetes-apps/node_feature_discovery/templates/nfd-rolebinding.yaml.j2
@@ -11,4 +11,3 @@ subjects:
 - kind: ServiceAccount
   name: {{ node_feature_discovery_worker_sa_name }}
   namespace: {{ node_feature_discovery_namespace }}
-
diff --git a/roles/kubernetes-apps/scheduler_plugins/templates/appgroup.diktyo.x-k8s.io_appgroups.yaml.j2 b/roles/kubernetes-apps/scheduler_plugins/templates/appgroup.diktyo.x-k8s.io_appgroups.yaml.j2
index 757a3b12d393a01b6451bb3f24dd2740a6da42c1..10c30c799769ececab58a4c3acb55887a955bc74 100644
--- a/roles/kubernetes-apps/scheduler_plugins/templates/appgroup.diktyo.x-k8s.io_appgroups.yaml.j2
+++ b/roles/kubernetes-apps/scheduler_plugins/templates/appgroup.diktyo.x-k8s.io_appgroups.yaml.j2
@@ -194,4 +194,4 @@ spec:
             type: object
         type: object
     served: true
-    storage: true
\ No newline at end of file
+    storage: true
diff --git a/roles/kubernetes-apps/scheduler_plugins/templates/cm-scheduler-plugins.yaml.j2 b/roles/kubernetes-apps/scheduler_plugins/templates/cm-scheduler-plugins.yaml.j2
index 7e022e8895b706b42a7f92069b56bb48c4b561fd..4b5e0248a02626688c2635c66b6f26d4de21510f 100644
--- a/roles/kubernetes-apps/scheduler_plugins/templates/cm-scheduler-plugins.yaml.j2
+++ b/roles/kubernetes-apps/scheduler_plugins/templates/cm-scheduler-plugins.yaml.j2
@@ -25,4 +25,4 @@ data:
 {% if scheduler_plugins_plugin_config is defined and scheduler_plugins_plugin_config | length != 0 %}
       pluginConfig:
 {{ scheduler_plugins_plugin_config | to_nice_yaml(indent=2, width=256) | indent(6, true) }}
-{% endif %}
\ No newline at end of file
+{% endif %}
diff --git a/roles/kubernetes-apps/scheduler_plugins/templates/deploy-scheduler-plugins.yaml.j2 b/roles/kubernetes-apps/scheduler_plugins/templates/deploy-scheduler-plugins.yaml.j2
index 114698a9419495f7c898180f937caf26bacde022..1ded700969ecd04fc06eb3d4b37bd7a70316bdd7 100644
--- a/roles/kubernetes-apps/scheduler_plugins/templates/deploy-scheduler-plugins.yaml.j2
+++ b/roles/kubernetes-apps/scheduler_plugins/templates/deploy-scheduler-plugins.yaml.j2
@@ -71,4 +71,4 @@ spec:
       volumes:
       - name: scheduler-config
         configMap:
-          name: scheduler-config
\ No newline at end of file
+          name: scheduler-config
diff --git a/roles/kubernetes-apps/scheduler_plugins/templates/namespace.yaml.j2 b/roles/kubernetes-apps/scheduler_plugins/templates/namespace.yaml.j2
index d54ae66fd80e182138fde1565e781baf0c402589..41b0806fef63ae9c1726d14cf78b45c20040fec2 100644
--- a/roles/kubernetes-apps/scheduler_plugins/templates/namespace.yaml.j2
+++ b/roles/kubernetes-apps/scheduler_plugins/templates/namespace.yaml.j2
@@ -4,4 +4,4 @@ kind: Namespace
 metadata:
   name: {{ scheduler_plugins_namespace }}
   labels:
-    name: {{ scheduler_plugins_namespace }}
\ No newline at end of file
+    name: {{ scheduler_plugins_namespace }}
diff --git a/roles/kubernetes-apps/scheduler_plugins/templates/networktopology.diktyo.x-k8s.io_networktopologies.yaml.j2 b/roles/kubernetes-apps/scheduler_plugins/templates/networktopology.diktyo.x-k8s.io_networktopologies.yaml.j2
index e33157c0f2feaba912cd8fcc68b21721073a900a..7e562f84792473d5c2a16285522c42a932621a09 100644
--- a/roles/kubernetes-apps/scheduler_plugins/templates/networktopology.diktyo.x-k8s.io_networktopologies.yaml.j2
+++ b/roles/kubernetes-apps/scheduler_plugins/templates/networktopology.diktyo.x-k8s.io_networktopologies.yaml.j2
@@ -145,4 +145,4 @@ spec:
             type: object
         type: object
     served: true
-    storage: true
\ No newline at end of file
+    storage: true
diff --git a/roles/kubernetes-apps/scheduler_plugins/templates/rbac-scheduler-plugins.yaml.j2 b/roles/kubernetes-apps/scheduler_plugins/templates/rbac-scheduler-plugins.yaml.j2
index aa6f211d7de62d3b87f4448332383bb234329c60..8e86f6bff2d4e0b8875729c374f31c26c8aecd8e 100644
--- a/roles/kubernetes-apps/scheduler_plugins/templates/rbac-scheduler-plugins.yaml.j2
+++ b/roles/kubernetes-apps/scheduler_plugins/templates/rbac-scheduler-plugins.yaml.j2
@@ -137,4 +137,4 @@ subjects:
   namespace: {{ scheduler_plugins_namespace }}
 - kind: ServiceAccount
   name: scheduler-plugins-controller
-  namespace: {{ scheduler_plugins_namespace }}
\ No newline at end of file
+  namespace: {{ scheduler_plugins_namespace }}
diff --git a/roles/kubernetes-apps/scheduler_plugins/templates/sa-scheduler-plugins.yaml.j2 b/roles/kubernetes-apps/scheduler_plugins/templates/sa-scheduler-plugins.yaml.j2
index 6c25e18090ca1d9a8c0d1a8824df0baf042f2415..7cefdb1847260028e6248849b949c4d6d17bca59 100644
--- a/roles/kubernetes-apps/scheduler_plugins/templates/sa-scheduler-plugins.yaml.j2
+++ b/roles/kubernetes-apps/scheduler_plugins/templates/sa-scheduler-plugins.yaml.j2
@@ -8,4 +8,4 @@ apiVersion: v1
 kind: ServiceAccount
 metadata:
   name: scheduler-plugins-controller
-  namespace: {{ scheduler_plugins_namespace }}
\ No newline at end of file
+  namespace: {{ scheduler_plugins_namespace }}
diff --git a/roles/kubernetes-apps/scheduler_plugins/templates/scheduling.x-k8s.io_elasticquotas.yaml.j2 b/roles/kubernetes-apps/scheduler_plugins/templates/scheduling.x-k8s.io_elasticquotas.yaml.j2
index d63f57209f44196d1bd39ec9dbb02ccbaa1289c0..e8f64c35497fae0d4df6d817c1e24c96a9b7d2aa 100644
--- a/roles/kubernetes-apps/scheduler_plugins/templates/scheduling.x-k8s.io_elasticquotas.yaml.j2
+++ b/roles/kubernetes-apps/scheduler_plugins/templates/scheduling.x-k8s.io_elasticquotas.yaml.j2
@@ -79,4 +79,4 @@ spec:
     served: true
     storage: true
     subresources:
-      status: {}
\ No newline at end of file
+      status: {}
diff --git a/roles/kubernetes-apps/scheduler_plugins/templates/scheduling.x-k8s.io_podgroups.yaml.j2 b/roles/kubernetes-apps/scheduler_plugins/templates/scheduling.x-k8s.io_podgroups.yaml.j2
index 3767cf962916ed60e07dc1cb748ff3af3a01f76b..a0790dc71cbc46a50aca37f8382a45fb76bdc0cd 100644
--- a/roles/kubernetes-apps/scheduler_plugins/templates/scheduling.x-k8s.io_podgroups.yaml.j2
+++ b/roles/kubernetes-apps/scheduler_plugins/templates/scheduling.x-k8s.io_podgroups.yaml.j2
@@ -94,4 +94,4 @@ spec:
     served: true
     storage: true
     subresources:
-      status: {}
\ No newline at end of file
+      status: {}
diff --git a/roles/kubernetes-apps/scheduler_plugins/templates/topology.node.k8s.io_noderesourcetopologies.yaml.j2 b/roles/kubernetes-apps/scheduler_plugins/templates/topology.node.k8s.io_noderesourcetopologies.yaml.j2
index d83ef0b9b5c2935594400f89450e9a4d4c9da056..567432f3f59338b8800decbbf84099a7983379b1 100644
--- a/roles/kubernetes-apps/scheduler_plugins/templates/topology.node.k8s.io_noderesourcetopologies.yaml.j2
+++ b/roles/kubernetes-apps/scheduler_plugins/templates/topology.node.k8s.io_noderesourcetopologies.yaml.j2
@@ -150,4 +150,4 @@ spec:
         - zones
         type: object
     served: true
-    storage: true
\ No newline at end of file
+    storage: true
diff --git a/roles/kubernetes/control-plane/templates/apiserver-tracing.yaml.j2 b/roles/kubernetes/control-plane/templates/apiserver-tracing.yaml.j2
index 98decde86b8561debe1b676c6f7a7cef9606fd5d..7301a354d5e592a0007fbb9f1a9509da1f50b826 100644
--- a/roles/kubernetes/control-plane/templates/apiserver-tracing.yaml.j2
+++ b/roles/kubernetes/control-plane/templates/apiserver-tracing.yaml.j2
@@ -1,4 +1,4 @@
 apiVersion: apiserver.config.k8s.io/v1beta1
 kind: TracingConfiguration
 endpoint: {{ kube_apiserver_tracing_endpoint }}
-samplingRatePerMillion: {{ kube_apiserver_tracing_sampling_rate_per_million }}
\ No newline at end of file
+samplingRatePerMillion: {{ kube_apiserver_tracing_sampling_rate_per_million }}
diff --git a/roles/kubernetes/node/templates/kubelet-config.v1beta1.yaml.j2 b/roles/kubernetes/node/templates/kubelet-config.v1beta1.yaml.j2
index 1a664a0edf7bb17d1d7ea43235e03cb48b337785..d45ede27214a6bad959d2957c0736e0518e54080 100644
--- a/roles/kubernetes/node/templates/kubelet-config.v1beta1.yaml.j2
+++ b/roles/kubernetes/node/templates/kubelet-config.v1beta1.yaml.j2
@@ -174,4 +174,4 @@ topologyManagerScope: {{ kubelet_topology_manager_scope }}
 tracing:
   endpoint: {{ kubelet_tracing_endpoint }}
   samplingRatePerMillion: {{ kubelet_tracing_sampling_rate_per_million }}
-{% endif %}
\ No newline at end of file
+{% endif %}
diff --git a/roles/network_plugin/calico/templates/calico-config.yml.j2 b/roles/network_plugin/calico/templates/calico-config.yml.j2
index 26983ecaed4f5592460ecbfb18a0da14d9415004..d949af1ec6bd4f7eafb9af1bf4944fdd522f8106 100644
--- a/roles/network_plugin/calico/templates/calico-config.yml.j2
+++ b/roles/network_plugin/calico/templates/calico-config.yml.j2
@@ -102,4 +102,3 @@ data:
         }
       ]
     }
-
diff --git a/roles/network_plugin/cilium/templates/cilium/config.yml.j2 b/roles/network_plugin/cilium/templates/cilium/config.yml.j2
index d294c6e291d97c01bf6ba3ac33389daa14ce0e8b..bdb07212bc58f280c63dbbb7d0008c210f4b7f36 100644
--- a/roles/network_plugin/cilium/templates/cilium/config.yml.j2
+++ b/roles/network_plugin/cilium/templates/cilium/config.yml.j2
@@ -134,7 +134,7 @@ data:
   ## DSR setting
   bpf-lb-mode: "{{ cilium_loadbalancer_mode }}"
 
-  # l2 
+  # l2
   enable-l2-announcements: "{{ cilium_l2announcements }}"
 
   # Enable Bandwidth Manager
diff --git a/roles/network_plugin/cilium/templates/cilium/cr.yml.j2 b/roles/network_plugin/cilium/templates/cilium/cr.yml.j2
index a4395b242035af4e6415993ce8c1db851f2cf2b6..833076de140d406374b4fda3c86c502311923c10 100644
--- a/roles/network_plugin/cilium/templates/cilium/cr.yml.j2
+++ b/roles/network_plugin/cilium/templates/cilium/cr.yml.j2
@@ -140,7 +140,7 @@ rules:
   verbs:
   - list
   - watch
-{% if cilium_version %} 
+{% if cilium_version %}
 - apiGroups:
   - coordination.k8s.io
   resources:
diff --git a/roles/network_plugin/cilium/templates/hubble/config.yml.j2 b/roles/network_plugin/cilium/templates/hubble/config.yml.j2
index 888db41242d1d65ee3dd16fd934d918f1e440a81..f3af717411045e1276de5f736ab91317c5fa5c98 100644
--- a/roles/network_plugin/cilium/templates/hubble/config.yml.j2
+++ b/roles/network_plugin/cilium/templates/hubble/config.yml.j2
@@ -12,10 +12,10 @@ data:
     peer-service: "hubble-peer.kube-system.svc.{{ dns_domain }}:443"
     listen-address: :4245
     metrics-listen-address: ":9966"
-    dial-timeout: 
-    retry-timeout: 
-    sort-buffer-len-max: 
-    sort-buffer-drain-timeout: 
+    dial-timeout:
+    retry-timeout:
+    sort-buffer-len-max:
+    sort-buffer-drain-timeout:
     tls-client-cert-file: /var/lib/hubble-relay/tls/client.crt
     tls-client-key-file: /var/lib/hubble-relay/tls/client.key
     tls-server-cert-file: /var/lib/hubble-relay/tls/server.crt
diff --git a/roles/network_plugin/cilium/templates/hubble/service.yml.j2 b/roles/network_plugin/cilium/templates/hubble/service.yml.j2
index 0f862a9c5eea1be6d4a865224ce4f3f16224e9db..48e90b82518c136a8968cc4a9ba1cee11f629e93 100644
--- a/roles/network_plugin/cilium/templates/hubble/service.yml.j2
+++ b/roles/network_plugin/cilium/templates/hubble/service.yml.j2
@@ -102,4 +102,3 @@ spec:
     protocol: TCP
     targetPort: 4244
   internalTrafficPolicy: Local
-
diff --git a/roles/network_plugin/kube-ovn/templates/cni-kube-ovn-crd.yml.j2 b/roles/network_plugin/kube-ovn/templates/cni-kube-ovn-crd.yml.j2
index 379381d681ba17ed33dc4b8dc02f94bcdd78fd2b..8040cc77bd31952b2661511d1748048e9908af05 100644
--- a/roles/network_plugin/kube-ovn/templates/cni-kube-ovn-crd.yml.j2
+++ b/roles/network_plugin/kube-ovn/templates/cni-kube-ovn-crd.yml.j2
@@ -1530,4 +1530,4 @@ spec:
       subresources:
         status: {}
   conversion:
-    strategy: None
\ No newline at end of file
+    strategy: None
diff --git a/scale.yml b/scale.yml
index b78fc69fdcfa6fa3e72ce8f1213e884345f6f5c2..3eebaee892c5640edf5f014f134dd61d8e013c58 100644
--- a/scale.yml
+++ b/scale.yml
@@ -1,3 +1,3 @@
 ---
 - name: Scale the cluster
-  ansible.builtin.import_playbook: playbooks/scale.yml
\ No newline at end of file
+  ansible.builtin.import_playbook: playbooks/scale.yml
diff --git a/scripts/openstack-cleanup/main.py b/scripts/openstack-cleanup/main.py
index 2ddccc067f57ada9d0fb1c68433208f01a1794e4..ba3d4586e5fd0e74af4f03fd62aa29cfb0902e15 100755
--- a/scripts/openstack-cleanup/main.py
+++ b/scripts/openstack-cleanup/main.py
@@ -61,7 +61,7 @@ def main():
 
         for ip in conn.network.ips():
             fn_if_old(conn.network.delete_ip, ip)
-                
+
         # After removing unnecessary subnet from router, retry to delete ports
         map_if_old(conn.network.delete_port,
                    conn.network.ports())
diff --git a/tests/files/vagrant_ubuntu20-flannel-collection.rb b/tests/files/vagrant_ubuntu20-flannel-collection.rb
index c739f58a2aba8ee87a89a2d481b5ec6630a07cfd..55daa19e3eb084297a46df3edf63e01c89631b96 100644
--- a/tests/files/vagrant_ubuntu20-flannel-collection.rb
+++ b/tests/files/vagrant_ubuntu20-flannel-collection.rb
@@ -6,4 +6,4 @@ $libvirt_volume_cache = "unsafe"
 # Checking for box update can trigger API rate limiting
 # https://www.vagrantup.com/docs/vagrant-cloud/request-limits.html
 $box_check_update = false
-$vm_cpus = 2
\ No newline at end of file
+$vm_cpus = 2
diff --git a/tests/requirements.txt b/tests/requirements.txt
index a9e1e7e3f2159ae546752674de7625fa776ea4ff..43eef23a3d9b46d0b60b6d50788aa7303ef6b6c7 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -5,8 +5,8 @@ ara[server]==1.7.1
 dopy==0.3.7
 molecule==24.2.1
 molecule-plugins[vagrant]==23.5.3
-python-vagrant==1.0.0
 pytest-testinfra==10.1.0
+python-vagrant==1.0.0
 tox==4.15.0
-yamllint==1.35.1
 tzdata==2024.1
+yamllint==1.35.1
diff --git a/tests/scripts/check_typo.sh b/tests/scripts/check_typo.sh
deleted file mode 100755
index 522d4b2d60c76b38387abf0640a76f838b7c4775..0000000000000000000000000000000000000000
--- a/tests/scripts/check_typo.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/bin/bash
-
-# cd to the root directory of kubespray
-cd $(dirname $0)/../../
-
-rm ./misspell*
-
-set -e
-wget https://github.com/client9/misspell/releases/download/v0.3.4/misspell_0.3.4_linux_64bit.tar.gz
-tar -zxvf ./misspell_0.3.4_linux_64bit.tar.gz
-chmod 755 ./misspell
-git ls-files | grep -v OWNERS_ALIASES | xargs ./misspell -error
diff --git a/tests/scripts/collection-build-install.sh b/tests/scripts/collection-build-install.sh
new file mode 100755
index 0000000000000000000000000000000000000000..6419850d7a76cc66040ea960e83d5c1013683cba
--- /dev/null
+++ b/tests/scripts/collection-build-install.sh
@@ -0,0 +1,7 @@
+#!/bin/sh -e
+export ANSIBLE_COLLECTIONS_PATH="./ansible_collections"
+ansible-galaxy collection build --force
+ansible-galaxy collection install kubernetes_sigs-kubespray-$(grep "^version:" galaxy.yml | awk '{print $2}').tar.gz
+ansible-galaxy collection list $(egrep -i '(name:\s+|namespace:\s+)' galaxy.yml | awk '{print $2}' | tr '\n' '.' | sed 's|\.$||g') | grep "^kubernetes_sigs.kubespray"
+test -f ansible_collections/kubernetes_sigs/kubespray/playbooks/cluster.yml
+test -f ansible_collections/kubernetes_sigs/kubespray/playbooks/reset.yml
diff --git a/tests/scripts/md-table/main.py b/tests/scripts/md-table/main.py
index 9e00005856405b8c1eb4a0cf116242a75585f594..09d5506e9bb42e4313676feddb265f3597a76302 100755
--- a/tests/scripts/md-table/main.py
+++ b/tests/scripts/md-table/main.py
@@ -4,7 +4,6 @@ import sys
 import glob
 from pathlib import Path
 import yaml
-from pydblite import Base
 import re
 import jinja2
 import sys
@@ -14,6 +13,7 @@ from pprint import pprint
 
 parser = argparse.ArgumentParser(description='Generate a Markdown table representing the CI test coverage')
 parser.add_argument('--dir', default='tests/files/', help='folder with test yml files')
+parser.add_argument('--output', default='docs/developers/ci.md', help='output file')
 
 
 args = parser.parse_args()
@@ -24,25 +24,26 @@ env = jinja2.Environment(loader=jinja2.FileSystemLoader(searchpath=sys.path[0]))
 # Data represents CI coverage data matrix
 class Data:
     def __init__(self):
-        self.db = Base(':memory:')
-        self.db.create('container_manager', 'network_plugin', 'operating_system')
+        self.container_managers = set()
+        self.network_plugins = set()
+        self.os = set()
+        self.combination = set()
 
 
-    def set(self, container_manager, network_plugin, operating_system):
-        self.db.insert(container_manager=container_manager, network_plugin=network_plugin, operating_system=operating_system)
-        self.db.commit()
-    def exists(self, container_manager, network_plugin, operating_system):
-        return len((self.db("container_manager") == container_manager) & (self.db("network_plugin") == network_plugin) & (self.db("operating_system") == operating_system)) > 0
+    def set(self, container_manager, network_plugin, os):
+        self.container_managers.add(container_manager)
+        self.network_plugins.add(network_plugin)
+        self.os.add(os)
+        self.combination.add(container_manager+network_plugin+os)
+
+    def exists(self, container_manager, network_plugin, os):
+        return (container_manager+network_plugin+os) in self.combination
 
     def jinja(self):
         template = env.get_template('table.md.j2')
-        container_engines = list(self.db.get_unique_ids('container_manager'))
-        network_plugins = list(self.db.get_unique_ids("network_plugin"))
-        operating_systems = list(self.db.get_unique_ids("operating_system"))
-
-        container_engines.sort()
-        network_plugins.sort()
-        operating_systems.sort()
+        container_engines = sorted(self.container_managers)
+        network_plugins = sorted(self.network_plugins)
+        operating_systems = sorted(self.os)
 
         return template.render(
             container_engines=container_engines,
@@ -91,6 +92,5 @@ for f in files:
     network_plugin = y.get('kube_network_plugin', 'calico')
     x = re.match(r"^[a-z-]+_([a-z0-9]+).*", f.name)
     operating_system = x.group(1)
-    data.set(container_manager=container_manager, network_plugin=network_plugin, operating_system=operating_system)
-#print(data.markdown())
-print(data.jinja())
+    data.set(container_manager=container_manager, network_plugin=network_plugin, os=operating_system)
+print(data.jinja(), file=open(args.output, 'w'))
diff --git a/tests/scripts/md-table/requirements.txt b/tests/scripts/md-table/requirements.txt
deleted file mode 100644
index 6d4aca36aa541c9dd7154be2e0c6fb09e7437c59..0000000000000000000000000000000000000000
--- a/tests/scripts/md-table/requirements.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-jinja2
-pathlib ; python_version < '3.10'
-pyaml
-pydblite
diff --git a/tests/scripts/md-table/test.sh b/tests/scripts/md-table/test.sh
deleted file mode 100755
index cf9df90856dccbc7985bda0dab2f67ee7f651eac..0000000000000000000000000000000000000000
--- a/tests/scripts/md-table/test.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/bash
-set -euxo pipefail
-
-echo "Install requirements..."
-pip install -r ./tests/scripts/md-table/requirements.txt
-
-echo "Generate current file..."
-./tests/scripts/md-table/main.py > tmp.md
-
-echo "Compare docs/developers/ci.md with actual tests in tests/files/*.yml ..."
-cmp docs/developers/ci.md tmp.md