From dac42105dd2dd230f45d16a05de042c0921761f3 Mon Sep 17 00:00:00 2001 From: yxxhero <11087727+yxxhero@users.noreply.github.com> Date: Fri, 1 May 2026 20:29:24 +0800 Subject: [PATCH] fix: deduplicate chart dependencies in helmfile.lock (#2567) When multiple releases reference the same chart with the same name, repository, and version, helmfile deps would write duplicate entries to helmfile.lock. This adds deduplication of resolved dependencies after sorting and before writing the lock file. Fixes #2562 Signed-off-by: yxxhero --- pkg/state/chart_dependency.go | 16 ++++++++ pkg/state/chart_dependency_test.go | 63 ++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/pkg/state/chart_dependency.go b/pkg/state/chart_dependency.go index 60101c8a..7f88081c 100644 --- a/pkg/state/chart_dependency.go +++ b/pkg/state/chart_dependency.go @@ -129,6 +129,20 @@ func (d *ResolvedDependencies) Get(chart, versionConstraint string) (string, err return "", fmt.Errorf("no resolved dependency found for \"%s\", running \"helmfile deps\" may resolve the issue", chart) } +func dedupResolvedDependencies(deps []ResolvedChartDependency) []ResolvedChartDependency { + seen := map[string]bool{} + result := make([]ResolvedChartDependency, 0, len(deps)) + for _, dep := range deps { + key := dep.ChartName + "|" + dep.Repository + "|" + dep.Version + if seen[key] { + continue + } + seen[key] = true + result = append(result, dep) + } + return result +} + func (st *HelmState) mergeLockedDependencies() (*HelmState, error) { filename, unresolved := getUnresolvedDependenciess(st) @@ -369,6 +383,8 @@ func (m *chartDependencyManager) doUpdate(chartLockFile string, unresolved *Unre return lockedReqs.ResolvedDependencies[i].ChartName < lockedReqs.ResolvedDependencies[j].ChartName }) + lockedReqs.ResolvedDependencies = dedupResolvedDependencies(lockedReqs.ResolvedDependencies) + lockedReqs.Version = version.Version() updatedLockFileContent, err = yaml.Marshal(lockedReqs) diff --git a/pkg/state/chart_dependency_test.go b/pkg/state/chart_dependency_test.go index 23c97c5c..70585b1c 100644 --- a/pkg/state/chart_dependency_test.go +++ b/pkg/state/chart_dependency_test.go @@ -6,6 +6,69 @@ import ( "github.com/stretchr/testify/require" ) +func TestDedupResolvedDependencies(t *testing.T) { + tests := []struct { + name string + input []ResolvedChartDependency + expected []ResolvedChartDependency + }{ + { + name: "no duplicates", + input: []ResolvedChartDependency{ + {ChartName: "app-template", Repository: "https://example.com", Version: "4.6.2"}, + {ChartName: "redis", Repository: "https://charts.bitnami.com/bitnami", Version: "17.0.7"}, + }, + expected: []ResolvedChartDependency{ + {ChartName: "app-template", Repository: "https://example.com", Version: "4.6.2"}, + {ChartName: "redis", Repository: "https://charts.bitnami.com/bitnami", Version: "17.0.7"}, + }, + }, + { + name: "duplicates removed", + input: []ResolvedChartDependency{ + {ChartName: "app-template", Repository: "https://example.com", Version: "4.6.2"}, + {ChartName: "app-template", Repository: "https://example.com", Version: "4.6.2"}, + }, + expected: []ResolvedChartDependency{ + {ChartName: "app-template", Repository: "https://example.com", Version: "4.6.2"}, + }, + }, + { + name: "same chart different versions kept", + input: []ResolvedChartDependency{ + {ChartName: "app-template", Repository: "https://example.com", Version: "4.6.2"}, + {ChartName: "app-template", Repository: "https://example.com", Version: "4.6.1"}, + }, + expected: []ResolvedChartDependency{ + {ChartName: "app-template", Repository: "https://example.com", Version: "4.6.2"}, + {ChartName: "app-template", Repository: "https://example.com", Version: "4.6.1"}, + }, + }, + { + name: "same chart different repos kept", + input: []ResolvedChartDependency{ + {ChartName: "app-template", Repository: "https://example.com", Version: "4.6.2"}, + {ChartName: "app-template", Repository: "https://other.com", Version: "4.6.2"}, + }, + expected: []ResolvedChartDependency{ + {ChartName: "app-template", Repository: "https://example.com", Version: "4.6.2"}, + {ChartName: "app-template", Repository: "https://other.com", Version: "4.6.2"}, + }, + }, + { + name: "empty input", + input: []ResolvedChartDependency{}, + expected: []ResolvedChartDependency{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := dedupResolvedDependencies(tt.input) + require.Equal(t, tt.expected, result) + }) + } +} + func TestGetUnresolvedDependenciess(t *testing.T) { tests := []struct { name string