| name | update-from-template |
| description | Update a project generated by python-package-copier from the latest template version while preserving local customizations (README, docs, CI workflows, dependencies, source code). Use when working inside a generated repo and the user wants to pull in template improvements, sync with upstream template changes, or apply template updates. Handles intelligent 3-tier conflict resolution using copier update with .rej files. Triggers on requests like "update from template", "sync template", "pull template changes", "upgrade template", or "apply template updates". |
Update From Template
Update a python-package-copier generated project from the latest template, preserving local customizations through intelligent 3-tier conflict resolution.
Prerequisites
- The project must have a
.copier-answers.yml file (generated by copier)
copier must be available (uvx copier or installed)
- Working directory must be clean (
git status shows no uncommitted changes)
Workflow
Step 1: Pre-flight Checks
Parse .copier-answers.yml and verify the project is ready for update:
cat .copier-answers.yml
git status --porcelain
Extract from .copier-answers.yml:
_commit: Current template version hash
_src_path: Template source (e.g., gh:stateful-y/python-package-copier)
- All copier variable values (needed for baseline generation in Step 2)
Abort if git status --porcelain produces output — require clean working tree.
Determine the target version:
- Default: latest (omit
--vcs-ref)
- If user specifies a version: use
--vcs-ref=<tag> (e.g., --vcs-ref=v1.2.0)
"Latest" means the template's newest released tag, not its newest commit.
Two consequences worth knowing before you start, because copier states the facts
without explaining them:
- Work that is merged but unreleased is not reachable. The update reports
Keeping template version <x> and changes nothing. If you are waiting on a
fix that has landed on the template's main branch, it needs a release first.
- A project whose
_commit is ahead of the newest tag cannot update at all.
It fails as You are downgrading from <newer> to <tag>. Downgrades are not supported. — not as a no-op. This happens when a project was generated or
updated with an explicit --vcs-ref <commit> pointing at an unreleased
commit. It stays stuck until the template cuts a release newer than that
commit. Prefer pinning to tags for exactly this reason.
Step 2: Detect Customizations
Generate a clean baseline to identify which files have local customizations:
TMPDIR=$(mktemp -d)
copier copy --trust --defaults --data-file .copier-answers.yml <_src_path> "$TMPDIR" --vcs-ref <_commit>
This creates a pristine copy of the project at the current template version with the same answers. Then diff:
diff -rq "$TMPDIR" . --exclude=.git --exclude=__pycache__ --exclude='*.pyc' --exclude=.venv --exclude=node_modules --exclude=site --exclude='*.egg-info'
For each file:
- Identical to baseline → no local customization (effectively Tier 1 for this update)
- Differs from baseline → has local customizations (check tier per file-classification.md)
- Only in project → locally-added file (always Tier 3)
- Only in baseline → file was deleted locally (note for later)
Build a classification map:
{file_path: {tier: 1|2|3, customized: bool, diff_summary: "..."}}
Report findings to the user: list customized files grouped by tier.
Clean up the temp directory.
Step 3: Create Update Branch
VERSION="latest"
git checkout -b template-update/$VERSION
Step 4: Run copier update
copier update --trust --conflict rej --skip-answered
If the template has new variables (added since _commit), copier will prompt for them. Provide answers by:
- Reading the prompt description
- Inferring from existing project context (e.g., if the project already uses examples, answer
include_examples: true)
- Using template defaults when no project context exists
After the update, collect .rej files:
find . -name '*.rej' -not -path './.git/*'
Also identify files copier modified without conflict:
git diff --name-only
Step 5: Resolve Conflicts
Read references/conflict-resolution.md for detailed merge patterns per file type.
5a: Handle .rej files
For each .rej file, determine the base file's tier from Step 2:
A .rej does NOT mean the file is untouched. git apply --reject applies every hunk
that applies cleanly and rejects only the rest, so a conflicted file is already partially
updated. Deleting the .rej keeps those applied hunks — which is wrong for any tier that
was supposed to reject them.
| Tier | Action |
|---|
| Tier 1 (template-managed) | Read .rej, apply its hunks to the local file (template wins). Delete .rej. |
| Tier 2 (merge-required) | git diff HEAD -- <file> first, to see which hunks already landed. Then merge: accept template improvements, restore any local content those hunks removed. Delete .rej. |
| Tier 3 (local-owned) | git checkout HEAD -- <file> to undo the hunks that already applied, then rm <file>.rej. Deleting the .rej alone leaves the template's changes in a file the project owns. |
5b: Handle files modified without conflict
For files copier updated cleanly (no .rej) that were classified as customized in Step 2:
| Tier | Action |
|---|
| Tier 1 or Tier 2 uncustomized | Accept — copier's update is correct |
| Tier 2 customized | Diff against git HEAD (git diff HEAD -- <file>). Verify local additions weren't lost. Restore any lost local content. |
| Tier 3 | Restore: git checkout HEAD -- <file> |
5c: Handle new and deleted files
- New template files (exist after update but not before): Accept
- Files deleted by template (existed before, removed after): Report to user, do not auto-delete — except a dropped config another tool still acts on.
copier update does not reliably delete a file the template stopped shipping, and a leftover config keeps running. This template replaced .github/dependabot.yml with renovate.json; a surviving dependabot.yml means Dependabot and Renovate both run and open duplicate PRs. Delete it explicitly (git rm .github/dependabot.yml) and confirm it is gone — the update is not complete while it remains.
- Locally-added files (Tier 3, not in template): Verify untouched
Step 6: Verify & Commit
find . -name '*.rej' -not -path './.git/*'
grep -r '<<<<<<<' . --include='*.py' --include='*.yml' --include='*.yaml' --include='*.toml' --include='*.md' --include='*.cfg' || true
test ! -f .github/dependabot.yml || echo "ERROR: .github/dependabot.yml still present -- run: git rm .github/dependabot.yml"
just test-fast 2>/dev/null || uv run pytest 2>/dev/null || echo "No test runner found"
just fix 2>/dev/null || uv run --locked prek run --all-files 2>/dev/null || echo "No linter found"
Re-install the git hooks after this update
copier update cannot touch .git/hooks — it works through git, and .git/ is not part of
the working tree. So an update that changes the hook runner leaves every existing clone
running the old shim, silently, until each person re-installs by hand:
uv run prek install -f
-f is required: the existing shim was written by another tool and is not overwritten
without it. Anyone who skips this keeps invoking a runner that can no longer parse
.pre-commit-config.yaml (it declares repo: builtin, which only prek understands), so
they get a confusing failure rather than a clean one. Tell every contributor, not just
whoever ran the update.
Pin uv in workflows the template does not own
copier update only touches files the template generates. The templated workflows
(tests.yml, changelog.yml, nightly.yml, commit-message.yml) pin an exact uv
version on every astral-sh/setup-uv step:
- uses: astral-sh/setup-uv@v7
with:
version: "0.10.0"
An unpinned step resolves "latest" over the network on every run, and a transient
blip there fails the job in a fraction of a second with ##[error]fetch failed,
before any real work. Any workflow you added yourself (anything not in the list
above) keeps its own astral-sh/setup-uv steps, and the update leaves them untouched.
After syncing, add the same exact version: pin to those steps by hand, matching the
value the templated workflows now use, so the flake cannot reach your bespoke CI either.
Version pins (uv, nox, git-cliff) are Renovate-owned
Renovate, not the template, keeps the uv, nox and git-cliff versions current in your
repo. The template only seeds them, so once Renovate has advanced a pin, a later
copier update must not roll it back. Usually there is nothing to do: when the template's
seed is unchanged, copier keeps your value automatically. If a template update also moved
a seed, copier raises a conflict on that version line. Keep your repo's value (the one
Renovate advanced) and discard the template's older seed — taking the template version here
silently downgrades a tool you already upgraded.
git add -A
git commit -m "chore: update from template <version>"
Report to user:
- Summary of changes accepted from template
- Files where local customizations were preserved
- Any files flagged for manual review (deleted by template, etc.)
- Suggest: review the branch diff with
git diff main...template-update/<version> before merging