GitHub App用安装令牌克隆私有仓库提示不存在,如何解决?
我配置了一个监听GitHub事件的GitHub App,已安装到私有仓库且拥有读写权限。使用git clone https://x-access-token:<TOKEN>@github.com/owner/repo_name.git命令时出现致命错误,提示仓库不存在。我看过Stack Overflow上的《Authentication issue》问题,但它适用于客户端到服务器的请求,而非我的服务器到服务器场景。
我参考GitHub官方的App认证相关文档,编写了生成JWT、获取安装访问令牌、转换克隆URL的函数,最后用GitPython尝试克隆仓库,但始终报错。相关代码如下:
生成JWT的函数
def generate_jwt(app_id=APP_ID, private_key_path=APP_PRIVATE_KEY_PATH): """ This function generates a JWT token for the GitHub App. Args: app_id (str): The GitHub App ID. private_key_path (pathlib.Path): The path to the private key file. Returns: str: The JWT token. """ with open(private_key_path, 'r', encoding='utf-8') as key_file: private_key = key_file.read() payload = { 'iat': datetime.now(), 'exp': datetime.now() + timedelta(minutes=10), 'iss': app_id } return jwt.encode(payload, private_key, algorithm='RS256')
获取安装访问令牌的函数
def get_installation_access_token(installation_id) -> str: """ Get the installation access token for the GitHub App. Args: installation_id (str): The installation id access token. Returns: str: The installation access token. """ if installation_id is None: return "No Installation ID" url = f"https://api.github.com/app/installations/{installation_id}/access_tokens" app_jwt = generate_jwt() # Make the API request headers = { "Authorization": f"Bearer {app_jwt}", "Accept": "application/vnd.github+json", "X-Github-Api-Version": "2022-11-28", } response = requests.post(url, headers=headers, timeout=10) # Extract the installation access token response.raise_for_status() installation_access_token = response.json()["token"] return installation_access_token
转换克隆URL的函数
def convert_clone_url_to_autenticated_url(clone_url: str, installation_access_token: str) -> str: """ Convert the clone URL to an authenticated URL. Args: clone_url (str): The clone URL. installation_access_token (str): The installation access token. Returns: str: The authenticated URL. """ # Parse the clone URL if not clone_url.startswith("https://github.com/"): raise ValueError("Invalid clone URL. It must start with 'https://github.com/'.") return clone_url.replace( "https://github.com/", f"https://x-access-token:{installation_access_token}@github.com/" )
GitPython克隆代码
# Clone the repository repo = None if not repo_dir.exists(): repo = git.Repo.clone_from(authenticated_url, repo_dir) else: repo = git.Repo(repo_dir)
修复JWT生成的时间戳格式
datetime.now()生成的是本地时间对象,但JWT要求的是Unix时间戳(秒数)。需要替换为UTC时间并转换为时间戳:payload = { 'iat': int(datetime.utcnow().timestamp()), 'exp': int((datetime.utcnow() + timedelta(minutes=10)).timestamp()), 'iss': app_id }错误的时间格式会导致JWT无效,进而获取到的安装令牌没有权限访问仓库,最终提示仓库不存在。
检查安装令牌的权限范围
确保GitHub App在安装时被授予了contents:read权限(私有仓库克隆需要该权限)。可以在GitHub App的设置页面确认权限配置,重新安装到仓库以应用权限变更。处理令牌中的特殊字符
安装访问令牌可能包含斜杠、等号等特殊字符,直接拼接到URL中会导致URL解析错误。需要对令牌进行URL编码:import urllib.parse def convert_clone_url_to_autenticated_url(clone_url: str, installation_access_token: str) -> str: if not clone_url.startswith("https://github.com/"): raise ValueError("Invalid clone URL. It must start with 'https://github.com/'.") encoded_token = urllib.parse.quote_plus(installation_access_token) return clone_url.replace( "https://github.com/", f"https://x-access-token:{encoded_token}@github.com/" )验证安装令牌的有效性
在获取令牌后,调用GitHub API验证令牌权限:def validate_token(token): headers = { "Authorization": f"token {token}", "Accept": "application/vnd.github+json", "X-Github-Api-Version": "2022-11-28", } # 替换为你的仓库API路径 response = requests.get("https://api.github.com/repos/owner/repo_name", headers=headers) print(response.status_code, response.json())如果返回404,说明令牌没有访问该仓库的权限;如果返回200,说明令牌有效,问题出在克隆URL或GitPython的调用上。
改用GitPython的认证参数传递方式
避免直接拼接URL,改用GitPython的auth参数传递凭证,更安全且不易出错:from git import Repo from git.exc import GitCommandError import os repo_dir = "./repo" clone_url = "https://github.com/owner/repo_name.git" token = get_installation_access_token(INSTALLATION_ID) if not os.path.exists(repo_dir): try: # 使用HTTPBasicAuth传递凭证 repo = Repo.clone_from( clone_url, repo_dir, auth=("x-access-token", token) ) except GitCommandError as e: print(f"克隆失败: {e}") else: repo = Repo(repo_dir)
内容的提问来源于stack exchange,提问作者newkid

