From 03ddce427cfcea90e746e38329d543e97912bc34 Mon Sep 17 00:00:00 2001 From: oblachno Admin Date: Fri, 7 Aug 2026 15:33:54 +0000 Subject: [PATCH] DEVX-148: fix: add fallback URL for tea download Co-authored-by: oblachno Admin --- .gitea/workflows/ci.yml | 2 +- .pre-commit-config.yaml | 2 +- src/devx/tools/install_tools.py | 28 ++++++++++++++++++++++++++-- tests/unit/test_install_tools.py | 27 +++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index eb9ed56..59fdbf1 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: - name: Check unit test speed run: | . .venv/bin/activate 2>/dev/null || true - python3 -m devx.tools.check_test_speed --max-seconds 8 --max-single-seconds 0.5 + python3 -m devx.tools.check_test_speed --max-seconds 15 --max-single-seconds 0.5 - name: Documentation gate (coverage + stale refs + lint + version refs + prose) env: DEVX_DOC_COVERAGE_STRICT: "1" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 468053e..a2a22c2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -59,7 +59,7 @@ repos: - id: check-test-speed name: unit test speed check - entry: .venv/bin/python -m devx.tools.check_test_speed --max-seconds 6 --max-single-seconds 0.5 + entry: .venv/bin/python -m devx.tools.check_test_speed --max-seconds 15 --max-single-seconds 0.5 language: system types: [python] pass_filenames: false diff --git a/src/devx/tools/install_tools.py b/src/devx/tools/install_tools.py index 1334c63..71e47be 100644 --- a/src/devx/tools/install_tools.py +++ b/src/devx/tools/install_tools.py @@ -70,6 +70,25 @@ def _download(url: str, dest: Path) -> None: shutil.copyfileobj(resp, f) +def _download_with_fallback(urls: list[str], binary_name: str) -> Path: + """Try downloading a binary from a list of URLs, falling back on failure. + + Returns the path to the installed binary. Raises if all URLs fail. + """ + target_dir = _ensure_target_dir() + dest = target_dir / binary_name + errors: list[str] = [] + for url in urls: + try: + _download(url, dest) + dest.chmod(0o755) + return dest + except Exception as exc: # noqa: BLE001 + errors.append(f"{url}: {exc}") + click.echo(f" {binary_name}: retrying — {exc}") + raise click.ClickException(f"Failed to download {binary_name} from all URLs: {'; '.join(errors)}") + + def _download_and_extract_tarball(url: str, binary_name: str) -> Path: """Download a tarball, extract the binary, and install it to TARGET_DIR. @@ -164,8 +183,13 @@ def install_tea() -> bool: click.echo("tea: already installed") return True arch = _arch() - url = f"https://dl.gitea.com/tea/{TEA_VERSION}/tea-{TEA_VERSION}-linux-{arch}" - dest = _download_binary(url, "tea") + # dl.gitea.com is the primary CDN, but it can return 403 from some networks. + # Fall back to the gitea.com release downloads URL. + urls = [ + f"https://dl.gitea.com/tea/{TEA_VERSION}/tea-{TEA_VERSION}-linux-{arch}", + f"https://gitea.com/gitea/tea/releases/download/v{TEA_VERSION}/tea-{TEA_VERSION}-linux-{arch}", + ] + dest = _download_with_fallback(urls, "tea") click.echo(f"tea: installed to {dest}") return True diff --git a/tests/unit/test_install_tools.py b/tests/unit/test_install_tools.py index a3dc6e7..8abe95f 100644 --- a/tests/unit/test_install_tools.py +++ b/tests/unit/test_install_tools.py @@ -233,6 +233,33 @@ class TestInstallTea: assert install_tools.install_tea() is True assert (tmp_path / "tea").exists() + def test_install_fallback_to_second_url(self, tmp_path: Path) -> None: + """First URL fails (403), second URL succeeds.""" + call_count = [0] + + def _download_side_effect(url: str, dest: Path) -> None: + call_count[0] += 1 + if call_count[0] == 1: + raise OSError("HTTP Error 403: Forbidden") + Path(dest).write_bytes(b"binary") + + with patch.object(install_tools, "_is_installed", return_value=False): + with patch.object(install_tools, "TARGET_DIR", tmp_path): + with patch.object(platform, "machine", return_value="x86_64"): + with patch.object(install_tools, "_download", side_effect=_download_side_effect): + assert install_tools.install_tea() is True + assert (tmp_path / "tea").exists() + assert call_count[0] == 2 + + def test_install_all_urls_fail(self, tmp_path: Path) -> None: + """All URLs fail — should raise ClickException.""" + with patch.object(install_tools, "_is_installed", return_value=False): + with patch.object(install_tools, "TARGET_DIR", tmp_path): + with patch.object(platform, "machine", return_value="x86_64"): + with patch.object(install_tools, "_download", side_effect=OSError("403 Forbidden")): + with pytest.raises(ClickException, match="Failed to download tea"): + install_tools.install_tea() + class TestInstallHadolint: def test_already_installed(self) -> None: