Skip to content

Template versioning and downstream updates

Renovate's copier manager delivers downstream updates; the template keeps release-please as its tag source

Id: f26da4b3-8b97-4cfc-9aba-64eba4acf04f Type: decision Status: active Evidence: confirmed Source: ADR-015 (docs/adr/015-template-self-versioning-and-copier-update-automation.md); prompted by hwid#113 Revisit when: Renovate fails its artifacts check on conflict markers (renovate#31600), or the copier manager accepts Copier's gh: shorthand See: release-automation.md#release-please-is-the-sole-release-orchestrator — 7ec8a937-fd4d-41f2-96b7-824fe3be913b — as of 2026-10-11 See: template-updates.md#copieryml-defines-no-_tasks — 86130379-1660-4992-8aba-8b6cd7b4a9f7 — as of 2026-10-11

Generated projects receive template updates through Renovate's first-party copier manager, which watches the template's git tags, runs the real copier update and opens a PR. No bespoke workflow ships. This repository's own release-please therefore must keep cutting tags — they are the datasource. Scaffolding uses the .git HTTPS URL because the gh: shorthand is not a git URL and the git-tags datasource rejects it silently.

Reason: nothing in a generated project watched for template changes and the template published no tags, so a default copier update found nothing. The template already ships Renovate as the canonical updater; its App token can push workflow files (the repo GITHUB_TOKEN cannot), and its PRs flow through the existing dashboard and review. Tags are cheap since release-please already runs and give adopters a changelog besides.

