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

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. 

14 

15"""GitHub authentication utilities.""" 

16 

17import logging 

18import os 

19import shutil 

20import subprocess 

21 

22logger = logging.getLogger(__name__) 

23 

24 

25def get_github_token() -> str: 

26 """Get GitHub token from environment variable or gh CLI. 

27 

28 Authentication priority: 

29 1. GITHUB_TOKEN environment variable (checked first) 

30 2. gh auth token command (fallback if gh CLI is available) 

31 

32 Returns: 

33 GitHub token string 

34 

35 Raises: 

36 ValueError: If neither authentication method is available or token is empty 

37 

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() 

54 

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 ) 

64 

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() 

74 

75 if not cli_token: 

76 raise ValueError("gh auth token returned empty token. " "Please run: gh auth login") 

77 

78 logger.debug("Using GitHub token from gh CLI (gh auth token)") 

79 return cli_token 

80 

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 

96 

97 

98def get_github_org() -> str | None: 

99 """Get user's primary GitHub organization from gh CLI. 

100 

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. 

104 

105 Returns: 

106 Organization login name, or None if not available 

107 

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 

117 

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() 

127 

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 

134 

135 except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e: 

136 logger.debug(f"Failed to get GitHub organization: {e}") 

137 return None