Public Access
122 lines
4.0 KiB
Python
122 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
|
# Implements: REQ-3
|
|
"""Detect changed Ansible roles and output fast molecule test commands.
|
|
|
|
Reuses ``devx.molecule.molecule_changed`` for role detection (which handles
|
|
playbook→role mapping and shared infrastructure paths).
|
|
|
|
Fast molecule = converge + verify only, single platform, no idempotence
|
|
check. Used in pre-merge CI to get quick feedback on Ansible changes
|
|
without running the full molecule suite (which runs nightly).
|
|
|
|
Usage:
|
|
python -m devx.ci.fast_molecule --base origin/master --head HEAD
|
|
|
|
Outputs the list of changed roles and the molecule commands to run.
|
|
In CI, pass ``--github-output`` to set ``fast-molecule-roles`` (space-
|
|
separated) and ``fast-molecule-needed`` (true/false) for downstream steps.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import click
|
|
from dotenv import load_dotenv
|
|
|
|
from devx.ci._shared import write_github_output
|
|
from devx.i18n import _
|
|
from devx.molecule.molecule_changed import detect_changed_roles, get_changed_files
|
|
|
|
load_dotenv()
|
|
|
|
|
|
def get_molecule_scenarios(role_name: str, roles_dir: str = "ansible/roles") -> list[str]:
|
|
"""Get list of molecule scenario names for a role."""
|
|
mol_dir = Path(roles_dir) / role_name / "molecule"
|
|
if not mol_dir.is_dir():
|
|
return []
|
|
scenarios = []
|
|
for p in mol_dir.iterdir():
|
|
if p.is_dir() and (p / "molecule.yml").exists():
|
|
scenarios.append(p.name)
|
|
return sorted(scenarios)
|
|
|
|
|
|
def build_molecule_commands(
|
|
roles: set[str],
|
|
roles_dir: str = "ansible/roles",
|
|
platform: str = "ubuntu-2604",
|
|
) -> list[str]:
|
|
"""Build molecule test commands for changed roles.
|
|
|
|
For each role, runs each scenario with converge + verify only
|
|
(skip create/destroy between scenarios, skip idempotence).
|
|
"""
|
|
commands: list[str] = []
|
|
for role in sorted(roles):
|
|
scenarios = get_molecule_scenarios(role, roles_dir)
|
|
if not scenarios:
|
|
continue
|
|
for scenario in scenarios:
|
|
cmd = f"molecule test -s {scenario} --destroy=never --platform-name={platform}"
|
|
commands.append(cmd)
|
|
return commands
|
|
|
|
|
|
@click.command()
|
|
@click.option("--base", default="origin/master", help=_("Base ref for diff"))
|
|
@click.option("--head", default="HEAD", help=_("Head ref for diff"))
|
|
@click.option("--roles-dir", default="ansible/roles", help=_("Directory containing Ansible roles"))
|
|
@click.option("--platform", default="ubuntu-2604", help=_("Single platform to test against"))
|
|
@click.option(
|
|
"--github-output",
|
|
"github_output",
|
|
is_flag=True,
|
|
default=False,
|
|
help=_("Write results to $GITHUB_OUTPUT"),
|
|
)
|
|
def cli(
|
|
base: str,
|
|
head: str,
|
|
roles_dir: str,
|
|
platform: str,
|
|
github_output: bool,
|
|
) -> None:
|
|
"""Detect changed roles and output fast molecule test commands."""
|
|
# Use molecule_changed for role detection (handles playbooks, shared infra)
|
|
files = get_changed_files(base)
|
|
if not files:
|
|
click.echo("[fast-molecule] No files changed.")
|
|
if github_output:
|
|
write_github_output("fast-molecule-needed", "false")
|
|
write_github_output("fast-molecule-roles", "")
|
|
return
|
|
|
|
roles = detect_changed_roles(files)
|
|
if not roles:
|
|
click.echo("[fast-molecule] No Ansible roles changed.")
|
|
if github_output:
|
|
write_github_output("fast-molecule-needed", "false")
|
|
write_github_output("fast-molecule-roles", "")
|
|
return
|
|
|
|
commands = build_molecule_commands(roles, roles_dir, platform)
|
|
|
|
if github_output:
|
|
write_github_output("fast-molecule-needed", "true" if commands else "false")
|
|
write_github_output("fast-molecule-roles", " ".join(sorted(roles)))
|
|
|
|
click.echo(_("[fast-molecule] Changed roles: {roles}", roles=", ".join(sorted(roles))))
|
|
if not commands:
|
|
click.echo("[fast-molecule] No molecule scenarios found for changed roles.")
|
|
return
|
|
|
|
click.echo(f"[fast-molecule] {len(commands)} scenario(s) to run:")
|
|
for cmd in commands:
|
|
click.echo(f" {cmd}")
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
cli()
|