Repository: argoproj-labs/argocd-image-updater

Issue:
Image Updater overrides Helm values with empty file in multi-source applications
**Describe the bug**
When updating an image in a Helm chart deployed via multi-source application (see below) using the `helmvalues` write-back-target, the image updater overrides the specified values file with an empty file (or creates an empty file if it doesn't exist) instead of writing image names/tags.

**To Reproduce**

Create an application that deploys _both_ a Helm chart and extra resources via kustomize. Have the image updater update an image used in the helm chart using the Git write-back-method and a Helm values write-back-target.

In my minimal reproduction case, I'm using the [Codecentric Keycloakx](https://github.com/codecentric/helm-charts/tree/master/charts/keycloakx) chart with an init container for the image to update. The directory structure in the GitOps repo is this:

```
apps/keycloak-playground/overlays/dev
 |- config.yaml
 |- kustomization.yaml
 |- cm.yaml
 |- values.yaml
```

The application manifest is generated from the `config.yaml` file using an application set (resulting manifest below).

<details>
<summary>Application manifest</summary>

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  annotations:
    argocd-image-updater.argoproj.io/git-repository: https://redacted.example.com/gitops.git
    argocd-image-updater.argoproj.io/image-list: theme=docker.io/hello-world:latest
    argocd-image-updater.argoproj.io/theme.helm.image-name: theme-image.name
    argocd-image-updater.argoproj.io/theme.helm.image-tag: theme-image.tag
    argocd-image-updater.argoproj.io/theme.update-strategy: digest
    argocd-image-updater.argoproj.io/write-back-method: git
    argocd-image-updater.argoproj.io/write-back-target: helmvalues:/apps/keycloak-playground/overlays/dev/theme-version.yaml
  name: keycloak-playground-dev
  namespace: argocd
spec:
  destination:
    namespace: dev
    server: https://kubernetes.default.svc
  project: dev
  sources:
    - path: apps/keycloak-playground/overlays/dev
      repoURL: https://redacted.example.com/gitops.git
      targetRevision: master
    - ref: appGitops
      repoURL: https://redacted.example.com/gitops.git
      targetRevision: master
    - chart: keycloakx
      helm:
        valueFiles:
          - $appGitops/apps/keycloak-playground/overlays/dev/values.yaml
      repoURL: ghcr.io/codecentric/helm-charts
      targetRevision: 7.1.4
```

</details>

<details>
<summary>kustomization.yaml</summary>

```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - cm.yaml
```

</details>

<details>
<summary>cm.yaml</summary>

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: keycloak-test-config
data:
  key: val
```

</details>

<details>
<summary>values.yaml</summary>

```yaml
extraInitContainers: |
  - name: theme
    image: docker.io/hello-world
  
args:
  - start-dev
```

</details>

**Expected behavior**
I expect the image updater to create/update the `theme-version.yaml` file with an image name and tag.

**Additional context**
Add any other context about the problem here.

**Version**
Image Updater: 0.17.0 (chart version 0.13.0)
Argo CD: 3.0.12

**Logs**

This happens on every update cycle:

```
time="2025-10-22T12:47:49Z" level=info msg="Starting configmap/secret informers"
time="2025-10-22T12:47:49Z" level=info msg="Configmap/secret informer synced"
time="2025-10-22T12:49:54Z" level=info msg="Initializing https://redacted.example.com/gitops.git to /tmp/git-keycloak-playground-dev3539967519"
time="2025-10-22T12:49:54Z" level=info msg="secrets informer cancelled"
time="2025-10-22T12:49:54Z" level=info msg="configmap informer cancelled"
time="2025-10-22T12:49:54Z" level=info msg="git fetch origin master --force --prune --depth 1" dir=/tmp/git-keycloak-playground-dev3539967519 execID=275c3
time="2025-10-22T12:49:54Z" level=info msg=Trace args="[git fetch origin master --force --prune --depth 1]" dir=/tmp/git-keycloak-playground-dev3539967519 operation_name="exec git" time_ms=578.967579
time="2025-10-22T12:49:54Z" level=info msg="git checkout --force master" dir=/tmp/git-keycloak-playground-dev3539967519 execID=0ecd9
time="2025-10-22T12:49:54Z" level=info msg=Trace args="[git checkout --force master]" dir=/tmp/git-keycloak-playground-dev3539967519 operation_name="exec git" time_ms=8.943865
time="2025-10-22T12:49:54Z" level=info msg="git clean -ffdx" dir=/tmp/git-keycloak-playground-dev3539967519 execID=391ef
time="2025-10-22T12:49:54Z" level=info msg=Trace args="[git clean -ffdx]" dir=/tmp/git-keycloak-playground-dev3539967519 operation_name="exec git" time_ms=1.4543100000000002
time="2025-10-22T12:49:54Z" level=info msg="Successfully updated the live application spec" application=keycloak-playground-dev
```

No errors/warnings.


Hints from issue comments:
@DeZin7: @JohnnyJayJay Had the same issue and couldnt figure out a solution if using version 0.17.0 which forced me to rollback to 0.14.0 and it started to work properly.  Looking at the v0.17.0 [code](https://github.com/argoproj-labs/argocd-image-updater/blob/master/pkg/argocd/update.go), the issue is likely in how the yaml.Node is being marshaled back. The yaml.Node structure might be getting corrupted or the marshal is producing empty output.

Solution patch:
diff --git a/pkg/argocd/argocd.go b/pkg/argocd/argocd.go
index 862b710..cd8b04f 100644
--- a/pkg/argocd/argocd.go
+++ b/pkg/argocd/argocd.go
@@ -697,6 +697,10 @@ func getApplicationSourceType(app *v1alpha1.Application) v1alpha1.ApplicationSou
 		strings.HasPrefix(st, common.KustomizationPrefix) {
 		return v1alpha1.ApplicationSourceTypeKustomize
 	}
+	if st, set := app.Annotations[common.WriteBackTargetAnnotation]; set &&
+		strings.HasPrefix(st, common.HelmPrefix) {
+		return v1alpha1.ApplicationSourceTypeHelm
+	}
 
 	if app.Spec.HasMultipleSources() {
 		for _, st := range app.Status.SourceTypes {
@@ -718,8 +722,10 @@ func getApplicationSourceType(app *v1alpha1.Application) v1alpha1.ApplicationSou
 func getApplicationSource(app *v1alpha1.Application) *v1alpha1.ApplicationSource {
 
 	if app.Spec.HasMultipleSources() {
+		sourceType := getApplicationSourceType(app)
 		for _, s := range app.Spec.Sources {
-			if s.Helm != nil || s.Kustomize != nil {
+			if (sourceType == v1alpha1.ApplicationSourceTypeKustomize && s.Kustomize != nil) ||
+				(sourceType == v1alpha1.ApplicationSourceTypeHelm && s.Helm != nil) {
 				return &s
 			}
 		}
diff --git a/pkg/argocd/update.go b/pkg/argocd/update.go
index bb9aa97..9de3f60 100644
--- a/pkg/argocd/update.go
+++ b/pkg/argocd/update.go
@@ -455,21 +455,6 @@ func marshalParamsOverride(app *v1alpha1.Application, originalData []byte) ([]by
 	appType := GetApplicationType(app)
 	appSource := getApplicationSource(app)
 
-	// Special handling for multi-source applications with helmvalues write-back target
-	// We need to ensure we get the Helm source, not the Kustomize source
-	if strings.HasPrefix(app.Annotations[common.WriteBackTargetAnnotation], common.HelmPrefix) {
-		// For helmvalues write-back, we need the Helm source specifically
-		if app.Spec.HasMultipleSources() {
-			for _, s := range app.Spec.Sources {
-				if s.Helm != nil {
-					appSource = &s
-					appType = ApplicationTypeHelm
-					break
-				}
-			}
-		}
-	}
-
 	switch appType {
 	case ApplicationTypeKustomize:
 		if appSource.Kustomize == nil {
@@ -546,38 +531,60 @@ func marshalParamsOverride(app *v1alpha1.Application, originalData []byte) ([]by
 				} else {
 					// image-tag annotation is present, so continue to process image-tag
 					helmParamVersion := getHelmParam(appSource.Helm.Parameters, helmAnnotationParamVersion)
-					if helmParamVersion == nil {
-						return nil, fmt.Errorf("%s parameter not found", helmAnnotationParamVersion)
+					var tagValue string
+					if helmParamVersion != nil {
+						tagValue = helmParamVersion.Value
+					} else {
+						// Parameter not found in spec, use the image tag from the container image
+						tagValue = c.GetTagWithDigest()
+						log.WithContext().AddField("application", app.GetName()).
+							Debugf("helm parameter %s not found in spec, using image tag value: %s", helmAnnotationParamVersion, tagValue)
 					}
-					err = setHelmValue(&helmNewValues, helmAnnotationParamVersion, helmParamVersion.Value)
-					if err != nil {
-						return nil, fmt.Errorf("failed to set image parameter version value: %v", err)
+					// Only set the tag value if it's non-empty to avoid invalid image references
+					if tagValue != "" {
+						err = setHelmValue(&helmNewValues, helmAnnotationParamVersion, tagValue)
+						if err != nil {
+							return nil, fmt.Errorf("failed to set image parameter version value: %v", err)
+						}
 					}
 				}
 
 				helmParamName := getHelmParam(appSource.Helm.Parameters, helmAnnotationParamName)
-				if helmParamName == nil {
-					return nil, fmt.Errorf("%s parameter not found", helmAnnotationParamName)
-				}
 
 				// Determine which value to use for the image name parameter
-				valueToSet := helmParamName.Value
-				if !emptyOriginalData && image.HasRegistryPrefix(valueToSet) {
-					// helmParamName.Value is in long form (has registry URL)
-					// Check the original value in helmNewValues to see if it's in short form
-					// Skip this check if originalData is empty
-					originalValue, err := getHelmValue(&helmNewValues, helmAnnotationParamName)
-					if err == nil {
-						// Original value exists and was found
-						if !image.HasRegistryPrefix(originalValue) {
-							// Original value is in short form, use the short form of the value to set
+				var valueToSet string
+				if helmParamName != nil {
+					valueToSet = helmParamName.Value
+					if !emptyOriginalData && image.HasRegistryPrefix(valueToSet) {
+						// helmParamName.Value is in long form (has registry URL)
+						// Check the original value in helmNewValues to see if it's in short form
+						// Skip this check if originalData is empty
+						originalValue, err := getHelmValue(&helmNewValues, helmAnnotationParamName)
+						if err == nil {
+							// Original value exists and was found
+							if !image.HasRegistryPrefix(originalValue) {
+								// Original value is in short form, use the short form of the value to set
+								valueToSet = image.ExtractShortForm(valueToSet)
+							}
+							// If originalValue is also in long form, keep using helmParamName.Value
+						}
+						// If getHelmValue returns an error (key not found), use helmParamName.Value as-is
+					}
+					// If helmParamName.Value is already in short form or originalData is empty, use it as-is
+				} else {
+					// Parameter not found in spec, use the full image name from the container image
+					valueToSet = c.GetFullNameWithoutTag()
+					// Check if original data has short form and convert if needed
+					if !emptyOriginalData {
+						originalValue, err := getHelmValue(&helmNewValues, helmAnnotationParamName)
+						if err == nil && !image.HasRegistryPrefix(originalValue) {
+							// Original value is in short form, use the short form
 							valueToSet = image.ExtractShortForm(valueToSet)
 						}
-						// If originalValue is also in long form, keep using helmParamName.Value
 					}
-					// If getHelmValue returns an error (key not found), use helmParamName.Value as-is
+					log.WithContext().AddField("application", app.GetName()).
+						Debugf("helm parameter %s not found in spec, using image name value: %s", helmAnnotationParamName, valueToSet)
 				}
-				// If helmParamName.Value is already in short form or originalData is empty, use it as-is
 
 				err = setHelmValue(&helmNewValues, helmAnnotationParamName, valueToSet)
 				if err != nil {



Test patch:
diff --git a/pkg/argocd/update_test.go b/pkg/argocd/update_test.go
index c3904fa..1fb39ee 100644
--- a/pkg/argocd/update_test.go
+++ b/pkg/argocd/update_test.go
@@ -2187,7 +2187,16 @@ replicas: 1
 		assert.Equal(t, "could not find an image-name annotation for image nginx", err.Error())
 	})
 
-	t.Run("Image-name annotation value not found in Helm source parameters list", func(t *testing.T) {
+	t.Run("Image-name annotation value not found in Helm source parameters list uses image from status", func(t *testing.T) {
+		// When the annotation-specified parameter name doesn't exist in spec.source.helm.parameters,
+		// the function should use the image name from app.Status.Summary.Images instead of failing
+		expected := `
+random: yaml
+image:
+  tag: v1.0.0
+wrongimage:
+  name: nginx
+`
 		app := v1alpha1.Application{
 			ObjectMeta: v1.ObjectMeta{
 				Name: "testapp",
@@ -2230,11 +2239,21 @@ replicas: 1
 		}
 
 		originalData := []byte(`random: yaml`)
-		_, err := marshalParamsOverride(&app, originalData)
-		assert.Error(t, err)
+		yamlOutput, err := marshalParamsOverride(&app, originalData)
+		require.NoError(t, err)
+		assert.Equal(t, strings.TrimSpace(strings.ReplaceAll(expected, "\t", "  ")), strings.TrimSpace(string(yamlOutput)))
 	})
 
-	t.Run("Image-tag annotation value not found in Helm source parameters list", func(t *testing.T) {
+	t.Run("Image-tag annotation value not found in Helm source parameters list uses tag from status", func(t *testing.T) {
+		// When the annotation-specified parameter name for tag doesn't exist in spec.source.helm.parameters,
+		// the function should use the image tag from app.Status.Summary.Images instead of failing
+		expected := `
+random: yaml
+wrongimage:
+  tag: v0.0.0
+image:
+  name: nginx
+`
 		app := v1alpha1.Application{
 			ObjectMeta: v1.ObjectMeta{
 				Name: "testapp",
@@ -2277,9 +2296,9 @@ replicas: 1
 		}
 
 		originalData := []byte(`random: yaml`)
-		_, err := marshalParamsOverride(&app, originalData)
-		assert.Error(t, err)
-		assert.Equal(t, "wrongimage.tag parameter not found", err.Error())
+		yamlOutput, err := marshalParamsOverride(&app, originalData)
+		require.NoError(t, err)
+		assert.Equal(t, strings.TrimSpace(strings.ReplaceAll(expected, "\t", "  ")), strings.TrimSpace(string(yamlOutput)))
 	})
 
 	t.Run("Invalid parameters merge for Helm source with Helm values file", func(t *testing.T) {
@@ -2406,14 +2425,16 @@ replicas: 1
 	})
 
 	t.Run("Multi-source application with Kustomize first and Helm values write-back", func(t *testing.T) {
-		// This reproduces issue #1289 where getApplicationSource returns the Kustomize source
-		// instead of the Helm source for multi-source applications
+		// This reproduces issue #1289 where a multi-source app contains both helm and kustomize sources.
+		// The correct source type and source should be used by image updater, based on the write-back-target value.
+		// Also verifies that when helm params are not present in spec, image updater should query app status to
+		// get image-name and image-tag values, instead of failing.
 		expected := `
 # auto generated by argocd image updater
 
 theme:
   image:
-    tag: latest
+    tag: v1.0.0
     name: docker.io/hello-world
 `
 		app := v1alpha1.Application{
@@ -2421,6 +2442,7 @@ theme:
 				Name: "multi-source-app",
 				Annotations: map[string]string{
 					"argocd-image-updater.argoproj.io/image-list":            "theme=docker.io/hello-world:latest",
+					"argocd-image-updater.argoproj.io/theme.update-strategy": "digest",
 					"argocd-image-updater.argoproj.io/write-back-method":     "git",
 					"argocd-image-updater.argoproj.io/write-back-target":     "helmvalues:/apps/test/theme-version.yaml",
 					"argocd-image-updater.argoproj.io/theme.helm.image-name": "theme.image.name",
@@ -2430,7 +2452,7 @@ theme:
 			Spec: v1alpha1.ApplicationSpec{
 				Sources: []v1alpha1.ApplicationSource{
 					{
-						// First source: Kustomize (this gets picked up by getApplicationSource!)
+						// First source: Kustomize
 						Path:      "apps/test/overlays/dev",
 						RepoURL:   "https://example.com/gitops.git",
 						Kustomize: &v1alpha1.ApplicationSourceKustomize{
@@ -2442,18 +2464,7 @@ theme:
 						Chart:   "my-chart",
 						RepoURL: "https://example.com/charts",
 						Helm: &v1alpha1.ApplicationSourceHelm{
-							Parameters: []v1alpha1.HelmParameter{
-								{
-									Name:        "theme.image.name",
-									Value:       "docker.io/hello-world",
-									ForceString: true,
-								},
-								{
-									Name:        "theme.image.tag",
-									Value:       "latest",
-									ForceString: true,
-								},
-							},
+							Parameters: []v1alpha1.HelmParameter{},
 						},
 					},
 				},
@@ -2465,7 +2476,7 @@ theme:
 				},
 				Summary: v1alpha1.ApplicationSummary{
 					Images: []string{
-						"docker.io/hello-world:latest",
+						"docker.io/hello-world:v1.0.0",
 					},
 				},
 			},