Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 6 additions & 1 deletion api/audit/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,13 @@


RELEASE_PIPELINE_CREATED_MESSAGE = "Release Pipeline: %s created"
RELEASE_PIPELINE_CLONED_MESSAGE = "Release Pipeline: %s cloned"
RELEASE_PIPELINE_UPDATED_MESSAGE = "Release Pipeline: %s updated"
RELEASE_PIPELINE_PUBLISHED_MESSAGE = "Release Pipeline: %s published"
# TODO: Add audit log for pipeline update
RELEASE_PIPELINE_UNPUBLISHED_MESSAGE = "Release Pipeline: %s Converted to Draft"
RELEASE_PIPELINE_DELETED_MESSAGE = "Release Pipeline: %s deleted"
RELEASE_PIPELINE_FEATURE_ADDED_MESSAGE = "Feature: %s added to Release Pipeline: %s"
RELEASE_PIPELINE_FEATURE_REMOVED_MESSAGE = (
"Feature: %s removed from Release Pipeline: %s"
)
FEATURE_STATE_UPDATED_BY_RELEASE_PIPELINE_MESSAGE = "Flag state / Remote config updated for feature: %s by Release pipeline: %s (stage: %s)"
26 changes: 14 additions & 12 deletions api/features/release_pipelines/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,15 @@
from audit.constants import (
RELEASE_PIPELINE_CREATED_MESSAGE,
RELEASE_PIPELINE_DELETED_MESSAGE,
RELEASE_PIPELINE_PUBLISHED_MESSAGE,
)
from audit.models import AuditLog
from audit.related_object_type import RelatedObjectType
from core.models import (
SoftDeleteExportableModel,
abstract_base_auditable_model_factory,
)
from features.release_pipelines.core.constants import MAX_PIPELINE_STAGES
from features.release_pipelines.core.exceptions import InvalidPipelineStateError
from features.versioning.models import EnvironmentFeatureVersion
from projects.models import Project
from users.models import FFAdminUser

Expand Down Expand Up @@ -72,7 +71,13 @@ def publish(self, published_by: FFAdminUser) -> None:
self.published_at = timezone.now()
self.published_by = published_by
self.save()
self._create_pipeline_published_audit_log()

def unpublish(self, unpublished_by: FFAdminUser) -> None:
if self.published_at is None:
raise InvalidPipelineStateError("Pipeline is not published.")
self.published_at = None
self.published_by = None
self.save()

def get_first_stage(self) -> "PipelineStage | None":
return self.stages.order_by("order").first()
Expand All @@ -90,18 +95,15 @@ def get_delete_log_message(
) -> typing.Optional[str]:
return RELEASE_PIPELINE_DELETED_MESSAGE % self.name

def has_feature_in_flight(self) -> bool:
has_feature_in_flight: bool = EnvironmentFeatureVersion.objects.filter(
published_at__isnull=True, pipeline_stage__in=self.stages.all()
).exists()
return has_feature_in_flight

def _get_project(self) -> Project:
return self.project

def _create_pipeline_published_audit_log(self) -> None:
AuditLog.objects.create(
related_object_id=self.id,
related_object_type=RelatedObjectType.RELEASE_PIPELINE.name,
project=self._get_project(),
log=RELEASE_PIPELINE_PUBLISHED_MESSAGE % self.name,
author=self.published_by,
)


class PipelineStage(models.Model):
name = models.CharField(max_length=255)
Expand Down
12 changes: 6 additions & 6 deletions api/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ flagsmith-split-testing = { git = "https://github.com/flagsmith/flagsmith-split-
optional = true

[tool.poetry.group.release-pipelines.dependencies]
flagsmith-private = { git = "https://github.com/Flagsmith/flagsmith-private/", rev = "v0.1.0" }
flagsmith-private = { git = "https://github.com/Flagsmith/flagsmith-private/", rev = "v0.2.1" }


[tool.poetry.group.dev.dependencies]
Expand Down
37 changes: 37 additions & 0 deletions api/tests/unit/features/release_pipeline/core/conftest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import pytest

from environments.models import Environment
from features.release_pipelines.core.models import (
PipelineStage,
PipelineStageAction,
PipelineStageTrigger,
ReleasePipeline,
StageActionType,
StageTriggerType,
)
from projects.models import Project

Expand All @@ -13,3 +19,34 @@ def release_pipeline(project: Project) -> ReleasePipeline:
project=project,
)
return release_pipeline # type: ignore[no-any-return]


