Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- #992: Implement automatic history purge logic
- #973: Enables CORS and JWT configuration for WebApplications in module.xml
- #1059: Add automatic orphaned module cleanup to update command

### Fixed
- #1001: The `unmap` and `enable` commands will now only activate CPF merge once after all namespaces have been configured instead after every namespace
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="ro" dataAlias="KeepOrphans" dataValue="1" description="Retains dependencies that are no longer required by the updated module. By default, these orphaned modules are automatically uninstalled." />
</command>

<command name="makedeployed">
Expand Down
62 changes: 61 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,52 @@ 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
$$$ThrowOnError(Module.BuildDependencyGraph(.dependencyGraph,,,,"",,,,,,))
set moduleName = ""
for {
set moduleName = $order(dependencyGraph(moduleName))
quit:moduleName=""

// Remove active dependencies from the existing list.
kill ExistDependencies(moduleName)
}
merge OrphanedModules = ExistDependencies
}

/// 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
283 changes: 283 additions & 0 deletions tests/integration_tests/Test/PM/Integration/UpdateOrphanCleanup.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
/// These tests verify that when a module is upgraded to a version with fewer
/// dependencies, the orphaned dependencies are handled based on the
/// -keep-orphans flag.
/// Scenarios:
/// 1. Standard Update: Orphaned dependencies are automatically uninstalled.
/// 2. -keep-orphans: Orphaned dependencies remain installed in the namespace.
Class Test.PM.Integration.UpdateOrphanCleanup Extends Test.PM.Integration.Base
{

Method TestRemoveOrphanedModsOnUpdate() 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 testRoot = $$$NormalizeDir($get(^UnitTestRoot))
set moduleDir = $$$NormalizeDir(##class(%File).GetDirectory(testRoot)_"/_data/demo-module1/")

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

do ..CreateDirectory(v1ModuleDir)
do ..CreateDirectory(v2ModuleDir)
do ..CreateDirectory(v3ModuleDir)

set mainModule = "demo-module1"
set isExist = ##class(%IPM.Storage.Module).NameExists(mainModule)
if isExist {
set status = ..RunCommand("uninstall demo-module1 -r")
do $$$AssertStatusOK(status, "Uninstalled the demo-module1 successfully")
}

set status = ..CreateModuleFile(v1ModuleDir, "DemoModuleV1")
do $$$AssertStatusOK(status, mainModule_" version 1.0.0 Module.xml created")
do ..CreateDependecnyModules(v1ModuleDir)
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: typo should be CreateDependencyModules

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 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
Copy Markdown
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
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 typo.


set moduleObj = ##class(%IPM.Storage.Module).NameOpen(mainModule)

if $isobject(moduleObj) {
set depsCount = moduleObj.Dependencies.Count()
for i=1:1:depsCount {
set depsName = moduleObj.Dependencies.GetAt(i)
do $$$LogMessage("Module "_depsName.Name _ " (dependency of " _mainModule _ ") installed successfully")
}
if depsCount = 3 {
do $$$LogMessage("All 3 dependencies for '"_mainModule_"' have been loaded successfully")
}
}

do $$$LogMessage("Updating '" _ mainModule _ "' to version 2.0.0.")
set status = ..CreateModuleFile(v2ModuleDir, "DemoModuleV2")
do $$$AssertStatusOK(status, mainModule_" version 2.0.0 Module.xml created")
do ..CreateDependecnyModules(v1ModuleDir)

do $$$LogMessage("Updating '"_mainModule_"' to version 2.0.0. Orphaned dependencies should be automatically uninstalled (default behavior).")
set status = ..RunCommand("update "_mainModule_" -p "_v2ModuleDir)
do $$$AssertStatusOK(status, "Successfully updated '" _ mainModule _ "' to version 2.0.0.")

set moduleObj = ##class(%IPM.Storage.Module).NameOpen(mainModule)
if $isobject(moduleObj) {
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
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 "Dependency module "depsName.Name" 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 the message. Thank you!

}
if depsCount = 2 {
do $$$LogMessage("Only 2 Dependency modules are exist after update the "_mainModule)
}
}

do $$$LogMessage("Verifying that 'test-dependency-1' was removed because it is no longer a dependency of " _ mainModule _ " in version 2.0.0")
set isExist = ##class(%IPM.Storage.Module).NameExists("test-dependency-1")
do $$$AssertTrue('isExist, "The orphaned dependency 'test-dependency-1' should be uninstalled after updating " _ mainModule _ " to version 2.0.0")

do $$$LogMessage("Updating '"_mainModule_"' to version 3.0.0 with -keep-orphans flag. Orphaned dependencies should be retained.")

set status = ..CreateModuleFile(v3ModuleDir, "DemoModuleV3")
do $$$AssertStatusOK(status, mainModule_" version 3.0.0 Module.xml created")
do ..CreateDependecnyModules(v1ModuleDir)

set status = ..RunCommand("update "_mainModule_" -p "_v3ModuleDir_" -keep-orphans")
do $$$AssertStatusOK(status, "Successfully updated '"_mainModule_"' to 3.0.0; dependencies were retained as specified by the -keep-orphans flag.")

set moduleObj = ##class(%IPM.Storage.Module).NameOpen(mainModule)
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("Dependency module "_depsName.Name_" exist after update "_mainModule_" to version 3.0.0")
}
set isExist = ##class(%IPM.Storage.Module).NameExists("test-dependency-2")
do $$$AssertTrue(isExist,"The orphaned 'test-dependency-2' module was successfully retained as expected.")
}

