Repository navigation
fix(install): shorten staging path segments to clear Windows MAX_PATH - #2941
Conversation
The staging root used a full uuid4().hex and each destination slot a full sha256 hexdigest, 96 hex characters before any real path. On a realistic Windows project root that pushed staged paths past 260 and failed the install with WinError 206. The root is now 12 hex characters and the slot 16, freeing 68. The orphan-detection pattern matches both lengths so an upgrade still collects staging roots left by an older version. Closes microsoft#2896
There was a problem hiding this comment.
🟡 Changes recommended
Two moderate test coverage and path-length validation issues remain unresolved.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Shortens staging path identifiers to prevent Windows MAX_PATH failures while preserving legacy cleanup compatibility.
Changes:
- Limits staging roots to 12 hex characters.
- Limits destination slots to 16 hex characters.
- Adds path-length and cleanup regression tests.
File summaries
| File | Summary |
|---|---|
tests/unit/install/test_resolution_staging_relocate.py |
Adds path-length coverage; needs the windows_compat marker and a full nested staged-file path assertion. |
tests/unit/install/test_install_transaction.py |
Tests cleanup for current and legacy staging-root formats. |
src/apm_cli/install/resolution_staging.py |
Implements shorter identifiers and legacy staging-root matching. |
Review details
Suppressed comments (1)
tests/unit/install/test_resolution_staging_relocate.py:244
- The length calculation only measures the staging root through the 16-character slot; the destination's nested components are hashed away, and no representative file path is appended below
replacement. This can pass while a path such as the nestedbacklog/release-notes/...content from issue #2896 still exceeds MAX_PATH, so build a representative nested staged-file path and assert its full length.
relative_len = len(str(replacement.relative_to(modules))) + len("apm_modules") + 1
old_scheme_relative_len = relative_len + (32 - 12) + (64 - 16)
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| assert [path.name for path in modules.iterdir()] == ["mixedorg"] | ||
|
|
||
|
|
||
| def test_prepare_replacement_slot_names_fit_windows_max_path(tmp_path: Path) -> None: |
The new path-length test guards Windows behaviour but carried no marker, so the PR-time Windows Compatibility Gate skipped the one case it exists for. This file marks per-test rather than at module level, so the marker goes on the test.
|
Added in 6fc0cf1. This file marks per-test rather than at module level, so the marker went on the test rather than a module-level
|
|
The bounded staging-path repair looks aligned with the stabilization priorities, but I do not have capacity to review this one myself. I am removing my pending review request so it does not imply otherwise. Sergio Sisternes (@sergio-sisternes-epam), can you confirm whether you have capacity to review the Windows/staging scope or coordinate a willing reviewer? The existing request for your review is not being treated as confirmed availability. For now #2896 stays in Next. We are not adding status/accepted or issuing a formal scope-approval record without a willing review contact. Mohammed Alkindi (@MohammedAlkindi), please do not spend time on further rebases or unrelated Windows failures for our benefit while we arrange review support. The intended review remains bounded to shorter transient staging paths, collision/concurrency safety and legacy cleanup; no arbitrary long-path guarantee or broad Windows cleanup is being requested. |
PR triage recommendationneeds-issue Recommendation only, not merge or scope approval. A responsible Linked issue#2896 (open; Proposed classification
Suggested next actionWait for a responsible human maintainer to record bounded scope, CODEOWNERS for this tree: Daniel Meppiel (@danielmeppiel) Sergio Sisternes (@sergio-sisternes-epam). Thank you for contributing this pull request. APM starts with an Generated by autopilot-pr-triage-worker. This comment is AI-generated and may contain errors. |
|
Mohammed Alkindi (@MohammedAlkindi) Daniel Meppiel (@danielmeppiel) I'm accepting this and looking for support to get this reproduced and tested on Windows before merging. |
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 1 | 0 | Shorter opaque segments preserve rollback and legacy cleanup, but the Windows regression test does not exercise a nested staged file. |
| CLI Logging Expert | 0 | 0 | 0 | No CLI output or diagnostic-rendering surface changed. |
| DevX UX Expert | 0 | 1 | 0 | Shorter opaque staging paths preserve install UX, but the MAX_PATH proof must cover a nested staged file. |
| Supply Chain Security Expert | 0 | 0 | 0 | Scoped entropy, locks, cleanup, and path containment remain sound. |
| OSS Growth Hacker | 0 | 1 | 0 | A release note would help affected Windows users discover the upgrade benefit. |
| Test Coverage Expert | 0 | 1 | 0 | Cleanup covers both root lengths, but the Windows regression lacks a nested staged-file assertion. |
| Performance Expert | 0 | 0 | 0 | Path length shrinks while hashing and cleanup complexity remain unchanged. |
B = highest-severity findings, R = recommended, N = nits. Counts communicate signal strength, not a gate.
Top 2 follow-ups
- [Test Coverage Expert] Assert a representative nested staged file fits the Windows MAX_PATH budget -- the current test proves the opaque replacement root is shorter, not that realistic package content written beneath it remains within the path limit.
- [OSS Growth Hacker] Add an Unreleased Fixed changelog entry for the Windows staging-path fix -- affected users need a durable upgrade reason without overstating support for arbitrary long paths.
Architecture
classDiagram
direction LR
class ResolutionStagingSession {
<<RollbackJournal>>
-_staging_root Path
-_staging_lock FileLock
+prepare_replacement(path) Path
+publish_replacement(replacement) Path
+remove_abandoned_roots() list
-_isolated_staging_path(bucket, destination) Path
}
class InstallTransaction {
+commit(result) InstallResult
}
class FileLock {
<<ExternalLock>>
}
InstallTransaction *-- ResolutionStagingSession : owns
ResolutionStagingSession ..> FileLock : activity marker
flowchart TD
A[download callback] --> B[prepare replacement]
B --> C[Create .apm-resolution-staging 12-hex replacement root]
C --> D[Write package content beneath staged path]
D --> E[publish replacement]
E --> F[Install transaction commit]
F --> G[Remove inactive 12-hex or legacy 32-hex staging roots]
Recommendation
Add the nested staged-file Windows path-budget regression test, then ship this bounded portability fix. Track the concise changelog entry alongside it so the user-facing upgrade value is clear.
Full per-persona findings
Python Architect
- [recommended] Exercise a representative nested file beneath the generated replacement root at
tests/unit/install/test_resolution_staging_relocate.py:228.
Actual resolver paths write package content beneath the checked staging directory, so a nested path can cross MAX_PATH even after the 68-character reduction.
DevX UX Expert
- [recommended] Exercise the MAX_PATH boundary with a nested file in the staged replacement at
tests/unit/install/test_resolution_staging_relocate.py:244.
The existing assertion ends at the opaque slot rather than the installed package content below it.
OSS Growth Hacker
- [recommended] Add an Unreleased Fixed changelog entry in
CHANGELOG.md.
The release note gives affected Windows users a searchable upgrade path.
Test Coverage Expert
- [recommended] Assert a representative nested staged file fits the MAX_PATH budget.
The cleanup coverage proves both root lengths are handled, but no test currently creates the nested staged payload.
CLI Logging Expert, Supply Chain Security Expert, and Performance Expert
No findings.
Auth Expert and Doc Writer
Inactive: the changed staging implementation and tests do not alter authentication or documentation-facing behavior.
This panel is advisory. Re-apply the panel-review label after addressing feedback to re-run.
The MAX_PATH regression stopped at the slot directory, so it proved the opaque segments were shorter but not that package content written beneath them stays under the limit. Expresses the budget as headroom, and records the fix in the changelog.
|
Pushed the nested-payload assertion and the Unreleased Fixed entry the panel asked for in d4a9605. Expressed as headroom the budget is concrete: with a representative nested file beneath the slot, the longest project root that still fits 260 goes from 61 characters to 129, which is exactly the 68 the change frees. Worth flagging that this is headroom and not a long-path guarantee, since the 134-character OneDrive root already used in that test still overflows once package content is appended, even after the fix. On reproducing the original WinError 206, this machine has |
|
That's great, thanks. I did reproduce it locally in the end and the conclusion was that with The failed operation reported So, Sergio Sisternes (@sergio-sisternes-epam) Daniel Meppiel (@danielmeppiel) , I approve this PR fixes the issue with the long path WinError. |
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 0 | 1 | Single-module repair preserves the existing staging authority and legacy cleanup; constants are optional polish. |
| DevX UX Expert | 0 | 0 | 1 | More Windows path headroom without new commands or configuration; simplify implementation-heavy release prose. |
| Supply Chain Security Expert | 0 | 0 | 1 | Existing containment/locking checks remain; explicit collision detection is optional defense-in-depth. |
| OSS Growth Hacker | 0 | 0 | 1 | The requested upgrade note is present; this is a small portability fix, not a broader launch. |
| Doc Writer | 0 | 1 | 1 | Narrow the headroom claim and add implementing-PR/contributor attribution. |
| Test Coverage Expert | 0 | 0 | 0 | Current-head regressions cover shortened names, nested-path budget and dual-format cleanup. |
| Performance Expert | 0 | 0 | 0 | Constant-size truncation and regex alternatives add no scans or asymptotic regression. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 3 follow-ups
- [Doc Writer] Describe bounded headroom, not universal success or 68 characters on every staging path. Suggested wording: "
apm installgains 68 characters of Windows path headroom for hashed resolution-staging slots, reducing MAX_PATH failures. Deep checkouts can still exceed the limit; cleanup continues to recognize older staging-root names." - [Doc Writer] Add
by @MohammedAlkindi (#2941)to the entry; retain the issue reference separately if useful. This is attribution polish, not another missing-changelog request. - [Supply Chain Security Expert] Optional later hardening: detect an already-reserved slot before deleting its contents and name both destinations. No natural hash collision was demonstrated; ordinary per-session collision risk is extremely low. This is not a request to expand this repair.
Architecture
classDiagram
direction LR
class InstallTransaction {
+resolution ResolutionStagingSession
+commit(result) InstallResult
+rollback() None
}
class ResolutionStagingSession {
<<Guard>>
-_staging_root Path
-_lock Lock
+prepare_replacement(path) Path
+commit() list
+rollback() list
+remove_abandoned_roots() list
-_isolated_staging_path(bucket, dest) Path
}
class ensure_path_within {
<<Pure>>
}
class safe_rmtree {
<<IOBoundary>>
}
class _STAGING_NAME {
<<Constant>>
}
InstallTransaction *-- ResolutionStagingSession : owns
ResolutionStagingSession ..> ensure_path_within : guards paths
ResolutionStagingSession ..> safe_rmtree : removes trees
ResolutionStagingSession ..> _STAGING_NAME : cleanup filter
note for ResolutionStagingSession "Mutex-guarded staging:\nuuid4().hex[:12] root\nsha256()[:16] slot\nfullmatch 12 or 32 cleanup"
flowchart TD
A["apm install"] --> B["InstallTransaction"]
B --> C["ResolutionStagingSession"]
C --> D["12-hex session root"]
D --> E{"operation"}
E -->|prepare_replacement| F["_isolated_staging_path"]
F --> G["16-hex destination slot"]
G --> H["Create replacement parent"]
E -->|commit| I["Remove own staging root"]
I --> J["Release own staging lock"]
J --> K["InstallTransaction.commit then calls remove_abandoned_roots"]
K --> L{"12-hex or legacy 32-hex candidate"}
L -->|matches| M["Attempt activity lock"]
M -->|acquired| N["Containment-checked cleanup"]
M -->|busy| O["Keep active session"]
L -->|no match| P["Keep unrelated path"]
E -->|rollback| Q["Restore journaled paths"]
Q --> R["Remove own staging root and release own lock"]
Recommendation
Technically, ship with the bounded changelog follow-ups above. The previous panel's evidence requests are satisfied; no broader Windows redesign, unrelated failure cleanup, or substantive implementation is being requested. Human issue-scope recording remains a separate unresolved governance action, not something this panel or the accepted label can supply. No labels, assignments, reviewer requests, code, or merge state were changed.
Full per-persona findings
Python Architect
- [nit] Centralize repeated staging-name lengths if they change again, at
src/apm_cli/install/resolution_staging.py:20.
The root length appears in generation, cleanup recognition and regression assertions; named constants could prevent drift. At this small scale, the current explicit values and comments are understandable. No new abstraction is needed for this fix.
DevX UX Expert
- [nit] Shorten implementation-heavy changelog wording at
CHANGELOG.md:11.
Users need the affected install symptom and bounded upgrade benefit, not theuuid4().hexand SHA-256 implementation detail. This overlaps the Doc Writer recommendation rather than creating another task.
Supply Chain Security Expert
- [nit] Consider explicit slot-collision detection in
prepare_replacement, atsrc/apm_cli/install/resolution_staging.py:69.
A forced-hash-collision experiment, not a naturally occurring collision, confirmed that a second reservation removes the first staged payload and overwrites the reverse destination mapping. The first publication then resolves to the second destination; a subsequent publication raises the not-reserved error. The error is therefore not a pre-deletion safeguard. Checking for an already-reserved slot before cleanup could make this exceptional failure explicit. With a 64-bit slot and ordinary session sizes, this remains optional hardening, not evidence of a realistic collision in this PR.
OSS Growth Hacker
- [nit] Keep the new release note concise at
CHANGELOG.md:11.
The symptom, bounded headroom benefit and legacy-cleanup compatibility carry the upgrade reason. Internal hash details can be omitted; no README, quickstart or launch work is needed.
Doc Writer
- [recommended] Scope the changelog claim to the paths actually shortened by 68 characters, at
CHANGELOG.md:11.
_isolated_staging_pathsaves 20 characters in the session root plus 48 in the hashed slot.prepare_pathretains the destination-relative backup path and saves only the root's 20 characters. Deep paths can still exceed MAX_PATH. Describe the gain without claiming all deep checkouts now succeed. - [nit] Attribute the implementing PR and external contributor at
CHANGELOG.md:11.
The current suffix references issue Install fails on Windows with WinError 206 (path too long) / WinError 3 due to double-hashed staging paths #2896 rather than PR fix(install): shorten staging path segments to clear Windows MAX_PATH #2941. Add contributor credit in the existing changelog style.
Test Coverage Expert
No findings. The added regression includes the nested payload and Windows marker; current/legacy cleanup is parametrized. Exact-head local commands, using the isolated head source on macOS:
python -m pytest -q tests/unit/install/test_resolution_staging_relocate.py tests/unit/install/test_install_transaction.py
51 passed in 8.36s
python -m pytest -q tests/unit/install/test_resolution_staging_relocate.py tests/unit/install/test_install_transaction.py -m windows_compat
41 passed, 10 deselected in 4.69s
python -m pytest -q tests/integration/test_resolution_staging_recovery.py
3 passed in 0.50s
The marker run is a subset of the first run. The nested-path budget assertion is arithmetic over the generated path; it is not a local Windows filesystem execution. Native default-MAX_PATH behavior is supported separately by the linked Windows report.
Performance Expert
No findings. Identifier truncation adds constant work to the existing path-length-dependent hash. Dual-format matching remains bounded, and orphan enumeration remains per session with unchanged I/O.
This panel is advisory. It does not block merge. Re-apply the
panel-review label after addressing feedback to re-run.
Generated by autopilot-pr-review-worker. This comment is AI-generated and may contain errors.
1c53229
into
microsoft:main
Description
The staging root used a full
uuid4().hexand each destination slot a fullsha256hexdigest: 96 hex characters before any real path. On a realistic Windows project root that pushes staged paths past 260 and fails the install withWinError 206.The root is now 12 hex characters and the slot 16, freeing 68.
_STAGING_NAMEmatches both lengths, so an upgrade still collects staging roots left by an older version.Fixes #2896
Type of change
Testing
ruff checkandruff format --checkon all three files: clean.Touched test files: 48 passed, 3 failed. All three fail identically on the unmodified source. Two need symlink privileges this machine lacks (
WinError 1314);test_workspace_lock_serializes_concurrent_processesis timing-dependent and failed 3/3 without the change.Full
tests/unit/install/: 2135 passed, 411 skipped, 51 failed, dominated by that same symlink limitation. I did not attribute all 51 individually, so I am not claiming the suite is green here.Not a spec change.
Written with Claude Code's help.