infrahub-server 1.4.13__py3-none-any.whl → 1.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. infrahub/actions/tasks.py +208 -16
  2. infrahub/api/artifact.py +3 -0
  3. infrahub/api/diff/diff.py +1 -1
  4. infrahub/api/internal.py +2 -0
  5. infrahub/api/query.py +2 -0
  6. infrahub/api/schema.py +27 -3
  7. infrahub/auth.py +5 -5
  8. infrahub/cli/__init__.py +2 -0
  9. infrahub/cli/db.py +160 -157
  10. infrahub/cli/dev.py +118 -0
  11. infrahub/cli/upgrade.py +56 -9
  12. infrahub/computed_attribute/tasks.py +19 -7
  13. infrahub/config.py +7 -2
  14. infrahub/core/attribute.py +35 -24
  15. infrahub/core/branch/enums.py +1 -1
  16. infrahub/core/branch/models.py +9 -5
  17. infrahub/core/branch/needs_rebase_status.py +11 -0
  18. infrahub/core/branch/tasks.py +72 -10
  19. infrahub/core/changelog/models.py +2 -10
  20. infrahub/core/constants/__init__.py +4 -0
  21. infrahub/core/constants/infrahubkind.py +1 -0
  22. infrahub/core/convert_object_type/object_conversion.py +201 -0
  23. infrahub/core/convert_object_type/repository_conversion.py +89 -0
  24. infrahub/core/convert_object_type/schema_mapping.py +27 -3
  25. infrahub/core/diff/model/path.py +4 -0
  26. infrahub/core/diff/payload_builder.py +1 -1
  27. infrahub/core/diff/query/artifact.py +1 -0
  28. infrahub/core/diff/query/field_summary.py +1 -0
  29. infrahub/core/graph/__init__.py +1 -1
  30. infrahub/core/initialization.py +7 -4
  31. infrahub/core/manager.py +3 -81
  32. infrahub/core/migrations/__init__.py +3 -0
  33. infrahub/core/migrations/exceptions.py +4 -0
  34. infrahub/core/migrations/graph/__init__.py +11 -10
  35. infrahub/core/migrations/graph/load_schema_branch.py +21 -0
  36. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +1 -1
  37. infrahub/core/migrations/graph/m037_index_attr_vals.py +11 -30
  38. infrahub/core/migrations/graph/m039_ipam_reconcile.py +9 -7
  39. infrahub/core/migrations/graph/m042_profile_attrs_in_db.py +147 -0
  40. infrahub/core/migrations/graph/m043_create_hfid_display_label_in_db.py +164 -0
  41. infrahub/core/migrations/graph/m044_backfill_hfid_display_label_in_db.py +864 -0
  42. infrahub/core/migrations/query/__init__.py +7 -8
  43. infrahub/core/migrations/query/attribute_add.py +8 -6
  44. infrahub/core/migrations/query/attribute_remove.py +134 -0
  45. infrahub/core/migrations/runner.py +54 -0
  46. infrahub/core/migrations/schema/attribute_kind_update.py +9 -3
  47. infrahub/core/migrations/schema/attribute_supports_profile.py +90 -0
  48. infrahub/core/migrations/schema/node_attribute_add.py +26 -5
  49. infrahub/core/migrations/schema/node_attribute_remove.py +13 -109
  50. infrahub/core/migrations/schema/node_kind_update.py +2 -1
  51. infrahub/core/migrations/schema/node_remove.py +2 -1
  52. infrahub/core/migrations/schema/placeholder_dummy.py +3 -2
  53. infrahub/core/migrations/shared.py +66 -19
  54. infrahub/core/models.py +2 -2
  55. infrahub/core/node/__init__.py +207 -54
  56. infrahub/core/node/create.py +53 -49
  57. infrahub/core/node/lock_utils.py +124 -0
  58. infrahub/core/node/node_property_attribute.py +230 -0
  59. infrahub/core/node/resource_manager/ip_address_pool.py +2 -1
  60. infrahub/core/node/resource_manager/ip_prefix_pool.py +2 -1
  61. infrahub/core/node/resource_manager/number_pool.py +2 -1
  62. infrahub/core/node/standard.py +1 -1
  63. infrahub/core/property.py +11 -0
  64. infrahub/core/protocols.py +8 -1
  65. infrahub/core/query/attribute.py +82 -15
  66. infrahub/core/query/ipam.py +16 -4
  67. infrahub/core/query/node.py +66 -188
  68. infrahub/core/query/relationship.py +44 -26
  69. infrahub/core/query/subquery.py +0 -8
  70. infrahub/core/relationship/model.py +69 -24
  71. infrahub/core/schema/__init__.py +56 -0
  72. infrahub/core/schema/attribute_schema.py +4 -2
  73. infrahub/core/schema/basenode_schema.py +42 -2
  74. infrahub/core/schema/definitions/core/__init__.py +2 -0
  75. infrahub/core/schema/definitions/core/check.py +1 -1
  76. infrahub/core/schema/definitions/core/generator.py +2 -0
  77. infrahub/core/schema/definitions/core/group.py +16 -2
  78. infrahub/core/schema/definitions/core/repository.py +7 -0
  79. infrahub/core/schema/definitions/core/transform.py +1 -1
  80. infrahub/core/schema/definitions/internal.py +12 -3
  81. infrahub/core/schema/generated/attribute_schema.py +2 -2
  82. infrahub/core/schema/generated/base_node_schema.py +6 -1
  83. infrahub/core/schema/manager.py +3 -0
  84. infrahub/core/schema/node_schema.py +1 -0
  85. infrahub/core/schema/relationship_schema.py +0 -1
  86. infrahub/core/schema/schema_branch.py +295 -10
  87. infrahub/core/schema/schema_branch_display.py +135 -0
  88. infrahub/core/schema/schema_branch_hfid.py +120 -0
  89. infrahub/core/validators/aggregated_checker.py +1 -1
  90. infrahub/database/graph.py +21 -0
  91. infrahub/display_labels/__init__.py +0 -0
  92. infrahub/display_labels/gather.py +48 -0
  93. infrahub/display_labels/models.py +240 -0
  94. infrahub/display_labels/tasks.py +192 -0
  95. infrahub/display_labels/triggers.py +22 -0
  96. infrahub/events/branch_action.py +27 -1
  97. infrahub/events/group_action.py +1 -1
  98. infrahub/events/node_action.py +1 -1
  99. infrahub/generators/constants.py +7 -0
  100. infrahub/generators/models.py +38 -12
  101. infrahub/generators/tasks.py +34 -16
  102. infrahub/git/base.py +38 -1
  103. infrahub/git/integrator.py +22 -14
  104. infrahub/graphql/api/dependencies.py +2 -4
  105. infrahub/graphql/api/endpoints.py +16 -6
  106. infrahub/graphql/app.py +2 -4
  107. infrahub/graphql/initialization.py +2 -3
  108. infrahub/graphql/manager.py +213 -137
  109. infrahub/graphql/middleware.py +12 -0
  110. infrahub/graphql/mutations/branch.py +16 -0
  111. infrahub/graphql/mutations/computed_attribute.py +110 -3
  112. infrahub/graphql/mutations/convert_object_type.py +44 -13
  113. infrahub/graphql/mutations/display_label.py +118 -0
  114. infrahub/graphql/mutations/generator.py +25 -7
  115. infrahub/graphql/mutations/hfid.py +125 -0
  116. infrahub/graphql/mutations/ipam.py +73 -41
  117. infrahub/graphql/mutations/main.py +61 -178
  118. infrahub/graphql/mutations/profile.py +195 -0
  119. infrahub/graphql/mutations/proposed_change.py +8 -1
  120. infrahub/graphql/mutations/relationship.py +2 -2
  121. infrahub/graphql/mutations/repository.py +22 -83
  122. infrahub/graphql/mutations/resource_manager.py +2 -2
  123. infrahub/graphql/mutations/webhook.py +1 -1
  124. infrahub/graphql/queries/resource_manager.py +1 -1
  125. infrahub/graphql/registry.py +173 -0
  126. infrahub/graphql/resolvers/resolver.py +2 -0
  127. infrahub/graphql/schema.py +8 -1
  128. infrahub/graphql/schema_sort.py +170 -0
  129. infrahub/graphql/types/branch.py +4 -1
  130. infrahub/graphql/types/enums.py +3 -0
  131. infrahub/groups/tasks.py +1 -1
  132. infrahub/hfid/__init__.py +0 -0
  133. infrahub/hfid/gather.py +48 -0
  134. infrahub/hfid/models.py +240 -0
  135. infrahub/hfid/tasks.py +191 -0
  136. infrahub/hfid/triggers.py +22 -0
  137. infrahub/lock.py +119 -42
  138. infrahub/locks/__init__.py +0 -0
  139. infrahub/locks/tasks.py +37 -0
  140. infrahub/patch/plan_writer.py +2 -2
  141. infrahub/permissions/constants.py +2 -0
  142. infrahub/profiles/__init__.py +0 -0
  143. infrahub/profiles/node_applier.py +101 -0
  144. infrahub/profiles/queries/__init__.py +0 -0
  145. infrahub/profiles/queries/get_profile_data.py +98 -0
  146. infrahub/profiles/tasks.py +63 -0
  147. infrahub/proposed_change/tasks.py +24 -5
  148. infrahub/repositories/__init__.py +0 -0
  149. infrahub/repositories/create_repository.py +113 -0
  150. infrahub/server.py +9 -1
  151. infrahub/services/__init__.py +8 -5
  152. infrahub/services/adapters/workflow/worker.py +5 -2
  153. infrahub/task_manager/event.py +5 -0
  154. infrahub/task_manager/models.py +7 -0
  155. infrahub/tasks/registry.py +6 -4
  156. infrahub/trigger/catalogue.py +4 -0
  157. infrahub/trigger/models.py +2 -0
  158. infrahub/trigger/setup.py +13 -4
  159. infrahub/trigger/tasks.py +6 -0
  160. infrahub/webhook/models.py +1 -1
  161. infrahub/workers/dependencies.py +3 -1
  162. infrahub/workers/infrahub_async.py +5 -1
  163. infrahub/workflows/catalogue.py +118 -3
  164. infrahub/workflows/initialization.py +21 -0
  165. infrahub/workflows/models.py +17 -2
  166. infrahub_sdk/branch.py +17 -8
  167. infrahub_sdk/checks.py +1 -1
  168. infrahub_sdk/client.py +376 -95
  169. infrahub_sdk/config.py +29 -2
  170. infrahub_sdk/convert_object_type.py +61 -0
  171. infrahub_sdk/ctl/branch.py +3 -0
  172. infrahub_sdk/ctl/check.py +2 -3
  173. infrahub_sdk/ctl/cli_commands.py +20 -12
  174. infrahub_sdk/ctl/config.py +8 -2
  175. infrahub_sdk/ctl/generator.py +6 -3
  176. infrahub_sdk/ctl/graphql.py +184 -0
  177. infrahub_sdk/ctl/repository.py +39 -1
  178. infrahub_sdk/ctl/schema.py +40 -10
  179. infrahub_sdk/ctl/task.py +110 -0
  180. infrahub_sdk/ctl/utils.py +4 -0
  181. infrahub_sdk/ctl/validate.py +5 -3
  182. infrahub_sdk/diff.py +4 -5
  183. infrahub_sdk/exceptions.py +2 -0
  184. infrahub_sdk/generator.py +7 -1
  185. infrahub_sdk/graphql/__init__.py +12 -0
  186. infrahub_sdk/graphql/constants.py +1 -0
  187. infrahub_sdk/graphql/plugin.py +85 -0
  188. infrahub_sdk/graphql/query.py +77 -0
  189. infrahub_sdk/{graphql.py → graphql/renderers.py} +88 -75
  190. infrahub_sdk/graphql/utils.py +40 -0
  191. infrahub_sdk/node/attribute.py +2 -0
  192. infrahub_sdk/node/node.py +28 -20
  193. infrahub_sdk/node/relationship.py +1 -3
  194. infrahub_sdk/playback.py +1 -2
  195. infrahub_sdk/protocols.py +54 -6
  196. infrahub_sdk/pytest_plugin/plugin.py +7 -4
  197. infrahub_sdk/pytest_plugin/utils.py +40 -0
  198. infrahub_sdk/repository.py +1 -2
  199. infrahub_sdk/schema/__init__.py +70 -4
  200. infrahub_sdk/schema/main.py +1 -0
  201. infrahub_sdk/schema/repository.py +8 -0
  202. infrahub_sdk/spec/models.py +7 -0
  203. infrahub_sdk/spec/object.py +54 -6
  204. infrahub_sdk/spec/processors/__init__.py +0 -0
  205. infrahub_sdk/spec/processors/data_processor.py +10 -0
  206. infrahub_sdk/spec/processors/factory.py +34 -0
  207. infrahub_sdk/spec/processors/range_expand_processor.py +56 -0
  208. infrahub_sdk/spec/range_expansion.py +118 -0
  209. infrahub_sdk/task/models.py +6 -4
  210. infrahub_sdk/timestamp.py +18 -6
  211. infrahub_sdk/transforms.py +1 -1
  212. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/METADATA +9 -10
  213. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/RECORD +221 -165
  214. infrahub_testcontainers/container.py +114 -2
  215. infrahub_testcontainers/docker-compose-cluster.test.yml +5 -0
  216. infrahub_testcontainers/docker-compose.test.yml +5 -0
  217. infrahub_testcontainers/models.py +2 -2
  218. infrahub_testcontainers/performance_test.py +4 -4
  219. infrahub/core/convert_object_type/conversion.py +0 -134
  220. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/LICENSE.txt +0 -0
  221. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/WHEEL +0 -0
  222. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/entry_points.txt +0 -0
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import hashlib
3
4
  from dataclasses import dataclass
