From ee7185113c84676ea5b1a7448935bb6779b2cd39 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 08:57:35 +0000 Subject: [PATCH 1/4] Initial plan From c0e964700dfc8cb1cee5a0eb457d316fa5226971 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 09:02:28 +0000 Subject: [PATCH 2/4] fix: preserve GHE custom domain host in lockfile download refs When build_download_ref() constructed the locked download ref string, it used only dep_ref.repo_url (e.g. "org/repo") without including the host. When re-parsed by DependencyReference.parse(), this defaulted to github.com, causing clone failures for GitHub Enterprise custom domain dependencies. Now includes dep_ref.host in the base ref when present, producing e.g. "github.example.com/org/repo#commitsha" which correctly resolves back to the enterprise host. Fixes #339 Co-authored-by: danielmeppiel <51440732+danielmeppiel@users.noreply.github.com> --- CHANGELOG.md | 1 + src/apm_cli/drift.py | 8 ++- tests/unit/test_install_update.py | 94 ++++++++++++++++++------------- 3 files changed, 63 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92eb909f..1be69531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `git credential fill` fallback for single-file downloads from private repos (#332) - GitHub API rate-limit 403 responses no longer misdiagnosed as authentication failures (#332) +- Lockfile now preserves the host for GitHub Enterprise custom domains so subsequent `apm install` clones from the correct server (#339) ### Changed 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..72ea1c7f 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,58 @@ 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" + assert "github.example.com" in ref + + 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 +248,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 +256,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 From 20143b4a621a7f9c9b7cdcabc380220ffa5d73f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 09:03:07 +0000 Subject: [PATCH 3/4] test: remove redundant assertion in GHE test Co-authored-by: danielmeppiel <51440732+danielmeppiel@users.noreply.github.com> --- tests/unit/test_install_update.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/test_install_update.py b/tests/unit/test_install_update.py index 72ea1c7f..8b705517 100644 --- a/tests/unit/test_install_update.py +++ b/tests/unit/test_install_update.py @@ -192,7 +192,6 @@ def test_ghe_custom_domain_host_preserved_in_locked_ref(self): ref = build_download_ref(dep, lockfile, update_refs=False, ref_changed=False) assert ref == "github.example.com/org/repo#abc123def456" - assert "github.example.com" in ref def test_ghe_custom_domain_subdirectory_host_preserved(self): """GHE custom domain host must appear for virtual/subdirectory deps too.""" From 9e07fa2b5f6f57ef15ce86d74b4820c5d1fb6886 Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Tue, 17 Mar 2026 13:48:51 +0100 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20correct=20CHANGELOG=20=E2=80=94=20on?= =?UTF-8?q?e=20entry=20per=20PR,=20add=20missing=20#332,=20fix=20#339?= =?UTF-8?q?=E2=86=92#338?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapse five #330 entries into one, add missing #332 auth-asymmetry entry, and correct PR reference #339→#338. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf321ca9..6714b4ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,16 +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 -- GitHub API rate-limit 403 responses no longer misdiagnosed as authentication failures (#332) -- Lockfile now preserves the host for GitHub Enterprise custom domains so subsequent `apm install` clones from the correct server (#339) -- `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