From 845ab6c075aea34b664695e7a39185f993070ecc Mon Sep 17 00:00:00 2001 From: Sola Babatunde Date: Mon, 16 Mar 2026 15:38:27 +0100 Subject: [PATCH 1/2] Add delete branch after merge --- backend/infrahub/core/branch/tasks.py | 8 + backend/infrahub/proposed_change/tasks.py | 17 ++ .../branch/test_branch_delete_after_merge.py | 145 ++++++++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 backend/tests/functional/branch/test_branch_delete_after_merge.py diff --git a/backend/infrahub/core/branch/tasks.py b/backend/infrahub/core/branch/tasks.py index 939644f3f9..e90298dc6e 100644 --- a/backend/infrahub/core/branch/tasks.py +++ b/backend/infrahub/core/branch/tasks.py @@ -47,6 +47,7 @@ from infrahub.workers.dependencies import get_component, get_database, get_event_service, get_workflow from infrahub.workflows.catalogue import ( BRANCH_CANCEL_PROPOSED_CHANGES, + BRANCH_DELETE, BRANCH_MERGE_POST_PROCESS, DIFF_REFRESH_ALL, DIFF_UPDATE, @@ -371,6 +372,13 @@ async def merge_branch(branch: str, context: InfrahubContext, proposed_change_id parameters={"branch_name": obj.name}, ) + if config.SETTINGS.main.delete_branch_after_merge and not obj.is_default and proposed_change_id is None: + await get_workflow().submit_workflow( + workflow=BRANCH_DELETE, + context=context, + parameters={"branch": obj.name}, + ) + # ------------------------------------------------------------- # Generate an event to indicate that a branch has been merged # NOTE: we still need to convert this event and potentially pull diff --git a/backend/infrahub/proposed_change/tasks.py b/backend/infrahub/proposed_change/tasks.py index 3a71f54782..7b1225201f 100644 --- a/backend/infrahub/proposed_change/tasks.py +++ b/backend/infrahub/proposed_change/tasks.py @@ -93,6 +93,7 @@ from infrahub.validators.tasks import start_validator from infrahub.workers.dependencies import get_cache, get_client, get_database, get_event_service, get_workflow from infrahub.workflows.catalogue import ( + BRANCH_DELETE, GIT_REPOSITORIES_CHECK_ARTIFACT_CREATE, GIT_REPOSITORY_INTERNAL_CHECKS_TRIGGER, GIT_REPOSITORY_USER_CHECKS_TRIGGER, @@ -245,6 +246,22 @@ async def merge_proposed_change( user_id=context.account.account_id, ) + if config.SETTINGS.main.delete_branch_after_merge and not source_branch.is_default: + open_pcs = await NodeManager.query( + db=db, + schema=InfrahubKind.PROPOSEDCHANGE, + filters={ + "source_branch__value": source_branch.name, + "state__value": ProposedChangeState.OPEN.value, + }, + ) + if not open_pcs: + await get_workflow().submit_workflow( + workflow=BRANCH_DELETE, + context=context, + parameters={"branch": source_branch.name}, + ) + current_user = await NodeManager.get_one_by_id_or_default_filter( id=context.account.account_id, kind=InfrahubKind.GENERICACCOUNT, db=db ) diff --git a/backend/tests/functional/branch/test_branch_delete_after_merge.py b/backend/tests/functional/branch/test_branch_delete_after_merge.py new file mode 100644 index 0000000000..3052975660 --- /dev/null +++ b/backend/tests/functional/branch/test_branch_delete_after_merge.py @@ -0,0 +1,145 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import ANY, AsyncMock, patch + +import pytest +from infrahub_sdk.graphql import Mutation + +from infrahub import config +from infrahub.core import registry +from infrahub.core.constants import InfrahubKind +from infrahub.services.adapters.workflow.local import WorkflowLocalExecution +from infrahub.workflows.catalogue import BRANCH_DELETE +from tests.helpers.schema import CAR_SCHEMA, load_schema +from tests.helpers.test_app import TestInfrahubApp + +if TYPE_CHECKING: + from pathlib import Path + + from infrahub_sdk import InfrahubClient + + from infrahub.database import InfrahubDatabase + from tests.adapters.message_bus import BusSimulator + + +class TestAutoDeleteBranchAfterMerge(TestInfrahubApp): + @pytest.fixture(scope="class") + async def initial_dataset( + self, + db: InfrahubDatabase, + initialize_registry: None, + git_repos_source_dir_module_scope: Path, + client: InfrahubClient, + bus_simulator: BusSimulator, + prefect_test_fixture: None, + ) -> None: + await load_schema(db, schema=CAR_SCHEMA) + + async def test_branch_auto_deleted_after_standard_merge_when_config_enabled( + self, initial_dataset: None, client: InfrahubClient, db: InfrahubDatabase + ) -> None: + config.SETTINGS.main.delete_branch_after_merge = True + branch = await client.branch.create(branch_name="auto_delete_standard_enabled") + + with patch.object(WorkflowLocalExecution, "submit_workflow", new_callable=AsyncMock) as mock_submit: + query = Mutation( + mutation="BranchMerge", + input_data={"data": {"name": branch.name}}, + query={"ok": None, "task": {"id": None}, "object": {"id": None}}, + ) + await client.execute_graphql(query=query.render()) + + mock_submit.assert_any_call( + workflow=BRANCH_DELETE, + context=ANY, + parameters={"branch": branch.name}, + ) + + async def test_branch_not_deleted_after_standard_merge_when_config_disabled( + self, initial_dataset: None, client: InfrahubClient, db: InfrahubDatabase + ) -> None: + config.SETTINGS.main.delete_branch_after_merge = False + branch = await client.branch.create(branch_name="auto_delete_standard_disabled") + + with patch.object(WorkflowLocalExecution, "submit_workflow", new_callable=AsyncMock) as mock_submit: + query = Mutation( + mutation="BranchMerge", + input_data={"data": {"name": branch.name}}, + query={"ok": None, "task": {"id": None}, "object": {"id": None}}, + ) + await client.execute_graphql(query=query.render()) + + delete_calls = [c for c in mock_submit.call_args_list if c.kwargs.get("workflow") == BRANCH_DELETE] + assert not delete_calls + + async def test_branch_auto_deleted_after_proposed_change_merge( + self, initial_dataset: None, client: InfrahubClient, db: InfrahubDatabase + ) -> None: + config.SETTINGS.main.delete_branch_after_merge = True + branch = await client.branch.create(branch_name="auto_delete_pc_enabled") + + pc = await client.create( + kind=InfrahubKind.PROPOSEDCHANGE, + data={ + "name": {"value": "test-pc-auto-delete"}, + "source_branch": {"value": branch.name}, + "destination_branch": {"value": registry.default_branch}, + "is_draft": {"value": False}, + }, + ) + await pc.save() + + with patch.object(WorkflowLocalExecution, "submit_workflow", new_callable=AsyncMock) as mock_submit: + update_query = Mutation( + mutation="CoreProposedChangeUpdate", + input_data={"data": {"id": pc.id, "state": {"value": "merged"}}}, + query={"ok": None, "object": {"state": {"value": None}}}, + ) + await client.execute_graphql(query=update_query.render()) + + mock_submit.assert_any_call( + workflow=BRANCH_DELETE, + context=ANY, + parameters={"branch": branch.name}, + ) + + @pytest.mark.skip("Multiple proposed changes are not allowed for the same branch") + async def test_branch_not_deleted_when_other_open_proposed_changes_exist( + self, initial_dataset: None, client: InfrahubClient, db: InfrahubDatabase + ) -> None: + config.SETTINGS.main.delete_branch_after_merge = True + branch = await client.branch.create(branch_name="auto_delete_pc_other_open") + + pc1 = await client.create( + kind=InfrahubKind.PROPOSEDCHANGE, + data={ + "name": {"value": "pc-open-1"}, + "source_branch": {"value": branch.name}, + "destination_branch": {"value": registry.default_branch}, + "is_draft": {"value": False}, + }, + ) + await pc1.save() + + pc2 = await client.create( + kind=InfrahubKind.PROPOSEDCHANGE, + data={ + "name": {"value": "pc-open-2"}, + "source_branch": {"value": branch.name}, + "destination_branch": {"value": "pc-open-1"}, + "is_draft": {"value": False}, + }, + ) + await pc2.save() + + with patch.object(WorkflowLocalExecution, "submit_workflow", new_callable=AsyncMock) as mock_submit: + update_query = Mutation( + mutation="CoreProposedChangeUpdate", + input_data={"data": {"id": pc1.id, "state": {"value": "merged"}}}, + query={"ok": None, "object": {"state": {"value": None}}}, + ) + await client.execute_graphql(query=update_query.render()) + + delete_calls = [c for c in mock_submit.call_args_list if c.kwargs.get("workflow") == BRANCH_DELETE] + assert not delete_calls From 83702fa9540af15cae734ddac26a4ac864e3ecbd Mon Sep 17 00:00:00 2001 From: Sola Babatunde Date: Tue, 17 Mar 2026 12:45:21 +0100 Subject: [PATCH 2/2] update implementation plan and phases --- dev/wip/ifc-2336/implementation-plan.md | 2 +- dev/wip/ifc-2336/phase-2.md | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dev/wip/ifc-2336/implementation-plan.md b/dev/wip/ifc-2336/implementation-plan.md index 888ca2a221..1cae05f68b 100644 --- a/dev/wip/ifc-2336/implementation-plan.md +++ b/dev/wip/ifc-2336/implementation-plan.md @@ -15,7 +15,7 @@ Add optional automatic branch deletion after merge. Both Infrahub branch deletio | Phase | Description | Priority | Status | Tests | |-------|-----------------------------------------|----------|-------------|----------------------| | 1 | Configuration settings | P1 | ✅ Done | 3 unit tests | -| 2 | Auto-delete Infrahub branch after merge | P1 | ⬜ Todo | 4 functional tests | +| 2 | Auto-delete Infrahub branch after merge | P1 | ✅ Done | 4 functional tests | | 3 | Git branch deletion workflow | P2 | ⬜ Todo | 3 unit tests | | 4 | Manual delete with Git option | P3 | ⬜ Todo | 2 unit tests | diff --git a/dev/wip/ifc-2336/phase-2.md b/dev/wip/ifc-2336/phase-2.md index 76f206578d..5a3d4c1deb 100644 --- a/dev/wip/ifc-2336/phase-2.md +++ b/dev/wip/ifc-2336/phase-2.md @@ -1,6 +1,6 @@ # Phase 2: Auto-delete Infrahub Branch After Merge -**Status:** ⬜ Todo +**Status:** ✅ Done **Priority:** P1 **Requirements:** FR-003, FR-004, FR-006, FR-012, FR-014 **Depends on:** Phase 1 @@ -15,11 +15,11 @@ After a successful merge (standard or proposed change), if `delete_branch_after_ ## Checklist -- [ ] Update `merge_branch()` to submit `BRANCH_DELETE` after successful merge -- [ ] Update `merge_proposed_change()` to submit `BRANCH_DELETE` after successful merge -- [ ] Guard against deleting the default branch (FR-014) -- [ ] Guard against deleting when other open proposed changes exist (edge case) -- [ ] Write functional tests +- [x] Update `merge_branch()` to submit `BRANCH_DELETE` after successful merge +- [x] Update `merge_proposed_change()` to submit `BRANCH_DELETE` after successful merge +- [x] Guard against deleting the default branch (FR-014) +- [x] Guard against deleting when other open proposed changes exist (edge case) +- [x] Write functional tests ---