Skip to content

Commit 7c4ef9f

Browse files
committed
wip: release pipeline
1 parent 4b54c64 commit 7c4ef9f

22 files changed

Lines changed: 898 additions & 12 deletions

File tree

api/app/settings/common.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
"features.multivariate",
114114
"features.versioning",
115115
"features.workflows.core",
116+
"features.release_pipelines.core",
116117
"segments",
117118
"app",
118119
"e2etests",
@@ -1067,6 +1068,14 @@
10671068
if importlib.util.find_spec("workflows_logic.stale_flags") is not None:
10681069
INSTALLED_APPS.append("workflows_logic.stale_flags")
10691070

1071+
RELEASE_PIPELINES_LOGIC_INSTALLED = (
1072+
importlib.util.find_spec("release_pipelines_logic") is not None
1073+
)
1074+
1075+
if RELEASE_PIPELINES_LOGIC_INSTALLED: # pragma: no cover
1076+
INSTALLED_APPS.append("release_pipelines_logic")
1077+
1078+
10701079
# Additional functionality for restricting authentication to a set of authentication methods in Flagsmith SaaS
10711080
AUTH_CONTROLLER_INSTALLED = importlib.util.find_spec("auth_controller") is not None
10721081
if AUTH_CONTROLLER_INSTALLED:

api/audit/constants.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,11 @@
6464
ENVIRONMENT_FEATURE_VERSION_PUBLISHED_MESSAGE = "New version published for feature: %s"
6565

6666
DATETIME_FORMAT = "%d/%m/%Y %H:%M:%S"
67+
68+
69+
RELEASE_PIPELINE_CREATED_MESSAGE = "Release Pipeline: %s created"
70+
RELEASE_PIPELINE_PUBLISHED_MESSAGE = "Release Pipeline: %s published"
71+
# TODO: Add audit log for pipeline update
72+
RELEASE_PIPELINE_DELETED_MESSAGE = "Release Pipeline: %s deleted"
73+
RELEASE_PIPELINE_FEATURE_ADDED_MESSAGE = "Feature: %s added to Release Pipeline: %s"
74+
FEATURE_STATE_UPDATED_BY_RELEASE_PIPELINE_MESSAGE = "Flag state / Remote config updated for feature: %s by Release pipeline: %s (stage: %s)"

api/audit/related_object_type.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ class RelatedObjectType(enum.Enum):
1111
IMPORT_REQUEST = "Import request"
1212
EF_VERSION = "Environment feature version"
1313
FEATURE_HEALTH = "Feature health status"
14+
RELEASE_PIPELINE = "Release pipeline"

