Skip to content

feat(lifecycle): add automatic orphaned module cleanup to update command#1059

Open
AshokThangavel wants to merge 11 commits intointersystems:mainfrom
AshokThangavel:feat-remove-stale-dependencies-on-update
Open

feat(lifecycle): add automatic orphaned module cleanup to update command#1059
AshokThangavel wants to merge 11 commits intointersystems:mainfrom
AshokThangavel:feat-remove-stale-dependencies-on-update

Conversation

@AshokThangavel
Copy link
Contributor

@AshokThangavel AshokThangavel commented Feb 1, 2026

Description

This PR introduces automatic management of orphaned dependencies during the update command. When a module is updated and its new version no longer requires certain dependencies, those "orphaned" modules are now automatically uninstalled to maintain namespace hygiene. A new flag, -keep-orphans, has been added to allow users to override this behavior.
fixes: #868

2. New Command Flag

  • Flag: -keep-orphans
  • Behavior: When present, the UninstallOrphanedDependencies method is skipped, leaving all previously installed dependencies intact.

output

zpm:USER>list
IPM (zpm)                              0.10.6-SNAPSHOT
demo-module1                           1.0.0
irisjwt                                1.0.0
iris-LocalizationLab (localizationlab) 1.0.0
objectscript-math                      0.0.5
zpm:USER>update demo-module1 -p /usr/irissys/mgr/user/mod1/

Building dependency graph...Done.
[USER|demo-module1]     Initialize START
[USER|demo-module1]     Initialize SUCCESS
[USER|demo-module1]     Reload START (/usr/irissys/mgr/user/mod1/)
[USER|demo-module1]     Reload SUCCESS
[demo-module1]  Module object refreshed.
[USER|demo-module1]     Validate START
[USER|demo-module1]     Validate SUCCESS
[USER|demo-module1]     Compile START
[USER|demo-module1]     Compile SUCCESS
[USER|demo-module1]     Activate START
[USER|demo-module1]     Configure START
[USER|demo-module1]     Configure SUCCESS
[USER|demo-module1]     Activate SUCCESS
[USER|demo-module1]     ApplyUpdateSteps START
[USER|demo-module1]     ApplyUpdateSteps SUCCESS
Uninstalling orphaned dependencies
[USER|irisjwt]  Clean START
[USER|irisjwt]  Unconfigure START
[USER|irisjwt]  Unconfigure SUCCESS
[USER|irisjwt]  Clean SUCCESS
Module installed successfully! To start call USER>Do ##class(Package.Class).Run()

zpm:USER>list
IPM (zpm)                              0.10.6-SNAPSHOT
demo-module1                           1.0.0
iris-LocalizationLab (localizationlab) 1.0.0
objectscript-math                      0.0.5

- Implement logic to identify and uninstall dependencies removed in new module versions.
- Introduce -keep-orphans (-ko) flag to allow users to opt-out of cleanup.
- Ensure cleanup logic is scoped strictly to the 'update' command.
- Add integration tests in Test.PM.Unit.OrphanCleanup to verify flag behavior.
- Update command metadata and help text for 'update'.
Copy link
Collaborator

@isc-jili isc-jili left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AshokThangavel functionality looks good! I left some comments. Mostly nits!
Also I think the CHANGELOG will need to be updated due to IPM version having changed in the interim

@@ -0,0 +1,229 @@
Class Test.PM.Integration.OrphanCleanup Extends Test.PM.Integration.Base
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: We already have an existing Update test suite in Test.PM.Integration.Update. I suggest either adding to that suite, or naming this test suite to be explicitly update related (eg. UpdateOrphanCleanup.cls)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. I've updated the class name to Test.PM.Integration.UpdateOrphanCleanup

<modifier name="path" aliases="p" value="true" description="Location of local tarball containing the updated version of the module. Overrides 'version' parameter if present." />
<modifier name="dev" dataAlias="DeveloperMode" dataValue="1" description="Sets the DeveloperMode flag for the module's lifecycle. Key consequences of this are that ^Sources will be configured for resources in the module, and installer methods will be called with the dev mode flag set." />
<modifier name="create-lockfile" aliases="lock" dataAlias="CreateLockFile" dataValue="1" description="Upon update, creates/updates the module's lock file." />
<modifier name="keep-orphans" aliases="ko" dataAlias="KeepOrphans" dataValue="1" description="Retains dependencies that are no longer required by the updated module. By default, these orphaned modules are automatically uninstalled." />
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I don't love the alias "ko" as it has "kill" implications which is the opposite of what this does (ie. "k" by itself in Objectscript means kill, "KO" means kill in other contexts) so that could be misleading. I suggest either naming keep-orphans differently, or abbreviating it differently

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you. I agree, I've modified the alias to -ro (Retain Orphans) .

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @AshokThangavel thank you for the change! I discussed with my team and we believe the best way is actually to remove the alias entirely. Since "ro" also has the more widely known meaning "read-only". It's okay for this command to not have an alias.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @isc-jili, thanks for confirming. I’ve removed the -ro alias for keep-orphans, and as suggested, there is now no alias for it. Thank you!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated the pull request description as well.

