fix(integrations): don't abort uninstall when the manifest file can't be deleted - #3805
Open
jawwad-ali wants to merge 1 commit into
Open
fix(integrations): don't abort uninstall when the manifest file can't be deleted#3805jawwad-ali wants to merge 1 commit into
jawwad-ali wants to merge 1 commit into
Conversation
…eleted `IntegrationManifest.uninstall()` guards every tracked-file `path.unlink()` with `except OSError: skipped.append(path)`, but the manifest's own `manifest.unlink()` is bare. The manifest is deleted *last*, so an undeletable manifest (read-only file, a directory left at the path, a Windows lock) raises after the tracked files are already gone. The caller loses the `(removed, skipped)` result and never runs its post-uninstall bookkeeping — reassigning the default integration, rewriting/removing `integration.json`, clearing init options — leaving a removed integration still recorded as installed. Report it in `skipped` like any other file we could not remove, mirroring the `path.unlink()` guard above and the same `except OSError: skipped.append(...)` pattern in kimi's legacy-directory cleanup. Co-Authored-By: Claude Opus 5 (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.
Problem
IntegrationManifest.uninstall()(src/specify_cli/integrations/manifest.py) guards every tracked-file delete:…but the manifest's own delete is bare:
The manifest is deleted last, after every tracked file is already gone. So an undeletable manifest — a read-only file, a directory left at the path, a Windows lock, an ACL — raises out of
uninstall()when the destructive work has already happened.Why this matters more than a lost return value
The caller doesn't just lose
(removed, skipped). Everything after the call is skipped (integrations/_install_commands.py:282):_write_integration_json(...)/_remove_integration_json(...)_clear_init_options_for_integration(...)So the files are gone but
integration.jsonstill records the integration as installed — and possibly still as the default. The project is left in a state the CLI can't reach again throughuninstall.Reproduction on current
main(2e44ed6)Leaving a directory at the manifest path (portable, no chmod, no monkeypatch):
After the fix, same harness:
Fix
+13/-1 in one source file — guard it like its sibling:
The empty-parent cleanup below is deliberately left unconditional rather than moved into an
else:. With the manifest still on disk its parent is non-empty, so the firstrmdir()raises and breaks immediately — anelse:would be behaviourally identical while re-indenting 8 lines for no reason. I mention it so the omission reads as a decision, not an oversight.Precedent
path.unlink()guard in this same function is the pattern being mirrored — the_sha256guard added by merged fix(integrations): guard _sha256 against unreadable managed files #3376 points at it explicitly: "(mirrors the path.unlink() OSError guard below)". fix(integrations): guard _sha256 against unreadable managed files #3376 hardened this function againstOSErrorfrom managed files but left the manifest unlink bare.integrations/kimi/__init__.py:151already doesexcept OSError: skipped.append(legacy_dir)for an undeletable legacy directory.Anticipating one review question
The CLI renders
skippedunder a header along the lines of "N modified file(s) were preserved", and an undeletable manifest isn't "modified". I still thinkskippedis right: the docstring commits only to`(removed, skipped)` — absolute paths, and kimi already puts undeletable directories there. The alternative — a third return value — would be a signature change to a public method for a rare I/O error. Happy to adjust the CLI wording in a follow-up if you'd rather the header were broader.Verification
PermissionError: [WinError 5]on unpatchedsrcand passes with the fix.tests/integrations/test_manifest.py→ 48 passed, 6 failed; the same 6 fail on cleanmain(2e44ed6) with 47 passed — all six are Windows symlink-privilege tests (test_rejects_symlink_target,test_symlink_removed_with_force, …), unrelated to this change. Delta is exactly my one new passing test.tests/integrations/→ 2253 passed, 19 skipped; failures identical to baseline.uvx ruff@0.15.0 check→ cleanThe new test needs no chmod and no monkeypatch:
Path.unlink()on a directory raisesIsADirectoryErroron Linux andPermissionErroron Windows/macOS — bothOSErrorsubclasses, so it exercises the guard on every CI platform.Written with assistance from Claude Code. Bug found, reproduced, and verified by me on current
main; every number above is from output I ran.