4
5
  from typing import TYPE_CHECKING, Any, Iterable
5
6
 
@@ -17,9 +18,9 @@ from infrahub.core.schema import (
17
18
  RelationshipSchema,
18
19
  TemplateSchema,
19
20
  )
20
- from infrahub.core.timestamp import Timestamp
21
21
  from infrahub.graphql.mutations.attribute import BaseAttributeCreate, BaseAttributeUpdate
22
22
  from infrahub.graphql.mutations.graphql_query import InfrahubGraphQLQueryMutation
23
+ from infrahub.graphql.mutations.profile import InfrahubProfileMutation
23
24
  from infrahub.types import ATTRIBUTE_TYPES, InfrahubDataType, get_attribute_type
24
25
 
25
26
  from .directives import DIRECTIVES
@@ -40,6 +41,7 @@ from .mutations.resource_manager import (
40
41
  InfrahubNumberPoolMutation,
41
42
  )
42
43
  from .mutations.webhook import InfrahubWebhookMutation
44
+ from .registry import registry
43
45
  from .resolvers.ipam import ipam_paginated_list_resolver
44
46
  from .resolvers.resolver import (
45
47
  account_resolver,
@@ -69,7 +71,6 @@ from .types.event import EVENT_TYPES
69
71
  if TYPE_CHECKING:
70
72
  from graphql import GraphQLSchema
71
73
 
72
- from infrahub.core.branch import Branch
73
74
  from infrahub.core.schema.schema_branch import SchemaBranch
74
75
 
75
76
 
@@ -93,81 +94,34 @@ class GraphqlMutations:
93
94
  delete: type[InfrahubMutation]
94
95
 
95
96
 
96
- def get_attr_kind(node_schema: MainSchemaTypes, attr_schema: AttributeSchema) -> str:
97
- if not config.SETTINGS.experimental_features.graphql_enums or not attr_schema.enum:
98
- return attr_schema.kind
99
- return get_enum_attribute_type_name(node_schema=node_schema, attr_schema=attr_schema)
97
+ @dataclass
98
+ class InterfaceReference:
99
+ reference: type[graphene.Interface]
100
+ reference_hash: str
100
101
 
101
102
 
102
103
  @dataclass
103
- class BranchDetails:
104
- branch_name: str
105
- schema_changed_at: Timestamp
106
- schema_hash: str
107
- gql_manager: GraphQLSchemaManager
104
+ class InfrahubObjectReference:
105
+ reference: type[InfrahubObject]
106
+ reference_hash: str
108
107
 
109
108
 
110
- class GraphQLSchemaManager:
111
- _extra_types: dict[str, GraphQLTypes] = {}
112
- _branch_details_by_name: dict[str, BranchDetails] = {}
113
-
114
- @classmethod
115
- def clear_cache(cls) -> None:
116
- cls._branch_details_by_name = {}
117
-
118
- @classmethod
119
- def purge_inactive(cls, active_branches: list[str]) -> set[str]:
120
- """Return inactive branches that were purged"""
121
- inactive_branches: set[str] = set()
122
- for branch_name in list(cls._branch_details_by_name):
123
- if branch_name not in active_branches:
124
- inactive_branches.add(branch_name)
125
- del cls._branch_details_by_name[branch_name]
126
- return inactive_branches
127
-
128
- @classmethod
129
- def _cache_branch(
130
- cls, branch: Branch, schema_branch: SchemaBranch, schema_hash: str | None = None
131
- ) -> BranchDetails:
132
- if not schema_hash:
133
- if branch.schema_hash:
134
- schema_hash = branch.schema_hash.main
135
- else:
136
- schema_hash = schema_branch.get_hash()
137
- branch_details = BranchDetails(
138
- branch_name=branch.name,
139
- schema_changed_at=Timestamp(branch.schema_changed_at) if branch.schema_changed_at else Timestamp(),
140
- schema_hash=schema_hash,
141
- gql_manager=cls(schema=schema_branch),
142
- )
143
- cls._branch_details_by_name[branch.name] = branch_details
144
- return branch_details
145
-
146
- @classmethod
147
- def get_manager_for_branch(cls, branch: Branch, schema_branch: SchemaBranch) -> GraphQLSchemaManager:
148
- if branch.name not in cls._branch_details_by_name:
149
- branch_details = cls._cache_branch(branch=branch, schema_branch=schema_branch)
150
- return branch_details.gql_manager
151
- cached_branch_details = cls._branch_details_by_name[branch.name]
152
- # try to use the schema_changed_at time b/c it is faster than checking the hash
153
- if branch.schema_changed_at:
154
- changed_at_time = Timestamp(branch.schema_changed_at)
155
- if changed_at_time > cached_branch_details.schema_changed_at:
156
- cached_branch_details = cls._cache_branch(branch=branch, schema_branch=schema_branch)
157
- return cached_branch_details.gql_manager
158
- if branch.schema_hash:
159
- current_hash = branch.active_schema_hash.main
160
- else:
161
- current_hash = schema_branch.get_hash()
162
- if cached_branch_details.schema_hash != current_hash:
163
- cached_branch_details = cls._cache_branch(
164
- branch=branch, schema_branch=schema_branch, schema_hash=current_hash
165
- )
109
+ @dataclass
110
+ class InfrahubEdgedReference:
111
+ reference: type[InfrahubObject]
112
+ reference_hash: str
113
+
114
+
115
+ def get_attr_kind(node_schema: MainSchemaTypes, attr_schema: AttributeSchema) -> str:
116
+ if not config.SETTINGS.experimental_features.graphql_enums or not attr_schema.enum:
117
+ return attr_schema.kind
118
+ return get_enum_attribute_type_name(node_schema=node_schema, attr_schema=attr_schema)
166
119
 
167
- return cached_branch_details.gql_manager
168
120
 
121
+ class GraphQLSchemaManager:
169
122
  def __init__(self, schema: SchemaBranch) -> None:
170
123
  self.schema = schema
124
+ self.schema_hash = schema.get_hash()
171
125
 
172
126
  self._full_graphql_schema: GraphQLSchema | None = None
173
127
  self._graphql_types: dict[str, GraphQLTypes] = {}
@@ -275,9 +229,7 @@ class GraphQLSchemaManager:
275
229
  raise ValueError(f"Unable to find {name!r}")
276
230
 
277
231
  def get_all(self) -> dict[str, GraphQLTypes]:
278
- infrahub_types = self._graphql_types
279
- infrahub_types.update(self._extra_types)
280
- return infrahub_types
232
+ return self._graphql_types
281
233
 
282
234
  def set_type(self, name: str, graphql_type: GraphQLTypes) -> None:
283
235
  self._graphql_types[name] = graphql_type
@@ -342,7 +294,9 @@ class GraphQLSchemaManager:
342
294
  )
