Files
template-engine-copier-3darch/instructions/01-reference-architecture.md
root-at-skicandClaude Opus 5 97991021ff template-engine-copier skill: initial import
Copier + Jinja template-repository design skill: reference architecture,
implementation workflow, validation checklist, and a full worked example
template (questionnaire, extensions, resolvers, wrapper-first .scripts).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 22:30:29 +03:00

219 lines
6.6 KiB
Markdown

# Copier + Jinja Template Engine Reference Architecture
This reference architecture is synthesized from:
- `iac-tmpl-aws-projects/`
- `iac-tmpl-aws-organizations/`
and should be treated as the default blueprint for new template repositories.
## 1) Copier core contract
Use `.copier/copier.yaml` as central control plane.
### Recommended shape
```yaml
_template_suffix: ".jinja"
_exclude:
- ".git"
- ".copier"
- ".scripts"
- ".venv"
- ".devbox"
- "devbox.json"
- "devbox.lock"
- ".gitlab-ci.yml"
- "Taskfile.yml"
- "pyproject.toml"
- "uv.lock"
- "copier-answers-new.yml"
- "copier-answers.example.yaml"
_jinja_extensions:
- copier_templates_extensions.TemplateExtensionLoader
# plus custom extension(s)
_answers_file: copier-answers.yml
!include .copier/questionnaire/*.yml
```
## 2) Questionnaire strategy
Keep questions modular under `.copier/questionnaire/`.
Why:
- supports domain separation,
- scales better than one monolithic YAML,
- makes review and versioning easier.
## 3) Context shaping strategy
For advanced templates (especially conditional directories + repeated resource modules), use custom context hook extension.
Pattern used in `iac-tmpl-aws-projects`:
- `.copier/extensions/context_hook_extension/hook.py`
- merged orchestration in `hook_function.py`
1. compute/normalize flat variables,
2. expand `[INDEX]` instance folders.
### Why `flat.*` variables
They reduce deep nested path usage in Jinja and make conditional file/folder naming concise:
- `flat.dev_is_enabled`
- `flat.dev_vm_is_enabled`
- `flat.dev_vm_instances_names`
They should also carry any normalized/safe identifiers needed by templates:
- `flat.project_safe`
- `flat.environment_safe`
- `flat.component_safe_names`
Rule: do **not** repeat normalization logic such as `lower | replace(...) | trim` throughout template paths or file bodies. If a safe identifier is needed more than once, calculate it once in the context hook (or a deterministic resolver) and expose it as a stable context variable like `flat.project_safe`.
## 4) Dynamic folder generation (`[INDEX]`)
In `iac-tmpl-aws-projects`, folders like:
```text
iac/{{ 'dev' if flat.dev_is_enabled }}/.../{{ flat.dev_vm_instances_names[INDEX] }}/
```
are expanded by context hook functions that:
1. find `[INDEX]` patterns,
2. map pattern -> context list,
3. create concrete per-instance folders,
4. replace `INDEX` placeholders in contents,
5. remove the template placeholder folder.
## 5) Custom Jinja resolvers
Keep custom Jinja helpers in `.copier/extensions/resolvers/` and register them in `_jinja_extensions`.
Examples discovered:
- `resolve_safe_tf_module_name(...)`
- `resolve_regions(...)`
- `resolve_availability_zones(...)`
- tags resolver(s) for project/organization context.
Rule: put transformation logic in extensions, not inline repeated Jinja blocks.
For safe names specifically, prefer context-hook derived variables over inline filter chains. Templates should read:
```text
applications/{{ flat.project_safe }}/...
```
not:
```text
applications/{{ project.name | lower | replace(' ', '-') | replace('_', '-') }}/...
```
## 6) Operational wrapper architecture
Use shell wrapper-first command surface:
`Taskfile -> api -> lib`
Concrete layout:
- `.scripts/copier/Taskfile.yml` - operator tasks
- `.scripts/copier/api/bootstrap-one.sh` - thin wrapper
- `.scripts/copier/lib/*.sh` - actual behavior
This keeps CLI UX stable while allowing internal evolution.
### Bootstrap from a temporary render workspace
Follow the `iac-tmpl-aws-projects` runtime pattern: the public API wrapper should not render directly from the live template repo folder. It should first create a sibling temporary copy and execute rendering from there.
Canonical flow:
1. Capture the live template folder name from `$PWD`.
2. Remove any previous sibling temp folder, e.g. `../tmp-${TEMPLATE_FOLDER}`.
3. Copy the live template folder to `../tmp-${TEMPLATE_FOLDER}`.
4. `cd` into the temp folder.
5. Load logger and copier indexes from the temp folder.
6. Run `_copier_bootstrap_one` from the temp folder.
7. Return to the live template folder after rendering.
Inside `_copier_bootstrap_one`, before invoking `copier copy`:
1. Copy `.copier/copier.yaml` to root `copier.yaml` when the template stores Copier config under `.copier/`.
2. Remove temp-copy internals that interfere with Copier local rendering:
- `.git`
- `.devbox`
- `.venv`
3. Then run `copier copy --trust --data-file ... . "$TARGET_DIR"` from the temp folder.
Why this matters:
- Copier sees a plain local template directory, not the live Git repository.
- It avoids Git/tag discovery noise such as `No git tags found in template; using HEAD as ref`.
- It prevents the render from mutating or deleting files in the live template repo.
- It keeps generated transient files (`copier.yaml`, `copier-answers-new.yml`, temp merge files) isolated in the temp copy.
Do not solve local bootstrap Git warnings by creating tags just to satisfy Copier. Tags are for intentional template releases, not for local render hygiene.
## 7) Answers merge pipeline
Use 3-step pre-render merge flow before `copier copy`:
1. ensure `copier-answers-new.yml` exists,
2. pull previous target answers (`<target>/copier-answers.yml`) if present,
3. merge explicit params file into the working answers file.
Then execute:
```bash
copier copy --trust --data-file "copier-answers-new.yml" ...
```
## 8) Generated CI file safety pattern
When template repo itself has CI files that must not be copied literally, use a temporary rendered filename:
- template file: `{{ '.tmp-gitlab-ci.yml' }}.jinja`
- post-render rename in bootstrap lib:
- `.tmp-gitlab-ci.yml` -> `.gitlab-ci.yml`
## 9) Dependency baseline
Keep Copier stack pinned in `pyproject.toml`:
- `copier`
- `copier-templates-extensions`
- supportive tooling (e.g. `check-jsonschema`, logging libs)
Use lockfile (`uv.lock`) for deterministic local behavior.
## 10) Where this is implemented in this skill
See concrete scaffold:
- `example/.copier/copier.yaml`
- `example/.copier/questionnaire/0100-template.yml`
- `example/.copier/extensions/context_hook_extension/hook.py`
- `example/.copier/extensions/resolvers/resolve_safe_identifier.py`
- `example/.scripts/copier/`
## 11) Environment variable prefix convention
Before implementing shell wrappers, **propose and confirm** the environment variable prefix with the user (e.g. `HL_V1_`, `PALANTIR_`, etc.).
Rules:
- Do not hardcode an inherited prefix from old scaffolds without confirmation.
- Keep one prefix consistently across `--env-vars-reader.sh`, bootstrap libs, and answer merge libs.
- Document the chosen prefix in README or implementation notes.