CI/CD & Deploy Model¶
Tatara enforces a strict GitOps-only deploy model. No component is ever deployed by running helm upgrade, kubectl set image, or kubectl apply by hand. Every deploy is a git-recorded semver release that a pipeline applies. The platform runs a semver push-CD cascade (cut over 2026-07-05): merging to a component repo cuts a version tag, publishes the image and charts under that version, and propagates the bump into tatara-helmfile as a bot-authored auto-merge PR.
The deploy chain¶
flowchart TD
A[Merge to component repo main<br/>merged PR carries a semver:major|minor|patch label] --> B[Component CI<br/>lint + test + build]
B --> C[release.yml: cut next tag vX.Y.Z]
C --> D[Publish image :vX.Y.Z to Harbor]
C --> E[Publish operator + tatara-project charts X.Y.Z to Harbor OCI]
D --> F[cd-release bot opens version-bump PR to tatara-helmfile<br/>coalesced into cd/deploy-train]
E --> F
F --> G[diff.yaml: sticky helmfile-diff PR comment]
F --> H[Auto-merge on green required checks<br/>gh pr merge --auto --squash]
H --> I[apply.yaml on ARC runner<br/>helmfile -e default apply]
I --> J[Live cluster updated] The cascade is not human-gated for the automated path: diff.yaml still posts a sticky helmfile diff comment for auditability, but the version-bump PR auto-merges on green required checks. A human gate exists only where you configure branch protection to require a review on the tatara-helmfile PR; by default the deploy-train merges itself.
Semver release and the version label¶
The release is driven by a semver:{major|minor|patch} label on the merged component PR. release.yml reads that label, cuts the next tag off main, and refuses to tag if no semver: label is present. The tag drives both the published image tag and the chart version.
A single release publishes, for the operator:
- Image
harbor.szymonrichert.pl/containers/tatara-operator:vX.Y.Z. The per-commit:SHORT_SHAtraceability image is pushed separately by the shared CI job on every commit; the release job publishes only:vX.Y.Z. Harbor'scontainersproject has tag immutability, so a tag is published exactly once. - Charts
tatara-operatorandtatara-project, both versioned as the bare semverX.Y.Z(nov, no SHA). The version bump intotatara-helmfileis gated on BOTH charts publishing, so a partial publish (operator chart up,tatara-projectchart missing) can never ship a half release into the helmfile.
Version pins in tatara-helmfile¶
Every deploy is two pins in tatara-helmfile, both pointing at the same semver release:
- Chart version in
helmfile.yaml.gotmpl- bare semver, e.g.version: 0.4.11. - Image tag in
values/tatara-operator/common.yaml- thev-prefixed tag, e.g.image.tag: "v0.4.11".
Note the asymmetry: the chart version has no leading v, the image tag does. The chart's deployment.yaml defaults the running image tag to .Chart.AppVersion, so a chart-version-only bump that leaves image.tag stale keeps the old image running. An image-tag-only bump fails helmfile apply because the pinned chart version does not exist in Harbor. The cd-release bot rewrites both pins in the same version-bump PR.
Keep chart pins recent
Harbor retains a bounded number of chart tags. A pin pointing to a chart version that has since been garbage-collected fails helmfile apply with "chart not found". The cascade keeps pins current automatically; hand-authored pins should track a live released version.
Component CI¶
Component repos build via per-repo CI triggered by GitHub webhook on push to main or PR. tatara-argo-workflows was decommissioned (2026-07-05); it was homelab GitLab CI, never part of the tatara platform runtime. The in-cluster argo-workflows Helm release remains as separate, unrelated homelab infrastructure.
Image build: rootless buildkitd on a Ceph PVC (durable). TCP probes (not exec probes) on the buildkitd Service. Single-arch builds for the homelab cluster.
Chart packaging: release charts are packaged with the bare semver version (helm package --version X.Y.Z) and helm pushed to Harbor OCI. Any SHA-suffixed pre-release chart version must use a g prefix (e.g. 0.0.0-g0707870) because Helm's semver validation rejects a pre-release segment that starts with a digit; a bare 0.0.0-0707870 is invalid.
ARC runner¶
The tatara-helmfile apply.yaml and diff.yaml workflows run on an in-cluster GitHub Actions Runner Controller (ARC, actions-runner-controller) scale set (runs-on: arc-runner-tatara-helmfile). ARC is the GitHub self-hosted runner autoscaler; it is unrelated to Argo CD. The runner ServiceAccount has a cluster-admin binding (bounded to the tatara namespace). It has no KUBECONFIG - it uses the in-cluster ServiceAccount token directly.
Runner failure modes:
- A stale
AutoscalingListener(deleted ERS ref) crash-loops and queues jobs forever. Fix: delete the staleAutoscalingListener. - Control-plane-pinned runners: a flapping control-plane node evicts in-flight jobs at a uniform ~18 min mark (appears as "operation was canceled" across all concurrent jobs).
Applies are serialized by a non-cancelling concurrency group (tatara-helmfile-apply), so coalesced deploy-train merges queue rather than racing; at most one helmfile apply is in flight.
tatara-project chart for enrollment CRs¶
Project and Repository CRs are managed by the tatara-project Helm chart rather than raw presync manifests. This keeps enrollment CRs under Helm ownership:
helm diffshows CR changes before applyhelm rollbackreverts enrollment changes- Clean ownership metadata on the CRs
The project-tatara and project-infrastructure releases use needs: [tatara-operator] so CRDs exist before CR application. The tatara-project chart is versioned and pinned in lockstep with tatara-operator (same X.Y.Z), so the CRs a release renders always match the operator's CRDs.
Why live patches are forbidden¶
kubectl set image or kubectl patch bypasses:
- The diff review step (no sticky diff is posted before the change lands)
- The rollback-on-failure safety net (
helmDefaults.rollbackOnFailure: true) - The git audit trail
Exception: incident response. A kubectl patch is permitted to unblock a down service, but it must be immediately re-asserted through a tatara-helmfile PR so live state matches the repo. Never use live patches as a deploy path.
CRD upgrade¶
CRDs are bundled in charts/tatara-operator/templates/crds.yaml and applied by helm upgrade. Pre-existing CRDs need a one-time ownership annotation before the chart can adopt them:
kubectl annotate crd projects.tatara.dev \
meta.helm.sh/release-name=tatara-operator \
meta.helm.sh/release-namespace=tatara \
--overwrite
kubectl label crd projects.tatara.dev \
app.kubernetes.io/managed-by=Helm \
--overwrite
Rollback¶
helmDefaults.rollbackOnFailure: true in helmfile.yaml.gotmpl instructs Helm to roll back all releases on any apply failure. This is automatic; no manual intervention is needed on a bad deploy.
Manual rollback is a revert of the version-bump commit in tatara-helmfile (which the cascade re-applies), or, for a live emergency: