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>
12 KiB
name, description, license, metadata, compatibility
| name | description | license | metadata | compatibility | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| development-gitops-argo-cd | 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". | Proprietary |
|
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
<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:
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:
{{ '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
repoURLortargetRevision— 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.jinjauses 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 — seereferences/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 raisesUndefinedErrorand fails the render for every environment. - New feature flags default to
falseincopier.yaml— resources are opt-in per environment. - No secrets in Git. Not in
.jinjatemplates, not invalues.yaml.jinja, not incopier-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 — seereferences/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/.
- Pick the band, the next free number, the kebab-case name, and the flag variable name.
- Declare the flag (
type: bool,default: false) and its config variables incopier.yaml. - Create
cluster/argocd/{{ 'NNN-name' if <flag> }}/application.yaml.jinja. - Create
cluster/resources/{{ 'NNN-name' if <flag> }}/values.yaml.jinja— identical expression. - Enable the flag and supply image/config values in the answer file of each target environment.
- 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.
# 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):
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:
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.yamlwithtype: boolanddefault: false. - The ArgoCD
metadata.nameequals the directory name exactly. spec.source.pathequals thecluster/resources/directory name exactly.- The conditional expressions in
cluster/argocd/andcluster/resources/are identical. repoURLandtargetRevisionuse Jinja2 variables.resources-finalizer.argocd.argoproj.iois present inmetadata.finalizers.values.yaml.jinjacarries therender()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.yamlinstead ofapplication.yaml.jinja. - Omitting the
resources-finalizer.argocd.argoproj.iofinalizer — deleting the app leaves orphaned cluster resources behind. - Disabling
selfHealorprunewithout 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_pathor_commitby hand in an answer file — both are Copier metadata and wrong values breakcopier update. - Skipping the render-and-diff step after editing an answer file — drift between answers and rendered output produces unexplained cluster state.