fix(install): clean up stale deployed files on rename/remove within a package (#666)#750
fix(install): clean up stale deployed files on rename/remove within a package (#666)#750Boubalou wants to merge 6 commits intomicrosoft:mainfrom
Conversation
…anup Pure set-difference helper that identifies deployed files no longer produced by the current install, enabling file-level cleanup to complement the existing package-level detect_orphans. Refs: microsoft#666
After each package's integration, diff the previous deployed_files (from the lockfile) against the fresh integration output and delete files that were renamed away or removed inside the package. Runs once per install, after the package-level orphan cleanup, mirroring the existing safety patterns (validate_deploy_path, cleanup_empty_parents, diagnostics error guard, verbose logging). Respects --dry-run. Closes microsoft#666
Two integration tests using a local-path package fixture: - renamed file cleanup on full install - renamed file cleanup on partial install (--only=apm) Refs: microsoft#666
|
@microsoft-github-policy-service agree company="Mirego" |
There was a problem hiding this comment.
Pull request overview
This PR fixes install-time drift handling by removing stale deployed files when a package remains installed but its integrated output changes (e.g., file rename/removal), keeping both disk and apm.lock.yaml consistent with the current integration result.
Changes:
- Added
detect_stale_files()helper insrc/apm_cli/drift.pyto compute per-package stale deployed paths via set difference. - Extended
apm installto delete stale deployed paths for packages processed in the current run, with safety validation and diagnostics. - Added unit + integration coverage for stale-file detection and rename cleanup (including partial installs), and adjusted integration test token skipping to allow local-only tests to run without GitHub auth.
Show a summary per file
| File | Description |
|---|---|
src/apm_cli/drift.py |
Documents and implements file-level stale drift detection via a new pure helper. |
src/apm_cli/commands/install.py |
Adds post-integration stale-file cleanup to remove previously deployed paths that are no longer produced. |
tests/unit/test_stale_file_detection.py |
Adds focused unit tests for detect_stale_files() semantics (empty/identity/rename/removal/etc.). |
tests/integration/test_diff_aware_install_e2e.py |
Adds E2E coverage for rename cleanup using a local-path dependency; moves token skips to only the networked test classes. |
Copilot's findings
Comments suppressed due to low confidence (4)
src/apm_cli/commands/install.py:2648
- This inline comment uses a Unicode em dash ("—"), but repo policy requires printable ASCII only in source files. Please replace it with ASCII punctuation (e.g., "--").
prev_dep = existing_lockfile.get_dependency(dep_key)
if not prev_dep:
continue # new package this install — nothing stale yet
old_deployed = builtins.list(prev_dep.deployed_files)
tests/integration/test_diff_aware_install_e2e.py:416
- This assertion message includes a Unicode em dash ("—"), but repo policy requires printable ASCII only in source files and CLI/test output strings. Please replace it with ASCII punctuation.
assert deployed_before, "No deployed files found — cannot verify cleanup"
tests/integration/test_diff_aware_install_e2e.py:453
- This docstring uses a Unicode em dash ("—"), but repo policy requires printable ASCII only in source files. Please replace it with ASCII punctuation (e.g., "--").
"""`apm install --only=apm` on a package with a renamed file still cleans up.
Verifies that partial installs clean files for the packages they touch
— a deliberate departure from detect_orphans (package-level), which
no-ops on partial installs."""
tests/integration/test_diff_aware_install_e2e.py:403
- These newly added step comments use Unicode box-drawing characters ("──"), but repo policy requires printable ASCII only in source files. Please replace with ASCII equivalents (e.g., "-- Step 1: ... --").
"""Rename a source primitive, re-install, assert old files gone and
lockfile deployed_files no longer lists the stale paths."""
# ── Step 1: initial install ──
_write_apm_yml_local(temp_project, local_pkg_root)
result1 = _run_apm(apm_command, ["install"], temp_project)
- Files reviewed: 4/4 changed files
- Comments generated: 5
Per .github/instructions/encoding.instructions.md, source files must stay within printable ASCII for Windows cp1252 compatibility. Replace U+2014 (em dash) and U+2500 (box drawing light horizontal) in the lines added by this PR. Pre-existing violations on main are left untouched. Refs: microsoft#666
…tion Adds a bullet to the Diff-Aware Installation list in the apm install reference, documenting that files from a still-present package that are no longer produced (rename/remove) are cleaned up on the next install. Complements the existing package-level orphan cleanup bullet. Refs: microsoft#666
|
Addressed the review. Two new commits on the branch:
Replaced U+2014 (em dash) and U+2500 (box drawing light horizontal) with ASCII equivalents (
Added a bullet under the "Diff-Aware Installation (manifest as source of truth)" section in On Not updated. That file is a one-liner table ( |
| # Skip all tests if no GitHub token is available | ||
| pytestmark = pytest.mark.skipif( | ||
| not os.environ.get("GITHUB_APM_PAT") and not os.environ.get("GITHUB_TOKEN"), | ||
| reason="GITHUB_APM_PAT or GITHUB_TOKEN required for GitHub API access", | ||
| ) | ||
|
|
||
|
|
There was a problem hiding this comment.
Removing this file level block and adding it three times below does not seem appropriate
Summary
Fixes #666. After an
apm install, files that were in a package's previousdeployed_filesbut are no longer produced by the current integration are now removed from disk (and fromapm.lock.yaml).Before this change, the existing
detect_orphans()cleaned up deployed files only when a package was removed fromapm.yml. File-level renames or removals inside a still-present package left stale artifacts that users had to delete manually.Approach
src/apm_cli/drift.py:detect_stale_files(old_deployed, new_deployed) -> set. Pure set difference. Placed alongsidedetect_orphansand documented in the module-level "kinds of drift" list.src/apm_cli/commands/install.py: a single post-loop cleanup placed immediately after the existing package-level orphan cleanup and before lockfile generation. It iteratespackage_deployed_files, looks up the priordeployed_filesfromexisting_lockfile, computes stale paths, and deletes them.The block mirrors the patterns established by the orphan cleanup directly above it:
BaseIntegrator.validate_deploy_pathBaseIntegrator.cleanup_empty_parentsdiagnostics.count_for_package(dep_key, 'error') > 0skipdiagnostics.error(...)+logger.verbose_detail(...), failed paths kept indeployed_filesfor retrylogger.verbose_detail(f"Removed N stale file(s) from {dep_key}")The block scopes naturally to packages touched by the current install, so partial installs (
apm install <pkg>) clean their own stale files — a deliberate, useful departure fromdetect_orphans, which no-ops for partial installs.Scope notes
--dry-runhandling. The currentinstall()command short-circuits at line 796 for dry-run (prints what would be installed and returns) without invoking_install_apm_dependencies. So neither the existing orphan cleanup nor this new stale cleanup runs in dry-run mode. Extending dry-run to simulate the full install is a separate change..apm/-sourced primitives atinstall.py:984-1033(vialocal_deployed_files) is left untouched. Unifying the two via a shared helper would be a welcome follow-up if maintainers want it — I kept them separate here to keep the diff reviewable.mainalready has pre-existing black/isort drift, and CI does not enforce those tools (only a customLint - no raw str(relative_to) patternsgrep). Running the formatters would spread cosmetic noise across files we otherwise leave untouched. The diff is kept surgical. Happy to add a formatting-only follow-up commit if you prefer.Testing
tests/unit/test_stale_file_detection.py(6 tests): empty sets, identity, rename, removal, addition, order/duplicates.tests/integration/test_diff_aware_install_e2e.py::TestFileRenamedWithinPackage(2 tests), using a throwaway local-path package fixture (no network, no token):--only=apm(asserts disk + lockfile)The file-level
pytestmarkskip was moved into each existing class so the new tests can run withoutGITHUB_APM_PAT.Local results:
tests/unit tests/test_console.py: 3881 passedTestFileRenamedWithinPackage: 2 passeddetect_stale_files: 6 passedCloses #666