diff --git a/pkg/state/selector_test.go b/pkg/state/selector_test.go index e027b9a6..fe6ff245 100644 --- a/pkg/state/selector_test.go +++ b/pkg/state/selector_test.go @@ -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) + } +} diff --git a/pkg/state/state.go b/pkg/state/state.go index 09bf65f6..b473af1f 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -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{}{} } } }