Skip to content

Commit 2825bb1

Browse files
authored
Merge pull request #243 from zryfish/dev/add_authentication_when_download_template
add github auth headers if there are GITHUB_TOKEN/GH_TOKEN set
2 parents 919ba00 + b1688b9 commit 2825bb1

1 file changed

Lines changed: 33 additions & 6 deletions

File tree

src/specify_cli/__init__.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,19 @@
5252
ssl_context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
5353
client = httpx.Client(verify=ssl_context)
5454

55+
def _github_token(cli_token: str | None = None) -> str | None:
56+
return cli_token or os.getenv("GH_TOKEN") or os.getenv("GITHUB_TOKEN")
57+
58+
def _github_auth_headers(cli_token: str | None = None) -> dict:
59+
"""Headers for GitHub REST API requests.
60+
- Uses Bearer auth if token present
61+
"""
62+
headers = {}
63+
token = _github_token(cli_token)
64+
if token:
65+
headers["Authorization"] = f"Bearer {token}"
66+
return headers
67+
5568
# Constants
5669
AI_CHOICES = {
5770
"copilot": "GitHub Copilot",
@@ -418,7 +431,7 @@ def init_git_repo(project_path: Path, quiet: bool = False) -> bool:
418431
os.chdir(original_cwd)
419432

420433

421-
def download_template_from_github(ai_assistant: str, download_dir: Path, *, script_type: str = "sh", verbose: bool = True, show_progress: bool = True, client: httpx.Client = None, debug: bool = False) -> Tuple[Path, dict]:
434+
def download_template_from_github(ai_assistant: str, download_dir: Path, *, script_type: str = "sh", verbose: bool = True, show_progress: bool = True, client: httpx.Client = None, debug: bool = False, github_token: str = None) -> Tuple[Path, dict]:
422435
repo_owner = "github"
423436
repo_name = "spec-kit"
424437
if client is None:
@@ -429,7 +442,12 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
429442
api_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest"
430443

431444
try:
432-
response = client.get(api_url, timeout=30, follow_redirects=True)
445+
response = client.get(
446+
api_url,
447+
timeout=30,
448+
follow_redirects=True,
449+
headers=_github_auth_headers(github_token) or None,
450+
)
433451
status = response.status_code
434452
if status != 200:
435453
msg = f"GitHub API returned {status} for {api_url}"
@@ -475,7 +493,14 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
475493
console.print(f"[cyan]Downloading template...[/cyan]")
476494

477495
try:
478-
with client.stream("GET", download_url, timeout=60, follow_redirects=True) as response:
496+
# Include auth header for initial GitHub request; it won't leak across cross-origin redirects
497+
with client.stream(
498+
"GET",
499+
download_url,
500+
timeout=60,
501+
follow_redirects=True,
502+
headers=_github_auth_headers(github_token) or None,
503+
) as response:
479504
if response.status_code != 200:
480505
body_sample = response.text[:400]
481506
raise RuntimeError(f"Download failed with {response.status_code}\nHeaders: {response.headers}\nBody (truncated): {body_sample}")
@@ -519,7 +544,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
519544
return zip_path, metadata
520545

521546

522-
def download_and_extract_template(project_path: Path, ai_assistant: str, script_type: str, is_current_dir: bool = False, *, verbose: bool = True, tracker: StepTracker | None = None, client: httpx.Client = None, debug: bool = False) -> Path:
547+
def download_and_extract_template(project_path: Path, ai_assistant: str, script_type: str, is_current_dir: bool = False, *, verbose: bool = True, tracker: StepTracker | None = None, client: httpx.Client = None, debug: bool = False, github_token: str = None) -> Path:
523548
"""Download the latest release and extract it to create a new project.
524549
Returns project_path. Uses tracker if provided (with keys: fetch, download, extract, cleanup)
525550
"""
@@ -536,7 +561,8 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
536561
verbose=verbose and tracker is None,
537562
show_progress=(tracker is None),
538563
client=client,
539-
debug=debug
564+
debug=debug,
565+
github_token=github_token
540566
)
541567
if tracker:
542568
tracker.complete("fetch", f"release {meta['release']} ({meta['size']:,} bytes)")
@@ -731,6 +757,7 @@ def init(
731757
here: bool = typer.Option(False, "--here", help="Initialize project in the current directory instead of creating a new one"),
732758
skip_tls: bool = typer.Option(False, "--skip-tls", help="Skip SSL/TLS verification (not recommended)"),
733759
debug: bool = typer.Option(False, "--debug", help="Show verbose diagnostic output for network and extraction failures"),
760+
github_token: str = typer.Option(None, "--github-token", help="GitHub token to use for API requests (or set GH_TOKEN or GITHUB_TOKEN environment variable)"),
734761
):
735762
"""
736763
Initialize a new Specify project from the latest template.
@@ -897,7 +924,7 @@ def init(
897924
local_ssl_context = ssl_context if verify else False
898925
local_client = httpx.Client(verify=local_ssl_context)
899926

900-
download_and_extract_template(project_path, selected_ai, selected_script, here, verbose=False, tracker=tracker, client=local_client, debug=debug)
927+
download_and_extract_template(project_path, selected_ai, selected_script, here, verbose=False, tracker=tracker, client=local_client, debug=debug, github_token=github_token)
901928

902929
# Ensure scripts are executable (POSIX)
903930
ensure_executable_scripts(project_path, tracker=tracker)

0 commit comments

Comments
 (0)