Repository: nf-core/tools

Issue:
installed_by field is not properly updated when updating a subworkflow
### Description of the bug

I installed a subworkflow (`vcf_annotate_ensemblvep`) on sarek, I had an on-going PR for it.
The subworkflow had some modifications (`TABIX_BGZIPTABIX` was removed and `TABIX_TABIX` was added) so I updated it, and it was well updated, without issues.
But when inspecting the `modules.json`, I noticed that `TABIX_BGZIPTABIX` was still listed as installed by `vcf_annotate_ensemblvep`

### Command used and terminal output

_No response_

### System information

_No response_


Discussion:

Resolving patch:
diff --git a/nf_core/components/update.py b/nf_core/components/update.py
--- a/nf_core/components/update.py
+++ b/nf_core/components/update.py
@@ -9,7 +9,12 @@
 import nf_core.modules.modules_utils
 import nf_core.utils
 from nf_core.components.components_command import ComponentCommand
-from nf_core.components.components_utils import prompt_component_version_sha
+from nf_core.components.components_utils import (
+    get_components_to_install,
+    prompt_component_version_sha,
+)
+from nf_core.components.install import ComponentInstall
+from nf_core.components.remove import ComponentRemove
 from nf_core.modules.modules_differ import ModulesDiffer
 from nf_core.modules.modules_json import ModulesJson
 from nf_core.modules.modules_repo import ModulesRepo
