# The other half of "CI does not deploy". # # CI's last act is a commit that changes image.*.digest in deploy/chart/values.yaml. # ArgoCD notices that commit and syncs. There is no kubeconfig in CI, no `helm upgrade` in # a pipeline step, and no human running kubectl. If you want to know what is running in the # cluster, you read git — that is the whole property, and a single `kubectl apply` from a # laptop is what destroys it (selfHeal below exists to undo exactly that). apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: svcforge namespace: argocd finalizers: # Without this, deleting the Application orphans every resource it created. - resources-finalizer.argocd.argoproj.io spec: project: default source: repoURL: https://gitea.oci-oci.duckdns.org/gitea_admin/svcforge.git targetRevision: master path: deploy/chart helm: releaseName: svcforge # No valueFiles override and no `parameters:` block. values.yaml in the repo is the # single source of truth — a parameter here would be a second place the deployed # digest could come from, invisible in the chart's own diff. destination: server: https://kubernetes.default.svc namespace: svcforge syncPolicy: automated: # Delete resources removed from the chart. Safe because the chart owns only svcforge # itself; tenant releases are created by the worker's helm calls and are not part of # this Application, so prune cannot reach them. prune: true # Revert manual edits. A hotfix applied by hand is a lie the next sync tells on. selfHeal: true syncOptions: - CreateNamespace=true # The migrate Job is a helm pre-install/pre-upgrade hook. ArgoCD maps helm hooks onto # its own PreSync phase, so migrations still run before any new pod starts, and a # non-zero exit fails the sync instead of rolling out pods onto an unmigrated schema. - ApplyOutOfSyncOnly=true retry: # 0, not 3, and for the same reason migrate-job.yaml sets backoffLimit: 0. # # An ArgoCD retry re-runs the WHOLE sync including the PreSync phase, and the migrate # Job is a helm pre-install/pre-upgrade hook that ArgoCD maps onto PreSync. So # `limit: 3` quietly reinstated the retry-a-failed-DDL behaviour that backoffLimit: 0 # exists to forbid — three attempts at the same failed migration, one readable error # turned into three, against a schema that may now be half-applied. # # There is no recovery path here that a retry helps with. Recovery from a failed # migration is a REVERT COMMIT: fix the SQL forward, push, and let ArgoCD sync the # new revision. Rolling back the app image does not roll back DDL that already # committed. limit: 0 backoff: duration: 20s factor: 2 maxDuration: 3m # The migrate Job is a hook, so ArgoCD deletes and recreates it every sync; its # generated fields would otherwise show as permanent drift and the app would never # report Synced. ignoreDifferences: - group: batch kind: Job jsonPointers: - /spec/template/metadata/labels # This cluster runs a Kyverno ClusterPolicy, `force-best-effort-cpu`, whose rule # set-cpu-request-to-zero rewrites every container's CPU request to "0" at admission. # It is deliberate and predates this app by well over a year: the nodes are # oversubscribed, and making pods BestEffort on CPU is how everything gets scheduled. # # The chart asks for 50m and the cluster writes 0, so without this the Deployments sit # permanently OutOfSync while being perfectly Healthy — the failure mode where a # dashboard is always yellow, everyone learns to ignore it, and it stops meaning # anything the day it goes yellow for a real reason. # # The chart deliberately keeps its real request rather than capitulating to 0. What the # chart asks for is the honest intent; what the cluster does with it is the cluster's # business, and a reader of the repo should see the former. # # jqPathExpressions, not jsonPointers: a pointer would have to name a container index, # and this has to hold for every container in every one of the three Deployments. - group: apps kind: Deployment jqPathExpressions: - .spec.template.spec.containers[].resources.requests.cpu