Skip to content

Commit 18b8c5b

Browse files
committed
Rework ephemeral
1 parent 42b00b2 commit 18b8c5b

File tree

3 files changed

+102
-40
lines changed

3 files changed

+102
-40
lines changed

src/redis_release/bht/state.py

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,46 @@
2222

2323

2424
class WorkflowEphemeral(BaseModel):
25-
"""Ephemeral workflow state that is not persisted."""
25+
"""Ephemeral workflow state. Reset on each run.
2626
27-
trigger_failed: bool = False
28-
trigger_attempted: bool = False
29-
identify_failed: bool = False
30-
timed_out: bool = False
31-
artifacts_download_failed: bool = False
32-
extract_result_failed: bool = False
33-
log_once_flags: Dict[str, bool] = Field(default_factory=dict)
27+
Each workflow step has a pair of fields indicating the step status:
28+
One ephemeral field is set when the step is attempted. It may have three states:
29+
- `None` (default): Step has not been attempted
30+
- `True`: Step has been attempted and failed
31+
- `False`: Step has been attempted and succeeded
32+
33+
Ephemeral fields are reset on each run. Their values are persisted but only until
34+
next run is started.
35+
So they indicate either current (if run is in progress) or last run state.
36+
37+
The other field indicates the step result, it may either have some value or be empty.
38+
39+
For example for trigger step we have `trigger_failed` ephemeral
40+
and `triggered_at` result fields.
41+
42+
Each step may be in one of the following states:
43+
Not started
44+
Failed
45+
Succeeded or OK
46+
Incorrect (this shouln't happen)
47+
48+
The following decision table show how step status is determined for trigger step.
49+
In general this logic is used to display release state table.
50+
51+
tigger_failed -> | None (default) | True | False |
52+
triggered_at: | | | |
53+
None | Not started | Failed | Incorrect |
54+
Has value | OK | Incorrect | OK |
55+
56+
"""
57+
58+
trigger_failed: Optional[bool] = None
59+
trigger_attempted: Optional[bool] = None
60+
identify_failed: Optional[bool] = None
61+
timed_out: Optional[bool] = None
62+
artifacts_download_failed: Optional[bool] = None
63+
extract_result_failed: Optional[bool] = None
64+
log_once_flags: Dict[str, bool] = Field(default_factory=dict, exclude=True)
3465

3566

3667
class Workflow(BaseModel):
@@ -47,17 +78,18 @@ class Workflow(BaseModel):
4778
conclusion: Optional[WorkflowConclusion] = None
4879
artifacts: Optional[Dict[str, Any]] = None
4980
result: Optional[Dict[str, Any]] = None
50-
ephemeral: WorkflowEphemeral = Field(
51-
default_factory=WorkflowEphemeral, exclude=True
52-
)
81+
ephemeral: WorkflowEphemeral = Field(default_factory=WorkflowEphemeral)
5382

5483

5584
class PackageMetaEphemeral(BaseModel):
56-
"""Ephemeral package metadata that is not persisted."""
85+
"""Ephemeral package metadata. Reset on each run.
86+
87+
See WorkflowEphemeral for more details.
88+
"""
5789

5890
force_rebuild: bool = False
5991
identify_ref_failed: bool = False
60-
log_once_flags: Dict[str, bool] = Field(default_factory=dict)
92+
log_once_flags: Dict[str, bool] = Field(default_factory=dict, exclude=True)
6193

6294

6395
class PackageMeta(BaseModel):
@@ -67,9 +99,7 @@ class PackageMeta(BaseModel):
6799
repo: str = ""
68100
ref: Optional[str] = None
69101
publish_internal_release: bool = False
70-
ephemeral: PackageMetaEphemeral = Field(
71-
default_factory=PackageMetaEphemeral, exclude=True
72-
)
102+
ephemeral: PackageMetaEphemeral = Field(default_factory=PackageMetaEphemeral)
73103

74104

75105
class Package(BaseModel):
@@ -81,19 +111,20 @@ class Package(BaseModel):
81111

82112

