From 82702eda77e746a928a11cfc5dbee31162f459ff Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 22 Apr 2026 19:45:33 +0000 Subject: [PATCH] Prevent token cleanup errors from masking primary failures Co-authored-by: Armen Zambrano G. --- src/github_app.py | 22 +++++++++++--- tests/test_github_app.py | 63 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 tests/test_github_app.py diff --git a/src/github_app.py b/src/github_app.py index c18b6a9..9949738 100644 --- a/src/github_app.py +++ b/src/github_app.py @@ -4,12 +4,18 @@ from __future__ import annotations import contextlib +import logging +import os import time from typing import Generator import jwt import requests +LOGGING_LEVEL = os.environ.get("LOGGING_LEVEL", logging.INFO) +logger = logging.getLogger(__name__) +logger.setLevel(LOGGING_LEVEL) + class GithubAppToken: def __init__(self, private_key, app_id) -> None: @@ -29,10 +35,18 @@ def get_token(self, installation_id: int) -> Generator[str, None, None]: # This token expires in an hour yield resp["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 {resp['token']}"}, + ) + except requests.RequestException: + # Token revocation is best-effort. The token expires quickly even + # if this request fails, and we should not mask the primary error. + 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..8146023 --- /dev/null +++ b/tests/test_github_app.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from unittest import mock + +import pytest +import requests + +from src.github_app import GithubAppToken + + +def _token_response(token: str = "temporary-token"): + response = mock.Mock() + response.raise_for_status.return_value = None + response.json.return_value = {"token": token} + return response + + +def test_get_token_cleanup_failure_does_not_mask_primary_error(): + post_response = _token_response() + + with ( + mock.patch.object( + GithubAppToken, + "get_authentication_header", + return_value={"Authorization": "Bearer test-jwt"}, + ), + mock.patch("src.github_app.requests.post", return_value=post_response), + mock.patch( + "src.github_app.requests.delete", + side_effect=requests.ConnectionError("connection reset"), + ) as delete_mock, + ): + token_manager = GithubAppToken(private_key="irrelevant", app_id="123") + + with pytest.raises(RuntimeError, match="primary failure"): + with token_manager.get_token(installation_id=42): + raise RuntimeError("primary failure") + + delete_mock.assert_called_once_with( + "https://api.github.com/installation/token", + headers={"Authorization": "token temporary-token"}, + ) + + +def test_get_token_cleanup_failure_is_best_effort_without_primary_error(): + post_response = _token_response() + + with ( + mock.patch.object( + GithubAppToken, + "get_authentication_header", + return_value={"Authorization": "Bearer test-jwt"}, + ), + mock.patch("src.github_app.requests.post", return_value=post_response), + mock.patch( + "src.github_app.requests.delete", + side_effect=requests.ConnectionError("connection reset"), + ), + ): + token_manager = GithubAppToken(private_key="irrelevant", app_id="123") + + with token_manager.get_token(installation_id=42) as token: + assert token == "temporary-token"