diff --git a/CHANGELOG.md b/CHANGELOG.md index b5bf9815..6714b4ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,14 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- `apm audit --format sarif|json|markdown --output` for CI artifact capture — SARIF integrates with GitHub Code Scanning (#330) -- `apm unpack` content scanning — blocks critical hidden characters unless `--force` (#330) -- `SecurityGate` centralizes security scanning with per-command policies — block (install/unpack), warn (compile/pack), report (audit) (#330) +- Audit hardening — `apm unpack` content scanning, SARIF/JSON/Markdown `--format`/`--output` for CI capture, `SecurityGate` policy engine, non-zero exits on critical findings (#330) ### Fixed -- `apm install` now exits non-zero when critical security findings block packages — consistent with `apm unpack` behavior (#330) -- `apm compile` now exits non-zero when critical hidden characters are detected in compiled output (#330) +- File-level downloads from private repos now use OS credential helpers (macOS Keychain, `gh auth login`, Windows Credential Manager) — closes auth gap between folder and file dependencies (#332) +- Lockfile now preserves the host for GitHub Enterprise custom domains so subsequent `apm install` clones from the correct server (#338) ## [0.8.0] - 2026-03-16 diff --git a/src/apm_cli/drift.py b/src/apm_cli/drift.py index 555c977b..f23c6888 100644 --- a/src/apm_cli/drift.py +++ b/src/apm_cli/drift.py @@ -192,7 +192,13 @@ def build_download_ref( if existing_lockfile and not update_refs and not ref_changed: locked_dep = existing_lockfile.get_dependency(dep_ref.get_unique_key()) if locked_dep and locked_dep.resolved_commit and locked_dep.resolved_commit != "cached": - base_ref = dep_ref.repo_url + # Include the host so the downloader can resolve the correct + # server (e.g. GitHub Enterprise custom domains). Without it + # ``DependencyReference.parse()`` would fall back to github.com. + if dep_ref.host: + base_ref = f"{dep_ref.host}/{dep_ref.repo_url}" + else: + base_ref = dep_ref.repo_url if dep_ref.virtual_path: base_ref = f"{base_ref}/{dep_ref.virtual_path}" download_ref = f"{base_ref}#{locked_dep.resolved_commit}" diff --git a/tests/unit/test_install_update.py b/tests/unit/test_install_update.py index 6a92cab9..8b705517 100644 --- a/tests/unit/test_install_update.py +++ b/tests/unit/test_install_update.py @@ -106,23 +106,6 @@ class TestDownloadRefLockfileOverride: original reference (or default branch). """ - @staticmethod - def _build_download_ref(dep_ref, existing_lockfile, update_refs): - """Reproduce the download_ref construction logic from cli.py. - - This mirrors the sequential download path. The same logic applies - to the parallel pre-download path. - """ - download_ref = str(dep_ref) - if existing_lockfile and not update_refs: - locked_dep = existing_lockfile.get_dependency(dep_ref.get_unique_key()) - if locked_dep and locked_dep.resolved_commit and locked_dep.resolved_commit != "cached": - base_ref = dep_ref.repo_url - if dep_ref.virtual_path: - base_ref = f"{base_ref}/{dep_ref.virtual_path}" - download_ref = f"{base_ref}#{locked_dep.resolved_commit}" - return download_ref - def _make_subdirectory_dep(self): return DependencyReference( repo_url="owner/monorepo", @@ -151,16 +134,16 @@ def test_subdirectory_lockfile_override_without_update(self): dep = self._make_subdirectory_dep() lockfile = self._mock_lockfile(dep) - ref = self._build_download_ref(dep, lockfile, update_refs=False) + ref = build_download_ref(dep, lockfile, update_refs=False, ref_changed=False) assert "#abc123def456" in ref - assert ref == "owner/monorepo/packages/my-skill#abc123def456" + assert ref == "github.com/owner/monorepo/packages/my-skill#abc123def456" def test_subdirectory_no_lockfile_override_with_update(self): """With --update, subdirectory download ref must NOT use locked SHA.""" dep = self._make_subdirectory_dep() lockfile = self._mock_lockfile(dep) - ref = self._build_download_ref(dep, lockfile, update_refs=True) + ref = build_download_ref(dep, lockfile, update_refs=True, ref_changed=False) assert "#abc123def456" not in ref assert ref == str(dep) @@ -169,7 +152,7 @@ def test_regular_lockfile_override_without_update(self): dep = self._make_regular_dep() lockfile = self._mock_lockfile(dep) - ref = self._build_download_ref(dep, lockfile, update_refs=False) + ref = build_download_ref(dep, lockfile, update_refs=False, ref_changed=False) assert "#abc123def456" in ref def test_regular_no_lockfile_override_with_update(self): @@ -177,13 +160,13 @@ def test_regular_no_lockfile_override_with_update(self): dep = self._make_regular_dep() lockfile = self._mock_lockfile(dep) - ref = self._build_download_ref(dep, lockfile, update_refs=True) + ref = build_download_ref(dep, lockfile, update_refs=True, ref_changed=False) assert "#abc123def456" not in ref def test_no_lockfile_returns_original_ref(self): """Without a lockfile, download ref is the original dependency string.""" dep = self._make_subdirectory_dep() - ref = self._build_download_ref(dep, existing_lockfile=None, update_refs=False) + ref = build_download_ref(dep, existing_lockfile=None, update_refs=False, ref_changed=False) assert ref == str(dep) def test_cached_lockfile_entry_not_overridden(self): @@ -191,25 +174,57 @@ def test_cached_lockfile_entry_not_overridden(self): dep = self._make_subdirectory_dep() lockfile = self._mock_lockfile(dep, resolved_commit="cached") - ref = self._build_download_ref(dep, lockfile, update_refs=False) + ref = build_download_ref(dep, lockfile, update_refs=False, ref_changed=False) assert ref == str(dep) + def test_ghe_custom_domain_host_preserved_in_locked_ref(self): + """GHE custom domain host must appear in the locked download ref. + + Regression test: without the host, DependencyReference.parse() + defaults to github.com and the clone fails for enterprise hosts. + """ + dep = DependencyReference( + repo_url="org/repo", + host="github.example.com", + reference=None, + ) + lockfile = self._mock_lockfile(dep) + + ref = build_download_ref(dep, lockfile, update_refs=False, ref_changed=False) + assert ref == "github.example.com/org/repo#abc123def456" + + def test_ghe_custom_domain_subdirectory_host_preserved(self): + """GHE custom domain host must appear for virtual/subdirectory deps too.""" + dep = DependencyReference( + repo_url="org/monorepo", + host="git.corp.internal", + reference=None, + virtual_path="packages/my-skill", + is_virtual=True, + ) + lockfile = self._mock_lockfile(dep) + + ref = build_download_ref(dep, lockfile, update_refs=False, ref_changed=False) + assert ref == "git.corp.internal/org/monorepo/packages/my-skill#abc123def456" + + def test_no_host_produces_plain_repo_url(self): + """When host is None, download ref uses plain repo_url (no prefix).""" + dep = DependencyReference( + repo_url="owner/repo", + host=None, + reference="main", + ) + lockfile = self._mock_lockfile(dep) + + ref = build_download_ref(dep, lockfile, update_refs=False, ref_changed=False) + assert ref == "owner/repo#abc123def456" + class TestPreDownloadRefLockfileOverride: - """Same as TestDownloadRefLockfileOverride but for the parallel pre-download path.""" + """Same as TestDownloadRefLockfileOverride but for the parallel pre-download path. - @staticmethod - def _build_pre_download_ref(dep_ref, existing_lockfile, update_refs): - """Reproduce the _pd_dlref construction logic from cli.py's pre-download loop.""" - _pd_dlref = str(dep_ref) - if existing_lockfile and not update_refs: - _pd_locked = existing_lockfile.get_dependency(dep_ref.get_unique_key()) - if _pd_locked and _pd_locked.resolved_commit and _pd_locked.resolved_commit != "cached": - _pd_base = dep_ref.repo_url - if dep_ref.virtual_path: - _pd_base = f"{_pd_base}/{dep_ref.virtual_path}" - _pd_dlref = f"{_pd_base}#{_pd_locked.resolved_commit}" - return _pd_dlref + Both paths now use ``build_download_ref()`` from ``drift.py``. + """ def _make_subdirectory_dep(self): return DependencyReference( @@ -232,7 +247,7 @@ def test_pre_download_no_lockfile_override_with_update(self): dep = self._make_subdirectory_dep() lockfile = self._mock_lockfile(dep) - ref = self._build_pre_download_ref(dep, lockfile, update_refs=True) + ref = build_download_ref(dep, lockfile, update_refs=True, ref_changed=False) assert "#abc123def456" not in ref def test_pre_download_lockfile_override_without_update(self): @@ -240,7 +255,7 @@ def test_pre_download_lockfile_override_without_update(self): dep = self._make_subdirectory_dep() lockfile = self._mock_lockfile(dep) - ref = self._build_pre_download_ref(dep, lockfile, update_refs=False) + ref = build_download_ref(dep, lockfile, update_refs=False, ref_changed=False) assert "#abc123def456" in ref