|
| 1 | +--- |
| 2 | +hip: "0027" |
| 3 | +title: "Bring .helmignore to parity with .gitignore file targeting syntax" |
| 4 | +authors: ["Scott Rigby <scott@r6by.com>"] |
| 5 | +created: "2025-12-14" |
| 6 | +type: "feature" |
| 7 | +status: "draft" |
| 8 | +requires: ["HIP-0020"] |
| 9 | +--- |
| 10 | + |
| 11 | +## Abstract |
| 12 | + |
| 13 | +This proposal brings `.helmignore` file targeting semantics to full parity with `.gitignore` syntax and matching rules for Helm Charts v3. The current `.helmignore` implementation diverges from `.gitignore` in critical ways—most notably in rule evaluation order (first-match vs. last-match), negation pattern behavior, and lack of `**` recursive glob support. These differences cause confusion and bugs for users who reasonably expect `.helmignore` to behave like `.gitignore`. By scoping this change to Charts v3 (per [HIP-0020][hip-0020]), existing charts continue to work unchanged while v3 charts opt into consistent, predictable ignore behavior. |
| 14 | + |
| 15 | +## Motivation |
| 16 | + |
| 17 | +Users familiar with `.gitignore` expect the same behavior from `.helmignore`. The current divergence causes: |
| 18 | + |
| 19 | +1. **Broken negation patterns**: Users cannot use whitelist-style patterns (e.g., `/*` then `!Chart.yaml`) because the current implementation has inverted negation semantics ([#8688], [#3622], [#1776]). |
| 20 | + |
| 21 | +2. **Missing recursive globs**: The `**` pattern is explicitly unsupported, forcing verbose workarounds for common cases like `**/test/` ([#12592]). |
| 22 | + |
| 23 | +3. **Unexpected first-match behavior**: Git uses "last matching rule wins"; Helm stops at the first match. This breaks patterns that progressively refine what to exclude. |
| 24 | + |
| 25 | +4. **Documentation gaps**: Current docs don't clearly explain limitations or differences from `.gitignore` ([#4638], [helm-www#1312]). |
| 26 | + |
| 27 | +### Evidence of User Pain |
| 28 | + |
| 29 | +A comprehensive search of helm/helm issues reveals **27 directly related issues** and **5 pull requests**. Key themes: |
| 30 | + |
| 31 | +- **Pattern matching logic**: [#8688], [#3622], [#1776], [#12592] |
| 32 | +- **Scope/behavior confusion**: [#6075], [#3050], [#10764] |
| 33 | +- **Documentation issues**: [#4638], [helm-www#1171], [helm-www#1312], [helm-www#1460] |
| 34 | + |
| 35 | +The recurring user expectation is clear: `.helmignore` should work like `.gitignore`. |
| 36 | + |
| 37 | +## Rationale |
| 38 | + |
| 39 | +### Why .gitignore Parity? |
| 40 | + |
| 41 | +1. **Reduced cognitive load**: Developers already know `.gitignore` semantics. Aligning `.helmignore` enables existing knowledge and patterns to be reused with Helm. |
| 42 | + |
| 43 | +2. **Well-documented spec**: Git's ignore format is [thoroughly documented][git-gitignore] and battle-tested across millions of repositories. |
| 44 | + |
| 45 | +3. **Ecosystem consistency**: Tools like Docker (`.dockerignore`), npm (`.npmignore`), and FluxCD (`.sourceignore`) all follow `.gitignore` semantics. |
| 46 | + |
| 47 | +### Why Charts v3? |
| 48 | + |
| 49 | +Per [HIP-0020][hip-0020], Charts v3 provides a clean opt-in boundary for breaking changes: |
| 50 | + |
| 51 | +1. **Breaking change isolation**: Charts explicitly declare `apiVersion: v3`, accepting new semantics. Charts v2 behavior is preserved unchanged. |
| 52 | + |
| 53 | +2. **No forced migration**: Existing charts continue to work. Users migrate on their own schedule. |
| 54 | + |
| 55 | +3. **Independent evolution**: Chart changes are decoupled from Helm version, allowing adequate time for testing and adoption. |
| 56 | + |
| 57 | +This is the appropriate scope because changing `.helmignore` semantics would break charts that depend on current (albeit surprising) behavior. |
| 58 | + |
| 59 | +## Specification |
| 60 | + |
| 61 | +### Behavioral Parity with .gitignore |
| 62 | + |
| 63 | +Charts v3 `.helmignore` files should support the full `.gitignore` pattern format as documented at [git-scm.com/docs/gitignore][git-gitignore]. |
| 64 | + |
| 65 | +#### Design Intent |
| 66 | + |
| 67 | +`.helmignore` is intended to follow Git's `.gitignore` file pattern matching semantics as documented at the time of this HIP's acceptance. These semantics have been intentionally stable for many years, and familiarity with them is a primary goal of this proposal. |
| 68 | + |
| 69 | +If Helm's `.helmignore` behavior diverges from Git's documented `.gitignore` behavior, that divergence should be treated as a bug and corrected—unless the divergence is explicitly documented in this proposal (see Scope Clarification). |
| 70 | + |
| 71 | +**Future Git changes**: If Git were to introduce incompatible changes to `.gitignore` matching semantics in the future, Helm would evaluate and explicitly decide whether to adopt those changes. Helm does not implicitly inherit future Git behavior changes. |
| 72 | + |
| 73 | +#### Recursive Glob Semantics (`**`) |
| 74 | + |
| 75 | +- `**/foo` — matches `foo` in all directories |
| 76 | +- `foo/**` — matches everything inside `foo/` |
| 77 | +- `a/**/b` — matches `a/b`, `a/x/b`, `a/x/y/b`, etc. |
| 78 | + |
| 79 | +#### Rule Evaluation Order |
| 80 | + |
| 81 | +**Last matching rule wins**. All rules are evaluated; the final matching rule determines whether a path is included or excluded. This enables patterns like: |
| 82 | + |
| 83 | +``` |
| 84 | +# Exclude all top-level entries (files and directories) |
| 85 | +/* |
| 86 | +
|
| 87 | +# But include these |
| 88 | +!Chart.yaml |
| 89 | +!values.yaml |
| 90 | +!templates/ |
| 91 | +``` |
| 92 | + |
| 93 | +#### Negation Behavior |
| 94 | + |
| 95 | +- `!pattern` re-includes files that match, reversing a prior exclusion |
| 96 | +- **Limitation**: Cannot re-include a file if its parent directory was excluded (Git skips listing excluded directories for performance) |
| 97 | + |
| 98 | +#### Scope Clarification |
| 99 | + |
| 100 | +Helm has no concept of staging or committing files. This proposal addresses **file targeting syntax and semantics only**—specifically, which files are included/excluded during chart operations. Git's behavior around staged/committed files does not apply. |
| 101 | + |
| 102 | +#### File Location |
| 103 | + |
| 104 | +Charts v3 `.helmignore` is loaded from the chart root directory only. Unlike `.gitignore`, which supports recursive ignore files in subdirectories within a repository, Helm loads `.helmignore` only from the chart root. Patterns in the root `.helmignore` apply to the entire chart tree. |
| 105 | + |
| 106 | +Recursive `.helmignore` loading within a chart (matching Git's nested `.gitignore` behavior) is a potential future enhancement but is out of scope for this HIP. |
| 107 | + |
| 108 | +### Technical Requirements |
| 109 | + |
| 110 | +1. **Parser/Matcher**: Implement a `.gitignore`-compatible lexer, parser, and matcher supporting all pattern types above. |
| 111 | + |
| 112 | +2. **Last-match semantics**: Evaluate all rules; final matching rule determines outcome. |
| 113 | + |
| 114 | +3. **Path normalization**: Normalize paths to forward slashes for cross-platform consistency. |
| 115 | + |
| 116 | +4. **Directory short-circuit**: Once a directory is excluded, skip traversing its contents (matches Git's performance optimization). |
| 117 | + |
| 118 | +### Integration Points |
| 119 | + |
| 120 | +The new `.gitignore`-parity matching should apply in all existing chart file operations (unchanged scope): |
| 121 | + |
| 122 | +- `helm package` |
| 123 | +- `helm lint` |
| 124 | +- `helm template` |
| 125 | +- `helm install` / `helm upgrade` (for local charts) |
| 126 | +- `.Files.Get` / `.Files.Glob` (file access in templates) |
| 127 | + |
| 128 | +This list is representative; the matching logic applies wherever chart files are loaded or accessed. |
| 129 | + |
| 130 | +### Gating |
| 131 | + |
| 132 | +- **Charts v3 (`apiVersion: v3`)**: New `.gitignore`-parity semantics |
| 133 | +- **Charts v2 (`apiVersion: v2`)**: Existing behavior unchanged |
| 134 | + |
| 135 | +## Backwards Compatibility |
| 136 | + |
| 137 | +### Charts v2 |
| 138 | + |
| 139 | +No behavior change. Existing `.helmignore` files continue to work exactly as before, preserving compatibility for all current charts. |
| 140 | + |
| 141 | +### Charts v3 |
| 142 | + |
| 143 | +Charts opting into v3 accept new `.helmignore` semantics. Potential breaking changes for charts migrating from v2: |
| 144 | + |
| 145 | +| Current Behavior | New Behavior | Migration Impact | |
| 146 | +| ------------------------ | -------------------------- | ------------------------------------------------------------ | |
| 147 | +| First-match evaluation | Last-match evaluation | Patterns relying on early termination may behave differently | |
| 148 | +| Negation inverts match | Negation re-includes | Whitelist patterns will finally work correctly | |
| 149 | +| `**` causes error | `**` works | No breakage (feature addition) | |
| 150 | +| Trailing spaces stripped | Preserved with `\ ` | Unlikely to affect real charts | |
| 151 | +| No escape sequences | `\#`, `\!`, `\ ` supported | No breakage (feature addition) | |
| 152 | + |
| 153 | +## Security Implications |
| 154 | + |
| 155 | +None. This change only affects which local files are considered during chart operations. It does not: |
| 156 | + |
| 157 | +- Introduce new attack surfaces |
| 158 | +- Add remote dependencies |
| 159 | +- Change network behavior |
| 160 | +- Affect chart signing or verification |
| 161 | + |
| 162 | +## How to Teach This |
| 163 | + |
| 164 | +### Documentation Updates |
| 165 | + |
| 166 | +1. **Primary statement**: "Charts v3 `.helmignore` uses identical pattern rules to `.gitignore`." |
| 167 | + |
| 168 | +2. **Link to Git documentation**: Reference [git-scm.com/docs/gitignore][git-gitignore] as background documentation for the pattern semantics that `.helmignore` follows. |
| 169 | + |
| 170 | +3. **Migration guide**: Document behavior differences between v2 and v3. |
| 171 | + |
| 172 | +4. **Examples**: Provide side-by-side comparisons showing: |
| 173 | + - Whitelist patterns (`/*`, `!Chart.yaml`) |
| 174 | + - Recursive globs (`**/test/`) |
| 175 | + - Last-match ordering |
| 176 | + |
| 177 | +5. **Improve v2 and v3 documentation**: Existing `.helmignore` documentation is insufficient, leading to end user confusion, so this should be documented as well. |
| 178 | + |
| 179 | +6. **File location**: Clarify that `.helmignore` must be in the chart root directory. Unlike `.gitignore`, nested `.helmignore` files are not loaded. This existing behavior is unchanged. |
| 180 | + |
| 181 | +### Scaffold Update |
| 182 | + |
| 183 | +Update the default `.helmignore` generated by `helm create` for Charts v3. The current scaffold has been largely unchanged since 2016 ([helm#1028][#1028]). The v3 scaffold should demonstrate gitignore-parity patterns (e.g., `**` globs, whitelist patterns with `!`). |
| 184 | + |
| 185 | +### Release Notes |
| 186 | + |
| 187 | +- Highlight UX improvement for users expecting `.gitignore` behavior |
| 188 | +- Link to this HIP and HIP-0020 for context |
| 189 | +- Emphasize opt-in nature via Charts v3 |
| 190 | + |
| 191 | +### Example Patterns |
| 192 | + |
| 193 | +```gitignore |
| 194 | +# Comments start with # |
| 195 | +
|
| 196 | +# Ignore all dotfiles |
| 197 | +.* |
| 198 | +
|
| 199 | +# But keep .helmignore itself |
| 200 | +!.helmignore |
| 201 | +
|
| 202 | +# Ignore test directories anywhere |
| 203 | +**/test/ |
| 204 | +**/tests/ |
| 205 | +
|
| 206 | +# Ignore IDE files |
| 207 | +.idea/ |
| 208 | +.vscode/ |
| 209 | +*.swp |
| 210 | +*.swo |
| 211 | +*~ |
| 212 | +
|
| 213 | +# Whitelist approach: exclude everything, then include specifics |
| 214 | +/* |
| 215 | +!Chart.yaml |
| 216 | +!values.yaml |
| 217 | +!values.schema.json |
| 218 | +!templates/ |
| 219 | +!charts/ |
| 220 | +!crds/ |
| 221 | +!files/ |
| 222 | +
|
| 223 | +# Directory-only patterns (trailing slash) |
| 224 | +vendor/ |
| 225 | +node_modules/ |
| 226 | +
|
| 227 | +# Anchored to root (leading slash) |
| 228 | +/local-only.yaml |
| 229 | +``` |
| 230 | + |
| 231 | +## Reference Implementation |
| 232 | + |
| 233 | +A proof-of-concept is available at [github.com/scottrigby/helmignore-ref][helmignore-ref], demonstrating that `go-git/go-git/v5/plumbing/format/gitignore` meets all HIP requirements with minimal wrapper code (~76 lines). The repository includes research documentation comparing library options. |
| 234 | + |
| 235 | +Implementation will be gated behind Charts v3 as specified in [HIP-0020][hip-0020]: |
| 236 | + |
| 237 | +1. New matcher package under `internal/chart/v3/ignore/` using go-git's gitignore library |
| 238 | +2. Integration with v3 chart loader |
| 239 | +3. Test suite covering all pattern types from this specification |
| 240 | +4. Migration tests validating v2 charts remain unchanged |
| 241 | +5. Update `helm create` scaffold for v3 charts (once [PR #31592][pr-31592] merges) |
| 242 | + |
| 243 | +## Rejected Ideas |
| 244 | + |
| 245 | +### Shell out to `git check-ignore` |
| 246 | + |
| 247 | +Calling the `git` binary would provide perfect behavioral parity. Rejected because: |
| 248 | + |
| 249 | +- Helm binaries must not have external runtime dependencies |
| 250 | +- Adds complexity for containerized/minimal environments |
| 251 | +- Performance overhead for repeated invocations |
| 252 | + |
| 253 | +### Partial parity (e.g., add `**` but keep first-match) |
| 254 | + |
| 255 | +Rejected because partial fixes would leave confusing inconsistencies. Users expect `.gitignore` behavior; half-measures perpetuate the problem. |
| 256 | + |
| 257 | +### Change behavior for all chart versions |
| 258 | + |
| 259 | +Rejected because it would break existing charts that depend on current (albeit surprising) semantics. Charts v3 provides clean isolation for breaking changes. |
| 260 | + |
| 261 | +## Open Issues |
| 262 | + |
| 263 | +### Subchart .helmignore Handling |
| 264 | + |
| 265 | +Should `.helmignore` files in subchart directories (`charts/*/`) be respected when processing an umbrella chart? |
| 266 | + |
| 267 | +**Current behavior**: Only the parent chart's `.helmignore` is loaded; subchart `.helmignore` files are ignored. |
| 268 | + |
| 269 | +**Open question**: Is this intended behavior, a bug, or an oversight? Arguments exist both ways: |
| 270 | + |
| 271 | +- **For respecting**: Chart authors expect their ignore rules to apply |
| 272 | +- **Against (or: may not matter)**: Parent chart may want control; also, helm-ignored files would already be excluded when the subchart was packaged, so they likely won't be present anyway |
| 273 | + |
| 274 | +**Action**: Investigate history (git commits, meeting notes, previous maintainers) and discuss with community before deciding whether to change this behavior. For now, this HIP preserves existing behavior. |
| 275 | + |
| 276 | +## References |
| 277 | + |
| 278 | +### Helm HIPs |
| 279 | + |
| 280 | +- [HIP-0020: Charts v3 Enablement][hip-0020] |
| 281 | + |
| 282 | +### Git Documentation |
| 283 | + |
| 284 | +- [git-scm.com/docs/gitignore][git-gitignore] |
| 285 | + |
| 286 | +### Libraries |
| 287 | + |
| 288 | +- [go-git/go-git gitignore][go-git-gitignore] — Recommended implementation library |
| 289 | + |
| 290 | +### Issues Directly Addressed by This HIP |
| 291 | + |
| 292 | +These issues are resolved by implementing `.gitignore` pattern matching parity: |
| 293 | + |
| 294 | +- [#8688][#8688] — Negation semantics are inverted; `!pattern` ignores non-matches instead of re-including matches. |
| 295 | +- [#3622][#3622] — Whitelist patterns (`/*` then `!Chart.yaml`) fail with "chart metadata missing" due to broken negation logic. |
| 296 | +- [#1776][#1776] — Pattern `.*` incorrectly matched the current directory, breaking charts. Shows user expectation of gitignore behavior. |
| 297 | +- [#12592][#12592] — Patterns like `charts/*/README.md` don't work; `**` glob support and improved matching would help. |
| 298 | +- [#12265][#12265] (PR) — Attempted partial fix for negation, stalled 2 years as a breaking change. This HIP provides the proper scope via Charts v3. |
| 299 | + |
| 300 | +### Issues Out of Scope (Context Only) |
| 301 | + |
| 302 | +These issues concern _when_ `.helmignore` applies, not pattern syntax. Included for context but not addressed by this HIP: |
| 303 | + |
| 304 | +- [#6075][#6075] — Users expected ignore to affect only `helm package`, but it affects all commands. This is intentional; HIP clarifies but doesn't change this. |
| 305 | +- [#3050][#3050] — Files in `.helmignore` are inaccessible to `.Files.Get`. Architectural issue about ignore scope, not pattern matching. |
| 306 | +- [#10764][#10764] — README files consume release storage; users want "exclude from release but include in package." Requires new scope distinction. |
| 307 | +- [#9436][#9436] — `.helmignore` doesn't exclude itself from packages. This is intentional becuase `.helmignore` is used for more than packaging, so various workflows could be disrupted (see https://github.com/helm/helm/issues/9436#issuecomment-792795063). Documentation update via this HIP should clarify this. |
| 308 | + |
| 309 | +### Feature Requests (Out of Scope) |
| 310 | + |
| 311 | +- [#1674][#1674], [#5675][#5675] — Global `~/.helmignore` support. Already partially implemented; could adopt gitignore parity separately. |
| 312 | + |
| 313 | +### Documentation Issues |
| 314 | + |
| 315 | +These show user confusion that better docs (and gitignore parity) would reduce: |
| 316 | + |
| 317 | +- [#4638][#4638] — Requested more `.helmignore` documentation; shows need for clearer syntax docs. |
| 318 | +- [helm-www#1171][helm-www#1171] — Docs say ignore affects "packaging" only, but it affects all operations. Needs correction regardless of HIP. |
| 319 | +- [helm-www#1312][helm-www#1312] — Docs don't specify `.helmignore` must be in chart root, not working directory. |
| 320 | +- [helm-www#1460][helm-www#1460] — Example pattern `/temp*` broke charts by matching `templates/`. Shows need for better examples. |
| 321 | + |
| 322 | +### Other Related PRs |
| 323 | + |
| 324 | +- [#13293][#13293] — Fix for broken symlinks in `.helmignore`. Tangentially related to ignore handling. |
| 325 | + |
| 326 | +<!-- Link definitions --> |
| 327 | + |
| 328 | +[hip-0020]: https://github.com/helm/community/blob/main/hips/hip-0020.md |
| 329 | +[helmignore-ref]: https://github.com/scottrigby/helmignore-ref |
| 330 | +[pr-31592]: https://github.com/helm/helm/pull/31592 |
| 331 | +[git-gitignore]: https://git-scm.com/docs/gitignore |
| 332 | +[go-git-gitignore]: https://github.com/go-git/go-git/tree/main/plumbing/format/gitignore |
| 333 | +[#8688]: https://github.com/helm/helm/issues/8688 |
| 334 | +[#3622]: https://github.com/helm/helm/issues/3622 |
| 335 | +[#1776]: https://github.com/helm/helm/issues/1776 |
| 336 | +[#12592]: https://github.com/helm/helm/issues/12592 |
| 337 | +[#6075]: https://github.com/helm/helm/issues/6075 |
| 338 | +[#3050]: https://github.com/helm/helm/issues/3050 |
| 339 | +[#9436]: https://github.com/helm/helm/issues/9436 |
| 340 | +[#10764]: https://github.com/helm/helm/issues/10764 |
| 341 | +[#1674]: https://github.com/helm/helm/issues/1674 |
| 342 | +[#5675]: https://github.com/helm/helm/issues/5675 |
| 343 | +[#1028]: https://github.com/helm/helm/pull/1028 |
| 344 | +[#4638]: https://github.com/helm/helm/issues/4638 |
| 345 | +[#12265]: https://github.com/helm/helm/pull/12265 |
| 346 | +[helm-www#1171]: https://github.com/helm/helm-www/issues/1171 |
| 347 | +[helm-www#1312]: https://github.com/helm/helm-www/issues/1312 |
| 348 | +[helm-www#1460]: https://github.com/helm/helm-www/issues/1460 |
| 349 | +[#13293]: https://github.com/helm/helm/pull/13293 |
0 commit comments