@@ -254,6 +259,7 @@ def update(self, component=None, silent=False, updated=None, check_diff_exist=Tr
                         self.update_linked_components(
                             modules_to_update, subworkflows_to_update, updated, check_diff_exist=False
                         )
+                        self.manage_changes_in_linked_components(component, modules_to_update, subworkflows_to_update)
 
                 elif self.show_diff:
                     ModulesDiffer.print_diff(
@@ -278,7 +284,7 @@ def update(self, component=None, silent=False, updated=None, check_diff_exist=Tr
                 # Clear the component directory and move the installed files there
                 self.move_files_from_tmp_dir(component, install_tmp_dir, modules_repo.repo_path, version)
                 # Update modules.json with newly installed component
-                self.modules_json.update(self.component_type, modules_repo, component, version, self.component_type)
+                self.modules_json.update(self.component_type, modules_repo, component, version, installed_by=None)
                 updated.append(component)
                 recursive_update = True
                 modules_to_update, subworkflows_to_update = self.get_components_to_update(component, modules_repo)
@@ -299,10 +305,16 @@ def update(self, component=None, silent=False, updated=None, check_diff_exist=Tr
                 if recursive_update and len(modules_to_update + subworkflows_to_update) > 0:
                     # Update linked components
                     self.update_linked_components(modules_to_update, subworkflows_to_update, updated)
+                    self.manage_changes_in_linked_components(component, modules_to_update, subworkflows_to_update)
             else:
                 # Don't save to a file, just iteratively update the variable
                 self.modules_json.update(
-                    self.component_type, modules_repo, component, version, self.component_type, write_file=False
+                    self.component_type,
+                    modules_repo,
+                    component,
+                    version,
+                    installed_by=None,
+                    write_file=False,
                 )
 
         if self.save_diff_fn:
@@ -871,6 +883,34 @@ def update_linked_components(self, modules_to_update, subworkflows_to_update, up
             self.update(m_update, silent=True, updated=updated, check_diff_exist=check_diff_exist)
             self._reset_component_type(original_component_type, original_update_all)
 
+    def manage_changes_in_linked_components(self, component, modules_to_update, subworkflows_to_update):
+        """Check for linked components added or removed in the new subworkflow version"""
+        if self.component_type == "subworkflows":
+            subworkflow_directory = Path(self.dir, self.component_type, self.modules_repo.repo_path, component)
+            included_modules, included_subworkflows = get_components_to_install(subworkflow_directory)
+            # If a new module/subworkflow is included in the subworklfow and wasn't included before
+            for module in included_modules:
+                if module not in modules_to_update:
+                    log.info(f"Installing newly included module '{module}' for '{component}'")
+                    install_module_object = ComponentInstall(self.dir, "modules", installed_by=component)
+                    install_module_object.install(module, silent=True)
+            for subworkflow in included_subworkflows:
+                if subworkflow not in subworkflows_to_update:
+                    log.info(f"Installing newly included subworkflow '{subworkflow}' for '{component}'")
+                    install_subworkflow_object = ComponentInstall(self.dir, "subworkflows", installed_by=component)
+                    install_subworkflow_object.install(subworkflow, silent=True)
+            # If a module/subworkflow has been removed from the subworkflow
+            for module in modules_to_update:
+                if module not in included_modules:
+                    log.info(f"Removing module '{module}' which is not included in '{component}' anymore.")
+                    remove_module_object = ComponentRemove("modules", self.dir)
+                    remove_module_object.remove(module, removed_by=component)
+            for subworkflow in subworkflows_to_update:
+                if subworkflow not in included_subworkflows:
+                    log.info(f"Removing subworkflow '{subworkflow}' which is not included in '{component}' anymore.")
+                    remove_subworkflow_object = ComponentRemove("subworkflows", self.dir)
+                    remove_subworkflow_object.remove(subworkflow, removed_by=component)
+
     def _change_component_type(self, new_component_type):
         original_component_type = self.component_type
         self.component_type = new_component_type
diff --git a/nf_core/modules/modules_json.py b/nf_core/modules/modules_json.py
--- a/nf_core/modules/modules_json.py
+++ b/nf_core/modules/modules_json.py
@@ -656,7 +656,7 @@ def update(
         repo_component_entry[component_name]["git_sha"] = component_version
         repo_component_entry[component_name]["branch"] = branch
         try:
-            if installed_by not in repo_component_entry[component_name]["installed_by"]:
+            if installed_by not in repo_component_entry[component_name]["installed_by"] and installed_by is not None:
                 repo_component_entry[component_name]["installed_by"].append(installed_by)
         except KeyError:
             repo_component_entry[component_name]["installed_by"] = [installed_by]


Test patch:
diff --git a/tests/subworkflows/update.py b/tests/subworkflows/update.py
--- a/tests/subworkflows/update.py
+++ b/tests/subworkflows/update.py
@@ -296,6 +296,33 @@ def test_update_all_subworkflows_from_module(self):
     )
 
 
+def test_update_change_of_included_modules(self):
+    """Update a subworkflow which has a module change in the new version."""
+    # Install an old version of vcf_annotate_ensemblvep with tabix/bgziptabix and without tabix/tabix
+    self.subworkflow_install_module_change.install("vcf_annotate_ensemblvep")
+    old_mod_json = ModulesJson(self.pipeline_dir).get_modules_json()
+
+    # Check that tabix/bgziptabix is there
+    assert "tabix/bgziptabix" in old_mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"][NF_CORE_MODULES_NAME]
+    assert Path(self.pipeline_dir, "modules", NF_CORE_MODULES_NAME, "tabix/bgziptabix").is_dir()
+    # Check that tabix/tabix is not there
+    assert "tabix/tabix" not in old_mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"][NF_CORE_MODULES_NAME]
+    assert not Path(self.pipeline_dir, "modules", NF_CORE_MODULES_NAME, "tabix/tabix").is_dir()
+
+    # Update vcf_annotate_ensemblvep without tabix/bgziptabix and with tabix/tabix
+    update_obj = SubworkflowUpdate(self.pipeline_dir, update_deps=True, show_diff=False)
+    assert update_obj.update("vcf_annotate_ensemblvep") is True
+
+    mod_json = ModulesJson(self.pipeline_dir).get_modules_json()
+
+    # Check that tabix/bgziptabix is not there
+    assert "tabix/bgziptabix" not in mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"][NF_CORE_MODULES_NAME]
+    assert not Path(self.pipeline_dir, "modules", NF_CORE_MODULES_NAME, "tabix/bgziptabix").is_dir()
+    # Check that tabix/tabix is there
+    assert "tabix/tabix" in mod_json["repos"][NF_CORE_MODULES_REMOTE]["modules"][NF_CORE_MODULES_NAME]
+    assert Path(self.pipeline_dir, "modules", NF_CORE_MODULES_NAME, "tabix/tabix").is_dir()
+
+
 def cmp_component(dir1, dir2):
     """Compare two versions of the same component"""
     files = ["main.nf", "meta.yml"]
diff --git a/tests/test_subworkflows.py b/tests/test_subworkflows.py
--- a/tests/test_subworkflows.py
+++ b/tests/test_subworkflows.py
@@ -67,6 +67,12 @@ def setUp(self):
             force=False,
             sha=OLD_SUBWORKFLOWS_SHA,
         )
+        self.subworkflow_install_module_change = nf_core.subworkflows.SubworkflowInstall(
+            self.pipeline_dir,
+            prompt=False,
+            force=False,
+            sha="8c343b3c8a0925949783dc547666007c245c235b",
+        )
         self.mods_install = nf_core.modules.ModuleInstall(self.pipeline_dir, prompt=False, force=True)
 
         # Set up remove objects
@@ -126,6 +132,7 @@ def tearDown(self):
         test_update_all,
         test_update_all_linked_components_from_subworkflow,
         test_update_all_subworkflows_from_module,
+        test_update_change_of_included_modules,
         test_update_with_config_dont_update,
         test_update_with_config_fix_all,
         test_update_with_config_fixed_version,