set -e skipped the one line that mattered
An install script hit a harmless error on a CRD I do not even use, and exited. The line it never reached was the one the whole platform depended on — and its absence would have surfaced an hour later as something else entirely.
My ArgoCD install script does four things in order: apply the manifests, wait for the rollout, patch a ConfigMap, restart the repo server. It runs under set -euo pipefail, like every other script in the repo.
It failed on step one:
The CustomResourceDefinition "applicationsets.argoproj.io" is invalid:
metadata.annotations: Too long: may not be more than 262144 bytes
That error is well understood. Client-side kubectl apply records the entire manifest in a last-applied-configuration annotation, and Kubernetes caps an object’s annotations at 256 KiB in total. ArgoCD’s ApplicationSet CRD is bigger than that. The fix is --server-side, which tracks ownership in managedFields instead and has no such limit.
What made this interesting was not the error. It was what the error prevented.
The line that never ran
Here is the shape of the script:
set -euo pipefail
kubectl apply -n argocd -f .../install.yaml # ← failed here
kubectl -n argocd rollout status deploy/argocd-server
kubectl -n argocd patch configmap argocd-cm --type merge \
-p '{"data":{"kustomize.buildOptions":"--enable-helm"}}' # ← never reached
kubectl -n argocd rollout restart deploy/argocd-repo-server
That patch is the single most important line in the repository.
My overlays render a Helm chart through Kustomize. Without --enable-helm, older Kustomize produces no output for those entries and reports no error. ArgoCD then syncs, finds nothing to apply beyond a couple of plain manifests, and reports Synced / Healthy. (Newer Kustomize — v5.7.1 in my testing — at least refuses loudly. ArgoCD ships its own copy, so which behaviour you get depends on the ArgoCD version, not your laptop.)
Green dashboard. Nearly empty namespace.
Why this is worse than it sounds
ArgoCD runs perfectly well without ApplicationSet. I do not use it. If the CRD had failed on its own, nothing would have broken.
Instead:
| What happened | What I would have seen |
|---|---|
| CRD exceeds the annotation limit | A red error, immediately, about a CRD I do not use |
set -e aborts the script | — |
--enable-helm never set | — |
| Root app applied an hour later | Synced / Healthy, and almost no pods |
The signature failure of the entire project, arriving an hour after its cause, with nothing connecting the two. I would have gone looking in the overlays.
The repo already knew about the annotation limit, incidentally. The kube-prometheus-stack Application sets ServerSideApply=true for exactly this reason, with a comment explaining why. It had simply never been applied to ArgoCD’s own installation.
The fix, and the better habit
kubectl apply -n argocd --server-side --force-conflicts -f .../install.yaml
--force-conflicts lets it reconcile a namespace where an earlier client-side apply had already claimed fields.
serverside-applied, then configmap/argocd-cm patched and the repo server restarted — the two steps that never ran the first time. The blurred box is the bootstrap admin password.But the durable fix is not that flag. It is noticing that set -e converts an error into an absence, and an absence is much harder to see than an error.
Two things I now do in bootstrap scripts:
Verify the critical settings at the end, not just the exit code.
kubectl -n argocd get cm argocd-cm \
-o jsonpath='{.data.kustomize\.buildOptions}'
# must print: --enable-helm
One line, and it turns “the script exited 0” into “the thing the script existed to do is actually true”.
Put the load-bearing line early, or isolate it. If a step matters more than the steps around it, do not let an unrelated failure upstream decide whether it runs.
A second one, same shape
The same build had a CI job fail twice in a row with two different errors, and the first one hid the second.
Attempt one: both matrix legs failed in under twelve seconds with
buildx failed with: Learn more at https://docs.docker.com/go/build-cache-backends/
GitHub truncated the real message. In full it was “Cache export is not supported for the docker driver” — the workflow used cache-to: type=gha without a docker/setup-buildx-action step, so it was running on the default builder, which cannot export a cache.
Fixed that. Attempt two failed with something much clearer:
invalid tag "ghcr.io/Syed-Amjad/web:5446e1e…": repository name must be lowercase
github.repository_owner returns the owner with its real capitalisation, and Docker repository names must be lowercase. GitHub Actions expressions have no lowercase function, so it is done in bash: ${GITHUB_REPOSITORY_OWNER,,}. The second error was only reachable once the first was out of the way.
The useful signal there was the timing. Two matrix legs with different dependencies, failing identically in twelve seconds, is not a code problem — that is less time than pulling a base image. Identical fast failures across a matrix mean configuration.
What I took from it
set -eturns a cosmetic error into a silent omission further down the file. The thing that breaks is not always the thing that errors.- Assert the outcome, not the exit code. A script that finished is not a script that worked. One
jsonpathcheck is cheaper than an hour. - Errors queue up. Fixing one does not mean you are done; it means you can now see the next one. Expect to iterate rather than assuming a single cause.
- Identical fast failures across a matrix are configuration, not code. Different inputs failing the same way in the same few seconds narrows it immediately.
- When a repo already documents a trap somewhere, check everywhere it applies. The annotation limit was known, commented and handled — in one file out of two.
From the GitOps delivery platform build.
References
- Bash manual: the
setbuiltin —-e,-uand-o pipefail. - Kubernetes: server-side apply — field ownership in
managedFields, and conflicts. - Kubernetes: annotations — the size limit on an object’s annotations.
- Argo CD: Kustomize — Helm charts —
kustomize.buildOptions: --enable-helm. - Docker: cache storage backends — why the default
dockerdriver cannot export a cache.