diff --git a/utils/build-galaxy-release.sh b/utils/build-galaxy-release.sh
index bbce0122b37d2fad90e699225004a78e19f3deb6..e3eb0b9c4e5f2c0e6c490e21f8aa9b0472d59fef 100755
--- a/utils/build-galaxy-release.sh
+++ b/utils/build-galaxy-release.sh
@@ -114,6 +114,8 @@ echo -e "\033[ACreating CHANGELOG.rst... \033[32;1mDONE\033[0m"
 
 sed -i -e "s/ansible.module_utils.ansible_freeipa_module/ansible_collections.${collection_prefix}.plugins.module_utils.ansible_freeipa_module/" plugins/modules/*.py
 
+python utils/create_action_group.py "meta/runtime.yml" "$collection_prefix"
+
 (cd plugins/module_utils && {
     ln -sf ../../roles/*/module_utils/*.py .
 })
diff --git a/utils/create_action_group.py b/utils/create_action_group.py
new file mode 100644
index 0000000000000000000000000000000000000000..6f6cec2658d0e0e6a12a18728e431f592d8affba
--- /dev/null
+++ b/utils/create_action_group.py
@@ -0,0 +1,24 @@
+import sys
+import yaml
+from facts import MANAGEMENT_MODULES
+
+
+def create_action_group(yml_file, project_prefix):
+    yaml_data = None
+    with open(yml_file) as f_in:
+        yaml_data = yaml.safe_load(f_in)
+
+    yaml_data.setdefault("action_groups", {})[
+        "%s.modules" % project_prefix
+    ] = MANAGEMENT_MODULES
+
+    with open(yml_file, 'w') as f_out:
+        yaml.safe_dump(yaml_data, f_out, default_flow_style=False,
+                       explicit_start=True)
+
+
+if len(sys.argv) != 3:
+    print("Usage: %s <runtime file> <collection prefix>" % sys.argv[0])
+    sys.exit(-1)
+
+create_action_group(sys.argv[1], sys.argv[2])
diff --git a/utils/facts.py b/utils/facts.py
new file mode 100644
index 0000000000000000000000000000000000000000..368712348646699087c2cd691f13800cd854bc13
--- /dev/null
+++ b/utils/facts.py
@@ -0,0 +1,41 @@
+import os
+
+
+def get_roles(dir):
+    roles = []
+
+    _rolesdir = "%s/roles/" % dir
+    for _role in os.listdir(_rolesdir):
+        _roledir = "%s/%s" % (_rolesdir, _role)
+        if not os.path.isdir(_roledir) or \
+           not os.path.isdir("%s/meta" % _roledir) or \
+           not os.path.isdir("%s/tasks" % _roledir):
+            continue
+        roles.append(_role)
+
+    return sorted(roles)
+
+
+def get_modules(dir):
+    management_modules = []
+    roles_modules = []
+
+    for root, _dirs, files in os.walk(dir):
+        if not root.startswith("%s/plugins/" % dir) and \
+           not root.startswith("%s/roles/" % dir):
+            continue
+        for _file in files:
+            if _file.endswith(".py"):
+                if root == "%s/plugins/modules" % dir:
+                    management_modules.append(_file[:-3])
+                elif root.startswith("%s/roles/" % dir):
+                    if root.endswith("/library"):
+                        roles_modules.append(_file[:-3])
+
+    return sorted(management_modules), sorted(roles_modules)
+
+
+BASE_DIR = os.path.abspath(os.path.dirname(__file__) + "/..")
+ROLES = get_roles(BASE_DIR)
+MANAGEMENT_MODULES, ROLES_MODULES = get_modules(BASE_DIR)
+ALL_MODULES = sorted(MANAGEMENT_MODULES + ROLES_MODULES)
diff --git a/utils/galaxyfy.py b/utils/galaxyfy.py
index bc5c16dae4e2c57dd509eec1545065449428994e..7fef051553ef73fae8fc750734d9eaa605fd8d7a 100644
--- a/utils/galaxyfy.py
+++ b/utils/galaxyfy.py
@@ -4,7 +4,7 @@
 # Authors:
 #   Thomas Woerner <twoerner@redhat.com>
 #
-# Copyright (C) 2019,2020 Red Hat
+# Copyright (C) 2019-2023 Red Hat
 # see file 'COPYING' for use and warranty information
 #
 # This program is free software; you can redistribute it and/or modify
@@ -21,49 +21,95 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import re
+from facts import ROLES, ALL_MODULES
+
+
+def get_indent(txt):
+    return len(txt) - len(txt.lstrip())
 
 
 def galaxyfy_playbook(project_prefix, collection_prefix, lines):
-    po1 = re.compile('(%s.*:)$' % project_prefix)
-    po2 = re.compile('(.*:) (%s.*)$' % project_prefix)
-    out_lines = []
+    po_module = re.compile('(%s.*):$' % project_prefix)
+    po_module_arg = re.compile('(%s.*): (.*)$' % project_prefix)
+    po_module_unnamed = re.compile('- (%s.*):$' % project_prefix)
+    po_role = re.compile('(.*:) (%s.*)$' % project_prefix)
 
-    pattern1 = r'%s.\1' % collection_prefix
-    pattern2 = r'\1 %s.\2' % collection_prefix
+    pattern_module = r'%s.\1:' % collection_prefix
+    pattern_module_arg = r'%s.\1: \2' % collection_prefix
+    pattern_module_unnamed = r'- %s.\1:' % collection_prefix
+    pattern_role = r'\1 %s.\2' % collection_prefix
 
+    out_lines = []
     changed = False
     changeable = False
     include_role = False
+    module_defaults = False
+    module_defaults_indent = -1
     for line in lines:
         stripped = line.strip()
         if stripped.startswith("- name:") or \
            stripped.startswith("- block:"):
             changeable = True
+            module_defaults = False
+            module_defaults_indent = -1
         elif stripped in ["set_fact:", "ansible.builtin.set_fact:", "vars:"]:
             changeable = False
             include_role = False
+            module_defaults = False
+            module_defaults_indent = -1
         elif stripped == "roles:":
             changeable = True
             include_role = False
+            module_defaults = False
+            module_defaults_indent = -1
         elif (stripped.startswith("include_role:") or
               stripped.startswith("ansible.builtin.include_role:")):
             include_role = True
+            module_defaults = False
+            module_defaults_indent = -1
         elif include_role and stripped.startswith("name:"):
-            line = po2.sub(pattern2, line)
-            changed = True
+            match = po_role.search(line)
+            if match and match.group(2) in ROLES:
+                line = po_role.sub(pattern_role, line)
+                changed = True
+        elif stripped == "module_defaults:":
+            changeable = True
+            include_role = False
+            module_defaults = True
+            module_defaults_indent = -1
+        elif module_defaults:
+            _indent = get_indent(line)
+            if module_defaults_indent == -1:
+                module_defaults_indent = _indent
+            if _indent == module_defaults_indent:
+                # only module, no YAML anchor or alias
+                match = po_module.search(line)
+                if match and match.group(1) in ALL_MODULES:
+                    line = po_module.sub(pattern_module, line)
+                    changed = True
+                # module with YAML anchor or alias
+                match = po_module_arg.search(line)
+                if match and match.group(1) in ALL_MODULES:
+                    line = po_module_arg.sub(pattern_module_arg, line)
+                    changed = True
         elif changeable and stripped.startswith("- role:"):
-            line = po2.sub(pattern2, line)
-            changed = True
+            match = po_role.search(line)
+            if match and match.group(2) in ROLES:
+                line = po_role.sub(pattern_role, line)
+                changed = True
         elif (changeable and stripped.startswith(project_prefix)
-              and not stripped.startswith(collection_prefix)  # noqa
               and stripped.endswith(":")):  # noqa
-            line = po1.sub(pattern1, line)
-            changed = True
-            changeable = False  # Only change first line in task
+            match = po_module.search(line)
+            if match and match.group(1) in ALL_MODULES:
+                line = po_module.sub(pattern_module, line)
+                changed = True
+                changeable = False  # Only change first line in task
         elif (stripped.startswith("- %s" % project_prefix)
               and stripped.endswith(":")):  # noqa
-            line = po1.sub(pattern1, line)
-            changed = True
+            match = po_module_unnamed.search(line)
+            if match and match.group(1) in ALL_MODULES:
+                line = po_module_unnamed.sub(pattern_module_unnamed, line)
+                changed = True
 
         out_lines.append(line)