Skip to content

Commit bce754e

Browse files
committed
feat: actionable error messages for unsupported Git hosts
- Add unsupported_host_error() function with clear fix instructions - Show supported hosts list (github.com, *.ghe.com, Azure DevOps) - Include platform-specific env var examples (Linux/macOS/Windows) - Display mismatch when GITHUB_HOST is set but different host is used - Add unit tests for new error message function
1 parent 5b6a85f commit bce754e

4 files changed

Lines changed: 125 additions & 6 deletions

File tree

docs/getting-started.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ apm install partner.ghe.com/external/integration # FQDN always works
124124
apm install github.com/public/open-source-package
125125
```
126126

127-
**Key Insight:** Use `GITHUB_HOST` to set your default for bare package names. Use FQDN syntax to explicitly specify any Git host.
127+
**Key Insight:** Use `GITHUB_HOST` to set your default for bare package names. Use FQDN syntax to specify supported hosts explicitly (e.g., `github.com`, `*.ghe.com`, Azure DevOps). Custom hosts require setting `GITHUB_HOST`.
128128

129129
### Azure DevOps Support
130130

src/apm_cli/models/apm_package.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import re
44
import urllib.parse
5-
from ..utils.github_host import is_supported_git_host, is_azure_devops_hostname, default_host
5+
from ..utils.github_host import is_supported_git_host, is_azure_devops_hostname, default_host, unsupported_host_error
66
import yaml
77
from dataclasses import dataclass
88
from enum import Enum
@@ -305,7 +305,7 @@ def parse(cls, dependency_str: str) -> "DependencyReference":
305305

306306
# SECURITY: Reject protocol-relative URLs (//example.com)
307307
if dependency_str.startswith('//'):
308-
raise ValueError("Unsupported Git host. Protocol-relative URLs are not allowed")
308+
raise ValueError(unsupported_host_error("//...", context="Protocol-relative URLs are not supported"))
309309

310310
# Early detection of virtual packages (3+ path segments)
311311
# Extract the core path before processing reference (#) and alias (@)
@@ -353,14 +353,14 @@ def parse(cls, dependency_str: str) -> "DependencyReference":
353353
else:
354354
# First segment has a dot but is NOT a valid Git host - REJECT
355355
raise ValueError(
356-
f"Unsupported Git host. Invalid hostname: {hostname or first_segment}"
356+
unsupported_host_error(hostname or first_segment)
357357
)
358358
except (ValueError, AttributeError) as e:
359359
# If we can't parse or validate, and first segment has dot, it's suspicious - REJECT
360360
if isinstance(e, ValueError) and "Unsupported Git host" in str(e):
361361
raise # Re-raise our security error
362362
raise ValueError(
363-
f"Unsupported Git host. Could not validate hostname: {first_segment}"
363+
unsupported_host_error(first_segment)
364364
)
365365
elif check_str.startswith('gh/'):
366366
# Handle 'gh/' shorthand - only if it's exactly at the start
@@ -562,7 +562,7 @@ def parse(cls, dependency_str: str) -> "DependencyReference":
562562
# Accept github.com, GitHub Enterprise, Azure DevOps, etc. Use parsed_url.hostname
563563
hostname = parsed_url.hostname or ""
564564
if not is_supported_git_host(hostname):
565-
raise ValueError(f"Unsupported Git host, got hostname: {parsed_url.netloc}")
565+
raise ValueError(unsupported_host_error(hostname or parsed_url.netloc))
566566

567567
# Extract and validate the path
568568
path = parsed_url.path.strip("/")

src/apm_cli/utils/github_host.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,49 @@ def is_supported_git_host(hostname: Optional[str]) -> bool:
7676
return False
7777

7878

79+
def unsupported_host_error(hostname: str, context: Optional[str] = None) -> str:
80+
"""Generate an actionable error message for unsupported Git hosts.
81+
82+
Args:
83+
hostname: The hostname that was rejected
84+
context: Optional context message (e.g., "Protocol-relative URLs are not supported")
85+
86+
Returns:
87+
str: A user-friendly error message with fix instructions
88+
"""
89+
current_host = os.environ.get("GITHUB_HOST", "")
90+
91+
msg = ""
92+
if context:
93+
msg += f"{context}\n\n"
94+
95+
msg += f"Unsupported Git host: '{hostname}'.\n"
96+
msg += "\n"
97+
msg += "APM only allows these Git hosts by default:\n"
98+
msg += " • github.com\n"
99+
msg += " • *.ghe.com (GitHub Enterprise Cloud)\n"
100+
msg += " • dev.azure.com, *.visualstudio.com (Azure DevOps)\n"
101+
msg += "\n"
102+
103+
if current_host:
104+
msg += f"Your GITHUB_HOST is set to: '{current_host}'\n"
105+
msg += f"But you're trying to use: '{hostname}'\n"
106+
msg += "\n"
107+
108+
msg += f"To use '{hostname}', set the GITHUB_HOST environment variable:\n"
109+
msg += "\n"
110+
msg += f" # Linux/macOS:\n"
111+
msg += f" export GITHUB_HOST={hostname}\n"
112+
msg += "\n"
113+
msg += f" # Windows (PowerShell):\n"
114+
msg += f' $env:GITHUB_HOST = "{hostname}"\n'
115+
msg += "\n"
116+
msg += f" # Windows (Command Prompt):\n"
117+
msg += f" set GITHUB_HOST={hostname}\n"
118+
119+
return msg
120+
121+
79122
def build_ssh_url(host: str, repo_ref: str) -> str:
80123
"""Build an SSH clone URL for the given host and repo_ref (owner/repo)."""
81124
return f"git@{host}:{repo_ref}.git"

tests/unit/test_github_host.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,37 @@ def test_sanitize_token_url_in_message():
109109
assert f"***@{host}" in sanitized
110110

111111

112+
def test_unsupported_host_error_message():
113+
"""Test that unsupported host error provides actionable guidance."""
114+
error_msg = github_host.unsupported_host_error("github.company.com")
115+
116+
# Should mention the hostname
117+
assert "github.company.com" in error_msg
118+
119+
# Should list supported hosts
120+
assert "github.com" in error_msg
121+
assert "*.ghe.com" in error_msg
122+
assert "dev.azure.com" in error_msg
123+
124+
# Should provide fix instructions for all platforms
125+
assert "export GITHUB_HOST=" in error_msg
126+
assert "$env:GITHUB_HOST" in error_msg
127+
assert "set GITHUB_HOST=" in error_msg
128+
129+
130+
def test_unsupported_host_error_shows_current_host(monkeypatch):
131+
"""Test that error shows current GITHUB_HOST if set."""
132+
monkeypatch.setenv("GITHUB_HOST", "other.company.com")
133+
134+
error_msg = github_host.unsupported_host_error("github.company.com")
135+
136+
# Should show the mismatch
137+
assert "other.company.com" in error_msg
138+
assert "github.company.com" in error_msg
139+
140+
monkeypatch.delenv("GITHUB_HOST", raising=False)
141+
142+
112143
# Azure DevOps URL builder tests
113144

114145
def test_build_ado_https_clone_url():
@@ -150,3 +181,48 @@ def test_build_ado_api_url():
150181
assert "path=apm.yml" in url
151182
assert "versionDescriptor.version=main" in url
152183
assert "api-version=7.0" in url
184+
185+
186+
# Unsupported host error message tests
187+
188+
def test_unsupported_host_error_message():
189+
"""Test that unsupported host error provides actionable guidance."""
190+
error_msg = github_host.unsupported_host_error("github.company.com")
191+
192+
# Should mention the hostname
193+
assert "github.company.com" in error_msg
194+
195+
# Should list supported hosts
196+
assert "github.com" in error_msg
197+
assert "*.ghe.com" in error_msg
198+
assert "dev.azure.com" in error_msg
199+
200+
# Should provide fix instructions for all platforms
201+
assert "export GITHUB_HOST=" in error_msg
202+
assert "$env:GITHUB_HOST" in error_msg
203+
assert "set GITHUB_HOST=" in error_msg
204+
205+
206+
def test_unsupported_host_error_with_context():
207+
"""Test that context message is included when provided."""
208+
error_msg = github_host.unsupported_host_error("//evil.com", context="Protocol-relative URLs are not supported")
209+
210+
# Should include the context
211+
assert "Protocol-relative URLs are not supported" in error_msg
212+
213+
# Should still include standard guidance
214+
assert "github.com" in error_msg
215+
assert "GITHUB_HOST" in error_msg
216+
217+
218+
def test_unsupported_host_error_shows_current_host(monkeypatch):
219+
"""Test that error shows current GITHUB_HOST if set."""
220+
monkeypatch.setenv("GITHUB_HOST", "other.company.com")
221+
222+
error_msg = github_host.unsupported_host_error("github.company.com")
223+
224+
# Should show the mismatch
225+
assert "other.company.com" in error_msg
226+
assert "github.company.com" in error_msg
227+
228+
monkeypatch.delenv("GITHUB_HOST", raising=False)

0 commit comments

Comments
 (0)