</>CodeWithKarani

Helm 'has no deployed releases': recovering a broken release history

Karani GeoffreyKarani Geoffrey8 min read

The deploy pipeline has been red for twenty minutes. Every run ends the same way:

Error: UPGRADE FAILED: "checkout-api" has no deployed releases

So you do the obvious thing and roll back. Revision 41 fails. Revision 40 fails. Revision 39, same message. At some point it dawns on you that Helm is not refusing a particular revision, it is refusing all of them, and the rollback you were counting on as your safety net does not exist.

This is not a chart problem and it is not you holding Helm wrong. Both helm upgrade and helm rollback need a revision marked deployed to use as a baseline for the diff. If every revision in the history is failed or pending-*, Helm has no baseline and refuses to do anything, including the one operation that would get you out. There is still no first-class command to repair this, which is why helm/helm#4349 has been open for years.

run helm history <release> -n <ns>. If no row says deployed, rollback cannot help you. Pick the revision that matches what is actually running and mark its release Secret as deployed:

kubectl -n prod patch secret sh.helm.release.v1.checkout-api.v39 \
  --type=merge -p '{"metadata":{"labels":{"status":"deployed"}}}'

Then run helm upgrade --atomic again. If nothing from the chart is actually running, a clean helm uninstall followed by helm install is faster and safer than patching. Prevent the whole class of problem by putting --atomic on every upgrade.

The exact errors you will see

Error: UPGRADE FAILED: "checkout-api" has no deployed releases
Error: "checkout-api" has no deployed releases
Error: cannot re-use a name that is still in use

The first comes from helm upgrade, the second from helm rollback, and the third from someone trying helm install as a way out while the broken release is still registered. All three describe the same underlying state.

Why this happens: Helm's history is a set of labelled Secrets

Helm 3 has no server component. Every revision of a release is stored as a Kubernetes Secret in the release namespace, of type helm.sh/release.v1 and named sh.helm.release.v1.<release>.v<revision>. The chart, the values and the rendered manifest are gzipped and base64-encoded into the Secret's data. The bit that matters here lives outside that blob, in the Secret's labels: name, owner=helm, status and version.

When you run an upgrade, Helm asks its storage layer for the currently deployed release. That lookup is a plain label query for name=<release>, owner=helm and status=deployed. If the query returns nothing, Helm returns the "has no deployed releases" error and stops before it renders anything. Rollback takes the same path, which is why rolling back to an older number does not help: the number is not the problem, the missing label is.

Release Secrets in namespace prod sh.helm.release.v1.checkout-api.v38 status=superseded sh.helm.release.v1.checkout-api.v39 status=failed sh.helm.release.v1.checkout-api.v40 status=failed sh.helm.release.v1.checkout-api.v41 status=pending-upgrade sh.helm.release.v1.checkout-api.v42 status=failed helm upgrade asks: name=checkout-api owner=helm status=deployed 0 results Revision 38 was pruned by --history-max, or is simply older than the retention window. With no row carrying status=deployed, upgrade and rollback both refuse before they render a single template.
Rollback is not a way out of this state, because rollback uses the same lookup that is already failing.

Two things get you here. The common one is a run of failed deploys: the first upgrade fails and is marked failed, and every subsequent attempt fails the same way and adds another failed revision, pushing the last good one further down. The second is retention. The --history-max flag on helm upgrade defaults to 10, and if someone has set it to 2 or 3 to keep etcd tidy, a short burst of failures is enough to prune the only deployed revision out of existence.

The fix

Step 1: Look at the history before you touch anything

helm history checkout-api -n prod

Expected output when you are in this hole:

REVISION  UPDATED                   STATUS           CHART              APP VERSION  DESCRIPTION
39        Mon Jul 13 21:04:11 2026  failed           checkout-api-1.8.2 1.8.2        Upgrade "checkout-api" failed
40        Mon Jul 13 21:19:57 2026  failed           checkout-api-1.8.3 1.8.3        Upgrade "checkout-api" failed
41        Mon Jul 13 22:02:40 2026  pending-upgrade  checkout-api-1.8.3 1.8.3        Preparing upgrade
42        Mon Jul 13 22:31:08 2026  failed           checkout-api-1.8.4 1.8.4        Upgrade "checkout-api" failed

Read the STATUS column, not the revision numbers. If a single row says deployed, you do not have this problem and something else is wrong. If one says pending-upgrade or pending-install and you are also seeing "another operation (install/upgrade/rollback) is in progress", deal with the stuck pending release first, because the two failures often arrive together.

Step 2: Find out what is actually running

This decides which repair you use, and it is the step people skip.

kubectl -n prod get all -l app.kubernetes.io/instance=checkout-api

If that returns nothing, or only broken pods that never became ready, the release is dead and the honest fix is to remove it and install again. If it returns healthy pods serving live traffic, do not run helm uninstall. Patch instead.

Step 3: Mark one revision as deployed

List the release Secrets with their labels so you can see the state Helm sees:

kubectl -n prod get secret -l owner=helm,name=checkout-api \
  --sort-by=.metadata.creationTimestamp --show-labels

Choose the revision whose rendered manifest matches what is running now. In practice that is the last revision before the breakage started, or, if a failed upgrade did apply its changes before timing out on readiness, the most recent one. When in doubt, dump it and read it:

helm get manifest checkout-api -n prod --revision 39 | head -40

Then patch its label:

kubectl -n prod patch secret sh.helm.release.v1.checkout-api.v39 \
  --type=merge -p '{"metadata":{"labels":{"status":"deployed"}}}'

Expected output: secret/sh.helm.release.v1.checkout-api.v39 patched.