343
295
  ATTRIBUTE_TYPES[base_enum_name] = data_type_class
344
296
 
345
- def _get_related_input_type(self, relationship: RelationshipSchema) -> type[RelatedNodeInput]:
297
+ def _get_related_input_type(
298
+ self, relationship: RelationshipSchema
299
+ ) -> type[RelatedNodeInput | RelatedIPPrefixNodeInput | RelatedIPAddressNodeInput]:
346
300
  peer_schema = self.schema.get(name=relationship.peer, duplicate=False)
347
301
  if peer_schema.is_ip_prefix:
348
302
  return RelatedIPPrefixNodeInput
@@ -400,12 +354,15 @@ class GraphQLSchemaManager:
400
354
  # Generate all GraphQL ObjectType, Nested, Paginated & NestedPaginated and store them in the registry
401
355
  for node_schema in full_schema.values():
402
356
  if isinstance(node_schema, NodeSchema | ProfileSchema | TemplateSchema):
403
- node_type = self.generate_graphql_object(schema=node_schema, populate_cache=True)
357
+ node_object_type = self.generate_graphql_object(schema=node_schema, populate_cache=True)
404
358
  node_type_edged = self.generate_graphql_edged_object(
405
- schema=node_schema, node=node_type, populate_cache=True
359
+ schema=node_schema, node=node_object_type, populate_cache=True
406
360
  )
