Skip to content

Commit dff9660

Browse files
committed
Identify target from by listing branches in a remote repo
1 parent 24c73d6 commit dff9660

File tree

8 files changed

+726
-43
lines changed

8 files changed

+726
-43
lines changed

src/redis_release/bht/behaviours.py

Lines changed: 171 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
"""
22
Actions and Conditions for the Release Tree
33
4+
Here we define only simple atomic actions and conditions.
5+
Next level composites are defined in `composites.py`.
6+
47
The guiding principles are:
58
69
* Actions should be atomic and represent a single task.
@@ -15,7 +18,7 @@
1518
import uuid
1619
from datetime import datetime
1720
from token import OP
18-
from typing import Any, Dict, Optional
21+
from typing import Any, Dict, List, Optional
1922

2023
from py_trees.behaviour import Behaviour
2124
from py_trees.common import Status
@@ -71,21 +74,156 @@ def check_task_exists(self) -> bool:
7174

7275
class IdentifyTargetRef(ReleaseAction):
7376
def __init__(
74-
self, name: str, package_meta: PackageMeta, log_prefix: str = ""
77+
self,
78+
name: str,
79+
package_meta: PackageMeta,
80+
release_meta: ReleaseMeta,
81+
github_client: GitHubClientAsync,
82+
log_prefix: str = "",
7583
) -> None:
7684
self.package_meta = package_meta
85+
self.release_meta = release_meta
86+
self.github_client = github_client
87+
self.release_version: Optional["RedisVersion"] = None
88+
self.branches: List[str] = []
7789
super().__init__(name=name, log_prefix=log_prefix)
7890

91+
def initialise(self) -> None:
92+
"""Initialize by parsing release version and listing branches."""
93+
# If ref is already set, nothing to do
94+
if self.package_meta.ref is not None:
95+
return
96+
97+
# Parse release version from tag
98+
if not self.release_meta.tag:
99+
self.logger.error("Release tag is not set")
100+
return
101+
102+
try:
103+
from ..models import RedisVersion
104+
105+
self.release_version = RedisVersion.parse(self.release_meta.tag)
106+
self.logger.debug(
107+
f"Parsed release version: {self.release_version.major}.{self.release_version.minor}"
108+
)
109+
except ValueError as e:
110+
self.logger.error(f"Failed to parse release tag: {e}")
111+
return
112+
113+
# List remote branches matching release pattern with major version
114+
# Pattern: release/MAJOR.\d+$ (e.g., release/8.\d+$ for major version 8)
115+
pattern = f"^release/{self.release_version.major}\\.\\d+$"
116+
self.task = asyncio.create_task(
117+
self.github_client.list_remote_branches(
118+
self.package_meta.repo, pattern=pattern
119+
)
120+
)
121+
79122
def update(self) -> Status:
123+
# If ref is already set, we're done
80124
if self.package_meta.ref is not None:
125+
self.logger.debug(f"Ref already set: {self.package_meta.ref}")
81126
return Status.SUCCESS
82-
# For now, just set a hardcoded ref
83-
self.package_meta.ref = "release/8.2"
84-
self.logger.info(
85-
f"[green]Target ref identified:[/green] {self.package_meta.ref}"
127+
128+
try:
129+
assert self.task is not None
130+
131+
# Wait for branch listing to complete
132+
if not self.task.done():
133+
return Status.RUNNING
134+
135+
self.branches = self.task.result()
136+
self.logger.debug(f"Found {len(self.branches)} branches")
137+
138+
# Sort branches and detect appropriate one
139+
sorted_branches = self._sort_branches(self.branches)
140+
detected_branch = self._detect_branch(sorted_branches)
141+
142+
if detected_branch:
143+
self.package_meta.ref = detected_branch
144+
self.logger.info(
145+
f"[green]Target ref identified:[/green] {self.package_meta.ref}"
146+
)
147+
self.feedback_message = f"Target ref set to {self.package_meta.ref}"
148+
return Status.SUCCESS
149+
else:
150+
self.logger.error("Failed to detect appropriate branch")
151+
self.feedback_message = "Failed to detect appropriate branch"
152+
return Status.FAILURE
153+
154+
except Exception as e:
155+
return self.log_exception_and_return_failure(e)
156+
157+
def _sort_branches(self, branches: List[str]) -> List[str]:
158+
"""Sort branches by version in descending order.
159+
160+
Args:
161+
branches: List of branch names (e.g., ["release/8.0", "release/8.4"])
162+
163+
Returns:
164+
Sorted list of branch names in descending order by version
165+
(e.g., ["release/8.4", "release/8.2", "release/8.0"])
166+
"""
167+
pattern = re.compile(r"^release/(\d+)\.(\d+)$")
168+
branch_versions = []
169+
170+
for branch in branches:
171+
match = pattern.match(branch)
172+
if match:
173+
major = int(match.group(1))
174+
minor = int(match.group(2))
175+
branch_versions.append((major, minor, branch))
176+
177+
# Sort by (major, minor) descending
178+
branch_versions.sort(reverse=True)
179+
180+
return [branch for _, _, branch in branch_versions]
181+
182+
def _detect_branch(self, sorted_branches: List[str]) -> Optional[str]:
183+
"""Detect the appropriate branch from sorted list of branches.
184+
185+
Walks over sorted list of branches (descending order) trying to find first
186+
branch equal to release/MAJOR.MINOR or lower version.
187+
188+
Args:
189+
sorted_branches: Sorted list of branch names in descending order
190+
(e.g., ["release/8.4", "release/8.2", "release/8.0"])
191+
Can be empty.
192+
193+
Returns:
194+
Branch name or None if no suitable branch found
195+
"""
196+
if not self.release_version:
197+
return None
198+
199+
if not sorted_branches:
200+
self.logger.warning("No release branches found matching pattern")
201+
return None
202+
203+
target_major = self.release_version.major
204+
target_minor = self.release_version.minor
205+
206+
# Pattern to extract version from branch name
207+
pattern = re.compile(r"^release/(\d+)\.(\d+)$")
208+
209+
# Walk through sorted branches (descending order)
210+
# Find first branch <= target version
211+
for branch in sorted_branches:
212+
match = pattern.match(branch)
213+
if match:
214+
major = int(match.group(1))
215+
minor = int(match.group(2))
216+
217+
if (major, minor) <= (target_major, target_minor):
218+
self.logger.debug(
219+
f"Found matching branch: {branch} for target {target_major}.{target_minor}"
220+
)
221+
return branch
222+
223+
self.logger.warning(
224+
f"No suitable branch found for version {target_major}.{target_minor}"
86225
)
87-
self.feedback_message = f"Target ref set to {self.package_meta.ref}"
88-
return Status.SUCCESS
226+
return None
89227

90228

91229
class TriggerWorkflow(ReleaseAction):
@@ -372,6 +510,31 @@ def update(self) -> Status:
372510
return self.log_exception_and_return_failure(e)
373511

374512

513+
class AttachReleaseHandleToPublishWorkflow(LoggingAction):
514+
def __init__(
515+
self,
516+
name: str,
517+
build_workflow: Workflow,
518+
publish_workflow: Workflow,
519+
log_prefix: str = "",
520+
) -> None:
521+
self.build_workflow = build_workflow
522+
self.publish_workflow = publish_workflow
523+
super().__init__(name=name, log_prefix=log_prefix)
524+
525+
def update(self) -> Status:
526+
if "release_handle" in self.publish_workflow.inputs:
527+
return Status.SUCCESS
528+
529+
if self.build_workflow.result is None:
530+
return Status.FAILURE
531+
532+
self.publish_workflow.inputs["release_handle"] = json.dumps(
533+
self.build_workflow.result
534+
)
535+
return Status.SUCCESS
536+
537+
375538
class ResetPackageState(ReleaseAction):
376539
def __init__(
377540
self,
@@ -528,31 +691,6 @@ def update(self) -> Status:
528691
return Status.SUCCESS
529692

530693

531-
class AttachReleaseHandleToPublishWorkflow(LoggingAction):
532-
def __init__(
533-
self,
534-
name: str,
535-
build_workflow: Workflow,
536-
publish_workflow: Workflow,
537-
log_prefix: str = "",
538-
) -> None:
539-
self.build_workflow = build_workflow
540-
self.publish_workflow = publish_workflow
541-
super().__init__(name=name, log_prefix=log_prefix)
542-
543-
def update(self) -> Status:
544-
if "release_handle" in self.publish_workflow.inputs:
545-
return Status.SUCCESS
546-
547-
if self.build_workflow.result is None:
548-
return Status.FAILURE
549-
550-
self.publish_workflow.inputs["release_handle"] = json.dumps(
551-
self.build_workflow.result
552-
)
553-
return Status.SUCCESS
554-
555-
556694
class IsForceRebuild(LoggingAction):
557695
def __init__(
558696
self, name: str, package_meta: PackageMeta, log_prefix: str = ""

src/redis_release/bht/composites.py

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
"""
2+
Higher level composites for the Release Tree
3+
4+
These composites are built from the atomic actions and conditions defined in `behaviours.py`.
5+
Here we make flag and state aware tree behaviors, implement retry and repeat patterns.
6+
7+
The guiding principle for the composites defined here is the same as in behaviours.py
8+
in a sense that we aim to make a more or less direct action without complex conditions
9+
(except for the flags)
10+
11+
More complex behaviors, including pre- and post- conditions are defined in `ppas.py`.
12+
"""
13+
114
from typing import Iterator, List, Optional
215
from typing import Sequence as TypingSequence
316

@@ -216,19 +229,22 @@ def __init__(
216229
)
217230

218231

219-
class IdentifyTargetRefGoal(FlagGuard):
232+
class IdentifyTargetRefGuarded(FlagGuard):
220233
def __init__(
221234
self,
222235
name: str,
223236
package_meta: PackageMeta,
224237
release_meta: ReleaseMeta,
238+
github_client: GitHubClientAsync,
225239
log_prefix: str = "",
226240
) -> None:
227241
super().__init__(
228-
None,
242+
None if name == "" else name,
229243
IdentifyTargetRef(
230244
"Identify Target Ref",
231245
package_meta,
246+
release_meta,
247+
github_client,
232248
log_prefix=log_prefix,
233249
),
234250
package_meta.ephemeral,
@@ -341,12 +357,22 @@ def __init__(
341357
default_package,
342358
log_prefix=log_prefix,
343359
)
344-
reset_package_state_wrapped = SuccessIsRunning(
360+
reset_package_state_running = SuccessIsRunning(
345361
"Success is Running", reset_package_state
346362
)
363+
reset_package_state_guarded = FlagGuard(
364+
None if name == "" else name,
365+
reset_package_state_running,
366+
package.meta.ephemeral,
367+
"identify_ref_failed",
368+
flag_value=True,
369+
raise_on=[],
370+
guard_status=Status.FAILURE,
371+
log_prefix=log_prefix,
372+
)
347373
super().__init__(
348374
None if name == "" else name,
349-
reset_package_state_wrapped,
375+
reset_package_state_guarded,
350376
workflow.ephemeral,
351377
"trigger_attempted",
352378
flag_value=True,
@@ -358,7 +384,7 @@ def __init__(
358384

359385
class RestartWorkflowGuarded(FlagGuard):
360386
"""
361-
Reset workflow if we didn't trigger the workflow in current run
387+
Reset workflow if we didn't trigger the workflow in current run and if there was no identify target ref error
362388
363389
This will only reset the workflow state
364390
@@ -369,6 +395,7 @@ def __init__(
369395
self,
370396
name: str,
371397
workflow: Workflow,
398+
package_meta: PackageMeta,
372399
default_workflow: Workflow,
373400
log_prefix: str = "",
374401
) -> None:
@@ -378,12 +405,22 @@ def __init__(
378405
default_workflow,
379406
log_prefix=log_prefix,
380407
)
381-
reset_workflow_state_wrapped = SuccessIsRunning(
408+
reset_workflow_state_running = SuccessIsRunning(
382409
"Success is Running", reset_workflow_state
383410
)
411+
reset_workflow_state_guarded = FlagGuard(
412+
None if name == "" else name,
413+
reset_workflow_state_running,
414+
package_meta.ephemeral,
415+
"identify_ref_failed",
416+
flag_value=True,
417+
raise_on=[],
418+
guard_status=Status.FAILURE,
419+
log_prefix=log_prefix,
420+
)
384421
super().__init__(
385422
None if name == "" else name,
386-
reset_workflow_state_wrapped,
423+
reset_workflow_state_guarded,
387424
workflow.ephemeral,
388425
"trigger_attempted",
389426
flag_value=True,

src/redis_release/bht/ppas.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
"""
2+
Here we define PPAs (Postcondition-Precondition-Action) composites to be used in backchaining.
3+
4+
See backchain.py for more details on backchaining.
5+
6+
Chains are formed and latched in `tree.py`
7+
8+
"""
9+
110
from typing import Union
211

312
from py_trees.composites import Selector, Sequence
@@ -18,7 +27,7 @@
1827
DownloadArtifactsListGuarded,
1928
ExtractArtifactResultGuarded,
2029
FindWorkflowByUUID,
21-
IdentifyTargetRefGoal,
30+
IdentifyTargetRefGuarded,
2231
TriggerWorkflowGuarded,
2332
WaitForWorkflowCompletion,
2433
)
@@ -108,14 +117,16 @@ def create_trigger_workflow_ppa(
108117
def create_identify_target_ref_ppa(
109118
package_meta: PackageMeta,
110119
release_meta: ReleaseMeta,
120+
github_client: GitHubClientAsync,
111121
log_prefix: str,
112122
) -> Union[Selector, Sequence]:
113123
return create_PPA(
114124
"Identify Target Ref",
115-
IdentifyTargetRefGoal(
125+
IdentifyTargetRefGuarded(
116126
"",
117127
package_meta,
118128
release_meta,
129+
github_client,
119130
log_prefix=log_prefix,
120131
),
121132
)

0 commit comments

Comments
 (0)