Copier Template Maintenance & Propagation
Patterns for the full lifecycle of a Copier project template: authoring changes, testing them, releasing versions, and rolling updates out to every repo generated from the template.
When to Use This Skill
- Editing the template repo (questions, scaffold files, tasks, CI)
- "Upstream this pattern to the template" — porting something proven in a descendant
- Tagging/releasing a new template version
- Applying a template update in a descendant repo (often via an automated "template update available" PR)
- Debugging
copier copy/copier updatefailures
Mental Model
- The template repo holds
copier.yml(questions + settings + tasks) and atemplate/subdirectory of scaffold files (some.jinja-suffixed for substitution). Git tags (v*) are the version protocol; GitHub Releases are the changelog protocol. - Each descendant carries
.copier-answers.ymlrecording its answers and_commit: vX.Y.Z— the template version it's on. Never hand-edit this file;copier updatemaintains it. copier updatere-renders from old-tag → newest tag and three-way merges against local changes. It always jumps to the latest tag (unless--vcs-refpins one) — a notification PR advertising v1.5.0 may actually land v1.8.0 if the template moved on. The job is never to bump a version string — it is to standardize whatever the template now sells while keeping this app's behavior intact. Hand-editing_commitor any version number to shrink a diff is always wrong; letcopier updatemove it, and absorb the real changes.- Run copier via
uvx copier ...(no global install needed). Templates with_tasksrequire--trust— without it copier refuses to render at all. Non-interactive contexts also need--defaults(and--data key=valuefor required questions without defaults).
Direction of Change
Prove patterns in a real descendant first, then upstream. Build and merge the feature in one generated project; once proven, port it into template/ with jinja-aware adaptations. The upstream PR body should cite the originating repo/PR and include validation evidence (a project generated from the branch passing typecheck/lint/tests). Exception: infra-only changes (CI jobs, hooks config) can go straight to the template. For risky changes, stage the rollout — hand-run script in one repo first, graduate to the template once the win is proven.
When working in a descendant and a fix belongs in the template too: fix the symptom locally and make the corresponding change in the template repo (verify you have the right repo with git remote -v — don't trust the directory name).
Releasing a Template Version
First determine how the template releases — it changes everything below:
ls release-please-config.json .release-please-manifest.json 2>/dev/null # present ⇒ release-please
If the template uses release-please (e.g. nuxt-copier-template)
You never git tag by hand. Merging to main runs release-please, which opens a "release PR"; merging that cuts the tag + GitHub Release. The Conventional-Commits type of your merged PR governs the entire outcome — both the version bump and whether a release happens at all:
feat:→ minor bump.fix:→ patch.feat!:/BREAKING CHANGE:→ major.chore:,docs:,refactor:,style:,test:,ci:,build:→ no version bump, no release. The change lands onmainbut sits in the (often hidden) "Miscellaneous" changelog bucket, invisible to descendants, until some laterfeat/fixrides out and drags it along.
The trap: a template change that should propagate — a new alias/convention, a raised dependency floor, anything descendants must adopt — is a feat (or fix), not a chore. Type it chore and it silently never releases; descendants track git tags, so no tag = no copier update PR. When in doubt about whether descendants need it, it's a feat.
If you already merged it as the wrong type (non-releasing), don't wait — force a release with an empty commit carrying a Release-As footer, via a normal PR (squash-merge it):
git commit --allow-empty -m "chore: release template <X.Y.Z>
<why this is being force-released>
Release-As: <X.Y.Z>"
release-please honors Release-As: regardless of commit types and opens the release PR at that exact version. Note: release PRs get no CI (the meta-test workflow doesn't run on release-please--branches--*), so if branch protection requires a status check, the release PR stays BLOCKED under a normal merge and an admin/maintainer must merge it — that's the expected path for release PRs here, not a failure.
If the template has no release automation (manual tags)
git checkout main && git pull --ff-only
git tag --sort=-v:refname | head -5 # see existing versions
git cat-file -t v<latest> # match the tag type convention (lightweight vs annotated)
git tag v<X.Y.Z> && git push origin v<X.Y.Z>
gh release create v<X.Y.Z> --title "v<X.Y.Z> — <summary>" --notes "$(cat <<'EOF'
## Changes
- ...
## Upgrading
Run `uvx copier update --trust --defaults` in your project.
Full diff: <template-repo-url>/compare/v<prev>...v<X.Y.Z>
EOF
)"
- Semver: patch = fixes/dep bumps; minor = new features, questions, or components; major = breaking structure changes.
- Always create the GitHub Release, not just the tag. Descendant notification PRs link to
/releases/tag/<version>— a tag without a release produces dead links downstream.
Automated Update Notification
The template ships its descendants a checker workflow (daily cron + workflow_dispatch) that compares .copier-answers.yml's _commit against the template's highest remote tag:
git ls-remote --tags --refs --sort=-v:refname <template-url> 'v*' | head -1
If newer, it pushes a static branch name (e.g. chore/template-update) with an --allow-empty commit and opens a PR whose body contains the version delta, release-notes/compare links, and step-by-step instructions an agent can execute. Hard-won details to keep if reimplementing: an explicit permissions: contents: write, pull-requests: write block (default token can't open PRs), a static branch name (dated branches caused duplicate PRs), and comparing tag versions, not commit SHAs.
Dependency Updates: Who Owns What
Two Renovate instances run, with a deliberate boundary so they never fight:
- The template's Renovate (in the template repo) keeps the pins in
template/package.json.jinjafresh via a custom regex manager, and auto-merges@gallopsystems/agent-skills— which release-please then cuts as a template release. Those bumps reach descendants throughcopier update. - Each descendant's Renovate (shipped as
renovate.json, gated on theinclude_renovatequestion) owns that repo's own app dependencies — the only place an upgrade can be tested against the real app's code and CI.
The one overlap is resolved by ownership: the descendant's renovate.json disables @gallopsystems/agent-skills, leaving it solely template-owned. Every other pin is the descendant's. Because the template keeps bumping all pins, a descendant's package.json arrives with version conflicts on copier update — resolve them by keeping the descendant's versions (see applying-updates.md → package.json dependency pins). Newly-scaffolded repos start on the template's pins and are freshened by their own Renovate within a day.
Branch Protection in Descendants
A template cannot enable branch protection for the repos it generates — GitHub reads required status checks from repo config, never from committed workflow files. So every descendant starts with nothing gating merges until someone sets it once (after the first CI run, so the check is known). This template's CI exposes a ci-success summary job to be exactly that gate — require it on main:
echo '{"required_status_checks":{"strict":false,"contexts":["ci-success"]},"enforce_admins":false,"required_pull_request_reviews":null,"restrictions":null}' \
| gh api -X PUT repos/<owner>/<repo>/branches/main/protection --input -
- Require the single
ci-successcontext, not individual job names. CI shards the test suite, so the per-leg check names embed the matrix size (test (shard 1/4)) and change as the project grows — protection pinned to them blocks every PR the moment the count shifts.ci-successis a summary job (needs:all gating jobs,if: always(), fails unless everyneeds.*.resultissuccess);needs.test.resultrolls the whole matrix into one value, so it stays correct at any shard count. If you add a template whose CI lacks such a job, create one rather than requiring the matrix legs directly. coverageis deliberately excluded fromci-successso the report never blocks a merge — don't add it to the required contexts.- The PUT body must include
required_status_checks,enforce_admins,required_pull_request_reviews, andrestrictions(any may benull) or the call 422s. Needs admin on the repo. - A Conventional-Commits PR-title check is only worth requiring on repos something actually reads the title — i.e. a published package with release-please/changelog automation. A private app (no release tooling,
"private": true, no version) gains nothing from it; don't gate on it there.
Further Reading
- Template anatomy & testing changes: template-authoring.md
- Applying an update in a descendant (the full procedure — read-the-changes-first, multi-version deltas,
.rejtriage, silent-overwrite review, and autonomous merge-readiness): applying-updates.md
Contributing Back
This skill grows by capturing what it missed. If you just worked through something in this domain that this skill did not cover — an error you had to figure out, a behavior that contradicts what is documented above, a workflow knot — ask the user: "Want me to contribute this back to the copier-template skill?"
If yes, run /contribute-skill. If that command is not available, do the equivalent inline: distill the generic lesson (placeholders only — no project names, IDs, domains, or secrets), then branch or fork gallop-systems/agent-skills and open a PR editing this skill.