Coverage for src / gh_secrets_and_vars_async / common.py: 77%
65 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-16 17:56 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-16 17:56 +0000
1import os
2import subprocess
3import sys
5from dotenv import dotenv_values
6from github import Auth, Github
7from github.GithubException import UnknownObjectException
8from github.Repository import Repository
9from loguru import logger
12def configure_logging(verbose: bool) -> None:
13 """Configure loguru: silent by default, compact format with --verbose."""
14 logger.remove()
15 if verbose:
16 logger.add(sys.stderr, level="DEBUG", format=" {message}")
19def _load_dotenv_values(filename: str = ".env") -> dict[str, str]:
20 """Read key/value pairs from ``filename`` without mutating the process environment."""
21 values = dotenv_values(filename)
22 return {key: value for key, value in values.items() if value is not None}
25def load_env_config(filename: str = ".env") -> tuple[str, str, str]:
26 """Return GH_* configuration with explicit environment variables taking precedence."""
27 env_values = _load_dotenv_values(filename)
28 gh_repo = os.environ.get("GH_REPO", env_values.get("GH_REPO", ""))
29 gh_account = os.environ.get("GH_ACCOUNT", env_values.get("GH_ACCOUNT", ""))
30 gh_token = os.environ.get("GH_TOKEN", env_values.get("GH_TOKEN", ""))
31 return gh_repo, gh_account, gh_token
34def _get_gh_cli_token() -> str:
35 """Return the token from ``gh auth token`` or an empty string if unavailable."""
36 try:
37 result = subprocess.run(
38 ["gh", "auth", "token"],
39 capture_output=True,
40 text=True,
41 check=True,
42 )
43 except (subprocess.CalledProcessError, FileNotFoundError):
44 return ""
45 return result.stdout.strip()
48def _resolve_token(filename: str = ".env", auth_source: str = "auto") -> str:
49 """Return a GitHub token from the configured auth source."""
50 dotenv_token = _load_dotenv_values(filename).get("GH_TOKEN", "").strip()
52 if auth_source == "dotenv":
53 if dotenv_token:
54 logger.debug("Using GitHub token from GH_TOKEN in .env (--env-auth).")
55 return dotenv_token
56 raise RuntimeError(
57 "No GitHub token found in .env. Remove --env-auth or add GH_TOKEN to .env."
58 )
60 if auth_source != "auto":
61 raise ValueError(f"Unsupported auth_source '{auth_source}'.")
63 token = os.environ.get("GH_TOKEN", "").strip()
64 if token:
65 logger.debug("Using GitHub token from GH_TOKEN environment variable.")
66 return token
68 gh_token = _get_gh_cli_token()
69 if gh_token:
70 if dotenv_token:
71 logger.debug(
72 "Using GitHub token from gh auth token. Ignoring GH_TOKEN from .env; "
73 "export GH_TOKEN in the current shell to force it."
74 )
75 else:
76 logger.debug("Using GitHub token from gh auth token.")
77 return gh_token
79 if dotenv_token:
80 logger.debug("Using GitHub token from GH_TOKEN in .env.")
81 return dotenv_token
83 raise RuntimeError(
84 "No GitHub token found. Set GH_TOKEN in .env / environment, "
85 "or authenticate with: gh auth login"
86 )
89def get_github_repo(
90 github_account: str,
91 github_repo_name: str,
92 auth_source: str = "auto",
93) -> Repository:
94 """Get the GitHub repository object.
96 Tries user lookup first, falls back to organization.
97 """
98 token = _resolve_token(auth_source=auth_source)
99 auth = Auth.Token(token)
100 g = Github(auth=auth)
101 try:
102 repo = g.get_user(github_account).get_repo(github_repo_name)
103 except UnknownObjectException as e:
104 logger.critical(e)
105 repo = g.get_organization(github_account).get_repo(github_repo_name)
106 logger.critical("You must add GH_USER to your env file.")
108 return repo
111def get_github_client(auth_source: str = "auto") -> Github:
112 """Create an authenticated Github client from env, ``gh auth token``, or ``.env``."""
113 token = _resolve_token(auth_source=auth_source)
114 auth = Auth.Token(token)
115 return Github(auth=auth)