Skip to content

Commit 2f5eca1

Browse files
committed
Add wait_for_completion option to BranchRebase and BranchDelete mutations
1 parent 93c4695 commit 2f5eca1

File tree

7 files changed

+235
-16
lines changed

7 files changed

+235
-16
lines changed

.github/workflows/ci.yml

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,6 @@ jobs:
312312
run: "poetry install --no-interaction --no-ansi"
313313
- name: "Mypy Tests"
314314
run: "poetry run invoke backend.mypy"
315-
- name: "Pylint Tests"
316-
run: "poetry run invoke backend.pylint"
317315
- name: "Integration Tests"
318316
run: "poetry run invoke backend.test-integration"
319317
- name: "Coveralls : Integration Tests"
@@ -325,6 +323,54 @@ jobs:
325323
flag-name: backend-integration
326324
parallel: true
327325

326+
backend-tests-functional:
327+
if: |
328+
always() && !cancelled() &&
329+
!contains(needs.*.result, 'failure') &&
330+
!contains(needs.*.result, 'cancelled') &&
331+
needs.files-changed.outputs.backend == 'true'
332+
needs: ["files-changed", "yaml-lint", "python-lint"]
333+
runs-on:
334+
group: "huge-runners"
335+
timeout-minutes: 30
336+
env:
337+
INFRAHUB_DB_TYPE: neo4j
338+
steps:
339+
- name: "Check out repository code"
340+
uses: "actions/checkout@v4"
341+
with:
342+
submodules: true
343+
- name: Set up Python
344+
uses: actions/setup-python@v5
345+
with:
346+
python-version: '3.12'
347+
- name: "Setup git credentials"
348+
run: "git config --global user.name 'Infrahub' && \
349+
git config --global user.email '[email protected]' && \
350+
git config --global --add safe.directory '*' && \
351+
git config --global credential.usehttppath true && \
352+
git config --global credential.helper /usr/local/bin/infrahub-git-credential"
353+
- name: "Setup Python environment"
354+
run: |
355+
poetry config virtualenvs.create true --local
356+
poetry env use 3.12
357+
- name: "Install dependencies"
358+
run: "poetry install --no-interaction --no-ansi"
359+
- name: "Mypy Tests"
360+
run: "poetry run invoke backend.mypy"
361+
- name: "Pylint Tests"
362+
run: "poetry run invoke backend.pylint"
363+
- name: "Functional Tests"
364+
run: "poetry run invoke backend.test-functional"
365+
- name: "Coveralls : Functional Tests"
366+
uses: coverallsapp/github-action@v2
367+
continue-on-error: true
368+
env:
369+
COVERALLS_SERVICE_NUMBER: ${{ github.sha }}
370+
with:
371+
flag-name: backend-functional
372+
parallel: true
373+
328374
backend-tests-memgraph:
329375
if: |
330376
always() && !cancelled() &&

backend/infrahub/graphql/initialization.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ def active_account_session(self) -> AccountSession:
4949
return self.account_session
5050
raise InitializationError("GraphQLContext doesn't contain an account_session")
5151

52+
@property
53+
def active_service(self) -> InfrahubServices:
54+
if self.service:
55+
return self.service
56+
raise InitializationError("GraphQLContext doesn't contain a service")
57+
5258

