Skip to content

fix: preserve executable permission bits on install (closes #1563)#1566

Merged
danielmeppiel merged 2 commits into
mainfrom
bbs/fix-1563-exec-bit
Jun 1, 2026
Merged

fix: preserve executable permission bits on install (closes #1563)#1566
danielmeppiel merged 2 commits into
mainfrom
bbs/fix-1563-exec-bit

Conversation

@danielmeppiel
Copy link
Copy Markdown
Collaborator

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.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 1, 2026 09:16
@danielmeppiel danielmeppiel added the status/shepherding Actively being driven by an APM shepherd run label Jun 1, 2026
@danielmeppiel danielmeppiel self-assigned this Jun 1, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 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_attr during 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>
@danielmeppiel
Copy link
Copy Markdown
Collaborator Author

APM Review Panel: ship_now

Executable-bit preservation fix ships with integration test coverage and no outstanding concerns.

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

This PR fixes a concrete install correctness defect: executable permission bits were silently dropped during reflink copy and Artifactory archive extraction. The fix is narrow, bounded by the existing path-traversal guard and 0o755 mask, and now defended by unit plus integration-with-fixtures coverage. CI is green across all reported checks.

Aligned with: portable-by-manifest: installed artifacts preserve filesystem semantics; pragmatic-as-npm: installed scripts work without manual chmod.

Panel summary

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
Loading
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]
Loading

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 -- deleted os.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.

@danielmeppiel danielmeppiel removed the status/shepherding Actively being driven by an APM shepherd run label Jun 1, 2026
@danielmeppiel danielmeppiel merged commit 29666e6 into main Jun 1, 2026
14 checks passed
@danielmeppiel danielmeppiel deleted the bbs/fix-1563-exec-bit branch June 1, 2026 13:58
danielmeppiel added a commit that referenced this pull request Jun 1, 2026
* 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>
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 can drop executable bits for skill scripts in archive/reflink paths

2 participants