Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 47 additions & 10 deletions gltest/direct/sdk_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@

CACHE_DIR = Path.home() / ".cache" / "gltest-direct"
GITHUB_RELEASES_URL = "https://github.com/genlayerlabs/genvm/releases"
GITHUB_API_RELEASES = "https://api.github.com/repos/genlayerlabs/genvm/releases"

# The runner bundle the direct loader knows how to consume.
UNIVERSAL_ASSET = "genvm-universal.tar.xz"
# Pins the GenVM release instead of tracking GitHub's "latest" — lets CI stay
# deterministic across GenVM releases that may restructure or drop assets.
GENVM_VERSION_ENV = "GENVM_VERSION"
# Used only when the GitHub API is unreachable; last release shipping the
# universal tarball as of writing.
FALLBACK_VERSION = "v0.2.16"

RUNNER_TYPE = "py-genlayer"
STD_LIB_TYPE = "py-lib-genlayer-std"
Expand All @@ -43,19 +53,47 @@ def parse_contract_header(contract_path: Path) -> Dict[str, str]:


def get_latest_version() -> str:
"""Get latest genvm release version from GitHub."""
"""Newest GenVM release that still ships the universal tarball.

Skips pre-releases and releases without ``genvm-universal.tar.xz``: the
0.3.0+ line restructured its release assets, so GitHub's bare "latest" can
point at a release the direct runner cannot consume.
"""
try:
req = urllib.request.Request(
f"{GITHUB_RELEASES_URL}/latest",
method="HEAD",
f"{GITHUB_API_RELEASES}?per_page=100",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Paginate release lookup before falling back

get_latest_version() only fetches ?per_page=100 once and then falls back to FALLBACK_VERSION if no matching asset is found in that single page. Since GitHub release listing is paginated (max 100 per page), once there are more than 100 newer releases without genvm-universal.tar.xz, this code will incorrectly return the hardcoded fallback instead of the newest compatible release, leading the direct runner to use a stale pinned version (or fail if that fallback tag/asset is removed).

Useful? React with 👍 / 👎.

headers={
"User-Agent": "gltest-direct",
"Accept": "application/vnd.github+json",
},
)
req.add_header("User-Agent", "gltest-direct")
with urllib.request.urlopen(req, timeout=10) as resp:
final_url = resp.url
version = final_url.split("/")[-1]
return version
releases = json.loads(resp.read().decode("utf-8"))
for release in releases: # GitHub returns releases newest-first
if release.get("prerelease") or release.get("draft"):
continue
assets = release.get("assets", [])
if any(asset.get("name") == UNIVERSAL_ASSET for asset in assets):
return release["tag_name"]
except Exception:
return "v0.2.12"
pass
return FALLBACK_VERSION
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated


def resolve_version() -> str:
"""Resolve which GenVM version the direct runner should use.

Priority: ``GENVM_VERSION`` env var > newest cached version > latest
release. The env var lets callers pin a deterministic version instead of
tracking whatever GitHub currently marks as the latest release.
"""
pinned = os.environ.get(GENVM_VERSION_ENV)
if pinned:
return pinned
cached = list_cached_versions()
if cached:
return cached[0]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return get_latest_version()


def list_cached_versions() -> List[str]:
Expand Down Expand Up @@ -208,8 +246,7 @@ def setup_sdk_paths(
contract_deps = parse_contract_header(contract_path)

if version is None:
cached = list_cached_versions()
version = cached[0] if cached else get_latest_version()
version = resolve_version()

tarball = download_artifacts(version)

Expand Down
88 changes: 88 additions & 0 deletions tests/gltest_direct/test_sdk_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Unit tests for direct-runner GenVM version resolution."""

import json

from gltest.direct import sdk_loader


class _FakeResponse:
"""Minimal context-manager stand-in for urllib's HTTP response."""

def __init__(self, payload):
self._payload = payload

def __enter__(self):
return self

def __exit__(self, *exc):
return False

def read(self):
return json.dumps(self._payload).encode("utf-8")


class TestResolveVersion:
"""resolve_version() precedence: env var > cache > latest release."""

def test_env_var_takes_precedence(self, monkeypatch):
monkeypatch.setenv(sdk_loader.GENVM_VERSION_ENV, "v1.2.3")
monkeypatch.setattr(sdk_loader, "list_cached_versions", lambda: ["v0.2.16"])
monkeypatch.setattr(sdk_loader, "get_latest_version", lambda: "v0.9.9")

assert sdk_loader.resolve_version() == "v1.2.3"

def test_falls_back_to_newest_cached_version(self, monkeypatch):
monkeypatch.delenv(sdk_loader.GENVM_VERSION_ENV, raising=False)
monkeypatch.setattr(sdk_loader, "list_cached_versions", lambda: ["v0.2.16"])
monkeypatch.setattr(sdk_loader, "get_latest_version", lambda: "v0.9.9")

assert sdk_loader.resolve_version() == "v0.2.16"

def test_falls_back_to_latest_when_no_cache(self, monkeypatch):
monkeypatch.delenv(sdk_loader.GENVM_VERSION_ENV, raising=False)
monkeypatch.setattr(sdk_loader, "list_cached_versions", lambda: [])
monkeypatch.setattr(sdk_loader, "get_latest_version", lambda: "v0.9.9")

assert sdk_loader.resolve_version() == "v0.9.9"


class TestGetLatestVersion:
"""get_latest_version() skips pre-releases and assetless releases."""

def test_skips_releases_without_universal_asset(self, monkeypatch):
releases = [
{"tag_name": "v0.3.0-rc0", "prerelease": False, "assets": [
{"name": "genvm-linux-amd64.tar.xz"},
]},
{"tag_name": "v0.2.16", "prerelease": False, "assets": [
{"name": sdk_loader.UNIVERSAL_ASSET},
]},
]
monkeypatch.setattr(
"urllib.request.urlopen", lambda *a, **k: _FakeResponse(releases)
)

assert sdk_loader.get_latest_version() == "v0.2.16"

def test_skips_prereleases(self, monkeypatch):
releases = [
{"tag_name": "v0.3.0-rc0", "prerelease": True, "assets": [
{"name": sdk_loader.UNIVERSAL_ASSET},
]},
{"tag_name": "v0.2.16", "prerelease": False, "assets": [
{"name": sdk_loader.UNIVERSAL_ASSET},
]},
]
monkeypatch.setattr(
"urllib.request.urlopen", lambda *a, **k: _FakeResponse(releases)
)

assert sdk_loader.get_latest_version() == "v0.2.16"

def test_returns_fallback_when_api_unreachable(self, monkeypatch):
def _boom(*a, **k):
raise OSError("network down")

monkeypatch.setattr("urllib.request.urlopen", _boom)

assert sdk_loader.get_latest_version() == sdk_loader.FALLBACK_VERSION
Loading