v0.1.3: PreservePaths + per-conflict resolver#5
Merged
StuartMeeks merged 1 commit intomainfrom May 3, 2026
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Some consuming CLIs need to keep files like
appsettings.Development.jsonacross 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.PreservePaths—IReadOnlyList<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 matchingIUpdateInstaller.InstallAsync(...)overload gain an optionalFunc<UpdateConflict, CancellationToken, Task<UpdateConflictResolution>>?resolver. When a new release ships an entry whose path matches a preserve pattern, the resolver picksKeepExistingorUseNew.null(default) meansKeepExisting— safe default for headless callers.UpdateConflict(RelativePath+ExistingSizeBytes+NewSizeBytes) andUpdateConflictResolution(KeepExisting|UseNew).update --strategy ask|keep|newflag onUpdateCommand. With--yesthe default iskeep(unattended runs never block on a prompt); without--yesthe default isaskand prompts the user per file via Spectre'sConfirm, showing the path + existing/new sizes.End-user-extensible via
appsettings.jsonPreservePathsis justIReadOnlyList<string>, so no new package API is needed for end-user extension. Consumer CLIs canIConfiguration.GetSection("SelfUpdate:Preserve").Get<string[]>()and concat with their in-code defaults. README has the recipe.Other
appsettings.jsonrecipe, and conflict resolution.PreservePathsandAllowInsecureManifestSourceadded to the configuration table.Test plan
IsPreservedmatching, Phase-1 skip, defaultKeepExisting,UseNewresolver path, glob preservation, and a release introducing a preservable path the user doesn't have yetdotnet build --configuration Releaseclean (TreatWarningsAsErrors)v0.1.3, verify both.nupkgand.snupkgindexed on nuget.org🤖 Generated with Claude Code