Skip to content

Commit

Permalink
feat(version-check): Added check to periodically check for new releas…
Browse files Browse the repository at this point in the history
…es on GitHub and inform when found (#111)

* feat(version-check): add version check functionality
* test: fix enable/disable version check
* review: address review feedback
* refactor: move version prompt enable/disable to a config subcommand, similar to git
* feat(version-check): add latest algokit version to doctor command
* refactor(version-check): remove version parsing, sorry time travelers 🖖
* lint: update to latest ruff version 🐩
* lint: exclude dist/ folder from mypy checks (if present)
* test(version-check): additional tests

Co-authored-by: Adam Chidlow <achidlow@users.noreply.github.com>
  • Loading branch information
daniel-makerx and achidlow authored Dec 23, 2022
1 parent 1eb7335 commit 1772439
Show file tree
Hide file tree
Showing 43 changed files with 370 additions and 48 deletions.
36 changes: 18 additions & 18 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ shellingham = "^1.5.0"
[tool.poetry.group.dev.dependencies]
pytest = "^7.2.0"
black = {extras = ["d"], version = "^22.10.0"}
ruff = "^0.0.150"
ruff = "^0.0.191"
pip-audit = "^2.4.7"
approvaltests = "^7.2.0"
pytest-mock = "^3.10.0"
Expand All @@ -37,7 +37,7 @@ algokit = "algokit.cli:algokit"

[tool.ruff]
line-length = 120
select = ["E", "F", "ANN", "U", "N", "C", "B", "A", "YTT", "M", "W", "FBT", "Q", "RUF", "I"]
select = ["E", "F", "ANN", "UP", "N", "C", "B", "A", "YTT", "W", "FBT", "Q", "RUF", "I"]
# note: remove A003 once flake8-builtins: builtins-ignorelist supported and can ignore id
ignore = ["ANN101", "C901", "A003"]
# Exclude a variety of commonly ignored directories.
Expand Down Expand Up @@ -79,7 +79,8 @@ line-length = 120
pythonpath = ["src", "tests"]

[tool.mypy]
files = "src/"
files = ["src"]
exclude = ["dist", "tests"]
python_version = "3.10"
warn_unused_ignores = true
warn_redundant_casts = true
Expand Down
10 changes: 8 additions & 2 deletions src/algokit/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import click

from algokit.cli.bootstrap import bootstrap_group
from algokit.cli.completions import completions_group
from algokit.cli.config import config_group
from algokit.cli.doctor import doctor_command
from algokit.cli.goal import goal_command
from algokit.cli.init import init_command
from algokit.cli.sandbox import sandbox_group
from algokit.core.conf import PACKAGE_NAME
from algokit.core.log_handlers import color_option, verbose_option
from algokit.core.version_prompt import do_version_prompt, skip_version_check_option


@click.group(
Expand All @@ -16,12 +19,15 @@
@click.version_option(package_name=PACKAGE_NAME)
@verbose_option
@color_option
def algokit() -> None:
pass
@skip_version_check_option
def algokit(*, skip_version_check: bool) -> None:
if not skip_version_check:
do_version_prompt()


algokit.add_command(bootstrap_group)
algokit.add_command(completions_group)
algokit.add_command(config_group)
algokit.add_command(doctor_command)
algokit.add_command(goal_command)
algokit.add_command(init_command)
Expand Down
1 change: 1 addition & 0 deletions src/algokit/cli/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from pathlib import Path

import click

from algokit.core.bootstrap import bootstrap_any_including_subdirs, bootstrap_env, bootstrap_poetry
from algokit.core.questionary_extensions import _get_confirm_default_yes_prompt

Expand Down
1 change: 1 addition & 0 deletions src/algokit/cli/completions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import click
import click.shell_completion
import shellingham # type: ignore

from algokit.core.atomic_write import atomic_write
from algokit.core.conf import get_app_config_dir

Expand Down
11 changes: 11 additions & 0 deletions src/algokit/cli/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import click

from algokit.core.version_prompt import version_prompt_configuration_command


@click.group("config", short_help="Configure AlgoKit options")
def config_group() -> None:
pass


config_group.add_command(version_prompt_configuration_command)
28 changes: 23 additions & 5 deletions src/algokit/cli/doctor.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import datetime as dt
import importlib.metadata
import logging
import platform
import sys

import click
import pyclip # type: ignore
from algokit.core.conf import PACKAGE_NAME

from algokit.core.conf import get_current_package_version
from algokit.core.doctor import DoctorResult, check_dependency
from algokit.core.sandbox import (
DOCKER_COMPOSE_MINIMUM_VERSION,
DOCKER_COMPOSE_VERSION_COMMAND,
parse_docker_compose_version_output,
)
from algokit.core.version_prompt import get_latest_github_version

logger = logging.getLogger(__name__)

WARNING_COLOR = "yellow"
CRITICAL_COLOR = "red"


@click.command(
"doctor",
Expand All @@ -36,7 +40,7 @@ def doctor_command(*, copy_to_clipboard: bool) -> None:
is_windows = os_type == "Windows"
service_outputs = {
"timestamp": DoctorResult(ok=True, output=dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat()),
"AlgoKit": DoctorResult(ok=True, output=importlib.metadata.version(PACKAGE_NAME)),
"AlgoKit": _get_algokit_version_output(),
"AlgoKit Python": DoctorResult(ok=True, output=f"{sys.version} (location: {sys.prefix})"),
"OS": DoctorResult(ok=True, output=platform.platform()),
"docker": check_dependency(
Expand Down Expand Up @@ -103,7 +107,7 @@ def doctor_command(*, copy_to_clipboard: bool) -> None:
if value.ok:
color = None
else:
color = "red" if key in critical_services else "yellow"
color = CRITICAL_COLOR if key in critical_services else WARNING_COLOR
msg = click.style(f"{key}: ", bold=True) + click.style(value.output, fg=color)
for ln in value.extra_help or []:
msg += f"\n {ln}"
Expand All @@ -112,7 +116,7 @@ def doctor_command(*, copy_to_clipboard: bool) -> None:
# print end message anyway
logger.info(
"\n"
"If you are experiencing a problem with algokit, feel free to submit an issue via:\n"
"If you are experiencing a problem with AlgoKit, feel free to submit an issue via:\n"
"https://github.com/algorandfoundation/algokit-cli/issues/new\n"
"Please include this output, if you want to populate this message in your clipboard, run `algokit doctor -c`"
)
Expand All @@ -127,3 +131,17 @@ def doctor_command(*, copy_to_clipboard: bool) -> None:

if any(not value.ok for value in service_outputs.values()):
raise click.exceptions.Exit(code=1)


def _get_algokit_version_output() -> DoctorResult:
current = get_current_package_version()
try:
latest = get_latest_github_version()
except Exception as ex:
logger.warning("Failed to check latest AlgoKit release version", exc_info=ex)
latest = None
if latest is None or current == latest:
output = current
else:
output = click.style(current, fg=WARNING_COLOR) + f" (latest: {latest})"
return DoctorResult(ok=True, output=output)
1 change: 1 addition & 0 deletions src/algokit/cli/goal.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging

import click

from algokit.core import proc

logger = logging.getLogger(__name__)
Expand Down
1 change: 1 addition & 0 deletions src/algokit/cli/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import copier.vcs # type: ignore
import prompt_toolkit.document
import questionary

from algokit.core import proc
from algokit.core.click_extensions import DeferredChoice
from algokit.core.log_handlers import EXTRA_EXCLUDE_FROM_CONSOLE
Expand Down
1 change: 1 addition & 0 deletions src/algokit/cli/sandbox.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging

import click

from algokit.cli.goal import goal_command
from algokit.core import proc
from algokit.core.sandbox import (
Expand Down
1 change: 1 addition & 0 deletions src/algokit/core/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import Callable, Iterator

import click

from algokit.core import proc

ENV_TEMPLATE = ".env.template"
Expand Down
5 changes: 5 additions & 0 deletions src/algokit/core/conf.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import platform
from importlib import metadata
from pathlib import Path

PACKAGE_NAME = "algokit"
Expand Down Expand Up @@ -36,3 +37,7 @@ def _get_relative_app_path(base_dir: str) -> Path:
result = path / PACKAGE_NAME
result.mkdir(parents=True, exist_ok=True)
return result


def get_current_package_version() -> str:
return metadata.version(PACKAGE_NAME)
1 change: 1 addition & 0 deletions src/algokit/core/proc.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from subprocess import run as subprocess_run

import click

from algokit.core.log_handlers import EXTRA_EXCLUDE_FROM_CONSOLE

logger = logging.getLogger(__name__)
Expand Down
1 change: 1 addition & 0 deletions src/algokit/core/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Any, cast

import httpx

from algokit.core.conf import get_app_config_dir
from algokit.core.proc import RunResult, run

Expand Down
106 changes: 106 additions & 0 deletions src/algokit/core/version_prompt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import logging
import os
import re
from datetime import timedelta
from time import time

import click
import httpx

from algokit.core.conf import get_app_config_dir, get_app_state_dir, get_current_package_version

logger = logging.getLogger(__name__)

LATEST_URL = "https://api.github.com/repos/algorandfoundation/algokit-cli/releases/latest"
VERSION_CHECK_INTERVAL = timedelta(weeks=1).total_seconds()
DISABLE_CHECK_MARKER = "disable-version-prompt"


def do_version_prompt() -> None:
if _skip_version_prompt():
logger.debug("Version prompt disabled")
return

current_version = get_current_package_version()
latest_version = get_latest_version_or_cached()
if latest_version is None:
logger.debug("Could not determine latest version")
return

if current_version != latest_version:
logger.info(f"You are using AlgoKit version {current_version}, however version {latest_version} is available.")
else:
logger.debug("Current version is up to date")


def get_latest_version_or_cached() -> str | None:
version_check_path = get_app_state_dir() / "last-version-check"

try:
last_checked = os.path.getmtime(version_check_path)
version = version_check_path.read_text(encoding="utf-8")
except IOError:
logger.debug(f"{version_check_path} inaccessible")
last_checked = 0
version = None
else:
logger.debug(f"{version} found in cache {version_check_path}")

if (time() - last_checked) > VERSION_CHECK_INTERVAL:
try:
version = get_latest_github_version()
except Exception as ex:
logger.debug("Checking for latest version failed", exc_info=ex)
# update last checked time even if check failed
version_check_path.touch()
else:
version_check_path.write_text(version, encoding="utf-8")
# handle case where the first check failed, so we have an empty file
return version or None


def get_latest_github_version() -> str:
headers = {"ACCEPT": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28"}
# TODO: remove GH_TOKEN auth once algokit repo is public
if gh_token := os.getenv("GH_TOKEN"):
headers["Authorization"] = f"Bearer {gh_token}"

response = httpx.get(LATEST_URL, headers=headers)
response.raise_for_status()

json = response.json()
tag_name = json["tag_name"]
logger.debug(f"Latest version tag: {tag_name}")
match = re.match(r"v(\d+\.\d+\.\d+)", tag_name)
if not match:
raise ValueError(f"Unable to extract version from tag_name: {tag_name}")
return match.group(1)


def _skip_version_prompt() -> bool:
disable_marker = get_app_config_dir() / DISABLE_CHECK_MARKER
return disable_marker.exists()


skip_version_check_option = click.option(
"--skip-version-check",
is_flag=True,
show_default=False,
default=False,
help="Skip version checking and prompting.",
)


@click.command("version-prompt", short_help="Enables or disables version prompt")
@click.argument("enable", required=False, type=bool, default=None)
def version_prompt_configuration_command(*, enable: bool | None) -> None:
if enable is None:
logger.info(str(not _skip_version_prompt()))
else:
disable_marker = get_app_config_dir() / DISABLE_CHECK_MARKER
if enable:
disable_marker.unlink(missing_ok=True)
logger.info("📡 Resuming check for new versions")
else:
disable_marker.touch()
logger.info("🚫 Will stop checking for new versions")
Loading

1 comment on commit 1772439

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
src/algokit
   __init__.py15753%6–13, 17–24, 32–34
   __main__.py220%1–3
src/algokit/cli
   completions.py103199%91
   init.py1651690%57, 204–205, 236, 239–241, 252, 290, 327, 336–338, 341–346, 361
src/algokit/core
   bootstrap.py1001684%75, 95, 150, 153, 159–173
   click_extensions.py472057%40–43, 50, 56, 67–68, 73–74, 79–80, 91, 104–114
   conf.py30487%13, 17, 25, 27
   doctor.py54394%63–65
   log_handlers.py68987%44–45, 50–51, 63, 112–116, 125
   proc.py44198%95
   sandbox.py112794%86, 151, 167, 182–184, 199
   version_prompt.py70987%27–28, 33, 52–55, 66, 76, 98
TOTAL10509591% 

Tests Skipped Failures Errors Time
135 0 💤 0 ❌ 0 🔥 12.240s ⏱️

Please sign in to comment.