Automerge minor bumps for newly covered roles

This commit is contained in:
Slavi Pantaleev
2026-08-30 18:09:15 +03:00
parent 8cf346b9ec
commit b8145c2806
4 changed files with 93 additions and 33 deletions
+73 -24
View File
@@ -1,14 +1,12 @@
#!/usr/bin/env python3
"""Keeps the Molecule patch-automerge rule in step with the roles that have a scenario.
"""Keeps Molecule-backed automerge rules in step with the available scenarios.
.github/renovate.json automerges patch bumps for roles listed by file name, on the grounds that
the bump runs that role's Molecule scenario before merging. That reasoning only holds while the
role actually has one.
.github/renovate.json automerges patch bumps for every role with a scenario. A narrower list of
explicitly approved roles also automerges minor bumps. In both cases, the bump runs that role's
Molecule scenario before merging, so the reasoning only holds while the role actually has one.
The dangerous direction is a role keeping automerge after losing its scenario: bumps would then
merge with nothing exercising them. The harmless direction - a role gaining a scenario without
being added - only means a missed opportunity, but it is reported too, since it is usually an
oversight rather than a decision.
The patch list must exactly match the scenarios. The minor list must be a subset of it: omission is
an explicit policy choice, while an extra entry would merge a minor bump without the required gate.
"""
import json
@@ -17,7 +15,28 @@ import sys
REPO = pathlib.Path(__file__).resolve().parent.parent
RENOVATE = REPO / ".github" / "renovate.json"
MARKER = "bin/check-molecule-automerge-list.py"
PATCH_RULE_MARKER = "bin/check-molecule-automerge-list.py (patch rule)"
MINOR_RULE_MARKER = "bin/check-molecule-automerge-list.py (minor rule)"
def find_rule(config: dict, marker: str) -> dict | None:
rules = [r for r in config.get("packageRules", []) if marker in r.get("description", "")]
if len(rules) != 1:
print(f"Expected exactly one rule mentioning {marker}, found {len(rules)}.", file=sys.stderr)
return None
return rules[0]
def listed_roles(rule: dict, label: str) -> set[str] | None:
listed = set()
for name in rule.get("matchFileNames", []):
parts = pathlib.PurePosixPath(name).parts
if parts[:2] == ("roles", "custom") and parts[3:] == ("defaults", "main.yml"):
listed.add(parts[2])
else:
print(f"Unexpected entry in the Molecule {label} automerge rule: {name}", file=sys.stderr)
return None
return listed
def main() -> int:
@@ -26,23 +45,26 @@ def main() -> int:
}
config = json.loads(RENOVATE.read_text())
rules = [r for r in config.get("packageRules", []) if MARKER in r.get("description", "")]
if len(rules) != 1:
print(f"Expected exactly one rule mentioning {MARKER}, found {len(rules)}.", file=sys.stderr)
patch_rule = find_rule(config, PATCH_RULE_MARKER)
minor_rule = find_rule(config, MINOR_RULE_MARKER)
if patch_rule is None or minor_rule is None:
return 1
listed = set()
for name in rules[0].get("matchFileNames", []):
parts = pathlib.PurePosixPath(name).parts
if parts[:2] == ("roles", "custom") and parts[3:] == ("defaults", "main.yml"):
listed.add(parts[2])
else:
print(f"Unexpected entry in the automerge rule: {name}", file=sys.stderr)
return 1
patch_roles = listed_roles(patch_rule, "patch")
minor_roles = listed_roles(minor_rule, "minor")
if patch_roles is None or minor_roles is None:
return 1
automerged_without_scenario = sorted(listed - with_scenario)
scenario_without_automerge = sorted(with_scenario - listed)
errors = False
if set(patch_rule.get("matchUpdateTypes", [])) != {"patch"}:
print("The Molecule patch automerge rule must match only patch updates.", file=sys.stderr)
errors = True
if set(minor_rule.get("matchUpdateTypes", [])) != {"minor"}:
print("The Molecule minor automerge rule must match only minor updates.", file=sys.stderr)
errors = True
automerged_without_scenario = sorted(patch_roles - with_scenario)
scenario_without_automerge = sorted(with_scenario - patch_roles)
if automerged_without_scenario:
print(
@@ -63,7 +85,34 @@ def main() -> int:
for role in scenario_without_automerge:
print(f" {role}", file=sys.stderr)
return 1 if (automerged_without_scenario or scenario_without_automerge) else 0
minor_without_scenario = sorted(minor_roles - with_scenario)
minor_without_patch = sorted(minor_roles - patch_roles)
if minor_without_scenario:
print(
"These roles automerge minor bumps but have no Molecule scenario:",
file=sys.stderr,
)
for role in minor_without_scenario:
print(f" {role}", file=sys.stderr)
if minor_without_patch:
print(
"These roles automerge minor bumps but are missing from the patch rule:",
file=sys.stderr,
)
for role in minor_without_patch:
print(f" {role}", file=sys.stderr)
return 1 if any(
[
errors,
automerged_without_scenario,
scenario_without_automerge,
minor_without_scenario,
minor_without_patch,
]
) else 0
if __name__ == "__main__":