Public Access
DEVX-149: fix: retry ansible-galaxy collection install on transient timeouts
This commit was merged in pull request #239.
This commit is contained in:
@@ -87,7 +87,7 @@ extra index and list devx in your dependencies:
|
|||||||
```toml
|
```toml
|
||||||
[project]
|
[project]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"devx>=0.49.2",
|
"devx>=0.49.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pip]
|
[tool.pip]
|
||||||
@@ -101,8 +101,8 @@ pip install -e .
|
|||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** If your project requires a specific devx version, pin it in
|
> **Note:** If your project requires a specific devx version, pin it in
|
||||||
> `dependencies` (for example, `"devx==0.49.2"`) or use a version constraint
|
> `dependencies` (for example, `"devx==0.49.3"`) or use a version constraint
|
||||||
> (for example, `"devx>=0.49.2,<0.50"`).
|
> (for example, `"devx>=0.49.3,<0.50"`).
|
||||||
|
|
||||||
### Optional extras
|
### Optional extras
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -74,14 +74,14 @@ Add devx to your `pyproject.toml` dependencies and configure the registry:
|
|||||||
```toml
|
```toml
|
||||||
[project]
|
[project]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"devx>=0.49.2",
|
"devx>=0.49.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pip]
|
[tool.pip]
|
||||||
extra-index-url = "https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple"
|
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
|
### Optional extras
|
||||||
|
|
||||||
|
|||||||
@@ -48,12 +48,12 @@ Add devx to your `pyproject.toml`:
|
|||||||
```toml
|
```toml
|
||||||
[project]
|
[project]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"devx>=0.49.2",
|
"devx>=0.49.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"devx>=0.49.2",
|
"devx>=0.49.3",
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
|
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
|
||||||
|
|
||||||
__version__ = "0.49.2"
|
__version__ = "0.49.3"
|
||||||
|
|||||||
+11
-1
@@ -15,6 +15,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType]
|
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
|
from devx.tokens import get_developer_token
|
||||||
|
|
||||||
@@ -56,14 +57,23 @@ def _install_pre_commit_hooks(bin_dir: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _install_ansible_collections(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")
|
galaxy = shutil.which("ansible-galaxy") or str(Path(bin_dir) / "ansible-galaxy")
|
||||||
requirements = Path("ansible/requirements.yml")
|
requirements = Path("ansible/requirements.yml")
|
||||||
if not requirements.exists():
|
if not requirements.exists():
|
||||||
click.echo(" ansible/requirements.yml not found — skipping collections.")
|
click.echo(" ansible/requirements.yml not found — skipping collections.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@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)])
|
_run([galaxy, "collection", "install", "-r", str(requirements)])
|
||||||
|
|
||||||
|
_do_install()
|
||||||
|
|
||||||
|
|
||||||
def _configure_tea_login() -> None:
|
def _configure_tea_login() -> None:
|
||||||
"""Configure tea CLI login from .env if a Gitea token is set.
|
"""Configure tea CLI login from .env if a Gitea token is set.
|
||||||
|
|||||||
@@ -114,6 +114,49 @@ class TestInstallAnsibleCollections:
|
|||||||
_install_ansible_collections(".venv/bin")
|
_install_ansible_collections(".venv/bin")
|
||||||
mock_run.assert_not_called()
|
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:
|
class TestConfigureTeaLogin:
|
||||||
@patch("devx.tools.setup.shutil.which", return_value=None)
|
@patch("devx.tools.setup.shutil.which", return_value=None)
|
||||||
|
|||||||
Reference in New Issue
Block a user