|
1 | 1 | """ |
2 | 2 | Actions and Conditions for the Release Tree |
3 | 3 |
|
| 4 | +Here we define only simple atomic actions and conditions. |
| 5 | +Next level composites are defined in `composites.py`. |
| 6 | +
|
4 | 7 | The guiding principles are: |
5 | 8 |
|
6 | 9 | * Actions should be atomic and represent a single task. |
|
15 | 18 | import uuid |
16 | 19 | from datetime import datetime |
17 | 20 | from token import OP |
18 | | -from typing import Any, Dict, Optional |
| 21 | +from typing import Any, Dict, List, Optional |
19 | 22 |
|
20 | 23 | from py_trees.behaviour import Behaviour |
21 | 24 | from py_trees.common import Status |
@@ -71,21 +74,156 @@ def check_task_exists(self) -> bool: |
71 | 74 |
|
72 | 75 | class IdentifyTargetRef(ReleaseAction): |
73 | 76 | 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 = "", |
75 | 83 | ) -> None: |
76 | 84 | 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] = [] |
77 | 89 | super().__init__(name=name, log_prefix=log_prefix) |
78 | 90 |
|
| 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 | + |
79 | 122 | def update(self) -> Status: |
| 123 | + # If ref is already set, we're done |
80 | 124 | if self.package_meta.ref is not None: |
| 125 | + self.logger.debug(f"Ref already set: {self.package_meta.ref}") |
81 | 126 | 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}" |
86 | 225 | ) |
87 | | - self.feedback_message = f"Target ref set to {self.package_meta.ref}" |
88 | | - return Status.SUCCESS |
| 226 | + return None |
89 | 227 |
|
90 | 228 |
|
91 | 229 | class TriggerWorkflow(ReleaseAction): |
@@ -372,6 +510,31 @@ def update(self) -> Status: |
372 | 510 | return self.log_exception_and_return_failure(e) |
373 | 511 |
|
374 | 512 |
|
| 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 | + |
375 | 538 | class ResetPackageState(ReleaseAction): |
376 | 539 | def __init__( |
377 | 540 | self, |
@@ -528,31 +691,6 @@ def update(self) -> Status: |
528 | 691 | return Status.SUCCESS |
529 | 692 |
|
530 | 693 |
|
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 | | - |
556 | 694 | class IsForceRebuild(LoggingAction): |
557 | 695 | def __init__( |
558 | 696 | self, name: str, package_meta: PackageMeta, log_prefix: str = "" |
|
0 commit comments