fix(bundler): bundle update uninstalls components dropped by new version#3353
Merged
mnriem merged 1 commit intoJul 7, 2026
Merged
Conversation
On refresh (bundle update), install_bundle iterated only the new plan's components, so a component the previous version owned but the new one no longer ships was left installed on disk while being dropped from the rewritten record (contributed only holds plan.components). With no record referencing it, remove_bundle could never clean it up — permanently orphaned, violating the provenance invariant (FR-022). After the component loop, when refresh and a prior record exists, uninstall each previously-owned component absent from the new plan — unless another bundle still needs it (components_still_needed refcount, mirroring remove_bundle), in which case it stays installed and is simply de-attributed. Runs inside the existing try so a failed removal takes the same no-record-written rollback path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes a bundler provenance/lifecycle bug where specify bundle update (via install_bundle(..., refresh=True)) could leave components from the prior bundle version installed on disk but no longer referenced by any installed-bundle record, making them impossible to clean up later.
Changes:
- Extend
install_bundle’s refresh/update flow to uninstall previously-owned components that are no longer present in the new install plan (unless still required by another bundle’s record). - Add integration tests covering (1) dropped components are uninstalled and removed from the updated record, and (2) dropped-but-still-shared components remain installed and are de-attributed from the updated bundle.
Show a summary per file
| File | Description |
|---|---|
src/specify_cli/bundler/services/installer.py |
On refresh updates, removes components that were previously attributed to the bundle but are absent from the new plan, respecting “still needed by other bundles” refcount logic. |
tests/integration/test_bundler_install_flow.py |
Adds tests validating correct uninstall/de-attribution behavior when updating to a bundle version that drops components, including shared-component scenarios. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 2/2 changed files
- Comments generated: 0
- Review effort level: Low
Collaborator
|
Thank you! |
kanfil
added a commit
to tikalk/agentic-sdlc-spec-kit
that referenced
this pull request
Jul 8, 2026
Upstream commits merged (9): - 0151d23 agy honors SPECKIT_INTEGRATION_AGY_EXTRA_ARGS (github#3347) - fb796c2 shell step validate() rejects non-string run (github#3348) - 220e6fc fan-in validate() rejects non-mapping output (github#3349) - 12faf7b workflow run/resume errors to stderr under --json (github#3352) - d5ba062 bundle update uninstalls dropped components (github#3353) - f1a8d8f release 0.12.7, begin 0.12.8.dev0 (github#3398) - 10d4bca guard _sha256 against unreadable managed files (github#3376) - a307894 return None on malformed GHES port (github#3379) - 882e1e9 exit cleanly on malformed IPv6 URLs in add commands (github#3369) Conflict: presets/_commands.py — combined IPv6 try/except + _esc() with fork accent() theming. Assisted-by: OpenCode (model: glm-5.2, autonomous)
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.
Description
When
specify bundle updaterefreshes a bundle to a new version that drops a component, that component is permanently orphaned.install_bundleiterates onlyplan.components(the new version's set). A component the previous version owned but the new one no longer ships is:contributedonly holdsplan.components).With no record referencing it,
remove_bundlecan never clean it up — it's stranded forever, violating the provenance invariant that records capture exactly what a bundle contributed (FR-022).Reproduced on main @
bba473cwith the repo'sFakeInstaller: install v1 (ext-a + ext-b), update to v2 (ext-a only,refresh=True) →remove_callsis empty,ext-bstays ininstalled, and the record lists only ext-a.Fix
After the component loop, when
refreshis True and a prior record exists, uninstall each previously-owned component absent from the new plan — unless another bundle still needs it (components_still_neededrefcount, mirroringremove_bundle), in which case it stays installed and is simply de-attributed from this bundle. The removal runs inside the existingtry, so a failure takes the same bounded-rollback / no-record-written path as the rest of the install.Testing
New tests in
test_bundler_install_flow.py:test_update_uninstalls_components_dropped_by_new_version: v1(ext-a,ext-b) → v2(ext-a) →ext-bis inremove_callsandresult.uninstalled, gone frominstalled, and dropped from the record. Fails before (ext-borphaned — verified by source-stash), passes after.test_update_keeps_component_still_needed_by_sibling_bundle: a sibling bundle also ownsext-b→ the update does not remove it; it stays installed and is only de-attributed from the updated bundle.Full
test_bundler_install_flow.py: 13 passed.uvx ruff checkclean.AI Disclosure
Found and fixed with Claude Code (Claude Fable 5) under my direction. AI traced the orphaning to the refresh path skipping dropped components; I reproduced it with FakeInstaller, mirrored
remove_bundle's refcount logic, verified fail-before/pass-after including the shared-component case, and reviewed the diff.