Skip to content

v0.24.0 — A bare `copier update` destroyed the service it was meant to upgrade

Choose a tag to compare

@github-actions github-actions released this 08 Aug 00:42
· 3 commits to main since this release
a02ea24

v0.24.0 — A bare copier update destroyed the service it was meant to upgrade

Release date: 2026-08-07

Hotfix for a destructive defect present in v0.23.0 and every release
before it. Found by running copier update on a freshly generated service
instead of assuming it worked — the same method that produced every finding
in the last two releases.

What was wrong

v0.23.0 pinned --vcs-ref on every documented copier copy. It left
copier update unpinned: in copier.yml's _message_after_copy, in
the scaffold-update skill, and in rule 15-template-lifecycle.

Same root cause — Copier resolves an unpinned git source to the
highest-sorting tag, and the frozen v1.0.0v1.12.0 audit snapshots sort
above every v0.x tag. Far worse consequence.

copier copy unpinned hands you a stale scaffold. copier update
unpinned rewrites a service you already have, backwards.

Measured on a real v0.23.0 service:

Bare copier update --vcs-ref=v0.23.0
Files after 435 627
Files deleted 582 0
.copier-answers.yml deleted present

The deleted answers file is the sharp edge. It is the record copier update reads, so once it is gone the service cannot recover on its own —
the operation removes the mechanism that would undo it.

And _message_after_copy was telling every adopter to run exactly that
form, as the last thing they read after scaffolding.

What changed

  • --vcs-ref pinned on every documented copier update.
  • _message_after_copy now states the consequence, not just the command.
  • scripts/check_adopter_scaffold_ref.py extended to cover copier update
    as well as copier copy, verified in both directions.

Known follow-ons (scoped, not regressions)

  • The v1.x tag-sort collision is still unresolved. This is the
    third defect it has produced — v0.22.0 reaching nobody, the
    copier copy docs, and now copier update — and the third fix that is
    a pin. Pinning is correct and lands safely under any long-term option,
    but it is mitigation, not resolution. The structural fixes (moving
    the frozen snapshots out of the tag namespace, or advancing the active
    line past v1.12.0) need their own ADR, because agentic/rules/18 and
    ADR-014 both declare those tags immutable. Not decided unilaterally.
  • The MIGRATION.md recovery procedure for an already-downgraded
    service is un-rehearsed.
    It is derived from the mechanism (the
    pre-update commit must exist, because copier update requires a clean
    tree), not from a performed recovery.
  • copier update across a real version gap is still unexercised.
    Verified: the destructive unpinned case, and a pinned same-version
    update proving non-destructiveness. An update that actually crosses two
    releases and three-way-merges local modifications has not been run.
  • Everything carried forward from v0.23.0: clock-allowlist brittleness,
    ruff UP/B/S rulesets, mypypyright, shadow-lane precision
    data.

Until the structural fix lands, assume any Copier invocation against
this repo needs an explicit ref, and expect the guard to be the thing that
remembers.

Verification

Both directions, on real generated services:

# unpinned  — 627 -> 435 files, 582 deleted, answers file GONE
# pinned    — 627 -> 627 files,   0 deleted, answers file PRESENT

Evidence in VALIDATION_LOG.md Entry 018.