fix: preserve executable permission bits on install (closes #1563)#1566
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes an install-time permissions regression by ensuring executable permission bits are preserved when APM materializes package files via (1) the reflink fast path and (2) the Artifactory ZIP extraction path, aligning behavior with the registry ZIP extractor.
Changes:
- Preserve file mode metadata on successful reflink clones by copying stat metadata after
clone_file(...). - Apply ZIP entry Unix mode bits from
ZipInfo.external_attrduring Artifactory archive extraction. - Add regression tests covering both the reflink fast path and Artifactory ZIP extraction.
Show a summary per file
| File | Description |
|---|---|
src/apm_cli/utils/file_ops.py |
Copies file metadata after successful reflink clone so executable bits are preserved. |
src/apm_cli/deps/download_strategies.py |
Applies ZIP Unix mode bits when extracting Artifactory archives to preserve executability. |
tests/unit/test_file_ops.py |
Adds a unit test asserting reflink fast path preserves executable bits. |
tests/unit/test_download_strategies.py |
Adds a unit test asserting Artifactory ZIP extraction preserves executable bits. |
Copilot's findings
- Files reviewed: 4/4 changed files
- Comments generated: 2
| dst = tmp_path / "dest.sh" | ||
| src.write_text("#!/bin/sh\n") | ||
| src.chmod(0o755) | ||
|
|
|
|
||
|
|
||
| def test_artifactory_archive_preserves_executable_bits(tmp_path): | ||
| response = SimpleNamespace(status_code=200, content=_zip_with_executable_script()) |
Adds integration-tier coverage for the Artifactory archive extraction path so the executable-bit fix is guarded through the real download delegate fixture, addressing CEO follow-up FU-1. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 0 | 0 | Small, architecture-local fix matching registry extractor and copy2 metadata semantics. |
| CLI Logging Expert | 0 | 0 | 0 | No CLI output, logging, or diagnostic rendering touched. |
| DevX UX Expert | 0 | 0 | 0 | Improves install/run expectations by keeping scripts executable. |
| Supply Chain Security Expert | 0 | 0 | 0 | Permission preservation is bounded by path safety and mode masking. |
| OSS Growth Hacker | 0 | 0 | 0 | Internal installer correctness fix with no growth-surface changes. |
| Test Coverage Expert | 0 | 0 | 0 | Unit and integration coverage now defend the changed permission paths. |
B = highest-signal findings, R = recommended, N = nits. Counts are signals, not gates. The maintainer ships.
Architecture
classDiagram
class DownloadDelegate {
+download_artifactory_archive()
}
class FileOps {
+robust_copy2()
-_reflink_copy_file()
}
class ZipInfo {
+external_attr
}
DownloadDelegate ..> ZipInfo : reads unix mode
FileOps ..> shutil : copystat after clone
flowchart TD
A[Artifactory ZIP archive] --> B[DownloadDelegate.download_artifactory_archive]
B --> C[path traversal guard]
C --> D[write file]
D --> E[chmod unix_mode and 0o755]
F[robust_copy2] --> G[clone_file fast path]
G --> H[shutil.copystat]
Recommendation
Ship now. The foldable test-coverage follow-up was resolved in 9600c8c7, and no remaining panel finding needs action in this PR.
Folded in this run
- (panel) Add integration-tier assertion that Artifactory archive extraction preserves executable bits through the real fixture path -- resolved in 9600c8c.
Regression-trap evidence (mutation-break gate)
tests/integration/test_download_strategies_selection.py::TestDownloadArtifactoryArchive::test_executable_bits_survive_archive_extraction-- deletedos.chmod(dest, unix_mode & 0o755); 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 silent. Full local lint gate also passed: pylint duplicate guard and auth-signal lint clean.
CI
gh pr checks 1566 --repo microsoft/apm: 13 successful, 0 failing, 0 cancelled, 0 pending checks (after 0 CI fix iteration(s)).
Mergeability status
| PR | head SHA | CEO stance | iters | folds | defers | Copilot rounds | CI | mergeable | mergeStateStatus | notes |
|---|---|---|---|---|---|---|---|---|---|---|
| #1566 | 9600c8c |
ship_now | 1 | 1 | 0 | 2 | green | MERGEABLE | BLOCKED | pending required review |
Convergence
1 outer iteration(s); 2 Copilot round(s). Final panel recommendation: ship_now.
Ready for maintainer review.
Full per-persona findings
Python Architect
No findings.
CLI Logging Expert
No findings.
DevX UX Expert
No findings.
Supply Chain Security Expert
No findings.
OSS Growth Hacker
No findings.
Auth Expert -- inactive
Changed files handle executable-bit preservation in download/file copy paths and tests; no AuthResolver, token, credential, or host classification behavior changes.
Doc Writer -- inactive
Runtime install internals and tests only; no user-facing documentation currently describes executable-bit preservation semantics, so no doc drift is created.
Test Coverage Expert
No findings.
This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.
* fix(tests): skip POSIX execute-bit assertions on Windows
Two unit tests asserted st_mode & 0o111 == 0o111, which fails on
Windows because NTFS does not honor POSIX execute bits. Guard both
with pytest.mark.skipif(sys.platform == 'win32'), matching the
existing convention used elsewhere in the suite.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scripts): guard empty PYTEST_EXTRA_ARGS array under bash 3.2
scripts/test-integration.sh runs under `set -euo pipefail`. On macOS
the default /bin/bash is 3.2, where expanding an empty array with a
bare "${arr[@]}" raises an unbound-variable error. Local integration
runs (PYTEST_EXTRA_ARGS unset) aborted before pytest with
'extra_args[@]: unbound variable'. Use the ${arr[@]+"${arr[@]}"}
guard so the empty-array expansion is safe; CI behaviour (with
PYTEST_EXTRA_ARGS set) is unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: release v0.16.1
Roll the [Unreleased] changelog into a dated 0.16.1 block and bump
pyproject.toml + uv.lock. Adds the previously-missing user-facing
entries for #1539 (apm doctor), #1566, #1569, #1567, #1553, #1552,
and #1538 surfaced by enumerating merged PRs since v0.16.0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* ci: parallelise macOS release integration tests to fix 20min timeout
The v0.16.1 release pipeline failed on the macOS x86_64 (Intel) build:
the consolidated job's "Run integration tests" step hit its 20-minute
timeout at ~61% progress. The tests were passing -- the slower Intel
runner simply could not finish the full suite serially in time, and the
arm64 runner was also near the edge.
Unlike ci-integration.yml, which shards the suite across four runners,
the release workflow runs the whole integration suite on a single
scarce macOS runner. Parallelise it in-process with xdist (-n 2,
matching ci-integration's per-shard width to bound shared-PAT API
concurrency) using --dist loadgroup so the home_env xdist_group markers
keep HOME-mutating tests serialized on one worker. Also raise the step
timeout to 30 minutes for headroom on the slow Intel runner.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: danielmeppiel <danielmeppiel@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Preserves executable permission bits when materializing package files through the reflink copy fast path and the Artifactory ZIP extraction path, aligning archive extraction with the registry extractor's Unix mode handling. How to test: uv run --locked --extra dev pytest tests/unit/test_file_ops.py tests/unit/test_reflink_phase3w5.py tests/unit/test_download_strategies.py tests/unit/install/test_artifactory_resolver.py -q; uv run --locked --extra dev ruff check src/ tests/ --quiet; uv run --locked --extra dev ruff format --check src/ tests/ --quiet. Closes #1563.