407
361
  nested_node_type_edged = self.generate_graphql_edged_object(
408
- schema=node_schema, node=node_type, relation_property=relationship_property, populate_cache=True
362
+ schema=node_schema,
363
+ node=node_object_type,
364
+ relation_property=relationship_property,
365
+ populate_cache=True,
409
366
  )
410
367
 
411
368
  self.generate_graphql_paginated_object(schema=node_schema, edge=node_type_edged, populate_cache=True)
@@ -533,6 +490,8 @@ class GraphQLSchemaManager:
533
490
  base_class = InfrahubIPPrefixMutation
534
491
  elif isinstance(node_schema, NodeSchema) and node_schema.is_ip_address:
535
492
  base_class = InfrahubIPAddressMutation
493
+ elif isinstance(node_schema, ProfileSchema):
494
+ base_class = InfrahubProfileMutation
536
495
  else:
537
496
  base_class = mutation_map.get(node_schema.kind, InfrahubMutation)
538
497
 
@@ -557,13 +516,15 @@ class GraphQLSchemaManager:
557
516
 
558
517
  return type("MutationMixin", (object,), class_attrs)
559
518
 
560
- def generate_graphql_object(self, schema: MainSchemaTypes, populate_cache: bool = False) -> type[InfrahubObject]:
519
+ def generate_graphql_object(self, schema: MainSchemaTypes, populate_cache: bool = False) -> InfrahubObjectReference:
561
520
  """Generate a GraphQL object Type from a Infrahub NodeSchema."""
562
521
 
563
522
  interfaces: set[type[InfrahubObject]] = set()
523
+ md5hash = hashlib.md5(usedforsecurity=False)
524
+ md5hash.update(f"{schema.kind}{schema.get_hash()}".encode())
564
525
 
565
526
  if isinstance(schema, NodeSchema | ProfileSchema | TemplateSchema) and schema.inherit_from:
566
- for generic_name in schema.inherit_from:
527
+ for generic_name in sorted(schema.inherit_from):
567
528
  generic = self.get_type(name=generic_name)
568
529
  interfaces.add(generic)
569
530
 
@@ -597,21 +558,26 @@ class GraphQLSchemaManager:
597
558
  req = "" if attr.optional else " (required)"
598
559
  main_attrs[attr.name] = graphene.Field(attr_type, description=f"{attr.description}{req}")
599
560
 
561
+ object_hash = md5hash.hexdigest()
562
+
600
563
  graphql_object = type(schema.kind, (InfrahubObject,), main_attrs)
564
+ registry.set_object_type(reference=graphql_object, reference_hash=object_hash, schema_hash=self.schema_hash)
601
565
 
602
566
  if populate_cache:
603
567
  self.set_type(name=schema.kind, graphql_type=graphql_object)
604
568
 
605
- return graphql_object
569
+ return InfrahubObjectReference(reference=graphql_object, reference_hash=object_hash)
606
570
 
607
- def generate_interface_object(
608
- self, schema: GenericSchema, populate_cache: bool = False
609
- ) -> type[graphene.Interface]:
571
+ def generate_interface_object(self, schema: GenericSchema, populate_cache: bool = False) -> InterfaceReference:
610
572
  meta_attrs = {
611
573
  "name": schema.kind,
612
574
  "description": schema.description,
613
575
  }
