Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
636a4df
Doc: Webhook updates (#8029)
FragmentedPacket Jan 5, 2026
e949f2b
support migrating attribute kind on profiles (#8011)
ajtmccarty Jan 5, 2026
e202e34
Merge branch 'stable' into stable-to-release-1.7
ajtmccarty Jan 5, 2026
aee50a8
update TestMigrationAttributeKind for mandatory attr profile support
ajtmccarty Jan 5, 2026
1a53c0e
Merge pull request #8034 from opsmill/stable-to-release-1.7
ajtmccarty Jan 6, 2026
a4660a8
fix(backend, tests): too many db connections in integration tests
fatih-acar Dec 30, 2025
dd540ef
fix(backend, tests): move prefect class scoped fixtures
fatih-acar Jan 5, 2026
b6b84ef
fix(backend, tests): move db class scoped fixtures
fatih-acar Jan 6, 2026
44c78eb
fix(docs): update deployment examples
fatih-acar Jan 6, 2026
2e0abdc
Fix e2e tests (#8049)
pa-lem Jan 7, 2026
53286f4
Deprecate "_updated_at" for GraphQL queries
ogenstad Jan 5, 2026
4797a80
Use correct argument type
ogenstad Jan 5, 2026
7c09674
Move NATS specific violations
ogenstad Jan 5, 2026
52afa3a
Use correct type for environment variable
ogenstad Jan 5, 2026
6a1e04e
Add entities architecture doc for AI agents (#8043)
pa-lem Jan 7, 2026
8cda533
Merge stable into release 1.7 (#8051)
pa-lem Jan 7, 2026
1f6e4ae
Merge pull request #8027 from opsmill/pog-mark-updated_at-deprecated
ogenstad Jan 8, 2026
4d7097c
Merge pull request #8031 from opsmill/pog-correct-environment-type
ogenstad Jan 8, 2026
5465cd0
Merge pull request #8028 from opsmill/pog-testcontainers-argument-type
ogenstad Jan 8, 2026
d776832
Merge pull request #8030 from opsmill/pog-move-violations-to-nats
ogenstad Jan 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -848,7 +848,7 @@ jobs:
needs.files-changed.outputs.e2e == 'true'
runs-on:
group: huge-runners
timeout-minutes: 40
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> No
// ------------
// start with all the Attribute vertices we might care about
// ------------
MATCH (n:%(schema_kind)s)-[:HAS_ATTRIBUTE]->(attr:Attribute)
MATCH (n:%(schema_kinds)s)-[:HAS_ATTRIBUTE]->(attr:Attribute)
WHERE attr.name = $attr_name
WITH DISTINCT n, attr

Expand Down Expand Up @@ -76,7 +76,7 @@ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> No
// ------------
WITH 1 AS one
LIMIT 1
MATCH (n:%(schema_kind)s)-[:HAS_ATTRIBUTE]->(attr:Attribute)
MATCH (n:%(schema_kinds)s)-[:HAS_ATTRIBUTE]->(attr:Attribute)
WHERE attr.name = $attr_name
WITH DISTINCT n, attr

Expand All @@ -94,7 +94,6 @@ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> No
RETURN has_value_e, av
}


// ------------
// create and update the HAS_VALUE edges
// ------------
Expand Down Expand Up @@ -154,7 +153,9 @@ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> No
SET n.updated_at = $at, n.updated_by = $user_id
}
""" % {
"schema_kind": self.migration.previous_schema.kind,
"schema_kinds": (
f"{self.migration.previous_schema.kind}|Profile{self.migration.previous_schema.kind}|Template{self.migration.previous_schema.kind}"
),
"branch_filter": branch_filter,
"new_attr_value_labels": new_attr_value_labels,
}
Expand Down
29 changes: 28 additions & 1 deletion backend/infrahub/core/schema/attribute_parameters.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import sys
from typing import Self
from typing import Any, Self

from pydantic import ConfigDict, Field, model_validator

Expand All @@ -24,6 +24,33 @@ def get_attribute_parameters_class_for_kind(kind: str) -> type[AttributeParamete
class AttributeParameters(HashableModel):
model_config = ConfigDict(extra="forbid")

@classmethod
def convert_from(cls, source: AttributeParameters) -> Self:
"""Convert from another AttributeParameters subclass.

Args:
source: The source AttributeParameters instance to convert from

Returns:
A new instance of the target class with compatible fields populated
"""
source_data = source.model_dump()
return cls.convert_from_dict(source_data=source_data)

@classmethod
def convert_from_dict(cls, source_data: dict[str, Any]) -> Self:
"""Convert from a dictionary to the target class.

Args:
source_data: The source dictionary to convert from

Returns:
A new instance of the target class with compatible fields populated
"""
target_fields = set(cls.model_fields.keys())
filtered_data = {k: v for k, v in source_data.items() if k in target_fields}
return cls(**filtered_data)


class TextAttributeParameters(AttributeParameters):
regex: str | None = Field(
Expand Down
11 changes: 9 additions & 2 deletions backend/infrahub/core/schema/attribute_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,20 @@ def validate_dropdown_choices(cls, values: Any) -> Any:
@field_validator("parameters", mode="before")
@classmethod
def set_parameters_type(cls, value: Any, info: ValidationInfo) -> Any:
"""Override parameters class if using base AttributeParameters class and should be using a subclass"""
"""Override parameters class if using base AttributeParameters class and should be using a subclass.

This validator handles parameter type conversion when an attribute's kind changes.
Fields from the source that don't exist in the target are silently dropped.
Fields with the same name in both classes are preserved.
"""
kind = info.data["kind"]
expected_parameters_class = get_attribute_parameters_class_for_kind(kind=kind)
if value is None:
return expected_parameters_class()
if not isinstance(value, expected_parameters_class) and isinstance(value, AttributeParameters):
return expected_parameters_class(**value.model_dump())
return expected_parameters_class.convert_from(value)
if isinstance(value, dict):
return expected_parameters_class.convert_from_dict(source_data=value)
return value

@model_validator(mode="after")
Expand Down
7 changes: 5 additions & 2 deletions backend/infrahub/core/validators/attribute/kind.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> No
self.params["null_value"] = NULL_VALUE

query = """
MATCH p = (n:%(node_kind)s)
MATCH (n:%(node_kinds)s)
CALL (n) {
MATCH path = (root:Root)<-[rr:IS_PART_OF]-(n)-[ra:HAS_ATTRIBUTE]-(:Attribute { name: $attr_name } )-[rv:HAS_VALUE]-(av:AttributeValue)
WHERE all(
Expand All @@ -51,7 +51,10 @@ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> No
WHERE all(r in relationships(full_path) WHERE r.status = "active")
AND attribute_value IS NOT NULL
AND attribute_value <> $null_value
""" % {"branch_filter": branch_filter, "node_kind": self.node_schema.kind}
""" % {
"branch_filter": branch_filter,
"node_kinds": f"{self.node_schema.kind}|Profile{self.node_schema.kind}|Template{self.node_schema.kind}",
}

self.add_to_query(query)
self.return_labels = ["node.uuid", "attribute_value", "value_relationship.branch as value_branch"]
Expand Down
33 changes: 33 additions & 0 deletions backend/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ async def _db(singleton: bool = True) -> InfrahubDatabase:
await driver.close()


@pytest.fixture(scope="class")
async def db_class() -> InfrahubDatabase:
return await build_database(singleton=False)


@pytest.fixture
async def empty_database(db: InfrahubDatabase) -> None:
await do_empty_database(db=db)
Expand Down Expand Up @@ -449,6 +454,13 @@ def prefect_container(request: pytest.FixtureRequest, load_settings_before_sessi
return start_prefect_server_container(request)


@pytest.fixture(scope="class")
def prefect_container_class(
request: pytest.FixtureRequest, load_settings_before_session: None
) -> dict[int, int] | None:
return start_prefect_server_container(request)


@pytest.fixture(scope="module")
def prefect(
prefect_container: dict[int, int] | None, reload_settings_before_each_module: None
Expand All @@ -470,6 +482,27 @@ def prefect(
yield server_api_url


@pytest.fixture(scope="class")
def prefect_class(
prefect_container_class: dict[int, int] | None, reload_settings_before_each_module: None
) -> Generator[str, None, None]:
if prefect_container_class:
server_port = prefect_container_class[PORT_PREFECT]
server_api_url = f"http://localhost:{server_port}/api"
else:
server_api_url = f"http://localhost:{PORT_PREFECT}/api"

with ExitStack() as stack:
stack.enter_context(
prefect_settings.temporary_settings(
updates={
prefect_settings.PREFECT_API_URL: server_api_url,
}
)
)
yield server_api_url


@pytest.fixture(scope="session", autouse=True)
def load_settings_before_session() -> None:
load_and_exit()
Expand Down
3 changes: 2 additions & 1 deletion backend/tests/helpers/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ async def test_client(
self,
dependency_provider: Provider,
db: InfrahubDatabase,
db_class: InfrahubDatabase,
initialize_registry: None,
redis: dict[int, int] | None,
nats: dict[int, int] | None,
Expand All @@ -135,7 +136,7 @@ async def test_client(
# NOTE 2: FastAPI does not have an asynchronous TestClient, thus we rely on httpx.AsyncClient which does not trigger
# lifespan events (see https://fastapi.tiangolo.com/advanced/async-tests/#in-detail).
async def _db(singleton: bool = True) -> InfrahubDatabase:
return await build_database(singleton=False)
return db_class

with dependency_provider.scope(build_database, _db):
async with lifespan(app):
Expand Down
38 changes: 3 additions & 35 deletions backend/tests/helpers/test_worker.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import asyncio
from contextlib import ExitStack
from typing import Any, AsyncGenerator, Generator
from typing import Any, AsyncGenerator
from uuid import UUID

import pytest
from infrahub_sdk import InfrahubClient
from prefect import settings as prefect_settings
from prefect.client.orchestration import PrefectClient
from prefect.client.schemas.actions import WorkPoolCreate
from prefect.client.schemas.filters import WorkPoolFilter, WorkPoolFilterId
Expand All @@ -22,11 +20,7 @@
from infrahub.workflows.catalogue import INFRAHUB_WORKER_POOL
from infrahub.workflows.initialization import setup_blocks
from infrahub.workflows.models import WorkerPoolDefinition
from tests.helpers.constants import (
PORT_PREFECT,
)
from tests.helpers.test_app import TestInfrahubAppWithoutLocalWorkflow
from tests.helpers.utils import start_prefect_server_container


class TestWorkerInfrahubAsync(TestInfrahubAppWithoutLocalWorkflow):
Expand Down Expand Up @@ -62,34 +56,8 @@ async def worker_run_flow(
)

@pytest.fixture(scope="class")
def prefect_container_class(
self, request: pytest.FixtureRequest, load_settings_before_session: Any
) -> dict[int, int] | None:
return start_prefect_server_container(request)

@pytest.fixture(scope="class")
def prefect_server(
self, prefect_container_class: dict[int, int] | None, reload_settings_before_each_module: Any
) -> Generator[str, None, None]:
if prefect_container_class:
server_port = prefect_container_class[PORT_PREFECT]
server_api_url = f"http://localhost:{server_port}/api"
else:
server_api_url = f"http://localhost:{PORT_PREFECT}/api"

with ExitStack() as stack:
stack.enter_context(
prefect_settings.temporary_settings(
updates={
prefect_settings.PREFECT_API_URL: server_api_url,
}
)
)
yield server_api_url

@pytest.fixture(scope="class")
async def prefect_client(self, prefect_server: str) -> PrefectClient:
return PrefectClient(api=prefect_server)
async def prefect_client(self, prefect_class: str) -> PrefectClient:
return PrefectClient(api=prefect_class)

@pytest.fixture(scope="class")
async def work_pool(self, prefect_client: PrefectClient) -> WorkPool:
Expand Down
16 changes: 12 additions & 4 deletions backend/tests/integration/git/test_git_repository.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from pathlib import Path
from typing import AsyncGenerator

import pytest
import yaml
from fast_depends import dependency_provider
from infrahub_sdk import Config, InfrahubClient
from infrahub_sdk.exceptions import NodeNotFoundError
from infrahub_sdk.protocols import CoreCheckDefinition, CoreGraphQLQuery, CoreTransformJinja2, CoreTransformPython
Expand All @@ -15,9 +17,10 @@
from infrahub.core.utils import count_relationships, delete_all_nodes
from infrahub.database import InfrahubDatabase
from infrahub.git import InfrahubRepository
from infrahub.server import app, app_initialization
from infrahub.server import app, lifespan
from infrahub.services.adapters.workflow.local import WorkflowLocalExecution
from infrahub.utils import get_models_dir
from infrahub.workers.dependencies import build_database
from infrahub.workflows.initialization import setup_task_manager
from tests.helpers.file_repo import FileRepo
from tests.helpers.test_app import TestInfrahubApp
Expand Down Expand Up @@ -65,9 +68,14 @@ async def test_client(
self,
base_dataset,
workflow_local,
) -> InfrahubTestClient:
await app_initialization(app)
return InfrahubTestClient(app=app)
db_class: InfrahubDatabase,
) -> AsyncGenerator[InfrahubTestClient, None]:
async def _db(singleton: bool = True) -> InfrahubDatabase:
return db_class

with dependency_provider.scope(build_database, _db):
async with lifespan(app):
yield InfrahubTestClient(app=app)

@pytest.fixture
async def client(self, test_client: InfrahubTestClient, integration_helper) -> InfrahubClient:
Expand Down
Loading
Loading