Rejected alternative: a bespoke copier-update.yml workflow — ~50 lines of security-sensitive YAML per project, and its default token cannot push .github/workflows/*, forcing every consumer to provision a PAT.

Rejected alternative: fohte/copier-update-action — the bespoke workflow as a third-party action, same token and conflict characteristics, plus an external action to trust with --trust.

Rejected alternative: drop the template's own versioning and track HEAD — the copier manager is tag-based, so release-please is the price of Renovate.

Consequence: a copier-update PR can look mergeable while carrying conflict markers; maintainers review for them. A repo pinned to a raw SHA needs one manual update to re-anchor onto a tag. Existing repos start receiving copier PRs once the Renovate App is installed and the template has a tag newer than their recorded _commit.

_src_path must be a real git URL. Copier records _src_path verbatim from the copier copy argument, and the gh: shorthand is Copier-only, so Renovate's git-tags datasource rejects it ("non-git url", then no-result) and never opens an update PR even though the template has tags. Renovate accepts _src_path only when it starts with git+https://, git+ssh://, git@ or git://, or ends with .git, which is why the README scaffold step and the setup guide's adoption section use the .git HTTPS URL. A project scaffolded with gh: can be fixed by rewriting the _src_path line; copier update preserves it. The manager is enabled by default (the shared preset sets no enabledManagers), so the template's own renovate.json needs no addition.

copier.yml defines no _tasks

Id: 86130379-1660-4992-8aba-8b6cd7b4a9f7 Type: decision Type: constraint Status: active Evidence: confirmed Source: ADR-015 (docs/adr/015-template-self-versioning-and-copier-update-automation.md); the constraint is re-affirmed in ADR-018, ADR-023, ADR-024 and ADR-035 Revisit when: the hosted Renovate App allows --trust, or Copier marks tasks safe See: template-updates.md#renovates-copier-manager-delivers-downstream-updates-the-template-keeps-release-please-as-its-tag-source — f26da4b3-8b97-4cfc-9aba-64eba4acf04f — as of 2026-10-11

The template has no post-copy tasks; the former git init task is gone and the scaffold instructions tell the adopter to run it. Every later feature that needs a post-render step (the render harness; formerly the repo-setup skill and the ai-rulez generator) is a rendered file or a documented command, never a task.

Reason: any task marks a template unsafe and forces --trust, which Renovate gates behind a self-hosted-only setting the hosted App disables — a task-bearing template makes the hosted App's copier update fail.

Rejected alternative: keep the git init task — breaks the update path for every hosted-Renovate adopter.

Shipped template/** pins are frozen from this repository's Renovate

Id: 86e0af71-9614-42c1-9363-d16170d942a8 Type: decision Type: constraint Status: active Evidence: confirmed Source: ADR-020 (docs/adr/020-freeze-shipped-template-pins-from-renovate.md); the confirmed conflict source was PR #196 Revisit when: seed staleness becomes a security concern faster than downstream Renovate catches up, or Renovate learns to skip lines the downstream already moved See: template-updates.md#renovates-copier-manager-delivers-downstream-updates-the-template-keeps-release-please-as-its-tag-source — f26da4b3-8b97-4cfc-9aba-64eba4acf04f — as of 2026-10-11

A packageRules entry in .github/renovate.json matches template/** and disables updates there. Shipped version literals (action SHAs, prek rev pins, mise tool versions, dependency pins) are seed values only; each generated project's own Renovate owns them afterwards. This repository's root workflows, prek.toml and mise.toml stay Renovate-managed.

Reason: copier update is a 3-way merge per file, and a pinned line conflicts when both sides moved it to different values. Both repositories' bots advanced the same literals on independent schedules, so copier-update PRs conflicted on nearly every release — and Renovate does not fail on those conflicts, so a PR looked mergeable with markers inside. With the template frozen, "theirs" equals "base" on every pin line and the downstream's value wins cleanly: the conflict class is eliminated structurally. matchFileNames plus enabled: false rather than ignorePaths preserves the shared preset's own ignore list and composes with its digest-pinning policy. The confirmed conflict source was the plain template/*.yml workflow files; the .jinja-suffixed files are not matched by Renovate's stock managers, so freezing them is a safe no-op unless the shared preset's custom managers are extended to reach them.

Rejected alternative: auto-resolve in the downstream's favour via a merge=ours driver or copier _exclude — _exclude is whole-file and would block genuine template changes; a per-downstream merge driver is fragile.

Rejected alternative: converge by cadence (group bumps, release immediately) — only shrinks the race window.

Consequence: new projects start with slightly stale but valid seeds and their own Renovate opens catch-up PRs; refreshing seeds before a release is a deliberate act that reintroduces conflicts for projects not yet caught up. Re-enabling Renovate for template/** reads as fixing staleness but rebuilds the conflict engine.

Retired template modules become adopter-owned

Id: 4dd6729a-697e-4588-a7a7-d37152018b6b Type: decision Type: constraint Status: active Evidence: confirmed Source: PR #333, version-sweep results; commits c150ec3 and 0f7a406 Verification: corroborated — copier.yml excludes the retired paths and requires Copier 9.10.3; the update skill records the same compatibility floor Revisit when: Copier changes update-deletion semantics or a retired module is rendered again See: components.md#one-application-payload-in-coreapppy-components-are-adapters-over-it — 8bb1e55e-f468-44fc-80a5-d3b3719e5ddc — as of 2026-10-11

The dropped utils/app.py and library-only core/app.py placeholders are excluded from updates, leaving existing copies under the adopter's ownership. The custom exclusion list explicitly retains Copier's default exclusions because _exclude replaces that list rather than extending it.

Reason: an adopter can have replaced a placeholder with real application code. Removing the template file can delete that edited copy without conflict markers or reject files, so ordinary conflict review cannot catch the loss. The recorded version sweep found that Copier 9.6.0–9.10.2 still deleted the excluded files; 9.10.3 and later preserved them. The version floor makes an older Copier refuse the update instead of silently deleting adopter code.

Rejected alternative: let normal template deletion remove the old modules. Rejected because the files may no longer be empty placeholders in the adopter.

Rejected alternative: use exclusions without raising the Copier minimum. Rejected because the version sweep showed the protection was ineffective on older supported versions.

The architectural reason for retiring the placeholders is the shared application payload entry in components.md; this entry records the distinct ownership and update-safety constraint.

Adoption audits preserve behavior before consolidating workflows

Id: dfd6cd98-9d07-4dc4-b58f-d1343d636ea4 Type: decision Status: active Evidence: confirmed Source: issue #348, PR #349; commit 0abcffc; docs/template-adoption.md Verification: corroborated — the adoption and update sections of the generated REPOSITORY_SETUP.rst (formerly a shipped skill) compare behavior and preserve customizations; the older update skill now shares that contract Revisit when: the adoption and update skills change their reconciliation contract See: repo-setup.md#a-resume-driven-repo-setup-skill-reads-check-blocks-from-the-setup-doc-no-separate-manifest — 68ac4665-dcde-456b-aef7-2faaf6fe51f4 — as of 2026-10-11

The shipped adoption skill treats template defaults as evidence to compare with existing project behavior, rather than automatic authority to replace it. It handles initial adoption, updates, and audits of already-committed updates; repository-settings changes remain the responsibility of repo-setup.

Reason: two workflows that both run prek can differ in event coverage and required-check identity. Consolidating them can discard manual dispatch or leave live branch protection waiting for a check that no longer exists. A local ruleset file alone cannot establish the live requirements. A post-update audit can compare the actual committed baseline and diff without rerunning Copier and disturbing the state being inspected.

Alternatives considered: retain both workflows, accepting duplicate PR execution, or consolidate after preserving event coverage and transitioning the required check. The documented walkthrough deliberately makes that choice conditional on the adopter's decision and evidence; neither option is rejected universally. Deletion without establishing equivalence is excluded because it can silently remove functionality or block merges.

The execution procedure is the "Adopting into an existing project" section of the generated REPOSITORY_SETUP.rst.

Which reconciliation policy governs machine-config and CI conflicts?

Id: 3f155d92-8518-4294-bfcd-26251c36e2e1 Type: constraint Status: superseded Evidence: unknown Source: skills/update/SKILL.md, Shared reconciliation; the formerly shipped template-adoption skill (now REPOSITORY_SETUP.rst); PR #349 Verification: uncorroborated — the original justification for category-based precedence could not be established; the conflicting wording found during recovery has since been replaced See: template-updates.md#adoption-audits-preserve-behavior-before-consolidating-workflows — dfd6cd98-9d07-4dc4-b58f-d1343d636ea4 — as of 2026-10-08 Superseded by: 9a6bf00c-33c1-4f0c-962e-7e960f9d2d58

At recovery, the two skills coexisted with conflicting guidance: the older update skill selected template-side CI/config hunks by category, while the newer adoption skill required behavioral preservation and decisions before consequential cleanup. The reviewed historical sources did not establish which policy governed their overlap or justify the older precedence rule.

The question has been resolved by the current decision below, rather than by claiming to have recovered the original justification.

Update and adoption skills share preservation-first reconciliation

Id: 9a6bf00c-33c1-4f0c-962e-7e960f9d2d58 Type: decision Status: superseded Evidence: confirmed Source: maintainer-approved reconciliation-policy review, 2026-10-08; the former docs/template-adoption.md (now REPOSITORY_SETUP.rst); skills/update/SKILL.md Verification: corroborated — the older update skill now preserves custom behavior, requires evidence for equivalence, and delegates repository-settings transitions to repo-setup Revisit when: either skill changes its scope or introduces a conflicting reconciliation policy Superseded by: e4cd1c46-28c3-4241-82c8-d5aac41c293f See: template-updates.md#adoption-audits-preserve-behavior-before-consolidating-workflows — dfd6cd98-9d07-4dc4-b58f-d1343d636ea4 — as of 2026-10-08

Superseded on 2026-10-12 by the companion-skill retirement decision. The preservation-first contract now lives in the generated REPOSITORY_SETUP.rst; the separate root skills/update delivery surface is retired.

At the time of this decision, both skills used the same preservation-first contract. The older update skill ran or reconciled updates; the generated adoption skill additionally handled initial adoption and audits of completed updates. Those workflow differences do not justify opposite advice about the same project customization.

Reason: file category does not establish behavioral equivalence. CI and machine configuration can encode adopter-specific event coverage, permissions, required-check identity, hooks, and dependencies. Replacing a hunk on the basis of its category can discard functioning customizations despite a green lint run. Routine maintenance can proceed directly when equivalence is established within the requested update; consequential behavior changes need an explicit decision.

The contract applies to clean merges as well as conflicts. Recovery uses the actual pre-update snapshot and preserves later edits: a current base branch can lack branch-owned release history, while the index may already hold the reset.

Rejected alternative: retain automatic template precedence for CI/config hunks. It gives template origin more authority than evidence of the adopter's existing behavior. Even action digest updates require checking release effects and preserving configured inputs rather than assuming equivalence from the SHA.

Repository-settings transitions remain with repo-setup; the reconciliation procedure is in the skills, not duplicated here. The former policy's historical justification remains unknown in the superseded record above.

Id: 4ecf9da8-3879-40c8-9a46-85f5cfc3bfc5 Type: decision Type: constraint Status: active Evidence: confirmed Source: empirical runs against Copier 9.18.2 (copy, update and recopy with a faked clock) Verification: corroborated — tests/test_license.py pins the recorded answer, the stability across copier update and the existing-project path Revisit when: Copier exposes the copy-versus-update operation to question defaults, or removes the strftime filter

LICENSE renders Copyright (c) <copyright_year>-present. copyright_year is an asked int question defaulting to the current year ('%Y' | strftime), so Copier records it in .copier-answers.yml and every later copier update keeps the creation year.

Reason: Copier writes only asked questions to the answers file; a when: false default is not recorded and is re-evaluated on each run. A hidden or inline current-year expression only looks stable on update, because the three-way merge re-renders the old side with the same clock: it conflicts when a neighbouring line changes, and copier recopy or any re-render at the recorded commit rewrites the year.

Rejected alternative: hard-coding 2025. It is the bug: every later project claims a year it did not start in.

Rejected alternative: a when: false default, or {{ now().year }} inline. Not recorded, so not stable (above). now is also a deprecated Copier builtin that warns it will be removed; the strftime filter is the supported spelling.

Consequence: a project scaffolded before this question has no answer and an existing 2025-present. Copier's question defaults cannot see whether the run is a copy or an update (_copier_operation exists only for tasks and exclusions), so a non-interactive update (Renovate) adopts the current year and rewrites that line. An interactive update prompts, and -d copyright_year=<year> keeps the original; _message_after_update says so. No default avoids this without leaving the bug in new projects.