Guidance for AI coding assistants working in fluxcd/helm-controller. Read this file before making changes.
These rules come from fluxcd/flux2/CONTRIBUTING.md and apply to every Flux repository.
- Do not add
Signed-off-byorCo-authored-bytrailers with your agent name. Only a human can legally certify the DCO. - Disclose AI assistance with an
Assisted-bytrailer naming your agent and model:Thegit commit -s -m "Add support for X" --trailer "Assisted-by: <agent-name>/<model-id>"
-sflag adds the human'sSigned-off-byfrom their git config — do not remove it. - Commit message format: Subject in imperative mood ("Add feature X" instead of "Adding feature X"), capitalized, no trailing period, ≤50 characters. Body wrapped at 72 columns, explaining what and why. No
@mentionsor#123issue references in the commit — put those in the PR description. - Trim verbiage: in PR descriptions, commit messages, and code comments. No marketing prose, no restating the diff, no emojis.
- Rebase, don't merge: Never merge
maininto the feature branch; rebase onto the latestmainand push with--force-with-lease. Squash before merge when asked. - Pre-PR gate:
make tidy fmt vet && make testmust pass and the working tree must be clean after codegen. Commit regenerated files in the same PR. - Flux is GA: Backward compatibility is mandatory. Breaking changes to CRD fields, status, CLI flags, metrics, or observable behavior will be rejected. Design additive changes and keep older API versions round-tripping.
- Copyright: All new
.gofiles must begin with the boilerplate fromhack/boilerplate.go.txt(Apache 2.0). Update the year to the current year when copying. - Spec docs: New features and API changes must be documented in
docs/spec/v2/helmreleases.md. Update it in the same PR that introduces the change. - Tests: New features, improvements and fixes must have test coverage. Add unit tests in
internal/controller/*_test.goand otherinternal/*packages as appropriate. Follow the existing patterns for test organization, fixtures, and assertions. Run tests locally before pushing.
Before submitting code, review your changes for the following:
- No secrets in logs or events. Never surface auth tokens, passwords, kubeconfig data, or Helm release secret contents in error messages, conditions, events, or log lines.
- No unchecked I/O. Close HTTP response bodies, file handles, and chart archive readers in
deferstatements. Check and propagate errors fromio.Copy,os.Remove,os.Rename. - No path traversal. Validate and sanitize file paths extracted from chart archives. Never
filepath.Joinwith untrusted components without validation. - No unbounded reads. Use
io.LimitReaderwhen reading from network or archive sources. Respect existing size limits; do not introduce new reads without bounds. - No direct Helm calls from reconcile code. All Helm SDK interactions go through
internal/action/ConfigFactory, which wires the observing storage driver. Bypassing it breaks history tracking and remediation. - No hardcoded defaults for security settings. TLS verification must remain enabled by default; impersonation and cross-namespace ref restrictions must be honored.
- Helm secret ownership. Helm release secrets (
sh.helm.release.v1.*) are owned by Helm, not the controller. Do not add logic that creates, modifies, or deletes them outside of Helm operations. - Error handling. Wrap errors with
%wfor chain inspection. Do not swallow errors silently. Return actionable error messages that help users diagnose the issue without leaking internal state. - Resource cleanup. Ensure temporary files, directories, and fetched chart artifacts are cleaned up on all code paths (success and error). Use
deferandt.TempDir()in tests. - Concurrency safety. Do not introduce shared mutable state without synchronization. Reconcilers run concurrently; per-object work must be isolated.
- No panics. Never use
panicin runtime code paths. Return errors and let the reconciler handle them gracefully. - Minimal surface. Keep new exported APIs, flags, and environment variables to the minimum needed. Every export is a backward-compatibility commitment.
helm-controller is a core component of the Flux GitOps Toolkit. It reconciles HelmRelease objects into real Helm releases on a target cluster. It depends on source-controller for chart delivery: either a HelmChart (generated from a HelmRepository, GitRepository, or Bucket) or an OCIRepository referenced through spec.chartRef. The controller drives the full Helm lifecycle (install, upgrade, test, rollback, uninstall) via the embedded Helm SDK, tracks its own release history in HelmRelease.status, and can detect and correct in-cluster drift against the manifest persisted in Helm storage.
main.go— controller entrypoint: flag parsing, feature gates, manager andHelmReleaseReconcilerwiring. The only binary.api/— separate Go module (github.com/fluxcd/helm-controller/api) with the public CRD types. Consumed by downstream projects.api/v2— current stable version (helm.toolkit.fluxcd.io/v2). Types:HelmRelease,HelmChartTemplate,DriftDetection,Snapshots, etc.api/v2beta2,api/v2beta1— prior versions retained for conversion and backward compatibility. Do not add new fields there.
config/— Kustomize manifests:crd/bases(generated CRDs),manager,rbac,default,samples,testdata. CRDs here are generated — never hand-edit.hack/—boilerplate.go.txtlicense header,api-docs/config forgen-crd-api-reference-docs.docs/— user-facing spec (docs/spec/v2/helmreleases.md), generated API reference (docs/api/v2/helm.md), and internal notes (docs/internal/).internal/— non-exported controller logic.controller/—HelmReleaseReconciler(top-levelReconcile, indexers, manager setup, envtest suite tests).reconcile/— the release state machine.AtomicReleaseis the top-levelActionReconcilerthat loops: computeReleaseStateviastate.go, pick the next action (Install,Upgrade,Test,RollbackRemediation,UninstallRemediation,Unlock,CorrectClusterDrift), run it, patch, and loop until done, stalled, or a retry is needed.helmchart_template.goreconciles theHelmChartsource object fromspec.chart.action/— thin wrappers around the Helm SDK (install.go,upgrade.go,rollback.go,uninstall.go,test.go,verify.go,diff.go,wait.go) plus theConfigFactorythat builds Helmaction.Configurationwith the observing storage driver. Helm semantics live here.release/— release name derivation, config/chart digest calculation, and storage-snapshot observation helpers.storage/—ObserverHelm storage driver: wraps the upstream driver so writes to Helm secrets/configmaps are captured intostatus.Historyregardless of whether the Helm action returned an error.postrender/— built-in Kustomize post-renderer (strategic merge, JSON6902, image patches) plusCommonRendererforspec.commonMetadata.diff/,cmp/,digest/— drift detection helpers and digest algorithms (sha256,sha384,sha512,blake3) used for snapshotting state.loader/— chart artifact fetch from source-controller (HTTP, retries).kube/,acl/,predicates/,inventory/,errors/,strings/,features/,oomwatch/,testutil/— small utility packages.controller_test/— shared end-to-end style fixtures.
- Group:
helm.toolkit.fluxcd.io. Kind:HelmRelease(the only CRD owned by this controller). - Served versions:
v2(storage, current),v2beta2,v2beta1. Finalizer:finalizers.fluxcd.io.HelmReleaseKindis exported fromapi/v2. - Types in
api/v2/helmrelease_types.go. Condition type constants (Released,TestSuccess,Remediated,Drifted, plusReady/Reconciling/Stalledfromfluxcd/pkg/apis/meta) are inapi/v2/condition_types.go. Release snapshots insnapshot_types.go. - The CRD manifest at
config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yamlis generated from kubebuilder markers. Edit the Go types and regenerate — never edit the YAML by hand.
All workflows go through the root Makefile. Go version tracks go.mod. Tools (controller-gen, gen-crd-api-reference-docs, setup-envtest) auto-install into build/gobin/.
make tidy— tidy both the root andapi/modules.make fmt/make vet— run in both modules.make generate—controller-gendeepcopy stubs inapi/.make manifests— regenerate CRDs and RBAC underconfig/from kubebuilder markers.make api-docs— regeneratedocs/api/v2/helm.md.make manager— buildbuild/bin/manager.make test— full suite. Depends ontidy generate fmt vet manifests api-docs install-envtest download-crd-deps; setsKUBEBUILDER_ASSETS, downloads thesource.toolkit.fluxcd.ioCRDs (HelmChart,OCIRepository,ExternalArtifact) pinned to the version ingo.mod, runsgo test ./...at the root, thengo test ./...insideapi/.make install/make run/make deploy/make dev-deploy— cluster workflows (seeDEVELOPMENT.md).make runexpects source-controller to be reachable (optionally viaSOURCE_CONTROLLER_LOCALHOST=localhost:8080).make docker-build/make docker-push— container image.
Check go.mod and the Makefile for current dependency and tool versions. After changing API types or kubebuilder markers, regenerate and commit the results:
make generate manifests api-docsGenerated files (never hand-edit):
api/*/zz_generated.deepcopy.goconfig/crd/bases/*.yamlconfig/rbac/role.yamldocs/api/v2/helm.md
Load-bearing replace in go.mod — do not remove:
helm.sh/helm/v4→github.com/fluxcd/helm/v4(Flux fork with upstream bugfixes). Do not remove without coordinating with maintainers.
The fluxcd/source-controller/api version in go.mod drives SOURCE_VER and determines which source CRDs are downloaded for envtest.
Bump fluxcd/pkg/* modules as a set — version skew breaks go.sum. Run make tidy after any bump.
- Standard
gofmt, explicit errors. All exported identifiers get doc comments, as do non-trivial unexported types and functions. - Controller structure.
HelmReleaseReconciler.Reconcilehandles suspend, dependency checks, artifact acquisition, impersonation, then delegates the Helm lifecycle toreconcile.NewAtomicRelease(...). Don't add Helm action logic to the top-level reconciler. - Release state machine. The
AtomicReleaseloop is authoritative. Any new action type must implementreconcile.ActionReconciler(Reconcile,Name,Type) and be selected byactionForStatebased on aReleaseStatusreturned byDetermineReleaseState. NewReconcilerTypeconstants go inreconcile/reconcile.go. Respect thereleaseStrategycontract so a single reconcile attempt never runs the same action twice. - Conditions. Writes go through
github.com/fluxcd/pkg/runtime/conditions. Owned conditions are listed inreconcile.OwnedConditions. The controller writesReleased,TestSuccess,Remediated,Reconciling,Stalled,Ready, andDrifted.Readyis a summary of the other owned conditions and must never be set directly outsidesummarize. - Release history.
status.Historyis aSnapshotslist populated by thestorage.Observerdriver, which wraps the real Helm driver so every persisted revision is observed even if the Helm action ultimately errored.MaxHistorydefaults to 5; truncation is handled by the observer/reconcile code, not by Helm's native history pruning. - Digests. Config (values), chart, and post-renderer digests are computed in
internal/digestandinternal/releaseusing the algorithm set by--snapshot-digest-algo(defaultsha256,blake3supported). Drift ofObservedPostRenderersDigestor the config digest forces an upgrade. - Drift detection. Configured per-HelmRelease via
spec.driftDetection.mode(disabled,warn,enabled). The old cluster-wideDetectDrift/CorrectDriftfeature gates are deprecated — prefer the spec field. Correction uses SSA throughfluxcd/pkg/ssa.--override-managerlets operators declare field managers whose changes should be overridden. - Atomic / wait semantics. Helm's
--atomicis not used directly; atomicity is implemented via the release state machine and remediation strategies. Waits usekstatuspolling (action/wait.go), or Helm's built-in wait whenspec.waitStrategyselects it. CELhealthCheckExprsrun inside the poller strategy only. - Remediation & retries.
spec.install.remediation/spec.upgrade.remediationdriveRemediateOnFailure; theDefaultToRetryOnFailurefeature gate swaps the default toRetryOnFailure. Branch offreq.Object.GetActiveRemediation()andGetActiveRetry(defaultToRetryOnFailure). - OCI charts. Consumed via
spec.chartRefpointing at anOCIRepository.DisableChartDigestTrackingdisables the upgrade-on-digest-change behavior for identical chart versions. - Post-renderers. Built-in
kustomize, pluscommonMetadataand user-definedpostRenderers. Implementations ininternal/postrender; new renderers plug intoBuildPostRenderers. - ACL. Cross-namespace source references are allowed by default but can be disabled with
--no-cross-namespace-refs. Theinternal/aclpackage centralizes this. - Test seam.
ConfigFactoryininternal/actionis the test seam for swapping Helm configuration.
- Unit and controller tests use
sigs.k8s.io/controller-runtime/envtestvia the Fluxpkg/runtime/testenvwrapper.make install-envtestdownloads control-plane binaries intobuild/testbin/;make testwiresKUBEBUILDER_ASSETSautomatically. make download-crd-depsfetches source-controller CRDs intobuild/config/crd/bases/. The controller suite loads bothbuild/...andconfig/crd/basesas CRD sources.- Integration tests live in
internal/controller/*_test.gowithTestMaininsuite_test.gosetting uptestEnvand atestserver.HTTPServerthat serves fake chart artifacts. - Reconcile/action-level tests live under
internal/reconcile/andinternal/action/; they exerciseActionReconcilerimplementations against a fake Helm storage and a mockedConfigFactory. - Run a single test:
make test GO_TEST_ARGS='-run TestAtomicRelease_Reconcile'. - The
api/module has its own small test suite;make testruns it after the main module.
- The release state machine is the source of truth. A naive "just call Helm upgrade" change inside
HelmReleaseReconcilerwill break remediation, history tracking, and drift correction. ExtendAtomicReleaseandDetermineReleaseStateinstead. - The observing storage driver (
internal/storage.Observer) is why partial Helm failures still show up instatus.History. status.Historyis capped byspec.maxHistory(default 5). The controller, not Helm, owns truncation. SettingmaxHistory: 0means unlimited.- On uninstall the controller runs Helm uninstall; leftover
sh.helm.release.v1.*secrets typically indicate a user-initiated partial cleanup and need manual intervention. spec.storageNamespacedefaults to the HelmRelease's namespace but can diverge fromspec.targetNamespace. Code that touches Helm storage must useHelmRelease.GetStorageNamespace(), not assume it equals the target.- CRD-heavy charts (kube-prometheus-stack and similar) interact poorly with
persistentClient: truewhen charts create CRDs outside Helm's CRD hooks. The field defaults to true; leave it that way unless you understand the trade-off. - Drift detection compares the Helm storage manifest against live objects via SSA dry-run. Charts that mutate their own rendered manifests in-cluster (webhooks, operators) loop forever unless their field managers are added to
--override-manageror their objects are excluded viaspec.driftDetection.ignore. AdoptLegacyReleases,DetectDrift, andCorrectDriftfeature gates are deprecated/ignored from v1.5.0. Do not add new code paths gated on them.api/is its own Go module. Changes to API types requirego mod tidyin both./and./api(handled bymake tidy). Downstream modules importapi/directly, so API refactors are user-visible.