diff --git a/app/cli.py b/app/cli.py index 941e25c..4b10816 100644 --- a/app/cli.py +++ b/app/cli.py @@ -2,7 +2,6 @@ import sys import click -import requests from click_aliases import ClickAliasedGroup from app.aliases import COMMAND_ALIASES @@ -10,7 +9,7 @@ from app.commands.repl import repl from app.commands.version import version from app.utils.click import ClickColor, CliContextKey, warn -from app.utils.version import Version +from app.utils.version import Version, fetch_latest_release_version from app.version import __version__ @@ -39,14 +38,13 @@ def cli(ctx: click.Context, verbose: bool) -> None: current_version = Version.parse_version_string(__version__) ctx.obj[CliContextKey.VERSION] = current_version - latest_version = ( - requests.get( - "https://github.com/git-mastery/app/releases/latest", allow_redirects=False - ) - .headers["Location"] - .rsplit("/", 1)[-1] - ) - if current_version.is_behind(Version.parse_version_string(latest_version)): + + # Latest version checking is a soft dependency, should not fail operation + latest_version, latest_version_error = fetch_latest_release_version() + if latest_version_error is not None: + warn(latest_version_error) + + if latest_version is not None and current_version.is_behind(latest_version): warn( click.style( f"Your version of Git-Mastery app {current_version} is behind the latest version {latest_version}.", diff --git a/app/utils/version.py b/app/utils/version.py index 600a7ee..de78a83 100644 --- a/app/utils/version.py +++ b/app/utils/version.py @@ -1,6 +1,11 @@ from dataclasses import dataclass from typing import Optional +import requests + +LATEST_RELEASE_TIMEOUT_SECONDS = 5 +LATEST_RELEASE_URL = "https://github.com/git-mastery/app/releases/latest" + @dataclass class Version: @@ -24,7 +29,9 @@ def parse_version_string(version: str) -> "Version": def parse(version: str) -> "Version": """Parse a plain version string (e.g., '1.2.3').""" parts = version.split(".") - if ("beta" in version and len(parts) != 4) or ("beta" not in version and len(parts) != 3): + if ("beta" in version and len(parts) != 4) or ( + "beta" not in version and len(parts) != 3 + ): raise ValueError( f"Invalid version string (expected 'MAJOR.MINOR.PATCH[-beta.PRERELEASE]'): {version!r}" ) @@ -57,3 +64,18 @@ def __repr__(self) -> str: if self.prerelease is not None: return f"v{self.major}.{self.minor}.{self.patch}-beta.{self.prerelease}" return f"v{self.major}.{self.minor}.{self.patch}" + + +def fetch_latest_release_version() -> tuple[Optional["Version"], Optional[str]]: + try: + response = requests.get( + LATEST_RELEASE_URL, + allow_redirects=False, + timeout=LATEST_RELEASE_TIMEOUT_SECONDS, + ) + location = response.headers.get("Location") + if location is None: + return None, "Unable to verify the latest version release" + return Version.parse_version_string(location.rsplit("/", 1)[-1]), None + except (requests.exceptions.RequestException, ValueError) as e: + return None, f"Unable to verify the latest version release: {e}"