Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [0.10.7] - Unreleased

### Added
- #1059: Add automatic orphaned module cleanup to update command

## [0.10.6] - 2026-02-24

Expand Down
5 changes: 5 additions & 0 deletions src/cls/IPM/Main.cls
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ This command is an alias for `module-action module-name publish`
update HS.JSON 3.0.0
</example>

<example description="Updates the HS.JSON module from the current version to version 3.0.0 and keep the orphaned modules">
update HS.JSON 3.0.0 -keep-orphans
</example>

<!-- Parameters -->
<parameter name="module" required="true" description="Name of module on which to perform update actions" />
<parameter name="version" description="Version (or version expression) of module to update; defaults to the latest available if unspecified. Is ignored if -path is provided." />
Expand All @@ -165,6 +169,7 @@ This command is an alias for `module-action module-name publish`
<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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

</command>

<command name="makedeployed">
Expand Down
61 changes: 60 additions & 1 deletion src/cls/IPM/Utils/Module.cls
Original file line number Diff line number Diff line change
Expand Up @@ -1129,11 +1129,21 @@ ClassMethod LoadNewModule(
$$$ThrowStatus($$$ERROR($$$GeneralError, msg))
}
}

set cmd = $get(params("cmd"))
set orphanCleanup = (cmd = "update") && ('$get(pParams("KeepOrphans")))
// Retrieve dependencies from the previous version and compare with the current version to find modules that are no longer required
if (orphanCleanup) {
set tModule = ##class(%IPM.Storage.Module).NameOpen(commandLineModuleName,,.tSC)
do ..GetExistingDependencies(tModule, .oldDependencies)
}
// This loads the new version of the module into storage. Overrides the module context of the currently installed module, if it exists.
set tSC = $system.OBJ.Load(pDirectory_"module.xml",$select(tVerbose:"d",1:"-d"),,.tLoadedList)
$$$ThrowOnError(tSC)

// Compare dependencies from the previous version with the latest version and collect orphaned dependencies as an array
if (orphanCleanup) {
do ..GetOrphanedDependencies(tModule,.oldDependencies, .orphanedDeps)
}
set tFirstLoaded = $order(tLoadedList(""))
if (tFirstLoaded = "") {
$$$ThrowStatus($$$ERROR($$$GeneralError,"No module definition found."))
Expand Down Expand Up @@ -1228,6 +1238,10 @@ ClassMethod LoadNewModule(
tcommit
}
$$$ThrowOnError(tSC)
// Uninstall orphaned dependencies
if (orphanCleanup) {
do ..UninstallOrphanedDependencies(.orphanedDeps, .pParams)
}
do tModule.WriteAfterInstallMessage()
} catch e {
set tSC = e.AsStatus()
Expand All @@ -1240,6 +1254,51 @@ ClassMethod LoadNewModule(
quit tSC
}

/// Uninstall orphaned dependencies identified during update.
/// Recursively removes modules not required by the main module or other local packages.
ClassMethod UninstallOrphanedDependencies(
ByRef orphanedDeps,
ByRef Params)
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

{
if '$data(orphanedDeps) {
quit
}
set dependencyModule=""
write !,"Uninstalling orphaned dependencies"
for {
set dependencyModule = $order(orphanedDeps(dependencyModule)) quit:dependencyModule=""
continue:'##class(%IPM.Storage.Module).NameExists(dependencyModule)
// Remove this module and its dependencies recursively, skipping any still in use
$$$ThrowOnError(##class(%IPM.Storage.Module).Uninstall(dependencyModule,,1,.Params))
}
}

/// Retrieves the dependency graph for the currently installed version of the module.
ClassMethod GetExistingDependencies(
Module As %IPM.Storage.Module,
Output DependencyGraph)
{
$$$ThrowOnError(Module.BuildDependencyGraph(.DependencyGraph,,,,"",,,,,,))
}

/// Compares the previous dependency graph with the current one to identify orphans.
ClassMethod GetOrphanedDependencies(
Module As %IPM.Storage.Module,
ByRef ExistDependencies,
Output OrphanedModules)
{
kill OrphanedModules
merge existDeps = ExistDependencies
Copy link
Copy Markdown
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
Copy Markdown
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!

set sc = Module.BuildDependencyGraph(.dependencyGraph,,,,"",,,,,,)
$$$ThrowOnError(sc)
set module=""
for {
set module = $order(dependencyGraph(module)) quit:module=""
kill existDeps(module)
}
merge OrphanedModules = existDeps
}

/// Load dependencies of a module in a synchronous manner, one at a time, in the correct order.
/// This method builds a dependency graph (optionally with phases) and installs missing dependencies.
ClassMethod LoadDependencies(
Expand Down
229 changes: 229 additions & 0 deletions tests/integration_tests/Test/PM/Integration/OrphanCleanup.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
Class Test.PM.Integration.OrphanCleanup Extends Test.PM.Integration.Base
Copy link
Copy Markdown
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
Copy Markdown
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

{

Method TestRemoveOrphanedModOnUpdate() As %Status
{
#define NormalizeDir(%dir) ##class(%File).NormalizeDirectory(%dir)
#define NormalizeFilename(%file,%dir) ##class(%File).NormalizeFilename(%file,%dir)

do $$$LogMessage("Verify and uninstall the demo-module1 if it was installed.")
set isExist = ##class(%IPM.Storage.Module).NameExists("demo-module1")
if isExist{
set status = ..RunCommand("uninstall demo-module1 -r")
do $$$AssertStatusOK(status, "Uninstalled the demo-module1 successfully")
}
set testRoot = $$$NormalizeDir($get(^UnitTestRoot))
set moduleDir = $$$NormalizeDir(##class(%File).GetDirectory(testRoot)_"/_data/demo-module/")

set v1ModuleDir = $$$NormalizeDir(moduleDir_"/v1.0.0")
set v2ModuleDir = $$$NormalizeDir(moduleDir_"/v2.0.0")
set v3ModuleDir = $$$NormalizeDir(moduleDir_"/v3.0.0")

do ..CreateDirectory(v1ModuleDir)
set moduleFile = $$$NormalizeFilename("module.xml",v1ModuleDir)
set status = ..CreateModuleFile(moduleFile, "DemoModuleV1")
do $$$AssertStatusOK(status, "demo-module1 version 1.0.0 Module.xml created")

do $$$LogMessage("Load demo-module version 1.0.0")
set status = ..RunCommand("load "_v1ModuleDir)
do $$$AssertStatusOK(status, "demo-module1 version 1.0.0 laoded successfully")

set moduleObj = ##class(%IPM.Storage.Module).NameOpen("demo-module1")
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
Copy Markdown
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
Copy Markdown
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!

}
if depsCount = 3 {
do $$$LogMessage("All 3 modules are loaded successfully")
Copy link
Copy Markdown
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
Copy Markdown
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!

}

do $$$LogMessage("Update the demo-module1 to version 2.0.0")
do ..CreateDirectory(v2ModuleDir)
set v2moduleFile = $$$NormalizeFilename("module.xml",v2ModuleDir)
set status = ..CreateModuleFile(v2moduleFile, "DemoModuleV2")
do $$$AssertStatusOK(status, "demo-module1 version 2.0.0 Module.xml created")
;
do $$$LogMessage("update the demo-module to version 2.0.0 without -keep-orphans flag. So orphaed modules will be removed from the package manager")
set status = ..RunCommand("update demo-module1 -p "_v2ModuleDir)
do $$$AssertStatusOK(status, "demo-module1 version 2.0.0 updated successfully")

set moduleObj = ##class(%IPM.Storage.Module).NameOpen("demo-module1")
set depsCount = moduleObj.Dependencies.Count()

for i=1:1:depsCount{
set depsName = moduleObj.Dependencies.GetAt(i)
do $$$LogMessage("Module "_depsName.Name_" exist")
Copy link
Copy Markdown
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
Copy Markdown
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 depsCount = 2 {
do $$$LogMessage("Only 2 modules are exist after update the module")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: "Only 2 dependency modules exist after updating module1"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

updated the message. Thank you!

}
do $$$LogMessage("Check the irisjwt module was uninstalled")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: "Check that the irisjwt module, which is no longer a dependency of module1 version 2.0.0, ..."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Additionally, following the CONTRIBUTING.md guidelines, I have replaced the existing dependency test modules with smaller, purpose-built modules. This prevents conflicts or errors during the installation

Thank You!

set isExist = ##class(%IPM.Storage.Module).NameExists("irisjwt")
do $$$AssertTrue('isExist," The orphaed irisjwt module was uninstalled successfully")

//
do $$$LogMessage("update the demo-module to version 3.0.0 with -keep-orphans flag. So No orphaed modules removed from the package manager")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: typos in "No orphaed"

Copy link
Copy Markdown
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 ..CreateDirectory(v3ModuleDir)
set v3moduleFile = $$$NormalizeFilename("module.xml",v3ModuleDir)
set status = ..CreateModuleFile(v3moduleFile, "DemoModuleV3")
do $$$AssertStatusOK(status, "demo-module1 version 2.0.0 Module.xml created")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: Should be 3.0.0 here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you. Fixed the code


set status = ..RunCommand("update demo-module1 -p "_v3ModuleDir_" -keep-orphans")
do $$$AssertStatusOK(status, "demo-module1 version updated to 3.0.0 successfully")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: Add "Dependencies should be kept due to -keep-orphans flag"

Copy link
Copy Markdown
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!


set moduleObj = ##class(%IPM.Storage.Module).NameOpen("demo-module1")
set depsCount = moduleObj.Dependencies.Count()
do $$$LogMessage("Dependency module count is "_depsCount)

for i=1:1:depsCount{
set depsName = moduleObj.Dependencies.GetAt(i)
do $$$LogMessage("Module "_depsName.Name_" exist")
Copy link
Copy Markdown
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
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed!

}
set isExist = ##class(%IPM.Storage.Module).NameExists("LocalizationLab")
do $$$AssertTrue(isExist," The orphaed LocalizationLab module still exist")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: The orphaned dependency module LocalizationLab still exists"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated the logic. I have replaced the existing dependency test modules with smaller, purpose-built modules. Thank you!

}

Method RunCommand(Command) As %Status
{
do $$$LogMessage("executing command "_Command)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: Capitalize "Executing"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed the Capitalize "Executing"

set status = ##class(%IPM.Main).Shell(Command)
return status
}

Method CreateDirectory(Directory)
{
if '##class(%File).DirectoryExists(Directory) {
set status = ##class(%File).CreateDirectoryChain(Directory)
do $$$AssertStatusOK(status," Directory"_Directory_" not exist. So created the directory")
Copy link
Copy Markdown
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
Copy Markdown
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.

}
else {
do $$$LogMessage("Directory "_Directory_" is already exist")
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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!

{
if ModuleFile=""||(moduleXdata="") {
quit 0
}
if '##class(%File).Exists(ModuleFile) {
do $$$LogMessage(ModuleFile_" not exist. So creating the xml")
Copy link
Copy Markdown
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
Copy Markdown
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!

}
set fileStream = ##class(%Stream.FileCharacter).%New()
set fileStream.Filename = ModuleFile
set v1ModFile = ##class(%Dictionary.XDataDefinition).%OpenId($classname()_"||"_moduleXdata)
do fileStream.CopyFrom(v1ModFile.Data)
set status = fileStream.%Save()
do $$$AssertStatusOK(status, "Module.xml created on "_ModuleFile)
return status
}

XData DemoModuleV1 [ MimeType = application/xml ]
{
<?xml version="1.0" encoding="UTF-8"?>
<Export generator="Cache" version="25">
<Document name="demo-module1.ZPM">
<Module>
<Name>demo-module1</Name>
<Version>1.0.0</Version>
<Description>description</Description>
<Keywords>keywords</Keywords>
<Author>
<Person>your name</Person>
<Organization>your organization</Organization>
<CopyrightDate>2020</CopyrightDate>
<License>MIT</License>
<Notes>notes</Notes>
</Author>
<Dependencies>
<ModuleReference>
<Name>objectscript-math</Name>
<Version>0.0.5</Version>
</ModuleReference>
<ModuleReference>
<Name>LocalizationLab</Name>
<Version>1.0.0</Version>
</ModuleReference>
<ModuleReference>
<Name>irisjwt</Name>
<Version>1.0.0</Version>
</ModuleReference>
</Dependencies>
<Packaging>module</Packaging>
<Default Name="count" Value="7"/>
<SourcesRoot>src</SourcesRoot>
</Module>
</Document>
</Export>
}

XData DemoModuleV2 [ MimeType = application/xml ]
{
<?xml version="1.0" encoding="UTF-8"?>
<Export generator="Cache" version="25">
<Document name="demo-module1.ZPM">
<Module>
<Name>demo-module1</Name>
<Version>2.0.0</Version>
<Description>description</Description>
<Keywords>keywords</Keywords>
<Author>
<Person>your name</Person>
<Organization>your organization</Organization>
<CopyrightDate>2020</CopyrightDate>
<License>MIT</License>
<Notes>notes</Notes>
</Author>
<Dependencies>
<ModuleReference>
<Name>objectscript-math</Name>
<Version>0.0.5</Version>
</ModuleReference>
<ModuleReference>
<Name>LocalizationLab</Name>
<Version>1.0.0</Version>
</ModuleReference>
</Dependencies>
<Packaging>module</Packaging>
<Default Name="count" Value="7"/>
<SourcesRoot>src</SourcesRoot>
</Module>
</Document>
</Export>
}

XData DemoModuleV3 [ MimeType = application/xml ]
{
<?xml version="1.0" encoding="UTF-8"?>
<Export generator="Cache" version="25">
<Document name="demo-module1.ZPM">
<Module>
<Name>demo-module1</Name>
<Version>3.0.0</Version>
<Description>description</Description>
<Keywords>keywords</Keywords>
<Author>
<Person>your name</Person>
<Organization>your organization</Organization>
<CopyrightDate>2020</CopyrightDate>
<License>MIT</License>
<Notes>notes</Notes>
</Author>
<Dependencies>
<ModuleReference>
<Name>objectscript-math</Name>
<Version>0.0.5</Version>
</ModuleReference>
</Dependencies>
<Packaging>module</Packaging>
<Default Name="count" Value="7"/>
<SourcesRoot>src</SourcesRoot>
</Module>
</Document>
</Export>
}

}