@pytest.fixture()
def pipeline_stage_enable_feature_on_enter(
release_pipeline: ReleasePipeline,
environment: Environment,
environment_v2_versioning: Environment,
) -> PipelineStage:
# Given
pipeline_stage = PipelineStage.objects.create(
name="Stage zero",
pipeline=release_pipeline,
order=0,
environment=environment,
)
(
PipelineStageTrigger.objects.create(
trigger_type=StageTriggerType.ON_ENTER.value, stage=pipeline_stage
),
)
PipelineStageAction.objects.create(
action_type=StageActionType.TOGGLE_FEATURE.value,
action_body={"enabled": True},
stage=pipeline_stage,
)
PipelineStageAction.objects.create(
action_type=StageActionType.UPDATE_FEATURE_VALUE.value,
action_body={"string_value": "stage_zero_value", "type": "unicode"},
stage=pipeline_stage,
)
return pipeline_stage
Original file line number Diff line number Diff line change
@@ -1,40 +1,21 @@
import pytest
from django.utils import timezone

from audit.constants import (
RELEASE_PIPELINE_CREATED_MESSAGE,
RELEASE_PIPELINE_DELETED_MESSAGE,
RELEASE_PIPELINE_PUBLISHED_MESSAGE,
)
from audit.models import AuditLog
from audit.related_object_type import RelatedObjectType
from environments.models import Environment
from features.models import Feature
from features.release_pipelines.core.exceptions import InvalidPipelineStateError
from features.release_pipelines.core.models import (
PipelineStage,
ReleasePipeline,
)
from features.versioning.models import EnvironmentFeatureVersion
from users.models import FFAdminUser


def test_release_pipeline_publish_creates_audit_log(
release_pipeline: ReleasePipeline, admin_user: FFAdminUser
) -> None:
# When
release_pipeline.publish(admin_user)

# Then
assert (
AuditLog.objects.filter(
related_object_id=release_pipeline.id,
related_object_type=RelatedObjectType.RELEASE_PIPELINE.name,
project=release_pipeline.project,
log=RELEASE_PIPELINE_PUBLISHED_MESSAGE % release_pipeline.name,
author=admin_user,
).exists()
is True
)


def test_release_pipeline_publish_raises_error_if_pipeline_is_already_published(
release_pipeline: ReleasePipeline, admin_user: FFAdminUser
) -> None:
Expand Down Expand Up @@ -153,3 +134,52 @@ def test_release_pipeline_get_delete_log_message(

# Then
assert release_pipeline.get_delete_log_message(release_pipeline) == expected_message


def test_release_pipeline_unpublish(
release_pipeline: ReleasePipeline, admin_user: FFAdminUser
) -> None:
# Given - the pipeline is already published
release_pipeline.publish(admin_user)

# When
release_pipeline.unpublish(admin_user)

# Then
assert release_pipeline.published_at is None
assert release_pipeline.published_by is None


def test_should_raise_error_when_unpublishing_unpublished_pipeline(
release_pipeline: ReleasePipeline, admin_user: FFAdminUser
) -> None:
# When/ Then
with pytest.raises(InvalidPipelineStateError, match="Pipeline is not published."):
release_pipeline.unpublish(admin_user)


def test_release_pipeline_has_feature_in_flight(
release_pipeline: ReleasePipeline,
environment: Environment,
pipeline_stage_enable_feature_on_enter: PipelineStage,
admin_user: FFAdminUser,
feature: Feature,
) -> None:
# Given an unpublished environment feature version
environment_version = EnvironmentFeatureVersion.objects.create(
feature=feature,
environment=environment,
pipeline_stage=pipeline_stage_enable_feature_on_enter,
published_at=None,
)

# Then
assert release_pipeline.has_feature_in_flight() is True

# Next, publish the environment feature version
environment_version.published_at = timezone.now()
environment_version.published_by = admin_user
environment_version.save()

# Then
assert release_pipeline.has_feature_in_flight() is False
Loading