83113
class ReleaseMetaEphemeral(BaseModel):
84-
"""Ephemeral release metadata that is not persisted."""
114+
"""Ephemeral release metadata. Reset on each run.
115+
116+
See WorkflowEphemeral for more details.
117+
"""
85118

86-
log_once_flags: Dict[str, bool] = Field(default_factory=dict)
119+
log_once_flags: Dict[str, bool] = Field(default_factory=dict, exclude=True)
87120

88121

89122
class ReleaseMeta(BaseModel):
90123
"""Metadata for the release."""
91124

92125
tag: Optional[str] = None
93126
release_type: Optional[ReleaseType] = None
94-
ephemeral: ReleaseMetaEphemeral = Field(
95-
default_factory=ReleaseMetaEphemeral, exclude=True
96-
)
127+
ephemeral: ReleaseMetaEphemeral = Field(default_factory=ReleaseMetaEphemeral)
97128

98129

99130
class ReleaseState(BaseModel):

src/redis_release/state_manager.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,9 +248,25 @@ def load(self) -> Optional[ReleaseState]:
248248
return None
249249

250250
state = ReleaseState(**state_data)
251+
252+
# Reset ephemeral fields to defaults if not in read-only mode
253+
if not self.read_only:
254+
self._reset_ephemeral_fields(state)
255+
251256
self.last_dump = state.model_dump_json(indent=2)
252257
return state
253258

259+
def _reset_ephemeral_fields(self, state: ReleaseState) -> None:
260+
"""Reset ephemeral fields to defaults (except log_once_flags which are always reset)."""
261+
# Reset release meta ephemeral
262+
state.meta.ephemeral = state.meta.ephemeral.__class__()
263+
264+
# Reset package ephemeral fields
265+
for package in state.packages.values():
266+
package.meta.ephemeral = package.meta.ephemeral.__class__()
267+
package.build.ephemeral = package.build.ephemeral.__class__()
268+
package.publish.ephemeral = package.publish.ephemeral.__class__()
269+
254270
def sync(self) -> None:
255271
"""Save state to storage backend if changed since last sync."""
256272
if self.read_only:

src/tests/test_state.py

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -304,36 +304,39 @@ def test_ephemeral_field_can_be_modified(self) -> None:
304304
assert workflow.ephemeral.timed_out is True
305305

306306
def test_ephemeral_field_not_serialized_to_json(self) -> None:
307-
"""Test that ephemeral field is excluded from JSON serialization."""
307+
"""Test that ephemeral field is serialized but log_once_flags are excluded."""
308308
workflow = Workflow(workflow_file="test.yml")
309309
workflow.ephemeral.trigger_failed = True
310310
workflow.ephemeral.timed_out = True
311+
workflow.ephemeral.log_once_flags["test_flag"] = True
311312

312313
# Serialize to JSON
313314
json_str = workflow.model_dump_json()
314315
json_data = json.loads(json_str)
315316

316-
# Verify ephemeral field is not in JSON
317-
assert "ephemeral" not in json_data
318-
assert "trigger_failed" not in json_data
319-
assert "timed_out" not in json_data
317+
# Verify ephemeral field IS in JSON (except log_once_flags)
318+
assert "ephemeral" in json_data
319+
assert json_data["ephemeral"]["trigger_failed"] is True
320+
assert json_data["ephemeral"]["timed_out"] is True
321+
assert "log_once_flags" not in json_data["ephemeral"]
320322

321323
# Verify other fields are present
322324
assert "workflow_file" in json_data
323325
assert json_data["workflow_file"] == "test.yml"
324326

325327
def test_ephemeral_field_not_in_model_dump(self) -> None:
326-
"""Test that ephemeral field is excluded from model_dump."""
328+
"""Test that ephemeral field is in model_dump but log_once_flags are excluded."""
327329
workflow = Workflow(workflow_file="test.yml")
328330
workflow.ephemeral.trigger_failed = True
331+
workflow.ephemeral.log_once_flags["test_flag"] = True
329332

330333
# Get dict representation
331334
data = workflow.model_dump()
332335