614
576
 
577
+ md5hash = hashlib.md5(usedforsecurity=False)
578
+ md5hash.update(f"interface-{schema.kind}{schema.get_hash()}".encode())
579
+ interface_hash = md5hash.hexdigest()
580
+
615
581
  main_attrs = {
616
582
  "id": graphene.Field(graphene.String, required=False, description="Unique identifier"),
617
583
  "hfid": graphene.Field(
@@ -620,7 +586,6 @@ class GraphQLSchemaManager:
620
586
  description="Human friendly identifier",
621
587
  ),
622
588
  "display_label": graphene.String(required=False),
623
- "Meta": type("Meta", (object,), meta_attrs),
624
589
  }
625
590
 
626
591
  for attr in schema.attributes:
@@ -628,12 +593,18 @@ class GraphQLSchemaManager:
628
593
  attr_type = self.get_type(name=get_attribute_type(kind=attr_kind).get_graphql_type_name())
629
594
  main_attrs[attr.name] = graphene.Field(attr_type, description=attr.description)
630
595
 
631
- interface_object = type(schema.kind, (InfrahubInterface,), main_attrs)
596
+ interface_object = registry.get_interface_type(reference_hash=interface_hash, schema_hash=self.schema_hash)
597
+ if not interface_object:
598
+ main_attrs["Meta"] = type("Meta", (object,), meta_attrs)
599
+ interface_object = type(schema.kind, (InfrahubInterface,), main_attrs)
600
+
601
+ interface_reference = InterfaceReference(reference=interface_object, reference_hash=interface_hash)
602
+ registry.set_interface_type(reference=interface_reference, schema_hash=self.schema_hash)
632
603
 
633
604
  if populate_cache:
634
605
  self.set_type(name=schema.kind, graphql_type=interface_object)
635
606
 
636
- return interface_object
607
+ return interface_reference
637
608
 
638
609
  def define_relationship_property(self, data_source: type[InfrahubObject], data_owner: type[InfrahubObject]) -> None:
639
610
  type_name = "RelationshipProperty"
@@ -721,7 +692,18 @@ class GraphQLSchemaManager:
721
692
  elif rel.cardinality == RelationshipCardinality.MANY:
722
693
  attrs[rel.name] = graphene.InputField(graphene.List(input_type), description=rel.description)
723
694
 
724
- return type(f"{schema.kind}CreateInput", (graphene.InputObjectType,), attrs)
695
+ input_name = f"{schema.kind}CreateInput"
696
+ md5hash = hashlib.md5(usedforsecurity=False)
697
+ md5hash.update(f"{input_name}{schema.get_hash()}".encode())
698
+ input_hash = md5hash.hexdigest()
699
+ mutation_input_type = registry.get_input_type(reference_hash=input_hash, schema_hash=self.schema_hash)
700
+ if not mutation_input_type:
701
+ mutation_input_type = type(input_name, (graphene.InputObjectType,), attrs)
702
+ registry.set_input_type(
703
+ reference=mutation_input_type, reference_hash=input_hash, schema_hash=self.schema_hash
704
+ )
705
+
706
+ return mutation_input_type
725
707
 
726
708
  def generate_graphql_mutation_update_input(self, schema: MainSchemaTypes) -> type[graphene.InputObjectType]:
727
709
  """Generate an InputObjectType Object from a Infrahub NodeSchema
@@ -734,7 +716,7 @@ class GraphQLSchemaManager:
734
716
  slug = InputField(StringAttributeUpdate, required=False)
735
717
  description = InputField(StringAttributeUpdate, required=False)
736
718
  """
737
- attrs: dict[str, graphene.String | graphene.InputField] = {
719
+ attrs: dict[str, graphene.String | graphene.InputField | graphene.List] = {
738
720
  "id": graphene.String(required=False),
739
721
  "hfid": graphene.List(of_type=graphene.String, required=False),
740
722
  }
@@ -760,7 +742,18 @@ class GraphQLSchemaManager:
760
742
  graphene.List(input_type), required=False, description=rel.description
761
743
  )
762
744
 
763
- return type(f"{schema.kind}UpdateInput", (graphene.InputObjectType,), attrs)
745
+ input_name = f"{schema.kind}UpdateInput"
746
+ md5hash = hashlib.md5(usedforsecurity=False)
747
+ md5hash.update(f"{input_name}{schema.get_hash()}".encode())
748
+ input_hash = md5hash.hexdigest()
749
+ mutation_input_type = registry.get_input_type(reference_hash=input_hash, schema_hash=self.schema_hash)
750
+ if not mutation_input_type:
751
+ mutation_input_type = type(input_name, (graphene.InputObjectType,), attrs)
752
+ registry.set_input_type(
753
+ reference=mutation_input_type, reference_hash=input_hash, schema_hash=self.schema_hash
754
+ )
755
+
756
+ return mutation_input_type
764
757
 
