Skip to content

Fix cross-package file protection in stale cleanup (#1831)#1856

Merged
danielmeppiel merged 3 commits into
mainfrom
sergio-sisternes-epam-fix-1831-stale-file-cleanup-on-update
Jun 20, 2026
Merged

Fix cross-package file protection in stale cleanup (#1831)#1856
danielmeppiel merged 3 commits into
mainfrom
sergio-sisternes-epam-fix-1831-stale-file-cleanup-on-update

Conversation

@sergio-sisternes-epam

Copy link
Copy Markdown
Collaborator

Description

When apm update fetches a new version of a package where upstream deleted a file, the stale-file cleanup phase correctly removes the orphaned local copy. However, if another currently-installed package also deploys a file at the same path, that file could be incorrectly deleted because there was no cross-package ownership check.

This PR adds a cross-package ownership guard to the cleanup phase: before passing stale files to deletion, it builds a union of all other packages' deployed_files and excludes any shared paths from the stale set. It also adds regression coverage for the legacy-lockfile graceful-skip behaviour (empty deployed_files from old APM versions).

Fixes #1831

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Maintenance / refactor

Testing

  • Tested locally
  • All existing tests pass (1661 install unit tests)
  • Added tests for new functionality (if applicable)

New tests in TestCrossPackageProtection:

  • Shared file is NOT removed when another package still claims it
  • File IS removed when no other package claims it

New tests in TestLegacyLockfileGracefulSkip:

  • Empty deployed_files (legacy lockfile) gracefully skips stale cleanup

Spec conformance (OpenAPM v0.1)

  • N/A -- this PR does not change OpenAPM-observable behaviour.

Add cross-package ownership check in the cleanup phase: before
deleting a stale file, verify no other currently-installed package
claims it in its deployed_files. This prevents incorrect deletion
when two packages share a path and one is updated.

Also add regression test for legacy lockfiles with empty
deployed_files confirming the graceful-skip behaviour (no stale
removal attempted, new deployed_files recorded for future diffs).

Closes #1831

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 19, 2026 18:00
@sergio-sisternes-epam sergio-sisternes-epam added the panel-review Trigger the apm-review-panel gh-aw workflow label Jun 19, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a correctness gap in the install/update stale-file cleanup phase: when updating one package removes a file that still exists because another installed package also deploys it, cleanup must not delete the shared path.

Changes:

  • Adds cross-package ownership filtering in cleanup.run() to exclude paths still deployed by other packages from the stale deletion set.
  • Adds unit tests covering shared-path protection and the “legacy lockfile has empty deployed_files” skip behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/apm_cli/install/phases/cleanup.py Introduces cross-package deployed-file union filtering before calling remove_stale_deployed_files.
tests/unit/install/phases/test_cleanup_phase.py Adds regression tests for cross-package protection and legacy lockfile empty-deployed_files skip behavior.

Comment thread src/apm_cli/install/phases/cleanup.py Outdated
Comment on lines +139 to +143
other_deployed: set = set()
for other_key, other_files in package_deployed_files.items():
if other_key != dep_key:
other_deployed.update(other_files)
stale = stale - other_deployed
Comment on lines +529 to +533
ctx = _make_ctx(
existing_lockfile=lf,
intended_dep_keys={"pkg-a", "pkg-b"},
package_deployed_files={
"pkg-a": [],
@github-actions github-actions Bot mentioned this pull request Jun 19, 2026

@phillipschandler19-web phillipschandler19-web left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        - name: Download a Build Artifact

uses: actions/download-artifact@v3.1.0
with:
# Artifact name
name: # optional
# Destination path
path: # optional
dependencies:
apm:
# 1. Shorthand, github.com is implicit
- acme/standards#v1.2.0

# 2. Shorthand with explicit host (GHE Cloud, GHES, GitLab)
- acme.ghe.com/acme/standards#v1.2.0
- gitlab.com/acme/standards#v1.2.0

# 3. Azure DevOps shorthand: org/project/repo
- dev.azure.com/acme-org/platform/standards#v1.2.0

# 4. Object form (any git URL, any port, custom protocol)
- git: ssh://git@bitbucket.acme.com:7999/team/standards.git
  ref: v1.2.0

# 5. Local path (file:// equivalent for unpacked bundles)
- ./vendor/standardshttps://opencode.ai/installgitlab.com/acme/standards#v1.2.0export GITHUB_APM_PAT=ghp_your_token

apm install

@phillipschandler19-web phillipschandler19-web left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sergio-sisternes-epam-fix-1831-stale-file-cleanup-on-update

prev_dep = existing_lockfile.get_dependency(dep_key)
if not prev_dep:
continue # new package this install -- nothing stale yet
stale = detect_stale_files(prev_dep.deployed_files, new_deployed)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

phillipschandler19-web

This comment was marked as spam.

danielmeppiel and others added 2 commits June 20, 2026 20:08
Precompute deployed-file ownership, add verbose breadcrumbs for protected stale paths, and add an integration regression for the shared-file update scenario. Addresses panel follow-ups from python-architect, cli-logging-expert, supply-chain-security-expert, performance-expert, and test-coverage-expert.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update the cleanup phase module docstring so it no longer claims the file is an unchanged extraction after the cross-package stale-file protection work. Addresses python-architect panel follow-up.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel

Copy link
Copy Markdown
Collaborator

APM Review Panel: ship_now

PR #1856 fixes stale-file cleanup so an update cannot delete a path that another installed package still deploys.

cc @sergio-sisternes-epam -- a fresh advisory pass is ready for your review.

The panel signals converged after the shepherd fold. The original cross-package ownership guard was directionally correct; this pass folded the in-scope follow-ups by precomputing the deployed-file union, adding verbose-only breadcrumbs for protected stale paths, documenting that integration outcomes are not the security boundary, and adding an integration-with-fixtures regression for two local packages sharing one deployed prompt. CI is green on the latest pushed head.

Aligned with: pragmatic-as-npm: update cleanup now behaves predictably for shared files while keeping the normal stale-deletion path intact; secure-by-default: destructive deletion still flows through content-hash provenance checks.

Panel summary

Persona B R N Takeaway
Python Architect 0 0 0 Final cleanup flow is simple: precompute ownership once, filter stale paths, then delegate deletion to the canonical cleanup helper.
CLI Logging Expert 0 0 0 Default output stays quiet for non-deletions; verbose mode now explains protected stale paths.
DevX UX Expert 0 0 0 Shared-file updates now match package-manager user expectations: do not delete files still owned by another dependency.
Supply Chain Security Expert 0 0 0 The guard is reliability-oriented and explicitly does not replace content-hash deletion provenance.
OSS Growth Hacker 0 0 0 Strong community bug fix with concrete regression coverage.
Test Coverage Expert 0 0 0 Unit and integration-with-fixtures tests cover the regression; mutation-break proved the guard is load-bearing.
Performance Expert 0 0 0 Ownership union is precomputed once; overhead is bounded and negligible.

Recommendation

Ship from the maintainer queue. No in-scope follow-ups remain from this shepherd pass.


Full per-persona findings

No remaining findings after the folded commits:

  • 0c3fcddb486c8b282f27786e009650a96d3b2b02 folded precomputation, verbose breadcrumbs, integration-tier coverage, and trust-boundary documentation.
  • 04e74e394765332414cbe7c1b10b62baba5887bc refreshed the cleanup phase module context after the behavior change.

Folded in this run

  • (panel) Precompute cross-package deployed-file ownership instead of rebuilding it per dependency -- resolved in 0c3fcddb486c8b282f27786e009650a96d3b2b02.
  • (panel) Add verbose-only diagnostics for stale paths kept because another package still deploys them -- resolved in 0c3fcddb486c8b282f27786e009650a96d3b2b02.
  • (panel) Document that package_deployed_files is integration outcome, not a security boundary -- resolved in 0c3fcddb486c8b282f27786e009650a96d3b2b02.
  • (panel) Add integration-with-fixtures coverage for the two-local-package shared-file update scenario -- resolved in 0c3fcddb486c8b282f27786e009650a96d3b2b02.
  • (panel) Refresh the cleanup phase module docstring after the behavior change -- resolved in 04e74e394765332414cbe7c1b10b62baba5887bc.

Copilot signals reviewed

  • pull review 4534610106 -- NOT-LEGIT: overview-only Copilot review; no actionable inline finding was present after two fetch rounds.

Regression-trap evidence (mutation-break gate)

  • tests/unit/install/phases/test_cleanup_phase.py::TestCrossPackageProtection::test_shared_file_not_removed_when_other_package_deploys_it -- deleted stale = stale - other_deployed; test FAILED as expected; guard restored.

Lint contract

uv run --extra dev ruff check src/ tests/ and
uv run --extra dev ruff format --check src/ tests/ both passed on the latest pushed head.

CI

gh pr checks 1856 --repo microsoft/apm --watch observed all required checks green on 04e74e394765332414cbe7c1b10b62baba5887bc after 0 CI fix iteration(s). CI run: https://github.com/microsoft/apm/actions/runs/27879757167

Mergeability status

Captured from gh pr view 1856 --json mergeable,mergeStateStatus,statusCheckRollup immediately after the last push of this run.

PR head SHA CEO stance iters folds defers Copilot rounds CI mergeable mergeStateStatus notes
#1856 04e74e3 ship_now 2 5 0 2 green MERGEABLE BLOCKED awaiting maintainer review

Convergence

2 outer iteration(s); 2 Copilot round(s). Final panel recommendation: ship_now.

Ready for maintainer review.

This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.

@danielmeppiel danielmeppiel removed the panel-review Trigger the apm-review-panel gh-aw workflow label Jun 20, 2026
@danielmeppiel danielmeppiel merged commit 704ad9f into main Jun 20, 2026
14 checks passed
@danielmeppiel danielmeppiel deleted the sergio-sisternes-epam-fix-1831-stale-file-cleanup-on-update branch June 20, 2026 19:47
danielmeppiel added a commit that referenced this pull request Jun 20, 2026
…#1871)

Add the user-facing PRs merged after the v0.21.0 tag that were missing
from the changelog (#1830 Added; #1854, #1856 Fixed), add the external-
contributor credit on #1855, and move #1853 out of the released [0.21.0]
section into [Unreleased] since it merged after the v0.21.0 tag. No version
bump -- everything stays under [Unreleased].

Co-authored-by: danielmeppiel <danielmeppiel@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

apm install/update does not remove locally deployed files that were deleted from upstream package

4 participants