DEVX-133: fix: auto-discover molecule root instead of hardcoding gitea-runner

This commit is contained in:
emil User
2026-07-13 02:25:59 +00:00
parent 7b624b0525
commit 50dcb67083
6 changed files with 93 additions and 22 deletions
+17 -3
View File
@@ -30,10 +30,24 @@ from devx.i18n import _
from devx.molecule.platforms import PLATFORMS, load_platforms
DEFAULT_MAX_RUNNERS = 3
MOLECULE_ROOT = Path("ansible/roles/gitea-runner/molecule")
DEFAULT_ROLES_ROOT = Path("ansible/roles")
def _default_molecule_root() -> Path:
"""Auto-discover the single molecule directory under ansible/roles/.
If exactly one role has a molecule/ subdirectory, return it.
Otherwise, fall back to the first role with a molecule/ directory.
"""
roles_root = DEFAULT_ROLES_ROOT
if not roles_root.is_dir():
return roles_root / "gitea_runner" / "molecule" # sensible default for error message
mol_dirs = sorted(d / "molecule" for d in roles_root.iterdir() if (d / "molecule").is_dir())
if mol_dirs:
return mol_dirs[0]
return roles_root / "molecule" # will produce a clear "not found" error
@dataclass(frozen=True)
class TestPair:
"""A (scenario, platform) combination to test."""
@@ -83,7 +97,7 @@ class MultiRoleTestPair:
def discover_scenarios(root: Path | None = None) -> list[str]:
"""Return sorted list of molecule scenario directory names."""
if root is None:
root = MOLECULE_ROOT
root = _default_molecule_root()
if not root.is_dir():
raise click.ClickException(_("Molecule directory not found: {path}", path=str(root)))
scenarios = [d.name for d in root.iterdir() if d.is_dir() and not d.name.startswith("_") and d.name != "common"]
@@ -318,7 +332,7 @@ def _write_github_env(key: str, value: str) -> None:
"--molecule-root",
type=click.Path(exists=True, file_okay=False, path_type=Path),
default=None,
help="Custom molecule directory (single-role mode). Default: ansible/roles/gitea-runner/molecule.",
help="Custom molecule directory (single-role mode). Default: auto-discovered under ansible/roles/*/molecule.",
)
@click.option(
"--roles-root",
+18 -4
View File
@@ -21,7 +21,20 @@ import click
from devx.molecule.platforms import PLATFORMS
ROLE_DIR = Path("ansible/roles/gitea-runner")
DEFAULT_ROLES_ROOT = Path("ansible/roles")
def _default_role_dir() -> Path:
"""Auto-discover the single role directory with molecule scenarios."""
roles_root = DEFAULT_ROLES_ROOT
if not roles_root.is_dir():
return roles_root / "gitea_runner" # sensible default for error message
role_dirs = sorted(d for d in roles_root.iterdir() if (d / "molecule").is_dir())
if role_dirs:
return role_dirs[0]
return roles_root / "role" # will produce a clear error
SCENARIOS = ["default", "multi-instance", "lifecycle", "template-content", "deregister", "update"]
@@ -72,15 +85,16 @@ def main(bin_dir: str) -> None:
if not Path(molecule_bin).exists():
raise click.ClickException(f"molecule not found at {molecule_bin}. Run 'make setup' first.")
if not ROLE_DIR.exists():
raise click.ClickException(f"Role directory not found: {ROLE_DIR}")
role_dir = _default_role_dir()
if not role_dir.exists():
raise click.ClickException(f"Role directory not found: {role_dir}")
base_env = dict(os.environ)
base_env["ANSIBLE_ALLOW_BROKEN_CONDITIONALS"] = "true"
base_env["ANSIBLE_INJECT_INVOCATION"] = "1"
for platform in PLATFORMS:
rc = _run_platform(molecule_bin, platform, ROLE_DIR, SCENARIOS, base_env)
rc = _run_platform(molecule_bin, platform, role_dir, SCENARIOS, base_env)
if rc != 0:
click.echo(f"FAILED on platform {platform['name']}", err=True)
sys.exit(rc)
+8 -2
View File
@@ -145,13 +145,19 @@ def resolve_role_dir(role: str, roles_root: Path | None, repo_root: Path) -> Pat
"""Resolve the working directory for a molecule pair.
For multi-role pairs (role non-empty), uses ``roles_root/role``.
For single-role pairs, uses ``repo_root/ansible/roles/gitea-runner``.
For single-role pairs, auto-discovers the first role with a molecule/
subdirectory under ``repo_root/ansible/roles/``.
"""
if role:
if roles_root is None:
roles_root = repo_root / "ansible" / "roles"
return roles_root / role
return repo_root / "ansible" / "roles" / "gitea-runner"
roles_dir = repo_root / "ansible" / "roles"
if roles_dir.is_dir():
role_dirs = sorted(d for d in roles_dir.iterdir() if (d / "molecule").is_dir())
if role_dirs:
return role_dirs[0]
return roles_dir / "role" # will produce a clear "not found" error
@click.command()
+26 -9
View File
@@ -9,10 +9,10 @@ from click.testing import CliRunner
from devx.molecule.distribute_molecule import (
DEFAULT_ROLES_ROOT,
MOLECULE_ROOT,
PLATFORMS,
MultiRoleTestPair,
TestPair,
_default_molecule_root,
_load_molecule_weights,
_lpt_distribute,
_scenario_weight,
@@ -43,8 +43,25 @@ class TestDiscoverScenarios:
discover_scenarios(tmp_path / "nonexistent")
assert "not found" in str(exc.value)
def test_default_root_constant(self) -> None:
assert Path("ansible/roles/gitea-runner/molecule") == MOLECULE_ROOT
def test_default_root_auto_discovery(self, tmp_path: Path) -> None:
"""_default_molecule_root auto-discovers first role with molecule/ dir."""
# When no roles exist, returns a fallback path
with patch("devx.molecule.distribute_molecule.DEFAULT_ROLES_ROOT", tmp_path / "roles"):
result = _default_molecule_root()
assert "molecule" in str(result)
# When a role has molecule/, it's discovered
(tmp_path / "roles" / "my_role" / "molecule").mkdir(parents=True)
with patch("devx.molecule.distribute_molecule.DEFAULT_ROLES_ROOT", tmp_path / "roles"):
result = _default_molecule_root()
assert result == tmp_path / "roles" / "my_role" / "molecule"
def test_default_root_no_molecule_dirs(self, tmp_path: Path) -> None:
"""When roles exist but none have molecule/, returns fallback path."""
(tmp_path / "roles" / "role_without_molecule").mkdir(parents=True)
with patch("devx.molecule.distribute_molecule.DEFAULT_ROLES_ROOT", tmp_path / "roles"):
result = _default_molecule_root()
assert result == tmp_path / "roles" / "molecule"
class TestPairEncoding:
@@ -150,7 +167,7 @@ class TestCli:
root = tmp_path / "molecule"
(root / "alpha").mkdir(parents=True)
(root / "beta").mkdir(parents=True)
with patch("devx.molecule.distribute_molecule.MOLECULE_ROOT", root):
with patch("devx.molecule.distribute_molecule._default_molecule_root", return_value=root):
runner = CliRunner()
result = runner.invoke(cli, ["--list"])
assert result.exit_code == 0
@@ -176,7 +193,7 @@ class TestCli:
root = tmp_path / "molecule"
for s in ["a", "b", "c"]:
(root / s).mkdir(parents=True)
with patch("devx.molecule.distribute_molecule.MOLECULE_ROOT", root):
with patch("devx.molecule.distribute_molecule._default_molecule_root", return_value=root):
runner = CliRunner()
result = runner.invoke(cli, ["--max-runners", "3"])
assert result.exit_code == 0
@@ -191,7 +208,7 @@ class TestCli:
root = tmp_path / "molecule"
(root / "alpha").mkdir(parents=True)
with patch("devx.molecule.distribute_molecule.MOLECULE_ROOT", root):
with patch("devx.molecule.distribute_molecule._default_molecule_root", return_value=root):
runner = CliRunner()
# 1-based index: "1" maps to internal 0
result = runner.invoke(cli, ["--runner-index", "1", "--max-runners", "3"])
@@ -209,7 +226,7 @@ class TestGithubEnv:
scenario = root / "alpha"
scenario.mkdir(parents=True)
(scenario / "molecule.yml").write_text("name: alpha\n")
with patch("devx.molecule.distribute_molecule.MOLECULE_ROOT", root):
with patch("devx.molecule.distribute_molecule._default_molecule_root", return_value=root):
runner = CliRunner()
result = runner.invoke(cli, ["--runner-index", "1", "--max-runners", "3", "--github-env"])
assert result.exit_code == 0
@@ -224,7 +241,7 @@ class TestGithubEnv:
scenario = root / "alpha"
scenario.mkdir(parents=True)
(scenario / "molecule.yml").write_text("name: alpha\n")
with patch("devx.molecule.distribute_molecule.MOLECULE_ROOT", root):
with patch("devx.molecule.distribute_molecule._default_molecule_root", return_value=root):
runner = CliRunner()
result = runner.invoke(
cli, ["--runner-index", "5", "--max-runners", "3", "--github-env", "--skip-if-excess"]
@@ -240,7 +257,7 @@ class TestGithubEnv:
scenario = root / "alpha"
scenario.mkdir(parents=True)
(scenario / "molecule.yml").write_text("name: alpha\n")
with patch("devx.molecule.distribute_molecule.MOLECULE_ROOT", root):
with patch("devx.molecule.distribute_molecule._default_molecule_root", return_value=root):
runner = CliRunner()
result = runner.invoke(cli, ["--runner-index", "1", "--max-runners", "3", "--github-env"])
assert result.exit_code != 0
+14 -2
View File
@@ -104,12 +104,24 @@ class TestMain:
assert result.exit_code != 0
assert "Role directory not found" in result.output
def test_role_dir_no_molecule(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Roles dir exists but no role has molecule/ — should error."""
monkeypatch.chdir(tmp_path)
bin_dir = tmp_path / ".venv" / "bin"
bin_dir.mkdir(parents=True)
(bin_dir / "molecule").touch()
(tmp_path / "ansible" / "roles" / "role_without_molecule").mkdir(parents=True)
runner = CliRunner()
result = runner.invoke(molecule_all.main, ["--bin", str(bin_dir)])
assert result.exit_code != 0
assert "Role directory not found" in result.output
def test_all_pass(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.chdir(tmp_path)
bin_dir = tmp_path / ".venv" / "bin"
bin_dir.mkdir(parents=True)
(bin_dir / "molecule").touch()
(tmp_path / "ansible" / "roles" / "gitea-runner").mkdir(parents=True)
(tmp_path / "ansible" / "roles" / "gitea-runner" / "molecule").mkdir(parents=True)
runner = CliRunner()
with patch("devx.molecule.molecule_all._run_platform", return_value=0):
@@ -122,7 +134,7 @@ class TestMain:
bin_dir = tmp_path / ".venv" / "bin"
bin_dir.mkdir(parents=True)
(bin_dir / "molecule").touch()
(tmp_path / "ansible" / "roles" / "gitea-runner").mkdir(parents=True)
(tmp_path / "ansible" / "roles" / "gitea-runner" / "molecule").mkdir(parents=True)
runner = CliRunner()
with patch("devx.molecule.molecule_all._run_platform", return_value=1):
+10 -2
View File
@@ -524,9 +524,17 @@ class TestResolveRoleDir:
result = resolve_role_dir("docker-base", None, tmp_path)
assert result == tmp_path / "ansible" / "roles" / "docker-base"
def test_single_role_uses_default(self, tmp_path: Path) -> None:
def test_single_role_auto_discovers(self, tmp_path: Path) -> None:
"""Single-role mode auto-discovers first role with molecule/ dir."""
roles_dir = tmp_path / "ansible" / "roles"
(roles_dir / "my_role" / "molecule").mkdir(parents=True)
result = resolve_role_dir("", None, tmp_path)
assert result == tmp_path / "ansible" / "roles" / "gitea-runner"
assert result == roles_dir / "my_role"
def test_single_role_no_roles_returns_fallback(self, tmp_path: Path) -> None:
"""When no roles exist, returns a fallback path (will error at runtime)."""
result = resolve_role_dir("", None, tmp_path)
assert "roles" in str(result)
class TestCliMultiRole: