diff --git a/src/github_app.py b/src/github_app.py index c18b6a9..d5bbadd 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: @@ -29,10 +32,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: + revoke_response = requests.delete( + "https://api.github.com/installation/token", + headers={"Authorization": f"token {resp['token']}"}, + timeout=10, + ) + revoke_response.raise_for_status() + except requests.RequestException: + logger.warning( + "Failed to revoke GitHub installation token; it will expire automatically.", + 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..f15fca8 --- /dev/null +++ b/tests/test_github_app.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +import requests +import responses + +from src.github_app import GithubAppToken + + +@responses.activate +def test_token_revocation_failure_does_not_mask_successful_work(): + token = "synthetic-token" + installation_id = 1234 + client = object.__new__(GithubAppToken) + client.headers = {"Authorization": "Bearer synthetic-jwt"} + + responses.post( + f"https://api.github.com/app/installations/{installation_id}/access_tokens", + json={"token": token}, + status=201, + ) + responses.delete( + "https://api.github.com/installation/token", + body=requests.ConnectionError("connection closed during cleanup"), + ) + + with client.get_token(installation_id) as installation_token: + assert installation_token == token + + assert len(responses.calls) == 2