diff --git a/CHANGELOG.md b/CHANGELOG.md index 05e3cb72..3298d94c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #1122: Packaging should recognize resources in dependency modules set to deploy - #1119: The update command should check version requirements using post-update values instead of what's currently installed - #1097: The Test resource processor now supports nested tests +- #1128: Fixed an issue where an update can fail if a resource is moved from one module to another ## [0.10.6] - 2026-02-24 diff --git a/src/cls/IPM/Storage/Module.cls b/src/cls/IPM/Storage/Module.cls index 05748a45..621e2c5d 100644 --- a/src/cls/IPM/Storage/Module.cls +++ b/src/cls/IPM/Storage/Module.cls @@ -1336,13 +1336,6 @@ Method GetResolvedReferences( { set tSC = $$$OK try { - if '$data(pDependencyGraph) { - set tSC = ..BuildDependencyGraph(.pDependencyGraph,,,,pPhases) - if $$$ISERR(tSC) { - quit - } - } - set pReferenceArray(..Name, ..Name_".ZPM") = "" set orderedResourceList = ..GetOrderedResourceList() @@ -1371,6 +1364,12 @@ Method GetResolvedReferences( } if pLockedDependencies { + if '$data(pDependencyGraph) { + set tSC = ..BuildDependencyGraph(.pDependencyGraph,,,,pPhases) + if $$$ISERR(tSC) { + quit + } + } set tModName = "" for { set tModName = $order(pDependencyGraph(tModName)) diff --git a/src/cls/IPM/Storage/ResourceReference.cls b/src/cls/IPM/Storage/ResourceReference.cls index 0a7d977e..b3813577 100644 --- a/src/cls/IPM/Storage/ResourceReference.cls +++ b/src/cls/IPM/Storage/ResourceReference.cls @@ -54,6 +54,14 @@ Property Children As array Of %String(MAXLEN = 1, XMLPROJECTION = "NONE"); Index Children On Children(KEYS); +/// Tells us if this resource belongs to a module that will be processed as a part of the "Update" command +/// If set to true, %OnBeforeSave() will not error if another resource of the same name tries to be saved +/// and will instead save that resource with the other module as new +/// This is useful for an "Update" command where a resource was moved from one module to another. +/// If the resource's new module gets updated first, we don't want to error on a resource listed in two modules +/// when it will only be in one in the final environment once the resource's previous home gets updated +Property MarkedForUpdate As %Boolean [ InitialExpression = 0 ]; + Method ProcessorGet() As %IPM.ResourceProcessor.Abstract { // Similar to LifecycleGet in Module. @@ -344,6 +352,21 @@ ClassMethod NonNullResourceNameOpen( quit tResult } +/// When given a name for a resource, set its MarkedForUpdate flag +/// See property description for more details on the usage of MarkedForUpdate +ClassMethod MarkResourceForUpdate( + resourceName As %String, + marking As %Boolean = 1) +{ + set resource = ..NonNullResourceNameOpen(resourceName,,.sc) + if (resource = $$$NULLOREF) { + // If unable to find a resource with this name, do nothing + quit + } + set resource.MarkedForUpdate = marking + $$$ThrowOnError(resource.%Save()) +} + ClassMethod GetStatus( InternalName As %String, Output pReferenced As %Boolean, @@ -432,6 +455,11 @@ Method %OnBeforeSave(insert As %Boolean) As %Status [ Private, ServerOnly = 1 ] if $$$ISERR(tSC) { quit } + if (tOtherInstance.MarkedForUpdate) { + // If other instance marked for update, then we should overwrite it with this instead of throwing an error + $$$ThrowOnError(..%DeleteId(tExistingID)) + quit + } if (tOtherInstance.Module.Name = ..Module.Name) { set tMsg = $$$FormatText("Resource '%1' is listed more than once in module '%2'",..Name,..Module.Name) } else { @@ -542,6 +570,9 @@ Storage Default IsAPI + +MarkedForUpdate + {%%PARENT}("Resources") ResourceReferenceDefaultData diff --git a/src/cls/IPM/Utils/Module.cls b/src/cls/IPM/Utils/Module.cls index d4192896..697b1964 100644 --- a/src/cls/IPM/Utils/Module.cls +++ b/src/cls/IPM/Utils/Module.cls @@ -1173,6 +1173,26 @@ ClassMethod LoadNewModule( } } + // Mark relevant resources for update + if (commandLineModuleName '= "") && ($get(params("cmd")) = "update") { + set tSC = moduleCurrent.GetResolvedReferences(.resourceArray, 1) + set moduleKey = "" + for { + set moduleKey = $order(resourceArray(moduleKey)) + if (moduleKey = "") { + quit + } + set resourceKey = "" + for { + set resourceKey = $order(resourceArray(moduleKey, resourceKey)) + if (resourceKey = "") { + quit + } + do ##class(%IPM.Storage.ResourceReference).MarkResourceForUpdate(resourceKey) + } + } + } + // 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) diff --git a/tests/integration_tests/Test/PM/Integration/Update.cls b/tests/integration_tests/Test/PM/Integration/Update.cls index b0090791..b64be723 100644 --- a/tests/integration_tests/Test/PM/Integration/Update.cls +++ b/tests/integration_tests/Test/PM/Integration/Update.cls @@ -680,6 +680,8 @@ Method TestDependencyVersionsUpdated() set modBPreVersion = "1.0.0" set modCPreVersion = "5.6.45+snapshot" + set modAErrorVersion = "2.0.5+snapshot" + set modAPostVersion = "2.1.0+snapshot" set modBPostVersion = "2.0.0+snapshot" set modCPostVersion = "6.2.0+snapshot" @@ -695,6 +697,7 @@ Method TestDependencyVersionsUpdated() do $$$AssertEquals(mod.Version.ToString(), modBPreVersion, "Module " _ modB _ " correctly installed as " _ modBPreVersion) set mod = ##class(%IPM.Storage.Module).NameOpen(modC) do $$$AssertEquals(mod.Version.ToString(), modCPreVersion, "Module " _ modC _ " correctly installed as " _ modCPreVersion) + kill mod // Install module-d prior to update set sc = ##class(%IPM.Main).Shell("install -v " _ modD) @@ -704,9 +707,15 @@ Method TestDependencyVersionsUpdated() set sc = ##class(%IPM.Main).Shell("update -v " _ modA _ " " _ modAPostVersion) do $$$AssertStatusNotOK(sc, "Updating " _ modA _ " fails due to version errors with previously installed " _ modD) - // Uninstall module-d then try the update again + // Uninstall module-d since we don't need it anymore set sc = ##class(%IPM.Main).Shell("uninstall -v " _ modD) do $$$AssertStatusOK(sc, "Successfully uninstalled " _ modD) + + // Try updating module-a to a version that will fail with same resource as in module-c + set sc = ##class(%IPM.Main).Shell("update -v " _ modA _ " " _ modAErrorVersion) + do $$$AssertStatusNotOK(sc, "Updating " _ modA _ " to version " _ modAErrorVersion _ " fails due to same resource in modules a and c") + + // Not do an update that should succeed set sc = ##class(%IPM.Main).Shell("update -v " _ modA _ " " _ modAPostVersion) do $$$AssertStatusOK(sc, "Successfully updated " _ modA _ " and its dependencies") diff --git a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-a/cls/Shared/Class2.cls b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-a/cls/Shared/Class2.cls new file mode 100644 index 00000000..6951bb00 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-a/cls/Shared/Class2.cls @@ -0,0 +1,9 @@ +Class Shared.Class2 +{ + +ClassMethod MethodA() +{ + write !, "This is ##class(Shared.Class2).MethodA() in module-a" +} + +} diff --git a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-a/module.xml b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-a/module.xml index d5f02ab0..4e1f6e14 100644 --- a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-a/module.xml +++ b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-a/module.xml @@ -5,6 +5,7 @@ module-a 2.1.0+snapshot + module-b diff --git a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-c/cls/Shared/Class1.cls b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-c/cls/Shared/Class1.cls new file mode 100644 index 00000000..60235a6a --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-c/cls/Shared/Class1.cls @@ -0,0 +1,9 @@ +Class Shared.Class1 +{ + +ClassMethod MethodA() +{ + write !, "This is ##class(Shared.Class1).MethodA() in module-c" +} + +} diff --git a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-c/module.xml b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-c/module.xml index 7c0928ff..668b3d86 100644 --- a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-c/module.xml +++ b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/post-update/module-c/module.xml @@ -5,6 +5,7 @@ module-c 6.2.0+snapshot + \ No newline at end of file diff --git a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-b/cls/Shared/Class1.cls b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-b/cls/Shared/Class1.cls new file mode 100644 index 00000000..7608c613 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-b/cls/Shared/Class1.cls @@ -0,0 +1,9 @@ +Class Shared.Class1 +{ + +ClassMethod MethodA() +{ + write !, "This is ##class(Shared.Class1).MethodA() in module-b" +} + +} diff --git a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-b/module.xml b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-b/module.xml index c5376a6f..0e897412 100644 --- a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-b/module.xml +++ b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-b/module.xml @@ -5,6 +5,7 @@ module-b 1.0.0 + module-c diff --git a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-c/cls/Shared/Class2.cls b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-c/cls/Shared/Class2.cls new file mode 100644 index 00000000..18a706d9 --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-c/cls/Shared/Class2.cls @@ -0,0 +1,9 @@ +Class Shared.Class2 +{ + +ClassMethod MethodA() +{ + write !, "This is ##class(Shared.Class2).MethodA() in module-c" +} + +} diff --git a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-c/module.xml b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-c/module.xml index 293705f5..b639d569 100644 --- a/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-c/module.xml +++ b/tests/integration_tests/Test/PM/Integration/_data/update-test/dependency-versions-updated/pre-update/module-c/module.xml @@ -5,6 +5,7 @@ module-c 5.6.45+snapshot + \ No newline at end of file