api/conftest.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import logging
23
import os
34
import site
@@ -17,6 +18,7 @@
1718
from django.core.cache import caches
1819
from django.db.backends.base.creation import TEST_DATABASE_PREFIX
1920
from django.test.utils import setup_databases
21+
from django.urls import reverse
2022
from flag_engine.segments.constants import EQUAL
2123
from moto import mock_dynamodb # type: ignore[import-untyped]
2224
from mypy_boto3_dynamodb.service_resource import DynamoDBServiceResource, Table
@@ -25,6 +27,7 @@
2527
from pytest_django.fixtures import SettingsWrapper
2628
from pytest_django.plugin import blocking_manager_key
2729
from pytest_mock import MockerFixture
30+
from rest_framework import status
2831
from rest_framework.authtoken.models import Token
2932
from rest_framework.test import APIClient
3033
from task_processor.task_run_method import TaskRunMethod
@@ -83,6 +86,8 @@
8386
from tests.test_helpers import fix_issue_3869
8487
from tests.types import (
8588
AdminClientAuthType,
89+
GetEnvironmentFlagsResponseJSONCallable,
90+
GetIdentityFlagsResponseJSONCallable,
8691
WithEnvironmentPermissionsCallable,
8792
WithOrganisationPermissionsCallable,
8893
WithProjectPermissionsCallable,
@@ -548,8 +553,8 @@ def multivariate_options(
548553

549554

550555
@pytest.fixture()
551-
def identity_matching_segment(project, trait): # type: ignore[no-untyped-def]
552-
segment = Segment.objects.create(name="Matching segment", project=project)
556+
def identity_matching_segment(project: Project, trait: Trait) -> Segment:
557+
segment: Segment = Segment.objects.create(name="Matching segment", project=project)
553558
matching_rule = SegmentRule.objects.create(
554559
segment=segment, type=SegmentRule.ALL_RULE
555560
)
@@ -563,10 +568,17 @@ def identity_matching_segment(project, trait): # type: ignore[no-untyped-def]
563568

564569

565570
@pytest.fixture()
566-
def api_client(): # type: ignore[no-untyped-def]
571+
def api_client() -> APIClient:
567572
return APIClient()
568573

569574

575+
@pytest.fixture()
576+
def sdk_client(environment: Environment) -> APIClient:
577+
client = APIClient()
578+
client.credentials(HTTP_X_ENVIRONMENT_KEY=environment.api_key)
579+
return client
580+
581+
570582
@pytest.fixture()
571583
def feature(project: Project, environment: Environment) -> Feature:
572584
return Feature.objects.create(name="Test Feature1", project=project) # type: ignore[no-any-return]
@@ -1265,3 +1277,46 @@ def set_github_webhook_secret() -> None:
12651277
from django.conf import settings
12661278

12671279
settings.GITHUB_WEBHOOK_SECRET = "secret-key"
1280+
1281+
1282+
# TODO: move _flags_response_json to common?
1283+
@pytest.fixture()
1284+
def get_environment_flags_response_json(
1285+
sdk_client: APIClient,
1286+
) -> GetEnvironmentFlagsResponseJSONCallable:
1287+
get_environment_flags_url = reverse("api-v1:flags")
1288+
1289+
def _get_environment_flags_response_json(num_expected_flags: int) -> typing.Dict: # type: ignore[type-arg]
1290+
_response = sdk_client.get(get_environment_flags_url)
1291+
assert _response.status_code == status.HTTP_200_OK
1292+
_response_json = _response.json()
1293+
assert len(_response_json) == num_expected_flags
1294+
return _response_json # type: ignore[no-any-return]
1295+
1296+
return _get_environment_flags_response_json
1297+
1298+
1299+
@pytest.fixture()
1300+
def get_identity_flags_response_json(
1301+
sdk_client: APIClient, identity: Identity
1302+
) -> GetIdentityFlagsResponseJSONCallable:
1303+
identities_url = reverse("api-v1:sdk-identities")
1304+
1305+
def _get_identity_flags_response_json( # type: ignore[no-untyped-def]
1306+
num_expected_flags: int, identifier: str = identity.identifier, **traits
1307+
) -> typing.Dict: # type: ignore[type-arg]
1308+
traits = traits or {}
1309+
data = {
1310+
"identifier": identifier,
1311+
"traits": [{"trait_key": k, "trait_value": v} for k, v in traits.items()],
1312+
}
1313+
1314+
_response = sdk_client.post(
1315+
identities_url, data=json.dumps(data), content_type="application/json"
1316+
)
1317+
assert _response.status_code == status.HTTP_200_OK
1318+
_response_json = _response.json()
1319+
assert len(_response_json["flags"]) == num_expected_flags
1320+
return _response_json # type: ignore[no-any-return]
1321+
1322+
return _get_identity_flags_response_json # type: ignore[return-value]

api/features/release_pipelines/core/__init__.py

Whitespace-only changes.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from core.apps import BaseAppConfig
2+
3+
4+
class ReleasePipelineConfig(BaseAppConfig):
5+
name = "features.release_pipelines.core"
6+
label = "release_pipelines_core"
7+
default = True
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
MAX_PIPELINE_STAGES = 30
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from rest_framework import status
2+
from rest_framework.exceptions import APIException
3+
4+
5+
class ReleasePipelineError(APIException):
6+
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
7+
8+
9+
class InvalidPipelineStateError(ReleasePipelineError):
10+
status_code = status.HTTP_400_BAD_REQUEST # type: ignore[assignment]

0 commit comments

Comments
 (0)