961 Commits
Author SHA1 Message Date
Daniel Pinto 226c16b88e fix: drop leading dot from release condition error message (#2812)
- Build the reported key path from the keys walked so far, so a missing value reads
  `'fooo'` or `'rnd42.unknown'` instead of `'.fooo'`; the "is not a map" message uses it too.
- Which condition values count as enabled is unchanged.
- `TestConditionEnabled` now asserts the exact error text.

Signed-off-by: Cayan <1619617+Cayan@users.noreply.github.com>
2026-09-30 09:00:23 +08:00
pznamenskyandyxxhero 5ed585e223 add --track-logs-interval flag (#2807)
* add --track-logs-interval flag

Signed-off-by: pznamensky <kompastver@gmail.com>

* address review nits: logsInterval plumbing and test coverage

- kubedog: move defaultLogsInterval next to TrackOptions in options.go,
  where the field it defaults lives
- kubedog: pass logsInterval through newProgressPrinter instead of a
  post-construction field assignment; zero falls back to the default
- kubedog: cover WithLogsInterval and the LogsInterval default in
  tracker tests
- config: document that defaultTrackLogsInterval is shared by sync and
  apply and mirrors kubedog's default
- config: add a negative-interval validation test case

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: pznamensky <kompastver@gmail.com>
Signed-off-by: yxxhero <aiopsclub@163.com>
Co-authored-by: yxxhero <aiopsclub@163.com>
2026-09-30 08:30:21 +08:00
Johannes Schmidtandyxxhero a219b5dbd2 fix: remove partially extracted chart when helm pull fails (#2805)
extracts in place, so a failed fetch can leave a partial chart in the cache. `forcedDownloadChart` returns the error but never removes the partial directory, and `findChartDirectory` accepts any directory containing a Chart.yaml, so the next run adopts the leftover as a valid cache entry and renders zero manifests without error. Remove the partial download while still holding the exclusive chart lock so the failure stays a failure.

Co-authored-by: yxxhero <aiopsclub@163.com>
Signed-off-by: Johannes Schmidt <jschmidt@canarytechnologies.com>
Signed-off-by: yxxhero <aiopsclub@163.com>
2026-09-23 13:14:13 +08:00
Tsukoyachiandyxxhero ca58090af4 feat: add per-release continue-on-error support (#2804)
* feat: add per-release continue-on-error support

Work in progress for discussion #2799.

This introduces the initial release-level configuration needed to
continue processing independent releases after a deployment failure.
The DAG execution and failure propagation behavior are still being
implemented.

Refs #2799

Signed-off-by: Axel Delille <Axel.delille31@gmail.com>

* fix: address review findings for continueOnError

Review fixes for the per-release continueOnError feature:

1. Regenerate values-file ID hashes in pkg/state/temp_test.go — adding
   ContinueOnError to ReleaseSpec shifts generateValuesID hashes, which
   broke TestGenerateID (the test file notes these must be regenerated
   whenever ReleaseSpec changes).

2. Remove the dead skippedErrors variable in withBatches and instead log
   each skipped release with logger.Warnf at decision time, so users see
   why a release never ran instead of only learning from the final error
   list.

3. Aggregate all errors from a state file instead of returning only
   errs[0] in visitStatesWithContext/processStateFileParallel. With
   continueOnError, multiple releases can fail or be skipped in one run;
   reporting only the first error hid the skip errors (and other
   failures), contradicting the feature's contract. Single-error
   rendering is unchanged.

4. Gate tolerated errors on ReleaseErrorCodeFailure so that non-failure
   release errors (e.g. helm-diff's "changes detected" exit code 2 can
   never enable continuation or block dependents.

5. Report skipped-release messages with the dependency's plain release
   name instead of its kubeContext/namespace-qualified needs id
   (e.g. dependency database instead of default/default/database),
   matching the documented message format.

6. Restructure the withBatches helpers into guard-clause style
   (filterBlockedReleases, toleratesBatchErrors) and drop the test-only
   logger nil-guards in favor of a nop logger in tests.

7. Add end-to-end coverage through App.Sync with the exectest fake helm:
   independent releases continue after a tolerated failure, dependents
   are skipped with an explicit error, the exit code stays non-zero, and
   fail-fast remains the default without continueOnError. Also cover the
   non-failure error code case at the withBatches level.

8. Use new(true) instead of a boolPtr helper (CI-enforced check-modernize)
   and document the failure-handling interaction in docs/releases.md.

Signed-off-by: yxxhero <aiopsclub@163.com>
EOF
)
Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: Axel Delille <Axel.delille31@gmail.com>
Signed-off-by: yxxhero <aiopsclub@163.com>
Co-authored-by: yxxhero <aiopsclub@163.com>
2026-09-21 07:48:50 +08:00
yxxheroandClaude ad81e4231e build: update Go to 1.27.1 and modernize the codebase (#2798)
Toolchain and CI:
- Bump go directive from 1.26.8 to 1.27.1 (go.mod)
- Use golang:1.27-alpine builder images in all Dockerfiles
- Bump golangci-lint to v2.13.2 (first release with go1.27 support)
- Add CI gate: `go fix -diff` fails when outdated Go patterns are
  detected (locally: `make check-modernize`)

Note: darwin binaries now require macOS 13 or later.

Lint fixes required by golangci-lint v2.13.2:
- goconst: ignore tests (all 436 findings were test-only; goconst
  got stricter since v2.12 and this option was added for it)
- openai.go: keep deprecated MaxTokens deliberately with a nolint
  rationale (max_tokens is the only form universally supported by
  OpenAI-compatible backends like One-API, LiteLLM, Ollama shim)
- state.go: drop always-nil flags param from appendChartVersionFlags
  (renamed to chartVersionFlags, unparam)

Modernization (go fix ./..., 62 files):
- interface{} -> any, maps.Copy, strings.SplitSeq, range-over-int,
  builtin min/max, slices.Contains/ContainsFunc/Sort, WaitGroup.Go,
  reflect.Type.Fields(), new(expr)
- exit_error.go: strings.Builder + fmt.Fprintf instead of string
  concatenation and WriteString(fmt.Sprintf(...)) (QF1012)
- chart_dependency.go: strings.CutLast for OCI dependency helpers

Signed-off-by: yxxhero <aiopsclub@163.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-09-16 13:29:02 +08:00
vulragrag-starandyxxhero d4a4135f9b fix: cancel kubedog-tracked helm subprocesses on SIGINT/SIGTERM (#2791)
* fix: cancel kubedog-tracked helm subprocesses on SIGINT/SIGTERM

Kubedog tracking rooted helm and tracker contexts at Background (via
traceOnlyContext), so App.Cancel never reached those subprocesses and
Ctrl+C blocked in CleanWaitGroup until helm exited on its own.

Thread the app cancel context into HelmState (SetCancelContext) and use
it for the three kubedog tracking call sites, keeping the per-release
WithCancel safety valve. Hooks stay on the non-canceling trace bridge.

Fixes #2770

Signed-off-by: Jason Wang <vulragrag@gmail.com>

* docs/test: align cancel-context docs with #2791 and pin the kubedog wiring

- traceOnlyContext comment no longer lists kubedog tracking among the
  detached paths; only hooks (#2771) remain, with a pointer to #2791.
- docs/proposals/otel-tracing.md sections 4.3/4.4/7/9 now mark the
  kubedog cancellation gap as fixed by #2791 (SetCancelContext), so the
  design doc stops misdescribing main.
- TestBufferHelmOutputRootsAtCancelContext pins the actual #2770 wiring:
  the context bufferHelmOutput hands to ContextSwapper.WithContext must
  become Done when the app cancel context is canceled, and stay
  non-cancelable when unset.

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: Jason Wang <vulragrag@gmail.com>
Signed-off-by: yxxhero <aiopsclub@163.com>
Co-authored-by: yxxhero <aiopsclub@163.com>
2026-09-15 07:35:09 +08:00
jimmyRandyxxhero bc0c103d79 feat: add HELMFILE_DISABLE_VALS env var to skip vals processing (#2471)
Signed-off-by: Jim Robinson <1643772+jimmyR@users.noreply.github.com>
Signed-off-by: yxxhero <aiopsclub@163.com>
Co-authored-by: yxxhero <aiopsclub@163.com>
2026-09-13 15:40:12 +08:00
hoppla20andClaude Opus 5 daa43e1081 feat(remote): support wildcards in remote values/secrets file selectors (#2787)
Extend git-getter style remote references (git::, s3::, https://, ...) used
in release and environment values/secrets to support glob patterns in the
file selector, e.g.:

  git::https://github.com/org/repo.git@config/*.yaml?ref=main

Remote.Fetch already downloads the whole repository/directory and joins the
"@<file>" selector onto it verbatim, so a wildcard selector already survives
untouched; the only missing piece was that Storage.resolveFile checked the
result with FileExistsAt instead of expanding it as a glob.

- pkg/remote/remote.go: add HasGlobPattern to detect a wildcard in the file
  selector (checking only the selector, not the raw URL, so "?ref=main" and
  IPv6/placeholder brackets elsewhere are not mistaken for wildcards). Reject
  wildcards in Fetch for getter shapes that can never expand one: plain
  http(s)/s3 (single object), non-archive forced s3:: (single object), and
  any getter used without an explicit "@" selector (Dir/File cannot be
  reliably split from the pattern otherwise).
- pkg/state/storage.go: resolveFile now globs the fetched cache path with the
  same st.fs.Glob/sort.Strings used for local values-file globs when the
  selector is a pattern, filtering out directory matches. A literal, existing
  path is still resolved directly. Fixed an existing err-shadowing hazard in
  the same code path while restructuring it.
- docs/environments.md: document the new wildcard support, its syntax
  (filepath.Match, no recursive **), and its getter/selector requirements.
- Tests: new cases in pkg/remote/remote_test.go (glob detection, Fetch
  wildcard expansion and cache-key sharing, rejected getter shapes) and
  pkg/state/storage_test.go (a real end-to-end wildcard fetch against a
  pinned upstream tag, plus a hermetic fan-out/sorting/missing-file test with
  no network access).

Release values/secrets keep their existing "glob patterns ... not supported
yet" restriction for multi-file matches (pkg/state/state.go), unchanged by
this commit and applying equally to local and remote globs. helmfiles: entries
are out of scope.

Signed-off-by: Vincent Cui <privat@vincentcui.de>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 09:16:26 +08:00
yxxhero 6e7aafd5e5 build(deps): bump helm-diff to v3.15.13 (#2788)
* build(deps): bump helm-diff to v3.15.13

Update helm-diff plugin version from v3.15.12 to v3.15.13 across
Dockerfiles, recommended version constant, CI matrix, and
integration test default.

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix(ci): add golden files for helm-diff >= 3.15.13

helm-diff v3.15.13 omits null/empty labels and annotations keys from
diffs (databus23/helm-diff#1068), which broke the suppress-output-line-
regex integration test: the ValidatingWebhookConfiguration manifest no
longer renders a bare 'annotations:' key under metadata.

Add new golden files suffixed -after-helm-diff-3.15.13 (helm3, helm4,
live variants) and select them via version_ge in the test case, keeping
the existing 3.11.0 and pre-3.11.0 goldens for older plugin versions.

Signed-off-by: yxxhero <aiopsclub@163.com>

* refactor(test): drop legacy helm-diff golden files

CI only exercises the recommended helm-diff version, so replace the
version-suffixed golden files (-after-helm-diff-3.11.0,
-after-helm-diff-3.15.13) with a single set of base golden files
(diff, diff-live, diff-helm4, diff-live-helm4) containing the
v3.15.13 output, and remove the now-dead version gating.

Also drop the 'install semver' CI step, whose only consumer was the
removed gating.

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix(test): remove helm-diff version gating from golden file selection

The previous commit deleted the version-suffixed golden files but the
selection gating was accidentally left behind, making the test reference
non-existent files for helm-diff >= 3.15.13.

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-09-11 17:01:59 +08:00
yxxhero 9d1a953ec2 chore: bump Helm to v3.22.0 and v4.3.0 (#2786)
- Update CI test matrix from v3.21.4/v4.2.4 to v3.22.0/v4.3.0
- Update Helm v4.3.0 SHA256 checksums in Dockerfiles
- Update go.mod dependencies (helm.sh/helm/v3 v3.22.0, helm.sh/helm/v4 v4.3.0)
- Update HelmRecommendedVersion to v4.3.0

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-09-11 12:14:32 +08:00
FloandClaude Fable 5.1 5a241f68dc fix(list): avoid deadlock in helmfile list with more than 100 states (#2773)
fix(list): collect releases under a mutex to avoid deadlock with more than 100 states

ListReleases gathered per-state results through a channel with a fixed
buffer of 100 that was drained only after ForEachState had visited every
state. The 101st state carrying releases therefore blocked on the send
forever: `helmfile list` hung with no output and ignored SIGTERM.

Append to the result slice under a mutex instead. ForEachState may run the
callback concurrently, and the existing sort keeps the output order
deterministic, so nothing else changes.

Adds TestListWithManyStates (101 helmfile.d states, fails fast on a hang).

Resolves #2772

Signed-off-by: Florian Müller <7556827+max06@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 07:53:07 +08:00
yxxhero 16259008d5 feat: opt-in OpenTelemetry tracing and metrics (experimental) (#2769)
* feat(telemetry): add opt-in OpenTelemetry tracing (PR 1: lifecycle + root span)

Implements the first increment of docs/proposals/otel-tracing.md (#2767):

- pkg/telemetry: SDK setup from standard OTEL_* env vars (autoexport for
  exporter selection, env-driven sampler/propagators, OTEL_SDK_DISABLED),
  command-span lifecycle, no-op-by-default accessors
- --otel-tracing flag / HELMFILE_OTEL_TRACING env switch
- root span "helmfile <command>" with file/environment/selectors/exit_code
  attributes; TRACEPARENT-based remote-parent extraction for CI correlation
- shutdown flush on both normal-exit and signal paths (nil-safe, 5s bound)
- app.New derives its context from telemetry.CommandContext()
  (Background-identical when tracing is disabled)
- docs: otel.md user guide, experimental-features entry, design proposal
- tests: hermetic unit tests, app context-contract pinning, flag registration

Telemetry problems never fail a run: exporter misconfiguration and export
errors degrade to disabled with a warning. When disabled, behavior and
performance are identical to before (no-op tracer, no goroutines, no
network).

Refs: #2767, #2758
Signed-off-by: yxxhero <aiopsclub@163.com>

* feat(telemetry): trace every external process + trace-context bridges (PR 2)

Implements the second increment of docs/proposals/otel-tracing.md (#2767):

- pkg/helmexec/span.go: one span per external process started by helmfile
  (helm invocations, hooks, plugin execs) at the ShellRunner choke point —
  helm.exec (with helm.subcommand) vs os.exec, with redacted exec.args,
  exec.exit_code, and error status on failure
- pkg/helmexec/redact.go: shared argument redaction with two profiles;
  legacy is byte-identical to the historical exit-error behavior (existing
  goldens unchanged), strict (spans) additionally covers --set=k=v and
  credential flags; exit_error.go now uses the shared helper
- orphan-trace bridges with bit-identical cancellation semantics
  (context.WithoutCancel of the command context): both kubedog call sites
  (state.go) and hook execution (event.Bus gains an optional Ctx consumed
  by its default runner; state.go sets it, nil falls back to TODO as before)
- OTLP end-to-end test (in-process httptest receiver, no external
  collector): span export, error status/exit code, redaction, and
  parent-linkage to the command span
- docs/otel.md updated to the now-traced surface

Verified end-to-end with the console exporter: helmfile template on a
local chart yields the command span plus helm.exec spans for helm
version/dependency/template, all nested under it.

Refs: #2767
Signed-off-by: yxxhero <aiopsclub@163.com>

* feat(telemetry): state-loading and hook spans (PR 3a)

Implements the third increment of docs/proposals/otel-tracing.md (#2767):

- helmfile.discover_states around findDesiredStateFiles and helmfile.load
  around loadDesiredStateFromYamlWithBaseDir; both cover all callers
  (incl. nested helmfiles) with no signature changes
- helmfile.render / helmfile.parse children per document part, parented
  through a traceCtx field on the unexported desiredStateLoader struct
  (set once at its single construction site)
- helmfile.hook span per hook execution: Trigger's per-hook body extracted
  into runHook (readability win on its own), the hook's subprocess span
  nests under it via a per-hook ctx-swapped ShellRunner clone
  (cancellation unchanged — Bus.Ctx never carries cancellation by contract)
- pkg/telemetry/otlptest: shared in-process OTLP/HTTP receiver harness,
  now used by helmexec, event, and app span tests
- golden span-tree test at the app layer (root -> discover -> load ->
  render/parse, via the exectest fake helm) and a hook-span nesting test
- nil-ctx guard for App literals built directly by tests (App.spanParentCtx)

Verified end-to-end with the console exporter: a template run over a
gotmpl state file with a prepare hook yields the full tree with the hook's
os.exec nested under helmfile.hook.

Refs: #2767
Signed-off-by: yxxhero <aiopsclub@163.com>

* feat(telemetry): per-release spans nested under the load span (PR 3b)

Implements the per-release increment of docs/proposals/otel-tracing.md
(#2767) — spans nest command -> load -> release -> helm exec:

- helmexec.HelmContext gains an optional Ctx carrying the per-release span
  context; the execer's new execWithContext funnel consumes it via a
  per-call runner clone (runnerWithCtx) so the shared, cached execer is
  never mutated across concurrent workers. The seven Interface methods
  that take a HelmContext (Sync/Diff/ReleaseStatus/List/DecryptSecret/
  Delete/Test) route through it; nil Ctx behaves exactly as before.
  exec() lost its always-nil override parameter on the way (unparam).
- pkg/state/span.go: SetTraceContext + startReleaseSpan/endReleaseSpan
  helpers (release/namespace/chart/labels attributes, sorted for stable
  output); a typed-nil guard (releaseErrAsError) avoids the classic
  nil-pointer-in-interface trap on *ReleaseError.
- release spans in the worker loops: SyncReleases, DiffReleases,
  DeleteReleasesForSync, PrepareCharts, and iterateOnReleases (status/
  delete/test via a new verb parameter); their HelmContext is stamped with
  the release span context where one is built.
- pkg/app sets st.SetTraceContext(loadCtx) right after loading a state
  file, rooting all per-release spans under helmfile.load.
- bridged one more detached tracking call found on the way
  (trackReleaseIfEnabled's context.Background in the sync worker).
- golden test: release span present, nested under load, correct
  attributes; unit test for the runnerWithCtx clone semantics.

Verified with the console exporter: helmfile template yields
release.prepare(demo) under load with full attributes.

Refs: #2767
Signed-off-by: yxxhero <aiopsclub@163.com>

* feat(telemetry): nest status/delete/test execs under their release spans

Completes the per-release exec nesting for the iterateOnReleases-based
loops (docs/proposals/otel-tracing.md §4.4 phase 2): the do closures now
receive the release span context and stamp it into their HelmContext, so
helm status/delete/test subprocess spans nest under
helmfile.release.<verb> like sync/diff already did.

- scatterGatherReleases/iterateOnReleases/doWithReleaseSpan: do gains a
  context parameter (the release span context)
- ReleaseStatuses/DeleteReleases/TestReleases closures stamp
  HelmContext.Ctx from it
- integration test with a real execer (version-probe shim binary): the
  release's status subprocess nests under helmfile.release.status, same
  trace, with helm.subcommand=status

This also makes the otel.md claim ("upgrade, diff, delete, status, test
nested under the release span") fully accurate.

Refs: #2767
Signed-off-by: yxxhero <aiopsclub@163.com>

* feat(telemetry): OTel metrics — helm exec duration and release results (PR 4)

Implements the metrics increment of docs/proposals/otel-tracing.md
(#2767) on the same provider, switch, and resource as traces:

- pkg/telemetry/metrics.go: helmfile.helm.exec.duration histogram
  (subcommand, success) and helmfile.release.count counter (verb,
  result). Instruments come from the otel global meter, so recording at
  call sites is branch-free no-op when telemetry is disabled.
- Setup builds the resource once and installs both providers; reader
  selection delegates to autoexport (OTEL_METRICS_EXPORTER: otlp |
  console | prometheus | none), the OTLP reader's interval honors
  OTEL_METRIC_EXPORT_INTERVAL (read by the SDK). Shutdown flushes both
  providers (errors.Join). StartCommandSpan now carries the meter
  provider across state transitions (fixes a nil-shutdown panic).
- helmexec: finishExecSpan records exec duration for helm binaries;
  state: endReleaseSpan counts release outcomes for sync/diff/delete/
  status/test/prepare (diff counted as success when no hard error).
- otlptest: recorder routes by OTLP path (/v1/traces vs /v1/metrics)
  and decodes metrics; new FindMetric helper.
- tests: metrics recorded as no-op when disabled, provider enabled with
  the none exporter, degradation on an invalid metrics exporter, and an
  integration assertion (status exec duration datapoint + one successful
  release.count) in the shim-based state test.

Verified with the console exporter: helmfile template emits
helmfile.helm.exec.duration per subcommand (version/dependency/
template) and helmfile.release.count{verb=prepare,result=success}=1.

Refs: #2767
Signed-off-by: yxxhero <aiopsclub@163.com>

* docs: complete OTel documentation coverage

- docs/cli.md: --otel-tracing in the CLI reference help block (verbatim
  from the cobra output)
- CHANGELOG.md: [Unreleased] Added entry for tracing + metrics
- docs/index.md: Observability highlight linking docs/otel.md
- docs/proposals/otel-tracing.md: add OTEL_METRICS_EXPORTER /
  OTEL_METRIC_EXPORT_INTERVAL rows to the env-var table and note the
  periodic reader + bounded metric cardinality in §7

Refs: #2767
Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: drop unused id parameter from parsePart (unparam)

The id parameter was never used inside the span wrapper; the caller's id
variable is still used for the render calls and error messages.

Refs: #2769
Signed-off-by: yxxhero <aiopsclub@163.com>

* fix(telemetry): address review — redaction gaps, kubedog valve, phantom metrics

Addresses all Copilot review comments on #2769:

Security (span payloads):
- exec.args: positional arguments are additionally passed through
  helmexec.RedactedURL, so credentials embedded in chart/repository URLs
  (AddRepo, RegistryLogin, OCI refs) are masked exactly like log output
- release spans sanitize helmfile.chart the same way
- error statuses no longer embed raw errors (which contain rendered
  commands, arguments, and subprocess output): the command span, release
  spans, hook spans, and exec spans now use generic descriptions; the
  concrete exit code remains an attribute, and RecordError on the root
  span is dropped

Correctness:
- kubedog safety valve restored: execWithContext now attaches the
  per-release span into the runner's own context instead of replacing it,
  so trackHandle.Cancel() can interrupt a wedged helm again and app
  cancellation semantics stay exactly as before the PR
- diff release spans/metrics: real failures are recorded (exit code 2
  "changes detected" still counts as success); previously every diff was
  exported as successful
- skipped releases no longer emit phantom spans and inflate
  helmfile.release.count: iterateOnReleases callers pass a skip predicate
  (skipUndesired for status/test; delete deletes undesired releases and
  passes nil)
- Setup shuts down the already-constructed tracer provider (bounded) when
  the metrics provider fails, instead of abandoning its batch goroutine

Tests: URL redaction cases (masked/untouched), spanAttachedContext
preserves the runner cancellation chain while attaching the caller's
span, skipUndesired, and a failing-hook span asserting the generic
message.

Refs: #2769
Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: lint — restore nolint placement and avoid nil context literal

- the skipUndesired insertion had displaced the // nolint: unparam
  directive off iterateOnReleases (helm param is intentionally unused
  there); also fixes a skipDesired/skipUndesired comment typo
- use a typed nil in TestSpanAttachedContext (staticcheck SA1012)

Refs: #2769
Signed-off-by: yxxhero <aiopsclub@163.com>

* fix(telemetry): address review round 2 — remote-ref redaction, wrapper helm binaries, hook release attribution

Addresses all 6 new review comments on #2769:

Security (remote references):
- new helmexec.RedactedRef sanitizes go-getter style references for
  telemetry: forced-form prefixes (git::, s3::) preserved, whole URL
  userinfo masked (usernames carry tokens too), credential-bearing query
  parameters masked using pkg/remote's heuristic (token/password/secret/
  key/signature). Applied to helmfile.file (command span), helmfile.path
  (discover_states), helmfile.chart (release spans), and exec.args —
  log-time RedactedURL is untouched so log output is unchanged

Correctness:
- wrapper helm binaries (--helm-binary custom names) are now classified
  as helm operations by an explicit context marker stamped in the execer
  funnel, instead of the executable-basename heuristic; the same
  classification gates helmfile.helm.exec.duration, so the metric no
  longer misses wrapper invocations (classifyExec)
- release-scoped hooks (presync/postsync/preuninstall/postuninstall/
  cleanup in the sync/delete/diff workers) now attach their helmfile.hook
  spans to the active helmfile.release.* span via a variadic parent on
  the trigger functions; global hooks keep the command context and all
  29 existing call sites compile unchanged; hook cancellation stays
  detached (WithoutCancel) as before
- signal-terminated runs (Shutdown with exitCode 130/143 and nil error)
  now mark the command span with error status, consistent with their
  nonzero exit code

Tests: RedactedRef table (forced forms, userinfo, s3/token query params,
untouched cases), classifyExec marker case, hookTraceContext parent
attribution + non-cancellability + fallback.

Refs: #2769
Signed-off-by: yxxhero <aiopsclub@163.com>

* fix(telemetry): address review round 3 — redaction corner cases, value runners

Addresses 5 of the 6 new review comments on #2769 (the sixth — an
unused strings import in exit_error.go — is a false positive: Indent
still uses strings.Split/Builder and the package compiles):

- RedactArgs read the previous token from the progressively redacted
  output, so {--set, --set-string, secret} leaked the secret
  (the masked value hid the following flag). Read the previous token
  from the original input, restoring the legacy contract for adjacent
  secret flags
- RedactedRef fails closed for malformed references: URL-like refs with
  invalid percent escapes export a fully redacted value, and an
  unparseable query is dropped entirely instead of exported verbatim
- ShellRunner has value receivers, so a ShellRunner VALUE satisfies the
  Runner API; the helm marker stamping and the per-release span
  attachment now handle both value and pointer forms (matching
  WithContext), so value-runner callers keep release nesting and the
  helm.exec classification/metric

Regression tests: adjacent secret flags (legacy + strict), malformed
URL-like ref, malformed query, value-runner marker + span attachment.

Refs: #2769
Signed-off-by: yxxhero <aiopsclub@163.com>

* fix(telemetry): stamp the helm marker on the stdin funnel too

execStdIn (registry login, repo add) called the runner directly, so
wrapper --helm-binary names were misclassified as os.exec and omitted
from helmfile.helm.exec.duration on that path. The marking now goes
through a shared markHelmRunner helper (value and pointer ShellRunner
forms) used by both execution funnels.

Refs: #2769
Signed-off-by: yxxhero <aiopsclub@163.com>

* fix(telemetry): redact helm's --kube-token in strict profile

Helm's global --kube-token carries a bearer token; both the
two-argument and inline forms are now masked in span exec.args
(legacy exit-error output is untouched, matching its historical
behavior).

Refs: #2769
Signed-off-by: yxxhero <aiopsclub@163.com>

* fix(telemetry): OTel metrics best-practice alignment

- helmfile.helm.exec.duration now declares explicit bucket boundaries
  tuned for seconds-scale helm invocations (5ms…600s); the SDK defaults
  are millisecond-oriented and lumped every sub-5s invocation — the
  common case — into the first bucket, defeating the histogram
- instruments are re-created under the installed provider with the
  instrumentation scope version stamped (Setup-time, race-free)
- helmfile.release.count declares the {release} curly-annotation unit
  per the metrics naming conventions

Tested end-to-end via the OTLP integration test: exported bounds are
the tuned set, units are asserted, and the scope carries the version.

Refs: #2769
Signed-off-by: yxxhero <aiopsclub@163.com>

* feat(telemetry): per-release duration metrics behind an opt-in switch

New helmfile.release.duration histogram (seconds, same tuned buckets)
with bounded dimensions by default (verb, result). Setting
HELMFILE_OTEL_METRICS_PER_RELEASE=true adds helmfile.release and
helmfile.namespace, answering "which release is slow" from dashboards:

- well-suited to bounded CI runs; long-lived centralized collection
  needs a backend capacity/TTL story (documented in docs/otel.md)
- per-release timing remains available in traces without the flag
- env read per call (release operations are low-frequency, and tests
  toggle it)

endReleaseSpan now takes the release and the operation start time; the
five worker-loop call sites pass them (doWithReleaseSpan, SyncReleases,
DeleteReleasesForSync, PrepareCharts, DiffReleases).

Verified end-to-end with the console exporter (default dims vs
per-release) and OTLP integration tests pinning both modes.

Refs: #2767, #2769
Signed-off-by: yxxhero <aiopsclub@163.com>

* refactor(telemetry): maintainability pass over the runner/metric plumbing

- StartCommandSpan copies the tracingState struct instead of enumerating
  fields by hand — that pattern dropped the meter provider once already
- the two value/pointer ShellRunner switches (helm marker, span
  attachment) are unified into one withRunnerCtx helper; the duplication
  caused two review rounds of value-form misses
- classifyExec derives the helm classification from the span name
  (helmExecSpanName constant) instead of returning a third parallel bool
- metrics: shared outcomeAttrs for the verb/result dimensions, and the
  bucket slice renamed to durationBuckets with a comment covering both
  histograms that use it

No behavior change; full -race suite green, lint clean.

Refs: #2769
Signed-off-by: yxxhero <aiopsclub@163.com>

* refactor(telemetry): consolidate test env lists, trace bridges, and hook prep; sync the design doc

Maintainability:
- HermeticEnvVars is now exported from pkg/telemetry (the owner of the
  env surface) and used by both telemetry tests and otlptest — the two
  copies had already drifted once (HELMFILE_OTEL_METRICS_PER_RELEASE
  needed updating in both)
- kubedogTraceContext and hookTraceContext were the same concept written
  twice; unified into traceOnlyContext(parent...) in span.go

Readability:
- runHook's nested kubectl rewrite extracted into prepareKubectlHook
  with guard-clause structure

Accuracy (docs ↔ code, drifted over five review rounds):
- §4.4 now describes the implemented mechanism: the release span is
  INJECTED into the runner's own context (preserving the kubedog safety
  valve) rather than the runner context being replaced, and helm
  classification is marker-based for wrapper binaries
- §5 exec span rows list the actual attributes incl. URL/query masking
- §6 strict profile documents RedactedRef, --kube-token, and the
  adjacent-token guarantee

No behavior change; full -race suite green, lint clean.

Refs: #2769
Signed-off-by: yxxhero <aiopsclub@163.com>

* refactor(telemetry): drop the dead noop state, relocate skipUndesired, sync user-facing accuracy

- tracingState.noop was dead weight in the enabled state and a
  copy-surface in every transition; a single package-level
  noopTracerProvider now backs Tracer while disabled
- skipUndesired moved next to doWithReleaseSpan in span.go, its only
  conceptual home (span/metric suppression, not run plumbing)
- accuracy: the package doc, --otel-tracing flag help,
  experimental-features entry, and CHANGELOG now all say tracing AND
  metrics and list the third instrument (helmfile.release.duration with
  the HELMFILE_OTEL_METRICS_PER_RELEASE opt-in) — these had drifted
  when the metric was added; the PR description's metric table is
  updated to match as well

No behavior change; full -race suite green (except the pre-existing
network-dependent TestStorage_resolveFile flake), lint clean.

Refs: #2769
Signed-off-by: yxxhero <aiopsclub@163.com>

* refactor(telemetry): flatten Setup, name the prefix bound, dedupe test fake; fix instrument-count drift

Readability/maintainability:
- Setup drops from 56 to 39 lines: provider construction (including the
  shutdown-tracer-on-meter-failure recovery) moves to newProviders in
  exporter.go next to the constructors it composes
- refredact's magic 16 becomes maxForcedFormPrefix with a comment
- span_test's hand-rolled fakeRunner removed in favor of the existing
  mockRunner (same package)

Accuracy:
- "Two instruments" wording survived in docs/otel.md and the design
  proposal §7 after helmfile.release.duration was added; both now say
  three and mention the per-release opt-in

No behavior change; full -race suite green (except the pre-existing
network flake), lint clean.

Refs: #2769
Signed-off-by: yxxhero <aiopsclub@163.com>

* refactor(telemetry): co-locate span machinery, drop a dead export, fix docs nits

- the span plumbing helpers (markHelmExec, withRunnerCtx,
  markHelmRunner, spanAttachedContext) move from exec.go to span.go,
  next to the marker type and classifiers they serve — exec.go keeps
  only the funnel call sites
- otlptest.SpanNames was never used outside the package; unexported
- isHelmBinary's comment now states it is the FALLBACK classifier
  (funnel invocations are marker-classified), replacing the outdated
  "cosmetic distinction" framing from before the marker existed
- docs/otel.md: release-scoped hooks nest under their release span
  (added in review round 2, never documented)

No behavior change; full -race suite green, lint clean.

Refs: #2769
Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-09-07 20:34:10 +08:00
c36dbfd417 fix: resolve OCI version constraints before deriving the shared chart cache path (#2768)
* fix: resolve OCI version constraints before deriving the shared chart cache path

When an OCI release uses a semver constraint (e.g. `~1`, `^2.0.0`, `*`),
`getOCIChartPath` currently derives the on-disk cache directory from the
raw constraint string via `safeVersionPath`, which substitutes constraint
characters (`~`, `^`, `>`, `<`, `!`, `|`, `=`, ` `, `,`, `*`) with `_`.
So `version: ~1` becomes `.../mychart/_1/` on disk. `acquireChartLock`
then refuses to refresh anything under the shared cache dir to avoid
race conditions between concurrent processes, so once the constraint is
first resolved and written to `_1/`, every subsequent render on that
machine (or that container replica) returns the pinned tarball
regardless of newer matching tags being published.

In multi-pod deployments like ArgoCD's argocd-repo-server this shows up
as intermittent stale renders: different pods populate their caches at
different moments and serve different snapshots of the same `~1`
release forever.

Fix: for OCI releases whose `version` looks like a constraint, run
`helm show chart <ref> --version <constraint> [flags]` and use the
returned metadata.Version as the effective version for all downstream
cache-key and path derivation. Helm already resolves the constraint
against the registry and returns the concrete matching Chart.yaml.
Callers get a content-addressable cache path (`.../mychart/1.0.1/`)
that naturally invalidates when the constraint resolves to a new
version. Exact-version releases and non-OCI releases skip the extra
call.

Adds an opt-out `resolveOCIVersions` field on `helmDefaults` and
`ReleaseSpec` (both default true). If the resolution call fails
transiently, the resolver logs a warning and falls back to the
pre-fix behavior so a network hiccup doesn't break rendering.

Adds `ShowChartWithFlags` to helmexec.Interface so the existing
`ShowChart` API stays backwards compatible.

Resolves #2766

Signed-off-by: Samuel Archambault <samuel.archambault@getmaintainx.com>

* refactor: move ShowChartWithFlags to a ChartInspector capability interface

Address Copilot review feedback on PR #2768: adding a method to the
exported helmexec.Interface is a source-breaking change for every
third-party implementation and mock of that interface, even though
ShowChart itself stayed backward-compatible.

Move ShowChartWithFlags off Interface and onto a new capability
interface, helmexec.ChartInspector, following the same pattern used
by the existing DependencyUpdater capability interface. The concrete
execer and the exectest.Helm test stub still satisfy it (they
already have the method); the OCI resolver in state.HelmState now
type-asserts and falls back to the pre-fix caching behavior when the
capability is absent, so downstream callers with their own
helmexec.Interface implementations keep compiling untouched.

Adds TestResolveOCIConstraintVersion_ChartInspectorFallback that
exercises the type-assertion path with a helm value that satisfies
Interface but deliberately does not satisfy ChartInspector.

Reverts ShowChartWithFlags additions from testutil.noCallHelmExec
and app_test.mockHelmExec since Interface no longer requires them.

Signed-off-by: Samuel Archambault <samuel.archambault@getmaintainx.com>

* fix: detect wildcard-segment semver constraints (1.x, 1.X) as constraints

Address Copilot review feedback on PR #2768: the previous
isVersionConstraint implementation scanned the input for operator
characters (~, ^, >, <, !, |, =, space, comma, *). Masterminds/semver
also accepts wildcard-segment constraints like "1.x", "1.X", "1.x.x",
and "1.2.X" that contain no operator characters. Those would slip past
the classifier, bypass OCI constraint resolution, and remain cached
forever under the raw ".../mychart/1.x/" path — the same stale-cache
bug the PR is meant to fix.

Replace the character scan with a semver-parser-based check: a value
is a constraint iff Masterminds/semver rejects it as a NewVersion but
accepts it as a NewConstraint. This correctly:

  - Recognizes wildcard forms (1.x, 1.X, 1.x.x, 1.2.x, v1.x).
  - Preserves exact versions where "x" appears in prerelease metadata
    ("1.0.0-alpha.x") or build metadata ("1.0.0+x", "1.0.0+build.x.1")
    without misclassifying them, which a naive "add x to the scanned
    charset" fix would have gotten wrong.
  - Continues to classify values that are neither a version nor a
    constraint (empty string, "latest", junk) as non-constraints; helm
    handles those elsewhere.

Removes the now-unused versionConstraintChars string constant.

Expands TestIsVersionConstraint with 8 wildcard cases and 3 prerelease
/build metadata cases containing "x", plus 2 non-parseable inputs.
Adds a "wildcard segment constraint resolves to concrete version"
subtest to TestResolveOCIConstraintVersion so the end-to-end pipeline
is exercised for a version string that has no operator characters.

Signed-off-by: Samuel Archambault <samuel.archambault@getmaintainx.com>

* test: add getOCIChart integration test proving cache-path/pull-flag wiring

Address Copilot review feedback on PR #2768. The existing unit test
exercised resolveOCIConstraintVersion in isolation but did not prove
that its output was propagated into the downstream cache key, cache
path, and `helm chart pull --version` flag. Add a targeted
integration test that:

  1. Calls getOCIChart with a constraint release (`~1`) and a helm
     mock whose ShowChartWithFlags returns Chart.yaml version 1.0.1.
  2. Asserts helm chart pull receives `--version 1.0.1`, not `~1`.
  3. Asserts the destination path passed to helm chart pull contains
     the resolved-version segment (`/1.0.1/`) and does NOT contain
     the raw-constraint segment (`/_1/`).
  4. Reads back the on-disk Chart.yaml under the cache path to
     confirm resolved version, path, and flag agree end to end.

Add a second test that runs the same release twice with different
resolver outputs (1.0.1, then 1.0.2 — simulating a newly published
matching tag) and asserts the two resolutions land in distinct cache
directories. This is the promise of the fix: once the raw constraint
is out of the path, a new matching tag stops silently reusing the
previously-resolved cache entry.

The integration test flushed out a real correctness gap in the
initial fix: getOCIChart resolved release.Version and chartVersion
but did NOT recompute the qualified OCI ref that
getOCIQualifiedChartName built pre-resolution. Helm was therefore
receiving `oci://<repo>/<chart>:<constraint>` alongside a
`--version <resolved>` flag — at best redundant, at worst rejected
by future Helm versions. Fixed by re-invoking
getOCIQualifiedChartName on the mutated release copy so the
embedded tag also carries the resolved value.

Isolates the shared helmfile cache via `t.Setenv(HELMFILE_CACHE_HOME,
t.TempDir())` so the OutputDirTemplate == "" code path (which writes
into remote.CacheDir) does not touch the user's real
`~/.cache/helmfile` during test runs.

Signed-off-by: Samuel Archambault <samuel.archambault@getmaintainx.com>

* refactor: flatten OCI constraint-resolution wiring in getOCIChart

Address review feedback on PR #2768:

- Extract the inline resolve/requalify block from getOCIChart into
  applyOCIConstraintResolution, keeping getOCIChart flat (guard-clause
  style) and making the resolution wiring independently testable. The
  helper returns the (possibly updated) release, qualified chart name,
  and chart version; every failure mode returns its inputs unchanged.

- On a re-qualify failure after a successful resolution, fall back to
  the pre-fix behavior entirely (raw constraint in cache key, ref, AND
  --version flag) instead of the previous half-resolved mix (resolved
  version in the cache key, raw constraint in the path and flag), which
  could desynchronize the in-process cache key from the on-disk path.

- Build the 'helm show chart' ref by reusing parseOCIChartRef instead of
  re-implementing its last-slash/last-colon tag-splitting inline. Same
  behavior for all realistic refs (registry ports preserved), and it
  also handles the digest suffix should one ever reach this point.

- Drop --devel from the resolver flags: helm documents --devel as
  ignored whenever --version is set, and --version is always passed on
  this path.

No behavior change intended beyond the requalify-failure fallback
(which cannot realistically trigger) and the removal of the inert
--devel flag.

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: classify partial semver versions (1, 1.2) as OCI constraints

Address review feedback on PR #2768: Masterminds' lenient parser accepts
partial versions like "1" or "1.2" as versions, so the previous
classifier (NewVersion fails && NewConstraint succeeds) treated them as
exact pins. But helm's OCI resolution — registry.GetTagMatchingVersionOrConstraint
— honors a version string as an exact pin ONLY when a registry tag
literally equals it; otherwise it parses the string as a constraint, and
"1"/"1.2" float across 1.x.y/1.2.y tags. Caching those under their raw
spelling reproduces the stale-cache bug of issue #2766, just with a
narrower trigger.

Replace the NewVersion probe with isFullSemver, which additionally
requires the whole major.minor.patch triple to be spelled out
(optional v prefix, prerelease, and build metadata all still count as
exact when the core is fully qualified). When a registry does carry a
literal tag equal to the version string, the resolver's
metadata.Version == chartVersion path reports no change, so literal-tag
pins keep today's behavior.

TestIsVersionConstraint: "1"/"1.0" flip to constraints, joined by new
v1.2/0/v1 cases and a 1.2.3 exact case. TestResolveOCIConstraintVersion
gains a "partial version resolves" subtest.

Docs updated to describe the parser-based classification instead of
"constraint characters".

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: skip OCI constraint resolution under skipRefresh

Address review feedback on PR #2768: the resolver ran even under
--skip-refresh, so offline and cache-only workflows gained a
'helm show chart' registry attempt per constraint-versioned OCI release.
It degraded gracefully (warn + fallback), but added registry-timeout
latency and warning noise per release.

skipOCIConstraintResolution now suppresses resolution when any of the
skipRefresh levels is set — CLI --skip-refresh (forced), per-release
skipRefresh, or helmDefaults.skipRefresh — with the same precedence the
other skipRefresh consumers in prepareChartForRelease use. Skipped runs
fall back to the constraint-keyed cache path, i.e. they reuse whatever a
previous non-skipped run resolved, which is what 'skip checking for
updates to cached charts' means for constraint versions.

The existing issue #2766 integration tests flip their opts to
SkipRefresh: false since they assert resolution happens. New coverage:
TestSkipOCIConstraintResolution (tri-state precedence table) and
TestGetOCIChart_SkipRefreshSkipsConstraintResolution (no inspector call,
raw constraint in --version and cache path).

Signed-off-by: yxxhero <aiopsclub@163.com>

* perf: memoize OCI constraint resolution per chart+constraint

Address review feedback on PR #2768: resolution ran before the
in-process chart-cache fast path and was not memoized, so every
constraint-versioned OCI release paid its own 'helm show chart' registry
round-trip on every render — including N releases sharing the same
chart+constraint, whose parallel workers could even resolve to different
versions if the registry changed between their lookups.

Memoize successful resolutions in resolvedOCIConstraints keyed by
(chart ref, constraint), mirroring the downloadedCharts pattern:

- Releases sharing a chart+constraint cost one round-trip per process
  and consistently use one resolved version per run.
- Only successful resolutions are memoized; failures may be transient.
- Flags are not part of the key: they govern TLS/verification/registry
  credentials, not which tag a constraint matches (--devel is already
  omitted as it is ignored whenever --version is set).
- Concurrent misses may both hit the registry; last write wins,
  harmlessly.

resetResolvedOCIConstraintsForTest is added alongside the existing
resetChartCacheForTest and wired into the issue #2766 tests — notably
ResolvesToDifferentVersionsPicksSeparateCachePaths, which reuses the
same chart+constraint across its two runs and would otherwise be served
the first resolution from the memo (which is exactly the intended
per-process semantics).

New coverage: TestResolveOCIConstraintVersion_Memoized (memo hit skips
the registry, different constraint is a different key) and
TestGetOCIChart_SharedConstraintResolvedOncePerProcess (two releases,
one inspector call, one pull, same path).

Signed-off-by: yxxhero <aiopsclub@163.com>

* test: cover URL-embedded OCI constraint resolution

Address review feedback on PR #2768: the existing integration tests only
exercised the repo-aliased spelling (chart: myrepo/mychart, version:
'~1') and the version-field spelling. The chart-URL spelling
(chart: oci://<registry>/<chart>:~1) takes a different branch in
getOCIQualifiedChartName — the URL version is deliberately NOT embedded
into the qualified ref and flows through --version only — so its
re-qualification after constraint resolution (release.Version mutated to
the resolved value, versionInURL still the constraint) was untested.

TestGetOCIChart_URLEmbeddedConstraintResolves asserts the resolver
receives the URL-embedded constraint, helm chart pull receives the
resolved version via --version with a tag-less ref, and the cache path
carries the resolved version segment instead of the raw constraint.

Also gofmt-aligns the test tables added in earlier commits and drops a
redundant 1.2.3 test case that tripped goconst.

Signed-off-by: yxxhero <aiopsclub@163.com>

* docs: note empty-version OCI releases are unaffected by resolveOCIVersions

Releases with no version: at all keep their pre-existing semantics: helm
picks the latest tag at pull time and helmfile caches it under a
version-less shared-cache path. Document the limitation alongside the
other resolveOCIVersions scope notes.

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: Samuel Archambault <samuel.archambault@getmaintainx.com>
Signed-off-by: yxxhero <aiopsclub@163.com>
Co-authored-by: Samuel Archambault <samuel.archambault@getmaintainx.com>
Co-authored-by: yxxhero <aiopsclub@163.com>
2026-09-07 16:50:25 +08:00
34ead21107 feat: allow helmfile to continue on failed releases (#2616)
* feat: allow helmfile to continue on failed releases

Co-authored-by: Peter Honeder <peter.honeder@unwired.at>
Signed-off-by: Niklas Ott <niklas.ott@unwired.at>

* fix: skip failed-prep releases, complete flag wiring, add tests and docs (#64)

Review follow-ups for --allow-failed-releases (#2616):

- Track per-release chart preparation failures in PrepareCharts (returned
  as a map keyed by release) and remove those releases from the state in
  Run.WithPreparedCharts when --allow-failed-releases is set, so a failed
  release is never executed against its original, un-prepared chart
  reference (which could either fail again with a duplicate error or, for
  charts requiring chartify, bypass patches/dependency modifications and
  produce an unintended result). All failures are still reported at the
  end via the aggregated MultiError.
- Complete the release identity on error results from
  prepareChartForRelease so failures are attributed to the correct
  release.
- With --allow-failed-releases, continue building dependencies of the
  remaining charts when 'helm dep build' fails for one release, and skip
  the affected releases during execution.
- Wire --allow-failed-releases into 'helmfile unittest' and 'helmfile
  status'; remove the dead flag wiring for write-values and list (both
  never prepare charts, see commandsSkipChartPrep).
- Simplify control flow (guard clauses, errors.As, drop dead code and
  redundant else branches).
- Add end-to-end coverage in pkg/app/issue_2616_test.go and extend the
  state-level tests; document the flag in docs/cli.md and CHANGELOG.md.

Signed-off-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

---------

Signed-off-by: Niklas Ott <niklas.ott@unwired.at>
Signed-off-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
Co-authored-by: Peter Honeder <peter.honeder@unwired.at>
Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
2026-09-03 06:26:46 +08:00
yxxhero 90acb70d95 build(deps): bump helm-diff to v3.15.12 (#2759)
Update helm-diff plugin version from v3.15.11 to v3.15.12 across
Dockerfiles, recommended version constant, CI matrix, and
integration test default.

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-08-30 13:27:43 +08:00
yxxhero 6034a54e0d refactor: drop empty-render workaround now that chartify handles it natively (#2747)
Bump github.com/helmfile/chartify to v0.28.2, which fixes the empty-render
crash upstream (helmfile/chartify#206, fixed in helmfile/chartify#207):
when a chart renders zero resources, chartify now treats it as a no-op
success itself - removing the chart's content dirs, cleaning Chart.yaml
dependencies and the lock file, and skipping the kustomize step - instead
of failing with:

  assertion failed: unexpected dir entry "" it must be the abs path to the output directory

That makes the helmfile-side string-matching workaround from #2724 dead
code: the error it matched can no longer be produced. Remove
isChartifyEmptyRenderOutputError and the special-cased no-op branch in
processChartification, so the empty-render case simply flows through the
regular chartify path.

Also bump github.com/helmfile/vals to v0.46.0 and refresh transitive
dependencies.

The regression test for #1757 is updated to assert the new direct
behavior: processChartification succeeds, returns a chartified chart
whose Chart.yaml no longer declares dependencies (so a subsequent
"helm dep build"/"helm template" cannot fail with "found in Chart.yaml,
but missing in charts/ directory"), renders empty output, and is cleaned
up by CleanupChartifyTempDirs.

Fixes #1757 (follow-up to #2724)

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-08-16 10:47:19 +08:00
Copilotandyxxhero e178870ce3 Bumping Helm versions to 3.21.4 and 4.2.4 (#2746)
* chore: bump Helm to v3.21.4 and v4.2.4

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* chore: finalize Helm version bump validation

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* fix: update Helm 4.2.4 Docker checksum pins

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
2026-08-15 16:55:43 +08:00
Thomas HanserandClaude Sonnet 5 2cb878d5a0 fix(#2741): prefetch shared remote charts instead of serializing sync (#2743)
* fix(state): prefetch shared remote charts instead of serializing sync/diff (#2741)

PR #2662 fixed a Windows chart-download race (#768) by wrapping the entire
helm upgrade/diff operation in a per-chart+version lock, not just the
download. Since sync/apply/diff never set ForceDownload, releases sharing a
remote chart end up fully serialized even at high --concurrency.

Add ChartPrepareOptions.PrefetchSharedRemoteCharts: PrepareCharts groups
selected releases by chart+version, and force-downloads (once) any chart
used by 2+ releases that also resolve to identical acquisition flags
(--verify/--keyring/--plain-http/--insecure-skip-tls-verify/--devel) and to
a configured repository (or OCI ref) - not a bare \"dir/chart\"-shaped local
path. That materializes release.ChartPath, which lets withChartOperationLock's
existing ChartPath != \"\" guard skip the lock, restoring concurrency without
touching the #768 protection for charts that aren't prefetched.

Also add chartFetchFlags to give forcedDownloadChart's \`helm fetch\` the same
verify/keyring/TLS flags flagsForUpgrade already applies, closing a parity
gap that predates this change (affects lint/unittest/pull too).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Thomas Hanser <gh@toms.place>

* fix(state): exclude verify-enabled charts from shared-chart prefetch, use NUL-delimited flag signature

Copilot review on #2741's PR flagged two issues in the shared-chart prefetch
added there:

1. forcedDownloadChart untars a shared chart into a local directory, but
   flagsForUpgrade unconditionally re-adds --verify for the later `helm
   upgrade` regardless of ChartPath. Helm's VerifyChart only accepts a
   packaged .tgz/provenance pair, not an unpacked directory, so upgrading a
   prefetched chart with verify enabled would fail. Exclude --verify from
   prefetch eligibility entirely rather than trying to suppress the later
   flag - those releases just keep the pre-existing serialized-lock
   behavior, unaffected by this feature.

2. The per-key flag-agreement signature joined flags with a space, which
   isn't injective: a keyring path containing a space and a flag-like token
   could collide with a different keyring plus a real flag, silently
   deduplicating releases with different acquisition settings. Join with
   NUL instead, which can't appear in an OS argument.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Thomas Hanser <gh@toms.place>

* fix(state): apply -chart override before shared-chart grouping

Copilot flagged that PrepareCharts grouped releases by release.Chart
before prepareChartForRelease applied st.OverrideChart (the -chart CLI
flag), so distinct original charts that all resolve to the same
overridden chart were never recognized as shared and missed the
prefetch. Apply the override once upfront, before the grouping loop
reads release.Chart.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Thomas Hanser <gh@toms.place>

---------

Signed-off-by: Thomas Hanser <gh@toms.place>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 08:57:58 +08:00
yxxhero c008d0e129 fix: update helm plugins via uninstall+reinstall to honor pinned version (#2727)
* fix: update helm plugins via uninstall+reinstall to honor pinned version (#2726)

helm plugin update re-installs a plugin from its cached source WITHOUT the
--version flag, so it does not reliably install the pinned version helmfile
requests. It reports success (exit 0) while re-downloading the unchanged
cached source, leaving 'helm plugin list' showing the old version.

This affected 'helmfile init --force' which reported a successful diff/secrets
plugin update while the old version remained installed. The reporter's
workaround was to uninstall the plugin before running init.

UpdatePlugin now uninstalls the existing plugin and reinstalls the exact
pinned version via AddPlugin (which passes --version). The unreliable
'plugin update' command is no longer used for the general path; this mirrors
the uninstall+reinstall strategy already used for helm-secrets on Helm 4.

Fixes #2726
Refs #2548

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: only ignore 'not found' uninstall errors in UpdatePlugin

Address review feedback: UpdatePlugin ignored all uninstall errors, which
could mask real failures (permissions, broken Helm) behind a less informative
'plugin already exists' error from the subsequent install.

Now only the expected 'not found' case (plugin removed concurrently, reported
by both Helm 3 'Plugin: <name> not found' and Helm 4 'plugin: <name> not
found') is ignored and install proceeds. Any other uninstall error is returned.

Adds tests for both branches.

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: extract pluginCmd constant to satisfy goconst in tests

golangci-lint (goconst, min-occurrences: 8) flagged the "plugin" string
literal in exec_test.go after the new UpdatePlugin tests pushed the package-
wide count from 7 (under threshold, passing on main) to 14.

Introduce a pluginCmd constant and use it for all helm sub-command checks in
the UpdatePlugin tests. No behavior change; drops the literal count back below
the threshold so CI goconst passes.

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: match helm plugin-absent error precisely; guard against plugin update regression

Address review feedback on UpdatePlugin:

1. The uninstall error check used strings.Contains(err, "not found"), which
   is too broad: it also matches unrelated failures such as a missing helm
   binary ("executable file not found") or an uninstall hook failing with
   "sh: ...: not found". Those would be silently treated as "plugin absent"
   and logged/proceeded past, masking the real problem.

   Match only helm's specific plugin-absent message instead, via a
   case-insensitive regex that covers both majors:
     Helm 4: "plugin: <name> not found"
     Helm 3: "Plugin: <name> not found"
   All other uninstall failures (permissions, missing binary, hook errors)
   are now surfaced.

2. The init test mock had no "plugin update" branch, so a production
   regression to calling helm plugin update would go undetected (false
   negative). Add an explicit case that records and fails on "plugin update".

Also adds a test for the missing-binary case and makes the plugin-absent test
table-driven across Helm 3/4 formats. Extracts installCmd to keep goconst
(min-occurrences: 8) satisfied after the new tests.

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-08-03 22:16:18 +08:00
Copilot 913ebfdfa4 bump helm-diff to v3.15.11 (#2725)
* bump helm-diff recommended version to v3.15.11

* bump helm-diff to v3.15.11 in CI workflow matrix

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-08-03 22:16:05 +08:00
henrichter-sap 7cf4ff9a25 feat: add --skip-diff-validation-on-install CLI flag (#2728)
Signed-off-by: Richter <h.richter@sap.com>
2026-08-03 21:11:45 +08:00
Ankit 1341fd8659 fix: skip chartification when chart renders zero resources (#2724)
* fix: skip chartification when chart renders zero resources

When a chart's templates render zero resources (e.g. everything is
gated behind a falsy `{{- if .Values.enabled }}`) and the release has
transformers, jsonPatches, or strategicMergePatches configured,
helmfile crashed with:

  assertion failed: unexpected dir entry "" it must be the abs path
  to the output directory

Root cause: chartify's replace.go runs `helm template --output-dir`
and expects exactly one directory entry under that output dir (the
rendered chart). When helm renders no resources, the output dir is
empty, so chartOutputDir stays "" and chartify's own assertion on it
being an absolute path fails. chartify (v0.28.0) doesn't expose a
typed/sentinel error for this, only the assertion text.

Since there's nothing to chartify when a release has no rendered
resources, treat this specific chartify failure as a no-op: keep
using the chart as-is and let helm template it normally (producing
the same empty output helm would have produced without chartify).
Any other chartify error is still surfaced unchanged.

Verified manually end-to-end with a real helm+kustomize:
- a chart with `enabled: false` + a transformer now runs without
  error instead of crashing
- a chart with `enabled: true` + the same transformer still gets
  transformed correctly (annotations applied), confirming the normal
  chartify path is untouched

Added unit tests for the new error-matching helper in
pkg/state/issue_1757_test.go.

Fixes #1757

Signed-off-by: ankit090701 <ankitanku090701@gmail.com>

* fix: return original chart path from empty-render no-op, not the deps-rewrite temp copy

Review feedback on the original fix (#2724) found a real bug: the
empty-render no-op path returned chartPath after it may have already
been reassigned to the temp copy created by rewriteChartDependencies
(for charts with relative file:// deps). That temp dir is removed by
a deferred cleanupTempChart() as soon as processChartification
returns, so the caller was handed a path to a directory that no
longer existed, breaking any subsequent helm command with "chart not
found" - narrow (only local charts with relative file:// deps that
also render zero resources) but real and reproducible.

Capture originalChartPath before the rewrite and return that instead.
Also flatten the nested `if err != nil { if isChartifyEmptyRenderOutputError...`
into two sequential checks per review, and link the upstream tracking
issue (helmfile/chartify#206, opened by a maintainer during review) in
the error-matching constant's doc comment.

Added TestProcessChartification_EmptyRenderReturnsSurvivingPath, an
end-to-end test exercising the real processChartification ->
chartify.Chartify wiring (not just the isChartifyEmptyRenderOutputError
helper) with a chart that has a relative file:// dependency and renders
zero resources - the exact conditions that trigger the bug. Verified
passing against real helm+kustomize in a Linux container; skipped on
Windows due to an unrelated, pre-existing Windows path-handling issue
in chartify's dependency resolution (a drive letter embedded in a
file:// URL gets mis-joined), independent of the code path under test.

Signed-off-by: ankit090701 <ankitanku090701@gmail.com>

* fix: narrow chartify empty-render error match to avoid false positives

Per Copilot's automated review on this PR: the previous substring,
"it must be the abs path to the output directory", matches chartify's
assertion regardless of what chartOutputDir actually is. In the real
empty-render case chartOutputDir is "" (formatted via %q as `""`), but
the same assertion (chartify replace.go:151) would also fire if
chartOutputDir were ever a non-empty-but-still-relative path - a
different, genuine bug that should be surfaced as an error, not
silently treated as an empty-render no-op.

Narrow the match to include the `unexpected dir entry ""` prefix, so
it can only match the exact empty-string case. Verified against the
actual chartify v0.28.0 source (fmt.Errorf with %q on chartOutputDir)
that this is precisely what the empty-render case produces.

Added a test case asserting the same assertion text with a non-empty
dir entry is correctly NOT treated as the empty-render case. Re-ran
the full test suite (including the real helm+kustomize end-to-end
integration test) in a Linux container to confirm the narrower match
still catches the actual reported bug.

Signed-off-by: ankit090701 <ankitanku090701@gmail.com>

---------

Signed-off-by: ankit090701 <ankitanku090701@gmail.com>
2026-08-02 07:28:57 +08:00
yxxhero 7f748fec1b feat: support Helm 4 --rollback-on-failure alongside deprecated --atomic (#2722)
* feat: support Helm 4 --rollback-on-failure alongside deprecated --atomic (#2712)

Helm 4 renamed the `--atomic` flag to `--rollback-on-failure`
(helm/helm#13629). The old flag still works under Helm 4 but is
deprecated (prints a warning) and slated for removal in Helm 5.

Add a `rollbackOnFailure` key to both `helmDefaults` (HelmSpec) and
`releases[]` (ReleaseSpec) that emits `--rollback-on-failure`. It
requires Helm 4+ (errors otherwise) and is mutually exclusive with
`atomic`.

Additionally, when the resolved Helm binary is v4+, an existing
`atomic: true` now emits `--rollback-on-failure` instead of `--atomic`,
so users are migrated off the deprecated flag automatically without any
config change. On older Helm, `atomic: true` continues to emit
`--atomic`.

Updated the spew-based values-ID hashes in temp_test.go that change
whenever ReleaseSpec gains a field (same approach as the --force-conflicts
change in #2480).

Closes #2712.

Signed-off-by: yxxhero <aiopsclub@163.com>

* test: add integration test for rollback-on-failure / atomic migration (#2712)

Covers the end-to-end plumbing that unit tests cannot (real helm version
detection + cluster deploy) via test/integration/run.sh:

  1. atomic: true parses, deploys a ConfigMap, and emits the version-correct
     flag: --rollback-on-failure on Helm 4 (auto-migration of the deprecated
     --atomic) and --atomic on Helm 3.
  2. rollbackOnFailure: true emits --rollback-on-failure on Helm 4 and is
     rejected with a clear Helm-4-required error on Helm 3.

Flag assertions grep the `exec: helm upgrade --install` lines logged under
--debug, matching flags as standalone tokens so the release name
"issue-2712-atomic" cannot be confused with the "--atomic" flag.

Verified locally against Helm 4.2.3: both atomic:true and
rollbackOnFailure:true emit --rollback-on-failure with no --atomic.

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-07-31 22:22:31 +08:00
yxxhero 7bebfab71a feat: add --repo-retries for helm repo and registry login commands (#2683)
* feat: add --repo-retries for retrying helm repo and registry login commands

Add a configurable retry mechanism for chart repository operations to
handle unstable networks (corporate proxies, slow internal registries).

Closes #1894

- New --repo-retries N flag and HELMFILE_REPO_RETRIES env var (default 0
  = opt-in, backward compatible)
- Retry applies to helm repo add, helm repo update (incl. ACR), and
  helm registry login with exponential backoff (1s, 2s, 4s, ..., capped 30s)
- Single retryRepoOp helper; per-attempt args/buffer are local to avoid
  state leaking across retries
- Tests cover succeed-after-retry, exhausted-retries, disabled-by-default,
  and regression guards for password-buffer and args-accumulation

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: address PR review (overflow guard, cancellable sleep, flag-override, docs)

Address Copilot review feedback on #2683:

- Cap backoff shift exponent at 5 to prevent time.Duration overflow on
  large --repo-retries values
- Make retry sleep context-aware (sleepCtx) so Ctrl+C aborts the retry
  loop promptly via the ShellRunner context
- Log a concise exit status instead of the verbose ExitError dump, and
  clarify the retry-counter wording ('retry N/M')
- Use -1 sentinel as the CLI default so --repo-retries=0 can explicitly
  disable retries even when HELMFILE_REPO_RETRIES is set
- Align help text and docs: retry applies 'on failure' (not just
  transient errors), document the 0-disables behavior
- Add tests for overflow guard, cancellable sleep, and flag-zero-disables

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: abort retries on canceled context, hide sentinel default, align comment

Address follow-up Copilot review on #2683:

- Fix tight-loop bug: sleepCtx now returns whether it completed vs was
  interrupted by context cancellation, and retryRepoOp aborts the retry
  loop on interruption so Ctrl+C no longer spins into rapid helm calls
- Hide the -1 sentinel from --help by overriding the displayed default
  to 0 (pflag DefValue), matching the documented default while keeping
  the flag-override semantics
- Correct HelmExecOptions.RepoRetry comment: 'on failure' not 'transient
  network errors', matching the actual retry behavior
- Add Test_Retry_AbortsOnCanceledContext covering the no-tight-loop path

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: copy args per retry in RegistryLogin, make cancel test deterministic

Address follow-up Copilot review on #2683:

- RegistryLogin: pass a per-attempt copy of args to execStdIn so its
  internal append (for helm.extra) can't alias the shared slice across
  retries
- Test_Retry_AbortsOnCanceledContext: cancel the context deterministically
  inside the op closure after the first attempt, replacing the flaky
  time.Sleep(20ms) goroutine

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: return error on unknown managed repo type instead of silent skip

Address Copilot review on #2683: AddRepo logged an error for an unknown
managed type but returned nil, silently succeeding while skipping the
repo add. Now returns an error so misconfigurations fail loudly.

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-07-23 18:10:04 +08:00
fabiobaiao 919aa29f15 feat: support disabling insecure template functions only (#2689)
Signed-off-by: fabiobaiao <53570695+fabiobaiao@users.noreply.github.com>
2026-07-17 17:52:59 +08:00
fabiobaiao e26c6fd13f feat: support disabling hooks (#2691)
Signed-off-by: fabiobaiao <53570695+fabiobaiao@users.noreply.github.com>
2026-07-16 11:54:13 +08:00
yxxhero 43aafeaad1 fix: ensure OCI registry login when SkipRepos is set (#2701)
* fix: ensure OCI registry login when SkipRepos is set (#1847)

Commands like build, status, list, and show-dag set SkipRepos: true to
avoid slow helm repo add/update for classic repos. However, this also
skipped helm registry login for OCI registries, causing 401 Unauthorized
errors when pulling OCI charts.

Add a variadic SyncOption parameter (backward compatible) with
WithOCIOnly() that limits repo processing to OCI registries only.
When skipRepos is true, callers now pass WithOCIOnly() so that OCI
authentication still happens before chart pulls.

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: reword OCI login comments per review feedback

RegistryLogin is a no-op when credentials are not configured, so the
word 'always' was misleading. Clarify that login is only needed when
credentials are present.

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: skip OCI login for commands that don't pull charts

Commands like 'list' and 'write-values' skip chart preparation entirely,
so OCI registry login is unnecessary for them. Extract the skip-command
list into a shared variable and use it to gate OCI-only login in
WithPreparedCharts.

Signed-off-by: yxxhero <aiopsclub@163.com>

* docs: clarify commandsSkipChartPrep comment per review feedback

Clarify that these commands only skip OCI login when skipRepos is true;
when skipRepos is false, SyncReposOnce still runs normally for all repos.

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-07-12 09:51:24 +08:00
yxxhero a9e4a8518a feat: center section divider to match table width (#2698)
The '========== Updated Releases ==========' header was a fixed 38
chars regardless of the table width below it. Now the divider line
is extended to match the table's visual width, with the title text
centered within the '=' borders.

- Add TableVisualWidth() to measure table width via runewidth
- Add HeaderDividerCentered() and HeaderDividerCenteredStyled()
  for centered dividers with optional bold+blue ANSI styling
- Refactor DisplayAffectedReleases to build the table first, then
  compute its width before logging the header
- Update all test snapshots and integration test output files

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-07-10 21:17:35 +08:00
yxxhero 30f529f529 chore: bump helm to v4.2.3 and v3.21.3 (#2699)
Signed-off-by: yxxhero <aiopsclub@163.com>
2026-07-10 21:17:08 +08:00
Arthur Garreauandyxxhero cc85562625 feat: Add ConditionTemplate support in releaseSpec (#2669)
* feat: Add ConditionTemplate support in releaseSpec

Signed-off-by: Arthur Garreau <arthur.garreau98@gmail.com>

* feat: improve testing, clarify doc

Signed-off-by: Arthur Garreau <arthur.garreau98@gmail.com>

* feat: update CHANGELOG for ConditionTemplate support and fix test cases for ID generation

Signed-off-by: Arthur Garreau <arthur.garreau98@gmail.com>

* Update pkg/state/state_exec_tmpl.go

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
Signed-off-by: Arthur Garreau <arthur.garreau98@gmail.com>

* Update pkg/state/state_exec_tmpl.go

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
Signed-off-by: Arthur Garreau <arthur.garreau98@gmail.com>

* Update docs/configuration.md

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
Signed-off-by: Arthur Garreau <arthur.garreau98@gmail.com>

* refactor: improve comments for condition checks and clean up whitespace

Signed-off-by: Arthur Garreau <arthur.garreau98@gmail.com>

---------

Signed-off-by: Arthur Garreau <arthur.garreau98@gmail.com>
Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
2026-07-01 18:44:43 +08:00
yxxhero afdb2487a6 feat: add inherits: for sub-helmfile config inheritance (#2680)
* feat: add `inherits:` for sub-helmfile config inheritance

Add an opt-in `inherits:` field to `helmfiles:` entries so a sub-helmfile
can inherit specific configuration categories from its parent:

  helmfiles:
  - path: myapp.yaml
    inherits: [repositories, environments]

Allowed values: repositories, helmDefaults, commonLabels, apiVersions,
kubeVersion, templates, environments. Child values win; parent fills gaps
(consistent with `bases:`). This directly fixes #1495, where a repository
declared in the parent was unavailable to sub-helmfiles, producing a
confusing "repo not found" error.

Implementation notes:
- The 6 pure fields (repositories, helmDefaults, commonLabels, apiVersions,
  kubeVersion, templates) are merged post-load via MergeInherited; verified
  all are consumed post-load (ExecuteTemplates/converge), never at parse.
- environments is injected pre-load as ctxEnv, because RenderedValues is
  baked at load time; the parent's resolved values become the base and the
  child's own environments: block overrides per key.
- helmDefaults uses a *HelmSpec pointer (value type is non-comparable) with
  a no-override mergo merge, so a child that omits helmDefaults inherits the
  parent's fully.
- A footgun warning (WarnUninheritedRepos) suggests
  `inherits: [repositories]` when a release references a repo the parent
  declares but the child lacks.
- Unknown inherits keys are rejected at parse time with the allowed set.

Inheritance is opt-in and fully backward compatible: empty (the default)
preserves the historical independent-sub-helmfile behavior.

Fixes #1495

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: address review — deep-copy inherited config and fix bases: doc link

- BuildInheritedConfig now deep-copies the pure fields via a YAML round-trip
  (Env via environment.DeepCopy) so the returned config never aliases the
  parent state's slices/maps, matching its doc comment. Now returns an error
  to surface round-trip failures; the call site in processNestedHelmfiles is
  updated. Added TestBuildInheritedConfig_PureFieldsAreDeepCopied to lock
  in the no-aliasing guarantee.
- Fix the broken `bases:` anchor (#) in shared-configuration-across-teams.md
  to point to writing-helmfile.md#layering-state-files.

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: address review — reject inherits without path and document helmDefaults caveat

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: address review — make AllowedInherits immutable and clarify effective-repo wording

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-07-01 14:44:16 +08:00
yxxhero ad10814842 fix: replace YAML-based DeepCopy with recursive deep copy to fix #973 (#2675)
Environment.DeepCopy() used a YAML marshal/unmarshal round-trip to copy
values. When SOPS/KMS-encrypted secret files contained values with special
characters (colons, quotes, braces such as ~masked:ab#7i7!;{'".), the
YAML round-trip could mangle or silently drop adjacent keys, producing
the "map has no entry for key" error reported in #973.

Replace the YAML-based DeepCopy with maputil.DeepCopyMap(), a proper
recursive deep copy that:
- Preserves original Go types (string "true" stays string, not bool)
- Never loses data due to special characters in values
- Normalises map[any]any keys to strings (matching CastKeysToStrings)

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-06-29 20:40:18 +08:00
yxxhero bbf925f154 test: add integration test for issue #1880 transformers with file:// deps (#2673)
* test: add integration test for issue #1880 transformers with file:// deps

Add an integration test reproducing the exact scenario from issue #1880:
a local chart with a relative file:// dependency (file://../library) used
together with kustomize transformers.

Before the fix (rewriteChartDependencies in PR #2334), chartify copied
the chart to a temp directory, breaking the relative file:// path and
causing helm dependency up to fail with:
  Error: directory /tmp/chartify.../monitoring/library not found

Also fix test artifact leak in issue923_test.go where OCI chart downloads
wrote to CWD because OutputDirTemplate lacked {{ .OutputDir }}.

Closes #1880

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix(test): guard exit-code captures against set -e in integration tests

Under set -e (enabled in run.sh), a failing command exits the shell
before 'var=$?' can execute, defeating diagnostic cat+fail blocks and
breaking helm diff tests that expect exit code 2.

Replaced 'cmd; var=$?' with 'var=0; cmd || var=$?' across 12 test
files (29 sites), matching the pattern already used in oci-parallel-pull.sh.

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-06-29 08:49:51 +08:00
yxxhero 8f1bb404d6 fix: support go-getter URLs in ad-hoc dependencies to fix #821 (#2670)
* fix: support go-getter URLs in ad-hoc dependencies to fix #821

Ad-hoc release dependencies (release.dependencies[].chart) that used
go-getter URLs like "git::https://host/repo.git@path?ref=tag" were passed
to chartify as-is. chartify then tried to resolve them via `helm repo
list`, which fails with "no helm list entry found for repository
\"git::https:\". please `helm repo add` it!".

The primary chart already fetched such URLs via downloadChartWithGoGetter,
but the ad-hoc dependency path in PrepareChartify only handled local
directories and OCI rewrites, so go-getter URLs fell through.

This adds a branch that detects remote go-getter URLs (remote.IsRemote)
and fetches them to a local cache directory via a new
downloadAdhocDepChartWithGoGetter helper, mirroring the primary-chart
fetch path. chartify then sees a local chart and takes its file:// branch.

Fixes #821

Signed-off-by: yxxhero <aiopsclub@163.com>

* test: add integration test for go-getter ad-hoc dependencies (#821)

Adds an integration test that commits a chart to a throwaway local git repo
and references it via a "git::file://..." go-getter URL as a release
ad-hoc dependency, then asserts `helmfile template` renders both the main
chart and the fetched dependency.

Using file:// (rather than https://) keeps the test deterministic and
network-free while exercising the exact fix path (remote.IsRemote +
downloadAdhocDepChartWithGoGetter in PrepareChartify). Verified to fail on
the unfixed code with the original "no helm list entry found for repository
\"git::file:\"" error and pass on the fixed code.

Signed-off-by: yxxhero <aiopsclub@163.com>

* test: surface helmfile error in issue #821 integration test

The integration runner (run.sh) enables `set -e`, so a non-zero helmfile
exit aborted the script before the captured output could be printed,
hiding the real failure in CI. Disable `set -e` around the helmfile
invocation and report the exit code plus full output on failure.

Signed-off-by: yxxhero <aiopsclub@163.com>

* test: fix issue-821 integration test chart path and errexit handling

Two issues caused the integration test to fail in CI (while passing in my
local smoke test, which used an absolute path):

1. The main chart path was relative to the integration CWD, but the
   helmfile.yaml is generated in a temp directory and helmfile resolves
   `chart:` relative to that directory (its basePath). The relative path
   was interpreted as a named-repo chart and failed instantly with
   `Error: repo test not found`. Resolve the case dir to an absolute path
   with `$(cd ... && pwd)`.

2. run.sh runs under `set -e`, so the unguarded helmfile invocation exited
   the whole script on failure before the captured output could be printed,
   hiding the real error. Capture the exit code via `cmd || rc=$?` so
   failures are reported.

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-06-28 15:28:05 +08:00
yxxhero e3f757d5ed feat: add --template-args flag to template/apply/sync for helm lookup() support (#2666)
* feat: add --template-args to enable helm lookup() during template/apply/sync (#1833)

Add a --template-args flag to the template, apply, and sync subcommands so
extra args (most notably --dry-run=server) can be passed to the helm template
invocation, enabling Helm's lookup() function to resolve live cluster values.

- template: --template-args reaches both chartify's pre-render helm template
  and the final helm template output (flagsForTemplate).
- apply/sync: --template-args reaches chartify's pre-render helm template.
  apply/sync already inject --dry-run=server automatically for cluster
  operations; the flag is an explicit opt-in for the template subcommand or
  for passing additional flags.
- When --dry-run is present in template args, kube-context/kubeconfig are also
  injected into chartify so lookup() can actually reach the cluster.
- Resolves the long-stale PR #1833 rebased onto current main, which already
  contains the cluster-connectivity infrastructure (issues #2271, #2309,
  #2355, #2444).
- Includes integration test (lookup.sh) covering both chartify and
  non-chartify scenarios.

Signed-off-by: yxxhero <aiopsclub@163.com>

* test: make lookup template nil-safe to fix integration CI

The lookup() function returns an empty map when the chart is rendered
without a cluster connection (notably the helm-diff phase of `helmfile
apply`). The original fixture chained `index` over the lookup result,
panicking with "index of untyped nil" during apply's diff rendering.

Guard every index with `default dict` so the template falls back to
"overwritten" when lookup is empty, while still resolving to the live
value ("init") when cluster access is available (--dry-run=server via
--template-args, or a real helm upgrade).

Signed-off-by: yxxhero <aiopsclub@163.com>

* feat: enable lookup() during apply/diff via --template-args in helm-diff

Thread --template-args into the helm-diff rendering path so that
`helmfile apply`/`diff --template-args="--dry-run=server"` resolves
Helm's lookup() function during the diff phase too. helm-diff supports
`--dry-run=server`, which explicitly "enables the cluster access ...
and the lookup template function".

Previously --template-args only reached chartify's pre-render (which is a
no-op for plain charts due to chartify's early-return when there is no
forceNamespace/patches/injections) and the final `helm template` of the
`template` subcommand. As a result `helmfile apply` on a lookup chart
rendered client-side during the diff phase.

Changes:
- pkg/state: add TemplateArgs to DiffOpts; append it in appendExtraDiffFlags
  (reaches every helm-diff invocation: apply, standalone diff, interactive
  sync), mirroring the existing flagsForTemplate handling.
- pkg/config + cmd: add --template-args to the diff/doctor commands and to
  DiffConfigProvider, so lookup works for `helmfile diff` as well.
- pkg/app: populate DiffOpts.TemplateArgs from apply/diff/sync-interactive.
- docs/cli.md: correct the previous overpromising wording and document diff
  support plus the nil-safe lookup guidance.
- tests: unit-test the TemplateArgs handling in appendExtraDiffFlags and
  flagsForTemplate; integration lookup.sh now exercises apply with
  --template-args="--dry-run=server".

Signed-off-by: yxxhero <aiopsclub@163.com>

* refactor: de-duplicate chartify template-args logic, add helmDefaults.templateArgs

Address review feedback on #2666:

1. Eliminate stale duplicated test helpers (issue_2444_test.go, issue_2355_test.go).
   Both files intentionally copied the processChartification flag-building logic
   with explicit SYNC WARNING comments, then drifted out of sync when #2666
   refactored the production code (needsKubeConnection gate, user-args merge).
   Extract the real logic into pure, unit-tested helpers
   (buildChartifyTemplateArgs, commandRequiresCluster) and delete the copies.

2. Add unit coverage for the new chartify merge path: template +
   --template-args=--dry-run=server now triggers kubeconfig/kube-context
   injection (TestTemplateArgsDryRunTriggersKubeInjection,
   TestTemplateArgsMergedBeforeInjection) — previously only covered by the
   cluster-dependent integration test.

3. Add a negative integration case (lookup.sh assert_template_fallback)
   verifying lookup() falls back to the default value WITHOUT --template-args,
   guarding against a regression that silently always connects to the cluster.

4. Add helmDefaults.templateArgs for parity with diffArgs/syncArgs, so users
   can enable lookup() support permanently instead of passing the flag on every
   invocation. CLI --template-args overrides (does not merge with) the default.
   Resolved via effectiveTemplateArgs, wired into the chartify, flagsForTemplate,
   and appendExtraDiffFlags paths.

5. Minor: capitalize --template-args help text to match surrounding flags;
   document helmDefaults.templateArgs precedence in docs/cli.md.

Signed-off-by: yxxhero <aiopsclub@163.com>

* test: cover helmDefaults->chartify composition; fix helm helm-diff typo

Address remaining review nits on #2666:

- Add TestHelmDefaultsTemplateArgsReachesChartify, a belt-and-suspenders test
  for the processChartification composition (effectiveTemplateArgs ->
  buildChartifyTemplateArgs), closing the last unit-level coverage gap for
  helmDefaults.templateArgs reaching the chartify path.

- Fix pre-existing typo in cmd/bind_diff_flags.go: 'pass args to helm helm-diff'
  -> 'Pass args to helm-diff' (doubled 'helm', lowercase).

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: correct 'helm helm-diff' typo in apply --diff-args help text

Sibling of the bind_diff_flags.go fix; the same doubled-'helm' typo and
lowercase help existed in cmd/apply.go's --diff-args registration, leaving
the apply and diff/doctor help strings inconsistent.

Signed-off-by: yxxhero <aiopsclub@163.com>

* docs: add helmDefaults.templateArgs to configuration reference

The complete helmfile.yaml schema in docs/configuration.md documents
diffArgs and syncArgs under helmDefaults but was missing the new
templateArgs field added in #2666. Add it beside syncArgs for
discoverability, noting the --template-args CLI override.

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-06-28 12:19:33 +08:00
yxxhero 0cf330611e fix: clean up chartify temp directories after helm operations (#2668)
fix: clean up chartify temp directories after helm operations (#1799)

Chartify creates temporary output directories (e.g. /tmp/chartify<random>/)
during chart preparation for commands like build, template, and diff. These
directories were never tracked for cleanup, causing disk space to accumulate
over time with thousands of orphaned chartify* folders.

The existing clean() closure in PrepareChartify only removed generated values
files, not the chartify output directory itself. The chartified chart must
survive until all helm operations complete, so it could not be removed during
chart preparation.

This change:
- Adds chartifyTempDirTracker to HelmState (pointer-based to avoid copy-lock
  issues from HelmState being copied in several places)
- Tracks chartify output dirs via addChartifyTempDir() after c.Chartify()
  succeeds in processChartification
- Cleans them up via CleanupChartifyTempDirs() in WithPreparedCharts after
  all helm operations complete
- Also removes empty parent temp directories (e.g. /tmp/chartify<random>/)

Fixes #1799

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-06-28 11:55:31 +08:00
yxxhero 76613bd0bb fix: serialize concurrent helm operations for same chart to fix #768 (#2662)
When multiple releases in a helmfile use the same remote chart, concurrent
helm upgrade/diff calls race on helm's internal repository cache file rename,
causing intermittent 'cannot rename: Access is denied' errors on Windows.

This fix introduces two layers of serialization:

1. withChartOperationLock (operation-level): wraps SyncRelease/DiffRelease
   calls with a per-chart+version mutex. Only applies to remote charts
   (release.ChartPath is empty); local/pre-fetched/OCI charts bypass the
   lock entirely. Different charts remain fully parallel.

2. Per-chart+version download mutex in forcedDownloadChart/getOCIChart
   (download-level): uses double-check locking to ensure only one
   helm fetch runs per unique chart+version within a process.

The fix does NOT change chart paths passed to helm, preserving backward
compatibility with all existing behavior and tests.

Trade-off: same-chart releases are fully serialized (the entire helm
operation including deployment, not just download). This is unavoidable
without pre-fetching because helm downloads and deploys atomically.
Releases with different charts are unaffected.

Fixes #768

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-06-25 14:37:04 +08:00
yxxhero 08d70bc9a7 fix: ensure OCI charts are prepared for needed releases with --include-needs (#2663)
fix: ensure OCI charts are prepared for needed releases with --include-needs (#923)

When using --include-needs with a selector, releases included via needs
must have their charts prepared (pulled/exported) before diff/sync/apply
can process them. The core fix (ChartPrepareOptions.IncludeTransitiveNeeds
= c.IncludeNeeds()) was already in place, but ForEachState calls in
Diff/Template/Lint/Unittest/Sync/Apply still passed c.IncludeTransitiveNeeds()
instead of c.IncludeNeeds(), creating an inconsistency that would resurface
if SetFilter(true) were ever added.

Changes:
- Use c.IncludeNeeds() in ForEachState for all commands supporting
  --include-needs (Diff, Template, Lint, Unittest, Sync, Apply, Doctor)
- Doctor is the only command with SetFilter(true), so this fixes a real
  bug: helmfile doctor --include-needs was silently ignored for filtering
- Add explanatory doc comment on ForEachState parameter semantics
- Enhance exectest.Helm.ChartPull to create minimal chart files and track
  pulls, enabling OCI chart testing
- Add resetChartCacheForTest() for test isolation from global chart cache
- Add regression tests for issue #923

Signed-off-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
Signed-off-by: yxxhero <aiopsclub@163.com>
2026-06-25 14:36:50 +08:00
9e66f55d23 fix: resolve symlinked plugin directories in GetPluginVersion (#2661)
In nix/devbox environments, helm plugin directories are typically
symlinks into the Nix store. GetPluginVersion used entry.IsDir()
which does not follow symlinks, causing the plugin to be reported
as not installed. Follow symlinks with os.Stat before skipping
non-directory entries.

Signed-off-by: Shane Starcher <shane.starcher@gmail.com>
Co-authored-by: Shane Starcher <shane.starcher@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-23 06:59:18 +08:00
yxxhero 9b943adc9e feat: add helmfile doctor command for AI-assisted diff analysis (#2660)
* feat: add `helmfile doctor` command for AI-assisted diff analysis

`helmfile doctor` runs `helmfile diff` and asks an OpenAI-compatible LLM to
summarize the changes and flag risks (data loss, security exposure, breaking
changes, downtime, performance, best-practice issues).

Key design decisions:
- When no LLM is configured, doctor is equivalent to `helmfile diff` with
  one exception: --show-secrets is always forced off (secrets never reach
  stdout, even without an LLM).
- Secrets are ALWAYS redacted via two layers: (1) ShowSecrets() forced to
  false so helm-diff emits <REDACTED> placeholders; (2) a defense-in-depth
  text redactor strips residual secret-looking content (Secret YAML blocks,
  sensitive key/value lines, base64 blobs, JWT tokens) before LLM transmission.
- LLM configuration precedence: env (HELMFILE_LLM_*) < helmfile.yaml (llm:)
  < CLI flags (--llm-*).
- Supports any OpenAI-compatible backend (OpenAI, Azure, One-API, LiteLLM,
  Ollama, etc.) with automatic response_format fallback for backends that
  don't support JSON mode.
- Prompt injection defense: release names and environment values are
  JSON-encoded before insertion into the LLM prompt.
- Exit codes: 0 (success/low-risk), 2 (high-risk gate, bypass with --force),
  1 (other errors). Helm-diff's 'detected changes' exit-2 is swallowed.

New packages:
- pkg/agent/llm: OpenAI-compatible client with JSON response parsing, mock
  client for testing, prompt builder with injection defense.
- pkg/agent/doctor: secret redactor (state machine + regex), report renderer
  (markdown + JSON), config resolver (env < yaml < flag merge).

Testing: 70+ unit tests covering redaction patterns, prompt injection,
response_format fallback, JSON parsing, yaml roundtrip, concurrency safety,
panic recovery, and error propagation. go test -race passes.

Documentation: full doctor section in docs/cli.md, llm: block reference in
docs/configuration.md, updated skills/helmfile for AI agents.

Signed-off-by: yxxhero <aiopsclub@163.com>

* docs: fix doctor equivalence wording per PR review

Per review feedback (PR #2660): the docs claimed doctor is 'equivalent to
helmfile diff — same flags, same output, same exit codes' in the unconfigured
path, but this over-promises because:

  1. doctor --output is the report format (not helm-diff's output format)
  2. helm-diff's --output is exposed as --diff-output in doctor
  3. --show-secrets is silently ignored

Updated all three locations (cli.md, cmd/doctor.go Long + godoc, pkg/app/doctor.go
godoc) to say 'falls back to helmfile diff with --show-secrets forced off' and
explicitly note the --output / --diff-output flag difference.

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-06-22 16:52:35 +08:00
yxxheroandRoman Mykhailiuk 9d3a9ad6c7 feat: parallel kubedog tracking with progress printer and safety valves (#2654)
* feat: parallel kubedog tracking with progress printer and safety valves

Rework kubedog integration so resource tracking runs in parallel with
helm upgrade/install, giving live progress output and recovering from
known helm/kubedog wedge conditions.

Core:
- kubedogTrackingHandle runs tracking in a background goroutine alongside
  the helm subprocess (startBackgroundKubedogTracking); helm output is
  buffered and replayed as a single block so it no longer interleaves
  with progress ticks.
- Capture UID+generation baselines before handing off to helm so each
  tracker waits until the resource actually changes (freshness gate).

Progress printer (pkg/kubedog/printer.go):
- Styled, auto-sized progress table with a heartbeat flusher, child (pod)
  status roll-up, pre-ready pod-phase handling, multi-namespace support,
  and optional color. PreviewBreakdown summarizes kept/filtered resources.

Safety valves (verify cluster state via the live API):
- Tracker-race valve (always on): when helm succeeds but a dyntracker
  goroutine is wedged, poll the API and cancel the tracker so wait()
  returns success instead of blocking until --track-timeout.
- Helm-stuck killer (opt-in via helmStuckGrace): if the cluster stays
  converged while helm v4's hook waiter is wedged, SIGINT the helm
  subprocess to recover.
- Failure watchdog (pkg/kubedog/watchdog.go): surface failing pods that
  never made it into dyntracker's resource graph.

Options: trackFailedLogs, helmStuckGrace, trackTimeout, color
(Color/NoColor), and resource filtering (trackKinds/skipKinds/
trackResources). PersistentVolumeClaim support in resource classification.

Signed-off-by: Roman Mykhailiuk <romanm@cybellum.com>

* test: add unit tests for kubedog tracking

Cover the progress printer, resource classification, the failure
watchdog, helm-output trimming/dedup, the release hard-timeout helper,
and the color/track option plumbing.

Signed-off-by: Roman Mykhailiuk <romanm@cybellum.com>

* test: update golden logs, e2e snapshots, and values-id fixtures

Refresh pkg/app testapply/testdestroy golden logs, e2e template
snapshots, and the TestGenerateID values-id golden hashes for the new
kubedog progress output and the merged release struct layout.

Signed-off-by: Roman Mykhailiuk <romanm@cybellum.com>

* refactor: remove dead kubedog display code and dedupe tracker setup

Deep-review pass on the parallel kubedog tracking changes:

- Remove pkg/kubedog/display.go (308 lines) and display_test.go (453 lines).
  These rendered progress for the legacy per-kind trackers that this PR
  replaces; in the merged tree every function is unreferenced outside its
  own tests. The new progressPrinter (printer.go) supersedes them.
  TestMain (color.ForceColor for deterministic ANSI in tests) is preserved
  in a new main_test.go.
- Dedupe trackWithKubedog: the post-helm fallback rebuilt the exact same
  tracker options as buildReleaseTracker. Reuse buildReleaseTracker instead,
  dropping ~40 lines of duplicated timeout/log/filter/tracker construction.

No behavior change; build, go vet, golangci-lint, and the kubedog/state
unit tests all pass.

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: stop waitForFreshness busy-looping the API after helm finishes

Once upstreamDoneCh closes it is always ready, so the select in
waitForFreshness stopped blocking on the ticker and re-ran probe() (a
live GET) as fast as the round-trip allowed for the whole 3s grace
window — hammering the API server once per tracked resource.

Track a local view of the channel and nil it out on first delivery so
the first hit records the timestamp (one fast retry, as intended) and
all subsequent polls are ticker-throttled. Functional behavior is
unchanged: return nil when fresh, errUpstreamDoneNoChange after grace.

Signed-off-by: yxxhero <aiopsclub@163.com>

* test: drop trailing blank line from helm4 OCI pull snapshots

The trailing-newline trim in helmexec.info() removes the blank line helm
prints after the OCI chart "Digest:" line. The helm3 (output.yaml)
snapshots never captured that line, but the helm4 (output-helm4.yaml)
snapshots for oci_chart_pull{,_direct,_once,_once2} and
issue_473_oci_chart_url_fetch still expected it, so they failed under
helm 4. Remove the blank line so the snapshots match the trimmed output.

Signed-off-by: yxxhero <aiopsclub@163.com>

* test: refresh diff-args integration goldens for styled release headers

DisplayAffectedReleases now emits a styled "========== Updated Releases
==========" header (matching the app/e2e goldens already updated by this
PR) and helmexec.info() trims the trailing blank after helm's install
status. Update the diff-args apply-stderr{,-helm4} and apply-live-
stderr{,-helm4} goldens accordingly so they match the actual stderr.

Signed-off-by: yxxhero <aiopsclub@163.com>

* test: drop trimmed blank line from v1-subhelmfile template golden

The trailing-newline trim in helmexec.info() removes the blank line helm
prints after '"incubator" has been added to your repositories'. Update
the v1-subhelmfile-multi-bases-with-array-values result and result-live
goldens so the template stdout comparison matches.

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: Roman Mykhailiuk <romanm@cybellum.com>
Signed-off-by: yxxhero <aiopsclub@163.com>
Co-authored-by: Roman Mykhailiuk <romanm@cybellum.com>
2026-06-21 13:43:17 +08:00
yxxhero e12c875863 build(deps): bump helm from v4.2.1 to v4.2.2 (#2651)
* build(deps): bump helm from v4.2.1 to v4.2.2

Signed-off-by: yxxhero <aiopsclub@163.com>

* build(deps): update helm v4.2.2 SHA256 checksums

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-06-19 07:06:30 +08:00
yxxhero c5eebc24bd fix: helmfile deps broken for OCI charts with underscores in path (#2648)
fix: helmfile deps broken for OCI charts with underscores in path (#954)

For OCI charts with multi-segment paths (e.g., myrepo/path_with_underscores/example),
helmfile was putting the full path as the dependency name in the generated Chart.yaml.
Helm then reconstructed the OCI reference using this name, and underscores in the
path caused issues with helm's OCI reference handling during dependency update.

Fix: move the chart path prefix into the repository URL and use only the chart
basename as the dependency name, matching Helm's recommended Chart.yaml format
for OCI dependencies:

  # Before (broken):
  dependencies:
  - name: path_with_underscores/example
    repository: oci://harbor.custom.com

  # After (fixed):
  dependencies:
  - name: example
    repository: oci://harbor.custom.com/path_with_underscores

The resulting OCI reference is identical, but the dependency name is now clean.

Includes backward-compatibility fallback for old lock files that used the full
path as the dependency name.

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-06-18 22:51:44 +08:00
yxxhero bbceb05b31 feat: add helm 4 --server-side flag support for diff and bump helm-diff to v3.15.10 (#2645)
Add appendServerSideFlagsForDiff to validate helm-diff plugin version
(v3.15.10+) before passing the --server-side flag to helm diff upgrade.
Extract resolveServerSideValue helper to share precedence logic between
upgrade and diff paths. Bump helm-diff recommended version to v3.15.10
across Dockerfiles, CI, and integration scripts.

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-06-17 09:07:44 +08:00
yxxhero 6191f821eb fix: restore s3:: vhost-style remote source support (#2643) (#2644)
go-getter v2 (used since v1.4) removed its built-in S3 getter, so helmfile's own AWS-SDK-v2 S3Getter was added to compensate. However the routing only handled the s3://bucket/key form (Getter==normal, Scheme==s3); the go-getter forced-getter vhost form s3::https://bucket.s3.region.amazonaws.com/key fell through to go-getter v2, which can no longer download S3 at all, producing 'error downloading'.

This restores 1.2.x behavior by:

- routing u.Getter==s3 URLs to the built-in S3Getter

- extending ParseS3Url to parse vhost/path-style amazonaws.com URLs (region/bucket/key), modeled on go-getter v1

- stripping the helmfile @<file> selector before deriving the S3 key

- auto-decompressing archive objects (tar.gz/zip/...) via go-getter v2 decompressors so the @<file> selector resolves inside a tarball, as go-getter v1 did

- cleaning up the cache dir on download/decompress failure (matching the GoGetter branch) and avoiding a nil-response panic in GetObject error handling

Fixes #2643

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-06-17 09:07:30 +08:00
yxxhero a94fdf4194 feat: add support for helm 4 --server-side upgrade flag (#2641)
* feat: add support for helm 4 --server-side upgrade flag

Add support for the helm 4 upgrade flag --server-side which accepts
"true", "false", or "auto" (default "auto"). This allows users to
explicitly control server-side apply behavior, which is needed for
releases originally installed with Helm 3 and being managed with Helm 4.

The flag can be configured via:
- CLI: --server-side flag on sync, apply, and diff commands
- helmDefaults.serverSide in helmfile.yaml
- releases[].serverSide per-release override

Precedence: release-level > CLI flag > helmDefaults.
Errors are returned when serverSide is set but running Helm 3, or when
an invalid value is provided.

Closes #2640

Signed-off-by: yxxhero <aiopsclub@163.com>

* test: update TestGenerateID expected hashes for new ServerSide field

Adding ServerSide *string to ReleaseSpec changes spew's %#v output and
shifts the FNV hash used by generateValuesID. Update the hard-coded want
values to the new deterministic hashes.

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-06-16 08:24:47 +08:00
yxxhero 047fdcd17d build(deps): bump helm-diff to v3.15.9 (#2642)
Update helm-diff plugin version from v3.15.8 to v3.15.9 across
Dockerfiles, recommended version constant, CI matrix, and
integration test default.

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-06-16 08:24:34 +08:00
yxxheroandcopilot-swe-agent[bot] 38e27ee439 fix: include release values in .Values for patch template rendering (#2556)
* fix: include release values in .Values for jsonPatches/strategicMergePatches/transformers gotmpl rendering

Before this fix, .Values in patch template files only contained environment
values, not the release's own values. This meant references like
{{ .Values.ingress.enabled }} would fail when ingress.enabled was set in
the release's values: file rather than environment values.

Now patch gotmpl files see .Values as merged(environment values, release values),
matching user expectations that values defined in the release should be
accessible in conditional patches.

Fixes #1904

Signed-off-by: yxxhero <aiopsclub@163.com>

* test: add more tests for resolveReleaseValues, renderValuesFileToBytesWithData, and generateTemporaryReleaseValuesFilesWithData

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/5da5c9d8-7464-4146-84b5-1433ed6193f3

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* test: simplify newTestHelmStateWithFiles by removing empty cleanup func

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/5da5c9d8-7464-4146-84b5-1433ed6193f3

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* fix: remove always-constant basePath param from newTestHelmStateWithFiles to fix unparam lint error

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/b4a669cb-692c-4ca6-a68b-1b04a062b989

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* fix: address c66017c review comments - error messages, defer-in-loop, map normalization, test cleanup

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/44988dd8-1c67-465b-995c-80525a24eb93

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* refactor: extract generateTemporaryReleaseValuesFilesCore to eliminate duplication; fix temp dir leaks in tests

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/b254ddda-aa95-4e2f-8dd9-1ce4c40eedb6

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* fix: remove rendered content from debug log; extract prepareReleaseValuesEntries shared helper

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/321c6ba5-f835-4afd-be5e-ee790bc6b4a5

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* fix: compute mergedReleaseTemplateData lazily in PrepareChartify

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/100b7974-3268-4dc8-be21-12bd82aa2dbb

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* fix: normalize nested YAML keys in resolveReleaseValues via CastKeysToStrings

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/d0129d85-9c7d-4a31-966e-fc0b05b74867

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* fix: use %w for error wrapping in release values resolution

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-15 16:31:46 +08:00
toyamagu-2021 (gussan) 6cf02956c2 Add App.WithFileSystem (#2638)
Signed-off-by: toyamagu-2021 <toyamagu2021@gmail.com>
2026-06-14 09:27:24 +08:00
Copilotandyxxhero 655399a03b Bump Helm support to 4.2.1 and 3.21.1 (#2635)
* chore: bump helm release pins

* chore: align helm module metadata

* chore: finalize helm patch bumps

* fix: add --plain-http flag for Helm 3.21+ OCI push in tests

Helm 3.21.1 introduced stricter security checks that reject HTTP
scheme downgrades when pushing to OCI registries, with the error:
  "blob upload Location downgrades scheme from https"

Previously only Helm 4 required --plain-http for HTTP-only OCI
registries. Now Helm 3.21+ also requires this flag.

Add a new requiresPlainHTTPForOCI() helper that returns true for
both Helm 4.x and Helm 3.21+, and use it in execHelmPush() instead
of isHelm4().

* fix: safe fallback in requiresPlainHTTPForOCI when version detection fails

Default to true (require --plain-http) when helm version detection
fails, since any Helm version that supports helm push also supports
the --plain-http flag. This avoids the inconsistent HELMFILE_HELM4
env var fallback which only covered Helm 4.

* fix: update snapshot tests for Helm 4.2.1 OCI pull output

Helm 4.2.1 now outputs additional 'Pulled:' and 'Digest: sha256:...'
lines after each OCI chart pull. The SHA256 digest is non-deterministic
because helm packages include build timestamps, so normalize it with
a regex placeholder.

- Add ociDigestRegex to normalize non-deterministic OCI digest values
- Create output-helm4.yaml for 5 tests that lacked Helm 4 snapshots
- Update output-helm4.yaml for oci_need and postrenderer to include
  the new Pulled/Digest lines from Helm dependency pull operations

* fix: update ociDigestRegex to match empty digest in Helm 4.2.1 OCI pull output

Helm 4.2.1 outputs "Digest: sha256:" (empty hash) when pulling OCI charts.
The regex required at least one hex char ([0-9a-f]+), so it did not match
and the digest was not normalized to $DIGEST in snapshot tests.

Also fix the replacement string: Go regex ReplaceAllString interprets $DIGEST
as a capture group reference (resolving to empty). Use $$DIGEST to produce
a literal $DIGEST in the output.

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yxxhero <aiopsclub@163.com>
2026-06-13 16:50:56 +08:00