set depsCount = moduleObj.Dependencies.Count()
for i=1:1:depsCount{
set depsName = moduleObj.Dependencies.GetAt(i)
do $$$LogMessage("Module "_depsName.Name_" installed successfully")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: in the log make it explicit that these installed modules are dependencies of module1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated the code!

do $$$LogMessage("Module "_depsName.Name_" installed successfully")
}
if depsCount = 3 {
do $$$LogMessage("All 3 modules are loaded successfully")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "All 3 dependency modules ..."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the message. Thank you!


for i=1:1:depsCount{
set depsName = moduleObj.Dependencies.GetAt(i)
do $$$LogMessage("Module "_depsName.Name_" exist")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "Dependency module ..."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the message.

{
if '##class(%File).DirectoryExists(Directory) {
set status = ##class(%File).CreateDirectoryChain(Directory)
do $$$AssertStatusOK(status," Directory"_Directory_" not exist. So created the directory")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "Directory "Directory" does not exist. So created the directory."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

modified the code.

do $$$AssertStatusOK(status," Directory"_Directory_" not exist. So created the directory")
}
else {
do $$$LogMessage("Directory "_Directory_" is already exist")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "Directory "Directory" already exists."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed. Thank you!


Method CreateModuleFile(
ModuleFile As %String = "",
moduleXdata As %String = "")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Inconsistent capitalization of variables

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected the Parameter variables Inconsistency. Thank you!

quit 0
}
if '##class(%File).Exists(ModuleFile) {
do $$$LogMessage(ModuleFile_" not exist. So creating the xml")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: does not exist

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added the changes!

Output OrphanedModules)
{
kill OrphanedModules
merge existDeps = ExistDependencies
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: inconsistent casing for vars
Also, what is the point of making a copy of ExistDependencies instead of acting directly on ExistDependencies? I don't see ExistDependencies being used after this method call.

Copy link
Contributor Author

@AshokThangavel AshokThangavel Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the code.
Thank you!

Copy link
Collaborator

@isc-jili isc-jili left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AshokThangavel it's getting really close! Left another round mostly small suggestions. Thanks for implementing this feature!

/// Recursively removes modules not required by the main module or other local packages.
ClassMethod UninstallOrphanedDependencies(
ByRef orphanedDeps,
ByRef Params)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: inconsistent capitalization of variables.
I suggest turning on the force-formatting rules found in IPM/.vscode/settings.json if working with VSCode.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the capitalization of variables. Thank you!

These are the existing Force-formatting rules in the settings.json. Please let me know if anything missing

// Force formatting rules
    "intersystems.testingManager.client.relativeTestRoot": "tests/unit_tests",
    "objectscript.multilineMethodArgs": true,
    "intersystems.language-server.formatting.expandClassNames": false,
    "intersystems.language-server.formatting.commands.case": "lower",
    "intersystems.language-server.formatting.commands.length": "long",
    "intersystems.language-server.formatting.system.case": "lower",
    "intersystems.language-server.formatting.system.length": "long",

Thank you!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah sorry I misremembered the rules. The rules look fine! They will not change variable capitalization so that will be up to us to catch any inconsistencies

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries. Thank you for the confirmation.

</command>

<command name="update" dataPrefix="D">
<summary>Updates a module to a newer version.</summary>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest adding to update command description and summary that by default it will uninstall all orphaned modules

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. Additionally, uninstalls any modules that are no longer required as dependencies by default - added this in the description


set status = ..CreateModuleFile(v1ModuleDir, "DemoModuleV1")
do $$$AssertStatusOK(status, mainModule_" version 1.0.0 Module.xml created")
do ..CreateDependecnyModules(v1ModuleDir)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: typo should be CreateDependencyModules

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the Method name typo. Thank you!


do $$$LogMessage("Loading '" _ mainModule _ "' version 1.0.0")
set status = ..RunCommand("load "_v1ModuleDir)
do $$$AssertStatusOK(status, mainModule_" version 1.0.0 laoded successfully")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: typo should be "loaded"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the typo.

set depsCount = moduleObj.Dependencies.Count()
for i=1:1:depsCount{
set depsName = moduleObj.Dependencies.GetAt(i)
do $$$LogMessage("Dependency module "_depsName.Name_" is exist")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should be "Dependency module "depsName.Name" exists"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed the message. Thank you!

return status
}

Method CreateDependecnyModules(Directory As %String = "")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: typo in method name should be "Dependency"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the methodName. Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Update framework enhancement: remove dependencies that don't exist in a new version of a module

2 participants