From 7f6f35911abfa485ae3a2795af9b327657966f04 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 18 Apr 2026 19:24:41 +0000 Subject: [PATCH] Handle GitHub token revoke network failures safely Co-authored-by: Armen Zambrano G. --- src/github_app.py | 18 ++++++++++---- tests/test_github_app.py | 51 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 tests/test_github_app.py diff --git a/src/github_app.py b/src/github_app.py index c18b6a9..a616b1b 100644 --- a/src/github_app.py +++ b/src/github_app.py @@ -4,12 +4,15 @@ from __future__ import annotations import contextlib +import logging import time from typing import Generator import jwt import requests +logger = logging.getLogger(__name__) + class GithubAppToken: def __init__(self, private_key, app_id) -> None: @@ -25,14 +28,19 @@ def get_token(self, installation_id: int) -> Generator[str, None, None]: ) req.raise_for_status() resp = req.json() + token = resp["token"] try: # This token expires in an hour - yield resp["token"] + yield token finally: - requests.delete( - "https://api.github.com/installation/token", - headers={"Authorization": f"token {resp['token']}"}, - ) + try: + requests.delete( + "https://api.github.com/installation/token", + headers={"Authorization": f"token {token}"}, + ) + except requests.RequestException: + # Best-effort cleanup should not mask the actual workflow failure. + logger.warning("Failed to revoke GitHub app installation token", exc_info=True) def get_jwt_token(self, private_key, app_id): payload = { diff --git a/tests/test_github_app.py b/tests/test_github_app.py new file mode 100644 index 0000000..5ea5543 --- /dev/null +++ b/tests/test_github_app.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from unittest.mock import Mock +from unittest.mock import patch + +import pytest +import requests + +from src.github_app import GithubAppToken + + +def _new_token_manager(): + manager = GithubAppToken.__new__(GithubAppToken) + manager.headers = {"Authorization": "Bearer fake-jwt"} + return manager + + +@patch("src.github_app.requests.delete") +@patch("src.github_app.requests.post") +def test_get_token_returns_token_when_cleanup_succeeds(mock_post, mock_delete): + response = Mock() + response.raise_for_status.return_value = None + response.json.return_value = {"token": "installation-token"} + mock_post.return_value = response + + manager = _new_token_manager() + with manager.get_token(123) as token: + assert token == "installation-token" + + mock_delete.assert_called_once_with( + "https://api.github.com/installation/token", + headers={"Authorization": "token installation-token"}, + ) + + +@patch("src.github_app.requests.delete") +@patch("src.github_app.requests.post") +def test_get_token_does_not_mask_original_error_when_cleanup_fails( + mock_post, + mock_delete, +): + response = Mock() + response.raise_for_status.return_value = None + response.json.return_value = {"token": "installation-token"} + mock_post.return_value = response + mock_delete.side_effect = requests.ConnectionError("cleanup network failure") + + manager = _new_token_manager() + with pytest.raises(RuntimeError, match="original failure"): + with manager.get_token(123): + raise RuntimeError("original failure")