fix: array merge regression - layer arrays now replace defaults#2367
Merged
yxxhero merged 9 commits intoJan 18, 2026
Merged
Conversation
f6aee45 to
d115008
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes a regression introduced by #2288 where layer/environment arrays were incorrectly merged element-by-element instead of being replaced entirely, breaking Helmfile's documented behavior. The fix introduces automatic sparse array detection: arrays with nil values (from --state-values-set CLI) merge element-by-element, while complete arrays without nils (from YAML layers) replace entirely.
Changes:
- Added auto-detection heuristic to distinguish sparse CLI arrays from complete layer arrays
- Modified
MergeMapsto accept optional merge strategy parameter (backward compatible) - Removed misleading comments that no longer accurately describe the behavior
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
pkg/maputil/maputil.go |
Added sparse array auto-detection in mergeSlices(), introduced MergeOptions with ArrayMergeStrategy |
pkg/maputil/maputil_test.go |
Added comprehensive tests for both sparse and complete array behaviors |
pkg/environment/environment.go |
Removed outdated comment in GetMergedValues() |
pkg/environment/environment_test.go |
Added regression tests for both #2353 (layer replace) and #2281 (sparse merge) |
pkg/state/create.go |
Removed outdated comment in loadEnvValues() |
pkg/state/storage_test.go |
Added CI-only skip for flaky git-dependent test |
test/integration/test-cases/issue-2353-layer-array-replace/ |
Added E2E integration test for layer array replacement |
test/integration/run.sh |
Added new integration test to test suite |
d115008 to
0b25494
Compare
…file#2353) PR helmfile#2288 introduced element-by-element array merging to fix helmfile#2281, but this caused a regression where layer/environment arrays were merged instead of replacing base arrays entirely. This fix uses automatic sparse array detection: - Arrays with nil values (from --state-values-set) merge element-by-element - Arrays without nils (from layer YAML) replace entirely This follows Helm's documented behavior where arrays replace rather than merge. Signed-off-by: Aditya Menon <[email protected]>
0b25494 to
5355ee4
Compare
…ging The previous approach using ArrayMergeStrategySparse detection didn't work for --state-values-set array[0]=value because setting index 0 produces no nils in the array. This fix adds a CLIOverrides field to Environment that keeps CLI values separate from layer values. CLI overrides are merged last using ArrayMergeStrategyMerge (always element-by-element), while layer values use the default strategy (arrays replace). This ensures: - --state-values-set array[0]=x only changes index 0, preserving other elements - Layer/environment file arrays still replace base arrays entirely - Issue helmfile#2281 fix is preserved (--state-values-set array[1].field=x works) Signed-off-by: Aditya Menon <[email protected]>
Signed-off-by: Aditya Menon <[email protected]>
- Add Defaults field merging from ctxEnv to preserve base values across helmfile parts separated by --- - Fix merge order: current part values now correctly override previous parts (was reversed, causing older values to win) - Update 147 snapshot test files for new Environment log format with CLIOverrides field This completes the fix for issue helmfile#2353 by ensuring: 1. Layer arrays replace entirely (not element-by-element merge) 2. CLI --state-values-set sparse arrays still merge element-by-element 3. Multi-part helmfiles properly inherit and override values Signed-off-by: Aditya Menon <[email protected]>
179da21 to
a313081
Compare
- Initialize EmptyEnvironment with empty maps to match New() constructor - Update test comment to accurately describe ArrayMergeStrategySparse Signed-off-by: Aditya Menon <[email protected]>
This commit fixes a regression in the CLIOverrides integration where templates accessing .Environment.Values couldn't see CLI override values. Changes: - Remove CLIOverrides-into-Values merge from Merge() to keep proper layering order (Defaults → Values → CLIOverrides) in GetMergedValues() - Update NewEnvironmentTemplateData to set envCopy.Values to the merged values, ensuring templates see the same values via both .Values and .Environment.Values This ensures: - Issue helmfile#2353: Layer arrays still replace entirely (Sparse strategy) - Issue helmfile#2281: CLI sparse arrays still merge element-by-element - Templates can access CLI overrides via .Environment.Values Signed-off-by: Aditya Menon <[email protected]>
Address Copilot review comments on PR helmfile#2367: - Document empty array edge case: explicitly setting [] clears base array - Document recursive strategy propagation for nested map merging - Add comprehensive behavior description for all array merge strategies Signed-off-by: Aditya Menon <[email protected]>
Environment value files (*.yaml.gotmpl) can reference CLI values via .Values. Previously, only env.Values was passed to template rendering, which didn't include CLIOverrides. Now we call env.GetMergedValues() to get Defaults + Values + CLIOverrides before rendering, so templates can access CLI values like: --state-values-set foo=bar This fixes the state-values-set-cli-args-in-environments integration test. Signed-off-by: Aditya Menon <[email protected]>
yxxhero
approved these changes
Jan 18, 2026
aditmeno
added a commit
to aditmeno/helmfile
that referenced
this pull request
Mar 5, 2026
…by-element PR helmfile#2367 introduced CLIOverrides to give --state-values-set element-by-element array merge semantics. However, nested helmfile values (helmfiles[].values:) were also routed into CLIOverrides, causing their arrays to merge instead of replace. This broke the pre-v1.3.0 behavior where passing an array via helmfiles[].values: would fully replace the child's default array. Add OverrideValuesAreCLI flag to SubhelmfileEnvironmentSpec so the loader can distinguish CLI flags from nested helmfile values. CLI values continue using CLIOverrides (element-by-element merge); nested helmfile values now use Values (Sparse merge strategy → full array replacement). Fixes helmfile#2451
aditmeno
added a commit
to aditmeno/helmfile
that referenced
this pull request
Mar 5, 2026
…by-element PR helmfile#2367 introduced CLIOverrides to give --state-values-set element-by-element array merge semantics. However, nested helmfile values (helmfiles[].values:) were also routed into CLIOverrides, causing their arrays to merge instead of replace. This broke the pre-v1.3.0 behavior where passing an array via helmfiles[].values: would fully replace the child's default array. Add OverrideValuesAreCLI flag to SubhelmfileEnvironmentSpec so the loader can distinguish CLI flags from nested helmfile values. CLI values continue using CLIOverrides (element-by-element merge); nested helmfile values now use Values (Sparse merge strategy → full array replacement). Fixes helmfile#2451 Signed-off-by: Aditya Menon <[email protected]>
aditmeno
added a commit
to aditmeno/helmfile
that referenced
this pull request
Mar 8, 2026
…by-element PR helmfile#2367 introduced CLIOverrides to give --state-values-set element-by-element array merge semantics. However, nested helmfile values (helmfiles[].values:) were also routed into CLIOverrides, causing their arrays to merge instead of replace. This broke the pre-v1.3.0 behavior where passing an array via helmfiles[].values: would fully replace the child's default array. Add OverrideValuesAreCLI flag to SubhelmfileEnvironmentSpec so the loader can distinguish CLI flags from nested helmfile values. CLI values continue using CLIOverrides (element-by-element merge); nested helmfile values now use Values (Sparse merge strategy → full array replacement). Fixes helmfile#2451 Signed-off-by: Aditya Menon <[email protected]>
aditmeno
added a commit
to aditmeno/helmfile
that referenced
this pull request
Mar 8, 2026
…by-element PR helmfile#2367 introduced CLIOverrides to give --state-values-set element-by-element array merge semantics. However, nested helmfile values (helmfiles[].values:) were also routed into CLIOverrides, causing their arrays to merge instead of replace. This broke the pre-v1.3.0 behavior where passing an array via helmfiles[].values: would fully replace the child's default array. Add OverrideValuesAreCLI flag to SubhelmfileEnvironmentSpec so the loader can distinguish CLI flags from nested helmfile values. CLI values continue using CLIOverrides (element-by-element merge); nested helmfile values now use Values (Sparse merge strategy → full array replacement). Fixes helmfile#2451 Signed-off-by: Aditya Menon <[email protected]>
aditmeno
added a commit
to aditmeno/helmfile
that referenced
this pull request
Mar 8, 2026
…by-element PR helmfile#2367 introduced CLIOverrides to give --state-values-set element-by-element array merge semantics. However, nested helmfile values (helmfiles[].values:) were also routed into CLIOverrides, causing their arrays to merge instead of replace. This broke the pre-v1.3.0 behavior where passing an array via helmfiles[].values: would fully replace the child's default array. Add OverrideValuesAreCLI flag to SubhelmfileEnvironmentSpec so the loader can distinguish CLI flags from nested helmfile values. CLI values continue using CLIOverrides (element-by-element merge); nested helmfile values now use Values (Sparse merge strategy → full array replacement). Fixes helmfile#2451 Signed-off-by: Aditya Menon <[email protected]>
aditmeno
added a commit
to aditmeno/helmfile
that referenced
this pull request
Mar 8, 2026
…by-element PR helmfile#2367 introduced CLIOverrides to give --state-values-set element-by-element array merge semantics. However, nested helmfile values (helmfiles[].values:) were also routed into CLIOverrides, causing their arrays to merge instead of replace. This broke the pre-v1.3.0 behavior where passing an array via helmfiles[].values: would fully replace the child's default array. Add OverrideValuesAreCLI flag to SubhelmfileEnvironmentSpec so the loader can distinguish CLI flags from nested helmfile values. CLI values continue using CLIOverrides (element-by-element merge); nested helmfile values now use Values (Sparse merge strategy → full array replacement). Fixes helmfile#2451 Signed-off-by: Aditya Menon <[email protected]>
yxxhero
pushed a commit
that referenced
this pull request
Mar 9, 2026
…by-element (#2458) PR #2367 introduced CLIOverrides to give --state-values-set element-by-element array merge semantics. However, nested helmfile values (helmfiles[].values:) were also routed into CLIOverrides, causing their arrays to merge instead of replace. This broke the pre-v1.3.0 behavior where passing an array via helmfiles[].values: would fully replace the child's default array. Add OverrideValuesAreCLI flag to SubhelmfileEnvironmentSpec so the loader can distinguish CLI flags from nested helmfile values. CLI values continue using CLIOverrides (element-by-element merge); nested helmfile values now use Values (Sparse merge strategy → full array replacement). Fixes #2451 Signed-off-by: Aditya Menon <[email protected]>
yxxhero
added a commit
that referenced
this pull request
Apr 10, 2026
…be dropped (#2527) PR #2367 introduced envCopy.Values = values in NewEnvironmentTemplateData, where values = GetMergedValues() = Defaults + Values + CLIOverrides. This caused .Environment.Values to include Defaults, so when multi-part helmfiles re-assigned environment values via {{ toYaml .Environment.Values }}, Defaults values (e.g. helmDefaults.atomic: true) were written into the environment Values field. Later, GetMergedValues() applied Values over Defaults, causing the stale atomic: true to win over the correct atomic: false override. Fix: set .Environment.Values to Values + CLIOverrides only (excluding Defaults), so re-assignment patterns don't pollute the Values layer with Defaults. Signed-off-by: yxx <[email protected]> Signed-off-by: yxxhero <[email protected]>
4 tasks
yxxhero
added a commit
that referenced
this pull request
Apr 12, 2026
…) (#2532) * fix: environment values pollution causing boolean false overrides to be dropped (#2527) PR #2367 introduced envCopy.Values = values in NewEnvironmentTemplateData, where values = GetMergedValues() = Defaults + Values + CLIOverrides. This caused .Environment.Values to include Defaults, so when multi-part helmfiles re-assigned environment values via {{ toYaml .Environment.Values }}, Defaults values (e.g. helmDefaults.atomic: true) were written into the environment Values field. Later, GetMergedValues() applied Values over Defaults, causing the stale atomic: true to win over the correct atomic: false override. Fix: set .Environment.Values to Values + CLIOverrides only (excluding Defaults), so re-assignment patterns don't pollute the Values layer with Defaults. Signed-off-by: yxx <[email protected]> Signed-off-by: yxxhero <[email protected]> * fix: rename test to correctly reflect Values override Defaults precedence Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/1b251877-7050-404b-8cc7-abd6aa3ec36b Co-authored-by: yxxhero <[email protected]> * test: flip regression test fixture to exercise false override (issue #2527) Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/c428fd46-b698-4e88-bff2-4c9ac72d2deb Co-authored-by: yxxhero <[email protected]> --------- Signed-off-by: yxx <[email protected]> Signed-off-by: yxxhero <[email protected]> Co-authored-by: copilot-swe-agent[bot] <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #2353 - Array merge regression introduced by PR #2288.
PR #2288 introduced element-by-element array merging to fix #2281 (where
--state-values-set array[1].field=valuewas replacing entire arrays). However, this caused a regression: layer/environment arrays were now being merged instead of replacing base arrays entirely, breaking helmfile's documented behavior.Root Cause
The
mergeSlices()function unconditionally performed element-by-element array merging, but there are two different use cases requiring different behaviors:--state-values-set array[1].x=ySolution: Two-Pronged Approach
1. Automatic Sparse Array Detection (ArrayMergeStrategySparse)
The key insight is that
--state-values-setcreates sparse arrays with nil values at unset indices, while layer YAML creates complete arrays without nils:The heuristic:
Edge cases (documented):
[]has no nils, so it replaces entirely (intentional: explicitly setting empty clears the base)[null, value]in YAML is treated as sparse (rare but correct behavior)2. CLIOverrides Field for Explicit Tracking
Added a new
CLIOverridesfield to theEnvironmentstruct to explicitly track CLI values (--state-values-set,--state-values-file) separately from layer values. This allows using different merge strategies:e.Values): UseArrayMergeStrategySparse(auto-detect based on nils)e.CLIOverrides): UseArrayMergeStrategyMerge(always element-by-element)This follows Helm's documented behavior where arrays replace rather than merge.
Changes
Core Implementation
pkg/environment/environment.goCLIOverridesfield; updatedNew(),DeepCopy(),Merge(),GetMergedValues(); initializedEmptyEnvironmentwith empty maps; keep CLIOverrides separate until final mergepkg/maputil/maputil.goArrayMergeStrategyenum (Sparse,Replace,Merge); addedMergeOptionsstruct; updatedmergeSlices()with auto-detection and comprehensive documentationpkg/state/create.goDefaultsandCLIOverridesfrom context environmentpkg/state/types.goNewEnvironmentTemplateData()to use merged values for.Environment.Values, ensuring templates see CLI overridespkg/app/desired_state_file_loader.goCLIOverridesinstead ofValuesTests
pkg/maputil/maputil_test.gopkg/environment/environment_test.goGetMergedValues()pkg/state/storage_test.gotest/integration/test-cases/issue-2353-layer-array-replace/pkg/app/testdata/**Key Fixes
Multi-Part Helmfile Support
Fixed issues with helmfiles containing multiple
---separated parts:values:from earlier parts now properly inherited viaDefaultsfieldTemplate Access to CLI Values
Templates accessing
.Environment.Valuesnow see the same merged values as.Values:NewEnvironmentTemplateData()creates an environment copy with pre-merged values.Valuesand.Environment.Valuesreturn Defaults + Values + CLIOverridesEnvironment Struct
Value Merge Priority (in GetMergedValues)
Defaults(base, lowest priority)Values(layer/environment values)CLIOverrides(CLI flags, highest priority, element-by-element merge)Test Plan
pkg/maputil(sparse vs complete array detection)pkg/environment(GetMergedValues with both scenarios)pkg/app(StateValueOverrides, EnvironmentValueOverrides, multi-part templates)pkg/state.Valuesand.Environment.ValuesBackward Compatibility
.Environment.ValuesCopilot Review Feedback Addressed
[]edge case behaviormergeSlices()