Repository: argoproj-labs/argocd-image-updater

Issue:
Updating empty values file in git fails unexpectedly
**Describe the bug**
Trying to update an empty values-example.yaml file in a git repository using ArgoCD Image Updater will cause an error in the controller instead of updating the image successfully

**To Reproduce**
Steps to reproduce the behavior:
- Create a git repo with a helm chart
- Create an file named values-example.yaml with only whitespace characters (newline f.e.)
- Create an application with the following annotations:
```yaml
argocd-image-updater.argoproj.io/force-update: "true"
argocd-image-updater.argoproj.io/write-back-method: git
argocd-image-updater.argoproj.io/write-back-target: 'helmvalues:values-example.yaml'
argocd-image-updater.argoproj.io/example.helm.image-tag: example.image.tag
argocd-image-updater.argoproj.io/example.helm.image-name: example.image.repository
argocd-image-updater.argoproj.io/image-list: example=<YOUR_OCI_REPOSITORY>
argocd-image-updater.argoproj.io/update-strategy: newest-build
```
- Create a new tag in the image repository

ArgoCD Image Updater will fail to update the values-example.yaml 

**Expected behavior**
ArgoCD should add to the values-example.yaml the following text:
```yaml
example:
  image:
    repository: ***
    tag: ***
```

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

**Version**
v16.0.0

**Logs**
time="2025-08-30T12:11:23Z" level=error msg="Could not update application spec: failed to set image parameter version value: unexpected type  for root" application=***

Discussion:
@zentavr: I have the same issue. My Application file is the next:
```yaml
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: "example-io"
  namespace: "sys-argocd"
  labels:
    'argocd.example.io/deployed-by': "argo-cd"
  annotations:
    notifications.argoproj.io/subscribe.slack: website-deployment-staging
    # ArgoCD Image Updater setup
    #argocd-image-updater.argoproj.io/write-back-method: "argocd"
    #argocd-image-updater.argoproj.io/write-back-method: "git"
    argocd-image-updater.argoproj.io/write-back-method: git:secret:sys-argocd/argocd-repo-argocd-git-shared
    argocd-image-updater.argoproj.io/git-repository: "git@github.com:example/argocd-staging-shared.git"
    argocd-image-updater.argoproj.io/git-branch: "main"
    argocd-image-updater.argoproj.io/write-back-target: "helmvalues:/example-shared-stage/environments/frontend/staging/example-io/.argocd-image-updater.yaml"

    # Images
    argocd-image-updater.argoproj.io/image-list: >-
      website=740462994053.dkr.ecr.us-east-1.amazonaws.com/website-exampleio-staging:website-app-develop-latest
    #
    argocd-image-updater.argoproj.io/website.update-strategy: "digest"
    argocd-image-updater.argoproj.io/website.platforms: "linux/arm64"
    argocd-image-updater.argoproj.io/website.helm.image-name: image.repository
    argocd-image-updater.argoproj.io/website.helm.image-tag: image.tag

# Spec Ref.: https://argo-cd.readthedocs.io/en/stable/user-guide/application-specification/
spec:
  # The project the application belongs to.
  project: "website"

  # Extra information to show in the Argo CD Application details tab
  info:
    - name: "Team: "
      value: "Frontend"

  destination:
    # The local cluster is called 'in-cluster' inside ArgoCD project
    name: "in-cluster"
    namespace: "example-website-staging"

  # Specifying multiple sources for an application is a beta feature. The UI and CLI still generally behave as if only
  # the first source is specified. Full UI/CLI support will be added in a future release. This feature is subject to
  # change in backwards incompatible ways until it is marked stable.
  # refs.:
  # - https://argo-cd.readthedocs.io/en/stable/user-guide/helm/
  # - https://argo-cd.readthedocs.io/en/stable/user-guide/projects/
  # - https://argo-cd.readthedocs.io/en/stable/user-guide/multiple_sources/
  sources:
    - # Can point to either a Helm chart repo or a git repo. Points to Helm Chart Repo
      repoURL: "https://helm-charts.example.io/charts"

      # helm specific config. The chart name to use.
      chart: "website"

      # For Helm, this refers to the chart version.
      # targetRevision: HEAD         # Use the latest available in the repo
      targetRevision: "0.3.2"        # We are stick at the certain helm version just in case... (values/setup/etc could change)
      helm:
        # Release name override (defaults to application name)
        # Please note that overriding the Helm release name might cause problems
        releaseName: "website"

        # If your aff has any helm hooks, kindly checkout ArgoCD document for the details and possible issues:
        # ref.: https://argo-cd.readthedocs.io/en/stable/user-guide/helm/#helm-hooks
        # If your app uses randAlphaNum helm function, check out this notice:
        # ref.: https://argo-cd.readthedocs.io/en/stable/user-guide/helm/#random-data

        # Helm values. We have multiply sources, so the config is a little bit tricky here
        # refs.:
        # - https://akuity.io/blog/argo-cd-helm-values-files/
        # - https://argo-cd.readthedocs.io/en/stable/user-guide/multiple_sources/#helm-value-files-from-external-git-repository
        valueFiles:
        - $values/example-shared-stage/environments/frontend/staging/example-io/values.yaml
    # Even when the ref field is configured with the path field, $value still represents the root of sources with the
    # ref field. Consequently, valueFiles must be specified as relative paths from the root of sources.
    - ref: values
      repoURL: "git@github.com:example/argocd-staging-shared.git"
      targetRevision: "main"

  # Sync policy
  syncPolicy:
    # automated sync by default retries failed attempts 5 times with following delays between
    # attempts ( 5s, 10s, 20s, 40s, 80s ); retry controlled using `retry` field.
    automated:
      # Specifies if resources should be pruned during auto-syncing ( false by default ).
      prune: false
      # Specifies if partial app sync should be executed when resources are changed only in target Kubernetes cluster
      # and no git change detected ( false by default ).
      selfHeal: true
      # Allows deleting all application resources during automatic syncing ( false by default ).
      allowEmpty: false

    # Sync options which modifies sync behavior
    syncOptions:
      # Namespace Auto-Creation ensures that namespace specified as the application destination exists in the destination cluster.
      - CreateNamespace=true
      - ServerSideApply=true
      - RespectIgnoreDifferences=true

    # The retry feature is available since v1.7
    retry:
      limit: 5 # number of failed sync attempt retries; unlimited number of attempts if less than 0
      backoff:
        duration: 5s # the amount to back off. Default unit is seconds, but could also be a duration (e.g. "2m", "1h")
        factor: 2 # a factor to multiply the base duration after each failed retry
        maxDuration: 3m # the maximum amount of time allowed for the backoff strategy

  ignoreDifferences:
    # for the specified json pointers
    - group: apps
      kind: Deployment
      name: web-website
      jsonPointers:
        - /spec/replicas

  # RevisionHistoryLimit limits the number of items kept in the application's revision history, which is used for
  # informational purposes as well as for rollbacks to previous versions. This should only be changed in exceptional
  # circumstances. Setting to zero will store no history. This will reduce storage used. Increasing will increase the
  # space used to store the history, so we do not recommend increasing it.
  revisionHistoryLimit: 10

```

Solution patch:
diff --git a/pkg/argocd/update.go b/pkg/argocd/update.go
index 2cb7836..a095821 100644
--- a/pkg/argocd/update.go
+++ b/pkg/argocd/update.go
@@ -11,6 +11,8 @@ import (
 	"sync"
 	"text/template"
 	"time"
+	"unicode"
+	"unicode/utf8"
 
 	"golang.org/x/exp/slices"
 
@@ -486,7 +488,7 @@ func marshalParamsOverride(app *v1alpha1.Application, originalData []byte) ([]by
 			images := GetImagesAndAliasesFromApplication(app)
 
 			var helmNewValues yaml.Node
-			if len(originalData) == 0 {
+			if isOnlyWhitespace(originalData) {
 				// allow non-exists target file
 				helmNewValues = yaml.Node{
 					Kind:        yaml.DocumentNode,
@@ -874,3 +876,17 @@ func commitChanges(app *v1alpha1.Application, wbc *WriteBackConfig, changeList [
 	}
 	return nil
 }
+
+func isOnlyWhitespace(data []byte) bool {
+	if len(data) == 0 {
+		return true
+	}
+	for i := 0; i < len(data); {
+		r, size := utf8.DecodeRune(data[i:])
+		if !unicode.IsSpace(r) {
+			return false
+		}
+		i += size
+	}
+	return true
+}



Test patch:
diff --git a/pkg/argocd/update_test.go b/pkg/argocd/update_test.go
index bc56587..6a115fb 100644
--- a/pkg/argocd/update_test.go
+++ b/pkg/argocd/update_test.go
@@ -2241,6 +2241,72 @@ replicas: 1
 		_, err := marshalParamsOverride(&app, nil)
 		assert.Error(t, err)
 	})
+
+	t.Run("Whitespace only values file from helm source does not cause error", func(t *testing.T) {
+		expected := `
+# auto generated by argocd image updater
+
+nginx:
+  image:
+    tag: v1.0.0
+    name: nginx
+`
+		app := v1alpha1.Application{
+			ObjectMeta: v1.ObjectMeta{
+				Name: "testapp",
+				Annotations: map[string]string{
+					"argocd-image-updater.argoproj.io/image-list":            "nginx=nginx, redis=redis",
+					"argocd-image-updater.argoproj.io/write-back-method":     "git",
+					"argocd-image-updater.argoproj.io/write-back-target":     "helmvalues:./test-values.yaml",
+					"argocd-image-updater.argoproj.io/nginx.helm.image-name": "nginx.image.name",
+					"argocd-image-updater.argoproj.io/nginx.helm.image-tag":  "nginx.image.tag",
+				},
+			},
+			Spec: v1alpha1.ApplicationSpec{
+				Sources: []v1alpha1.ApplicationSource{
+					{
+						Chart: "my-app",
+						Helm: &v1alpha1.ApplicationSourceHelm{
+							ReleaseName: "my-app",
+							ValueFiles:  []string{"$values/some/dir/values.yaml"},
+							Parameters: []v1alpha1.HelmParameter{
+								{
+									Name:        "nginx.image.name",
+									Value:       "nginx",
+									ForceString: true,
+								},
+								{
+									Name:        "nginx.image.tag",
+									Value:       "v1.0.0",
+									ForceString: true,
+								},
+							},
+						},
+						RepoURL:        "https://example.com/example",
+						TargetRevision: "main",
+					},
+				},
+			},
+			Status: v1alpha1.ApplicationStatus{
+				SourceTypes: []v1alpha1.ApplicationSourceType{
+					v1alpha1.ApplicationSourceTypeHelm,
+					"",
+				},
+				Summary: v1alpha1.ApplicationSummary{
+					Images: []string{
+						"nginx:v0.0.0",
+					},
+				},
+			},
+		}
+
+		originalData := []byte(`
+`)
+		yaml, err := marshalParamsOverride(&app, originalData)
+		require.NoError(t, err)
+		assert.NotEmpty(t, yaml)
+		assert.Equal(t, strings.TrimSpace(strings.ReplaceAll(expected, "\t", "  ")), strings.TrimSpace(string(yaml)))
+	})
 }
 
 func Test_SetHelmValue(t *testing.T) {