5359
def prepare_graphql_params(
5460
db: InfrahubDatabase,

backend/infrahub/graphql/mutations/branch.py

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import TYPE_CHECKING, Any
44

55
import pydantic
6-
from graphene import Boolean, Field, InputField, InputObjectType, List, Mutation, String
6+
from graphene import Boolean, Field, InputField, InputObjectType, List, Mutation, ObjectType, String
77
from infrahub_sdk.utils import extract_fields, extract_fields_first_node
88
from opentelemetry import trace
99
from typing_extensions import Self
@@ -41,6 +41,10 @@
4141
log = get_logger()
4242

4343

44+
class TaskInfo(ObjectType):
45+
id = Field(String)
46+
47+
4448
class BranchCreateInput(InputObjectType):
4549
id = String(required=False)
4650
name = String(required=True)
@@ -129,18 +133,29 @@ class BranchUpdateInput(InputObjectType):
129133
class BranchDelete(Mutation):
130134
class Arguments:
131135
data = BranchNameInput(required=True)
136+
wait_until_completion = Boolean(required=False)
132137

133138
ok = Boolean()
139+
task = Field(TaskInfo, required=False)
134140

135141
@classmethod
136142
@retry_db_transaction(name="branch_delete")
137-
async def mutate(cls, root: dict, info: GraphQLResolveInfo, data: BranchNameInput) -> Self:
143+
async def mutate(
144+
cls, root: dict, info: GraphQLResolveInfo, data: BranchNameInput, wait_until_completion: bool = True
145+
) -> Self:
138146
context: GraphqlContext = info.context
139-
140147
obj = await Branch.get_by_name(db=context.db, name=str(data.name))
141-
assert context.service
142-
await context.service.workflow.execute_workflow(workflow=BRANCH_DELETE, parameters={"branch": obj.name})
143-
return cls(ok=True)
148+
149+
if wait_until_completion:
150+
await context.active_service.workflow.execute_workflow(
151+
workflow=BRANCH_DELETE, parameters={"branch": obj.name}
152+
)
153+
return cls(ok=True)
154+
155+
workflow = await context.active_service.workflow.submit_workflow(
156+
workflow=BRANCH_DELETE, parameters={"branch": obj.name}
157+
)
158+
return cls(ok=True, task={"id": str(workflow.id)})
144159

145160

146161
class BranchUpdate(Mutation):
@@ -170,27 +185,38 @@ async def mutate(cls, root: dict, info: GraphQLResolveInfo, data: BranchNameInpu
170185
class BranchRebase(Mutation):
171186
class Arguments:
172187
data = BranchNameInput(required=True)
188+
wait_until_completion = Boolean(required=False)
173189

174190
ok = Boolean()
175191
object = Field(BranchType)
192+
task = Field(TaskInfo, required=False)
176193

177194
@classmethod
178-
async def mutate(cls, root: dict, info: GraphQLResolveInfo, data: BranchNameInput) -> Self:
195+
async def mutate(
196+
cls, root: dict, info: GraphQLResolveInfo, data: BranchNameInput, wait_until_completion: bool = True
197+
) -> Self:
179198
context: GraphqlContext = info.context
180199

181-
if not context.service:
182-
raise ValueError("Service must be provided to rebase a branch.")
183-
184200
obj = await Branch.get_by_name(db=context.db, name=str(data.name))
201+
task: dict | None = None
185202

186-
await context.service.workflow.execute_workflow(workflow=BRANCH_REBASE, parameters={"branch": obj.name})
203+
if wait_until_completion:
204+
await context.active_service.workflow.execute_workflow(
205+
workflow=BRANCH_REBASE, parameters={"branch": obj.name}
206+
)
207+
208+
# Pull the latest information about the branch from the database directly
209+
obj = await Branch.get_by_name(db=context.db, name=str(data.name))
210+
else:
211+
workflow = await context.active_service.workflow.submit_workflow(
212+
workflow=BRANCH_REBASE, parameters={"branch": obj.name}
213+
)
214+
task = {"id": workflow.id}
187215

188-
# Pull the latest information about the branch from the database directly
189-
obj = await Branch.get_by_name(db=context.db, name=str(data.name))
190216
fields = await extract_fields_first_node(info=info)
191217
ok = True
192218

193-
return cls(object=await obj.to_graphql(fields=fields.get("object", {})), ok=ok)
219+
return cls(object=await obj.to_graphql(fields=fields.get("object", {})), ok=ok, task=task)
194220

195221

196222
class BranchValidate(Mutation):

backend/tests/functional/__init__.py

Whitespace-only changes.

backend/tests/functional/branch/__init__.py

Whitespace-only changes.
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
import pytest
6+
from infrahub_sdk.exceptions import BranchNotFoundError
7+
from infrahub_sdk.graphql import Mutation
8+
9+
from infrahub.core.constants import InfrahubKind
10+
from infrahub.core.node import Node
11+
from infrahub.services.adapters.cache.redis import RedisCache
12+
from tests.constants import TestKind
13+
from tests.helpers.file_repo import FileRepo
14+
from tests.helpers.schema import CAR_SCHEMA, load_schema
15+
from tests.helpers.test_app import TestInfrahubApp
16+
17+
if TYPE_CHECKING:
18+
from pathlib import Path
19+
20+
from infrahub_sdk import InfrahubClient
21+
22+
from infrahub.database import InfrahubDatabase
23+
from tests.adapters.message_bus import BusSimulator
24+
25+
26+
class TestBranchMutations(TestInfrahubApp):
27+
@pytest.fixture(scope="class")
28+
async def initial_dataset(
29+
self,
30+
db: InfrahubDatabase,
31+
initialize_registry: None,
32+
git_repos_source_dir_module_scope: Path,
33+
client: InfrahubClient,
34+
bus_simulator: BusSimulator,
35+
prefect_test_fixture: None,
36+
) -> str:
37+
await load_schema(db, schema=CAR_SCHEMA)
38+
39+
john = await Node.init(schema=TestKind.PERSON, db=db)
40+
await john.new(db=db, name="John", height=175, description="The famous Joe Doe")
41+
await john.save(db=db)
42+
43+
koenigsegg = await Node.init(schema=TestKind.MANUFACTURER, db=db)
44+
await koenigsegg.new(db=db, name="Koenigsegg")
45+
await koenigsegg.save(db=db)
46+
47+
people = await Node.init(schema=InfrahubKind.STANDARDGROUP, db=db)
48+
await people.new(db=db, name="people", members=[john])
49+
await people.save(db=db)
50+
51+
jesko = await Node.init(schema=TestKind.CAR, db=db)
52+
await jesko.new(
53+
db=db,
54+
name="Jesko",
55+
color="Red",
56+
description="A limited production mid-engine sports car",
57+
owner=john,
58+
manufacturer=koenigsegg,
59+
)
60+
await jesko.save(db=db)
61+
62+
bus_simulator.service.cache = RedisCache()
63+
FileRepo(name="car-dealership", sources_directory=git_repos_source_dir_module_scope)
64+
client_repository = await client.create(
65+
kind=InfrahubKind.REPOSITORY,
66+
data={"name": "car-dealership", "location": f"{git_repos_source_dir_module_scope}/car-dealership"},
67+
)
68+
await client_repository.save()
69+
return client_repository.id
70+
71+
async def test_branch_delete_async(self, initial_dataset: str, client: InfrahubClient) -> None:
72+
branch = await client.branch.create(branch_name="branch_to_delete")
73+
74+
query = Mutation(
75+
mutation="BranchDelete",
76+
input_data={"data": {"name": branch.name}, "wait_until_completion": False},
77+
query={"ok": None, "task": {"id": None}},
78+
)
79+
result = await client.execute_graphql(query=query.render())
80+
assert result["BranchDelete"]["ok"] is True
81+
assert result["BranchDelete"]["task"]["id"]
82+
83+
with pytest.raises(BranchNotFoundError):
84+
await client.branch.get(branch_name=branch.name)
85+
86+
async def test_branch_delete(self, initial_dataset: str, client: InfrahubClient) -> None:
87+
branch = await client.branch.create(branch_name="branch_to_delete_sync")
88+
89+
query = Mutation(
90+
mutation="BranchDelete",
91+
input_data={"data": {"name": branch.name}},
92+
query={"ok": None, "task": {"id": None}},
93+
)
94+
result = await client.execute_graphql(query=query.render())
95+
assert result["BranchDelete"]["ok"] is True
96+
assert result["BranchDelete"]["task"] is None
97+
98+
with pytest.raises(BranchNotFoundError):
99+
await client.branch.get(branch_name=branch.name)
100+
101+
async def test_branch_rebase_async(self, initial_dataset: str, client: InfrahubClient) -> None:
102+
branch = await client.branch.create(branch_name="branch_to_rebase")
103+
104+
query = Mutation(
105+
mutation="BranchRebase",
106+
input_data={"data": {"name": branch.name}, "wait_until_completion": False},
107+
query={"ok": None, "task": {"id": None}, "object": {"id": None}},
108+
)
109+
result = await client.execute_graphql(query=query.render())
110+
assert result["BranchRebase"]["ok"] is True
111+
assert result["BranchRebase"]["object"]["id"] == branch.id
112+
assert result["BranchRebase"]["task"]["id"]
113+
114+
branch_after = await client.branch.get(branch_name=branch.name)
115+
assert branch.branched_from != branch_after.branched_from
116+
117+
async def test_branch_rebase(self, initial_dataset: str, client: InfrahubClient) -> None:
118+
branch = await client.branch.create(branch_name="branch_to_rebase_sync")
119+
120+
query = Mutation(
121+
mutation="BranchRebase",
122+
input_data={"data": {"name": branch.name}},
123+
query={"ok": None, "task": {"id": None}, "object": {"id": None}},
124+
)
125+
result = await client.execute_graphql(query=query.render())
126+
assert result["BranchRebase"]["ok"] is True
127+
assert result["BranchRebase"]["object"]["id"] == branch.id
128+
assert result["BranchRebase"]["task"] is None
129+
130+
branch_after = await client.branch.get(branch_name=branch.name)
131+
assert branch.branched_from != branch_after.branched_from

tasks/backend.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,16 @@ def test_integration(context: Context, database: str = INFRAHUB_DATABASE) -> Opt
138138
return execute_command(context=context, command=f"{exec_cmd}")
139139

140140

141+
@task(optional=["database"])
142+
def test_functional(context: Context, database: str = INFRAHUB_DATABASE) -> Optional[Result]:
143+
with context.cd(ESCAPED_REPO_PATH):
144+
exec_cmd = f"poetry run pytest -n {NBR_WORKERS} -v --cov=infrahub {MAIN_DIRECTORY}/tests/functional"
145+
if database == "neo4j":
146+
exec_cmd += " --neo4j"
147+
print(f"{exec_cmd=}")
148+
return execute_command(context=context, command=f"{exec_cmd}")
149+
150+
141151
@task
142152
def test_scale_env_start(
143153
context: Context, database: str = INFRAHUB_DATABASE, gunicorn_workers: int = 4

0 commit comments

Comments
 (0)