From 06d0994b8486d5e373d286f4cb892c81ea049610 Mon Sep 17 00:00:00 2001 From: Thomas Arrow Date: Thu, 19 Mar 2026 00:45:42 +0000 Subject: [PATCH 01/12] Fix interactive apply asks in no change situation (#945) This commit makes the apply logic exit early in the event there are no changes to releases. I believe this effectively reverts helmfile#522. Updates relevant snapshots Clarify conditions under which preapply hooks are triggered to include that they will no longer fire if there is a no-op. Docs as requested by the maintainer from a copilot request made by them. Fixes: helmfile#679 Signed-off-by: Thomas Arrow Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com> --- docs/index.md | 2 +- pkg/app/app.go | 4 ++++ .../testapply/helm-status-check-to-release-existence/log | 5 ----- pkg/app/testdata/testapply/noop/log | 7 ------- .../testdata/testapply_hooks/hooks_for_no-diff_release/log | 6 ------ 5 files changed, 5 insertions(+), 19 deletions(-) diff --git a/docs/index.md b/docs/index.md index 1aeb0a9d..87efad75 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1588,7 +1588,7 @@ Hooks associated to `presync` events are triggered before each release is synced This is the ideal event to execute any commands that may mutate the cluster state as it will not be run for read-only operations like `lint`, `diff` or `template`. `preapply` hooks are triggered before a release is uninstalled, installed, or upgraded as part of `helmfile apply`. -This is the ideal event to hook into when you are going to use `helmfile apply` for every kind of change and you want the hook to be triggered regardless of whether the releases have changed or not. Be sure to make each `preapply` hook command idempotent. Otherwise, rerunning helmfile-apply on a transient failure may end up either breaking your cluster, or the hook that runs for the second time will never succeed. +This is the ideal event to hook into when you are going to use `helmfile apply` for every kind of change. Note that preapply hooks will only run if at least one release has changes to apply. Be sure to make each `preapply` hook command idempotent. Otherwise, rerunning `helmfile apply` on a transient failure may end up either breaking your cluster, or the hook that runs for the second time will never succeed. `preuninstall` hooks are triggered immediately before a release is uninstalled as part of `helmfile apply`, `helmfile sync`, `helmfile delete`, and `helmfile destroy`. diff --git a/pkg/app/app.go b/pkg/app/app.go index cdc31745..10a232b9 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -1704,6 +1704,10 @@ Do you really want to apply? // Traverse DAG of all the releases so that we don't suffer from false-positive missing dependencies st.Releases = selectedAndNeededReleases + if len(releasesToBeUpdated) == 0 && len(releasesToBeDeleted) == 0 { + return true, false, nil + } + if !interactive || interactive && r.askForConfirmation(confMsg) { if _, preapplyErrors := withDAG(st, helm, a.Logger, state.PlanOptions{Purpose: "invoking preapply hooks for", Reverse: true, SelectedReleases: toApplyWithNeeds, SkipNeeds: true}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error { for _, r := range subst.Releases { diff --git a/pkg/app/testdata/testapply/helm-status-check-to-release-existence/log b/pkg/app/testdata/testapply/helm-status-check-to-release-existence/log index 59565383..5aacb4ea 100644 --- a/pkg/app/testdata/testapply/helm-status-check-to-release-existence/log +++ b/pkg/app/testdata/testapply/helm-status-check-to-release-existence/log @@ -2,8 +2,3 @@ merged environment: &{default map[] map[] map[]} 2 release(s) found in helmfile.yaml Checking release existence using `helm status` for release foo_notFound -invoking preapply hooks for 1 groups of releases in this order: -GROUP RELEASES -1 default//bar, default//foo_notFound - -invoking preapply hooks for releases in group 1/1: default//bar, default//foo_notFound diff --git a/pkg/app/testdata/testapply/noop/log b/pkg/app/testdata/testapply/noop/log index 661f5a67..dc714f59 100644 --- a/pkg/app/testdata/testapply/noop/log +++ b/pkg/app/testdata/testapply/noop/log @@ -1,10 +1,3 @@ merged environment: &{default map[] map[] map[]} 2 release(s) found in helmfile.yaml -invoking preapply hooks for 2 groups of releases in this order: -GROUP RELEASES -1 default//foo -2 default//bar - -invoking preapply hooks for releases in group 1/2: default//foo -invoking preapply hooks for releases in group 2/2: default//bar diff --git a/pkg/app/testdata/testapply_hooks/hooks_for_no-diff_release/log b/pkg/app/testdata/testapply_hooks/hooks_for_no-diff_release/log index b2f0bdff..0da06e80 100644 --- a/pkg/app/testdata/testapply_hooks/hooks_for_no-diff_release/log +++ b/pkg/app/testdata/testapply_hooks/hooks_for_no-diff_release/log @@ -1,9 +1,3 @@ hook[prepare] logs | foo hook[prepare] logs | - -hook[preapply] logs | foo -hook[preapply] logs | - -hook[cleanup] logs | foo -hook[cleanup] logs | From 2ccd899399ae780d84275c05c71f48bcf54ca276 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:58:18 +0800 Subject: [PATCH 02/12] build(deps): bump google.golang.org/grpc from 1.79.2 to 1.79.3 (#2490) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.79.2 to 1.79.3. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.79.2...v1.79.3) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-version: 1.79.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 62cfd842..0e9d5038 100644 --- a/go.mod +++ b/go.mod @@ -104,7 +104,7 @@ require ( golang.org/x/time v0.15.0 // indirect google.golang.org/api v0.271.0 // indirect google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/grpc v1.79.2 // indirect + google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/ini.v1 v1.67.1 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect diff --git a/go.sum b/go.sum index b9d8245a..0ee5191b 100644 --- a/go.sum +++ b/go.sum @@ -1025,8 +1025,8 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi 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.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= -google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= 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= From df01afbbeb8c04845cba11a71079216518180588 Mon Sep 17 00:00:00 2001 From: yxxhero <11087727+yxxhero@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:25:03 +0800 Subject: [PATCH 03/12] fix: helmfile list now reflects version from helmfile.lock (#2486) * fix: helmfile list now reflects version from helmfile.lock The list command now resolves locked dependencies before returning release information, ensuring the version field reflects the pinned version from helmfile.lock when present. Fixes #1953 Signed-off-by: yxxhero * fix: address PR review comments - Remove redundant maps.Copy in list() - labels already merged by GetReleasesWithLabels() - Fix default lockfile path to use basePath for multi-file mode - Update test to expect basePath-joined lockfile path - Add multi-file test for lockfile resolution in helmfile.d directory Signed-off-by: yxxhero * fix more test Signed-off-by: yxxhero * fix tests Signed-off-by: yxxhero * fix: propagate errors instead of panic in list() When skipCharts=false, errors from list() now properly propagate instead of causing a crash. Uses a closure variable to capture the error and propagates it after withPreparedCharts completes. Signed-off-by: yxxhero * fix tests Signed-off-by: yxxhero * fix tests Signed-off-by: yxxhero * fix tests Signed-off-by: yxxhero --------- Signed-off-by: yxxhero --- pkg/app/app.go | 37 ++++---- pkg/app/app_list_test.go | 158 ++++++++++++++++++++++++++++++++++ pkg/state/chart_dependency.go | 20 ++++- pkg/state/state_test.go | 10 +-- 4 files changed, 199 insertions(+), 26 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index 10a232b9..a93ced24 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -640,26 +640,27 @@ func (a *App) ListReleases(c ListConfigProvider) error { err := a.ForEachState(func(run *Run) (_ bool, errs []error) { var stateReleases []*HelmRelease - var err error + var listErr error if !c.SkipCharts() { - err = run.withPreparedCharts("list", state.ChartPrepareOptions{ + prepErr := run.withPreparedCharts("list", state.ChartPrepareOptions{ SkipRepos: true, SkipDeps: true, Concurrency: 2, }, func() { - rel, err := a.list(run) - if err != nil { - panic(err) - } - stateReleases = rel + stateReleases, listErr = a.list(run) }) + if prepErr != nil { + errs = append(errs, prepErr) + } + if listErr != nil { + errs = append(errs, listErr) + } } else { - stateReleases, err = a.list(run) - } - - if err != nil { - errs = append(errs, err) + stateReleases, listErr = a.list(run) + if listErr != nil { + errs = append(errs, listErr) + } } if len(stateReleases) > 0 { @@ -701,14 +702,16 @@ func (a *App) ListReleases(c ListConfigProvider) error { func (a *App) list(run *Run) ([]*HelmRelease, error) { var releases []*HelmRelease - for _, r := range run.state.Releases { + resolvedState, err := run.state.ResolveDeps() + if err != nil { + return nil, fmt.Errorf("unable to resolve dependencies for %s: %w", run.state.FilePath, err) + } + + for _, r := range resolvedState.Releases { labels := "" if r.Labels == nil { r.Labels = map[string]string{} } - for k, v := range run.state.CommonLabels { - r.Labels[k] = v - } var keys []string for k := range r.Labels { @@ -722,7 +725,7 @@ func (a *App) list(run *Run) ([]*HelmRelease, error) { } labels = strings.Trim(labels, ",") - enabled, err := state.ConditionEnabled(r, run.state.Values()) + enabled, err := state.ConditionEnabled(r, resolvedState.Values()) if err != nil { return nil, err } diff --git a/pkg/app/app_list_test.go b/pkg/app/app_list_test.go index f8084682..9fbb4644 100644 --- a/pkg/app/app_list_test.go +++ b/pkg/app/app_list_test.go @@ -2,6 +2,7 @@ package app import ( "bytes" + "encoding/json" "os" "testing" @@ -301,3 +302,160 @@ func TestListWithJSONOutput(t *testing.T) { testListWithJSONOutput(t, configImpl{skipCharts: true}) }) } + +func TestListWithLockFileVersion(t *testing.T) { + files := map[string]string{ + "/path/to/helmfile.yaml": ` +repositories: +- name: bitnami + url: https://charts.bitnami.com/bitnami + +releases: +- name: redis + namespace: default + chart: bitnami/redis + version: ">=1.0.0" +`, + "/path/to/helmfile.lock": `version: v0.0.0 +digest: sha256:abc123 +generated: "2024-01-01T00:00:00Z" +dependencies: +- name: redis + repository: https://charts.bitnami.com/bitnami + version: 17.0.7 +`, + } + + stdout := os.Stdout + defer func() { os.Stdout = stdout }() + + var buffer bytes.Buffer + syncWriter := testhelper.NewSyncWriter(&buffer) + logger := helmexec.NewLogger(syncWriter, "debug") + + valsRuntime, err := vals.New(vals.Options{CacheSize: 32}) + if err != nil { + t.Fatalf("unexpected error creating vals runtime: %v", err) + } + + app := appWithFs(&App{ + OverrideHelmBinary: DefaultHelmBinary, + fs: ffs.DefaultFileSystem(), + OverrideKubeContext: "default", + DisableKubeVersionAutoDetection: true, + Env: "default", + Logger: logger, + valsRuntime: valsRuntime, + }, files) + + expectNoCallsToHelm(app) + + out, err := testutil.CaptureStdout(func() { + err := app.ListReleases(configImpl{skipCharts: true, output: "json"}) + assert.Nil(t, err) + }) + assert.NoError(t, err) + + var releases []HelmRelease + if err := json.Unmarshal([]byte(out), &releases); err != nil { + t.Fatalf("failed to parse JSON output: %v", err) + } + + assert.Len(t, releases, 1, "expected 1 release") + assert.Equal(t, "redis", releases[0].Name) + assert.Equal(t, "bitnami/redis", releases[0].Chart) + assert.Equal(t, "17.0.7", releases[0].Version, "expected version from helmfile.lock") +} + +func TestListWithLockFileVersion_MultiFile(t *testing.T) { + files := map[string]string{ + "/path/to/helmfile.d/first.yaml": ` +repositories: +- name: bitnami + url: https://charts.bitnami.com/bitnami + +releases: +- name: redis + namespace: default + chart: bitnami/redis + version: ">=1.0.0" +`, + "/path/to/helmfile.d/first.lock": `version: v0.0.0 +digest: sha256:abc123 +generated: "2024-01-01T00:00:00Z" +dependencies: +- name: redis + repository: https://charts.bitnami.com/bitnami + version: 17.0.7 +`, + "/path/to/helmfile.d/second.yaml": ` +repositories: +- name: bitnami + url: https://charts.bitnami.com/bitnami + +releases: +- name: nginx + namespace: default + chart: bitnami/nginx + version: ">=1.0.0" +`, + "/path/to/helmfile.d/second.lock": `version: v0.0.0 +digest: sha256:def456 +generated: "2024-01-01T00:00:00Z" +dependencies: +- name: nginx + repository: https://charts.bitnami.com/bitnami + version: 15.0.0 +`, + } + + stdout := os.Stdout + defer func() { os.Stdout = stdout }() + + var buffer bytes.Buffer + syncWriter := testhelper.NewSyncWriter(&buffer) + logger := helmexec.NewLogger(syncWriter, "debug") + + valsRuntime, err := vals.New(vals.Options{CacheSize: 32}) + if err != nil { + t.Fatalf("unexpected error creating vals runtime: %v", err) + } + + app := appWithFs(&App{ + OverrideHelmBinary: DefaultHelmBinary, + fs: ffs.DefaultFileSystem(), + OverrideKubeContext: "default", + DisableKubeVersionAutoDetection: true, + Env: "default", + Logger: logger, + valsRuntime: valsRuntime, + }, files) + + expectNoCallsToHelm(app) + + out, err := testutil.CaptureStdout(func() { + err := app.ListReleases(configImpl{skipCharts: true, output: "json"}) + assert.Nil(t, err) + }) + assert.NoError(t, err) + + var releases []HelmRelease + if err := json.Unmarshal([]byte(out), &releases); err != nil { + t.Fatalf("failed to parse JSON output: %v", err) + } + + assert.Len(t, releases, 2, "expected 2 releases") + + releaseMap := make(map[string]HelmRelease) + for _, r := range releases { + releaseMap[r.Name] = r + } + + redis := releaseMap["redis"] + assert.Equal(t, "bitnami/redis", redis.Chart) + assert.Equal(t, "17.0.7", redis.Version, "expected redis version from first.lock") + + nginx := releaseMap["nginx"] + assert.Equal(t, "bitnami/nginx", nginx.Chart) + assert.Equal(t, "15.0.0", nginx.Version, "expected nginx version from second.lock") +} diff --git a/pkg/state/chart_dependency.go b/pkg/state/chart_dependency.go index f8ee0de3..60101c8a 100644 --- a/pkg/state/chart_dependency.go +++ b/pkg/state/chart_dependency.go @@ -140,8 +140,15 @@ func (st *HelmState) mergeLockedDependencies() (*HelmState, error) { // When basePath is set (e.g. when loaded with baseDir instead of os.Chdir), // resolve the lock file path relative to basePath so it can be found // without changing the working directory. - if lockFile != "" && st.basePath != "" && !filepath.IsAbs(lockFile) { - lockFile = filepath.Join(st.basePath, lockFile) + switch { + case lockFile != "": + if st.basePath != "" && !filepath.IsAbs(lockFile) { + lockFile = filepath.Join(st.basePath, lockFile) + } + case st.basePath != "": + // When no custom lockfile is specified, use the default lockfile name + // joined with basePath to ensure it's found when not changing CWD. + lockFile = filepath.Join(st.basePath, filename+".lock") } depMan := NewChartDependencyManager(filename, st.logger, lockFile) @@ -258,8 +265,13 @@ func getUnresolvedDependenciess(st *HelmState) (string, *UnresolvedDependencies) func updateDependencies(st *HelmState, shell helmexec.DependencyUpdater, unresolved *UnresolvedDependencies, filename, wd string) (*HelmState, error) { lockFile := st.LockFile - if lockFile != "" && st.basePath != "" && !filepath.IsAbs(lockFile) { - lockFile = filepath.Join(st.basePath, lockFile) + switch { + case lockFile != "": + if st.basePath != "" && !filepath.IsAbs(lockFile) { + lockFile = filepath.Join(st.basePath, lockFile) + } + case st.basePath != "": + lockFile = filepath.Join(st.basePath, filename+".lock") } depMan := NewChartDependencyManager(filename, st.logger, lockFile) diff --git a/pkg/state/state_test.go b/pkg/state/state_test.go index 5de789b9..b8e62573 100644 --- a/pkg/state/state_test.go +++ b/pkg/state/state_test.go @@ -2548,10 +2548,10 @@ generated: 2019-05-16T15:42:45.50486+09:00 } logger := helmexec.NewLogger(io.Discard, "debug") - basePath := "/src" + basePath := filepath.ToSlash(t.TempDir()) state := &HelmState{ basePath: basePath, - FilePath: "/src/helmfile.yaml", + FilePath: filepath.Join(basePath, "helmfile.yaml"), ReleaseSetSpec: ReleaseSetSpec{ Releases: []ReleaseSpec{ { @@ -2584,8 +2584,8 @@ generated: 2019-05-16T15:42:45.50486+09:00 } fs := testhelper.NewTestFs(map[string]string{ - "/example/Chart.yaml": `foo: FOO`, - "/src/example/Chart.yaml": `foo: FOO`, + "/example/Chart.yaml": `foo: FOO`, + filepath.Join(basePath, "example/Chart.yaml"): `foo: FOO`, }) fs.Cwd = basePath state = injectFs(state, fs) @@ -2648,7 +2648,7 @@ func TestHelmState_ResolveDeps_NoLockFile(t *testing.T) { logger: logger, fs: &filesystem.FileSystem{ ReadFile: func(f string) ([]byte, error) { - if f != "helmfile.lock" { + if f != filepath.Join("/src", "helmfile.lock") { return nil, fmt.Errorf("stub: unexpected file: %s", f) } return nil, os.ErrNotExist From cbe383136e448aef7c4317f5019706ffdeb23022 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 07:55:30 +0800 Subject: [PATCH 04/12] build(deps): bump k8s.io/client-go from 0.35.2 to 0.35.3 (#2492) Bumps [k8s.io/client-go](https://github.com/kubernetes/client-go) from 0.35.2 to 0.35.3. - [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md) - [Commits](https://github.com/kubernetes/client-go/compare/v0.35.2...v0.35.3) --- updated-dependencies: - dependency-name: k8s.io/client-go dependency-version: 0.35.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 0e9d5038..338b8d37 100644 --- a/go.mod +++ b/go.mod @@ -37,8 +37,8 @@ require ( gopkg.in/yaml.v3 v3.0.1 helm.sh/helm/v3 v3.20.1 helm.sh/helm/v4 v4.1.3 - k8s.io/apimachinery v0.35.2 - k8s.io/client-go v0.35.2 + k8s.io/apimachinery v0.35.3 + k8s.io/client-go v0.35.3 ) require ( @@ -340,7 +340,7 @@ require ( gopkg.in/gookit/color.v1 v1.1.6 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/api v0.35.2 // indirect + k8s.io/api v0.35.3 // indirect k8s.io/apiextensions-apiserver v0.35.1 // indirect k8s.io/apiserver v0.35.1 // indirect k8s.io/cli-runtime v0.35.1 // indirect diff --git a/go.sum b/go.sum index 0ee5191b..bb1c95d1 100644 --- a/go.sum +++ b/go.sum @@ -1060,18 +1060,18 @@ helm.sh/helm/v4 v4.1.3 h1:Abfmb+oJUtxoaXDyB2Jhw1zRk3hT6aFfHta+AXb8Lno= helm.sh/helm/v4 v4.1.3/go.mod h1:5dSo8rRgn3OTkDAc/k0Ipw5/Q+BlqKIKZwa0XwSiINI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw= -k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60= +k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ= +k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4= k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w= k8s.io/apiextensions-apiserver v0.35.1/go.mod h1:2CN4fe1GZ3HMe4wBr25qXyJnJyZaquy4nNlNmb3R7AQ= -k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8= -k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8= +k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/apiserver v0.35.1 h1:potxdhhTL4i6AYAa2QCwtlhtB1eCdWQFvJV6fXgJzxs= k8s.io/apiserver v0.35.1/go.mod h1:BiL6Dd3A2I/0lBnteXfWmCFobHM39vt5+hJQd7Lbpi4= k8s.io/cli-runtime v0.35.1 h1:uKcXFe8J7AMAM4Gm2JDK4mp198dBEq2nyeYtO+JfGJE= k8s.io/cli-runtime v0.35.1/go.mod h1:55/hiXIq1C8qIJ3WBrWxEwDLdHQYhBNRdZOz9f7yvTw= -k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o= -k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g= +k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg= +k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c= k8s.io/component-base v0.35.1 h1:XgvpRf4srp037QWfGBLFsYMUQJkE5yMa94UsJU7pmcE= k8s.io/component-base v0.35.1/go.mod h1:HI/6jXlwkiOL5zL9bqA3en1Ygv60F03oEpnuU1G56Bs= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= From 57c2b51eb0da6a04e762e9ea27b98ca6283c12a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 08:38:08 +0800 Subject: [PATCH 05/12] build(deps): bump github.com/fatih/color from 1.18.0 to 1.19.0 (#2494) Bumps [github.com/fatih/color](https://github.com/fatih/color) from 1.18.0 to 1.19.0. - [Release notes](https://github.com/fatih/color/releases) - [Commits](https://github.com/fatih/color/compare/v1.18.0...v1.19.0) --- updated-dependencies: - dependency-name: github.com/fatih/color dependency-version: 1.19.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 338b8d37..850dd2da 100644 --- a/go.mod +++ b/go.mod @@ -57,7 +57,7 @@ require ( github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/blang/semver v3.5.1+incompatible // indirect github.com/dimchansky/utfbom v1.1.1 // indirect - github.com/fatih/color v1.18.0 + github.com/fatih/color v1.19.0 github.com/fujiwara/tfstate-lookup v1.10.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect diff --git a/go.sum b/go.sum index bb1c95d1..ba4ea454 100644 --- a/go.sum +++ b/go.sum @@ -307,8 +307,8 @@ github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSY github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw= github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fluxcd/cli-utils v0.37.2-flux.1 h1:tQ588ghtRN+E+kHq415FddfqA9v4brn/1WWgrP6rQR0= From 43b426892ef18b73ca217e2128e8566dd7275fec Mon Sep 17 00:00:00 2001 From: yxxhero <11087727+yxxhero@users.noreply.github.com> Date: Sat, 21 Mar 2026 14:29:32 +0800 Subject: [PATCH 06/12] fix: cleanup hooks not receiving error signal (#2475) * fix: cleanup hooks not receiving error signal Closes #1041 Signed-off-by: yxxhero <11087727+yxxhero@users.noreply.github.com> Signed-off-by: yxxhero * Add tests for cleanup hooks error propagation Signed-off-by: yxxhero * fix tests Signed-off-by: yxxhero * fix tests Signed-off-by: yxxhero * fix tests Signed-off-by: yxxhero * fix tests Signed-off-by: yxxhero --------- Signed-off-by: yxxhero <11087727+yxxhero@users.noreply.github.com> Signed-off-by: yxxhero --- pkg/app/app.go | 61 ++++++++----- pkg/app/cleanup_hooks_error_test.go | 121 ++++++++++++++++++++++++++ pkg/app/run.go | 13 ++- pkg/event/bus_test.go | 128 ++++++++++++++++++++++++++++ pkg/state/state.go | 4 +- 5 files changed, 300 insertions(+), 27 deletions(-) create mode 100644 pkg/app/cleanup_hooks_error_test.go diff --git a/pkg/app/app.go b/pkg/app/app.go index a93ced24..7fb6da5e 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -170,8 +170,9 @@ func (a *App) Diff(c DiffConfigProvider) error { Validate: c.Validate(), Concurrency: c.Concurrency(), IncludeTransitiveNeeds: c.IncludeNeeds(), - }, func() { + }, func() []error { msg, matched, affected, errs = a.diff(run, c) + return errs }) if msg != nil { @@ -245,8 +246,9 @@ func (a *App) Template(c TemplateConfigProvider) error { Values: c.Values(), KubeVersion: c.KubeVersion(), HelmOCIPlainHTTP: a.HelmOCIPlainHTTP, - }, func() { + }, func() []error { ok, errs = a.template(run, c) + return errs }) if prepErr != nil { @@ -265,8 +267,9 @@ func (a *App) WriteValues(c WriteValuesConfigProvider) error { SkipDeps: c.SkipDeps(), SkipCleanup: c.SkipCleanup(), Concurrency: c.Concurrency(), - }, func() { + }, func() []error { ok, errs = a.writeValues(run, c) + return errs }) if prepErr != nil { @@ -318,8 +321,9 @@ func (a *App) Lint(c LintConfigProvider) error { SkipCleanup: c.SkipCleanup(), Concurrency: c.Concurrency(), IncludeTransitiveNeeds: c.IncludeNeeds(), - }, func() { + }, func() []error { ok, lintErrs, errs = a.lint(run, c) + return append(errs, lintErrs...) }) if prepErr != nil { @@ -359,8 +363,9 @@ func (a *App) Unittest(c UnittestConfigProvider) error { SkipCleanup: c.SkipCleanup(), Concurrency: c.Concurrency(), IncludeTransitiveNeeds: c.IncludeTransitiveNeeds(), - }, func() { + }, func() []error { ok, unittestErrs, errs = a.unittest(run, c) + return append(errs, unittestErrs...) }) if prepErr != nil { @@ -395,7 +400,9 @@ func (a *App) Fetch(c FetchConfigProvider) error { OutputDir: c.OutputDir(), OutputDirTemplate: c.OutputDirTemplate(), Concurrency: c.Concurrency(), - }, func() {}) + }, func() []error { + return nil + }) if prepErr != nil { errs = append(errs, prepErr) @@ -420,8 +427,9 @@ func (a *App) Sync(c SyncConfigProvider) error { IncludeTransitiveNeeds: c.IncludeNeeds(), Validate: c.Validate(), Concurrency: c.Concurrency(), - }, func() { + }, func() []error { ok, errs = a.sync(run, c) + return errs }) if prepErr != nil { @@ -456,7 +464,7 @@ func (a *App) Apply(c ApplyConfigProvider) error { Validate: c.Validate(), Concurrency: c.Concurrency(), IncludeTransitiveNeeds: c.IncludeNeeds(), - }, func() { + }, func() []error { matched, updated, es := a.apply(run, c) mut.Lock() @@ -464,6 +472,7 @@ func (a *App) Apply(c ApplyConfigProvider) error { mut.Unlock() ok, errs = matched, es + return errs }) if prepErr != nil { @@ -492,8 +501,9 @@ func (a *App) Status(c StatusesConfigProvider) error { SkipRepos: true, SkipDeps: true, Concurrency: c.Concurrency(), - }, func() { + }, func() []error { ok, errs = a.status(run, c) + return errs }) if err != nil { @@ -514,8 +524,9 @@ func (a *App) Destroy(c DestroyConfigProvider) error { Concurrency: c.Concurrency(), DeleteWait: c.DeleteWait(), DeleteTimeout: c.DeleteTimeout(), - }, func() { + }, func() []error { ok, errs = a.delete(run, true, c) + return errs }) if err != nil { errs = append(errs, err) @@ -540,8 +551,9 @@ func (a *App) Test(c TestConfigProvider) error { SkipRefresh: c.SkipRefresh(), SkipDeps: c.SkipDeps(), Concurrency: c.Concurrency(), - }, func() { + }, func() []error { errs = a.test(run, c) + return errs }) if err != nil { @@ -559,11 +571,12 @@ func (a *App) PrintDAGState(c DAGConfigProvider) error { SkipRepos: true, SkipDeps: true, Concurrency: 2, - }, func() { + }, func() []error { err = a.dag(run) if err != nil { errs = append(errs, err) } + return errs }) return ok, errs }, false, SetFilter(true)) @@ -575,7 +588,7 @@ func (a *App) PrintState(c StateConfigProvider) error { SkipRepos: true, SkipDeps: true, Concurrency: 2, - }, func() { + }, func() []error { if c.EmbedValues() { for i := range run.state.Releases { r := run.state.Releases[i] @@ -583,7 +596,7 @@ func (a *App) PrintState(c StateConfigProvider) error { values, err := run.state.LoadYAMLForEmbedding(&r, r.Values, r.MissingFileHandler, r.ValuesPathPrefix) if err != nil { errs = []error{err} - return + return errs } run.state.Releases[i].Values = values @@ -591,7 +604,7 @@ func (a *App) PrintState(c StateConfigProvider) error { secrets, err := run.state.LoadYAMLForEmbedding(&r, r.Secrets, r.MissingFileHandler, r.ValuesPathPrefix) if err != nil { errs = []error{err} - return + return errs } run.state.Releases[i].Secrets = secrets @@ -601,17 +614,18 @@ func (a *App) PrintState(c StateConfigProvider) error { stateYaml, err := run.state.ToYaml() if err != nil { errs = []error{err} - return + return errs } sourceFile, err := run.state.FullFilePath() if err != nil { errs = []error{err} - return + return errs } fmt.Printf("---\n# Source: %s\n\n%+v", sourceFile, stateYaml) errs = []error{} + return errs }) if err != nil { @@ -647,15 +661,18 @@ func (a *App) ListReleases(c ListConfigProvider) error { SkipRepos: true, SkipDeps: true, Concurrency: 2, - }, func() { - stateReleases, listErr = a.list(run) + }, func() []error { + rel, err := a.list(run) + if err != nil { + errs = append(errs, err) + return []error{err} + } + stateReleases = rel + return nil }) if prepErr != nil { errs = append(errs, prepErr) } - if listErr != nil { - errs = append(errs, listErr) - } } else { stateReleases, listErr = a.list(run) if listErr != nil { diff --git a/pkg/app/cleanup_hooks_error_test.go b/pkg/app/cleanup_hooks_error_test.go new file mode 100644 index 00000000..9a877486 --- /dev/null +++ b/pkg/app/cleanup_hooks_error_test.go @@ -0,0 +1,121 @@ +package app + +import ( + "sync" + "testing" + + "github.com/helmfile/vals" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + + "github.com/helmfile/helmfile/pkg/exectest" + ffs "github.com/helmfile/helmfile/pkg/filesystem" + "github.com/helmfile/helmfile/pkg/helmexec" +) + +func TestCleanupHooksErrorPropagation(t *testing.T) { + type testcase struct { + files map[string]string + releaseName string + expectedError bool + expectedInLogs string + } + + check := func(t *testing.T, tc testcase) { + t.Helper() + + var helm = &exectest.Helm{ + FailOnUnexpectedList: true, + FailOnUnexpectedDiff: true, + DiffMutex: &sync.Mutex{}, + ChartsMutex: &sync.Mutex{}, + ReleasesMutex: &sync.Mutex{}, + } + + valsRuntime, err := vals.New(vals.Options{CacheSize: 32}) + if err != nil { + t.Fatalf("unexpected error creating vals runtime: %v", err) + } + + bs := runWithLogCapture(t, "info", func(t *testing.T, logger *zap.SugaredLogger) { + t.Helper() + + app := appWithFs(&App{ + OverrideHelmBinary: DefaultHelmBinary, + fs: ffs.DefaultFileSystem(), + OverrideKubeContext: "default", + DisableKubeVersionAutoDetection: true, + Env: "default", + Logger: logger, + helms: map[helmKey]helmexec.Interface{ + createHelmKey("helm", "default"): helm, + }, + valsRuntime: valsRuntime, + }, tc.files) + + syncErr := app.Sync(applyConfig{ + concurrency: 1, + logger: logger, + }) + + if tc.expectedError { + assert.Error(t, syncErr, "expected error for release %s", tc.releaseName) + } else { + assert.NoError(t, syncErr, "unexpected error for release %s", tc.releaseName) + } + }) + + logOutput := bs.String() + assert.Contains(t, logOutput, tc.expectedInLogs, "unexpected log output") + } + + t.Run("cleanup hook receives error when sync fails", func(t *testing.T) { + check(t, testcase{ + releaseName: "error-release", + files: map[string]string{ + "/path/to/helmfile.yaml": ` +hooks: + - name: global-cleanup + events: + - cleanup + showlogs: true + command: echo + args: + - "error is '{{ .Event.Error }}'" + +releases: + - name: error-release + chart: incubator/raw + namespace: default +`, + }, + expectedError: true, + expectedInLogs: "error is 'failed processing release error-release: error'", + }) + }) + + t.Run("cleanup hook receives nil when sync succeeds", func(t *testing.T) { + check(t, testcase{ + releaseName: "success-release", + files: map[string]string{ + "/path/to/helmfile.yaml": ` +hooks: + - name: global-cleanup + events: + - cleanup + showlogs: true + command: echo + args: + - "error is '{{ .Event.Error }}'" + +releases: + - name: success-release + chart: incubator/raw + namespace: default +`, + }, + expectedError: false, + expectedInLogs: "error is ''", + }) + }) +} diff --git a/pkg/app/run.go b/pkg/app/run.go index 10bd281d..91e25bb5 100644 --- a/pkg/app/run.go +++ b/pkg/app/run.go @@ -57,7 +57,7 @@ func (r *Run) prepareChartsIfNeeded(helmfileCommand string, dir string, concurre return releaseToChart, nil } -func (r *Run) withPreparedCharts(helmfileCommand string, opts state.ChartPrepareOptions, f func()) error { +func (r *Run) withPreparedCharts(helmfileCommand string, opts state.ChartPrepareOptions, f func() []error) error { if r.ReleaseToChart != nil { panic("Run.PrepareCharts can be called only once") } @@ -119,9 +119,16 @@ func (r *Run) withPreparedCharts(helmfileCommand string, opts state.ChartPrepare r.ReleaseToChart = releaseToChart - f() + errs := f() + var firstErr error + for _, e := range errs { + if e != nil { + firstErr = e + break + } + } - _, err = r.state.TriggerGlobalCleanupEvent(helmfileCommand) + _, err = r.state.TriggerGlobalCleanupEvent(helmfileCommand, firstErr) return err } diff --git a/pkg/event/bus_test.go b/pkg/event/bus_test.go index feb4eb2a..26c4b0f3 100644 --- a/pkg/event/bus_test.go +++ b/pkg/event/bus_test.go @@ -1,8 +1,10 @@ package event import ( + "errors" "fmt" "io" + "strings" "testing" "go.uber.org/zap" @@ -13,6 +15,11 @@ import ( ) type runner struct { + executeCalls []struct { + cmd string + args []string + env map[string]string + } } func (r *runner) ExecuteStdIn(cmd string, args []string, env map[string]string, stdin io.Reader) ([]byte, error) { @@ -20,6 +27,11 @@ func (r *runner) ExecuteStdIn(cmd string, args []string, env map[string]string, } func (r *runner) Execute(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) { + r.executeCalls = append(r.executeCalls, struct { + cmd string + args []string + env map[string]string + }{cmd: cmd, args: args, env: env}) if cmd == "ng" { return nil, fmt.Errorf("cmd failed due to invalid cmd: %s", cmd) } @@ -188,3 +200,119 @@ func TestTrigger(t *testing.T) { } } } + +func TestTriggerCleanupEventWithError(t *testing.T) { + runner := &runner{} + + core, _ := observer.New(zap.InfoLevel) + logger := zap.New(core).Sugar() + + testError := errors.New("sync failed: release error") + + hooks := []Hook{ + { + Name: "cleanup-with-error", + Events: []string{"cleanup"}, + Command: "echo", + Args: []string{"error is '{{ .Event.Error }}'"}, + ShowLogs: true, + }, + } + + bus := &Bus{ + Hooks: hooks, + StateFilePath: "/path/to/helmfile.yaml", + BasePath: ".", + Namespace: "default", + Env: environment.Environment{Name: "default"}, + Logger: logger, + Fs: ffs.DefaultFileSystem(), + Runner: runner, + } + + data := map[string]any{ + "HelmfileCommand": "sync", + } + + executed, err := bus.Trigger("cleanup", testError, data) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !executed { + t.Fatal("expected cleanup hook to be executed") + } + + if len(runner.executeCalls) != 1 { + t.Fatalf("expected 1 execute call, got %d", len(runner.executeCalls)) + } + + call := runner.executeCalls[0] + if call.cmd != "echo" { + t.Errorf("expected command 'echo', got %q", call.cmd) + } + + if len(call.args) != 1 { + t.Fatalf("expected 1 arg, got %d", len(call.args)) + } + + expectedArg := "error is 'sync failed: release error'" + if !strings.Contains(call.args[0], "error is") { + t.Errorf("expected arg to contain 'error is', got %q", call.args[0]) + } + + if call.args[0] != expectedArg { + t.Errorf("expected arg %q, got %q", expectedArg, call.args[0]) + } +} + +func TestTriggerCleanupEventWithNilError(t *testing.T) { + runner := &runner{} + + core, _ := observer.New(zap.InfoLevel) + logger := zap.New(core).Sugar() + + hooks := []Hook{ + { + Name: "cleanup-nil-error", + Events: []string{"cleanup"}, + Command: "echo", + Args: []string{"error is '{{ .Event.Error }}'"}, + ShowLogs: true, + }, + } + + bus := &Bus{ + Hooks: hooks, + StateFilePath: "/path/to/helmfile.yaml", + BasePath: ".", + Namespace: "default", + Env: environment.Environment{Name: "default"}, + Logger: logger, + Fs: ffs.DefaultFileSystem(), + Runner: runner, + } + + data := map[string]any{ + "HelmfileCommand": "sync", + } + + executed, err := bus.Trigger("cleanup", nil, data) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !executed { + t.Fatal("expected cleanup hook to be executed") + } + + if len(runner.executeCalls) != 1 { + t.Fatalf("expected 1 execute call, got %d", len(runner.executeCalls)) + } + + call := runner.executeCalls[0] + expectedArg := "error is ''" + if call.args[0] != expectedArg { + t.Errorf("expected arg %q, got %q", expectedArg, call.args[0]) + } +} diff --git a/pkg/state/state.go b/pkg/state/state.go index 5f929fd5..56e4a361 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -3105,8 +3105,8 @@ func (st *HelmState) TriggerGlobalPrepareEvent(helmfileCommand string) (bool, er return st.triggerGlobalReleaseEvent("prepare", nil, helmfileCommand) } -func (st *HelmState) TriggerGlobalCleanupEvent(helmfileCommand string) (bool, error) { - return st.triggerGlobalReleaseEvent("cleanup", nil, helmfileCommand) +func (st *HelmState) TriggerGlobalCleanupEvent(helmfileCommand string, evtErr error) (bool, error) { + return st.triggerGlobalReleaseEvent("cleanup", evtErr, helmfileCommand) } func (st *HelmState) triggerGlobalReleaseEvent(evt string, evtErr error, helmfileCmd string) (bool, error) { From 5c67cbcd6a4e8b9d0747d0667747baa39005d861 Mon Sep 17 00:00:00 2001 From: Hristiyan Ivanov <62302646+hristiy4n@users.noreply.github.com> Date: Sun, 22 Mar 2026 00:34:33 +0100 Subject: [PATCH 07/12] fix: pass --timeout flag through to helm for sync and apply (#2495) * fix: propagate timeout flag Signed-off-by: Hristiyan Ivanov * test: add test for propagating timeout flag Signed-off-by: Hristiyan Ivanov * feat: add timeout flag to apply command Signed-off-by: Hristiyan Ivanov * test: add test for timeout flag for helmfile apply Signed-off-by: Hristiyan Ivanov * fix: improve description of timeout flag Signed-off-by: Hristiyan Ivanov --------- Signed-off-by: Hristiyan Ivanov --- cmd/apply.go | 1 + cmd/sync.go | 2 +- pkg/app/app.go | 2 ++ pkg/app/app_apply_test.go | 28 +++++++++++++++++++ pkg/app/app_sync_test.go | 25 +++++++++++++++++ pkg/app/app_test.go | 5 ++++ pkg/app/config.go | 2 ++ .../timeout_flag_is_passed_to_helm/log | 21 ++++++++++++++ pkg/config/apply.go | 7 +++++ 9 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 pkg/app/testdata/testapply_2/timeout_flag_is_passed_to_helm/log diff --git a/cmd/apply.go b/cmd/apply.go index aa399737..dc9982b8 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -61,6 +61,7 @@ func NewApplyCmd(globalCfg *config.GlobalImpl) *cobra.Command { f.BoolVar(&applyOptions.SuppressDiff, "suppress-diff", false, "suppress diff in the output. Usable in new installs") f.BoolVar(&applyOptions.Wait, "wait", false, `Override helmDefaults.wait setting "helm upgrade --install --wait"`) f.BoolVar(&applyOptions.WaitForJobs, "wait-for-jobs", false, `Override helmDefaults.waitForJobs setting "helm upgrade --install --wait-for-jobs"`) + f.IntVar(&applyOptions.Timeout, "timeout", 0, `Override helmDefaults.timeout in seconds for "helm upgrade --install --timeout" (default 0, which uses helmDefaults.timeout or helm's default if not set)`) f.BoolVar(&applyOptions.ReuseValues, "reuse-values", false, `Override helmDefaults.reuseValues "helm upgrade --install --reuse-values"`) f.BoolVar(&applyOptions.ResetValues, "reset-values", false, `Override helmDefaults.reuseValues "helm upgrade --install --reset-values"`) f.StringVar(&applyOptions.PostRenderer, "post-renderer", "", `pass --post-renderer to "helm template" or "helm upgrade --install"`) diff --git a/cmd/sync.go b/cmd/sync.go index 376b1938..fdedbd72 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -47,7 +47,7 @@ func NewSyncCmd(globalCfg *config.GlobalImpl) *cobra.Command { f.BoolVar(&syncOptions.SyncReleaseLabels, "sync-release-labels", false, "sync release labels to the target release") f.BoolVar(&syncOptions.Wait, "wait", false, `Override helmDefaults.wait setting "helm upgrade --install --wait"`) f.BoolVar(&syncOptions.WaitForJobs, "wait-for-jobs", false, `Override helmDefaults.waitForJobs setting "helm upgrade --install --wait-for-jobs"`) - f.IntVar(&syncOptions.Timeout, "timeout", 0, `Override helmDefaults.timeout setting "helm upgrade --install --timeout" (default 0, which means no timeout)`) + f.IntVar(&syncOptions.Timeout, "timeout", 0, `Override helmDefaults.timeout in seconds for "helm upgrade --install --timeout" (default 0, which uses helmDefaults.timeout or helm's default if not set)`) f.BoolVar(&syncOptions.ReuseValues, "reuse-values", false, `Override helmDefaults.reuseValues "helm upgrade --install --reuse-values"`) f.BoolVar(&syncOptions.ResetValues, "reset-values", false, `Override helmDefaults.reuseValues "helm upgrade --install --reset-values"`) f.StringVar(&syncOptions.PostRenderer, "post-renderer", "", `pass --post-renderer to "helm template" or "helm upgrade --install"`) diff --git a/pkg/app/app.go b/pkg/app/app.go index 7fb6da5e..66748a44 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -1785,6 +1785,7 @@ Do you really want to apply? Wait: c.Wait(), WaitRetries: c.WaitRetries(), WaitForJobs: c.WaitForJobs(), + Timeout: c.Timeout(), ReuseValues: c.ReuseValues(), ResetValues: c.ResetValues(), PostRenderer: c.PostRenderer(), @@ -2253,6 +2254,7 @@ Do you really want to sync? Wait: c.Wait(), WaitRetries: c.WaitRetries(), WaitForJobs: c.WaitForJobs(), + Timeout: c.Timeout(), ReuseValues: c.ReuseValues(), ResetValues: c.ResetValues(), PostRenderer: c.PostRenderer(), diff --git a/pkg/app/app_apply_test.go b/pkg/app/app_apply_test.go index 2f235f1a..7dd663cf 100644 --- a/pkg/app/app_apply_test.go +++ b/pkg/app/app_apply_test.go @@ -25,6 +25,7 @@ func TestApply_2(t *testing.T) { fields fields ns string concurrency int + timeout int skipDiffOnInstall bool error string files map[string]string @@ -84,6 +85,7 @@ func TestApply_2(t *testing.T) { syncErr := app.Apply(applyConfig{ // if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic. concurrency: tc.concurrency, + timeout: tc.timeout, logger: logger, skipDiffOnInstall: tc.skipDiffOnInstall, skipNeeds: tc.fields.skipNeeds, @@ -653,4 +655,30 @@ foo 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default concurrency: 1, }) }) + + t.Run("timeout flag is passed to helm", func(t *testing.T) { + check(t, testcase{ + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: my-release + chart: incubator/raw + namespace: default +`, + }, + timeout: 300, + concurrency: 1, + upgraded: []exectest.Release{ + {Name: "my-release", Flags: []string{"--timeout", "300s", "--kube-context", "default", "--namespace", "default"}}, + }, + diffs: map[exectest.DiffKey]error{ + {Name: "my-release", Chart: "incubator/raw", Flags: "--kube-context default --namespace default --reset-values --detailed-exitcode"}: helmexec.ExitError{Code: 2}, + }, + lists: map[exectest.ListKey]string{ + {Filter: "^my-release$", Flags: listFlags("default", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE +my-release 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default +`, + }, + }) + }) } diff --git a/pkg/app/app_sync_test.go b/pkg/app/app_sync_test.go index 200e8f5c..ce38e77e 100644 --- a/pkg/app/app_sync_test.go +++ b/pkg/app/app_sync_test.go @@ -25,6 +25,7 @@ func TestSync(t *testing.T) { fields fields ns string concurrency int + timeout int skipDiffOnInstall bool error string files map[string]string @@ -81,6 +82,7 @@ func TestSync(t *testing.T) { syncErr := app.Sync(applyConfig{ concurrency: tc.concurrency, + timeout: tc.timeout, logger: logger, skipDiffOnInstall: tc.skipDiffOnInstall, skipNeeds: tc.fields.skipNeeds, @@ -478,4 +480,27 @@ releases: concurrency: 1, }) }) + + t.Run("timeout flag is passed to helm", func(t *testing.T) { + check(t, testcase{ + files: map[string]string{ + "/path/to/helmfile.yaml": ` +releases: +- name: my-release + chart: incubator/raw + namespace: default +`, + }, + timeout: 600, + concurrency: 1, + upgraded: []exectest.Release{ + {Name: "my-release", Flags: []string{"--timeout", "600s", "--kube-context", "default", "--namespace", "default"}}, + }, + lists: map[exectest.ListKey]string{ + {Filter: "^my-release$", Flags: listFlags("default", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE +my-release 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default +`, + }, + }) + }) } diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index ffc500fb..da895b46 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -2369,6 +2369,7 @@ type applyConfig struct { wait bool waitRetries int waitForJobs bool + timeout int reuseValues bool postRenderer string postRendererArgs []string @@ -2409,6 +2410,10 @@ func (a applyConfig) WaitForJobs() bool { return a.waitForJobs } +func (a applyConfig) Timeout() int { + return a.timeout +} + func (a applyConfig) Values() []string { return a.values } diff --git a/pkg/app/config.go b/pkg/app/config.go index 03bdc627..024e7cbe 100644 --- a/pkg/app/config.go +++ b/pkg/app/config.go @@ -59,6 +59,7 @@ type ApplyConfigProvider interface { Wait() bool WaitRetries() int WaitForJobs() bool + Timeout() int IncludeTests() bool @@ -114,6 +115,7 @@ type SyncConfigProvider interface { Wait() bool WaitRetries() int WaitForJobs() bool + Timeout() int SyncArgs() string Validate() bool diff --git a/pkg/app/testdata/testapply_2/timeout_flag_is_passed_to_helm/log b/pkg/app/testdata/testapply_2/timeout_flag_is_passed_to_helm/log new file mode 100644 index 00000000..5e22123e --- /dev/null +++ b/pkg/app/testdata/testapply_2/timeout_flag_is_passed_to_helm/log @@ -0,0 +1,21 @@ +merged environment: &{default map[] map[] map[]} +1 release(s) found in helmfile.yaml + +Affected releases are: + my-release (incubator/raw) UPDATED + +invoking preapply hooks for 1 groups of releases in this order: +GROUP RELEASES +1 default/default/my-release + +invoking preapply hooks for releases in group 1/1: default/default/my-release +processing 1 groups of releases in this order: +GROUP RELEASES +1 default/default/my-release + +processing releases in group 1/1: default/default/my-release + +UPDATED RELEASES: +NAME NAMESPACE CHART VERSION DURATION +my-release default incubator/raw 3.1.0 0s + diff --git a/pkg/config/apply.go b/pkg/config/apply.go index f97d712c..872844f7 100644 --- a/pkg/config/apply.go +++ b/pkg/config/apply.go @@ -57,6 +57,8 @@ type ApplyOptions struct { WaitRetries int // WaitForJobs is true if the helm command should wait for the jobs to be completed WaitForJobs bool + // Timeout is the timeout for helm operations in seconds + Timeout int // Propagate '--skip-schema-validation' to helmv3 template and helm install SkipSchemaValidation bool // ReuseValues is true if the helm command should reuse the values @@ -235,6 +237,11 @@ func (a *ApplyImpl) WaitForJobs() bool { return a.ApplyOptions.WaitForJobs } +// Timeout returns the timeout. +func (a *ApplyImpl) Timeout() int { + return a.ApplyOptions.Timeout +} + // ReuseValues returns the ReuseValues. func (a *ApplyImpl) ReuseValues() bool { if !a.ResetValues() { From 472e8c7a2d4025d8076d1ef274169c9a85fa169c Mon Sep 17 00:00:00 2001 From: yxxhero <11087727+yxxhero@users.noreply.github.com> Date: Tue, 24 Mar 2026 09:42:54 +0800 Subject: [PATCH 08/12] fix: error on missing secret key when using vals (#2496) * fix: error on missing secret key when using vals Add HELMFILE_VALS_FAIL_ON_MISSING_KEY_IN_MAP environment variable to control whether vals should fail when a referenced key does not exist in the secret map. Previously, when a secret reference like ref+vault://path#/nonexistent-key pointed to a non-existent key, vals would silently return an empty string without error. This could lead to deployments with missing configuration. Default behavior remains backward compatible (returns empty string). Set HELMFILE_VALS_FAIL_ON_MISSING_KEY_IN_MAP=true to enable strict mode. Fixes #1563 Signed-off-by: yxxhero * refactor: extract buildValsOptions helper and improve tests - Extract buildValsOptions() to make vals configuration testable - Use t.Setenv instead of manual env save/restore in tests - Test actual vals.Options output including FailOnMissingKeyInMap Addresses PR review comments on #2496 Signed-off-by: yxxhero * fix: use strconv.ParseBool and make tests hermetic - Use strconv.ParseBool for FailOnMissingKeyInMap parsing to support common boolean values like 'TRUE', '1', '0', etc. - Always set env vars explicitly in tests (even to empty string) to prevent flaky tests when env vars are set externally - Add test cases for various boolean formats Signed-off-by: yxxhero * docs: add documentation for vals-related environment variables Add documentation for: - HELMFILE_AWS_SDK_LOG_LEVEL: configure AWS SDK logging for vals - HELMFILE_VALS_FAIL_ON_MISSING_KEY_IN_MAP: enable strict mode for secret refs Signed-off-by: yxxhero * fix: improve error handling and case-insensitive comparison - buildValsOptions now returns error for invalid boolean values instead of silently defaulting to false - Use strings.EqualFold for case-insensitive 'off' comparison to handle OFF, Off, etc. - Add test cases for invalid boolean and uppercase OFF - Update docs to mention case-insensitive and error behavior Signed-off-by: yxxhero * fix: normalize log level and improve singleton initialization - Normalize AWS log level 'off' to lowercase for true case-insensitivity - Replace sync.Once with mutex to allow recovery from config errors - Update tests to expect normalized 'off' value - Update docs to clarify when error is raised Signed-off-by: yxxhero --------- Signed-off-by: yxxhero --- docs/index.md | 2 + pkg/envvar/const.go | 6 + pkg/plugins/vals.go | 110 ++++++++++------ pkg/plugins/vals_test.go | 264 ++++++++++++++++++++++++++------------- 4 files changed, 260 insertions(+), 122 deletions(-) diff --git a/docs/index.md b/docs/index.md index 87efad75..218d9d10 100644 --- a/docs/index.md +++ b/docs/index.md @@ -593,6 +593,8 @@ Helmfile uses some OS environment variables to override default behaviour: * `HELMFILE_FILE_PATH` - specify the path to the helmfile.yaml file * `HELMFILE_INTERACTIVE` - enable interactive mode, expecting `true` lower case. The same as `--interactive` CLI flag * `HELMFILE_RENDER_YAML` - force helmfile.yaml to be rendered as a Go template regardless of file extension, expecting `true` lower case. Useful for migrating from v0 to v1 without renaming files to `.gotmpl` +* `HELMFILE_AWS_SDK_LOG_LEVEL` - configure AWS SDK logging level for vals library. Valid values: `off` (default, secure, case-insensitive), `minimal`, `standard`, `verbose`, or custom comma-separated values like `request,response`. See issue #2270 for details +* `HELMFILE_VALS_FAIL_ON_MISSING_KEY_IN_MAP` - enable strict mode for vals secret references. When set to `true` (or any value accepted by Go's `strconv.ParseBool` like `TRUE`, `1`), vals will fail when a referenced key does not exist in the secret map. Invalid values will cause an error when vals is initialized (when secret refs are first evaluated). Default is `false` (when unset or empty) for backward compatibility. See issue #1563 for details ## CLI Reference diff --git a/pkg/envvar/const.go b/pkg/envvar/const.go index b5df7100..cd4d52bc 100644 --- a/pkg/envvar/const.go +++ b/pkg/envvar/const.go @@ -28,4 +28,10 @@ const ( // Can be overridden by AWS_SDK_GO_LOG_LEVEL environment variable // See issue #2270 and vals PR #893 AWSSDKLogLevel = "HELMFILE_AWS_SDK_LOG_LEVEL" + + // ValsFailOnMissingKeyInMap controls whether vals should fail when a key is missing in a map. + // When set to "true", vals returns an error if a referenced key does not exist in the secret map. + // Default is false for backward compatibility (returns empty string for missing keys). + // See issue #1563 + ValsFailOnMissingKeyInMap = "HELMFILE_VALS_FAIL_ON_MISSING_KEY_IN_MAP" ) diff --git a/pkg/plugins/vals.go b/pkg/plugins/vals.go index 8c65ceaf..f0c2767f 100644 --- a/pkg/plugins/vals.go +++ b/pkg/plugins/vals.go @@ -1,8 +1,10 @@ package plugins import ( + "fmt" "io" "os" + "strconv" "strings" "sync" @@ -17,46 +19,82 @@ const ( ) var instance *vals.Runtime -var once sync.Once +var mu sync.Mutex + +func buildValsOptions() (vals.Options, error) { + // Configure AWS SDK logging via HELMFILE_AWS_SDK_LOG_LEVEL environment variable + // Default: "off" to prevent sensitive information (tokens, auth headers) from being exposed + // See issue #2270 and vals PR helmfile/vals#893 + // + // Valid values: + // - "off" (default): No AWS SDK logging - secure, prevents credential leakage + // - "minimal": Log retries only - minimal debugging info + // - "standard": Log retries + requests - moderate debugging (previous default) + // - "verbose": Log everything - full debugging (requests, responses, bodies, signing) + // - Custom: Comma-separated values like "request,response" + // + // Note: AWS_SDK_GO_LOG_LEVEL environment variable always takes precedence over this setting + // Note: Case-insensitive for known values like "off", "OFF", "Off" + logLevel := strings.TrimSpace(os.Getenv(envvar.AWSSDKLogLevel)) + + // Configure fail on missing key behavior + // Default to false for backward compatibility + // Set HELMFILE_VALS_FAIL_ON_MISSING_KEY_IN_MAP=true to enable strict mode + // Supports common boolean values: "true", "TRUE", "1", etc. + // See issue #1563 + envVal := strings.TrimSpace(os.Getenv(envvar.ValsFailOnMissingKeyInMap)) + var failOnMissingKey bool + if envVal != "" { + var err error + failOnMissingKey, err = strconv.ParseBool(envVal) + if err != nil { + return vals.Options{}, fmt.Errorf("invalid value for %s: %q (must be a valid boolean)", envvar.ValsFailOnMissingKeyInMap, envVal) + } + } + + // Default to "off" for security if not specified + if logLevel == "" { + logLevel = "off" + } + + // Normalize known values to lowercase for case-insensitive handling + if strings.EqualFold(logLevel, "off") { + logLevel = "off" + } + + opts := vals.Options{ + CacheSize: valsCacheSize, + FailOnMissingKeyInMap: failOnMissingKey, + AWSLogLevel: logLevel, + } + + // Also suppress vals' own internal logging unless user wants verbose output + // This prevents vals' log messages (separate from AWS SDK logs) from exposing credentials + if logLevel == "off" { + opts.LogOutput = io.Discard + } + // For other levels, allow vals to log to default output for debugging + + return opts, nil +} func ValsInstance() (*vals.Runtime, error) { - var err error - once.Do(func() { - // Configure AWS SDK logging via HELMFILE_AWS_SDK_LOG_LEVEL environment variable - // Default: "off" to prevent sensitive information (tokens, auth headers) from being exposed - // See issue #2270 and vals PR helmfile/vals#893 - // - // Valid values: - // - "off" (default): No AWS SDK logging - secure, prevents credential leakage - // - "minimal": Log retries only - minimal debugging info - // - "standard": Log retries + requests - moderate debugging (previous default) - // - "verbose": Log everything - full debugging (requests, responses, bodies, signing) - // - Custom: Comma-separated values like "request,response" - // - // Note: AWS_SDK_GO_LOG_LEVEL environment variable always takes precedence over this setting - logLevel := strings.TrimSpace(os.Getenv(envvar.AWSSDKLogLevel)) + mu.Lock() + defer mu.Unlock() - opts := vals.Options{ - CacheSize: valsCacheSize, - } + if instance != nil { + return instance, nil + } - // Default to "off" for security if not specified - if logLevel == "" { - logLevel = "off" - } + opts, err := buildValsOptions() + if err != nil { + return nil, err + } - // Set AWS SDK log level for vals library - opts.AWSLogLevel = logLevel + instance, err = vals.New(opts) + if err != nil { + return nil, err + } - // Also suppress vals' own internal logging unless user wants verbose output - // This prevents vals' log messages (separate from AWS SDK logs) from exposing credentials - if logLevel == "off" { - opts.LogOutput = io.Discard - } - // For other levels, allow vals to log to default output for debugging - - instance, err = vals.New(opts) - }) - - return instance, err + return instance, nil } diff --git a/pkg/plugins/vals_test.go b/pkg/plugins/vals_test.go index 31d71d40..2b98489e 100644 --- a/pkg/plugins/vals_test.go +++ b/pkg/plugins/vals_test.go @@ -2,11 +2,11 @@ package plugins import ( "io" - "os" - "strings" "testing" "github.com/helmfile/vals" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/helmfile/helmfile/pkg/envvar" ) @@ -25,19 +25,165 @@ func TestValsInstance(t *testing.T) { } } -// TestAWSSDKLogLevelConfiguration tests the AWS SDK log level configuration logic +func TestBuildValsOptions(t *testing.T) { + tests := []struct { + name string + awsLogLevel string + failOnMissingKey string + expectedLogLevel string + expectedFailOnMissingKey bool + expectedLogOutputDiscarded bool + expectError bool + }{ + { + name: "defaults", + awsLogLevel: "", + failOnMissingKey: "", + expectedLogLevel: "off", + expectedFailOnMissingKey: false, + expectedLogOutputDiscarded: true, + }, + { + name: "explicit failOnMissingKey true", + awsLogLevel: "", + failOnMissingKey: "true", + expectedLogLevel: "off", + expectedFailOnMissingKey: true, + expectedLogOutputDiscarded: true, + }, + { + name: "failOnMissingKey false", + awsLogLevel: "", + failOnMissingKey: "false", + expectedLogLevel: "off", + expectedFailOnMissingKey: false, + expectedLogOutputDiscarded: true, + }, + { + name: "failOnMissingKey with whitespace", + awsLogLevel: "", + failOnMissingKey: " true ", + expectedLogLevel: "off", + expectedFailOnMissingKey: true, + expectedLogOutputDiscarded: true, + }, + { + name: "failOnMissingKey uppercase TRUE", + awsLogLevel: "", + failOnMissingKey: "TRUE", + expectedLogLevel: "off", + expectedFailOnMissingKey: true, + expectedLogOutputDiscarded: true, + }, + { + name: "failOnMissingKey numeric 1", + awsLogLevel: "", + failOnMissingKey: "1", + expectedLogLevel: "off", + expectedFailOnMissingKey: true, + expectedLogOutputDiscarded: true, + }, + { + name: "failOnMissingKey numeric 0", + awsLogLevel: "", + failOnMissingKey: "0", + expectedLogLevel: "off", + expectedFailOnMissingKey: false, + expectedLogOutputDiscarded: true, + }, + { + name: "failOnMissingKey invalid value", + awsLogLevel: "", + failOnMissingKey: "invalid", + expectError: true, + }, + { + name: "aws log level verbose", + awsLogLevel: "verbose", + failOnMissingKey: "", + expectedLogLevel: "verbose", + expectedFailOnMissingKey: false, + expectedLogOutputDiscarded: false, + }, + { + name: "aws log level with whitespace", + awsLogLevel: " minimal ", + failOnMissingKey: "", + expectedLogLevel: "minimal", + expectedFailOnMissingKey: false, + expectedLogOutputDiscarded: false, + }, + { + name: "aws log level OFF uppercase", + awsLogLevel: "OFF", + failOnMissingKey: "", + expectedLogLevel: "off", + expectedFailOnMissingKey: false, + expectedLogOutputDiscarded: true, + }, + { + name: "aws log level Off mixed case", + awsLogLevel: "Off", + failOnMissingKey: "", + expectedLogLevel: "off", + expectedFailOnMissingKey: false, + expectedLogOutputDiscarded: true, + }, + { + name: "aws log level Off mixed case", + awsLogLevel: "Off", + failOnMissingKey: "", + expectedLogLevel: "off", + expectedFailOnMissingKey: false, + expectedLogOutputDiscarded: true, + }, + { + name: "both options set", + awsLogLevel: "standard", + failOnMissingKey: "true", + expectedLogLevel: "standard", + expectedFailOnMissingKey: true, + expectedLogOutputDiscarded: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv(envvar.AWSSDKLogLevel, tt.awsLogLevel) + t.Setenv(envvar.ValsFailOnMissingKeyInMap, tt.failOnMissingKey) + + opts, err := buildValsOptions() + + if tt.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), envvar.ValsFailOnMissingKeyInMap) + return + } + + require.NoError(t, err) + + assert.Equal(t, tt.expectedLogLevel, opts.AWSLogLevel) + assert.Equal(t, tt.expectedFailOnMissingKey, opts.FailOnMissingKeyInMap) + assert.Equal(t, valsCacheSize, opts.CacheSize) + + isDiscarded := opts.LogOutput == io.Discard + assert.Equal(t, tt.expectedLogOutputDiscarded, isDiscarded) + }) + } +} + func TestAWSSDKLogLevelConfiguration(t *testing.T) { tests := []struct { name string envValue string expectedLogLevel string - expectedLogOutput bool // true if LogOutput should be io.Discard + expectedLogOutput bool }{ { name: "no env var defaults to off", envValue: "", expectedLogLevel: "off", - expectedLogOutput: true, // LogOutput should be io.Discard + expectedLogOutput: true, }, { name: "explicit off", @@ -45,11 +191,17 @@ func TestAWSSDKLogLevelConfiguration(t *testing.T) { expectedLogLevel: "off", expectedLogOutput: true, }, + { + name: "OFF uppercase", + envValue: "OFF", + expectedLogLevel: "off", + expectedLogOutput: true, + }, { name: "minimal logging", envValue: "minimal", expectedLogLevel: "minimal", - expectedLogOutput: false, // LogOutput should NOT be io.Discard + expectedLogOutput: false, }, { name: "standard logging", @@ -73,94 +225,34 @@ func TestAWSSDKLogLevelConfiguration(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Note: This test verifies the configuration logic, not the actual vals.New() call - // since ValsInstance() uses sync.Once and can only be initialized once per test run. + t.Setenv(envvar.AWSSDKLogLevel, tt.envValue) - // Simulate the logic from ValsInstance() - var logLevel string - if tt.envValue != "" { - logLevel = strings.TrimSpace(tt.envValue) - } + opts, err := buildValsOptions() + require.NoError(t, err) - // Default to "off" for security if not specified - if logLevel == "" { - logLevel = "off" - } + assert.Equal(t, tt.expectedLogLevel, opts.AWSLogLevel) - // Verify expected log level - if logLevel != tt.expectedLogLevel { - t.Errorf("Expected log level %q, got %q", tt.expectedLogLevel, logLevel) - } - - // Verify LogOutput configuration logic - opts := vals.Options{ - CacheSize: valsCacheSize, - } - opts.AWSLogLevel = logLevel - - // Verify LogOutput is set to io.Discard only when level is "off" - if tt.expectedLogOutput { - opts.LogOutput = io.Discard - if opts.LogOutput != io.Discard { - t.Error("Expected LogOutput to be io.Discard for 'off' level") - } - } + isDiscarded := opts.LogOutput == io.Discard + assert.Equal(t, tt.expectedLogOutput, isDiscarded) }) } } -// TestEnvironmentVariableReading verifies that the HELMFILE_AWS_SDK_LOG_LEVEL env var is read correctly -func TestEnvironmentVariableReading(t *testing.T) { - tests := []struct { - name string - envValue string - expectedValue string - }{ - { - name: "empty defaults to off", - envValue: "", - expectedValue: "off", - }, - { - name: "whitespace trimmed", - envValue: " minimal ", - expectedValue: "minimal", - }, - { - name: "standard value preserved", - envValue: "standard", - expectedValue: "standard", - }, - } +func TestBuildValsOptionsIntegration(t *testing.T) { + t.Run("valid configuration produces working vals options", func(t *testing.T) { + t.Setenv(envvar.AWSSDKLogLevel, "off") + t.Setenv(envvar.ValsFailOnMissingKeyInMap, "true") - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Save and restore env var - original := os.Getenv(envvar.AWSSDKLogLevel) - defer func() { - if original == "" { - os.Unsetenv(envvar.AWSSDKLogLevel) - } else { - os.Setenv(envvar.AWSSDKLogLevel, original) - } - }() + opts, err := buildValsOptions() + require.NoError(t, err) - // Set test env var - if tt.envValue == "" { - os.Unsetenv(envvar.AWSSDKLogLevel) - } else { - os.Setenv(envvar.AWSSDKLogLevel, tt.envValue) - } + assert.Equal(t, valsCacheSize, opts.CacheSize) + assert.Equal(t, "off", opts.AWSLogLevel) + assert.True(t, opts.FailOnMissingKeyInMap) + assert.Equal(t, io.Discard, opts.LogOutput) - // Read and process like ValsInstance() does - logLevel := strings.TrimSpace(os.Getenv(envvar.AWSSDKLogLevel)) - if logLevel == "" { - logLevel = "off" - } - - if logLevel != tt.expectedValue { - t.Errorf("Expected %q, got %q", tt.expectedValue, logLevel) - } - }) - } + rt, err := vals.New(opts) + require.NoError(t, err) + assert.NotNil(t, rt) + }) } From e72315a876778b0f0cfbc74a87e5d9f983c765be Mon Sep 17 00:00:00 2001 From: yxxhero <11087727+yxxhero@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:28:41 +0800 Subject: [PATCH 09/12] build: update helm-diff to v3.15.3 (#2498) Signed-off-by: yxxhero --- .github/workflows/ci.yaml | 12 ++++++------ Dockerfile | 2 +- Dockerfile.debian-stable-slim | 2 +- Dockerfile.ubuntu | 2 +- pkg/app/init.go | 2 +- test/integration/run.sh | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5683dc20..c8fe94c8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -96,35 +96,35 @@ jobs: - helm-version: v3.18.6 kustomize-version: v5.8.0 plugin-secrets-version: 4.7.4 - plugin-diff-version: 3.15.1 + plugin-diff-version: 3.15.3 extra-helmfile-flags: '' # In case you need to test some optional helmfile features, # enable it via extra-helmfile-flags below. - helm-version: v3.18.6 kustomize-version: v5.8.0 plugin-secrets-version: 4.7.4 - plugin-diff-version: 3.15.1 + plugin-diff-version: 3.15.3 extra-helmfile-flags: '--enable-live-output' - helm-version: v3.20.1 kustomize-version: v5.8.0 plugin-secrets-version: 4.7.4 - plugin-diff-version: 3.15.1 + plugin-diff-version: 3.15.3 extra-helmfile-flags: '' - helm-version: v3.20.1 kustomize-version: v5.8.0 plugin-secrets-version: 4.7.4 - plugin-diff-version: 3.15.1 + plugin-diff-version: 3.15.3 extra-helmfile-flags: '--enable-live-output' # Helmfile now supports both Helm 3.x and Helm 4.x - helm-version: v4.1.3 kustomize-version: v5.8.0 plugin-secrets-version: 4.7.4 - plugin-diff-version: 3.15.1 + plugin-diff-version: 3.15.3 extra-helmfile-flags: '' - helm-version: v4.1.3 kustomize-version: v5.8.0 plugin-secrets-version: 4.7.4 - plugin-diff-version: 3.15.1 + plugin-diff-version: 3.15.3 extra-helmfile-flags: '--enable-live-output' steps: - uses: actions/checkout@v6 diff --git a/Dockerfile b/Dockerfile index 1e7340e9..6709dc81 100644 --- a/Dockerfile +++ b/Dockerfile @@ -95,7 +95,7 @@ RUN set -x && \ [ "$(age-keygen --version)" = "${AGE_VERSION}" ] ARG HELM_SECRETS_VERSION="4.7.4" -RUN helm plugin install https://github.com/databus23/helm-diff --version v3.15.1 --verify=false && \ +RUN helm plugin install https://github.com/databus23/helm-diff --version v3.15.3 --verify=false && \ helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-${HELM_SECRETS_VERSION}.tgz --verify=false && \ helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-getter-${HELM_SECRETS_VERSION}.tgz --verify=false && \ helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-post-renderer-${HELM_SECRETS_VERSION}.tgz --verify=false && \ diff --git a/Dockerfile.debian-stable-slim b/Dockerfile.debian-stable-slim index fe542dbf..9d4f7de8 100644 --- a/Dockerfile.debian-stable-slim +++ b/Dockerfile.debian-stable-slim @@ -104,7 +104,7 @@ RUN set -x && \ [ "$(age-keygen --version)" = "${AGE_VERSION}" ] ARG HELM_SECRETS_VERSION="4.7.4" -RUN helm plugin install https://github.com/databus23/helm-diff --version v3.15.1 --verify=false && \ +RUN helm plugin install https://github.com/databus23/helm-diff --version v3.15.3 --verify=false && \ helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-${HELM_SECRETS_VERSION}.tgz --verify=false && \ helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-getter-${HELM_SECRETS_VERSION}.tgz --verify=false && \ helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-post-renderer-${HELM_SECRETS_VERSION}.tgz --verify=false && \ diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index ee21fc1c..757f4500 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -104,7 +104,7 @@ RUN set -x && \ [ "$(age-keygen --version)" = "${AGE_VERSION}" ] ARG HELM_SECRETS_VERSION="4.7.4" -RUN helm plugin install https://github.com/databus23/helm-diff --version v3.15.1 --verify=false && \ +RUN helm plugin install https://github.com/databus23/helm-diff --version v3.15.3 --verify=false && \ helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-${HELM_SECRETS_VERSION}.tgz --verify=false && \ helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-getter-${HELM_SECRETS_VERSION}.tgz --verify=false && \ helm plugin install https://github.com/jkroepke/helm-secrets/releases/download/v${HELM_SECRETS_VERSION}/secrets-post-renderer-${HELM_SECRETS_VERSION}.tgz --verify=false && \ diff --git a/pkg/app/init.go b/pkg/app/init.go index 223fd10f..7fcd0e81 100644 --- a/pkg/app/init.go +++ b/pkg/app/init.go @@ -19,7 +19,7 @@ import ( const ( HelmRequiredVersion = "v3.18.6" // Minimum required version (supports Helm 3.x and 4.x) - HelmDiffRecommendedVersion = "v3.15.1" + HelmDiffRecommendedVersion = "v3.15.3" HelmRecommendedVersion = "v4.1.0" // Recommended to use latest Helm 4 HelmSecretsRecommendedVersion = "v4.7.4" // v4.7.0+ works with both Helm 3 (single plugin) and Helm 4 (split plugin architecture) HelmGitRecommendedVersion = "v1.3.0" diff --git a/test/integration/run.sh b/test/integration/run.sh index 80099707..5f4dfdde 100755 --- a/test/integration/run.sh +++ b/test/integration/run.sh @@ -27,7 +27,7 @@ export HELM_DATA_HOME="${helm_dir}/data" export HELM_HOME="${HELM_DATA_HOME}" export HELM_PLUGINS="${HELM_DATA_HOME}/plugins" export HELM_CONFIG_HOME="${helm_dir}/config" -HELM_DIFF_VERSION="${HELM_DIFF_VERSION:-3.15.1}" +HELM_DIFF_VERSION="${HELM_DIFF_VERSION:-3.15.3}" HELM_GIT_VERSION="${HELM_GIT_VERSION:-1.4.1}" HELM_SECRETS_VERSION="${HELM_SECRETS_VERSION:-4.7.4}" export GNUPGHOME="${PWD}/${dir}/.gnupg" From c70b20ad7a90c6b843a31dae1b1a42c958bd5ae5 Mon Sep 17 00:00:00 2001 From: Jinyu Date: Tue, 24 Mar 2026 21:01:44 +0800 Subject: [PATCH 10/12] feat: add an arg that passing description to `helm upgrade` command (#2497) * feat: add an arg that passing description to `helm upgrade` command fix: github actions Signed-off-by: swimablefish * fix: lint and test failed Signed-off-by: swimablefish * feat: encapsulation Signed-off-by: swimablefish * feat: add version gate Signed-off-by: swimablefish * feat: rephrase Signed-off-by: swimablefish --------- Signed-off-by: swimablefish --- cmd/apply.go | 1 + cmd/sync.go | 1 + pkg/app/app.go | 2 + pkg/app/app_test.go | 4 + pkg/app/config.go | 4 + pkg/config/apply.go | 7 ++ pkg/config/sync.go | 7 ++ pkg/state/state.go | 33 +++++++- pkg/state/state_test.go | 182 ++++++++++++++++++++++++++++++++++++++++ pkg/state/temp_test.go | 12 +-- 10 files changed, 246 insertions(+), 7 deletions(-) diff --git a/cmd/apply.go b/cmd/apply.go index dc9982b8..c79e97cb 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -72,6 +72,7 @@ func NewApplyCmd(globalCfg *config.GlobalImpl) *cobra.Command { f.StringVar(&applyOptions.TrackMode, "track-mode", "", "Track mode for releases: 'helm' (default), 'helm-legacy' (Helm v4 only), or 'kubedog'") f.IntVar(&applyOptions.TrackTimeout, "track-timeout", 0, `Timeout in seconds for kubedog tracking (0 to use default 300s timeout)`) f.BoolVar(&applyOptions.TrackLogs, "track-logs", false, "Enable log streaming with kubedog tracking") + f.StringVar(&applyOptions.Description, "description", "", `Set description for all releases. If set, overridesdescriptions in helmfile.yaml. Will be passed to "helm upgrade --description"`) return cmd } diff --git a/cmd/sync.go b/cmd/sync.go index fdedbd72..5d180103 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -57,6 +57,7 @@ func NewSyncCmd(globalCfg *config.GlobalImpl) *cobra.Command { f.StringVar(&syncOptions.TrackMode, "track-mode", "", "Track mode for releases: 'helm' (default), 'helm-legacy' (Helm v4 only), or 'kubedog'") f.IntVar(&syncOptions.TrackTimeout, "track-timeout", 0, `Timeout in seconds for kubedog tracking (0 to use default 300s timeout)`) f.BoolVar(&syncOptions.TrackLogs, "track-logs", false, "Enable log streaming with kubedog tracking") + f.StringVar(&syncOptions.Description, "description", "", `Set description for all releases. If set, overrides descriptions in helmfile.yaml. Will be passed to "helm upgrade --description"`) return cmd } diff --git a/pkg/app/app.go b/pkg/app/app.go index 66748a44..a4b40b88 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -1798,6 +1798,7 @@ Do you really want to apply? TrackMode: c.TrackMode(), TrackTimeout: c.TrackTimeout(), TrackLogs: c.TrackLogs(), + Description: c.Description(), } return subst.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), syncOpts) })) @@ -2267,6 +2268,7 @@ Do you really want to sync? TrackMode: c.TrackMode(), TrackTimeout: c.TrackTimeout(), TrackLogs: c.TrackLogs(), + Description: c.Description(), } return subst.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), opts) })) diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index da895b46..21ec2d4e 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -2610,6 +2610,10 @@ func (a applyConfig) TrackLogs() bool { return a.trackLogs } +func (a applyConfig) Description() string { + return "" +} + type depsConfig struct { skipRepos bool includeTransitiveNeeds bool diff --git a/pkg/app/config.go b/pkg/app/config.go index 024e7cbe..9b92e498 100644 --- a/pkg/app/config.go +++ b/pkg/app/config.go @@ -92,6 +92,8 @@ type ApplyConfigProvider interface { TrackTimeout() int TrackLogs() bool + Description() string + concurrencyConfig interactive loggingConfig @@ -129,6 +131,8 @@ type SyncConfigProvider interface { TrackTimeout() int TrackLogs() bool + Description() string + DAGConfig concurrencyConfig diff --git a/pkg/config/apply.go b/pkg/config/apply.go index 872844f7..8580bba2 100644 --- a/pkg/config/apply.go +++ b/pkg/config/apply.go @@ -88,6 +88,8 @@ type ApplyOptions struct { TrackTimeout int // TrackLogs enables log streaming with kubedog TrackLogs bool + // Description is the description that will be passed to helm upgrade --description + Description string } // NewApply creates a new Apply @@ -314,6 +316,11 @@ func (a *ApplyImpl) TrackLogs() bool { return a.ApplyOptions.TrackLogs } +// Description returns the description. +func (a *ApplyImpl) Description() string { + return a.ApplyOptions.Description +} + func (a *ApplyImpl) ValidateConfig() error { validTrackModes := []string{"helm", "helm-legacy", "kubedog"} if a.ApplyOptions.TrackMode != "" && !slices.Contains(validTrackModes, a.ApplyOptions.TrackMode) { diff --git a/pkg/config/sync.go b/pkg/config/sync.go index 79a90eb6..8124108b 100644 --- a/pkg/config/sync.go +++ b/pkg/config/sync.go @@ -59,6 +59,8 @@ type SyncOptions struct { TrackTimeout int // TrackLogs enables log streaming with kubedog TrackLogs bool + // Description is the description that will be passed to helm upgrade --description + Description string } // NewSyncOptions creates a new Apply @@ -214,6 +216,11 @@ func (t *SyncImpl) TrackLogs() bool { return t.SyncOptions.TrackLogs } +// Description returns the description. +func (t *SyncImpl) Description() string { + return t.SyncOptions.Description +} + func (t *SyncImpl) ValidateConfig() error { validTrackModes := []string{"helm", "helm-legacy", "kubedog"} if t.SyncOptions.TrackMode != "" && !slices.Contains(validTrackModes, t.SyncOptions.TrackMode) { diff --git a/pkg/state/state.go b/pkg/state/state.go index 56e4a361..cec6396d 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -353,7 +353,10 @@ type ReleaseSpec struct { UnitTests []string `yaml:"unitTests,omitempty"` // Name is the name of this release - Name string `yaml:"name,omitempty"` + Name string `yaml:"name,omitempty"` + + // Description is the description for this release that will be passed to helm upgrade with --description flag + Description string `yaml:"description,omitempty"` Namespace string `yaml:"namespace,omitempty"` Labels map[string]string `yaml:"labels,omitempty"` Values []any `yaml:"values,omitempty"` @@ -909,6 +912,7 @@ type SyncOpts struct { TrackMode string TrackTimeout int TrackLogs bool + Description string } type SyncOpt interface{ Apply(*SyncOpts) } @@ -3394,6 +3398,28 @@ func (st *HelmState) appendChartDownloadFlags(flags []string, release *ReleaseSp return flags } +// appendDescriptionFlags appends the helm command-line flag for release description +// Command line takes precedence over config file +func (st *HelmState) appendDescriptionFlags(flags []string, release *ReleaseSpec, opt *SyncOpts, helm helmexec.Interface) ([]string, error) { + description := release.Description + if opt != nil && opt.Description != "" { + description = opt.Description + } + + if description != "" { + if !helm.IsVersionAtLeast("3.3.0") { + // Determine error message based on source + if opt != nil && opt.Description != "" { + return nil, fmt.Errorf("--description flag requires Helm 3.3.0 or greater") + } + return nil, fmt.Errorf("releases[].description requires Helm 3.3.0 or greater") + } + flags = append(flags, "--description", description) + } + + return flags, nil +} + func (st *HelmState) needsPlainHttp(release *ReleaseSpec, repo *RepositorySpec) bool { var repoPlainHttp, relPlainHttp bool if repo != nil { @@ -3523,6 +3549,11 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp flags = st.appendPostRenderFlags(flags, release, postRenderer, helm) + flags, err := st.appendDescriptionFlags(flags, release, opt, helm) + if err != nil { + return nil, nil, err + } + var postRendererArgs []string if opt != nil { postRendererArgs = opt.PostRendererArgs diff --git a/pkg/state/state_test.go b/pkg/state/state_test.go index b8e62573..98c1f258 100644 --- a/pkg/state/state_test.go +++ b/pkg/state/state_test.go @@ -908,6 +908,188 @@ func TestHelmState_flagsForUpgrade(t *testing.T) { "--namespace", "test-namespace", }, }, + { + name: "description-from-release", + defaults: HelmSpec{ + Verify: false, + CreateNamespace: &disable, + }, + version: semver.MustParse("3.10.0"), + release: &ReleaseSpec{ + Chart: "test/chart", + Version: "0.1", + Name: "test-charts", + Namespace: "test-namespace", + Description: "Release description from config", + }, + syncOpts: &SyncOpts{}, + want: []string{ + "--version", "0.1", + "--description", "Release description from config", + "--namespace", "test-namespace", + }, + }, + { + name: "description-from-cli", + defaults: HelmSpec{ + Verify: false, + CreateNamespace: &disable, + }, + version: semver.MustParse("3.10.0"), + release: &ReleaseSpec{ + Chart: "test/chart", + Version: "0.1", + Name: "test-charts", + Namespace: "test-namespace", + }, + syncOpts: &SyncOpts{ + Description: "CLI description from --description flag", + }, + want: []string{ + "--version", "0.1", + "--description", "CLI description from --description flag", + "--namespace", "test-namespace", + }, + }, + { + name: "description-cli-overrides-release", + defaults: HelmSpec{ + Verify: false, + CreateNamespace: &disable, + }, + version: semver.MustParse("3.10.0"), + release: &ReleaseSpec{ + Chart: "test/chart", + Version: "0.1", + Name: "test-charts", + Namespace: "test-namespace", + Description: "Release description from config", + }, + syncOpts: &SyncOpts{ + Description: "CLI description overrides config", + }, + want: []string{ + "--version", "0.1", + "--description", "CLI description overrides config", + "--namespace", "test-namespace", + }, + }, + { + name: "description-empty-string-not-passed", + defaults: HelmSpec{ + Verify: false, + CreateNamespace: &disable, + }, + version: semver.MustParse("3.10.0"), + release: &ReleaseSpec{ + Chart: "test/chart", + Version: "0.1", + Name: "test-charts", + Namespace: "test-namespace", + Description: "", + }, + syncOpts: &SyncOpts{ + Description: "", + }, + want: []string{ + "--version", "0.1", + "--namespace", "test-namespace", + }, + }, + { + name: "description-from-config-unsupported-version-3.1.0", + defaults: HelmSpec{ + Verify: false, + CreateNamespace: &disable, + }, + version: semver.MustParse("3.1.0"), + release: &ReleaseSpec{ + Chart: "test/chart", + Version: "0.1", + Name: "test-charts", + Namespace: "test-namespace", + Description: "Release description from config", + }, + syncOpts: &SyncOpts{}, + wantErr: "releases[].description requires Helm 3.3.0 or greater", + }, + { + name: "description-from-config-unsupported-version-3.2.4", + defaults: HelmSpec{ + Verify: false, + CreateNamespace: &disable, + }, + version: semver.MustParse("3.2.4"), + release: &ReleaseSpec{ + Chart: "test/chart", + Version: "0.1", + Name: "test-charts", + Namespace: "test-namespace", + Description: "Release description from config", + }, + syncOpts: &SyncOpts{}, + wantErr: "releases[].description requires Helm 3.3.0 or greater", + }, + { + name: "description-from-cli-unsupported-version", + defaults: HelmSpec{ + Verify: false, + CreateNamespace: &disable, + }, + version: semver.MustParse("3.2.0"), + release: &ReleaseSpec{ + Chart: "test/chart", + Version: "0.1", + Name: "test-charts", + Namespace: "test-namespace", + }, + syncOpts: &SyncOpts{ + Description: "CLI description from --description flag", + }, + wantErr: "--description flag requires Helm 3.3.0 or greater", + }, + { + name: "description-empty-on-old-version", + defaults: HelmSpec{ + Verify: false, + CreateNamespace: &disable, + }, + version: semver.MustParse("3.1.0"), + release: &ReleaseSpec{ + Chart: "test/chart", + Version: "0.1", + Name: "test-charts", + Namespace: "test-namespace", + // No description set + }, + syncOpts: &SyncOpts{}, + want: []string{ + "--version", "0.1", + "--namespace", "test-namespace", + // No --description flag should appear + }, + }, + { + name: "description-from-config-supported-version-3.3.0", + defaults: HelmSpec{ + Verify: false, + CreateNamespace: &disable, + }, + version: semver.MustParse("3.3.0"), + release: &ReleaseSpec{ + Chart: "test/chart", + Version: "0.1", + Name: "test-charts", + Namespace: "test-namespace", + Description: "Release description from config", + }, + syncOpts: &SyncOpts{}, + want: []string{ + "--version", "0.1", + "--description", "Release description from config", + "--namespace", "test-namespace", + }, + }, } for i := range tests { tt := tests[i] diff --git a/pkg/state/temp_test.go b/pkg/state/temp_test.go index 0dcc6153..7ea16939 100644 --- a/pkg/state/temp_test.go +++ b/pkg/state/temp_test.go @@ -38,39 +38,39 @@ func TestGenerateID(t *testing.T) { run(testcase{ subject: "baseline", release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"}, - want: "foo-values-5bc9c89c6b", + want: "foo-values-6ccb848dcd", }) run(testcase{ subject: "different bytes content", release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"}, data: []byte(`{"k":"v"}`), - want: "foo-values-7bf9c8bcdf", + want: "foo-values-5bcbbc4c85", }) run(testcase{ subject: "different map content", release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"}, data: map[string]any{"k": "v"}, - want: "foo-values-65694d8947", + want: "foo-values-7c6468f955", }) run(testcase{ subject: "different chart", release: ReleaseSpec{Name: "foo", Chart: "stable/envoy"}, - want: "foo-values-856c5f7dd5", + want: "foo-values-8645f5847f", }) run(testcase{ subject: "different name", release: ReleaseSpec{Name: "bar", Chart: "incubator/raw"}, - want: "bar-values-fff55fbf5", + want: "bar-values-54bd8c865", }) run(testcase{ subject: "specific ns", release: ReleaseSpec{Name: "foo", Chart: "incubator/raw", Namespace: "myns"}, - want: "myns-foo-values-6bfbb74765", + want: "myns-foo-values-b4849b445", }) for id, n := range ids { From a9175ab927787c5763b0891a17260496187d901a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 06:06:45 +0800 Subject: [PATCH 11/12] build(deps): bump azure/setup-helm from 4.3.1 to 5.0.0 (#2499) --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c8fe94c8..dbbdee63 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -57,7 +57,7 @@ jobs: go-version-file: go.mod - name: check disk usage run: df -h - - uses: azure/setup-helm@v4.3.1 + - uses: azure/setup-helm@v5.0.0 with: version: ${{ matrix.helm-version }} - name: Build From 2053fdd94eda94471f5cfd1abbcb8798af3e354e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 06:07:15 +0800 Subject: [PATCH 12/12] build(deps): bump github.com/aws/aws-sdk-go-v2/service/s3 from 1.97.1 to 1.97.2 (#2500) --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 850dd2da..67c02224 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/Masterminds/sprig/v3 v3.3.0 github.com/aws/aws-sdk-go-v2/config v1.32.12 - github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1 + github.com/aws/aws-sdk-go-v2/service/s3 v1.97.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 @@ -156,7 +156,7 @@ require ( 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.41.4 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.0 // indirect diff --git a/go.sum b/go.sum index ba4ea454..b689e7bb 100644 --- a/go.sum +++ b/go.sum @@ -155,8 +155,8 @@ 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.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 h1:3kGOqnh1pPeddVa/E37XNTaWJ8W6vrbYV9lJEkCnhuY= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= @@ -183,8 +183,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 h1:siU1A6xjUZ2N8 github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw= github.com/aws/aws-sdk-go-v2/service/kms v1.50.2 h1:UOHOXigIzDRaEU03CBQcZ5uW7FNC7E+vwfhsQWXl5RQ= github.com/aws/aws-sdk-go-v2/service/kms v1.50.2/go.mod h1:nAa5gmcmAmjXN3tGuhPSHLXFeWv+7nzKhjZzh8F7MH0= -github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1 h1:csi9NLpFZXb9fxY7rS1xVzgPRGMt7MSNWeQ6eo247kE= -github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1/go.mod h1:qXVal5H0ChqXP63t6jze5LmFalc7+ZE7wOdLtZ0LCP0= +github.com/aws/aws-sdk-go-v2/service/s3 v1.97.2 h1:MRNiP6nqa20aEl8fQ6PJpEq11b2d40b16sm4WD7QgMU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.97.2/go.mod h1:FrNA56srbsr3WShiaelyWYEo70x80mXnVZ17ZZfbeqg= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.3 h1:9bb0dEq1WzA0ZxIGG2EmwEgxfMAJpHyusxwbVN7f6iM= github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.3/go.mod h1:2z9eg35jfuRtdPE4Ci0ousrOU9PBhDBilXA1cwq9Ptk= github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=