Skip to content

Commit 566af08

Browse files
committed
Rework ephemeral
1 parent aadc8ba commit 566af08

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
@@ -234,9 +234,25 @@ def load(self) -> Optional[ReleaseState]:
234234
return None
235235

236236
state = ReleaseState(**state_data)
237+
238+
# Reset ephemeral fields to defaults if not in read-only mode
239+
if not self.read_only:
240+
self._reset_ephemeral_fields(state)
241+
237242
self.last_dump = state.model_dump_json(indent=2)
238243
return state
239244

245+
def _reset_ephemeral_fields(self, state: ReleaseState) -> None:
246+
"""Reset ephemeral fields to defaults (except log_once_flags which are always reset)."""
247+
# Reset release meta ephemeral
248+
state.meta.ephemeral = state.meta.ephemeral.__class__()
249+
250+
# Reset package ephemeral fields
251+
for package in state.packages.values():
252+
package.meta.ephemeral = package.meta.ephemeral.__class__()
253+
package.build.ephemeral = package.build.ephemeral.__class__()
254+
package.publish.ephemeral = package.publish.ephemeral.__class__()
255+
240256
def sync(self) -> None:
241257
"""Save state to storage backend if changed since last sync."""
242258
if self.read_only:

src/tests/test_state.py

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

307307
def test_ephemeral_field_not_serialized_to_json(self) -> None:
308-
"""Test that ephemeral field is excluded from JSON serialization."""
308+
"""Test that ephemeral field is serialized but log_once_flags are excluded."""
309309
workflow = Workflow(workflow_file="test.yml")
310310
workflow.ephemeral.trigger_failed = True
311311
workflow.ephemeral.timed_out = True
312+
workflow.ephemeral.log_once_flags["test_flag"] = True
312313

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

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

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

326328
def test_ephemeral_field_not_in_model_dump(self) -> None:
327-
"""Test that ephemeral field is excluded from model_dump."""
329+
"""Test that ephemeral field is in model_dump but log_once_flags are excluded."""
328330
workflow = Workflow(workflow_file="test.yml")
329331
workflow.ephemeral.trigger_failed = True
332+
workflow.ephemeral.log_once_flags["test_flag"] = True
330333

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

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

339342
def test_ephemeral_field_initialized_on_deserialization(self) -> None:
340343
"""Test that ephemeral field is initialized when loading from JSON."""
@@ -348,7 +351,7 @@ def test_ephemeral_field_initialized_on_deserialization(self) -> None:
348351
assert workflow.ephemeral.timed_out is False
349352

350353
def test_release_state_ephemeral_not_serialized(self) -> None:
351-
"""Test that ephemeral fields are not serialized in ReleaseState."""
354+
"""Test that ephemeral fields are serialized but log_once_flags are excluded."""
352355
config = Config(
353356
version=1,
354357
packages={
@@ -366,19 +369,22 @@ def test_release_state_ephemeral_not_serialized(self) -> None:
366369
# Modify ephemeral fields
367370
state.packages["test-package"].build.ephemeral.trigger_failed = True
368371
state.packages["test-package"].publish.ephemeral.timed_out = True
372+
state.packages["test-package"].build.ephemeral.log_once_flags["test"] = True
369373

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

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

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

383389

384390
class TestReleaseMeta:
@@ -450,7 +456,7 @@ def test_force_rebuild_field_can_be_modified(self) -> None:
450456
assert state.packages["test-package"].meta.ephemeral.force_rebuild is True
451457

452458
def test_ephemeral_not_serialized(self) -> None:
453-
"""Test that ephemeral field is not serialized to JSON."""
459+
"""Test that ephemeral field is serialized but log_once_flags are excluded."""
454460
config = Config(
455461
version=1,
456462
packages={
@@ -465,12 +471,21 @@ def test_ephemeral_not_serialized(self) -> None:
465471

466472
state = ReleaseState.from_config(config)
467473
state.packages["test-package"].meta.ephemeral.force_rebuild = True
474+
state.packages["test-package"].meta.ephemeral.log_once_flags["test"] = True
468475

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

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

475490

476491
class TestStateSyncerWithArgs:

0 commit comments

Comments
 (0)