A component bump touches that role's defaults/main.yml, which is on the Molecule workflow's path filter, so the change-detection job runs that role's scenario. The bump therefore only merges once the new version has actually been started on the configuration the role renders. That is a real gate, and a stronger one than a human reading a version number in a diff. Patch only. A minor carries behaviour changes that no static rule can judge - the MASH fleet tried a minor-automerge preset across 21 stateless roles and reverted all of them, because reading a given release's notes is what decides it, and that is a job for a human or an agent rather than a config file. Branch push rather than a PR, as with the other automerge rules here. The list has to stay in step with the roles that actually have a scenario, so bin/check-molecule-automerge-list.py enforces it from prek. The direction that matters is a role keeping automerge after losing its scenario: bumps would then merge with nothing exercising them. It reports the harmless direction too, since a role gaining a scenario without being listed is usually an oversight. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SEH3vxYSQ5SV4N5z61eyGTcreate-pull-request/i18n
| @@ -79,6 +79,24 @@ | |||||
| ], | ], | ||||
| "automerge": true, | "automerge": true, | ||||
| "automergeType": "branch" | "automergeType": "branch" | ||||
| }, | |||||
| { | |||||
| "description": "Automerge patch bumps of components whose role has a Molecule scenario (via branch push - no PR). A bump touches that role's defaults/main.yml, which makes the Molecule workflow run that role's scenario, so the update only merges once the component has been started on the configuration the role renders. Patch only: a minor carries behaviour changes no static rule can judge, and reading the release notes to decide is a job for a human or an agent. Keep this list in step with the roles that have a scenario - bin/check-molecule-automerge-list.py enforces that.", | |||||
| "matchFileNames": [ | |||||
| "roles/custom/matrix-alertmanager-receiver/defaults/main.yml", | |||||
| "roles/custom/matrix-bot-baibot/defaults/main.yml", | |||||
| "roles/custom/matrix-bot-matrix-reminder-bot/defaults/main.yml", | |||||
| "roles/custom/matrix-bridge-heisenbridge/defaults/main.yml", | |||||
| "roles/custom/matrix-bridge-hookshot/defaults/main.yml", | |||||
| "roles/custom/matrix-bridge-mautrix-discord/defaults/main.yml", | |||||
| "roles/custom/matrix-bridge-mautrix-meta-messenger/defaults/main.yml", | |||||
| "roles/custom/matrix-bridge-mautrix-whatsapp/defaults/main.yml" | |||||
| ], | |||||
| "matchUpdateTypes": [ | |||||
| "patch" | |||||
| ], | |||||
| "automerge": true, | |||||
| "automergeType": "branch" | |||||
| } | } | ||||
| ], | ], | ||||
| "pre-commit": { | "pre-commit": { | ||||
| @@ -38,3 +38,9 @@ repos: | |||||
| language: script | language: script | ||||
| files: '(examples/vars\.yml|roles/custom/matrix_playbook_migration/defaults/main\.yml)' | files: '(examples/vars\.yml|roles/custom/matrix_playbook_migration/defaults/main\.yml)' | ||||
| pass_filenames: false | pass_filenames: false | ||||
| - id: check-molecule-automerge-list | |||||
| name: Check the Molecule automerge list matches the roles that have a scenario | |||||
| entry: bin/check-molecule-automerge-list.py | |||||
| language: script | |||||
| files: '(\.github/renovate\.json|roles/custom/[^/]+/molecule/default/molecule\.yml)' | |||||
| pass_filenames: false | |||||
| @@ -0,0 +1,70 @@ | |||||
| #!/usr/bin/env python3 | |||||
| """Keeps the Molecule patch-automerge rule in step with the roles that have a scenario. | |||||
| .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. | |||||
| 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. | |||||
| """ | |||||
| import json | |||||
| import pathlib | |||||
| import sys | |||||
| REPO = pathlib.Path(__file__).resolve().parent.parent | |||||
| RENOVATE = REPO / ".github" / "renovate.json" | |||||
| MARKER = "bin/check-molecule-automerge-list.py" | |||||
| def main() -> int: | |||||
| with_scenario = { | |||||
| p.parts[-4] for p in (REPO / "roles" / "custom").glob("*/molecule/default/molecule.yml") | |||||
| } | |||||
| 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) | |||||
| 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 | |||||
| automerged_without_scenario = sorted(listed - with_scenario) | |||||
| scenario_without_automerge = sorted(with_scenario - listed) | |||||
| if automerged_without_scenario: | |||||
| print( | |||||
| "These roles automerge patch bumps but have no Molecule scenario, so nothing would\n" | |||||
| "exercise the bump before it merges. Remove them from the rule in\n" | |||||
| ".github/renovate.json, or give them a scenario:", | |||||
| file=sys.stderr, | |||||
| ) | |||||
| for role in automerged_without_scenario: | |||||
| print(f" {role}", file=sys.stderr) | |||||
| if scenario_without_automerge: | |||||
| print( | |||||
| "These roles have a Molecule scenario but are not in the automerge rule in\n" | |||||
| ".github/renovate.json, so their patch bumps still need a button press:", | |||||
| file=sys.stderr, | |||||
| ) | |||||
| for role in scenario_without_automerge: | |||||
| print(f" {role}", file=sys.stderr) | |||||
| return 1 if (automerged_without_scenario or scenario_without_automerge) else 0 | |||||
| if __name__ == "__main__": | |||||
| sys.exit(main()) | |||||
| @@ -0,0 +1,3 @@ | |||||
| SPDX-FileCopyrightText: 2026 Slavi Pantaleev | |||||
| SPDX-License-Identifier: AGPL-3.0-or-later | |||||