From 8a1fd633ab4f2f7097efad1bde2f3ea8cc6b608e Mon Sep 17 00:00:00 2001 From: Yentl Frickx Date: Fri, 5 Sep 2025 14:55:46 +0200 Subject: [PATCH 1/5] Add upgrade command for in-place-upgrade --- cmd/aem/instance.go | 27 +++++++ pkg/instance_manager.go | 1 + pkg/local_instance.go | 132 +++++++++++++++++++++++----------- pkg/local_instance_manager.go | 26 ++++++- 4 files changed, 144 insertions(+), 42 deletions(-) diff --git a/cmd/aem/instance.go b/cmd/aem/instance.go index 5c1cf37b..10bf97b1 100644 --- a/cmd/aem/instance.go +++ b/cmd/aem/instance.go @@ -24,6 +24,7 @@ func (c *CLI) instanceCmd() *cobra.Command { cmd.AddCommand(c.instanceAwaitCmd()) cmd.AddCommand(c.instanceBackupCmd()) cmd.AddCommand(c.instanceImportCmd()) + cmd.AddCommand(c.instanceUpgradeCmd()) return cmd } @@ -142,6 +143,32 @@ func (c *CLI) instanceCreateCmd() *cobra.Command { } } +func (c *CLI) instanceUpgradeCmd() *cobra.Command { + return &cobra.Command{ + Use: "upgrade", + Short: "Upgrades AEM instance(s) if needed", + Aliases: []string{"update"}, + Run: func(cmd *cobra.Command, args []string) { + localInstances, err := c.aem.InstanceManager().SomeLocals() + if err != nil { + c.Error(err) + return + } + upgradedInstances, err := c.aem.InstanceManager().Upgrade(localInstances) + if err != nil { + c.Error(err) + return + } + c.SetOutput("upgraded", upgradedInstances) + if len(upgradedInstances) > 0 { + c.Changed(fmt.Sprintf("upgraded instance(s) (%d)", len(upgradedInstances))) + } else { + c.Ok("no instance(s) to upgrade") + } + }, + } +} + func (c *CLI) instanceStartCmd() *cobra.Command { return &cobra.Command{ Use: "start", diff --git a/pkg/instance_manager.go b/pkg/instance_manager.go index 893ee774..5b443f4a 100644 --- a/pkg/instance_manager.go +++ b/pkg/instance_manager.go @@ -159,6 +159,7 @@ func (im *InstanceManager) newFromConfig(id string) *Instance { i.local.SecretVars = cv.GetStringSlice(fmt.Sprintf("instance.config.%s.secret_vars", id)) i.local.SlingProps = cv.GetStringSlice(fmt.Sprintf("instance.config.%s.sling_props", id)) i.local.UnpackDir = cv.GetString(fmt.Sprintf("instance.config.%s.unpack_dir", id)) + i.local.AllowInPlaceUpgrade = cv.GetBool(fmt.Sprintf("instance.config.%s.allow_in_place_upgrade", id)) } return i } diff --git a/pkg/local_instance.go b/pkg/local_instance.go index 09cc606e..34f61499 100644 --- a/pkg/local_instance.go +++ b/pkg/local_instance.go @@ -29,14 +29,15 @@ import ( type LocalInstance struct { instance *Instance - Version string - JvmOpts []string - StartOpts []string - RunModes []string - EnvVars []string - SecretVars []string - SlingProps []string - UnpackDir string + Version string + JvmOpts []string + StartOpts []string + RunModes []string + EnvVars []string + SecretVars []string + SlingProps []string + UnpackDir string + AllowInPlaceUpgrade bool } type LocalInstanceState struct { @@ -50,15 +51,16 @@ type LocalInstanceState struct { } const ( - LocalInstanceScriptStart = "start" - LocalInstanceScriptStop = "stop" - LocalInstanceScriptStatus = "status" - LocalInstanceBackupExtension = "aemb.tar.zst" - LocalInstanceUser = "admin" - LocalInstanceWorkDirName = common.AppId - LocalInstanceNameCommon = "common" - LocalInstanceSecretsDir = "conf/secret" - LocalInstanceVersionDefault = "1" + LocalInstanceScriptStart = "start" + LocalInstanceScriptStop = "stop" + LocalInstanceScriptStatus = "status" + LocalInstanceBackupExtension = "aemb.tar.zst" + LocalInstanceUser = "admin" + LocalInstanceWorkDirName = common.AppId + LocalInstanceNameCommon = "common" + LocalInstanceSecretsDir = "conf/secret" + LocalInstanceVersionDefault = "1" + LocalInstanceAllowInPlaceUpgradeDefault = false ) func (li LocalInstance) Instance() *Instance { @@ -74,6 +76,7 @@ func NewLocal(i *Instance) *LocalInstance { li.EnvVars = []string{} li.SecretVars = []string{} li.SlingProps = []string{} + li.AllowInPlaceUpgrade = LocalInstanceAllowInPlaceUpgradeDefault return li } @@ -185,18 +188,21 @@ var ( LocalInstancePasswordRegex = regexp.MustCompile("^[a-zA-Z0-9_]{5,}$") ) -func (li LocalInstance) CheckRecreationNeeded() error { +func (li LocalInstance) CheckUpgradeNeeded() (bool, error) { createLock := li.createLock() if createLock.IsLocked() { state, err := createLock.State() if err != nil { - return err + return false, err } if !state.UpToDate { - return fmt.Errorf("%s > outdated and need to be recreated as distribution JAR changed from '%s' to '%s'", li.instance.IDColor(), state.Locked.JarName, state.Current.JarName) + if li.AllowInPlaceUpgrade { + return true, nil + } + return true, fmt.Errorf("%s > outdated and need to be upgraded as distribution JAR changed from '%s' to '%s'", li.instance.IDColor(), state.Locked.JarName, state.Current.JarName) } } - return nil + return false, nil } func (li LocalInstance) CheckPassword() error { @@ -241,6 +247,21 @@ func (li LocalInstance) Create() error { return nil } +func (li LocalInstance) Upgrade() error { + log.Infof("%s > upgrading", li.instance.IDColor()) + if err := li.unpackJarFile(); err != nil { + return err + } + if err := li.copyLicenseFile(); err != nil { + return err + } + if err := li.adapt(); err != nil { + return err + } + log.Infof("%s > upgraded", li.instance.IDColor()) + return nil +} + func (li LocalInstance) Import() error { log.Infof("%s > importing", li.instance.IDColor()) @@ -294,6 +315,33 @@ func (li LocalInstance) unpackJarFile() error { if !pathx.Exists(startScript) { return fmt.Errorf("%s > unpacking files went wrong as e.g start script does not exist '%s'", li.instance.IDColor(), startScript) } + + // Only check crx-quickstart/app for JARs + appDir := filepath.Join(li.QuickstartDir(), "app") + jarFiles, err := filepath.Glob(filepath.Join(appDir, "*.jar")) + if err != nil { + return fmt.Errorf("%s > error searching for JAR files in app dir: %w", li.instance.IDColor(), err) + } + if len(jarFiles) > 1 { + type jarInfo struct { + path string + modTime time.Time + } + var jars []jarInfo + for _, path := range jarFiles { + info, err := os.Stat(path) + if err == nil { + jars = append(jars, jarInfo{path, info.ModTime()}) + } + } + sort.Slice(jars, func(i, j int) bool { + return jars[i].modTime.After(jars[j].modTime) + }) + for _, ji := range jars[1:] { + log.Infof("%s > removing outdated JAR from app dir: %s", li.instance.IDColor(), ji.path) + _ = os.Remove(ji.path) + } + } return nil } @@ -538,31 +586,33 @@ func (li LocalInstance) updateLock() osx.Lock[localInstanceUpdateLock] { return zero, err } return localInstanceUpdateLock{ - Version: li.Version, - HTTPPort: li.instance.HTTP().Port(), - RunModes: strings.Join(li.RunModes, ","), - JVMOpts: strings.Join(li.JvmOpts, " "), - JavaHome: javaHomeDir, - Password: cryptox.HashString(li.instance.password), - EnvVars: strings.Join(li.EnvVars, ","), - SecretVars: cryptox.HashString(strings.Join(li.SecretVars, ",")), - SlingProps: strings.Join(li.SlingProps, ","), - Overrides: overrides, + Version: li.Version, + HTTPPort: li.instance.HTTP().Port(), + RunModes: strings.Join(li.RunModes, ","), + JVMOpts: strings.Join(li.JvmOpts, " "), + JavaHome: javaHomeDir, + Password: cryptox.HashString(li.instance.password), + EnvVars: strings.Join(li.EnvVars, ","), + SecretVars: cryptox.HashString(strings.Join(li.SecretVars, ",")), + SlingProps: strings.Join(li.SlingProps, ","), + Overrides: overrides, + AllowInPlaceUpgrade: li.AllowInPlaceUpgrade, }, nil }) } type localInstanceUpdateLock struct { - Version string `yaml:"version"` - JVMOpts string `yaml:"jvm_opts"` - JavaHome string `yaml:"java_home"` - RunModes string `yaml:"run_modes"` - HTTPPort string `yaml:"http_port"` - Password string `yaml:"password"` - Overrides string `yaml:"overrides"` - EnvVars string `yaml:"env_vars"` - SecretVars string `yaml:"secret_vars"` - SlingProps string `yaml:"sling_props"` + Version string `yaml:"version"` + JVMOpts string `yaml:"jvm_opts"` + JavaHome string `yaml:"java_home"` + RunModes string `yaml:"run_modes"` + HTTPPort string `yaml:"http_port"` + Password string `yaml:"password"` + Overrides string `yaml:"overrides"` + EnvVars string `yaml:"env_vars"` + SecretVars string `yaml:"secret_vars"` + SlingProps string `yaml:"sling_props"` + AllowInPlaceUpgrade bool `yaml:"allow_in_place_upgrade"` } func (li LocalInstance) Stop() error { diff --git a/pkg/local_instance_manager.go b/pkg/local_instance_manager.go index 5f99fe83..7fc507e1 100644 --- a/pkg/local_instance_manager.go +++ b/pkg/local_instance_manager.go @@ -72,7 +72,7 @@ func (o *LocalOpts) Initialize() error { } // post-validation phase for _, instance := range o.manager.Locals() { - if err := instance.Local().CheckRecreationNeeded(); err != nil { // depends on SDK prepare + if _, err := instance.Local().CheckUpgradeNeeded(); err != nil { // depends on SDK prepare return err } } @@ -129,6 +129,10 @@ func (im *InstanceManager) CreateAll() ([]Instance, error) { return im.Create(im.Locals()) } +func (im *InstanceManager) UpgradeAll() ([]Instance, error) { + return im.Upgrade(im.Locals()) +} + func (im *InstanceManager) Create(instances []Instance) ([]Instance, error) { created := []Instance{} if err := im.LocalOpts.Initialize(); err != nil { @@ -147,6 +151,26 @@ func (im *InstanceManager) Create(instances []Instance) ([]Instance, error) { return created, nil } +func (im *InstanceManager) Upgrade(instances []Instance) ([]Instance, error) { + upgraded := []Instance{} + if err := im.LocalOpts.Initialize(); err != nil { + return upgraded, err + } + log.Info(InstancesMsg(instances, "upgrading")) + for _, i := range instances { + if !i.local.IsCreated() { + return nil, fmt.Errorf("instance not yet created: %s", i.IDColor()) + } else if upgradeNeeded, _ := i.local.CheckUpgradeNeeded(); upgradeNeeded { + err := i.local.Upgrade() + if err != nil { + return nil, err + } + upgraded = append(upgraded, i) + } + } + return upgraded, nil +} + func (im *InstanceManager) Import(instances []Instance) ([]Instance, error) { imported := []Instance{} log.Info(InstancesMsg(instances, "importing")) From 0a9752ce241f5f8cfa5099657c4d4b0056c986dc Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Tue, 10 Feb 2026 10:10:06 +0100 Subject: [PATCH 2/5] Upgrade polished --- pkg/instance_manager.go | 1 - pkg/local_instance.go | 100 ++++++++++++++++++---------------- pkg/local_instance_manager.go | 27 +++++++-- 3 files changed, 73 insertions(+), 55 deletions(-) diff --git a/pkg/instance_manager.go b/pkg/instance_manager.go index 5b443f4a..893ee774 100644 --- a/pkg/instance_manager.go +++ b/pkg/instance_manager.go @@ -159,7 +159,6 @@ func (im *InstanceManager) newFromConfig(id string) *Instance { i.local.SecretVars = cv.GetStringSlice(fmt.Sprintf("instance.config.%s.secret_vars", id)) i.local.SlingProps = cv.GetStringSlice(fmt.Sprintf("instance.config.%s.sling_props", id)) i.local.UnpackDir = cv.GetString(fmt.Sprintf("instance.config.%s.unpack_dir", id)) - i.local.AllowInPlaceUpgrade = cv.GetBool(fmt.Sprintf("instance.config.%s.allow_in_place_upgrade", id)) } return i } diff --git a/pkg/local_instance.go b/pkg/local_instance.go index 34f61499..be76e041 100644 --- a/pkg/local_instance.go +++ b/pkg/local_instance.go @@ -29,15 +29,14 @@ import ( type LocalInstance struct { instance *Instance - Version string - JvmOpts []string - StartOpts []string - RunModes []string - EnvVars []string - SecretVars []string - SlingProps []string - UnpackDir string - AllowInPlaceUpgrade bool + Version string + JvmOpts []string + StartOpts []string + RunModes []string + EnvVars []string + SecretVars []string + SlingProps []string + UnpackDir string } type LocalInstanceState struct { @@ -51,16 +50,15 @@ type LocalInstanceState struct { } const ( - LocalInstanceScriptStart = "start" - LocalInstanceScriptStop = "stop" - LocalInstanceScriptStatus = "status" - LocalInstanceBackupExtension = "aemb.tar.zst" - LocalInstanceUser = "admin" - LocalInstanceWorkDirName = common.AppId - LocalInstanceNameCommon = "common" - LocalInstanceSecretsDir = "conf/secret" - LocalInstanceVersionDefault = "1" - LocalInstanceAllowInPlaceUpgradeDefault = false + LocalInstanceScriptStart = "start" + LocalInstanceScriptStop = "stop" + LocalInstanceScriptStatus = "status" + LocalInstanceBackupExtension = "aemb.tar.zst" + LocalInstanceUser = "admin" + LocalInstanceWorkDirName = common.AppId + LocalInstanceNameCommon = "common" + LocalInstanceSecretsDir = "conf/secret" + LocalInstanceVersionDefault = "1" ) func (li LocalInstance) Instance() *Instance { @@ -76,7 +74,6 @@ func NewLocal(i *Instance) *LocalInstance { li.EnvVars = []string{} li.SecretVars = []string{} li.SlingProps = []string{} - li.AllowInPlaceUpgrade = LocalInstanceAllowInPlaceUpgradeDefault return li } @@ -188,19 +185,28 @@ var ( LocalInstancePasswordRegex = regexp.MustCompile("^[a-zA-Z0-9_]{5,}$") ) -func (li LocalInstance) CheckUpgradeNeeded() (bool, error) { +func (li LocalInstance) CheckRecreationNeeded() error { createLock := li.createLock() if createLock.IsLocked() { state, err := createLock.State() if err != nil { - return false, err + return err } if !state.UpToDate { - if li.AllowInPlaceUpgrade { - return true, nil - } - return true, fmt.Errorf("%s > outdated and need to be upgraded as distribution JAR changed from '%s' to '%s'", li.instance.IDColor(), state.Locked.JarName, state.Current.JarName) + return fmt.Errorf("%s > outdated and need to be upgraded as distribution JAR changed from '%s' to '%s'; consider using 'aem instance upgrade' command", li.instance.IDColor(), state.Locked.JarName, state.Current.JarName) + } + } + return nil +} + +func (li LocalInstance) IsUpgradeNeeded() (bool, error) { + createLock := li.createLock() + if createLock.IsLocked() { + state, err := createLock.State() + if err != nil { + return false, err } + return !state.UpToDate, nil } return false, nil } @@ -586,33 +592,31 @@ func (li LocalInstance) updateLock() osx.Lock[localInstanceUpdateLock] { return zero, err } return localInstanceUpdateLock{ - Version: li.Version, - HTTPPort: li.instance.HTTP().Port(), - RunModes: strings.Join(li.RunModes, ","), - JVMOpts: strings.Join(li.JvmOpts, " "), - JavaHome: javaHomeDir, - Password: cryptox.HashString(li.instance.password), - EnvVars: strings.Join(li.EnvVars, ","), - SecretVars: cryptox.HashString(strings.Join(li.SecretVars, ",")), - SlingProps: strings.Join(li.SlingProps, ","), - Overrides: overrides, - AllowInPlaceUpgrade: li.AllowInPlaceUpgrade, + Version: li.Version, + HTTPPort: li.instance.HTTP().Port(), + RunModes: strings.Join(li.RunModes, ","), + JVMOpts: strings.Join(li.JvmOpts, " "), + JavaHome: javaHomeDir, + Password: cryptox.HashString(li.instance.password), + EnvVars: strings.Join(li.EnvVars, ","), + SecretVars: cryptox.HashString(strings.Join(li.SecretVars, ",")), + SlingProps: strings.Join(li.SlingProps, ","), + Overrides: overrides, }, nil }) } type localInstanceUpdateLock struct { - Version string `yaml:"version"` - JVMOpts string `yaml:"jvm_opts"` - JavaHome string `yaml:"java_home"` - RunModes string `yaml:"run_modes"` - HTTPPort string `yaml:"http_port"` - Password string `yaml:"password"` - Overrides string `yaml:"overrides"` - EnvVars string `yaml:"env_vars"` - SecretVars string `yaml:"secret_vars"` - SlingProps string `yaml:"sling_props"` - AllowInPlaceUpgrade bool `yaml:"allow_in_place_upgrade"` + Version string `yaml:"version"` + JVMOpts string `yaml:"jvm_opts"` + JavaHome string `yaml:"java_home"` + RunModes string `yaml:"run_modes"` + HTTPPort string `yaml:"http_port"` + Password string `yaml:"password"` + Overrides string `yaml:"overrides"` + EnvVars string `yaml:"env_vars"` + SecretVars string `yaml:"secret_vars"` + SlingProps string `yaml:"sling_props"` } func (li LocalInstance) Stop() error { diff --git a/pkg/local_instance_manager.go b/pkg/local_instance_manager.go index 7fc507e1..9dfe3aab 100644 --- a/pkg/local_instance_manager.go +++ b/pkg/local_instance_manager.go @@ -3,15 +3,16 @@ package pkg import ( "bytes" "fmt" + "os" + "strings" + "time" + "github.com/dustin/go-humanize" "github.com/samber/lo" log "github.com/sirupsen/logrus" "github.com/wttech/aemc/pkg/common/fmtx" "github.com/wttech/aemc/pkg/common/pathx" "github.com/wttech/aemc/pkg/common/timex" - "os" - "strings" - "time" ) type LocalOpts struct { @@ -70,9 +71,12 @@ func (o *LocalOpts) Initialize() error { if _, err := o.manager.aem.vendorManager.oakRun.PrepareWithChanged(); err != nil { return err } - // post-validation phase + return nil +} + +func (o *LocalOpts) CheckRecreationNeeded() error { for _, instance := range o.manager.Locals() { - if _, err := instance.Local().CheckUpgradeNeeded(); err != nil { // depends on SDK prepare + if err := instance.Local().CheckRecreationNeeded(); err != nil { return err } } @@ -138,6 +142,9 @@ func (im *InstanceManager) Create(instances []Instance) ([]Instance, error) { if err := im.LocalOpts.Initialize(); err != nil { return created, err } + if err := im.LocalOpts.CheckRecreationNeeded(); err != nil { + return created, err + } log.Info(InstancesMsg(instances, "creating")) for _, i := range instances { if !i.local.IsCreated() { @@ -160,7 +167,12 @@ func (im *InstanceManager) Upgrade(instances []Instance) ([]Instance, error) { for _, i := range instances { if !i.local.IsCreated() { return nil, fmt.Errorf("instance not yet created: %s", i.IDColor()) - } else if upgradeNeeded, _ := i.local.CheckUpgradeNeeded(); upgradeNeeded { + } + upgradeNeeded, err := i.local.IsUpgradeNeeded() + if err != nil { + return nil, err + } + if upgradeNeeded { err := i.local.Upgrade() if err != nil { return nil, err @@ -233,6 +245,9 @@ func (im *InstanceManager) Start(instances []Instance) ([]Instance, error) { if err := im.LocalOpts.Initialize(); err != nil { return []Instance{}, err } + if err := im.LocalOpts.CheckRecreationNeeded(); err != nil { + return []Instance{}, err + } if !im.LocalOpts.ServiceMode { log.Info(InstancesMsg(instances, "checking started & out-of-date")) From 83cbfb3e274df9fd11792641dd713337e7a041ac Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Tue, 10 Feb 2026 10:23:24 +0100 Subject: [PATCH 3/5] Upgrade protected --- pkg/local_instance.go | 79 +++++++++++++++++++++++++++++-------------- 1 file changed, 53 insertions(+), 26 deletions(-) diff --git a/pkg/local_instance.go b/pkg/local_instance.go index be76e041..638bade5 100644 --- a/pkg/local_instance.go +++ b/pkg/local_instance.go @@ -2,6 +2,15 @@ package pkg import ( "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" + "time" + "github.com/magiconair/properties" "github.com/wttech/aemc/pkg/common" "github.com/wttech/aemc/pkg/common/cryptox" @@ -12,14 +21,6 @@ import ( "github.com/wttech/aemc/pkg/common/stringsx" "github.com/wttech/aemc/pkg/common/timex" "github.com/wttech/aemc/pkg/instance" - "os" - "os/exec" - "path/filepath" - "regexp" - "sort" - "strconv" - "strings" - "time" "github.com/samber/lo" log "github.com/sirupsen/logrus" @@ -253,8 +254,22 @@ func (li LocalInstance) Create() error { return nil } +// Upgrade performs in-place upgrade of the AEM instance. +// See: https://experienceleague.adobe.com/en/docs/experience-manager-65/content/implementing/deploying/upgrading/in-place-upgrade func (li LocalInstance) Upgrade() error { + if !li.IsCreated() { + return fmt.Errorf("%s > cannot upgrade as it is not created", li.instance.IDColor()) + } + if li.IsRunning() { + return fmt.Errorf("%s > cannot upgrade as it is running", li.instance.IDColor()) + } log.Infof("%s > upgrading", li.instance.IDColor()) + + // Reset init lock to force re-initialization of sling.properties and other configs on next start. + // This is needed because unpack overwrites sling.properties with default values from the new JAR + if err := li.initLock().Unlock(); err != nil { + return err + } if err := li.unpackJarFile(); err != nil { return err } @@ -264,6 +279,7 @@ func (li LocalInstance) Upgrade() error { if err := li.adapt(); err != nil { return err } + log.Infof("%s > upgraded", li.instance.IDColor()) return nil } @@ -321,31 +337,42 @@ func (li LocalInstance) unpackJarFile() error { if !pathx.Exists(startScript) { return fmt.Errorf("%s > unpacking files went wrong as e.g start script does not exist '%s'", li.instance.IDColor(), startScript) } + if err := li.cleanupOutdatedAppJars(); err != nil { + return err + } + return nil +} - // Only check crx-quickstart/app for JARs +// cleanupOutdatedAppJars removes old JAR files from crx-quickstart/app after upgrade. +// When upgrading AEM, the new quickstart JAR unpacks a new app JAR but leaves the old one. +// This causes conflicts, so we keep only the newest JAR file. +func (li LocalInstance) cleanupOutdatedAppJars() error { appDir := filepath.Join(li.QuickstartDir(), "app") jarFiles, err := filepath.Glob(filepath.Join(appDir, "*.jar")) if err != nil { return fmt.Errorf("%s > error searching for JAR files in app dir: %w", li.instance.IDColor(), err) } - if len(jarFiles) > 1 { - type jarInfo struct { - path string - modTime time.Time - } - var jars []jarInfo - for _, path := range jarFiles { - info, err := os.Stat(path) - if err == nil { - jars = append(jars, jarInfo{path, info.ModTime()}) - } + if len(jarFiles) <= 1 { + return nil + } + type jarInfo struct { + path string + modTime time.Time + } + var jars []jarInfo + for _, path := range jarFiles { + info, err := os.Stat(path) + if err == nil { + jars = append(jars, jarInfo{path, info.ModTime()}) } - sort.Slice(jars, func(i, j int) bool { - return jars[i].modTime.After(jars[j].modTime) - }) - for _, ji := range jars[1:] { - log.Infof("%s > removing outdated JAR from app dir: %s", li.instance.IDColor(), ji.path) - _ = os.Remove(ji.path) + } + sort.Slice(jars, func(i, j int) bool { + return jars[i].modTime.After(jars[j].modTime) + }) + for _, ji := range jars[1:] { + log.Infof("%s > removing outdated JAR from app dir: %s", li.instance.IDColor(), ji.path) + if err := os.Remove(ji.path); err != nil { + return fmt.Errorf("%s > cannot remove outdated JAR '%s': %w", li.instance.IDColor(), ji.path, err) } } return nil From 299df33d5af4316cecfe008d8d15e13d13b94bd9 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Tue, 10 Feb 2026 10:24:15 +0100 Subject: [PATCH 4/5] Minor --- pkg/local_instance.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/local_instance.go b/pkg/local_instance.go index 638bade5..d771e438 100644 --- a/pkg/local_instance.go +++ b/pkg/local_instance.go @@ -194,7 +194,7 @@ func (li LocalInstance) CheckRecreationNeeded() error { return err } if !state.UpToDate { - return fmt.Errorf("%s > outdated and need to be upgraded as distribution JAR changed from '%s' to '%s'; consider using 'aem instance upgrade' command", li.instance.IDColor(), state.Locked.JarName, state.Current.JarName) + return fmt.Errorf("%s > outdated and need to be upgraded as distribution JAR changed from '%s' to '%s'; consider using upgrade command", li.instance.IDColor(), state.Locked.JarName, state.Current.JarName) } } return nil From 512c6b546162c5027a57f548563f80a13bb9170c Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Tue, 10 Feb 2026 11:13:19 +0100 Subject: [PATCH 5/5] HArdening --- local.env | 2 +- pkg/local_instance.go | 47 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/local.env b/local.env index 8efb421c..1d0e8537 100755 --- a/local.env +++ b/local.env @@ -8,4 +8,4 @@ AEM_AUTHOR_DEBUG_ADDR=127.0.0.1:14502 AEM_PUBLISH_USER=admin AEM_PUBLISH_PASSWORD=admin AEM_PUBLISH_HTTP_URL=http://localhost:4503 -AEM_PUBLISH_DEBUG_ADDR=127.0.0.1:14503 +AEM_PUBLISH_DEBUG_ADDR=127.0.0.1:14503 \ No newline at end of file diff --git a/pkg/local_instance.go b/pkg/local_instance.go index d771e438..f56efdd2 100644 --- a/pkg/local_instance.go +++ b/pkg/local_instance.go @@ -51,15 +51,16 @@ type LocalInstanceState struct { } const ( - LocalInstanceScriptStart = "start" - LocalInstanceScriptStop = "stop" - LocalInstanceScriptStatus = "status" - LocalInstanceBackupExtension = "aemb.tar.zst" - LocalInstanceUser = "admin" - LocalInstanceWorkDirName = common.AppId - LocalInstanceNameCommon = "common" - LocalInstanceSecretsDir = "conf/secret" - LocalInstanceVersionDefault = "1" + LocalInstanceScriptStart = "start" + LocalInstanceScriptStop = "stop" + LocalInstanceScriptStatus = "status" + LocalInstanceScriptQuickstart = "quickstart" + LocalInstanceBackupExtension = "aemb.tar.zst" + LocalInstanceUser = "admin" + LocalInstanceWorkDirName = common.AppId + LocalInstanceNameCommon = "common" + LocalInstanceSecretsDir = "conf/secret" + LocalInstanceVersionDefault = "1" ) func (li LocalInstance) Instance() *Instance { @@ -270,6 +271,10 @@ func (li LocalInstance) Upgrade() error { if err := li.initLock().Unlock(); err != nil { return err } + // Remove old bin scripts so that unpack can replace them with new versions + if err := li.cleanupBinScripts(); err != nil { + return err + } if err := li.unpackJarFile(); err != nil { return err } @@ -378,6 +383,30 @@ func (li LocalInstance) cleanupOutdatedAppJars() error { return nil } +// cleanupBinScripts removes old bin scripts before upgrade so that unpack can replace them with new versions. +// Without this, AEM unpack detects modified files and creates .NEW_ copies instead of overwriting. +func (li LocalInstance) cleanupBinScripts() error { + binDir := filepath.Join(li.QuickstartDir(), "bin") + if !pathx.Exists(binDir) { + return nil + } + scripts := []string{ + LocalInstanceScriptStart, LocalInstanceScriptStart + ".bat", + LocalInstanceScriptStop, LocalInstanceScriptStop + ".bat", + LocalInstanceScriptStatus, LocalInstanceScriptStatus + ".bat", + LocalInstanceScriptQuickstart, LocalInstanceScriptQuickstart + ".bat", + } + for _, script := range scripts { + scriptPath := filepath.Join(binDir, script) + if pathx.Exists(scriptPath) { + if err := os.Remove(scriptPath); err != nil { + return fmt.Errorf("%s > cannot remove old bin script '%s': %w", li.instance.IDColor(), scriptPath, err) + } + } + } + return nil +} + func (li LocalInstance) copyLicenseFile() error { sdk, err := li.VendorManager().Quickstart().IsDistSDK() if err != nil {