765
758
  def generate_graphql_mutation_upsert_input(
766
759
  self, schema: NodeSchema | ProfileSchema | TemplateSchema
@@ -775,7 +768,7 @@ class GraphQLSchemaManager:
775
768
  slug = InputField(StringAttributeUpdate, required=True)
776
769
  description = InputField(StringAttributeUpdate, required=False)
777
770
  """
778
- attrs: dict[str, graphene.String | graphene.InputField] = {
771
+ attrs: dict[str, graphene.String | graphene.InputField | graphene.List] = {
779
772
  "id": graphene.String(required=False),
780
773
  "hfid": graphene.List(of_type=graphene.String, required=False),
781
774
  }
@@ -801,7 +794,18 @@ class GraphQLSchemaManager:
801
794
  elif rel.cardinality == RelationshipCardinality.MANY:
802
795
  attrs[rel.name] = graphene.InputField(graphene.List(input_type), description=rel.description)
803
796
 
804
- return type(f"{schema.kind}UpsertInput", (graphene.InputObjectType,), attrs)
797
+ input_name = f"{schema.kind}UpsertInput"
798
+ md5hash = hashlib.md5(usedforsecurity=False)
799
+ md5hash.update(f"{input_name}{schema.get_hash()}".encode())
800
+ input_hash = md5hash.hexdigest()
801
+ mutation_input_type = registry.get_input_type(reference_hash=input_hash, schema_hash=self.schema_hash)
802
+ if not mutation_input_type:
803
+ mutation_input_type = type(input_name, (graphene.InputObjectType,), attrs)
804
+ registry.set_input_type(
805
+ reference=mutation_input_type, reference_hash=input_hash, schema_hash=self.schema_hash
806
+ )
807
+
808
+ return mutation_input_type
805
809
 
806
810
  def generate_graphql_mutation_create(
807
811
  self,
@@ -813,17 +817,27 @@ class GraphQLSchemaManager:
813
817
  """Generate a GraphQL Mutation to CREATE an object based on the specified NodeSchema."""
814
818
  name = f"{schema.kind}{mutation_type}"
815
819
 
816
- object_type = self.generate_graphql_object(schema=schema)
820
+ md5hash = hashlib.md5(usedforsecurity=False)
821
+ md5hash.update(f"{name}{schema.get_hash()}".encode())
822
+ mutation_hash = md5hash.hexdigest()
817
823
 
818
- main_attrs: dict[str, Any] = {"ok": graphene.Boolean(), "object": graphene.Field(object_type)}
824
+ mutation_object = registry.get_mutation_type(reference_hash=mutation_hash, schema_hash=self.schema_hash)
825
+ if not mutation_object:
826
+ object_type = self.generate_graphql_object(schema=schema)
819
827
 
820
- meta_attrs: dict[str, Any] = {"schema": schema, "name": name, "description": schema.description}
821
- main_attrs["Meta"] = type("Meta", (object,), meta_attrs)
828
+ main_attrs: dict[str, Any] = {"ok": graphene.Boolean(), "object": graphene.Field(object_type.reference)}
822
829
 
823
- args_attrs = {"data": input_type(required=True), "context": ContextInput(required=False)}
824
- main_attrs["Arguments"] = type("Arguments", (object,), args_attrs)
830
+ meta_attrs: dict[str, Any] = {"schema": schema, "name": name, "description": schema.description}
831
+ main_attrs["Meta"] = type("Meta", (object,), meta_attrs)
825
832
 
826
- return type(name, (base_class,), main_attrs)
833
+ args_attrs = {"data": input_type(required=True), "context": ContextInput(required=False)}
834
+ main_attrs["Arguments"] = type("Arguments", (object,), args_attrs)
835
+ mutation_object = type(name, (base_class,), main_attrs)
836
+ registry.set_mutation_type(
837
+ reference=mutation_object, reference_hash=mutation_hash, schema_hash=self.schema_hash
838
+ )
839
+
840
+ return mutation_object
827
841
 
828
842
  def generate_graphql_mutation_update(
829
843
  self,
@@ -834,34 +848,50 @@ class GraphQLSchemaManager:
834
848
  """Generate a GraphQL Mutation to UPDATE an object based on the specified NodeSchema."""
835
849
  name = f"{schema.kind}Update"
836
850
 
837
- object_type = self.generate_graphql_object(schema=schema)
851
+ md5hash = hashlib.md5(usedforsecurity=False)
852
+ md5hash.update(f"{name}{schema.get_hash()}".encode())
853
+ mutation_hash = md5hash.hexdigest()
838
854
 
839
- main_attrs: dict[str, Any] = {"ok": graphene.Boolean(), "object": graphene.Field(object_type)}
855
+ mutation_object = registry.get_mutation_type(reference_hash=mutation_hash, schema_hash=self.schema_hash)
856
+ if not mutation_object:
857
+ object_type = self.generate_graphql_object(schema=schema)
840
858
 
841
- meta_attrs: dict[str, Any] = {"schema": schema, "name": name, "description": schema.description}
842
- main_attrs["Meta"] = type("Meta", (object,), meta_attrs)
859
+ main_attrs: dict[str, Any] = {"ok": graphene.Boolean(), "object": graphene.Field(object_type.reference)}
843
860
 
844
- args_attrs = {"data": input_type(required=True), "context": ContextInput(required=False)}
845
- main_attrs["Arguments"] = type("Arguments", (object,), args_attrs)
861
+ meta_attrs: dict[str, Any] = {"schema": schema, "name": name, "description": schema.description}
862
+ main_attrs["Meta"] = type("Meta", (object,), meta_attrs)
846
863
 
847
- return type(name, (base_class,), main_attrs)
864
+ args_attrs = {"data": input_type(required=True), "context": ContextInput(required=False)}
865
+ main_attrs["Arguments"] = type("Arguments", (object,), args_attrs)
866
+
867
+ mutation_object = type(name, (base_class,), main_attrs)
868
+ registry.set_mutation_type(
869
+ reference=mutation_object, reference_hash=mutation_hash, schema_hash=self.schema_hash
870
+ )
871
+
872
+ return mutation_object
848
873
 
849
- @staticmethod
850
874
  def generate_graphql_mutation_delete(
851
- schema: NodeSchema | ProfileSchema | TemplateSchema, base_class: type[InfrahubMutation] = InfrahubMutation
875
+ self, schema: NodeSchema | ProfileSchema | TemplateSchema, base_class: type[InfrahubMutation] = InfrahubMutation
852
876
  ) -> type[InfrahubMutation]:
853
877
  """Generate a GraphQL Mutation to DELETE an object based on the specified NodeSchema."""
854
878
  name = f"{schema.kind}Delete"
879
+ md5hash = hashlib.md5(usedforsecurity=False)
880
+ md5hash.update(f"{name}{schema.get_hash()}".encode())
881
+ mutation_hash = md5hash.hexdigest()
882
+ mutation_object = registry.get_mutation_type(reference_hash=mutation_hash, schema_hash=self.schema_hash)
883
+ if not mutation_object:
884
+ main_attrs: dict[str, Any] = {"ok": graphene.Boolean()}
885
+ meta_attrs = {"schema": schema, "name": name, "description": schema.description}
886
+ main_attrs["Meta"] = type("Meta", (object,), meta_attrs)
887
+ args_attrs: dict[str, Any] = {"data": DeleteInput(required=True), "context": ContextInput(required=False)}
888
+ main_attrs["Arguments"] = type("Arguments", (object,), args_attrs)
889
+ mutation_object = type(name, (base_class,), main_attrs)
890
+ registry.set_mutation_type(
891
+ reference=mutation_object, reference_hash=mutation_hash, schema_hash=self.schema_hash
892
+ )
855
893
 
856
- main_attrs: dict[str, Any] = {"ok": graphene.Boolean()}
857
-
858
- meta_attrs = {"schema": schema, "name": name, "description": schema.description}
859
- main_attrs["Meta"] = type("Meta", (object,), meta_attrs)
860
-
861
- args_attrs: dict[str, Any] = {"data": DeleteInput(required=True), "context": ContextInput(required=False)}
862
- main_attrs["Arguments"] = type("Arguments", (object,), args_attrs)
863
-
864
- return type(name, (base_class,), main_attrs)
894
+ return mutation_object
865
895
 
866
896
  def generate_filters(
867
897
  self, schema: MainSchemaTypes, top_level: bool = False, include_properties: bool = True
@@ -934,16 +964,20 @@ class GraphQLSchemaManager:
934
964
  def generate_graphql_edged_object(
935
965
  self,
936
966
  schema: MainSchemaTypes,
937
- node: type[InfrahubObject],
967
+ node: InterfaceReference | InfrahubObjectReference,
938
968
  relation_property: type[InfrahubObject] | None = None,
939
969
  populate_cache: bool = False,
940
- ) -> type[InfrahubObject]:
970
+ ) -> InfrahubEdgedReference:
941
971
  """Generate a edged GraphQL object Type from a Infrahub NodeSchema for pagination."""
942
972
 
943
973
  object_name = f"Edged{schema.kind}"
944
974
  if relation_property:
945
975
  object_name = f"NestedEdged{schema.kind}"
946
976
 
977
+ md5hash = hashlib.md5(usedforsecurity=False)
978
+ md5hash.update(f"{object_name}{schema.get_hash()}{node.reference_hash}".encode())
979
+ edge_hash = md5hash.hexdigest()
980
+
947
981
  meta_attrs: dict[str, Any] = {
948
982
  "schema": schema,
949
983
  "name": object_name,
@@ -952,22 +986,27 @@ class GraphQLSchemaManager:
952
986
  }
953
987
 
954
988
  main_attrs: dict[str, Any] = {
955
- "node": graphene.Field(node, required=False),
989
+ "node": graphene.Field(node.reference, required=False),
956
990
  "Meta": type("Meta", (object,), meta_attrs),
957
991
  }
958
992
 
959
993
  if relation_property:
960
994
  main_attrs["properties"] = graphene.Field(relation_property, required=False)
961
995
 
962
- graphql_edged_object = type(object_name, (InfrahubObject,), main_attrs)
996
+ graphql_edged_object = registry.get_edge_type(reference_hash=edge_hash, schema_hash=self.schema_hash)
997
+ if not graphql_edged_object:
998
+ graphql_edged_object = type(object_name, (InfrahubObject,), main_attrs)
999
+ registry.set_edge_type(
1000
+ reference=graphql_edged_object, reference_hash=edge_hash, schema_hash=self.schema_hash
1001
+ )
963
1002
 
964
1003
  if populate_cache:
965
1004
  self.set_type(name=object_name, graphql_type=graphql_edged_object)
966
1005
 
967
- return graphql_edged_object
1006
+ return InfrahubEdgedReference(reference=graphql_edged_object, reference_hash=edge_hash)
968
1007
 
969
1008
  def generate_graphql_paginated_object(
970
- self, schema: MainSchemaTypes, edge: type[InfrahubObject], nested: bool = False, populate_cache: bool = False
1009
+ self, schema: MainSchemaTypes, edge: InfrahubEdgedReference, nested: bool = False, populate_cache: bool = False
971
1010
  ) -> type[InfrahubObject]:
972
1011
  """Generate a paginated GraphQL object Type from a Infrahub NodeSchema."""
973
1012
 
@@ -975,6 +1014,10 @@ class GraphQLSchemaManager:
975
1014
  if nested:
976
1015
  object_name = f"NestedPaginated{schema.kind}"
977
1016
 
1017
+ md5hash = hashlib.md5(usedforsecurity=False)
1018
+ md5hash.update(f"{object_name}{schema.get_hash()}{edge.reference_hash}".encode())
1019
+ paginated_hash = md5hash.hexdigest()
1020
+
978
1021
  meta_attrs: dict[str, Any] = {
979
1022
  "schema": schema,
980
1023
  "name": object_name,
@@ -985,14 +1028,21 @@ class GraphQLSchemaManager:
985
1028
 
986
1029
  main_attrs: dict[str, Any] = {
987
1030
  "count": graphene.Int(required=True),
988
- "edges": graphene.List(of_type=graphene.NonNull(edge), required=True),
1031
+ "edges": graphene.List(of_type=graphene.NonNull(edge.reference), required=True),
989
1032
  "permissions": graphene.Field(
990
1033
  PaginatedObjectPermission, required=True, resolver=parent_field_name_resolver
991
1034
  ),
992
- "Meta": type("Meta", (object,), meta_attrs),
993
1035
  }
994
1036
 
995
- graphql_paginated_object = type(object_name, (InfrahubObject,), main_attrs)
1037
+ graphql_paginated_object = registry.get_paginated_type(
1038
+ reference_hash=paginated_hash, schema_hash=self.schema_hash
1039
+ )
1040
+ if not graphql_paginated_object:
1041
+ main_attrs["Meta"] = type("Meta", (object,), meta_attrs)
1042
+ graphql_paginated_object = type(object_name, (InfrahubObject,), main_attrs)
1043
+ registry.set_paginated_type(
1044
+ reference=graphql_paginated_object, reference_hash=paginated_hash, schema_hash=self.schema_hash
1045
+ )
996
1046
 
997
1047
  if populate_cache:
998
1048
  self.set_type(name=object_name, graphql_type=graphql_paginated_object)
@@ -1022,27 +1072,53 @@ class GraphQLSchemaManager:
1022
1072
  main_attrs["properties"] = graphene.Field(relation_property, required=False)
1023
1073
 
1024
1074
  object_name = f"NestedEdged{schema.kind}"
1025
- nested_interface_object = type(object_name, (InfrahubObject,), main_attrs)
1075
+ md5hash = hashlib.md5(usedforsecurity=False)
1076
+ md5hash.update(f"{object_name}{schema.get_hash()}".encode())
1077
+ paginated_hash = md5hash.hexdigest()
1078
+ nested_interface_object = registry.get_paginated_type(
1079
+ reference_hash=paginated_hash, schema_hash=self.schema_hash
1080
+ )
1081
+ if not nested_interface_object:
1082
+ nested_interface_object = type(object_name, (InfrahubObject,), main_attrs)
1083
+ registry.set_paginated_type(
1084
+ reference=nested_interface_object, reference_hash=paginated_hash, schema_hash=self.schema_hash
1085
+ )
1026
1086
 
1027
1087
  if populate_cache:
1028
1088
  self.set_type(name=object_name, graphql_type=nested_interface_object)
1029
1089
 
1030
1090
  return nested_interface_object
1031
1091
 
1032
- @staticmethod
1033
1092
  def generate_paginated_interface_object(
1034
- schema: GenericSchema, base_interface: type[graphene.ObjectType]
1093
+ self, schema: GenericSchema, base_interface: type[graphene.ObjectType]
1035
1094
  ) -> type[InfrahubObject]:
1036
- meta_attrs: dict[str, Any] = {
1037
- "name": f"NestedPaginated{schema.kind}",
1038
- "schema": schema,
1039
- "description": schema.description,
1040
- }
1095
+ object_name = f"NestedPaginated{schema.kind}"
1096
+ md5hash = hashlib.md5(usedforsecurity=False)
1097
+ md5hash.update(f"{object_name}{schema.get_hash()}".encode())
1098
+ paginated_hash = md5hash.hexdigest()
1099
+
1100
+ nested_interface_object = registry.get_paginated_type(
1101
+ reference_hash=paginated_hash, schema_hash=self.schema_hash
1102
+ )
1103
+ if not nested_interface_object:
1104
+ meta_attrs: dict[str, Any] = {
1105
+ "name": object_name,
1106
+ "schema": schema,
1107
+ "description": schema.description,
1108
+ }
1109
+
1110
+ main_attrs: dict[str, Any] = {
1111
+ "count": graphene.Int(required=True),
1112
+ "edges": graphene.List(of_type=graphene.NonNull(base_interface)),
1113
+ "Meta": type("Meta", (object,), meta_attrs),
1114
+ }
1115
+
1116
+ nested_interface_object = type(object_name, (InfrahubObject,), main_attrs)
1117
+ registry.set_paginated_type(
1118
+ reference=nested_interface_object, reference_hash=paginated_hash, schema_hash=self.schema_hash
1119
+ )
1120
+
1121
+ return nested_interface_object
1041
1122
 
1042
- main_attrs: dict[str, Any] = {
1043
- "count": graphene.Int(required=True),
1044
- "edges": graphene.List(of_type=graphene.NonNull(base_interface)),
1045
- "Meta": type("Meta", (object,), meta_attrs),
1046
- }
1047
1123
 
1048
- return type(f"NestedPaginated{schema.kind}", (InfrahubObject,), main_attrs)
1124
+ registry._register_manager(manager=GraphQLSchemaManager)
@@ -0,0 +1,12 @@
1
+ from infrahub.core.branch.needs_rebase_status import check_need_rebase_status
2
+
3
+ ALLOWED_MUTATIONS_ON_NEED_REBASE_BRANCH = ["BranchRebase", "BranchDelete", "BranchCreate", "ProposedChangeCreate"]
4
+
5
+
6
+ def raise_on_mutation_on_branch_needing_rebase(next, root, info, **kwargs): # type: ignore # noqa
7
+ if info.operation.operation.value == "mutation":
8
+ mutation_name = info.operation.selection_set.selections[0].name.value
9
+ if mutation_name not in ALLOWED_MUTATIONS_ON_NEED_REBASE_BRANCH:
10
+ check_need_rebase_status(branch=info.context.branch)
11
+
12
+ return next(root, info, **kwargs)
@@ -7,8 +7,11 @@ from opentelemetry import trace
7
7
  from typing_extensions import Self
8
8
 
9
9
  from infrahub.branch.merge_mutation_checker import verify_branch_merge_mutation_allowed
10
+ from infrahub.core import registry
10
11
  from infrahub.core.branch import Branch
12
+ from infrahub.core.branch.enums import BranchStatus
11
13
  from infrahub.database import retry_db_transaction
14
+ from infrahub.exceptions import BranchNotFoundError, ValidationError
12
15
  from infrahub.graphql.context import apply_external_context
13
16
  from infrahub.graphql.field_extractor import extract_graphql_fields
14
17
  from infrahub.graphql.types.context import ContextInput
@@ -66,12 +69,21 @@ class BranchCreate(Mutation):
66
69
  background_execution: bool = False,
67
70
  wait_until_completion: bool = True,
68
71
  ) -> Self:
72
+ if data.origin_branch and data.origin_branch != registry.default_branch:
73
+ raise ValueError(f"origin_branch must be '{registry.default_branch}'")
74
+
69
75
  graphql_context: GraphqlContext = info.context
70
76
  task: dict | None = None
71
77
 
72
78
  model = BranchCreateModel(**data)
73
79
  await apply_external_context(graphql_context=graphql_context, context_input=context)
74
80
 
81
+ try:
82
+ await Branch.get_by_name(db=graphql_context.db, name=model.name)
83
+ raise ValidationError(f"The branch {model.name} already exists")
84
+ except BranchNotFoundError:
85
+ pass
86
+
75
87
  if background_execution or not wait_until_completion:
76
88
  workflow = await graphql_context.active_service.workflow.submit_workflow(
77
89
  workflow=BRANCH_CREATE, context=graphql_context.get_context(), parameters={"model": model}
@@ -279,6 +291,10 @@ class BranchMerge(Mutation):
279
291
  db=graphql_context.db, account_session=graphql_context.active_account_session
280
292
  )
281
293
 
294
+ obj = await Branch.get_by_name(db=graphql_context.db, name=branch_name)
295
+ if obj.status == BranchStatus.NEED_UPGRADE_REBASE:
296
+ raise ValidationError(f"Cannot merge branch '{branch_name}' with status '{obj.status.name}'")
297
+
282
298
  if wait_until_completion:
283
299
  await graphql_context.active_service.workflow.execute_workflow(
284
300
  workflow=BRANCH_MERGE_MUTATION,