DEVX-149: fix: retry ansible-galaxy collection install on transient timeouts

This commit is contained in:
2026-08-07 20:26:52 +00:00
parent 03f057b55a
commit 2669a0ea73
6 changed files with 63 additions and 10 deletions
+3 -3
View File
@@ -87,7 +87,7 @@ extra index and list devx in your dependencies:
```toml
[project]
dependencies = [
"devx>=0.49.2",
"devx>=0.49.3",
]
[tool.pip]
@@ -101,8 +101,8 @@ pip install -e .
```
> **Note:** If your project requires a specific devx version, pin it in
> `dependencies` (for example, `"devx==0.49.2"`) or use a version constraint
> (for example, `"devx>=0.49.2,<0.50"`).
> `dependencies` (for example, `"devx==0.49.3"`) or use a version constraint
> (for example, `"devx>=0.49.3,<0.50"`).
### Optional extras
+2 -2
View File
@@ -74,14 +74,14 @@ Add devx to your `pyproject.toml` dependencies and configure the registry:
```toml
[project]
dependencies = [
"devx>=0.49.2",
"devx>=0.49.3",
]
[tool.pip]
extra-index-url = "https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple"
```
Pin a specific version if needed: `"devx==0.49.2"` or `"devx>=0.49.2,<0.50"`.
Pin a specific version if needed: `"devx==0.49.3"` or `"devx>=0.49.3,<0.50"`.
### Optional extras
+2 -2
View File
@@ -48,12 +48,12 @@ Add devx to your `pyproject.toml`:
```toml
[project]
dependencies = [
"devx>=0.49.2",
"devx>=0.49.3",
]
[project.optional-dependencies]
dev = [
"devx>=0.49.2",
"devx>=0.49.3",
]
```
+1 -1
View File
@@ -1,3 +1,3 @@
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
__version__ = "0.49.2"
__version__ = "0.49.3"
+12 -2
View File
@@ -15,6 +15,7 @@ from pathlib import Path
import click
from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType]
from tenacity import retry, stop_after_attempt, wait_exponential
from devx.tokens import get_developer_token
@@ -56,13 +57,22 @@ def _install_pre_commit_hooks(bin_dir: str) -> None:
def _install_ansible_collections(bin_dir: str) -> None:
"""Install required Ansible Galaxy collections if requirements exist."""
"""Install required Ansible Galaxy collections if requirements exist.
Retries up to 3 times with exponential backoff to handle transient
network timeouts when contacting galaxy.ansible.com.
"""
galaxy = shutil.which("ansible-galaxy") or str(Path(bin_dir) / "ansible-galaxy")
requirements = Path("ansible/requirements.yml")
if not requirements.exists():
click.echo(" ansible/requirements.yml not found — skipping collections.")
return
_run([galaxy, "collection", "install", "-r", str(requirements)])
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=2, min=2, max=10), reraise=True)
def _do_install() -> None:
_run([galaxy, "collection", "install", "-r", str(requirements)])
_do_install()
def _configure_tea_login() -> None:
+43
View File
@@ -114,6 +114,49 @@ class TestInstallAnsibleCollections:
_install_ansible_collections(".venv/bin")
mock_run.assert_not_called()
@patch("tenacity.nap.time.sleep")
@patch("devx.tools.setup.shutil.which", return_value="/usr/local/bin/ansible-galaxy")
@patch("devx.tools.setup._run")
def test_retries_on_transient_failure(
self, mock_run: MagicMock, mock_which: MagicMock, mock_sleep: MagicMock, tmp_path: Path
) -> None:
"""ansible-galaxy install should retry on transient network errors."""
import subprocess as _subprocess
req = tmp_path / "ansible" / "requirements.yml"
req.parent.mkdir(parents=True)
req.write_text("collections: []")
# First call fails (timeout), second succeeds
mock_run.side_effect = [
_subprocess.CalledProcessError(1, ["ansible-galaxy", "collection", "install"]),
None,
]
with patch("devx.tools.setup.Path") as mock_path:
mock_path.return_value.exists.return_value = True
mock_path.return_value.__str__ = lambda _: str(req)
_install_ansible_collections(".venv/bin")
assert mock_run.call_count == 2
@patch("tenacity.nap.time.sleep")
@patch("devx.tools.setup.shutil.which", return_value="/usr/local/bin/ansible-galaxy")
@patch("devx.tools.setup._run")
def test_exhausts_retries_then_raises(
self, mock_run: MagicMock, mock_which: MagicMock, mock_sleep: MagicMock, tmp_path: Path
) -> None:
"""After 3 attempts, the error should propagate."""
import subprocess as _subprocess
req = tmp_path / "ansible" / "requirements.yml"
req.parent.mkdir(parents=True)
req.write_text("collections: []")
mock_run.side_effect = _subprocess.CalledProcessError(1, ["ansible-galaxy"])
with patch("devx.tools.setup.Path") as mock_path:
mock_path.return_value.exists.return_value = True
mock_path.return_value.__str__ = lambda _: str(req)
with pytest.raises(_subprocess.CalledProcessError):
_install_ansible_collections(".venv/bin")
assert mock_run.call_count == 3
class TestConfigureTeaLogin:
@patch("devx.tools.setup.shutil.which", return_value=None)