Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package helmfile for openSUSE:Factory checked in at 2026-08-01 18:35:30 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/helmfile (Old) and /work/SRC/openSUSE:Factory/.helmfile.new.16738 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "helmfile" Sat Aug 1 18:35:30 2026 rev:99 rq:1368972 version:1.7.2 Changes: -------- --- /work/SRC/openSUSE:Factory/helmfile/helmfile.changes 2026-07-18 22:26:06.252337090 +0200 +++ /work/SRC/openSUSE:Factory/.helmfile.new.16738/helmfile.changes 2026-08-01 18:38:12.186156740 +0200 @@ -1,0 +2,29 @@ +Sat Aug 1 08:02:34 UTC 2026 - Manfred Hollstein <[email protected]> + +- Update to version 1.7.2: + * build(deps): bump github.com/aws/aws-sdk-go-v2/service/s3 from + 1.105.1 to 1.105.2 by @dependabot[bot] in #2707 + * build(deps): bump github.com/aws/aws-sdk-go-v2/config from + 1.32.30 to 1.32.31 by @dependabot[bot] in #2708 + * build(deps): bump github.com/aws/aws-sdk-go-v2/service/s3 from + 1.105.2 to 1.106.0 by @dependabot[bot] in #2709 + * build(deps): bump google.golang.org/grpc from 1.82.0 to 1.82.1 + by @dependabot[bot] in #2711 + * build(deps): bump gitpython from 3.1.50 to 3.1.52 in /docs by + @dependabot[bot] in #2710 + * feat: add --repo-retries for helm repo and registry login + commands by @yxxhero in #2683 + * build(deps): bump github.com/helmfile/vals from 0.44.5 to 0.45.0 + by @dependabot[bot] in #2713 + * build(deps): bump github.com/mattn/go-runewidth from 0.0.24 to + 0.0.27 by @dependabot[bot] in #2714 + * build(deps): bump gitpython from 3.1.52 to 3.1.54 in /docs by + @dependabot[bot] in #2715 + * build(deps): bump go.yaml.in/yaml/v3 from 3.0.4 to 3.0.5 by + @dependabot[bot] in #2716 + * build(deps): bump github.com/aws/aws-sdk-go-v2/service/s3 from + 1.106.0 to 1.106.2 by @dependabot[bot] in #2719 + * feat: support Helm 4 --rollback-on-failure alongside deprecated + --atomic by @yxxhero in #2722 + +------------------------------------------------------------------- Old: ---- helmfile-1.7.1.tar.gz New: ---- helmfile-1.7.2.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ helmfile.spec ++++++ --- /var/tmp/diff_new_pack.xpnbGB/_old 2026-08-01 18:38:13.306195278 +0200 +++ /var/tmp/diff_new_pack.xpnbGB/_new 2026-08-01 18:38:13.310195415 +0200 @@ -17,9 +17,9 @@ # -%define git_commit 919aa29f151b67d10ca5dc7f0c54eafa9e7838a5 +%define git_commit 7f748fec1b979a74aa7793e7a4d2f27944ad9748 Name: helmfile -Version: 1.7.1 +Version: 1.7.2 Release: 0 Summary: Deploy Kubernetes Helm Charts License: MIT ++++++ _service ++++++ --- /var/tmp/diff_new_pack.xpnbGB/_old 2026-08-01 18:38:13.362197205 +0200 +++ /var/tmp/diff_new_pack.xpnbGB/_new 2026-08-01 18:38:13.370197480 +0200 @@ -5,7 +5,7 @@ <param name="exclude">.git</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> - <param name="revision">v1.7.1</param> + <param name="revision">v1.7.2</param> <param name="changesgenerate">enable</param> <param name="changesauthor">[email protected]</param> </service> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.xpnbGB/_old 2026-08-01 18:38:13.402198581 +0200 +++ /var/tmp/diff_new_pack.xpnbGB/_new 2026-08-01 18:38:13.410198856 +0200 @@ -1,7 +1,7 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/helmfile/helmfile.git</param> - <param name="changesrevision">919aa29f151b67d10ca5dc7f0c54eafa9e7838a5</param> + <param name="changesrevision">7f748fec1b979a74aa7793e7a4d2f27944ad9748</param> </service> </servicedata> ++++++ helmfile-1.7.1.tar.gz -> helmfile-1.7.2.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helmfile-1.7.1/cmd/root.go new/helmfile-1.7.2/cmd/root.go --- old/helmfile-1.7.1/cmd/root.go 2026-07-17 11:52:59.000000000 +0200 +++ new/helmfile-1.7.2/cmd/root.go 2026-07-31 16:22:31.000000000 +0200 @@ -135,6 +135,13 @@ fs.BoolVar(&globalOptions.DisableForceUpdate, "disable-force-update", false, `do not force helm repos to update when executing "helm repo add" (Helm 3 only)`) fs.BoolVar(&globalOptions.EnforcePluginVerification, "enforce-plugin-verification", false, `fail plugin installation if verification is not supported (for security purposes)`) fs.BoolVar(&globalOptions.HelmOCIPlainHTTP, "oci-plain-http", false, `use plain HTTP for OCI registries (required for local/insecure registries in Helm 4)`) + fs.IntVar(&globalOptions.RepoRetry, "repo-retries", -1, `Number of times to retry "helm repo add/update" and "helm registry login" on failure, with exponential backoff (1s, 2s, 4s, ..., capped at 30s). Set to 0 to disable retries. Overrides "HELMFILE_REPO_RETRIES" OS environment variable when specified`) + // The actual default is -1 (a sentinel meaning "flag not set, fall back to + // the env var"); display "0" in --help to match the documented default and + // avoid confusing users with a negative value. + if f := fs.Lookup("repo-retries"); f != nil { + f.DefValue = "0" + } fs.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, `Silence output. Equivalent to log-level warn. Overrides "HELMFILE_QUIET" OS environment variable when specified`) fs.StringVar(&globalOptions.Kubeconfig, "kubeconfig", "", "Use a particular kubeconfig file") fs.StringVar(&globalOptions.KubeContext, "kube-context", "", `Set kubectl context. Overrides "HELMFILE_KUBE_CONTEXT" OS environment variable when specified. Uses current kubectl context by default`) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helmfile-1.7.1/docs/cli.md new/helmfile-1.7.2/docs/cli.md --- old/helmfile-1.7.1/docs/cli.md 2026-07-17 11:52:59.000000000 +0200 +++ new/helmfile-1.7.2/docs/cli.md 2026-07-31 16:22:31.000000000 +0200 @@ -55,6 +55,7 @@ -n, --namespace string Set namespace. Overrides "HELMFILE_NAMESPACE" OS environment variable when specified. Uses the namespace set in the context by default, and is available in templates as {{ .Namespace }} --no-color Output without color. Overrides "HELMFILE_NO_COLOR" and "NO_COLOR" OS environment variables when specified -q, --quiet Silence output. Equivalent to log-level warn. Overrides "HELMFILE_QUIET" OS environment variable when specified + --repo-retries int Number of times to retry "helm repo add/update" and "helm registry login" on failure, with exponential backoff (1s, 2s, 4s, ..., capped at 30s). Set to 0 to disable retries. Overrides "HELMFILE_REPO_RETRIES" OS environment variable when specified -l, --selector stringArray Only run using the releases that match labels. Labels can take the form of foo=bar or foo!=bar. A release must match all labels in a group in order to be used. Multiple groups can be specified at once. "--selector tier=frontend,tier!=proxy --selector tier=backend" will match all frontend, non-proxy releases AND all backend releases. @@ -547,6 +548,7 @@ | `--skip-refresh` | false | Skip running `helm repo update` (lighter than `--skip-deps` which also skips dependency build) | | `--enforce-plugin-verification` | false | Fail plugin installation if verification is not supported | | `--oci-plain-http` | false | Use plain HTTP for OCI registries (required for local/insecure registries in Helm 4) | +| `--repo-retries` | `0` | Number of times to retry `helm repo add/update` and `helm registry login` on failure, with exponential backoff (1s, 2s, 4s, ..., capped at 30s). Set to 0 to disable retries. Overrides `HELMFILE_REPO_RETRIES` | #### fetch flags diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helmfile-1.7.1/docs/configuration.md new/helmfile-1.7.2/docs/configuration.md --- old/helmfile-1.7.1/docs/configuration.md 2026-07-17 11:52:59.000000000 +0200 +++ new/helmfile-1.7.2/docs/configuration.md 2026-07-31 16:22:31.000000000 +0200 @@ -246,8 +246,12 @@ # Defines the strategy to use when updating. Possible value is: # - "reinstallIfForbidden": Performs an uninstall before the update only if the update is forbidden (e.g., due to permission issues or conflicts). updateStrategy: "" - # restores previous state in case of failed release (default false) + # restores previous state in case of failed release (default false). + # On Helm 4+ this emits --rollback-on-failure (the successor to the deprecated --atomic). atomic: true + # restores previous state on a failed release via the Helm 4 --rollback-on-failure flag + # (default false). Requires Helm 4 or greater. Mutually exclusive with atomic. + rollbackOnFailure: false # when true, cleans up any new resources created during a failed release (default false) cleanupOnFail: false # --kube-context to be passed to helm commands @@ -437,6 +441,8 @@ | `enableDNS` | bool | false | Enable DNS lookups when rendering templates | | `skipCRDs` | bool | false | Skip CRDs during installation | | `skipRefresh` | bool | false | Skip running `helm dependency up` | +| `atomic` | bool | false | Restore previous state on a failed install/upgrade. On Helm 4+ emits `--rollback-on-failure` (the successor to the deprecated `--atomic`); on older Helm emits `--atomic` | +| `rollbackOnFailure` | bool | false | Restore previous state on a failed install/upgrade via the Helm 4 `--rollback-on-failure` flag. Requires Helm 4 or greater. Mutually exclusive with `atomic` | | `forceConflicts` | bool | false | Force server-side apply changes against conflicts (Helm 4 only) | | `takeOwnership` | bool | false | Take ownership of existing resources | | `serverSide` | string | | Controls the helm 4 `--server-side` flag. Must be `"true"`, `"false"`, or `"auto"` (Helm 4 only) | diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helmfile-1.7.1/docs/requirements.txt new/helmfile-1.7.2/docs/requirements.txt --- old/helmfile-1.7.1/docs/requirements.txt 2026-07-17 11:52:59.000000000 +0200 +++ new/helmfile-1.7.2/docs/requirements.txt 2026-07-31 16:22:31.000000000 +0200 @@ -2,7 +2,7 @@ click==8.1.2 ghp-import==2.0.2 gitdb==4.0.9 -GitPython==3.1.50 +GitPython==3.1.54 importlib-metadata==4.11.3 Jinja2==3.1.6 Markdown==3.8.1 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helmfile-1.7.1/go.mod new/helmfile-1.7.2/go.mod --- old/helmfile-1.7.1/go.mod 2026-07-17 11:52:59.000000000 +0200 +++ new/helmfile-1.7.2/go.mod 2026-07-31 16:22:31.000000000 +0200 @@ -6,8 +6,8 @@ dario.cat/mergo v1.0.2 github.com/Masterminds/semver/v3 v3.5.0 github.com/Masterminds/sprig/v3 v3.3.0 - github.com/aws/aws-sdk-go-v2/config v1.32.30 - github.com/aws/aws-sdk-go-v2/service/s3 v1.105.1 + github.com/aws/aws-sdk-go-v2/config v1.32.31 + github.com/aws/aws-sdk-go-v2/service/s3 v1.106.2 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/go-test/deep v1.1.1 github.com/gofrs/flock v0.13.0 @@ -19,7 +19,7 @@ github.com/hashicorp/go-getter/v2 v2.2.3 github.com/hashicorp/hcl/v2 v2.24.0 github.com/helmfile/chartify v0.28.0 - github.com/helmfile/vals v0.44.5 + github.com/helmfile/vals v0.45.0 github.com/sashabaranov/go-openai v1.41.2 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 @@ -33,7 +33,7 @@ go.szostok.io/version v1.2.0 go.uber.org/zap v1.28.0 go.yaml.in/yaml/v2 v2.4.4 - go.yaml.in/yaml/v3 v3.0.4 + go.yaml.in/yaml/v3 v3.0.5 golang.org/x/sync v0.22.0 golang.org/x/term v0.45.0 gopkg.in/yaml.v3 v3.0.1 @@ -92,7 +92,7 @@ github.com/klauspost/compress v1.18.4 // indirect github.com/lib/pq v1.12.3 // indirect github.com/mattn/go-colorable v0.1.15 // indirect - github.com/mattn/go-runewidth v0.0.24 + github.com/mattn/go-runewidth v0.0.27 github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect @@ -114,7 +114,7 @@ golang.org/x/time v0.15.0 // indirect google.golang.org/api v0.288.0 // indirect google.golang.org/genproto v0.0.0-20260622175928-b703f567277d // indirect - google.golang.org/grpc v1.82.0 // indirect + google.golang.org/grpc v1.82.1 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/ini.v1 v1.67.3 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect @@ -127,7 +127,7 @@ cloud.google.com/go/auth v0.20.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect - cloud.google.com/go/kms v1.31.0 // indirect + cloud.google.com/go/kms v1.32.0 // indirect cloud.google.com/go/longrunning v1.2.0 // indirect cloud.google.com/go/monitoring v1.29.0 // indirect cloud.google.com/go/secretmanager v1.20.0 // indirect @@ -166,26 +166,26 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/avelino/slugify v0.0.0-20180501145920-855f152bd774 // indirect - github.com/aws/aws-sdk-go-v2 v1.42.1 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.19.29 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30 // indirect + github.com/aws/aws-sdk-go-v2 v1.43.2 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.15 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.30 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31 // indirect github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.28 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.33 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.33 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.34 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.14 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.26 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.33 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.34 // indirect github.com/aws/aws-sdk-go-v2/service/kms v1.54.1 // indirect - github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.42.5 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.4.1 // indirect + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.43.1 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.5.0 // indirect github.com/aws/aws-sdk-go-v2/service/ssm v1.72.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.32.1 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.1 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.44.1 // indirect - github.com/aws/smithy-go v1.27.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.33.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.45.0 // indirect + github.com/aws/smithy-go v1.27.5 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helmfile-1.7.1/go.sum new/helmfile-1.7.2/go.sum --- old/helmfile-1.7.1/go.sum 2026-07-17 11:52:59.000000000 +0200 +++ new/helmfile-1.7.2/go.sum 2026-07-31 16:22:31.000000000 +0200 @@ -15,8 +15,8 @@ cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/iam v1.11.0 h1:KieQ9Pb+LLPak1O3Rv3GgCxhnmkYf7Xyh0P5HfF1jFM= cloud.google.com/go/iam v1.11.0/go.mod h1:KP+nKGugNJW4LcLx1uEZcq1ok5sQHFaQehQNl4QDgV4= -cloud.google.com/go/kms v1.31.0 h1:LS8N92OxFDgOLg5NCo3OmbvjtQAIVT5gUHVLKIDHaFE= -cloud.google.com/go/kms v1.31.0/go.mod h1:YIyXZym11R5uovJJt4oN5eUL3oPmirF3yKeIh6QAf4U= +cloud.google.com/go/kms v1.32.0 h1:s+rEluaaZKhLVjrIWG7uNBsnWbiitElzNzFGyp6+nIg= +cloud.google.com/go/kms v1.32.0/go.mod h1:CSGvW6GnMQbY+1nOHcIzhMtHSbExXlOmCKjWtYVjcpA= cloud.google.com/go/logging v1.18.0 h1:KhzZq+1cSkPH9YUaKLLhLtQxIHitVayBmk0sGfoM9+k= cloud.google.com/go/logging v1.18.0/go.mod h1:ZGKnpBaURITh+g/uom2VhbiFoFWvejcrHPDhxFtU/gI= cloud.google.com/go/longrunning v1.2.0 h1:WjYH3YHBGCxGJP9M4dWGHBfXr/cFIjMkNgWcJj7/iMM= @@ -154,50 +154,50 @@ github.com/avelino/slugify v0.0.0-20180501145920-855f152bd774/go.mod h1:5wi5YYOpfuAKwL5XLFYopbgIl/v7NZxaJpa/4X6yFKE= github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= -github.com/aws/aws-sdk-go-v2 v1.42.1 h1:9eOTgu1z/dVtYpNZ3/8/XbbaX0x/BqE3HUzAzs6K0ek= -github.com/aws/aws-sdk-go-v2 v1.42.1/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 h1:3IZY0XAJquT3aHzbkHfPzy4ACPcEjVG0x87KOwtpqGY= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14/go.mod h1:zwM6veDkhGgQFqkBy+uT28AAYpLu+uFMlPl+rCg/73E= -github.com/aws/aws-sdk-go-v2/config v1.32.30 h1:XwsEzpTJfQYJbFicz/QMLwAZdyeNVVoOEkbF7R3gPJk= -github.com/aws/aws-sdk-go-v2/config v1.32.30/go.mod h1:Ud32SuMc+/9BGxfpSVld7HrE2o05JwKmXY4M3jOQNZU= -github.com/aws/aws-sdk-go-v2/credentials v1.19.29 h1:WHZGssHH887cO0ox07SIQZsFx3MKD4ps6w0xUEmnKYQ= -github.com/aws/aws-sdk-go-v2/credentials v1.19.29/go.mod h1:Mhl0xR6zjguiuj00XRx2wMx22sAltk7oya39sT7fdg8= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30 h1:/hi1JADLEW9YYryEz1w4GQu0EtP23pP553Cf9KgsDV4= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30/go.mod h1:/3AOgy4K17Dm4ucMZVC/MJkzy5kmfKUcINRHZyo0koQ= +github.com/aws/aws-sdk-go-v2 v1.43.2 h1:cl+IXwWb3qazClUcm08tGSsB6OiuV83JVJO9B0jQcPc= +github.com/aws/aws-sdk-go-v2 v1.43.2/go.mod h1:WEzLKBh/mEjXvx1FtQMWgSxMSTVqxQzjkRtk5fa3wkg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.15 h1:rq/p1VNFfygoKEQ9hHMKsKBE98lspPvT8IxaFs5mFhw= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.15/go.mod h1:bELIhlPfW8OkpDhP1MvCjHDtvv8NhiBTz+K4o26zrXA= +github.com/aws/aws-sdk-go-v2/config v1.32.31 h1:n4nY9O3QKoHIkL85EX+V8RcMFtOhlpTFhGArg915PXk= +github.com/aws/aws-sdk-go-v2/config v1.32.31/go.mod h1:PN0NYDCCoOpGGsZ2+elDUidmHfQBPyYzN2GCgl8HEBs= +github.com/aws/aws-sdk-go-v2/credentials v1.19.30 h1:TTCvvzFU6gXa4iJecNG/0F/B0oYTiazoRECr2XyLHrY= +github.com/aws/aws-sdk-go-v2/credentials v1.19.30/go.mod h1:jKxAp2AEncnliinzpgOSZDFv6+VjvWhjw/AtbfsWT9U= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31 h1:kfVL5wAunCJycL6MOQ6aNh6PlAYEymflcjuKmrWUA0o= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31/go.mod h1:nWfRNDAppujCQgOUd43lKT4yeLv9z3nJ3bw1G3BgQKo= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.28 h1:ez4y5o7sa0uaRI8BquYOXtZpioUPhbQEh7Igm88oV9U= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.28/go.mod h1:TpmZOrQA12XKEpVypgBGZSQBsm1WUTndCiSnbDsbvug= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 h1:xM/Is9cKMHa8Jj8zkvWhvrFkZsXJV9E+BB4g0HW0duQ= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30/go.mod h1:WueJeNDZvK1fMYEWJIkcivBfEzUkTpBhzlrUKKY8EuA= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 h1:jn46zC9LdsVR/ZpMIJqMqb8hHv31BlLx3ulVqNspUOk= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30/go.mod h1:1hTMsAgbdS/AtUi4bw8+gUuh1pceo+eXRLfpSuSQj3M= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31 h1:3GUprIsfmGcC5SACIyB0e7E0BM1O1b3Erl5CePYIAeQ= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31/go.mod h1:7PuV1yl5e2xnUbm+RqvVg5i2iBM8EyijZNoI9wsOoOc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 h1:mbRIur/BiHK6SKPjoBIXSE/hJ6g6JGRLuxQy1jGjlN4= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13/go.mod h1:ITg9em2KbJx1s0y4aqRX5OYWG6HBZ5TVR//OdpEZ2CQ= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23 h1:9Fjh6fi/U5JEStVZijmaMpUwE/gvBJj7x2B/PjbO9To= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23/go.mod h1:iMoT2f1tClxrWAAnKCXjZQ6LOmfLrMG14wmnWpM+F14= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 h1:/Z5jmNrKsSD7EmDjzAPsm/3L9IuOkzaynklJZ1qX7S4= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30/go.mod h1:lEzEZnOosE7zi8Z6royW1cFJTD9fpab4Ul1SBrllewk= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31 h1:uao4A3QZ5UmB326V6KF+qRpv9Tjz7IlnlnTbbANntlU= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31/go.mod h1:I/1+z0VwL1GhQyLgkoHDlygpUZ+iTAwOQ/NsftiUL2I= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.33 h1:HAp1wLFZzch054uh3FK7rcVYg4v7J2FxVf3h3IGNZas= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.33/go.mod h1:mJk5fmqnF+WUlMdPG37pR2Fh3oh6r8F6ZGUgPKvzu0c= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.33 h1:0YA0aCKgsJyno6xkFfaIgjE3/wK08+Qxo9nQfe1UrWM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.33/go.mod h1:UZqj4WIdTH+ga8Y/DgpAuy/8cGjM3h7gDCliJYGg2SE= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.34 h1:HQYnjFnXpX8EbPW5M1QT8mXzesRPwly0HEPTcFlS02Y= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.34/go.mod h1:tGzj56niKYZBbDIRhwPGDqrULzmWv5b6uBQGqyNaFZw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.14 h1:SA43nfaY7+1jjMNIc2ywu99JLJLButtIdLP6j+bT870= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.14/go.mod h1:Du3llKcwbQvHsTXSLzTOGQz0DTDBMEzdg7DAGu7inrY= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.26 h1:Eflerh7atY6HN0yz60peNLOkJA2ZKUyYjZexMbqwMCE= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.26/go.mod h1:dCAXNDmik9NuTjfsvCvW22S6ZFpxmtoliFoQu5XFkh8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.33 h1:mqI7OrxN/DUH85F5OqVn3cIfuZ3+HVcebUm2N8mLlgQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.33/go.mod h1:eZ5jdEpvaaOU8nWWE4cTAJETSEA5FZoWxvNRao4piHY= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.34 h1:Lercr2QB2rOrCwyOusmnQ7IiopfkGcZAgMJbjcSdK/s= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.34/go.mod h1:W0xXPPCb2HAqa3cp2f/nRvE+jGgBmchiuXrfBRlfb1I= github.com/aws/aws-sdk-go-v2/service/kms v1.54.1 h1:aeJAJyvWS3gQ679pJbz8ZdOh3MViD1zvEdoZMVEawbg= github.com/aws/aws-sdk-go-v2/service/kms v1.54.1/go.mod h1:0RXNc6Yf3AvSMldGD6Lcch96Ojlw2TtGnHsqfD/L4u8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.105.1 h1:LkBKxAOE5WXjlFuFZqPG1rREnl6I6QCMElcXFDEidos= -github.com/aws/aws-sdk-go-v2/service/s3 v1.105.1/go.mod h1:zdmCoFO/dSI7GlrwsPqFJI+WlFnSU4Tc8TJnlXrM1Do= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.42.5 h1:CWn32XhRKVJQlCpU5T1PpspYbgUisB+Ke1w7NR4AHZw= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.42.5/go.mod h1:oUyL28WfxY0RqPhFpkrWZx26Cu4JlyrWMMcWq8qqhi0= -github.com/aws/aws-sdk-go-v2/service/signin v1.4.1 h1:V7ZZ300WPXGjvkyore5DGe0ljVPOxCXie/thWdtSBXE= -github.com/aws/aws-sdk-go-v2/service/signin v1.4.1/go.mod h1:mxC0nT/C8wMMS97DemZPzvUZxvIt+2Iq+eS3JdFZGgg= +github.com/aws/aws-sdk-go-v2/service/s3 v1.106.2 h1:lFSYDEyC1JHucMH3fdczMTnDaghqNttyRXKM8JY9EJQ= +github.com/aws/aws-sdk-go-v2/service/s3 v1.106.2/go.mod h1:aw1E7RCjxs5Sd8N6WdICMcMroff12Tzxte+ELXXNqRU= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.43.1 h1:ZI18/nuaDBwwMJ95paJrb4NT2TbqEvptj/rlMkEO7DI= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.43.1/go.mod h1:oUyL28WfxY0RqPhFpkrWZx26Cu4JlyrWMMcWq8qqhi0= +github.com/aws/aws-sdk-go-v2/service/signin v1.5.0 h1:OHH5iTQvVGmfHjX/5Q+vFuA/Rf2x6/95aJ/75QCQSm4= +github.com/aws/aws-sdk-go-v2/service/signin v1.5.0/go.mod h1:mCF3AK9PpL49oOrhniUXWAfhVBVQ/XbytoE5eccZUIs= github.com/aws/aws-sdk-go-v2/service/ssm v1.72.0 h1:jl+7QcR+PEJVQXK1W5NSXw9EKd+w7Cu4Pwj/WvUIHb0= github.com/aws/aws-sdk-go-v2/service/ssm v1.72.0/go.mod h1:xabzRvdbMs3FG9kU5M6RUOuCW6wXDkpdIqoXXNzA1nQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.32.1 h1:gYFYh4iLLcAOJRLNPY2aD2g9DIhKn4eof8UkIrr1rTk= -github.com/aws/aws-sdk-go-v2/service/sso v1.32.1/go.mod h1:u8af9Nqkmqnr96f7v9nHqzZT9XBwbXEkTiqT4ROuJSE= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.1 h1:arjT9Cm3/WYbGmD5TUZHk4UQn4Lle1fUNZs5FC6CtF0= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.1/go.mod h1:DMPWJBjYs6+3+f/qhBFEFPPlQ6NlhWjai3dJNvipJ84= -github.com/aws/aws-sdk-go-v2/service/sts v1.44.1 h1:RvfHDg+xvAeZ+5741vUEjpOVtYSIm93W2zhx10Xtydw= -github.com/aws/aws-sdk-go-v2/service/sts v1.44.1/go.mod h1:9gdl4RrflIdpDb2TlXshWgR1F9TeCkvqDx77Vpr4Z/Q= -github.com/aws/smithy-go v1.27.3 h1:F3Zb497UhhskkfpJmfkXswyo+t0sh9OTBnIHjogWbVY= -github.com/aws/smithy-go v1.27.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/aws-sdk-go-v2/service/sso v1.33.0 h1:CaJyYhxBE0M/HJX/YvSaSmQlsI91VHB0lKU8LtLxL3A= +github.com/aws/aws-sdk-go-v2/service/sso v1.33.0/go.mod h1:+e6BMRMPjBQoCw/WovYR9GLy2IU0z4Q77smOB1DraSg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0 h1:tC323YV77QdafeBr6LUhLDTsboyuyHLNRwAyCP44kGU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0/go.mod h1:SfLK1sgviHmbI+MozR9iDwDjL4cdCVZtahsjoR+z7wg= +github.com/aws/aws-sdk-go-v2/service/sts v1.45.0 h1:Pd6PNlp4t8PTXxqzstICl52Wsy78vpjFZ7PRUj44mJc= +github.com/aws/aws-sdk-go-v2/service/sts v1.45.0/go.mod h1:rmQ0TnHzuLPmabgjPcsywhsSOmaBDgzR4zvDxSPsGdg= +github.com/aws/smithy-go v1.27.5 h1:d1ro7KpYOYwP6m73YFa+Kc/A130VsAdX68SpsJwARMM= +github.com/aws/smithy-go v1.27.5/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -538,8 +538,8 @@ github.com/hashicorp/vault/api v1.23.0/go.mod h1:zransKiB9ftp+kgY8ydjnvCU7Wk8i9L0DYWpXeMj9ko= github.com/helmfile/chartify v0.28.0 h1:AK04JykpFXQAtIbEN2DwAZsoSNdDZdAGY5W5FIoK8bk= github.com/helmfile/chartify v0.28.0/go.mod h1:LKUELzZ2TaaCAD4EAFbaCkmRXNnt4uWlJts7GBdFVAQ= -github.com/helmfile/vals v0.44.5 h1:o/f8V6PR/7iw0aR6FBT6sJsH6Zin/RDlXxJljYqAuQM= -github.com/helmfile/vals v0.44.5/go.mod h1:JD1ioew+7grDWMYVsQ9+e1lUunfao/fJV7uV0ReKqjc= +github.com/helmfile/vals v0.45.0 h1:j5e9enLhBfaiYKKPJWEFAzW0DQJlVXyvnKPWWP3gdJg= +github.com/helmfile/vals v0.45.0/go.mod h1:dJ5VGNN0cbusJoAFFINFeimbLV19hhtJJ1328ZFLJzA= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8= @@ -598,8 +598,8 @@ github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= -github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0= +github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= @@ -889,8 +889,9 @@ go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -1033,8 +1034,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= -google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helmfile-1.7.1/pkg/app/app.go new/helmfile-1.7.2/pkg/app/app.go --- old/helmfile-1.7.1/pkg/app/app.go 2026-07-17 11:52:59.000000000 +0200 +++ new/helmfile-1.7.2/pkg/app/app.go 2026-07-31 16:22:31.000000000 +0200 @@ -37,6 +37,7 @@ DisableForceUpdate bool EnforcePluginVerification bool HelmOCIPlainHTTP bool + RepoRetry int DisableKubeVersionAutoDetection bool SequentialHelmfiles bool @@ -87,6 +88,7 @@ DisableForceUpdate: conf.DisableForceUpdate(), EnforcePluginVerification: conf.EnforcePluginVerification(), HelmOCIPlainHTTP: conf.HelmOCIPlainHTTP(), + RepoRetry: conf.RepoRetry(), SequentialHelmfiles: conf.SequentialHelmfiles(), Logger: conf.Logger(), Kubeconfig: conf.Kubeconfig(), @@ -983,6 +985,7 @@ DisableForceUpdate: a.DisableForceUpdate, EnforcePluginVerification: a.EnforcePluginVerification, HelmOCIPlainHTTP: a.HelmOCIPlainHTTP, + RepoRetry: a.RepoRetry, }, a.Logger, kubeconfig, kubectx, &helmexec.ShellRunner{ Logger: a.Logger, Ctx: a.ctx, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helmfile-1.7.1/pkg/app/config.go new/helmfile-1.7.2/pkg/app/config.go --- old/helmfile-1.7.1/pkg/app/config.go 2026-07-17 11:52:59.000000000 +0200 +++ new/helmfile-1.7.2/pkg/app/config.go 2026-07-31 16:22:31.000000000 +0200 @@ -15,6 +15,7 @@ DisableForceUpdate() bool EnforcePluginVerification() bool HelmOCIPlainHTTP() bool + RepoRetry() int SkipDeps() bool SkipRefresh() bool SequentialHelmfiles() bool diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helmfile-1.7.1/pkg/config/global.go new/helmfile-1.7.2/pkg/config/global.go --- old/helmfile-1.7.1/pkg/config/global.go 2026-07-17 11:52:59.000000000 +0200 +++ new/helmfile-1.7.2/pkg/config/global.go 2026-07-31 16:22:31.000000000 +0200 @@ -5,6 +5,7 @@ "fmt" "io" "os" + "strconv" "go.uber.org/zap" "golang.org/x/term" @@ -42,6 +43,11 @@ EnforcePluginVerification bool // HelmOCIPlainHTTP is true if Helm should use plain HTTP for OCI registries HelmOCIPlainHTTP bool + // RepoRetry is the number of times to retry "helm repo add/update" and + // "helm registry login" on failure with exponential backoff. + // A negative value (the CLI default sentinel) means "unset" and falls back + // to the HELMFILE_REPO_RETRIES env var; 0 explicitly disables retries. + RepoRetry int // Quiet is true if the output should be quiet. Quiet bool // Kubeconfig is the path to the kubeconfig file to use. @@ -278,6 +284,21 @@ return g.GlobalOptions.HelmOCIPlainHTTP } +// RepoRetry returns the number of times to retry helm repo and registry login +// operations on failure, with exponential backoff. A negative value means the +// flag was not specified, so the HELMFILE_REPO_RETRIES env var is consulted; +// this lets --repo-retries=0 explicitly disable retries even when the env var +// is set. +func (g *GlobalImpl) RepoRetry() int { + if g.GlobalOptions.RepoRetry >= 0 { + return g.GlobalOptions.RepoRetry + } + if v, err := strconv.Atoi(os.Getenv(envvar.RepoRetry)); err == nil && v > 0 { + return v + } + return 0 +} + // SequentialHelmfiles returns whether to process helmfile.d files sequentially func (g *GlobalImpl) SequentialHelmfiles() bool { return g.GlobalOptions.SequentialHelmfiles diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helmfile-1.7.1/pkg/config/global_test.go new/helmfile-1.7.2/pkg/config/global_test.go --- old/helmfile-1.7.1/pkg/config/global_test.go 2026-07-17 11:52:59.000000000 +0200 +++ new/helmfile-1.7.2/pkg/config/global_test.go 2026-07-31 16:22:31.000000000 +0200 @@ -420,3 +420,31 @@ }) } } + +// TestRepoRetry tests the repo-retries flag and HELMFILE_REPO_RETRIES env var fallback +func TestRepoRetry(t *testing.T) { + // RepoRetry < 0 means the flag was not specified (CLI default sentinel). + unset := GlobalOptions{RepoRetry: -1} + tests := []struct { + name string + opts GlobalOptions + env string + expected int + }{ + {name: "default (unset)", opts: unset, env: "", expected: 0}, + {name: "env set", opts: unset, env: "3", expected: 3}, + {name: "flag set", opts: GlobalOptions{RepoRetry: 5}, env: "", expected: 5}, + {name: "flag overrides env", opts: GlobalOptions{RepoRetry: 5}, env: "3", expected: 5}, + {name: "flag zero disables env", opts: GlobalOptions{RepoRetry: 0}, env: "3", expected: 0}, + {name: "invalid env ignored", opts: unset, env: "abc", expected: 0}, + {name: "negative env ignored", opts: unset, env: "-1", expected: 0}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Setenv(envvar.RepoRetry, test.env) + received := NewGlobalImpl(&test.opts).RepoRetry() + require.Equalf(t, test.expected, received, "RepoRetry expected %d, received %d", test.expected, received) + }) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helmfile-1.7.1/pkg/envvar/const.go new/helmfile-1.7.2/pkg/envvar/const.go --- old/helmfile-1.7.1/pkg/envvar/const.go 2026-07-17 11:52:59.000000000 +0200 +++ new/helmfile-1.7.2/pkg/envvar/const.go 2026-07-31 16:22:31.000000000 +0200 @@ -25,6 +25,7 @@ GoYamlV3 = "HELMFILE_GO_YAML_V3" CacheHome = "HELMFILE_CACHE_HOME" Interactive = "HELMFILE_INTERACTIVE" + RepoRetry = "HELMFILE_REPO_RETRIES" RenderYaml = "HELMFILE_RENDER_YAML" // force helmfile.yaml to be rendered as template regardless of extension, expecting "true" lower case // AWSSDKLogLevel controls AWS SDK logging level diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helmfile-1.7.1/pkg/helmexec/exec.go new/helmfile-1.7.2/pkg/helmexec/exec.go --- old/helmfile-1.7.1/pkg/helmexec/exec.go 2026-07-17 11:52:59.000000000 +0200 +++ new/helmfile-1.7.2/pkg/helmexec/exec.go 2026-07-31 16:22:31.000000000 +0200 @@ -3,6 +3,7 @@ import ( "bytes" "context" + "errors" "fmt" "io" "net/url" @@ -12,6 +13,7 @@ "strconv" "strings" "sync" + "time" "unicode" "github.com/Masterminds/semver/v3" @@ -38,6 +40,9 @@ DisableForceUpdate bool // If true, do not force helm repos to update when executing "helm repo add" (Helm 3) EnforcePluginVerification bool // If true, fail plugin installation if verification is not supported HelmOCIPlainHTTP bool // If true, use plain HTTP for OCI registries + // RepoRetry is the number of times to retry helm repo and registry login + // operations on failure, with exponential backoff. 0 disables retries. + RepoRetry int } type execer struct { @@ -272,11 +277,91 @@ helm.options.DisableForceUpdate = forceUpdate } -func (helm *execer) AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string, passCredentials, skipTLSVerify bool) error { - var args []string +// repoRetryBaseBackoff is the base unit for exponential backoff between repo +// operation retries (doubles each attempt, capped at 30x). Exposed as a +// package-level variable so tests can shrink it to avoid real sleeps. +var repoRetryBaseBackoff = time.Second + +// retryRepoOp runs op, retrying up to helm.options.RepoRetry times on failure +// with exponential backoff (1x, 2x, 4x, ..., capped at 30x the base unit). It +// returns the output from the (last) attempt. A zero/negative RepoRetry +// disables retrying — op runs exactly once. +func (helm *execer) retryRepoOp(name string, op func() ([]byte, error)) ([]byte, error) { + maxRetries := helm.options.RepoRetry var out []byte var err error + for attempt := 0; ; attempt++ { + out, err = op() + if err == nil || maxRetries <= 0 || attempt >= maxRetries { + return out, err + } + // Cap the shift exponent at 5 (2^5 = 32x already exceeds the 30x cap) + // so very large --repo-retries values can't overflow time.Duration. + shift := attempt + if shift > 5 { + shift = 5 + } + backoff := repoRetryBaseBackoff * time.Duration(1<<shift) + if backoff > 30*repoRetryBaseBackoff { + backoff = 30 * repoRetryBaseBackoff + } + helm.logger.Warnf("repo operation %q failed (%s); retry %d/%d in %v", + name, conciseError(err), attempt+1, maxRetries, backoff) + // sleepCtx returns false if interrupted by context cancellation; in that + // case stop retrying so a canceled context (Ctrl+C) doesn't spin into a + // tight loop of rapid helm invocations. + if !helm.sleepCtx(backoff) { + return out, err + } + } +} + +// conciseError returns a short reason for logging. For a helm ExitError it +// reports just the exit status, avoiding the very verbose PATH/ARGS/OUTPUT +// dump that Error() produces. +func conciseError(err error) string { + var ee ExitError + if errors.As(err, &ee) { + return fmt.Sprintf("exit status %d", ee.ExitStatus()) + } + return err.Error() +} + +// sleepCtx sleeps for d, returning early if the runner's context is canceled, +// so an interrupt (Ctrl+C) aborts the retry loop promptly rather than blocking +// until the full backoff elapses. It returns true if the full duration elapsed, +// or false if it was interrupted by context cancellation. Falls back to +// time.Sleep (returning true) for runners without a context (e.g. the test +// mockRunner). +func (helm *execer) sleepCtx(d time.Duration) bool { + ctx := helm.runnerContext() + if ctx == nil { + time.Sleep(d) + return true + } + timer := time.NewTimer(d) + defer timer.Stop() + select { + case <-timer.C: + return true + case <-ctx.Done(): + return false + } +} + +// runnerContext returns the ShellRunner's context if the runner is a +// ShellRunner (pointer or value), else nil. +func (helm *execer) runnerContext() context.Context { + switch r := helm.runner.(type) { + case *ShellRunner: + return r.Ctx + case ShellRunner: + return r.Ctx + } + return nil +} +func (helm *execer) AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string, passCredentials, skipTLSVerify bool) error { if name == "" && repository != "" { helm.logger.Infof("empty field name\n") return fmt.Errorf("empty field name") @@ -288,46 +373,52 @@ helm.extra = savedExtra }() + var out []byte + var err error switch managed { case "acr": helm.logger.Infof("Adding repo %v (acr)", name) - out, err = helm.azcli(name) + out, err = helm.retryRepoOp(fmt.Sprintf("add %s (acr)", name), func() ([]byte, error) { + return helm.azcli(name) + }) case "": - args = append(args, "repo", "add", name, repository) - - // --force-update is needed for both Helm 3.3.2+ and Helm 4 - // to ensure repository indexes are updated when a repository already exists - // See https://github.com/helm/helm/pull/8777 - if !helm.options.DisableForceUpdate && (helm.IsHelm4() || helm.IsVersionAtLeast("3.3.2")) { - args = append(args, "--force-update") - } + helm.logger.Infof("Adding repo %v %v", name, repository) + out, err = helm.retryRepoOp(fmt.Sprintf("add %s", name), func() ([]byte, error) { + // args is local to each attempt, so the username/password append + // below can't accumulate across retries. + args := []string{"repo", "add", name, repository} + + // --force-update is needed for both Helm 3.3.2+ and Helm 4 + // to ensure repository indexes are updated when a repository already exists + // See https://github.com/helm/helm/pull/8777 + if !helm.options.DisableForceUpdate && (helm.IsHelm4() || helm.IsVersionAtLeast("3.3.2")) { + args = append(args, "--force-update") + } - if certfile != "" && keyfile != "" { - args = append(args, "--cert-file", certfile, "--key-file", keyfile) - } - if cafile != "" { - args = append(args, "--ca-file", cafile) - } + if certfile != "" && keyfile != "" { + args = append(args, "--cert-file", certfile, "--key-file", keyfile) + } + if cafile != "" { + args = append(args, "--ca-file", cafile) + } - if passCredentials { - args = append(args, "--pass-credentials") - } - if skipTLSVerify { - args = append(args, "--insecure-skip-tls-verify") - } - helm.logger.Infof("Adding repo %v %v", name, repository) - if username != "" && password != "" { - args = append(args, "--username", username, "--password-stdin") - buffer := bytes.Buffer{} - fmt.Fprintf(&buffer, "%s\n", password) - out, err = helm.execStdIn(args, map[string]string{}, &buffer) - } else { - out, err = helm.exec(args, map[string]string{}, nil) - } + if passCredentials { + args = append(args, "--pass-credentials") + } + if skipTLSVerify { + args = append(args, "--insecure-skip-tls-verify") + } + if username != "" && password != "" { + args = append(args, "--username", username, "--password-stdin") + buffer := bytes.Buffer{} + fmt.Fprintf(&buffer, "%s\n", password) + return helm.execStdIn(args, map[string]string{}, &buffer) + } + return helm.exec(args, map[string]string{}, nil) + }) default: helm.logger.Errorf("ERROR: unknown type '%v' for repository %v", managed, name) - out = nil - err = nil + err = fmt.Errorf("unknown managed type %q for repository %v", managed, name) } helm.info(out) @@ -341,7 +432,9 @@ defer func() { helm.extra = savedExtra }() - out, err := helm.exec([]string{"repo", "update"}, map[string]string{}, nil) + out, err := helm.retryRepoOp("update", func() ([]byte, error) { + return helm.exec([]string{"repo", "update"}, map[string]string{}, nil) + }) helm.info(out) return err } @@ -373,11 +466,15 @@ } args = append(args, "--username", username, "--password-stdin") - buffer := bytes.Buffer{} - fmt.Fprintf(&buffer, "%s\n", password) helm.logger.Info("Logging in to registry") - out, err := helm.execStdIn(args, map[string]string{"HELM_EXPERIMENTAL_OCI": "1"}, &buffer) + out, err := helm.retryRepoOp(fmt.Sprintf("registry login %s", repository), func() ([]byte, error) { + buffer := bytes.Buffer{} + fmt.Fprintf(&buffer, "%s\n", password) + // Copy args so execStdIn's internal append (for helm.extra) can't alias + // the shared slice across retries. + return helm.execStdIn(append([]string{}, args...), map[string]string{"HELM_EXPERIMENTAL_OCI": "1"}, &buffer) + }) helm.info(out) return err } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helmfile-1.7.1/pkg/helmexec/exec_test.go new/helmfile-1.7.2/pkg/helmexec/exec_test.go --- old/helmfile-1.7.1/pkg/helmexec/exec_test.go 2026-07-17 11:52:59.000000000 +0200 +++ new/helmfile-1.7.2/pkg/helmexec/exec_test.go 2026-07-31 16:22:31.000000000 +0200 @@ -2,6 +2,7 @@ import ( "bytes" + "context" "fmt" "io" "os" @@ -10,6 +11,7 @@ "regexp" "strings" "testing" + "time" "github.com/Masterminds/semver/v3" "github.com/google/go-cmp/cmp" @@ -24,9 +26,31 @@ output []byte versionOutput []byte // if set, returned for "helm version --short" probe; overrides default Helm 4 fallback err error + // failCount is the number of times Execute/ExecuteStdIn fail (with + // errTransientNet) before succeeding. Used to exercise retry logic. + failCount int + calls int + // stdins records the stdin content passed to each ExecuteStdIn call, + // so tests can assert that retries still pass the full payload (e.g. the + // password is not consumed by a prior attempt). + stdins []string + // execArgs records the args passed to each Execute/ExecuteStdIn call, so + // tests can assert that retries don't accumulate duplicated flags. + execArgs [][]string } +var errTransientNet = fmt.Errorf("transient network error") + func (mock *mockRunner) ExecuteStdIn(cmd string, args []string, env map[string]string, stdin io.Reader) ([]byte, error) { + mock.calls++ + mock.execArgs = append(mock.execArgs, append([]string{}, args...)) + if stdin != nil { + b, _ := io.ReadAll(stdin) + mock.stdins = append(mock.stdins, string(b)) + } + if mock.failCount > 0 && mock.calls <= mock.failCount { + return nil, errTransientNet + } return mock.output, mock.err } @@ -39,6 +63,11 @@ return []byte("v4.0.1+g12500dd"), nil } } + mock.calls++ + mock.execArgs = append(mock.execArgs, append([]string{}, args...)) + if mock.failCount > 0 && mock.calls <= mock.failCount { + return nil, errTransientNet + } return mock.output, mock.err } @@ -260,8 +289,8 @@ err = helm.AddRepo("otherRepo", "", "", "", "", "", "", "unknown", false, false) expected = `ERROR: unknown type 'unknown' for repository otherRepo ` - if err != nil { - t.Errorf("unexpected error: %v", err) + if err == nil { + t.Errorf("expected error for unknown managed type, got nil") } if buffer.String() != expected { t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected) @@ -388,6 +417,201 @@ } } +// newRetryExecer builds an execer with the given RepoRetry option and a +// mockRunner that fails failCount times before succeeding. +func newRetryExecer(t *testing.T, repoRetry, failCount int) (*execer, *mockRunner) { + t.Helper() + logger := NewLogger(os.Stdout, "debug") + runner := &mockRunner{failCount: failCount} + helm, err := New("helm", HelmExecOptions{RepoRetry: repoRetry}, logger, "config", "dev", runner) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + return helm, runner +} + +// withTestBackoff shrinks repoRetryBaseBackoff for the test's duration so retry +// tests don't sleep for real. The restore is via t.Cleanup so it always runs. +func withTestBackoff(t *testing.T) { + t.Helper() + original := repoRetryBaseBackoff + repoRetryBaseBackoff = time.Millisecond + t.Cleanup(func() { repoRetryBaseBackoff = original }) +} + +// retryTestCase is the shared shape for the AddRepo/UpdateRepo/RegistryLogin +// retry table tests. +type retryTestCase struct { + name string + repoRetry int + failCount int + wantErr bool + wantCalls int // expected number of exec (non-version) calls + managed string +} + +func runRetryCases(t *testing.T, run func(*testing.T, *execer, *mockRunner, retryTestCase), cases []retryTestCase) { + t.Helper() + withTestBackoff(t) + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + helm, runner := newRetryExecer(t, tt.repoRetry, tt.failCount) + run(t, helm, runner, tt) + if runner.calls != tt.wantCalls { + t.Errorf("runner.calls = %d, want %d", runner.calls, tt.wantCalls) + } + }) + } +} + +func Test_AddRepo_Retry(t *testing.T) { + runRetryCases(t, func(t *testing.T, helm *execer, runner *mockRunner, tt retryTestCase) { + err := helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "user", "pass", tt.managed, false, false) + if (err != nil) != tt.wantErr { + t.Errorf("AddRepo() error = %v, wantErr %v", err, tt.wantErr) + } + // For the normal (non-acr) path, each attempt's args must contain + // --username exactly once: guards against the username/password flags + // accumulating across retries. + if tt.managed == "" { + for i, a := range runner.execArgs { + if c := strings.Count(strings.Join(a, " "), "--username"); c != 1 { + t.Errorf("AddRepo() attempt %d: --username count = %d, want 1 (args=%v)", i+1, c, a) + } + } + } + }, []retryTestCase{ + {name: "succeeds after one retry", repoRetry: 2, failCount: 1, wantErr: false, wantCalls: 2}, + {name: "retries exhausted", repoRetry: 1, failCount: 2, wantErr: true, wantCalls: 2}, + {name: "retry disabled runs once", repoRetry: 0, failCount: 1, wantErr: true, wantCalls: 1}, + {name: "acr managed retries", repoRetry: 3, failCount: 2, wantErr: false, wantCalls: 3, managed: "acr"}, + }) +} + +func Test_UpdateRepo_Retry(t *testing.T) { + runRetryCases(t, func(t *testing.T, helm *execer, _ *mockRunner, tt retryTestCase) { + err := helm.UpdateRepo() + if (err != nil) != tt.wantErr { + t.Errorf("UpdateRepo() error = %v, wantErr %v", err, tt.wantErr) + } + }, []retryTestCase{ + {name: "succeeds after one retry", repoRetry: 2, failCount: 1, wantErr: false, wantCalls: 2}, + {name: "retries exhausted", repoRetry: 1, failCount: 2, wantErr: true, wantCalls: 2}, + {name: "retry disabled runs once", repoRetry: 0, failCount: 1, wantErr: true, wantCalls: 1}, + }) +} + +func Test_RegistryLogin_Retry(t *testing.T) { + runRetryCases(t, func(t *testing.T, helm *execer, runner *mockRunner, tt retryTestCase) { + err := helm.RegistryLogin("repo.example.com", "user", "pass", "", "", "", false) + if (err != nil) != tt.wantErr { + t.Errorf("RegistryLogin() error = %v, wantErr %v", err, tt.wantErr) + } + // Every attempt must carry the full password (regression guard: + // the password buffer must not be consumed by a prior attempt). + for i, s := range runner.stdins { + if s != "pass\n" { + t.Errorf("RegistryLogin() attempt %d stdin = %q, want %q", i+1, s, "pass\n") + } + } + }, []retryTestCase{ + {name: "succeeds after one retry", repoRetry: 2, failCount: 1, wantErr: false, wantCalls: 2}, + {name: "retries exhausted", repoRetry: 1, failCount: 2, wantErr: true, wantCalls: 2}, + {name: "retry disabled runs once", repoRetry: 0, failCount: 1, wantErr: true, wantCalls: 1}, + }) +} + +// Test_Retry_LargeCount_NoOverflow verifies that a large --repo-retries value +// doesn't overflow the backoff duration (the shift exponent must be capped). +func Test_Retry_LargeCount_NoOverflow(t *testing.T) { + withTestBackoff(t) + // failCount > repoRetry so every attempt fails; attempt indices exceed the + // shift cap of 5, exercising the overflow guard without panicking. + const retries = 10 + helm, runner := newRetryExecer(t, retries, retries+1) + err := helm.UpdateRepo() + if err == nil { + t.Fatal("UpdateRepo() expected error, got nil") + } + if runner.calls != retries+1 { // 1 initial + retries + t.Errorf("runner.calls = %d, want %d", runner.calls, retries+1) + } +} + +// Test_SleepCtx_Canceled verifies sleepCtx returns promptly (and false) when +// the runner's context is already canceled, rather than blocking for the full +// duration. +func Test_SleepCtx_Canceled(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() // cancel immediately + helm := &execer{ + runner: &ShellRunner{Ctx: ctx}, + logger: NewLogger(io.Discard, "debug"), + } + start := time.Now() + completed := helm.sleepCtx(30 * time.Second) + elapsed := time.Since(start) + if completed { + t.Error("sleepCtx() = true with a canceled context; want false") + } + if elapsed > time.Second { + t.Errorf("sleepCtx blocked for %v with a canceled context; expected prompt return", elapsed) + } +} + +// Test_SleepCtx_NoContext verifies sleepCtx falls back to time.Sleep (returning +// true) when the runner has no context (e.g. mockRunner). +func Test_SleepCtx_NoContext(t *testing.T) { + helm := &execer{ + runner: &mockRunner{}, + logger: NewLogger(io.Discard, "debug"), + } + start := time.Now() + completed := helm.sleepCtx(5 * time.Millisecond) + elapsed := time.Since(start) + if !completed { + t.Error("sleepCtx() = false without a context; want true") + } + if elapsed < 4*time.Millisecond { + t.Errorf("sleepCtx returned in %v without a context; expected to sleep ~5ms", elapsed) + } +} + +// Test_Retry_AbortsOnCanceledContext verifies that retryRepoOp stops retrying +// once the runner's context is canceled, rather than spinning into a tight +// loop of rapid helm invocations. +func Test_Retry_AbortsOnCanceledContext(t *testing.T) { + withTestBackoff(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + runner := &mockRunner{failCount: 100} // always fails + helm := &execer{ + options: HelmExecOptions{RepoRetry: 100}, + version: semver.MustParse("v4.0.1"), + runner: &ShellRunner{Ctx: ctx}, + logger: NewLogger(io.Discard, "debug"), + } + // Cancel deterministically inside the op after the first attempt runs, so + // the retry loop must bail out at sleepCtx — no timing-based flakiness. + calls := 0 + _, err := helm.retryRepoOp("test", func() ([]byte, error) { + calls++ + out, e := runner.Execute("helm", []string{"repo", "update"}, nil, false) + if calls == 1 { + cancel() + } + return out, e + }) + if err == nil { + t.Fatal("expected error, got nil") + } + // The context is canceled during the first attempt; sleepCtx must observe + // it and abort, so only the first attempt should run. + if calls > 2 { + t.Errorf("calls = %d after cancellation; expected <= 2 (no tight loop)", calls) + } +} + func Test_SyncRelease(t *testing.T) { var buffer bytes.Buffer logger := NewLogger(&buffer, "debug") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helmfile-1.7.1/pkg/state/state.go new/helmfile-1.7.2/pkg/state/state.go --- old/helmfile-1.7.1/pkg/state/state.go 2026-07-17 11:52:59.000000000 +0200 +++ new/helmfile-1.7.2/pkg/state/state.go 2026-07-31 16:22:31.000000000 +0200 @@ -239,6 +239,9 @@ Force bool `yaml:"force"` // Atomic, when set to true, restore previous state in case of a failed install/upgrade attempt Atomic bool `yaml:"atomic"` + // RollbackOnFailure, when set to true, restores previous state on a failed install/upgrade via the + // Helm 4 --rollback-on-failure flag (the successor to the deprecated --atomic flag). Requires Helm 4 or greater. + RollbackOnFailure bool `yaml:"rollbackOnFailure"` // CleanupOnFail, when set to true, the --cleanup-on-fail helm flag is passed to the upgrade command CleanupOnFail bool `yaml:"cleanupOnFail,omitempty"` // ForceConflicts, when set to true, force server-side apply changes against conflicts (Helm 4 only) @@ -363,6 +366,9 @@ UpdateStrategy string `yaml:"updateStrategy,omitempty"` // Atomic, when set to true, restore previous state in case of a failed install/upgrade attempt Atomic *bool `yaml:"atomic,omitempty"` + // RollbackOnFailure, when set to true, restores previous state on a failed install/upgrade via the + // Helm 4 --rollback-on-failure flag (the successor to the deprecated --atomic flag). Requires Helm 4 or greater. + RollbackOnFailure *bool `yaml:"rollbackOnFailure,omitempty"` // CleanupOnFail, when set to true, the --cleanup-on-fail helm flag is passed to the upgrade command CleanupOnFail *bool `yaml:"cleanupOnFail,omitempty"` // ForceConflicts, when set to true, force server-side apply changes against conflicts (Helm 4 only) @@ -4002,8 +4008,26 @@ flags = append(flags, "--recreate-pods") } - if release.Atomic != nil && *release.Atomic || release.Atomic == nil && st.HelmDefaults.Atomic { + atomicEnabled := (release.Atomic != nil && *release.Atomic) || (release.Atomic == nil && st.HelmDefaults.Atomic) + rollbackOnFailureEnabled := (release.RollbackOnFailure != nil && *release.RollbackOnFailure) || (release.RollbackOnFailure == nil && st.HelmDefaults.RollbackOnFailure) + + if atomicEnabled && rollbackOnFailureEnabled { + return nil, nil, fmt.Errorf("atomic and rollbackOnFailure are mutually exclusive (check both releases[].atomic/rollbackOnFailure and helmDefaults.atomic/rollbackOnFailure)") + } + + // rollbackOnFailure maps to the Helm 4 --rollback-on-failure flag and is unsupported on older Helm. + if rollbackOnFailureEnabled && !helm.IsHelm4() { + return nil, nil, fmt.Errorf("rollbackOnFailure requires Helm 4 or greater (set via releases[].rollbackOnFailure or helmDefaults.rollbackOnFailure)") + } + + // Helm 4 renamed --atomic to --rollback-on-failure; the former is deprecated (prints a warning) + // and slated for removal in Helm 5. Emit the new flag on Helm 4+ for both `atomic: true` and + // `rollbackOnFailure: true` so users migrate off it automatically; on older Helm, `atomic: true` + // still emits --atomic. See issue #2712. + if atomicEnabled && !helm.IsHelm4() { flags = append(flags, "--atomic") + } else if atomicEnabled || rollbackOnFailureEnabled { + flags = append(flags, "--rollback-on-failure") } if release.CleanupOnFail != nil && *release.CleanupOnFail || release.CleanupOnFail == nil && st.HelmDefaults.CleanupOnFail { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helmfile-1.7.1/pkg/state/state_test.go new/helmfile-1.7.2/pkg/state/state_test.go --- old/helmfile-1.7.1/pkg/state/state_test.go 2026-07-17 11:52:59.000000000 +0200 +++ new/helmfile-1.7.2/pkg/state/state_test.go 2026-07-31 16:22:31.000000000 +0200 @@ -646,8 +646,10 @@ { name: "atomic", defaults: HelmSpec{ - Atomic: false, + Atomic: false, + CreateNamespace: &disable, }, + version: semver.MustParse("3.10.0"), release: &ReleaseSpec{ Chart: "test/chart", Version: "0.1", @@ -662,10 +664,32 @@ }, }, { + name: "atomic-helm4", + defaults: HelmSpec{ + Atomic: false, + CreateNamespace: &disable, + }, + version: semver.MustParse("4.0.0"), + release: &ReleaseSpec{ + Chart: "test/chart", + Version: "0.1", + Atomic: &enable, + Name: "test-charts", + Namespace: "test-namespace", + }, + want: []string{ + "--version", "0.1", + "--rollback-on-failure", + "--namespace", "test-namespace", + }, + }, + { name: "atomic-override-default", defaults: HelmSpec{ - Atomic: true, + Atomic: true, + CreateNamespace: &disable, }, + version: semver.MustParse("3.10.0"), release: &ReleaseSpec{ Chart: "test/chart", Version: "0.1", @@ -681,8 +705,10 @@ { name: "atomic-from-default", defaults: HelmSpec{ - Atomic: true, + Atomic: true, + CreateNamespace: &disable, }, + version: semver.MustParse("3.10.0"), release: &ReleaseSpec{ Chart: "test/chart", Version: "0.1", @@ -696,6 +722,128 @@ }, }, { + name: "atomic-from-default-helm4", + defaults: HelmSpec{ + Atomic: true, + CreateNamespace: &disable, + }, + version: semver.MustParse("4.0.0"), + release: &ReleaseSpec{ + Chart: "test/chart", + Version: "0.1", + Name: "test-charts", + Namespace: "test-namespace", + }, + want: []string{ + "--version", "0.1", + "--rollback-on-failure", + "--namespace", "test-namespace", + }, + }, + { + name: "rollback-on-failure", + defaults: HelmSpec{ + CreateNamespace: &disable, + }, + version: semver.MustParse("4.0.0"), + release: &ReleaseSpec{ + Chart: "test/chart", + Version: "0.1", + RollbackOnFailure: &enable, + Name: "test-charts", + Namespace: "test-namespace", + }, + want: []string{ + "--version", "0.1", + "--rollback-on-failure", + "--namespace", "test-namespace", + }, + }, + { + name: "rollback-on-failure-from-default", + defaults: HelmSpec{ + RollbackOnFailure: true, + CreateNamespace: &disable, + }, + version: semver.MustParse("4.0.0"), + release: &ReleaseSpec{ + Chart: "test/chart", + Version: "0.1", + Name: "test-charts", + Namespace: "test-namespace", + }, + want: []string{ + "--version", "0.1", + "--rollback-on-failure", + "--namespace", "test-namespace", + }, + }, + { + name: "rollback-on-failure-override-default", + defaults: HelmSpec{ + RollbackOnFailure: true, + CreateNamespace: &disable, + }, + version: semver.MustParse("4.0.0"), + release: &ReleaseSpec{ + Chart: "test/chart", + Version: "0.1", + RollbackOnFailure: &disable, + Name: "test-charts", + Namespace: "test-namespace", + }, + want: []string{ + "--version", "0.1", + "--namespace", "test-namespace", + }, + }, + { + name: "rollback-on-failure-helm3-error", + defaults: HelmSpec{ + CreateNamespace: &disable, + }, + version: semver.MustParse("3.10.0"), + release: &ReleaseSpec{ + Chart: "test/chart", + Version: "0.1", + RollbackOnFailure: &enable, + Name: "test-charts", + Namespace: "test-namespace", + }, + wantErr: "rollbackOnFailure requires Helm 4 or greater (set via releases[].rollbackOnFailure or helmDefaults.rollbackOnFailure)", + }, + { + name: "rollback-on-failure-from-default-helm3-error", + defaults: HelmSpec{ + RollbackOnFailure: true, + CreateNamespace: &disable, + }, + version: semver.MustParse("3.10.0"), + release: &ReleaseSpec{ + Chart: "test/chart", + Version: "0.1", + Name: "test-charts", + Namespace: "test-namespace", + }, + wantErr: "rollbackOnFailure requires Helm 4 or greater (set via releases[].rollbackOnFailure or helmDefaults.rollbackOnFailure)", + }, + { + name: "atomic-and-rollback-on-failure-mutually-exclusive-helm4", + defaults: HelmSpec{ + CreateNamespace: &disable, + }, + version: semver.MustParse("4.0.0"), + release: &ReleaseSpec{ + Chart: "test/chart", + Version: "0.1", + Atomic: &enable, + RollbackOnFailure: &enable, + Name: "test-charts", + Namespace: "test-namespace", + }, + wantErr: "atomic and rollbackOnFailure are mutually exclusive (check both releases[].atomic/rollbackOnFailure and helmDefaults.atomic/rollbackOnFailure)", + }, + { name: "cleanup-on-fail", defaults: HelmSpec{ CleanupOnFail: false, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helmfile-1.7.1/pkg/state/temp_test.go new/helmfile-1.7.2/pkg/state/temp_test.go --- old/helmfile-1.7.1/pkg/state/temp_test.go 2026-07-17 11:52:59.000000000 +0200 +++ new/helmfile-1.7.2/pkg/state/temp_test.go 2026-07-31 16:22:31.000000000 +0200 @@ -38,39 +38,39 @@ run(testcase{ subject: "baseline", release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"}, - want: "foo-values-6765d87f7c", + want: "foo-values-5bcd864488", }) run(testcase{ subject: "different bytes content", release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"}, data: []byte(`{"k":"v"}`), - want: "foo-values-6d7db5f8b", + want: "foo-values-59cd566bbc", }) run(testcase{ subject: "different map content", release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"}, data: map[string]any{"k": "v"}, - want: "foo-values-5c55f66dd", + want: "foo-values-58bcc85765", }) run(testcase{ subject: "different chart", release: ReleaseSpec{Name: "foo", Chart: "stable/envoy"}, - want: "foo-values-7665888bf4", + want: "foo-values-7796c46c49", }) run(testcase{ subject: "different name", release: ReleaseSpec{Name: "bar", Chart: "incubator/raw"}, - want: "bar-values-bc6f974bd", + want: "bar-values-5b5f6f54cc", }) run(testcase{ subject: "specific ns", release: ReleaseSpec{Name: "foo", Chart: "incubator/raw", Namespace: "myns"}, - want: "myns-foo-values-76f8c7c596", + want: "myns-foo-values-547578788f", }) for id, n := range ids { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helmfile-1.7.1/test/integration/run.sh new/helmfile-1.7.2/test/integration/run.sh --- old/helmfile-1.7.1/test/integration/run.sh 2026-07-17 11:52:59.000000000 +0200 +++ new/helmfile-1.7.2/test/integration/run.sh 2026-07-31 16:22:31.000000000 +0200 @@ -151,6 +151,7 @@ . ${dir}/test-cases/issue-1172-selector-required-env.sh . ${dir}/test-cases/kubedog-tracking.sh . ${dir}/test-cases/lookup.sh +. ${dir}/test-cases/issue-2712-rollback-on-failure.sh # ALL DONE ----------------------------------------------------------------------------------------------------------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helmfile-1.7.1/test/integration/test-cases/issue-2712-rollback-on-failure/input/atomic.yaml new/helmfile-1.7.2/test/integration/test-cases/issue-2712-rollback-on-failure/input/atomic.yaml --- old/helmfile-1.7.1/test/integration/test-cases/issue-2712-rollback-on-failure/input/atomic.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/helmfile-1.7.2/test/integration/test-cases/issue-2712-rollback-on-failure/input/atomic.yaml 2026-07-31 16:22:31.000000000 +0200 @@ -0,0 +1,14 @@ +releases: +- name: issue-2712-atomic + chart: ../../../charts/raw + atomic: true + values: + - templates: + - | + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ .Release.Name }}-cm + namespace: {{ .Release.Namespace }} + data: + version: "1" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helmfile-1.7.1/test/integration/test-cases/issue-2712-rollback-on-failure/input/rollback.yaml new/helmfile-1.7.2/test/integration/test-cases/issue-2712-rollback-on-failure/input/rollback.yaml --- old/helmfile-1.7.1/test/integration/test-cases/issue-2712-rollback-on-failure/input/rollback.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/helmfile-1.7.2/test/integration/test-cases/issue-2712-rollback-on-failure/input/rollback.yaml 2026-07-31 16:22:31.000000000 +0200 @@ -0,0 +1,14 @@ +releases: +- name: issue-2712-rollback + chart: ../../../charts/raw + rollbackOnFailure: true + values: + - templates: + - | + apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ .Release.Name }}-cm + namespace: {{ .Release.Namespace }} + data: + version: "1" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/helmfile-1.7.1/test/integration/test-cases/issue-2712-rollback-on-failure.sh new/helmfile-1.7.2/test/integration/test-cases/issue-2712-rollback-on-failure.sh --- old/helmfile-1.7.1/test/integration/test-cases/issue-2712-rollback-on-failure.sh 1970-01-01 01:00:00.000000000 +0100 +++ new/helmfile-1.7.2/test/integration/test-cases/issue-2712-rollback-on-failure.sh 2026-07-31 16:22:31.000000000 +0200 @@ -0,0 +1,88 @@ +# Issue #2712: Support Helm 4 --rollback-on-failure alongside deprecated --atomic +# https://github.com/helmfile/helmfile/issues/2712 +# +# End-to-end coverage: +# 1. atomic: true is accepted (no unknown-field parse error), deploys, and emits the +# version-correct flag: --rollback-on-failure on Helm 4 (auto-migration of the +# deprecated --atomic), --atomic on Helm 3. +# 2. rollbackOnFailure: true emits --rollback-on-failure on Helm 4 and is rejected +# with a clear Helm-4-required error on Helm 3. +# +# The flag-emission assertions inspect the `exec: helm upgrade --install ...` lines +# that helmfile logs under --debug (see pkg/helmexec/exec.go exec helper). + +issue_2712_input_dir="${cases_dir}/issue-2712-rollback-on-failure/input" +issue_2712_tmp=$(mktemp -d) + +cleanup_issue_2712() { + rm -rf "${issue_2712_tmp}" +} +trap cleanup_issue_2712 EXIT + +# Succeeds when the captured --debug log contains a `helm upgrade --install` command +# carrying the given flag. The flag is matched as a standalone argument (surrounded by +# whitespace) so a release name such as "issue-2712-atomic" can never be confused with +# the "--atomic" flag. $1 = log file, $2 = flag token (e.g. --rollback-on-failure). +issue_2712_assert_upgrade_flag() { + grep -E "upgrade --install" "$1" | grep -qE "(^|[[:space:]])${2}([[:space:]]|$)" +} + +# --- Test 1: atomic: true is accepted, deploys, and emits the version-correct flag --- +if [ "${HELMFILE_HELM4}" = "1" ]; then + test_start "issue-2712 atomic migrates to --rollback-on-failure (Helm 4)" +else + test_start "issue-2712 atomic emits --atomic (Helm 3)" +fi + +info "Syncing release with atomic: true" +if ! ${helmfile} --debug -f "${issue_2712_input_dir}/atomic.yaml" sync > "${issue_2712_tmp}/atomic.log" 2>&1; then + cat "${issue_2712_tmp}/atomic.log" + fail "helmfile sync with atomic: true should succeed" +fi + +info "Verifying ConfigMap issue-2712-atomic-cm was applied" +${kubectl} get configmap issue-2712-atomic-cm > /dev/null \ + || fail "ConfigMap issue-2712-atomic-cm should exist after sync" + +if [ "${HELMFILE_HELM4}" = "1" ]; then + issue_2712_assert_upgrade_flag "${issue_2712_tmp}/atomic.log" "--rollback-on-failure" \ + || { cat "${issue_2712_tmp}/atomic.log"; fail "Helm 4 should migrate atomic:true to --rollback-on-failure"; } + if issue_2712_assert_upgrade_flag "${issue_2712_tmp}/atomic.log" "--atomic"; then + fail "Helm 4 should not emit the deprecated --atomic flag for atomic:true" + fi + test_pass "issue-2712 atomic migrates to --rollback-on-failure (Helm 4)" +else + issue_2712_assert_upgrade_flag "${issue_2712_tmp}/atomic.log" "--atomic" \ + || { cat "${issue_2712_tmp}/atomic.log"; fail "Helm 3 should emit --atomic for atomic:true"; } + test_pass "issue-2712 atomic emits --atomic (Helm 3)" +fi + +# --- Test 2: rollbackOnFailure behavior is version-gated --- +if [ "${HELMFILE_HELM4}" = "1" ]; then + test_start "issue-2712 rollbackOnFailure emits --rollback-on-failure (Helm 4)" + info "Syncing release with rollbackOnFailure: true" + if ! ${helmfile} --debug -f "${issue_2712_input_dir}/rollback.yaml" sync > "${issue_2712_tmp}/rollback.log" 2>&1; then + cat "${issue_2712_tmp}/rollback.log" + fail "helmfile sync with rollbackOnFailure: true should succeed on Helm 4" + fi + ${kubectl} get configmap issue-2712-rollback-cm > /dev/null \ + || fail "ConfigMap issue-2712-rollback-cm should exist after sync" + issue_2712_assert_upgrade_flag "${issue_2712_tmp}/rollback.log" "--rollback-on-failure" \ + || { cat "${issue_2712_tmp}/rollback.log"; fail "rollbackOnFailure:true should emit --rollback-on-failure on Helm 4"; } + test_pass "issue-2712 rollbackOnFailure emits --rollback-on-failure (Helm 4)" +else + test_start "issue-2712 rollbackOnFailure rejected on Helm 3" + info "Syncing release with rollbackOnFailure: true (expected to fail)" + if ${helmfile} -f "${issue_2712_input_dir}/rollback.yaml" sync > "${issue_2712_tmp}/rollback.log" 2>&1; then + cat "${issue_2712_tmp}/rollback.log" + fail "helmfile sync with rollbackOnFailure: true should fail on Helm 3" + fi + grep -q "rollbackOnFailure requires Helm 4" "${issue_2712_tmp}/rollback.log" \ + || { cat "${issue_2712_tmp}/rollback.log"; fail "expected 'rollbackOnFailure requires Helm 4' error on Helm 3"; } + test_pass "issue-2712 rollbackOnFailure rejected on Helm 3" +fi + +# --- Cleanup --- +info "Cleaning up issue-2712 releases" +${helmfile} -f "${issue_2712_input_dir}/atomic.yaml" destroy > /dev/null 2>&1 || true +${helmfile} -f "${issue_2712_input_dir}/rollback.yaml" destroy > /dev/null 2>&1 || true ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/helmfile/vendor.tar.gz /work/SRC/openSUSE:Factory/.helmfile.new.16738/vendor.tar.gz differ: char 16, line 1
