Skip to content

Commit e977ace

Browse files
authored
feat: Release pipelines logic v0.2.1 (#5763)
1 parent 15b23b0 commit e977ace

File tree

6 files changed

+116
-42
lines changed

6 files changed

+116
-42
lines changed

api/audit/constants.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,13 @@
6767

6868

6969
RELEASE_PIPELINE_CREATED_MESSAGE = "Release Pipeline: %s created"
70+
RELEASE_PIPELINE_CLONED_MESSAGE = "Release Pipeline: %s cloned"
71+
RELEASE_PIPELINE_UPDATED_MESSAGE = "Release Pipeline: %s updated"
7072
RELEASE_PIPELINE_PUBLISHED_MESSAGE = "Release Pipeline: %s published"
71-
# TODO: Add audit log for pipeline update
73+
RELEASE_PIPELINE_UNPUBLISHED_MESSAGE = "Release Pipeline: %s Converted to Draft"
7274
RELEASE_PIPELINE_DELETED_MESSAGE = "Release Pipeline: %s deleted"
7375
RELEASE_PIPELINE_FEATURE_ADDED_MESSAGE = "Feature: %s added to Release Pipeline: %s"
76+
RELEASE_PIPELINE_FEATURE_REMOVED_MESSAGE = (
77+
"Feature: %s removed from Release Pipeline: %s"
78+
)
7479
FEATURE_STATE_UPDATED_BY_RELEASE_PIPELINE_MESSAGE = "Flag state / Remote config updated for feature: %s by Release pipeline: %s (stage: %s)"

api/features/release_pipelines/core/models.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,15 @@
77
from audit.constants import (
88
RELEASE_PIPELINE_CREATED_MESSAGE,
99
RELEASE_PIPELINE_DELETED_MESSAGE,
10-
RELEASE_PIPELINE_PUBLISHED_MESSAGE,
1110
)
12-
from audit.models import AuditLog
1311
from audit.related_object_type import RelatedObjectType
1412
from core.models import (
1513
SoftDeleteExportableModel,
1614
abstract_base_auditable_model_factory,
1715
)
1816
from features.release_pipelines.core.constants import MAX_PIPELINE_STAGES
1917
from features.release_pipelines.core.exceptions import InvalidPipelineStateError
18+
from features.versioning.models import EnvironmentFeatureVersion
2019
from projects.models import Project
2120
from users.models import FFAdminUser
2221

@@ -72,7 +71,13 @@ def publish(self, published_by: FFAdminUser) -> None:
7271
self.published_at = timezone.now()
7372
self.published_by = published_by
7473
self.save()
75-
self._create_pipeline_published_audit_log()
74+
75+
def unpublish(self, unpublished_by: FFAdminUser) -> None:
76+
if self.published_at is None:
77+
raise InvalidPipelineStateError("Pipeline is not published.")
78+
self.published_at = None
79+
self.published_by = None
80+
self.save()
7681

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

98+
def has_feature_in_flight(self) -> bool:
99+
has_feature_in_flight: bool = EnvironmentFeatureVersion.objects.filter(
100+
published_at__isnull=True, pipeline_stage__in=self.stages.all()
101+
).exists()
102+
return has_feature_in_flight
103+
93104
def _get_project(self) -> Project:
94105
return self.project
95106

96-
def _create_pipeline_published_audit_log(self) -> None:
97-
AuditLog.objects.create(
98-
related_object_id=self.id,
99-
related_object_type=RelatedObjectType.RELEASE_PIPELINE.name,
100-
project=self._get_project(),
101-
log=RELEASE_PIPELINE_PUBLISHED_MESSAGE % self.name,
102-
author=self.published_by,
103-
)
104-
105107

106108
class PipelineStage(models.Model):
107109
name = models.CharField(max_length=255)

api/poetry.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ flagsmith-split-testing = { git = "https://github.com/flagsmith/flagsmith-split-
207207
optional = true
208208

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

212212

213213
[tool.poetry.group.dev.dependencies]

api/tests/unit/features/release_pipeline/core/conftest.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import pytest
22

3+
from environments.models import Environment
34
from features.release_pipelines.core.models import (
5+
PipelineStage,
6+
PipelineStageAction,
7+
PipelineStageTrigger,
48
ReleasePipeline,
9+
StageActionType,
10+
StageTriggerType,
511
)
612
from projects.models import Project
713

@@ -13,3 +19,34 @@ def release_pipeline(project: Project) -> ReleasePipeline:
1319
project=project,
1420
)
1521
return release_pipeline # type: ignore[no-any-return]
22+
23+
24+
@pytest.fixture()
25+
def pipeline_stage_enable_feature_on_enter(
26+
release_pipeline: ReleasePipeline,
27+
environment: Environment,
28+
environment_v2_versioning: Environment,
29+
) -> PipelineStage:
30+
# Given
31+
pipeline_stage = PipelineStage.objects.create(
32+
name="Stage zero",
33+
pipeline=release_pipeline,
34+
order=0,
35+
environment=environment,
36+
)
37+
(
38+
PipelineStageTrigger.objects.create(
39+
trigger_type=StageTriggerType.ON_ENTER.value, stage=pipeline_stage
40+
),
41+
)
42+
PipelineStageAction.objects.create(
43+
action_type=StageActionType.TOGGLE_FEATURE.value,
44+
action_body={"enabled": True},
45+
stage=pipeline_stage,
46+
)
47+
PipelineStageAction.objects.create(
48+
action_type=StageActionType.UPDATE_FEATURE_VALUE.value,
49+
action_body={"string_value": "stage_zero_value", "type": "unicode"},
50+
stage=pipeline_stage,
51+
)
52+
return pipeline_stage

api/tests/unit/features/release_pipeline/core/test_unit_release_pipeline_models.py

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,21 @@
11
import pytest
2+
from django.utils import timezone
23

34
from audit.constants import (
45
RELEASE_PIPELINE_CREATED_MESSAGE,
56
RELEASE_PIPELINE_DELETED_MESSAGE,
6-
RELEASE_PIPELINE_PUBLISHED_MESSAGE,
77
)
8-
from audit.models import AuditLog
9-
from audit.related_object_type import RelatedObjectType
108
from environments.models import Environment
9+
from features.models import Feature
1110
from features.release_pipelines.core.exceptions import InvalidPipelineStateError
1211
from features.release_pipelines.core.models import (
1312
PipelineStage,
1413
ReleasePipeline,
1514
)
15+
from features.versioning.models import EnvironmentFeatureVersion
1616
from users.models import FFAdminUser
1717

1818

19-
def test_release_pipeline_publish_creates_audit_log(
20-
release_pipeline: ReleasePipeline, admin_user: FFAdminUser
21-
) -> None:
22-
# When
23-
release_pipeline.publish(admin_user)
24-
25-
# Then
26-
assert (
27-
AuditLog.objects.filter(
28-
related_object_id=release_pipeline.id,
29-
related_object_type=RelatedObjectType.RELEASE_PIPELINE.name,
30-
project=release_pipeline.project,
31-
log=RELEASE_PIPELINE_PUBLISHED_MESSAGE % release_pipeline.name,
32-
author=admin_user,
33-
).exists()
34-
is True
35-
)
36-
37-
3819
def test_release_pipeline_publish_raises_error_if_pipeline_is_already_published(
3920
release_pipeline: ReleasePipeline, admin_user: FFAdminUser
4021
) -> None:
@@ -153,3 +134,52 @@ def test_release_pipeline_get_delete_log_message(
153134

154135
# Then
155136
assert release_pipeline.get_delete_log_message(release_pipeline) == expected_message
137+
138+
139+
def test_release_pipeline_unpublish(
140+
release_pipeline: ReleasePipeline, admin_user: FFAdminUser
141+
) -> None:
142+
# Given - the pipeline is already published
143+
release_pipeline.publish(admin_user)
144+
145+
# When
146+
release_pipeline.unpublish(admin_user)
147+
148+
# Then
149+
assert release_pipeline.published_at is None
150+
assert release_pipeline.published_by is None
151+
152+
153+
def test_should_raise_error_when_unpublishing_unpublished_pipeline(
154+
release_pipeline: ReleasePipeline, admin_user: FFAdminUser
155+
) -> None:
156+
# When/ Then
157+
with pytest.raises(InvalidPipelineStateError, match="Pipeline is not published."):
158+
release_pipeline.unpublish(admin_user)
159+
160+
161+
def test_release_pipeline_has_feature_in_flight(
162+
release_pipeline: ReleasePipeline,
163+
environment: Environment,
164+
pipeline_stage_enable_feature_on_enter: PipelineStage,
165+
admin_user: FFAdminUser,
166+
feature: Feature,
167+
) -> None:
168+
# Given an unpublished environment feature version
169+
environment_version = EnvironmentFeatureVersion.objects.create(
170+
feature=feature,
171+
environment=environment,
172+
pipeline_stage=pipeline_stage_enable_feature_on_enter,
173+
published_at=None,
174+
)
175+
176+
# Then
177+
assert release_pipeline.has_feature_in_flight() is True
178+
179+
# Next, publish the environment feature version
180+
environment_version.published_at = timezone.now()
181+
environment_version.published_by = admin_user
182+
environment_version.save()
183+
184+
# Then
185+
assert release_pipeline.has_feature_in_flight() is False

0 commit comments

Comments
 (0)