# Good Example: GitOps Pull Request > A pull request that changes ArgoCD Application manifests following > Atelier's GitOps + Operators principles. Each aspect cites the > principle it satisfies. ## The PR A PR titled `promote payments-api 1.2.3 to prod` opened against the GitOps repo `platform/gitops`. It changes the `targetRevision` of the payments-api Application from `1.2.2` to `1.2.3`, adds a sync-wave annotation to a new migration Job, and tightens the AppProject's destination allow-list. CI runs plan/diff; nothing pushes to the cluster. ### The Commit ```yaml # manifests/prod/payments-api.yaml — the only file changed apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: payments-api namespace: argocd finalizers: - resources-finalizer.argocd.argoproj.io spec: source: repoURL: https://git.example.com/platform/payments targetRevision: 1.2.3 # was 1.2.2 — pinned, not latest path: manifests/prod destination: server: https://kubernetes.default.svc namespace: payments syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=false - PrunePropagationPolicy=foreground ``` ```yaml # manifests/prod/payments-db-migration.yaml — new file, wave-ordered apiVersion: batch/v1 kind: Job metadata: name: payments-db-migrate-1.2.3 namespace: payments annotations: argocd.argoproj.io/sync-wave: "-1" # PreSync: run before the app spec: backoffLimit: 0 ttlSecondsAfterFinished: 86400 template: spec: restartPolicy: OnFailure serviceAccountName: payments-migrator containers: - name: migrate image: registry.example.com/payments/migrate:1.2.3 args: ["up", "--to", "1.2.3"] ``` ```yaml # projects/payments.yaml — AppProject tightened in the same PR apiVersion: argoproj.io/v1alpha1 kind: AppProject metadata: name: payments namespace: argocd spec: sourceRepos: - https://git.example.com/platform/payments destinations: - server: https://kubernetes.default.svc namespace: payments # prod-app can only deploy to payments ns clusterResourceWhitelist: - group: "" kind: Namespace # allowed to create its own namespace roles: - name: payments-team policies: - p, proj:payments:payments-team, applications, sync, payments/*, allow ``` ### The CI Pipeline (runs on the PR, before merge) ```text # .github/workflows/gitops-plan.yml (illustrative steps) - name: validate manifests run: argocd app manifests manifests/prod/ | kubeconform -strict - name: diff against live cluster (read-only, no apply) run: argocd app diff payments-api --server $ARGOCD_SERVER --auth-token $READ_ONLY_TOKEN # CI holds a READ-ONLY ArgoCD token. It never holds kubectl rights. # A non-empty diff is the PR's proposed change, rendered for review. - name: opa gate (admission policy pre-check) run: opa eval -i manifests/prod/ -d policies/ "data.k8s.admission.deny" # Policy violations fail the PR before merge, not after deploy. ``` ## What Makes It Good ### Git is the Source of Truth (GitOps P1, C1 Correctness) - The promotion is a commit. The cluster's desired state is a derivative of this repo; the repo is the authority. If the change is wrong, `git revert` is the rollback — the recovery path is the history. - See `domains/gitops-operators/first-principles.md` P1 and `domains/gitops-operators/argocd.md` (Application CRD). ### Pull, Don't Push (GitOps P3, C4 Locality) - CI holds a **read-only** ArgoCD token for `app diff`. It holds no `kubectl` rights against the production cluster. The cluster's ArgoCD controller pulls the merged commit; nothing pushes to the cluster. A compromised CI token can read, not deploy. - See `domains/gitops-operators/argocd.md` (RBAC and SSO) and `domains/gitops-operators/flux.md` for the same pull boundary from the Flux side. ### State is Immutable and Versioned (GitOps P5, C5 Reversibility) - `targetRevision: 1.2.3` — the Application pins a specific chart revision, not `latest`. The commit that changed it is a permanent record; `git revert` restores `1.2.2` and ArgoCD's `selfHeal` converges the cluster back. No force-push; history is the audit trail. - See `domains/gitops-operators/first-principles.md` P5 and `domains/infrastructure-as-code/state.md` (State is Truth). ### Sync Waves Order Correctness (GitOps P4, C1) - The migration Job carries `argocd.argoproj.io/sync-wave: "-1"` so it runs in `PreSync` before the payments-api Deployment that depends on the new schema. Wave ordering is a correctness mechanism, not performance — the app starting before its migration is a correctness bug. - See `domains/gitops-operators/argocd.md` (Sync Waves and Hooks). ### Reconcile, Don't Mutate by Hand (GitOps P8) - `selfHeal: true` + `prune: true` means a hand-edited drift on a managed resource is overwritten on the next loop. The fix for drift is a new commit, not `kubectl edit`. The PR author does not SSH into the cluster to "fix" anything. - See `domains/gitops-operators/argocd.md` (Diff and Drift) and `domains/gitops-operators/first-principles.md` P8. ### Least Privilege Reconciliation (GitOps P10, C8 Economy) - The AppProject `payments` restricts the Application to the `payments` namespace and the `payments` repo. The controller's ServiceAccount (not shown) is bound to a namespace-scoped Role, not `cluster-admin`. The PR *tightens* the allow-list — least privilege is a direction, not a one-time setting. - See `domains/gitops-operators/argocd.md` (RBAC and SSO) and `domains/kubernetes/rbac.md`. ### Policy is a Gate (Compliance P5, cross-link) - The `opa eval` step runs the admission policy against the proposed manifests before merge. A violation fails the PR; the non-compliant state is never realized. Detection is not enforcement; this is enforcement. - See `domains/compliance/policy-as-code.md` and `domains/devops/ci-cd.md`. ### Failure is Observable (GitOps P9) - A sync failure or health degradation on `payments-api` emits ArgoCD status (`Degraded` / `OutOfSync`) and a notification. Silent drift is the bug; this PR does not disable notifications. - See `domains/gitops-operators/argocd.md` (Health and Status) and `domains/observability/metrics.md`. ## What This PR Does NOT Do (And Why That's Good) - Does **not** run `kubectl apply` from CI — that is the push pattern, a P3 violation (see `examples/bad/` for the anti-pattern). - Does **not** use `argocd app set` as the steady state — the change is in git, not in an imperative command's history. - Does **not** store raw Secrets in the GitOps repo — secrets arrive via Sealed Secrets / SOPS / External Secrets, encrypted in git. - Does **not** float `targetRevision: latest` — the Application pins a version; "latest" is an unknown model of the system. ## Cross-Domain Links - `domains/gitops-operators/argocd.md` — the Application CRD, sync waves, RBAC/AppProjects, and the pull model. - `domains/gitops-operators/flux.md` — the same PR pattern from the Flux side (Kustomization CRD, per-cluster autonomy). - `domains/kubernetes/workloads.md` — the Deployment/Job the Application reconciles. - `domains/kubernetes/rbac.md` — the ServiceAccount + Role the controller and the migration Job run as. - `domains/compliance/policy-as-code.md` — the OPA gate is a compliance-as-a-gate enforcement point. - `domains/devops/P4 Rollback First` — `git revert` is the rollback; `selfHeal` is the convergence.