helmfile apply: Diff Then Sync Releases
helmfile apply computes a diff and upgrades only the releases that actually changed, making it the efficient deploy command.
apply is the workhorse: it diffs every release in helmfile.yaml and applies just the ones with changes, so it is fast and noise-free in CI.
What it does
helmfile apply renders all releases, diffs them against the cluster using helm-diff, and runs helm upgrade --install only for releases that differ. Unchanged releases are skipped, so repeated runs are cheap. It needs the helm-diff plugin.
Common usage
helmfile -e production apply
# apply only a subset by label
helmfile apply --selector tier=frontend
# skip the diff for brand-new installs (faster first deploy)
helmfile apply --skip-diff-on-installOptions
| Flag | What it does |
|---|---|
| -e, --environment <name> | Select an environment block |
| --selector key=value | Apply only releases matching the label |
| --skip-diff-on-install | Do not diff releases that are not yet installed |
| --suppress-secrets | Hide secret values in the diff output |
| --wait | Pass helm --wait so it blocks on readiness |
In CI
apply is preferred over sync in pipelines because it only touches changed releases, keeping logs clean and runs fast. Pin the environment with -e so the right values file loads. Add --wait when you want the job to fail on an unhealthy rollout.
Common errors in CI
"Error: unknown command \"diff\" for \"helm\"" means the helm-diff plugin is missing; run helmfile init --force. "in ./helmfile.yaml: failed to read ... environment values" means an environment or values file is missing. "Error: UPGRADE FAILED: another operation (install/upgrade/rollback) is in progress" means a prior helm operation is stuck; roll it back or delete the pending release.
Using this in CI
A runner has no kubeconfig, no cached context, and no interactive auth. Every kubectl invocation in CI needs the context supplied explicitly, and most confusing CI failures here are the command running against the wrong cluster or no cluster at all.
# never rely on the ambient context on a runner
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pods
# confirm what you are actually connected to before mutating anything
kubectl config current-context
kubectl cluster-info
# fail fast instead of hanging on an unreachable API server
kubectl --request-timeout=30s get nodes