fix: simplify collectDirectNeedsOnly to use need IDs as-is

After ApplyOverrides/reformat(), need IDs are already fully-qualified
(matching ReleaseToID format). The previous name-based lookup could
select the wrong dependency when multiple releases share the same name
across namespaces/kubecontexts.

Also adds a cross-namespace test case to verify correct behavior when
releases share names across different namespaces.

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/e8effb99-17de-4b2e-ae14-cc90c2108146
This commit is contained in:
copilot-swe-agent[bot] 2026-03-25 10:32:33 +00:00
parent a1bf95e9ba
commit 02a5de3330
2 changed files with 53 additions and 15 deletions

View File

@ -225,3 +225,53 @@ func TestSelectReleasesWithOverridesWithIncludedTransitives(t *testing.T) {
}
}
}
func TestSelectReleasesWithIncludeNeedsCrossNamespace(t *testing.T) {
// When multiple releases share the same name across different namespaces,
// includeNeeds should resolve the correct dependency using fully-qualified IDs
// rather than name-based lookup (which would be ambiguous).
example := []byte(`releases:
- name: frontend
namespace: team-a
chart: stable/testchart
needs:
- team-a/backend
- name: backend
namespace: team-a
chart: stable/testchart
- name: backend
namespace: team-b
chart: stable/testchart
`)
state := stateTestEnv{
Files: map[string]string{
"/helmfile.yaml": string(example),
},
WorkDir: "/",
}.MustLoadState(t, "/helmfile.yaml", "default")
state.Selectors = []string{"name=frontend"}
var err error
state.Releases, err = state.GetReleasesWithOverrides()
if err != nil {
t.Fatal(err)
}
state.Releases = state.GetReleasesWithLabels()
rs, err := state.GetSelectedReleases(true, false)
if err != nil {
t.Fatal(err)
}
var got []string
for _, r := range rs {
got = append(got, r.Namespace+"/"+r.Name)
}
// Should include team-a/backend (direct need) but NOT team-b/backend
want := []string{"team-a/frontend", "team-a/backend"}
if d := cmp.Diff(want, got); d != "" {
t.Errorf("cross-namespace include-needs: %s", d)
}
}

View File

@ -3065,24 +3065,12 @@ func unmarkNeedsDirectOnly(filteredReleases []Release) {
func collectDirectNeedsOnly(filteredReleases []Release) map[string]struct{} {
directNeeds := map[string]struct{}{}
nameToID := map[string]string{}
for _, r := range filteredReleases {
nameToID[r.Name] = ReleaseToID(&r.ReleaseSpec)
}
for _, r := range filteredReleases {
if !r.Filtered {
for _, need := range r.ReleaseSpec.Needs {
if fullID, ok := nameToID[need]; ok {
directNeeds[fullID] = struct{}{}
} else {
parts := strings.Split(need, "/")
needName := parts[len(parts)-1]
if fullID, ok := nameToID[needName]; ok {
directNeeds[fullID] = struct{}{}
} else {
directNeeds[need] = struct{}{}
}
}
// After ApplyOverrides/reformat(), need IDs are already fully-qualified
// (matching ReleaseToID format), so we collect them as-is.
directNeeds[need] = struct{}{}
}
}
}