Method RunCommand(Command) As %Status
{
do $$$LogMessage("Executing command "_Command)
set status = ##class(%IPM.Main).Shell(Command)
return status
}

Method CreateDirectory(Directory As %String = "")
{
if '##class(%File).DirectoryExists(Directory) {
set status = ##class(%File).CreateDirectoryChain(Directory)
do $$$AssertStatusOK(status, "Target directory '" _ Directory _ "' was successfully created.")
}
}

Method CreateModuleFile(
ModuleDir As %String = "",
ModuleXdata As %String = "") As %Status
{
if ModuleDir=""||(ModuleXdata="") {
quit 0
}

set moduleFile = ##class(%File).NormalizeFilename("module.xml",ModuleDir)
if '##class(%File).Exists(moduleFile) {
do $$$LogMessage("Module file '" _ moduleFile _ "' not found; creating XML file.")
}
set fileStream = ##class(%Stream.FileCharacter).%New()
set fileStream.Filename = moduleFile
set modFileXData = ##class(%Dictionary.XDataDefinition).%OpenId($classname()_"||"_ModuleXdata)
do fileStream.CopyFrom(modFileXData.Data)
set status = fileStream.%Save()
do $$$AssertStatusOK(status, "Module manifest successfully created at '" _ moduleFile _ "'")
return status
}

Method CreateDependecnyModules(Directory 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: typo in method name should be "Dependency"

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 methodName. Thank you!

{
if ##class(%File).DirectoryExists(Directory) {
set dependencyModuleList = $listbuild("DepModule1", "DepModule2", "DepModule3" )
set ptr = 0
while $listnext(dependencyModuleList, ptr, dependencyModule){
do $$$LogMessage("Processing: "_dependencyModule_" ptr:"_ptr)
set dependencyModuleDir = ##class(%File).NormalizeDirectory(Directory_"/.modules/"_dependencyModule)
set status = ##class(%File).CreateDirectoryChain(dependencyModuleDir)
do $$$AssertStatusOK(status, ".module directory "_dependencyModuleDir_" created for creating the dependency modules")

do $$$LogMessage("Creating "_dependencyModule_" for loading into main module 'demo-module1'")
do ..CreateModuleFile(dependencyModuleDir, dependencyModule)
do $$$LogMessage("module file creation successful'")
}
}
}

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>
<Dependencies>
<ModuleReference>
<Name>test-dependency-1</Name>
<Version>1.0.0</Version>
</ModuleReference>
<ModuleReference>
<Name>test-dependency-2</Name>
<Version>1.0.0</Version>
</ModuleReference>
<ModuleReference>
<Name>test-dependency-3</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>
<Dependencies>
<ModuleReference>
<Name>test-dependency-2</Name>
<Version>1.0.0</Version>
</ModuleReference>
<ModuleReference>
<Name>test-dependency-3</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>
<Dependencies>
<ModuleReference>
<Name>test-dependency-3</Name>
<Version>1.0.0</Version>
</ModuleReference>
</Dependencies>
<Packaging>module</Packaging>
<Default Name="count" Value="7"/>
<SourcesRoot>src</SourcesRoot>
</Module>
</Document>
</Export>
}

/// dependency modules for testing
XData DepModule1
{
<?xml version="1.0" encoding="UTF-8"?>
<Export generator="Cache" version="25">
<Document name="test-dependency-1.ZPM">
<Module>
<Name>test-dependency-1</Name>
<Version>1.0.0</Version>
<Packaging>module</Packaging>
</Module>
</Document>
</Export>
}

XData DepModule2
{
<?xml version="1.0" encoding="UTF-8"?>
<Export generator="Cache" version="25">
<Document name="test-dependency-2.ZPM">
<Module>
<Name>test-dependency-2</Name>
<Version>1.0.0</Version>
<Packaging>module</Packaging>
</Module>
</Document>
</Export>
}

XData DepModule3
{
<?xml version="1.0" encoding="UTF-8"?>
<Export generator="Cache" version="25">
<Document name="test-dependency-3.ZPM">
<Module>
<Name>test-dependency-3</Name>
<Version>1.0.0</Version>
<Packaging>module</Packaging>
</Module>
</Document>
</Export>
}

}