333-
# Verify ephemeral field is not in dict
334-
assert "ephemeral" not in data
335-
assert "trigger_failed" not in data
336-
assert "timed_out" not in data
336+
# Verify ephemeral field IS in dict (except log_once_flags)
337+
assert "ephemeral" in data
338+
assert data["ephemeral"]["trigger_failed"] is True
339+
assert "log_once_flags" not in data["ephemeral"]
337340

338341
def test_ephemeral_field_initialized_on_deserialization(self) -> None:
339342
"""Test that ephemeral field is initialized when loading from JSON."""
@@ -347,7 +350,7 @@ def test_ephemeral_field_initialized_on_deserialization(self) -> None:
347350
assert workflow.ephemeral.timed_out is False
348351

349352
def test_release_state_ephemeral_not_serialized(self) -> None:
350-
"""Test that ephemeral fields are not serialized in ReleaseState."""
353+
"""Test that ephemeral fields are serialized but log_once_flags are excluded."""
351354
config = Config(
352355
version=1,
353356
packages={
@@ -365,19 +368,22 @@ def test_release_state_ephemeral_not_serialized(self) -> None:
365368
# Modify ephemeral fields
366369
state.packages["test-package"].build.ephemeral.trigger_failed = True
367370
state.packages["test-package"].publish.ephemeral.timed_out = True
371+
state.packages["test-package"].build.ephemeral.log_once_flags["test"] = True
368372

369373
# Serialize to JSON
370374
json_str = state.model_dump_json()
371375
json_data = json.loads(json_str)
372376

373-
# Verify ephemeral fields are not in JSON
377+
# Verify ephemeral fields ARE in JSON (except log_once_flags)
374378
build_workflow = json_data["packages"]["test-package"]["build"]
375379
publish_workflow = json_data["packages"]["test-package"]["publish"]
376380

377-
assert "ephemeral" not in build_workflow
378-
assert "trigger_failed" not in build_workflow
379-
assert "ephemeral" not in publish_workflow
380-
assert "timed_out" not in publish_workflow
381+
assert "ephemeral" in build_workflow
382+
assert build_workflow["ephemeral"]["trigger_failed"] is True
383+
assert "log_once_flags" not in build_workflow["ephemeral"]
384+
assert "ephemeral" in publish_workflow
385+
assert publish_workflow["ephemeral"]["timed_out"] is True
386+
assert "log_once_flags" not in publish_workflow["ephemeral"]
381387

382388

383389
class TestReleaseMeta:
@@ -449,7 +455,7 @@ def test_force_rebuild_field_can_be_modified(self) -> None:
449455
assert state.packages["test-package"].meta.ephemeral.force_rebuild is True
450456

451457
def test_ephemeral_not_serialized(self) -> None:
452-
"""Test that ephemeral field is not serialized to JSON."""
458+
"""Test that ephemeral field is serialized but log_once_flags are excluded."""
453459
config = Config(
454460
version=1,
455461
packages={
@@ -464,12 +470,21 @@ def test_ephemeral_not_serialized(self) -> None:
464470

465471
state = ReleaseState.from_config(config)
466472
state.packages["test-package"].meta.ephemeral.force_rebuild = True
473+
state.packages["test-package"].meta.ephemeral.log_once_flags["test"] = True
467474

468475
json_str = state.model_dump_json()
469476
json_data = json.loads(json_str)
470477

471-
assert "ephemeral" not in json_data["packages"]["test-package"]["meta"]
472-
assert "force_rebuild" not in json_data["packages"]["test-package"]["meta"]
478+
# Ephemeral field IS serialized (except log_once_flags)
479+
assert "ephemeral" in json_data["packages"]["test-package"]["meta"]
480+
assert (
481+
json_data["packages"]["test-package"]["meta"]["ephemeral"]["force_rebuild"]
482+
is True
483+
)
484+
assert (
485+
"log_once_flags"
486+
not in json_data["packages"]["test-package"]["meta"]["ephemeral"]
487+
)
473488

474489

475490
class TestStateSyncerWithArgs:

0 commit comments

Comments
 (0)