diff --git a/.github/renovate.json b/.github/renovate.json index 928ce8b76..bc6803cb7 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -79,6 +79,24 @@ ], "automerge": true, "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": { diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 280275657..8e14fdb57 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,3 +38,9 @@ repos: language: script files: '(examples/vars\.yml|roles/custom/matrix_playbook_migration/defaults/main\.yml)' 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 diff --git a/bin/check-molecule-automerge-list.py b/bin/check-molecule-automerge-list.py new file mode 100755 index 000000000..4b1464262 --- /dev/null +++ b/bin/check-molecule-automerge-list.py @@ -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()) diff --git a/bin/check-molecule-automerge-list.py.license b/bin/check-molecule-automerge-list.py.license new file mode 100644 index 000000000..dbb307901 --- /dev/null +++ b/bin/check-molecule-automerge-list.py.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2026 Slavi Pantaleev + +SPDX-License-Identifier: AGPL-3.0-or-later