Skip to content

Commit 70b34fa

Browse files
authored
fix: enhance git remote validation by trying SSH URLs for generic hosts (#584)
1 parent d5fecb6 commit 70b34fa

File tree

2 files changed

+155
-9
lines changed

2 files changed

+155
-9
lines changed

src/apm_cli/commands/install.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -431,15 +431,30 @@ def _validate_package_exists(package, verbose=False, auth_resolver=None):
431431
if verbose_log:
432432
verbose_log(f"Trying git ls-remote for {dep_ref.host}")
433433

434-
cmd = ["git", "ls-remote", "--heads", "--exit-code", package_url]
435-
result = subprocess.run(
436-
cmd,
437-
capture_output=True,
438-
text=True,
439-
encoding="utf-8",
440-
timeout=30,
441-
env=validate_env,
442-
)
434+
# For generic hosts, try SSH first (no credentials needed when SSH
435+
# keys are configured) before falling back to HTTPS.
436+
urls_to_try = []
437+
if is_generic:
438+
ssh_url = ado_downloader._build_repo_url(
439+
dep_ref.repo_url, use_ssh=True, dep_ref=dep_ref
440+
)
441+
urls_to_try = [ssh_url, package_url]
442+
else:
443+
urls_to_try = [package_url]
444+
445+
result = None
446+
for probe_url in urls_to_try:
447+
cmd = ["git", "ls-remote", "--heads", "--exit-code", probe_url]
448+
result = subprocess.run(
449+
cmd,
450+
capture_output=True,
451+
text=True,
452+
encoding="utf-8",
453+
timeout=30,
454+
env=validate_env,
455+
)
456+
if result.returncode == 0:
457+
break
443458

444459
if verbose_log:
445460
if result.returncode == 0:

tests/unit/test_install_command.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -862,3 +862,134 @@ def test_global_rejects_tilde_local_path(self):
862862
assert "not supported at user scope" in result.output
863863
finally:
864864
os.chdir(self.original_dir)
865+
866+
# ---------------------------------------------------------------------------
867+
# Generic-host SSH-first validation tests
868+
# ---------------------------------------------------------------------------
869+
870+
class TestGenericHostSshFirstValidation:
871+
"""Tests for the SSH-first ls-remote logic added for generic (non-GitHub/ADO) hosts."""
872+
873+
def _make_completed_process(self, returncode, stderr=""):
874+
"""Return a minimal subprocess.CompletedProcess-like mock."""
875+
mock = MagicMock()
876+
mock.returncode = returncode
877+
mock.stderr = stderr
878+
mock.stdout = ""
879+
return mock
880+
881+
@patch("subprocess.run")
882+
def test_generic_host_tries_ssh_first_and_succeeds(self, mock_run):
883+
"""SSH URL is tried first for generic hosts and used when it succeeds."""
884+
from apm_cli.commands.install import _validate_package_exists
885+
886+
# SSH probe succeeds on the first call
887+
mock_run.return_value = self._make_completed_process(returncode=0)
888+
889+
result = _validate_package_exists(
890+
"git@git.example.org:org/group/repo.git", verbose=False
891+
)
892+
893+
assert result is True
894+
# subprocess.run must have been called at least once
895+
assert mock_run.call_count >= 1
896+
# First call must use the SSH URL
897+
first_call_cmd = mock_run.call_args_list[0][0][0]
898+
assert any("git@git.example.org" in arg for arg in first_call_cmd), (
899+
f"Expected SSH URL in first ls-remote call, got: {first_call_cmd}"
900+
)
901+
902+
@patch("subprocess.run")
903+
def test_generic_host_falls_back_to_https_when_ssh_fails(self, mock_run):
904+
"""HTTPS fallback is used for generic hosts when SSH ls-remote fails."""
905+
from apm_cli.commands.install import _validate_package_exists
906+
907+
# SSH probe fails, HTTPS succeeds
908+
mock_run.side_effect = [
909+
self._make_completed_process(returncode=128, stderr="ssh: connect to host"),
910+
self._make_completed_process(returncode=0),
911+
]
912+
913+
result = _validate_package_exists(
914+
"git@git.example.org:org/group/repo.git", verbose=False
915+
)
916+
917+
assert result is True
918+
assert mock_run.call_count == 2
919+
# First call: SSH
920+
first_cmd = mock_run.call_args_list[0][0][0]
921+
assert any("git@git.example.org" in arg for arg in first_cmd), (
922+
f"Expected SSH URL in first call, got: {first_cmd}"
923+
)
924+
# Second call: HTTPS
925+
second_cmd = mock_run.call_args_list[1][0][0]
926+
assert any("https://git.example.org" in arg for arg in second_cmd), (
927+
f"Expected HTTPS URL in second call, got: {second_cmd}"
928+
)
929+
930+
@patch("subprocess.run")
931+
def test_generic_host_returns_false_when_both_transports_fail(self, mock_run):
932+
"""Validation returns False when both SSH and HTTPS fail for a generic host."""
933+
from apm_cli.commands.install import _validate_package_exists
934+
935+
mock_run.return_value = self._make_completed_process(
936+
returncode=128, stderr="fatal: could not read Username"
937+
)
938+
939+
result = _validate_package_exists(
940+
"git@git.example.org:org/group/repo.git", verbose=False
941+
)
942+
943+
assert result is False
944+
assert mock_run.call_count == 2 # tried SSH then HTTPS
945+
946+
@patch("subprocess.run")
947+
def test_github_host_skips_ssh_attempt(self, mock_run):
948+
"""GitHub.com repositories do NOT go through the SSH-first ls-remote path."""
949+
950+
import urllib.request
951+
import urllib.error
952+
953+
from apm_cli.commands.install import _validate_package_exists
954+
955+
with patch("urllib.request.urlopen") as mock_urlopen:
956+
mock_urlopen.side_effect = urllib.error.HTTPError(
957+
url="https://api.github.com/repos/owner/repo",
958+
code=404, msg="Not Found", hdrs={}, fp=None,
959+
)
960+
result = _validate_package_exists("owner/repo", verbose=False)
961+
962+
assert result is False
963+
# No ls-remote call should have been made for a github.com host
964+
ls_remote_calls = [
965+
call for call in mock_run.call_args_list
966+
if "ls-remote" in (call[0][0] if call[0] else [])
967+
]
968+
assert len(ls_remote_calls) == 0, (
969+
f"Expected no ls-remote calls for github.com, got: {ls_remote_calls}"
970+
)
971+
972+
@patch("subprocess.run")
973+
def test_ghes_host_skips_ssh_attempt(self, mock_run):
974+
"""A GHES host is treated as GitHub, not generic SSH probe is skipped."""
975+
from apm_cli.commands.install import _validate_package_exists
976+
977+
mock_run.return_value = self._make_completed_process(returncode=0)
978+
979+
result = _validate_package_exists(
980+
"company.ghe.com/team/internal-repo", verbose=False
981+
)
982+
983+
assert result is True
984+
ls_remote_calls = [
985+
call for call in mock_run.call_args_list
986+
if "ls-remote" in (call[0][0] if call[0] else [])
987+
]
988+
assert len(ls_remote_calls) == 1, (
989+
f"Expected exactly 1 ls-remote call for GHES host, got: {ls_remote_calls}"
990+
)
991+
only_cmd = ls_remote_calls[0][0][0]
992+
# Must use HTTPS, not SSH
993+
assert all("git@" not in arg for arg in only_cmd), (
994+
f"Expected HTTPS-only URL for GHES host, got: {only_cmd}"
995+
)

0 commit comments

Comments
 (0)