From 60728608e4aa9901aab4ef91309e880b3e20a386 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 29 Apr 2026 07:02:17 +0000 Subject: [PATCH 1/3] Retry transient GitHub config fetch failures Co-authored-by: Armen Zambrano G. --- src/sentry_config.py | 28 +++++++++++++++++++++++++++- tests/test_sentry_config_file.py | 20 ++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/sentry_config.py b/src/sentry_config.py index 30678da..b8722ea 100644 --- a/src/sentry_config.py +++ b/src/sentry_config.py @@ -7,6 +7,8 @@ from functools import lru_cache import requests +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry LOGGING_LEVEL = os.environ.get("LOGGING_LEVEL", logging.INFO) logger = logging.getLogger(__name__) @@ -15,6 +17,26 @@ SENTRY_CONFIG_API_URL = ( "https://api.github.com/repos/{owner}/.sentry/contents/sentry_config.ini" ) +GITHUB_API_TIMEOUT_SECONDS = 10 +GITHUB_API_RETRIES = 2 + + +@lru_cache(maxsize=1) +def _github_api_session() -> requests.Session: + retry = Retry( + total=GITHUB_API_RETRIES, + connect=GITHUB_API_RETRIES, + read=GITHUB_API_RETRIES, + status=GITHUB_API_RETRIES, + backoff_factor=0.25, + status_forcelist=(500, 502, 503, 504), + allowed_methods=("GET",), + raise_on_status=False, + ) + adapter = HTTPAdapter(max_retries=retry) + session = requests.Session() + session.mount("https://api.github.com/", adapter) + return session def fetch_dsn_for_github_org(org: str, token: str) -> str: @@ -27,7 +49,11 @@ def fetch_dsn_for_github_org(org: str, token: str) -> str: api_url = SENTRY_CONFIG_API_URL.replace("{owner}", org) # - Get meta about sentry_config.ini file - resp = requests.get(api_url, headers=headers) + resp = _github_api_session().get( + api_url, + headers=headers, + timeout=GITHUB_API_TIMEOUT_SECONDS, + ) resp.raise_for_status() meta = resp.json() diff --git a/tests/test_sentry_config_file.py b/tests/test_sentry_config_file.py index db95aa4..2ed2ccb 100644 --- a/tests/test_sentry_config_file.py +++ b/tests/test_sentry_config_file.py @@ -3,7 +3,9 @@ from unittest import TestCase import responses +from requests.exceptions import ConnectionError +from src.sentry_config import _github_api_session from src.sentry_config import fetch_dsn_for_github_org from src.sentry_config import SENTRY_CONFIG_API_URL as api_url @@ -34,6 +36,7 @@ class TestSentryConfigCase(TestCase): def setUp(self) -> None: + _github_api_session.cache_clear() self.api_url = api_url.replace("{owner}", org) responses.add( method="GET", @@ -47,6 +50,23 @@ def setUp(self) -> None: def test_fetch_parse_sentry_config_file(self) -> None: assert fetch_dsn_for_github_org(org, token) == expected_dsn + @responses.activate + def test_fetch_retries_transient_connection_error(self) -> None: + responses.replace( + method="GET", + url=self.api_url, + body=ConnectionError("Connection reset by peer"), + ) + responses.add( + method="GET", + url=self.api_url, + json=sentry_config_file_meta, + status=200, + ) + + assert fetch_dsn_for_github_org(org, token) == expected_dsn + assert len(responses.calls) == 2 + def test_fetch_private_repo(self) -> None: pass From 6ac00e2081c27368cd7b18556a39011eb60dfd3a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 29 Apr 2026 07:03:10 +0000 Subject: [PATCH 2/3] Fix transient retry regression test Co-authored-by: Armen Zambrano G. --- tests/test_sentry_config_file.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_sentry_config_file.py b/tests/test_sentry_config_file.py index 2ed2ccb..a01b1f8 100644 --- a/tests/test_sentry_config_file.py +++ b/tests/test_sentry_config_file.py @@ -53,8 +53,8 @@ def test_fetch_parse_sentry_config_file(self) -> None: @responses.activate def test_fetch_retries_transient_connection_error(self) -> None: responses.replace( - method="GET", - url=self.api_url, + responses.GET, + self.api_url, body=ConnectionError("Connection reset by peer"), ) responses.add( From a2cb7aab1ee3ae7fc0f6de75aa7a86191c4ddce3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 29 Apr 2026 07:03:53 +0000 Subject: [PATCH 3/3] Use explicit retries for GitHub config fetch Co-authored-by: Armen Zambrano G. --- src/sentry_config.py | 50 +++++++++++++++++--------------- tests/test_sentry_config_file.py | 2 -- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/sentry_config.py b/src/sentry_config.py index b8722ea..4eea2df 100644 --- a/src/sentry_config.py +++ b/src/sentry_config.py @@ -4,11 +4,8 @@ import logging import os from configparser import ConfigParser -from functools import lru_cache import requests -from requests.adapters import HTTPAdapter -from urllib3.util.retry import Retry LOGGING_LEVEL = os.environ.get("LOGGING_LEVEL", logging.INFO) logger = logging.getLogger(__name__) @@ -19,24 +16,33 @@ ) GITHUB_API_TIMEOUT_SECONDS = 10 GITHUB_API_RETRIES = 2 +GITHUB_API_RETRY_STATUS_CODES = {500, 502, 503, 504} -@lru_cache(maxsize=1) -def _github_api_session() -> requests.Session: - retry = Retry( - total=GITHUB_API_RETRIES, - connect=GITHUB_API_RETRIES, - read=GITHUB_API_RETRIES, - status=GITHUB_API_RETRIES, - backoff_factor=0.25, - status_forcelist=(500, 502, 503, 504), - allowed_methods=("GET",), - raise_on_status=False, - ) - adapter = HTTPAdapter(max_retries=retry) - session = requests.Session() - session.mount("https://api.github.com/", adapter) - return session +def _fetch_github_config(api_url: str, headers: dict[str, str]) -> requests.Response: + for attempt in range(GITHUB_API_RETRIES + 1): + try: + resp = requests.get( + api_url, + headers=headers, + timeout=GITHUB_API_TIMEOUT_SECONDS, + ) + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): + if attempt == GITHUB_API_RETRIES: + raise + logger.warning("Retrying transient GitHub config fetch failure") + continue + + if resp.status_code not in GITHUB_API_RETRY_STATUS_CODES: + return resp + if attempt == GITHUB_API_RETRIES: + return resp + logger.warning( + "Retrying GitHub config fetch after HTTP %s", + resp.status_code, + ) + + raise RuntimeError("unreachable") def fetch_dsn_for_github_org(org: str, token: str) -> str: @@ -49,11 +55,7 @@ def fetch_dsn_for_github_org(org: str, token: str) -> str: api_url = SENTRY_CONFIG_API_URL.replace("{owner}", org) # - Get meta about sentry_config.ini file - resp = _github_api_session().get( - api_url, - headers=headers, - timeout=GITHUB_API_TIMEOUT_SECONDS, - ) + resp = _fetch_github_config(api_url, headers) resp.raise_for_status() meta = resp.json() diff --git a/tests/test_sentry_config_file.py b/tests/test_sentry_config_file.py index a01b1f8..7225a5e 100644 --- a/tests/test_sentry_config_file.py +++ b/tests/test_sentry_config_file.py @@ -5,7 +5,6 @@ import responses from requests.exceptions import ConnectionError -from src.sentry_config import _github_api_session from src.sentry_config import fetch_dsn_for_github_org from src.sentry_config import SENTRY_CONFIG_API_URL as api_url @@ -36,7 +35,6 @@ class TestSentryConfigCase(TestCase): def setUp(self) -> None: - _github_api_session.cache_clear() self.api_url = api_url.replace("{owner}", org) responses.add( method="GET",