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