This is a follow-up to #1278 / #1281, which introduced the RetryOnFailure strategy for .spec.install.strategy and .spec.upgrade.strategy.
Today, RetryOnFailure retries a failed install/upgrade indefinitely at the fixed .retryInterval, with no way to bound the number of attempts. Unlike the RemediateOnFailure strategy — which uses .remediation.retries (0 = no retries, negative = infinite) and transitions the HelmRelease to Stalled with reason RetriesExceeded once retries are exhausted — a HelmRelease using RetryOnFailure can never reach a terminal Stalled state. For a release that is failing for a permanent reason (bad values, invalid chart, misconfigured cluster), this means the controller reconciles forever with no signal that human intervention is needed.
This has real operational cost: #1359 showed that unbounded RetryOnFailure loops were already filling up .status.history (and etcd) before that specific growth bug was fixed — a symptom of retrying forever with no upper bound or off-ramp.
Proposal
Add an optional .retries field to .spec.install.strategy and .spec.upgrade.strategy (alongside the existing .retryInterval), with the same semantics already used by .remediation.retries elsewhere in this API:
- A non-negative value caps the number of retry attempts before the controller stops and marks the HelmRelease
Stalled (reason RetriesExceeded, consistent with how RemediateOnFailure already reports this).
- A negative value (default:
-1) preserves today's behavior of infinite retries, so this is fully backward compatible.
API Changes
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
spec:
install:
strategy:
name: RetryOnFailure
retryInterval: 5m
retries: -1 # defaults to -1 (infinite, current behavior); a non-negative value caps attempts before Stalled
upgrade:
strategy:
name: RetryOnFailure
retryInterval: 5m
retries: -1 # same semantics as install.strategy.retries
Note on naming: I've named this field .retries to match the existing .remediation.retries convention (negative = infinite). Since .remediation.retries is documented as ignored when the strategy is RetryOnFailure, open to alternative names (e.g. maxRetries) if maintainers feel a second field literally named retries in a different location would be confusing — happy to go either way.
Why this matters
This brings parity between RetryOnFailure and RemediateOnFailure: both strategies would support bounded retries with a consistent, discoverable terminal state (Stalled / RetriesExceeded), instead of RetryOnFailure being the only strategy that can never surface "this needs attention."
This is a follow-up to #1278 / #1281, which introduced the
RetryOnFailurestrategy for.spec.install.strategyand.spec.upgrade.strategy.Today,
RetryOnFailureretries a failed install/upgrade indefinitely at the fixed.retryInterval, with no way to bound the number of attempts. Unlike theRemediateOnFailurestrategy — which uses.remediation.retries(0 = no retries, negative = infinite) and transitions the HelmRelease toStalledwith reasonRetriesExceededonce retries are exhausted — a HelmRelease usingRetryOnFailurecan never reach a terminalStalledstate. For a release that is failing for a permanent reason (bad values, invalid chart, misconfigured cluster), this means the controller reconciles forever with no signal that human intervention is needed.This has real operational cost: #1359 showed that unbounded
RetryOnFailureloops were already filling up.status.history(and etcd) before that specific growth bug was fixed — a symptom of retrying forever with no upper bound or off-ramp.Proposal
Add an optional
.retriesfield to.spec.install.strategyand.spec.upgrade.strategy(alongside the existing.retryInterval), with the same semantics already used by.remediation.retrieselsewhere in this API:Stalled(reasonRetriesExceeded, consistent with howRemediateOnFailurealready reports this).-1) preserves today's behavior of infinite retries, so this is fully backward compatible.API Changes
Note on naming: I've named this field
.retriesto match the existing.remediation.retriesconvention (negative = infinite). Since.remediation.retriesis documented as ignored when the strategy isRetryOnFailure, open to alternative names (e.g.maxRetries) if maintainers feel a second field literally namedretriesin a different location would be confusing — happy to go either way.Why this matters
This brings parity between
RetryOnFailureandRemediateOnFailure: both strategies would support bounded retries with a consistent, discoverable terminal state (Stalled/RetriesExceeded), instead ofRetryOnFailurebeing the only strategy that can never surface "this needs attention."