Aligns the skill name with the renamed repository and the development-* naming used by development-scripts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
253 lines
12 KiB
Markdown
253 lines
12 KiB
Markdown
---
|
||
name: development-gitops-argo-cd
|
||
description: >
|
||
Create, review, refactor, debug, and maintain GitOps repositories where ArgoCD syncs
|
||
cluster state from a Copier-templated Git repo — app-of-apps ArgoCD Application manifests
|
||
under cluster/argocd/, Helm values.yaml.jinja templates under cluster/resources/, Jinja2
|
||
conditional directory names used as feature flags, copier.yaml variable schemas, and
|
||
per-environment copier-answers.yaml files. Use when adding a new operator, database, or
|
||
workload to a cluster; editing sync policies, prune/self-heal, finalizers, or destination
|
||
namespaces; wiring container image address/tag through Copier variables into Helm values;
|
||
enabling or disabling a feature flag for one environment; comparing environment drift or
|
||
running copier update to re-render a deployed environment; or debugging an app stuck
|
||
OutOfSync, a template that fails to render, or blank lines that break rendered YAML — even
|
||
when the user only names their repository (for example CogArchHubGitOps) instead of saying
|
||
"GitOps" or "ArgoCD".
|
||
license: Proprietary
|
||
metadata:
|
||
author: workspace-skills-code-agent
|
||
version: "1.0"
|
||
spec: agentskills.io/specification
|
||
reference-implementation: CogArchHubGitOps
|
||
compatibility: Designed for Cline, Claude Code, GitHub Copilot, OpenAI Codex, and other compatible agent environments
|
||
---
|
||
|
||
# ArgoCD GitOps on a Copier Template Repository
|
||
|
||
Work rules for GitOps repositories that combine three layers: **Copier** renders the
|
||
repository from per-environment answer files, **ArgoCD** reads the rendered output and syncs
|
||
it, and **Helm** consumes the rendered `values.yaml` at sync time.
|
||
|
||
The rules here are repo-agnostic. `references/cogarchhubgitops-reference.md` records a
|
||
complete, concrete instance of every pattern (bands in use, flag names, global settings) —
|
||
read it when working in that repository, or as a worked example when standing up a new one.
|
||
|
||
## Scope routing
|
||
|
||
Pick the reference file that matches the change; do not load all of them.
|
||
|
||
| The change touches | Read |
|
||
|---|---|
|
||
| An `application.yaml.jinja` — sync policy, finalizers, destination, source path | `references/argocd-applications.md` |
|
||
| `copier.yaml`, any `.jinja` syntax, conditional directory names, the `render()` macro | `references/copier-templates.md` |
|
||
| A `values.yaml.jinja` — image wiring, `global` block, env vars, probes, replicas | `references/helm-values.md` |
|
||
| A `<env>/copier-answers.yaml` — feature flags, image tags, drift, `copier update` | `references/environments.md` |
|
||
| Adding a whole new resource across all three layers at once | `references/adding-a-resource.md` |
|
||
| CogArchHubGitOps specifically — bands in use, flag names, global settings | `references/cogarchhubgitops-reference.md` |
|
||
|
||
## 1. Canonical repository shape
|
||
|
||
```text
|
||
<repo>/
|
||
├── copier.yaml # variable schema + defaults
|
||
├── cluster/
|
||
│ ├── argocd/
|
||
│ │ ├── application.yaml.jinja # root app-of-apps (000-root)
|
||
│ │ └── {{ 'NNN-name' if <flag> }}/
|
||
│ │ └── application.yaml.jinja # one child Application per resource
|
||
│ └── resources/
|
||
│ └── {{ 'NNN-name' if <flag> }}/
|
||
│ └── values.yaml.jinja # Helm values for that Application
|
||
└── <env>/
|
||
└── copier-answers.yaml # per-environment answers
|
||
```
|
||
|
||
Every file under `cluster/` is a Copier template. There are no raw `.yaml` files there — a
|
||
manifest without the `.jinja` extension will be copied through unrendered and will reach
|
||
ArgoCD with literal `{{ … }}` in it.
|
||
|
||
## 2. The three-layer contract
|
||
|
||
Every managed resource spans three layers that must stay in sync:
|
||
|
||
```text
|
||
Layer 1 — copier.yaml
|
||
declares the feature flag + every configuration variable
|
||
Layer 2 — cluster/argocd/{{ 'NNN-name' if flag }}/application.yaml.jinja
|
||
tells ArgoCD where the chart and values live
|
||
Layer 3 — cluster/resources/{{ 'NNN-name' if flag }}/values.yaml.jinja
|
||
supplies the rendered values.yaml to Helm at sync time
|
||
```
|
||
|
||
All three share the **same feature flag variable** and the **same conditional directory
|
||
expression**. Breaking that symmetry is the single most common failure in this repo shape:
|
||
if the two directory expressions differ, one side renders and the other is omitted, and
|
||
ArgoCD fails with a path-not-found error.
|
||
|
||
**Mirroring rule** — every `cluster/argocd/<dir>/application.yaml.jinja` must have a
|
||
`cluster/resources/<same-dir>/` sibling containing at least a `values.yaml.jinja` (for Helm
|
||
apps) or raw manifests. Same expression, character for character.
|
||
|
||
## 3. App-of-apps bootstrap
|
||
|
||
`cluster/argocd/application.yaml.jinja` is the **root application**, conventionally named
|
||
`000-root`. It watches `cluster/argocd/` recursively inside the rendered repository. When
|
||
ArgoCD reconciles the root, it discovers every `application.yaml` beneath it and creates
|
||
those child `Application` resources automatically. Adding a child app therefore requires no
|
||
change to the root — only a new conditional directory.
|
||
|
||
## 4. Numeric band convention
|
||
|
||
Every child directory and its ArgoCD `Application` name carry a three-digit prefix:
|
||
|
||
| Band | Domain | Examples |
|
||
|---|---|---|
|
||
| 000 | Root app-of-apps | `000-root` |
|
||
| 200–299 | Operators | ESO, CNPG, Redis operator |
|
||
| 300–399 | Cluster configuration | ClusterSecretStore, namespaces |
|
||
| 400–499 | Databases | PostgreSQL cluster, Redis cluster |
|
||
| 700–799 | Advantage / MCP workloads | MCP servers, AI agents |
|
||
| 800–899 | Platform applications | notification, integration, agent-service |
|
||
|
||
Sub-groups inside a band take the next free number in that band — e.g. `210-eso-operator-group`,
|
||
`213-eso-subscription`, `215-eso-configuration`. Choose the band **before** writing any file
|
||
and scan existing directories to confirm the number is free. Never reuse a number across
|
||
domains.
|
||
|
||
## 5. Conditional directories are the feature-flag mechanism
|
||
|
||
Directory names inside `cluster/` are Jinja2 expressions:
|
||
|
||
```text
|
||
{{ '210-eso-operator-group' if operators.eso_is_enabled }}
|
||
```
|
||
|
||
Copier evaluates the expression at render time. Truthy → the directory renders under that
|
||
name. Falsy → **the entire directory tree is omitted**, so neither the ArgoCD app nor its
|
||
resources are generated.
|
||
|
||
This is why manifests in this repo contain almost no Jinja2 logic. Do not use `{% if %}`
|
||
blocks to conditionally include whole YAML sections — gate the directory instead. Templates
|
||
substitute values; directories decide what exists.
|
||
|
||
## 6. Non-negotiable rules
|
||
|
||
These apply to every change regardless of which layer it touches:
|
||
|
||
- **Never hardcode `repoURL` or `targetRevision`** — always `{{ gitops.repo_url }}` and
|
||
`{{ gitops.branch }}`.
|
||
- **Never hardcode image address or tag** in a values template — environments must deploy
|
||
different versions of the same template.
|
||
- **Every `values.yaml.jinja` uses the `{% macro render() %}` blank-line-stripping footer.**
|
||
YAML is whitespace-sensitive and unexpanded conditionals leave blank lines that break the
|
||
parse. The pattern is mandatory, not stylistic — see `references/copier-templates.md`.
|
||
- **Use `.get()` chains with defaults** for any nested variable that may be absent in some
|
||
environment: `{{ applications.get('svc', {}).get('replicas', 1) }}`. Direct dot access on
|
||
an optional key raises `UndefinedError` and fails the render for every environment.
|
||
- **New feature flags default to `false`** in `copier.yaml` — resources are opt-in per
|
||
environment.
|
||
- **No secrets in Git.** Not in `.jinja` templates, not in `values.yaml.jinja`, not in
|
||
`copier-answers.yaml`. Use placeholders and inject at runtime via ExternalSecret /
|
||
ClusterSecretStore.
|
||
- **Feature flags are YAML booleans** (`true`/`false`) in answer files. Helm string-booleans
|
||
(`is_internal`, `maintenance`) are quoted strings (`"true"`/`"false"`). These are different
|
||
things — see `references/environments.md`.
|
||
|
||
## 7. Adding a new resource — the short path
|
||
|
||
Full walkthrough with copy-paste templates: `references/adding-a-resource.md`.
|
||
Starting templates: `assets/example-cluster-resource/`.
|
||
|
||
1. Pick the band, the next free number, the kebab-case name, and the flag variable name.
|
||
2. Declare the flag (`type: bool`, `default: false`) and its config variables in `copier.yaml`.
|
||
3. Create `cluster/argocd/{{ 'NNN-name' if <flag> }}/application.yaml.jinja`.
|
||
4. Create `cluster/resources/{{ 'NNN-name' if <flag> }}/values.yaml.jinja` — identical expression.
|
||
5. Enable the flag and supply image/config values in the answer file of each target environment.
|
||
6. Render with the flag off **and** on, and validate both (section 8).
|
||
|
||
## 8. Validation
|
||
|
||
Never commit a template change without rendering it. A broken template blocks every
|
||
environment that renders from the repo, not just the one you were working on.
|
||
|
||
```bash
|
||
# Render with defaults — new resource must NOT appear (flag defaults to false)
|
||
copier copy . /tmp/gitops-off --overwrite --defaults
|
||
ls /tmp/gitops-off/cluster/argocd/ | grep "<NNN>" # expect no output
|
||
|
||
# Render with the resource enabled
|
||
copier copy . /tmp/gitops-on --overwrite --defaults \
|
||
--data '<domain>.<descriptor>_is_enabled=true' \
|
||
--data '<domain>.<descriptor>.image.address=myregistry/myimage' \
|
||
--data '<domain>.<descriptor>.image.tag=1.0.0'
|
||
ls /tmp/gitops-on/cluster/argocd/ | grep "<NNN>" # expect the directory
|
||
|
||
# Render a real environment
|
||
copier copy . /tmp/gitops-env --overwrite --answers-file <env>/copier-answers.yaml
|
||
|
||
# No .jinja may leak into rendered output
|
||
find /tmp/gitops-env -name "*.jinja" | wc -l # must be 0
|
||
|
||
# Every rendered YAML must parse
|
||
find /tmp/gitops-env -name "*.yaml" -exec python3 -c \
|
||
"import yaml, sys; yaml.safe_load(open(sys.argv[1]))" {} \;
|
||
|
||
# Every Application's source.path must exist under the rendered tree
|
||
for app in $(find /tmp/gitops-env/cluster/argocd -name "application.yaml" -exec \
|
||
python3 -c "import yaml,sys; print(yaml.safe_load(open(sys.argv[1]))['spec']['source']['path'])" {} \;); do
|
||
[ -d "/tmp/gitops-env/$app" ] || echo "MISSING resources dir: $app"
|
||
done
|
||
```
|
||
|
||
Jinja2 syntax check without rendering (fast feedback while editing):
|
||
|
||
```bash
|
||
python3 -c "
|
||
from jinja2 import Environment
|
||
import pathlib
|
||
env = Environment()
|
||
for f in pathlib.Path('cluster').rglob('*.jinja'):
|
||
env.parse(f.read_text()); print('OK:', f)
|
||
"
|
||
```
|
||
|
||
Helm-lint the rendered values against the chart when a chart path is available:
|
||
|
||
```bash
|
||
helm lint <chart-path> -f /tmp/gitops-on/cluster/resources/<NNN-name>/values.yaml
|
||
```
|
||
|
||
## 9. Consistency checklist
|
||
|
||
Before committing any resource addition or rename:
|
||
|
||
- [ ] The three-digit number is unique and in the correct band.
|
||
- [ ] The flag is declared in `copier.yaml` with `type: bool` and `default: false`.
|
||
- [ ] The ArgoCD `metadata.name` equals the directory name exactly.
|
||
- [ ] `spec.source.path` equals the `cluster/resources/` directory name exactly.
|
||
- [ ] The conditional expressions in `cluster/argocd/` and `cluster/resources/` are identical.
|
||
- [ ] `repoURL` and `targetRevision` use Jinja2 variables.
|
||
- [ ] `resources-finalizer.argocd.argoproj.io` is present in `metadata.finalizers`.
|
||
- [ ] `values.yaml.jinja` carries the `render()` macro and stripping footer.
|
||
- [ ] All optional variable references use `.get()` with a default.
|
||
- [ ] No secrets in any new or edited file.
|
||
- [ ] At least one environment's answer file enables and fully configures the resource.
|
||
- [ ] Rendered flag-off and flag-on, both validated.
|
||
|
||
## 10. Top mistakes to avoid
|
||
|
||
- Creating the ArgoCD app without the mirrored `cluster/resources/` directory — ArgoCD errors
|
||
with path-not-found.
|
||
- Using different conditional expressions on the two sides — one renders, the other vanishes.
|
||
- Writing a raw `application.yaml` instead of `application.yaml.jinja`.
|
||
- Omitting the `resources-finalizer.argocd.argoproj.io` finalizer — deleting the app leaves
|
||
orphaned cluster resources behind.
|
||
- Disabling `selfHeal` or `prune` without documenting why — it silently defeats the GitOps
|
||
contract.
|
||
- Enabling a flag in an answer file without supplying the required image/config variables —
|
||
the rendered values file has empty fields and Helm fails at sync.
|
||
- Editing `_src_path` or `_commit` by hand in an answer file — both are Copier metadata and
|
||
wrong values break `copier update`.
|
||
- Skipping the render-and-diff step after editing an answer file — drift between answers and
|
||
rendered output produces unexplained cluster state.
|