Two things to be precise about here. First, exactly one revision should carry status=deployed; if you patch several, Helm's behaviour becomes unpredictable. Second, this patch changes the label that the lookup queries, not the status recorded inside the gzipped release object in the Secret's data. That is why helm history may still print failed for the revision you just patched while helm upgrade works fine again. It is cosmetic, and it corrects itself on the next successful upgrade, which supersedes the old revisions and writes a fresh one.

Step 4: Upgrade again, with a seatbelt this time

helm upgrade checkout-api ./charts/checkout-api -n prod \
  --atomic --timeout 10m --history-max 20

--atomic waits for resources to become ready and, on failure, rolls the release back to the previous revision automatically. That rollback is what keeps a deployed revision in the history, which is precisely the property you just lost. Note that --atomic implies --wait, so a chart with a workload that never becomes ready will now fail slowly instead of failing fast. That is the trade, and it is worth it.

Step 5: The clean-slate path, for when nothing is running

helm uninstall checkout-api -n prod
helm install checkout-api ./charts/checkout-api -n prod --atomic --timeout 10m

Be very clear about what helm uninstall deletes. Everything in the release manifest, including Services (so any LoadBalancer IP is likely to change) and, depending on your chart, PersistentVolumeClaims. Read the manifest first. On a stateful chart in production this is an outage, not a repair.

There is a middle path if you must detach Helm from live resources without deleting them: delete only the release Secrets, then reinstall while letting Helm adopt the existing objects. Helm will refuse to take ownership unless each resource carries the label app.kubernetes.io/managed-by=Helm and the annotations meta.helm.sh/release-name and meta.helm.sh/release-namespace. Adding those by hand across a dozen resources is fiddly and easy to get wrong, so keep this for the case where an outage is genuinely unacceptable.

Verification

helm history checkout-api -n prod
helm status checkout-api -n prod

You want exactly one row in deployed state, at the highest revision number, and helm status reporting STATUS: deployed. Then prove the escape hatch is back before you need it:

helm rollback checkout-api --dry-run -n prod

If that plans a rollback instead of erroring, your history is healthy again. Finally, confirm only one Secret claims the deployed label:

kubectl -n prod get secret -l owner=helm,name=checkout-api,status=deployed

What people get wrong

Rolling back to progressively older revisions. The single most common reaction, and it cannot work. Rollback resolves the deployed release using the same query that is already returning nothing. Ten attempts produce ten identical errors and a lot of lost time.

Reaching for --force. On helm upgrade, --force replaces resources rather than patching them, which will happily delete and recreate a Service or fail outright on immutable fields in a StatefulSet. It does not create a deployed baseline, so it does not address this error at all.

Uninstalling on reflex. In a dev namespace, uninstall and reinstall is the right call and takes thirty seconds. In production, running it against a chart with a PVC or a LoadBalancer, it turns a recoverable metadata problem into a genuine incident. Always run step 2 first.

Retrying the failed deploy in CI. Every automatic retry appends another failed revision and pushes the last good one closer to the retention cliff. If your pipeline retries Helm upgrades, cap it at one attempt and make the second failure page a human.

Setting --history-max very low to save space. Release Secrets are small. A limit of 2 or 3 is how a bad afternoon becomes an unrecoverable history. Ten is the default for a reason; twenty is a reasonable choice for a busy service.

When it is still broken

  1. Check your storage backend. If HELM_DRIVER is set to configmap or sql, your history is not in Secrets at all and the patch above targets the wrong object. Run kubectl -n prod get configmap -l owner=helm and adjust.
  2. Check the namespace. Helm resolves releases per namespace. A release that "does not exist" is very often a missing -n in a pipeline where the default context changed. Confirm with helm list -A.
  3. Check for a stuck pending status. A revision left in pending-upgrade by a Helm process that was killed mid-flight blocks new operations with a different message, and needs its own fix.
  4. Read why the upgrade failed in the first place. You have restored the ability to deploy, not fixed the deploy. kubectl -n prod describe pod and kubectl -n prod get events --sort-by=.lastTimestamp will usually show the failed readiness probe, missing image or unbound claim that started all of this. If it is storage, my notes on unbound PersistentVolumeClaims cover the usual suspects.

The prevention is genuinely simple and almost nobody does it: put --atomic and a realistic --timeout on every single upgrade, in every pipeline, from the first day of a project. A failed deploy then rolls itself back, the history always contains a deployed revision, and you never read this error again.

Frequently asked questions

What does 'has no deployed releases' mean in Helm?
It means no revision in the release history carries the status 'deployed', so Helm has no baseline to diff the new manifest against. Helm 3 stores each revision as a Secret labelled with name, owner and status, and both upgrade and rollback look up the deployed revision by that label query. If every revision is failed or pending, the query returns nothing and both commands refuse to run.
Why does helm rollback also fail with 'has no deployed releases'?
Because rollback resolves the current deployed release using the same label query that upgrade uses. Trying progressively older revision numbers changes nothing, since the revision number is not what Helm is missing. You have to restore a deployed status first, either by patching the release Secret or by uninstalling and installing fresh.
Is it safe to patch a Helm release Secret to status deployed?
It is safe as long as you patch exactly one revision, and you choose the revision whose rendered manifest matches what is actually running in the cluster. The patch changes only the label that Helm queries, so helm history may still display the old status until the next successful upgrade supersedes it. Verify with 'helm get manifest <release> --revision N' before patching.
How do I stop Helm release history from breaking again?
Run every upgrade with --atomic and a realistic --timeout, so a failed upgrade rolls itself back and always leaves a deployed revision in the history. Do not set --history-max to a very low number, since a short run of failures can then prune the last good revision away. In CI, avoid blindly retrying failed Helm upgrades, because each retry appends another failed revision.
#Helm#Kubernetes#kubectl#CI/CD#Deployment
Keep reading

Related articles