Coverage for src / agent / providers / github / auth.py: 100%
41 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 14:30 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 14:30 +0000
1# Copyright 2025-2026 Microsoft Corporation
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
15"""GitHub authentication utilities."""
17import logging
18import os
19import shutil
20import subprocess
22logger = logging.getLogger(__name__)
25def get_github_token() -> str:
26 """Get GitHub token from environment variable or gh CLI.
28 Authentication priority:
29 1. GITHUB_TOKEN environment variable (checked first)
30 2. gh auth token command (fallback if gh CLI is available)
32 Returns:
33 GitHub token string
35 Raises:
36 ValueError: If neither authentication method is available or token is empty
38 Examples:
39 >>> # With GITHUB_TOKEN set
40 >>> token = get_github_token()
41 >>> # With gh CLI configured
42 >>> token = get_github_token()
43 """
44 # First, check GITHUB_TOKEN environment variable
45 env_token = os.getenv("GITHUB_TOKEN")
46 if env_token:
47 if not env_token.strip():
48 raise ValueError(
49 "GITHUB_TOKEN environment variable is set but empty. "
50 "Please provide a valid GitHub token."
51 )
52 logger.debug("Using GitHub token from GITHUB_TOKEN environment variable")
53 return env_token.strip()
55 # Fallback to gh CLI authentication
56 if not shutil.which("gh"):
57 raise ValueError(
58 "GitHub authentication failed: No GITHUB_TOKEN environment variable found "
59 "and gh CLI is not installed.\n\n"
60 "Please either:\n"
61 "1. Set GITHUB_TOKEN environment variable: export GITHUB_TOKEN=ghp_...\n"
62 "2. Install and configure gh CLI: gh auth login"
63 )
65 try:
66 result = subprocess.run(
67 ["gh", "auth", "token"],
68 capture_output=True,
69 text=True,
70 check=True,
71 timeout=5,
72 )
73 cli_token = result.stdout.strip()
75 if not cli_token:
76 raise ValueError("gh auth token returned empty token. " "Please run: gh auth login")
78 logger.debug("Using GitHub token from gh CLI (gh auth token)")
79 return cli_token
81 except subprocess.CalledProcessError as e:
82 stderr = e.stderr.strip() if e.stderr else "Unknown error"
83 raise ValueError(
84 f"Failed to get GitHub token from gh CLI: {stderr}\n\n"
85 "Please either:\n"
86 "1. Configure gh CLI: gh auth login\n"
87 "2. Set GITHUB_TOKEN environment variable: export GITHUB_TOKEN=ghp_..."
88 ) from e
89 except subprocess.TimeoutExpired as e:
90 raise ValueError(
91 "gh auth token command timed out after 5 seconds.\n\n"
92 "Please either:\n"
93 "1. Try running: gh auth login\n"
94 "2. Set GITHUB_TOKEN environment variable: export GITHUB_TOKEN=ghp_..."
95 ) from e
98def get_github_org() -> str | None:
99 """Get user's primary GitHub organization from gh CLI.
101 Returns the first organization from the user's org list, which is
102 typically their primary enterprise organization (e.g., "microsoft").
103 This enables organization-scoped API requests with higher rate limits.
105 Returns:
106 Organization login name, or None if not available
108 Examples:
109 >>> org = get_github_org()
110 >>> # Returns "microsoft" for Microsoft employees
111 >>> # Returns None if gh CLI is not available or user has no orgs
112 """
113 # Check if gh CLI is available
114 if not shutil.which("gh"):
115 logger.debug("gh CLI not available, cannot detect organization")
116 return None
118 try:
119 result = subprocess.run(
120 ["gh", "api", "user/orgs", "--jq", ".[0].login"],
121 capture_output=True,
122 text=True,
123 check=True,
124 timeout=5,
125 )
126 org_login = result.stdout.strip()
128 if org_login:
129 logger.debug(f"Detected GitHub organization: {org_login}")
130 return org_login
131 else:
132 logger.debug("User has no organization memberships")
133 return None
135 except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
136 logger.debug(f"Failed to get GitHub organization: {e}")
137 return None