Skip to content

v0.1.3: PreservePaths + per-conflict resolver#5

Merged
StuartMeeks merged 1 commit intomainfrom
preserve-paths-and-conflicts
May 3, 2026
Merged

v0.1.3: PreservePaths + per-conflict resolver#5
StuartMeeks merged 1 commit intomainfrom
preserve-paths-and-conflicts

Conversation

@StuartMeeks
Copy link
Copy Markdown
Owner

@StuartMeeks StuartMeeks commented May 3, 2026

Summary

Some consuming CLIs need to keep files like appsettings.Development.json across an update. Until now the installer treated every entry in the install directory as package-owned, so user-placed files ended up in .old/ after the swap and got swept up at next startup. This PR adds opt-in preservation plus a per-conflict resolution path, fully backwards-compatible.

New API

  • SelfUpdaterOptions.PreservePathsIReadOnlyList<string> of glob patterns identifying top-level entries the installer must leave alone (appsettings.Development.json, appsettings.*.json, data/**, *.db, …). Matched entries skip Phase 1 (not moved to .old/) and Phase 2 (protected from being overwritten).
  • ISelfUpdater.InstallAsync(release, progress, onConflict, ct) and the matching IUpdateInstaller.InstallAsync(...) overload gain an optional Func<UpdateConflict, CancellationToken, Task<UpdateConflictResolution>>? resolver. When a new release ships an entry whose path matches a preserve pattern, the resolver picks KeepExisting or UseNew. null (default) means KeepExisting — safe default for headless callers.
  • New public types UpdateConflict (RelativePath + ExistingSizeBytes + NewSizeBytes) and UpdateConflictResolution (KeepExisting | UseNew).
  • update --strategy ask|keep|new flag on UpdateCommand. With --yes the default is keep (unattended runs never block on a prompt); without --yes the default is ask and prompts the user per file via Spectre's Confirm, showing the path + existing/new sizes.

End-user-extensible via appsettings.json

PreservePaths is just IReadOnlyList<string>, so no new package API is needed for end-user extension. Consumer CLIs can IConfiguration.GetSection("SelfUpdate:Preserve").Get<string[]>() and concat with their in-code defaults. README has the recipe.

Other

  • CHANGELOG restructured. Prior 0.1.0 / 0.1.1 / 0.1.2 entries were all piled under "Unreleased"; now properly demarcated by version + date in standard Keep-a-Changelog shape. 0.1.3 has its own section.
  • README has a new "Preserving user files across updates" section covering glob syntax, top-level matching, the appsettings.json recipe, and conflict resolution. PreservePaths and AllowInsecureManifestSource added to the configuration table.

Test plan

  • 165 tests passing locally (was 150) — 15 new covering IsPreserved matching, Phase-1 skip, default KeepExisting, UseNew resolver path, glob preservation, and a release introducing a preservable path the user doesn't have yet
  • dotnet build --configuration Release clean (TreatWarningsAsErrors)
  • CI green on Linux / macOS / Windows
  • After merge: tag v0.1.3, verify both .nupkg and .snupkg indexed on nuget.org

🤖 Generated with Claude Code

Some CLIs need to keep files like appsettings.Development.json across
an update. Until now the installer treated every entry in the install
directory as package-owned, so user-placed files ended up in .old/ on
swap and got swept up at next startup. New API:

- SelfUpdaterOptions.PreservePaths — IReadOnlyList<string> of globs
  identifying top-level entries the installer must leave alone.
  Matched entries are skipped in Phase 1 (not moved to .old/) and
  protected in Phase 2.
- ISelfUpdater.InstallAsync(RemoteRelease, progress, onConflict, ct)
  and IUpdateInstaller.InstallAsync(...) gain an optional resolver
  delegate. When a new release ships an entry whose path matches a
  preserved pattern, the resolver decides KeepExisting vs UseNew.
  null (default) = KeepExisting (safe for headless callers).
- New public types UpdateConflict (RelativePath + sizes) and
  UpdateConflictResolution (KeepExisting | UseNew).
- UpdateCommand gains --strategy ask|keep|new. With --yes the default
  is keep; without --yes the default is ask, prompting per file via
  Spectre's Confirm. Each prompt shows the path + existing/new sizes.

Layered config support is documented (and works without new API):
end-user CLIs can read additional PreservePaths entries from
appsettings*.json via IConfiguration and concat with their in-code
defaults — README has the recipe.

CHANGELOG restructured: prior 0.1.0 / 0.1.1 / 0.1.2 sections were all
piled under "Unreleased"; now properly demarcated by version + date.

Tests: 150 → 165 (+15 covering IsPreserved match logic, Phase-1 skip,
KeepExisting default, UseNew resolver, glob preservation, and a
release introducing a preservable path the user doesn't have yet).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@StuartMeeks StuartMeeks merged commit 552d1cd into main May 3, 2026
4 checks passed
@StuartMeeks StuartMeeks deleted the preserve-paths-and-conflicts branch May 3, 2026 13:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant