Skip to content

Commit de7c278

Browse files
feat(BE): release pipeline v0.1.0 (#5496)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 3107972 commit de7c278

23 files changed

Lines changed: 857 additions & 13 deletions

File tree

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ FROM build-python AS build-python-private
108108
# and integrate private modules
109109
ARG SAML_REVISION
110110
ARG RBAC_REVISION
111-
ARG WITH="saml,auth-controller,ldap,workflows,licensing"
111+
ARG WITH="saml,auth-controller,ldap,workflows,licensing,release-pipelines"
112112
RUN --mount=type=secret,id=github_private_cloud_token \
113113
echo "https://$(cat /run/secrets/github_private_cloud_token):@github.com" > ${HOME}/.git-credentials && \
114114
git config --global credential.helper store && \

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: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -548,8 +548,8 @@ def multivariate_options(
548548

549549

550550
@pytest.fixture()
551-
def identity_matching_segment(project, trait): # type: ignore[no-untyped-def]
552-
segment = Segment.objects.create(name="Matching segment", project=project)
551+
def identity_matching_segment(project: Project, trait: Trait) -> Segment:
552+
segment: Segment = Segment.objects.create(name="Matching segment", project=project)
553553
matching_rule = SegmentRule.objects.create(
554554
segment=segment, type=SegmentRule.ALL_RULE
555555
)
@@ -563,7 +563,7 @@ def identity_matching_segment(project, trait): # type: ignore[no-untyped-def]
563563

564564

565565
@pytest.fixture()
566-
def api_client(): # type: ignore[no-untyped-def]
566+
def api_client() -> APIClient:
567567
return APIClient()
568568

569569

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]
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
# Generated by Django 4.2.22 on 2025-07-01 04:34
2+
3+
from django.conf import settings
4+
import django.core.validators
5+
from django.db import migrations, models
6+
import django.db.models.deletion
7+
import simple_history.models # type: ignore[import-untyped]
8+
import uuid
9+
10+
11+
class Migration(migrations.Migration):
12+
13+
initial = True
14+
15+
dependencies = [
16+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
17+
("api_keys", "0003_masterapikey_is_admin"),
18+
("projects", "0027_add_create_project_level_change_requests_permission"),
19+
("environments", "0037_add_uuid_field"),
20+
]
21+
22+
operations = [
23+
migrations.CreateModel(
24+
name="PipelineStage",
25+
fields=[
26+
(
27+
"id",
28+
models.AutoField(
29+
auto_created=True,
30+
primary_key=True,
31+
serialize=False,
32+
verbose_name="ID",
33+
),
34+
),
35+
("name", models.CharField(max_length=255)),
36+
(
37+
"order",
38+
models.PositiveSmallIntegerField(
39+
validators=[django.core.validators.MaxValueValidator(30)]
40+
),
41+
),
42+
(
43+
"environment",
44+
models.ForeignKey(
45+
on_delete=django.db.models.deletion.CASCADE,
46+
related_name="pipeline_stages",
47+
to="environments.environment",
48+
),
49+
),
50+
],
51+
),
52+
migrations.CreateModel(
53+
name="ReleasePipeline",
54+
fields=[
55+
(
56+
"id",
57+
models.AutoField(
58+
auto_created=True,
59+
primary_key=True,
60+
serialize=False,
61+
verbose_name="ID",
62+
),
63+
),
64+
(
65+
"deleted_at",
66+
models.DateTimeField(
67+
blank=True,
68+
db_index=True,
69+
default=None,
70+
editable=False,
71+
null=True,
72+
),
73+
),
74+
(
75+
"uuid",
76+
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
77+
),
78+
("name", models.CharField(max_length=255)),
79+
("description", models.TextField(blank=True, null=True)),
80+
("published_at", models.DateTimeField(blank=True, null=True)),
81+
(
82+
"project",
83+
models.ForeignKey(
84+
on_delete=django.db.models.deletion.CASCADE,
85+
related_name="release_pipelines",
86+
to="projects.project",
87+
),
88+
),
89+
(
90+
"published_by",
91+
models.ForeignKey(
92+
blank=True,
93+
null=True,
94+
on_delete=django.db.models.deletion.SET_NULL,
95+
related_name="published_release_pipelines",
96+
to=settings.AUTH_USER_MODEL,
97+
),
98+
),
99+
],
100+
options={
101+
"abstract": False,
102+
},
103+
),
104+
migrations.CreateModel(
105+
name="PipelineStageTrigger",
106+
fields=[
107+
(
108+
"id",
109+
models.AutoField(
110+
auto_created=True,
111+
primary_key=True,
112+
serialize=False,
113+
verbose_name="ID",
114+
),
115+
),
116+
(
117+
"trigger_type",
118+
models.CharField(
119+
choices=[
120+
("ON_ENTER", "Trigger when flag enters stage"),
121+
("WAIT_FOR", "Trigger after waiting for x amount of time"),
122+
],
123+
default="ON_ENTER",
124+
max_length=50,
125+
),
126+
),
127+
("trigger_body", models.JSONField(null=True)),
128+
(
129+
"stage",
130+
models.OneToOneField(
131+
on_delete=django.db.models.deletion.CASCADE,
132+
related_name="trigger",
133+
to="release_pipelines_core.pipelinestage",
134+
),
135+
),
136+
],
137+
),
138+
migrations.CreateModel(
139+
name="PipelineStageAction",
140+
fields=[
141+
(
142+
"id",
143+
models.AutoField(
144+
auto_created=True,
145+
primary_key=True,
146+
serialize=False,
147+
verbose_name="ID",
148+
),
149+
),
150+
(
151+
"action_type",
152+
models.CharField(
153+
choices=[
154+
(
155+
"TOGGLE_FEATURE",
156+
"Enable/Disable Feature for the environment",
157+
),
158+
(
159+
"UPDATE_FEATURE_VALUE",
160+
"Update Feature Value for the environment",
161+
),
162+
(
163+
"TOGGLE_FEATURE_FOR_SEGMENT",
164+
"Enable/Disable Feature for a specific segment",
165+
),
166+
(
167+
"UPDATE_FEATURE_VALUE_FOR_SEGMENT",
168+
"Update Feature Value for a specific segment",
169+
),
170+
],
171+
default="TOGGLE_FEATURE",
172+
max_length=50,
173+
),
174+
),
175+
("action_body", models.JSONField(null=True)),
176+
(
177+
"stage",
178+
models.ForeignKey(
179+
on_delete=django.db.models.deletion.CASCADE,
180+
related_name="actions",
181+
to="release_pipelines_core.pipelinestage",
182+
),
183+
),
184+
],
185+
),
186+
migrations.AddField(
187+
model_name="pipelinestage",
188+
name="pipeline",
189+
field=models.ForeignKey(
190+
on_delete=django.db.models.deletion.CASCADE,
191+
related_name="stages",
192+
to="release_pipelines_core.releasepipeline",
193+
),
194+
),
195+
migrations.CreateModel(
196+
name="HistoricalReleasePipeline",
197+
fields=[
198+
(
199+
"id",
200+
models.IntegerField(
201+
auto_created=True, blank=True, db_index=True, verbose_name="ID"
202+
),
203+
),
204+
(
205+
"deleted_at",
206+
models.DateTimeField(
207+
blank=True,
208+
db_index=True,
209+
default=None,
210+
editable=False,
211+
null=True,
212+
),
213+
),
214+
(
215+
"uuid",
216+
models.UUIDField(db_index=True, default=uuid.uuid4, editable=False),
217+
),
218+
("name", models.CharField(max_length=255)),
219+
("description", models.TextField(blank=True, null=True)),
220+
("published_at", models.DateTimeField(blank=True, null=True)),
221+
("history_id", models.AutoField(primary_key=True, serialize=False)),
222+
("history_date", models.DateTimeField()),
223+
("history_change_reason", models.CharField(max_length=100, null=True)),
224+
(
225+
"history_type",
226+
models.CharField(
227+
choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")],
228+
max_length=1,
229+
),
230+
),
231+
(
232+
"history_user",
233+
models.ForeignKey(
234+
null=True,
235+
on_delete=django.db.models.deletion.SET_NULL,
236+
related_name="+",
237+
to=settings.AUTH_USER_MODEL,
238+
),
239+
),
240+
(
241+
"master_api_key",
242+
models.ForeignKey(
243+
blank=True,
244+
null=True,
245+
on_delete=django.db.models.deletion.DO_NOTHING,
246+
to="api_keys.masterapikey",
247+
),
248+
),
249+
(
250+
"project",
251+
models.ForeignKey(
252+
blank=True,
253+
db_constraint=False,
254+
null=True,
255+
on_delete=django.db.models.deletion.DO_NOTHING,
256+
related_name="+",
257+
to="projects.project",
258+
),
259+
),
260+
(
261+
"published_by",
262+
models.ForeignKey(
263+
blank=True,
264+
db_constraint=False,
265+
null=True,
266+
on_delete=django.db.models.deletion.DO_NOTHING,
267+
related_name="+",
268+
to=settings.AUTH_USER_MODEL,
269+
),
270+
),
271+
],
272+
options={
273+
"verbose_name": "historical release pipeline",
274+
"ordering": ("-history_date", "-history_id"),
275+
"get_latest_by": "history_date",
276+
},
277+
bases=(simple_history.models.HistoricalChanges, models.Model),
278+
),
279+
migrations.AddConstraint(
280+
model_name="pipelinestage",
281+
constraint=models.UniqueConstraint(
282+
fields=("pipeline", "order"), name="unique_pipeline_stage_order"
283+
),
284+
),
285+
]

0 commit comments

Comments
 (0)