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>
5.8 KiB
Implementation Workflow (Copier + Jinja Template Repository)
Follow this sequence when creating a new template-engine repository.
Phase 1 — Define rendering contract
- Define template scope (what downstream repository you generate).
- Identify stable input schema sections (e.g. tenant/project/environments/resources).
- Decide what is:
- static files,
- conditional files/folders,
- repeatable per-instance blocks.
Deliverable: first draft of copier-answers.example.yaml.
Phase 2 — Bootstrap Copier skeleton
Create:
.copier/copier.yaml.copier/questionnaire/*.yml{{ _copier_conf.answers_file }}.jinja
Use baseline:
_template_suffix: ".jinja"- modular
!include .copier/questionnaire/*.yml - explicit
_excludelist _answers_file: copier-answers.yml
Phase 3 — Create operational wrappers
Implement wrapper chain:
- root
Taskfile.ymlincludes.scripts/copier/Taskfile.yml .scripts/copier/Taskfile.ymlexposesbootstrap-one.scripts/copier/api/bootstrap-one.shremains thin- confirm ENV var prefix with user (or propose one) before implementing lib internals
.scripts/copier/lib/contains logic and answers merge helpers
Minimum behavior in lib:
- parse CLI args with both styles:
--arg valueand--arg=value(for--copier-target-dir,--copier-force-copy,--copier-params-file), - prepare merged answers file,
- run
copier copy --trust --data-file ...from a sibling temporary copy of the template repo, not from the live template repo folder, - post-render sanitization/renames if needed.
Mandatory temp-copy bootstrap pattern from iac-tmpl-aws-projects:
- In
api/bootstrap-one.sh:- derive the live template folder name from
basename "$PWD", - delete any old sibling
../tmp-${TEMPLATE_FOLDER}, - copy the whole template repo into
../tmp-${TEMPLATE_FOLDER}, cdinto the temp folder,- source
.scripts/loggers/lib/--index.shand.scripts/copier/lib/--index.sh, - execute
_copier_bootstrap_one, - return to the original live template folder.
- derive the live template folder name from
- In
lib/-bootstrap-one.sh, beforecopier copy:- copy
.copier/copier.yamlto rootcopier.yamlif the repo uses.copier/copier.yaml, - remove
.git,.devbox, and.venvfrom the temp copy, - run Copier from the temp folder into the real target.
- copy
Do not create Git tags merely to silence Copier's local template version warning. The preferred local bootstrap hygiene is rendering from the sanitized temp copy. Use --vcs-ref=HEAD only when there is an explicit reason to render directly from a Git-backed live folder.
Phase 4 — Add Jinja template layers
- Add top-level templates (
iac/main.tf.jinja, providers, backend, etc.). - Gate directories via folder-name expressions:
{{ 'dev' if flat.dev_is_enabled }}
- Gate module calls inside files via
{% if ... %}.
Pattern:
- folder condition controls existence,
- inner condition controls file content clarity.
Mandatory wrapped render-path rule:
- For every optional module/resource, apply conditions at the path level (folder/file names), not only inside file contents.
- Preferred pattern (from
iac-tmpl-aws-projects):iac/{{ 'dev' if flat.dev_is_enabled }}/{{ 'compute' if flat.dev_compute_is_enabled }}/main.tf.jinja
- Apply this systematically to all optional Jinja outputs so disabled features do not produce placeholder files/directories.
Mandatory safe-variable rule:
- Do not embed repeated normalization/sanitization logic in template paths or file bodies, e.g. avoid:
{{ project.name | lower | replace(' ', '-') | replace('_', '-') | replace('.', '-') }}
- If a safe name is needed, calculate it once in the context hook and expose a named variable, e.g.:
flat.project_safeflat.environment_safeflat.component_safe_names
- Use the named variable in templates:
applications/{{ flat.project_safe }}/main.yaml.jinja
- If a one-off transformation is genuinely needed, prefer a small deterministic resolver over duplicating filter chains.
Phase 5 — Add custom extensions (if complexity requires)
When logic gets repetitive, move it into extensions.
Resolver extension examples
- normalize names:
resolve_safe_tf_module_name(name) - derive tags / regions / zones
Context hook extension examples
- calculate
flat.*shortcuts, - calculate safe/normalized names such as
flat.project_safeandflat.environment_safe, - expand
[INDEX]directories into per-instance folders.
Register in .copier/copier.yaml under _jinja_extensions.
Phase 6 — Implement answers merge strategy
Use this exact order to avoid destructive updates:
copier-answers-new.ymlcreate if missing,- copy
<target>/copier-answers.ymlif present, - merge params file into working answers file (
yq eval-all ...).
Then run copier using merged file.
Phase 7 — Handle template/self-repo conflicts
If template repo contains files that should not be copied 1:1 (e.g. self CI):
- render as temp name (
.tmp-gitlab-ci.yml), - rename in target during bootstrap.
Phase 8 — Add docs and examples
Add/update:
README.mdwith local setup and bootstrap command,copier-answers.example.yamlwith realistic schema examples,- optional
bootstrap-one-testtask for local maintainer testing.
Phase 9 — Validate end-to-end
Run two critical scenarios:
- fresh render into empty target,
- re-render into existing target with existing
copier-answers.yml.
Both must succeed without manual edits.
Quick-start from this skill example
If you want to accelerate implementation, copy example/ and then:
- expand questionnaire files,
- expand
flat.*derivation logic in context hook, - add domain-specific resolver extensions,
- extend
iac/**/*.jinjaresource tree, - keep bootstrap/answers flow unchanged unless you have a strong reason.