From bf365eaac216b500cbfecb58e8f3f6f9af86041e Mon Sep 17 00:00:00 2001 From: Petar Shtuchkin Date: Tue, 25 Nov 2025 16:56:31 +0200 Subject: [PATCH 01/13] Snap release, move around the code and some refactoring --- config.yaml | 11 + src/redis_release/bht/behaviours.py | 296 +----------------- src/redis_release/bht/behaviours_docker.py | 26 ++ src/redis_release/bht/behaviours_homebrew.py | 273 ++++++++++++++++ src/redis_release/bht/behaviours_snap.py | 281 +++++++++++++++++ src/redis_release/bht/composites.py | 29 +- src/redis_release/bht/tree.py | 4 +- src/redis_release/bht/tree_factory.py | 309 +++++++++++++++---- 8 files changed, 872 insertions(+), 357 deletions(-) create mode 100644 src/redis_release/bht/behaviours_docker.py create mode 100644 src/redis_release/bht/behaviours_homebrew.py create mode 100644 src/redis_release/bht/behaviours_snap.py diff --git a/config.yaml b/config.yaml index 3e513b5..b4e4107 100644 --- a/config.yaml +++ b/config.yaml @@ -52,3 +52,14 @@ packages: publish_workflow: release_publish.yml publish_timeout_minutes: 10 publish_inputs: {} + snap: + package_type: snap + repo: redislabsdev/redis-snap + # homebrew has one fixed release branch: main + ref: main + build_workflow: release_build_and_test.yml + build_inputs: {} + publish_internal_release: yes + publish_workflow: release_publish.yml + publish_timeout_minutes: 10 + publish_inputs: {} diff --git a/src/redis_release/bht/behaviours.py b/src/redis_release/bht/behaviours.py index 862c079..c8dd461 100644 --- a/src/redis_release/bht/behaviours.py +++ b/src/redis_release/bht/behaviours.py @@ -26,16 +26,9 @@ from redis_release.bht.state import reset_model_to_defaults from ..github_client_async import GitHubClientAsync -from ..models import ( - HomebrewChannel, - PackageType, - RedisVersion, - ReleaseType, - WorkflowConclusion, - WorkflowStatus, -) +from ..models import RedisVersion, ReleaseType, WorkflowConclusion, WorkflowStatus from .logging_wrapper import PyTreesLoggerWrapper -from .state import HomebrewMeta, Package, PackageMeta, ReleaseMeta, Workflow +from .state import Package, PackageMeta, ReleaseMeta, Workflow logger = logging.getLogger(__name__) @@ -721,273 +714,6 @@ def update(self) -> Status: return Status.SUCCESS -class DockerWorkflowInputs(ReleaseAction): - """ - Docker uses only release_tag input which is set automatically in TriggerWorkflow - """ - - def __init__( - self, - name: str, - workflow: Workflow, - package_meta: PackageMeta, - release_meta: ReleaseMeta, - log_prefix: str = "", - ) -> None: - self.workflow = workflow - self.package_meta = package_meta - self.release_meta = release_meta - super().__init__(name=name, log_prefix=log_prefix) - - def update(self) -> Status: - return Status.SUCCESS - - -class HomewbrewWorkflowInputs(ReleaseAction): - def __init__( - self, - name: str, - workflow: Workflow, - package_meta: HomebrewMeta, - release_meta: ReleaseMeta, - log_prefix: str = "", - ) -> None: - self.workflow = workflow - self.package_meta = package_meta - self.release_meta = release_meta - super().__init__(name=f"{name} - homebrew", log_prefix=log_prefix) - - def update(self) -> Status: - if self.package_meta.release_type is not None: - self.workflow.inputs["release_type"] = self.package_meta.release_type.value - if self.release_meta.tag is not None: - self.workflow.inputs["release_tag"] = self.release_meta.tag - if self.package_meta.homebrew_channel is not None: - self.workflow.inputs["channel"] = self.package_meta.homebrew_channel.value - return Status.SUCCESS - - -class DetectHombrewReleaseAndChannel(ReleaseAction): - def __init__( - self, - name: str, - package_meta: HomebrewMeta, - release_meta: ReleaseMeta, - log_prefix: str = "", - ) -> None: - self.package_meta = package_meta - self.release_meta = release_meta - self.release_version: Optional[RedisVersion] = None - super().__init__(name=name, log_prefix=log_prefix) - - def initialise(self) -> None: - if self.release_meta.tag is None: - self.logger.error("Release tag is not set") - return - if self.release_version is not None: - return - - self.feedback_message = "" - try: - self.release_version = RedisVersion.parse(self.release_meta.tag) - except ValueError as e: - self.logger.error(f"Failed to parse release tag: {e}") - return - - def update(self) -> Status: - if self.release_meta.tag is None: - logger.error("Release tag is not set") - return Status.FAILURE - - if ( - self.package_meta.homebrew_channel is not None - and self.package_meta.release_type is not None - ): - pass - else: - assert self.release_version is not None - if self.package_meta.release_type is None: - if self.release_version.is_internal: - self.package_meta.release_type = ReleaseType.INTERNAL - else: - if self.release_version.is_ga: - self.package_meta.release_type = ReleaseType.PUBLIC - elif self.release_version.is_rc: - self.package_meta.release_type = ReleaseType.PUBLIC - else: - self.package_meta.release_type = ReleaseType.INTERNAL - - if self.package_meta.homebrew_channel is None: - if self.release_version.is_ga: - self.package_meta.homebrew_channel = HomebrewChannel.STABLE - else: - # RC, internal, or any other version goes to RC channel - self.package_meta.homebrew_channel = HomebrewChannel.RC - self.feedback_message = f"release_type: {self.package_meta.release_type.value}, homebrew_channel: {self.package_meta.homebrew_channel.value}" - - if self.log_once( - "homebrew_channel_detected", self.package_meta.ephemeral.log_once_flags - ): - self.logger.info( - f"Hombrew release_type: {self.package_meta.release_type}, homebrew_channel: {self.package_meta.homebrew_channel}" - ) - - return Status.SUCCESS - - -class ClassifyHomebrewVersion(ReleaseAction): - """Classify Homebrew version by downloading and parsing the cask file. - - This behavior downloads the appropriate Homebrew cask file (redis.rb or redis-rc.rb) - based on the homebrew_channel, extracts the version, and compares it with the - release tag version to determine if the version is acceptable. - """ - - def __init__( - self, - name: str, - package_meta: HomebrewMeta, - release_meta: ReleaseMeta, - github_client: GitHubClientAsync, - log_prefix: str = "", - ) -> None: - self.package_meta = package_meta - self.release_meta = release_meta - self.github_client = github_client - self.task: Optional[asyncio.Task] = None - self.release_version: Optional[RedisVersion] = None - self.cask_version: Optional[RedisVersion] = None - super().__init__(name=name, log_prefix=log_prefix) - - def initialise(self) -> None: - """Initialize by validating inputs and starting download task.""" - if self.package_meta.ephemeral.is_version_acceptable is not None: - return - - self.feedback_message = "" - # Validate homebrew_channel is set - if self.package_meta.homebrew_channel is None: - self.logger.error("Homebrew channel is not set") - return - - # Validate repo and ref are set - if not self.package_meta.repo: - self.logger.error("Package repository is not set") - return - - if not self.package_meta.ref: - self.logger.error("Package ref is not set") - return - - # Parse release version from tag - if self.release_meta.tag is None: - self.logger.error("Release tag is not set") - return - - if self.package_meta.release_type is None: - self.logger.error("Package release type is not set") - return - - try: - self.release_version = RedisVersion.parse(self.release_meta.tag) - self.logger.debug(f"Parsed release version: {self.release_version}") - except ValueError as e: - self.logger.error(f"Failed to parse release tag: {e}") - return - - # Determine which cask file to download based on channel - if self.package_meta.homebrew_channel == HomebrewChannel.STABLE: - cask_file = "Casks/redis.rb" - elif self.package_meta.homebrew_channel == HomebrewChannel.RC: - cask_file = "Casks/redis-rc.rb" - else: - self.logger.error( - f"Unknown homebrew channel: {self.package_meta.homebrew_channel}" - ) - return - - self.logger.debug( - f"Downloading cask file: {cask_file} from {self.package_meta.repo}@{self.package_meta.ref}" - ) - - # Start async task to download the cask file from package repo and ref - self.task = asyncio.create_task( - self.github_client.download_file( - self.package_meta.repo, cask_file, self.package_meta.ref - ) - ) - - def update(self) -> Status: - """Process downloaded cask file and classify version.""" - if self.package_meta.ephemeral.is_version_acceptable is not None: - return Status.SUCCESS - - try: - assert self.task is not None - - # Wait for download to complete - if not self.task.done(): - return Status.RUNNING - - # Get the downloaded content - cask_content = self.task.result() - if cask_content is None: - self.logger.error("Failed to download cask file") - return Status.FAILURE - - # Parse version from cask file - # Look for: version "X.Y.Z" - version_match = re.search( - r'^\s*version\s+"([^"]+)"', cask_content, re.MULTILINE - ) - if not version_match: - self.logger.error("Could not find version declaration in cask file") - return Status.FAILURE - - version_str = version_match.group(1) - self.logger.debug(f"Found version in cask file: {version_str}") - - # Parse the cask version - try: - self.cask_version = RedisVersion.parse(version_str) - self.logger.info( - f"Cask version: {self.cask_version}, Release version: {self.release_version}" - ) - except ValueError as e: - self.logger.error(f"Failed to parse cask version '{version_str}': {e}") - return Status.FAILURE - - # Compare versions: cask version >= release version means acceptable - assert self.release_version is not None - self.package_meta.remote_version = str(self.cask_version) - log_prepend = "" - prepend_color = "green" - if self.release_version >= self.cask_version: - self.package_meta.ephemeral.is_version_acceptable = True - self.feedback_message = ( - f"release {self.release_version} >= cask {self.cask_version}" - ) - log_prepend = "Version acceptable: " - else: - self.package_meta.ephemeral.is_version_acceptable = False - log_prepend = "Version NOT acceptable: " - prepend_color = "yellow" - self.feedback_message = ( - f"release {self.release_version} < cask {self.cask_version}" - ) - if self.log_once( - "homebrew_version_classified", - self.package_meta.ephemeral.log_once_flags, - ): - self.logger.info( - f"[{prepend_color}]{log_prepend}{self.feedback_message}[/]" - ) - return Status.SUCCESS - - except Exception as e: - return self.log_exception_and_return_failure(e) - - ### Conditions ### @@ -1181,21 +907,3 @@ def update(self) -> Status: if self.package_meta.ephemeral.force_rebuild: return Status.SUCCESS return Status.FAILURE - - -class NeedToReleaseHomebrew(LoggingAction): - def __init__( - self, - name: str, - package_meta: HomebrewMeta, - release_meta: ReleaseMeta, - log_prefix: str = "", - ) -> None: - self.package_meta = package_meta - self.release_meta = release_meta - super().__init__(name=name, log_prefix=log_prefix) - - def update(self) -> Status: - if self.package_meta.ephemeral.is_version_acceptable is True: - return Status.SUCCESS - return Status.FAILURE diff --git a/src/redis_release/bht/behaviours_docker.py b/src/redis_release/bht/behaviours_docker.py new file mode 100644 index 0000000..5a0a1e4 --- /dev/null +++ b/src/redis_release/bht/behaviours_docker.py @@ -0,0 +1,26 @@ +from py_trees.common import Status + +from redis_release.bht.behaviours import ReleaseAction +from redis_release.bht.state import PackageMeta, ReleaseMeta, Workflow + + +class DockerWorkflowInputs(ReleaseAction): + """ + Docker uses only release_tag input which is set automatically in TriggerWorkflow + """ + + def __init__( + self, + name: str, + workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str = "", + ) -> None: + self.workflow = workflow + self.package_meta = package_meta + self.release_meta = release_meta + super().__init__(name=name, log_prefix=log_prefix) + + def update(self) -> Status: + return Status.SUCCESS diff --git a/src/redis_release/bht/behaviours_homebrew.py b/src/redis_release/bht/behaviours_homebrew.py new file mode 100644 index 0000000..3bcb02f --- /dev/null +++ b/src/redis_release/bht/behaviours_homebrew.py @@ -0,0 +1,273 @@ +import asyncio +import re +from typing import Optional + +from py_trees.common import Status + +from redis_release.bht.behaviours import LoggingAction, ReleaseAction, logger +from redis_release.bht.state import HomebrewMeta, ReleaseMeta, Workflow +from redis_release.github_client_async import GitHubClientAsync +from redis_release.models import HomebrewChannel, RedisVersion, ReleaseType + + +class NeedToReleaseHomebrew(LoggingAction): + def __init__( + self, + name: str, + package_meta: HomebrewMeta, + release_meta: ReleaseMeta, + log_prefix: str = "", + ) -> None: + self.package_meta = package_meta + self.release_meta = release_meta + super().__init__(name=name, log_prefix=log_prefix) + + def update(self) -> Status: + if self.package_meta.ephemeral.is_version_acceptable is True: + return Status.SUCCESS + return Status.FAILURE + + +class HomewbrewWorkflowInputs(ReleaseAction): + def __init__( + self, + name: str, + workflow: Workflow, + package_meta: HomebrewMeta, + release_meta: ReleaseMeta, + log_prefix: str = "", + ) -> None: + self.workflow = workflow + self.package_meta = package_meta + self.release_meta = release_meta + super().__init__(name=f"{name} - homebrew", log_prefix=log_prefix) + + def update(self) -> Status: + if self.package_meta.release_type is not None: + self.workflow.inputs["release_type"] = self.package_meta.release_type.value + if self.release_meta.tag is not None: + self.workflow.inputs["release_tag"] = self.release_meta.tag + if self.package_meta.homebrew_channel is not None: + self.workflow.inputs["channel"] = self.package_meta.homebrew_channel.value + return Status.SUCCESS + + +class DetectHombrewReleaseAndChannel(ReleaseAction): + def __init__( + self, + name: str, + package_meta: HomebrewMeta, + release_meta: ReleaseMeta, + log_prefix: str = "", + ) -> None: + self.package_meta = package_meta + self.release_meta = release_meta + self.release_version: Optional[RedisVersion] = None + super().__init__(name=name, log_prefix=log_prefix) + + def initialise(self) -> None: + if self.release_meta.tag is None: + self.logger.error("Release tag is not set") + return + if self.release_version is not None: + return + + self.feedback_message = "" + try: + self.release_version = RedisVersion.parse(self.release_meta.tag) + except ValueError as e: + self.logger.error(f"Failed to parse release tag: {e}") + return + + def update(self) -> Status: + if self.release_meta.tag is None: + logger.error("Release tag is not set") + return Status.FAILURE + + if ( + self.package_meta.homebrew_channel is not None + and self.package_meta.release_type is not None + ): + return Status.SUCCESS + else: + assert self.release_version is not None + if self.package_meta.release_type is None: + if self.release_version.is_internal: + self.package_meta.release_type = ReleaseType.INTERNAL + else: + if self.release_version.is_ga: + self.package_meta.release_type = ReleaseType.PUBLIC + elif self.release_version.is_rc: + self.package_meta.release_type = ReleaseType.PUBLIC + else: + self.package_meta.release_type = ReleaseType.INTERNAL + + if self.package_meta.homebrew_channel is None: + if self.release_version.is_ga: + self.package_meta.homebrew_channel = HomebrewChannel.STABLE + else: + # RC, internal, or any other version goes to RC channel + self.package_meta.homebrew_channel = HomebrewChannel.RC + self.feedback_message = f"release_type: {self.package_meta.release_type.value}, homebrew_channel: {self.package_meta.homebrew_channel.value}" + + if self.log_once( + "homebrew_channel_detected", self.package_meta.ephemeral.log_once_flags + ): + self.logger.info( + f"Hombrew release_type: {self.package_meta.release_type}, homebrew_channel: {self.package_meta.homebrew_channel}" + ) + + return Status.SUCCESS + + +class ClassifyHomebrewVersion(ReleaseAction): + """Classify Homebrew version by downloading and parsing the cask file. + + This behavior downloads the appropriate Homebrew cask file (redis.rb or redis-rc.rb) + based on the homebrew_channel, extracts the version, and compares it with the + release tag version to determine if the version is acceptable. + """ + + def __init__( + self, + name: str, + package_meta: HomebrewMeta, + release_meta: ReleaseMeta, + github_client: GitHubClientAsync, + log_prefix: str = "", + ) -> None: + self.package_meta = package_meta + self.release_meta = release_meta + self.github_client = github_client + self.task: Optional[asyncio.Task] = None + self.release_version: Optional[RedisVersion] = None + self.cask_version: Optional[RedisVersion] = None + super().__init__(name=name, log_prefix=log_prefix) + + def initialise(self) -> None: + """Initialize by validating inputs and starting download task.""" + if self.package_meta.ephemeral.is_version_acceptable is not None: + return + + self.feedback_message = "" + # Validate homebrew_channel is set + if self.package_meta.homebrew_channel is None: + self.logger.error("Homebrew channel is not set") + return + + # Validate repo and ref are set + if not self.package_meta.repo: + self.logger.error("Package repository is not set") + return + + if not self.package_meta.ref: + self.logger.error("Package ref is not set") + return + + # Parse release version from tag + if self.release_meta.tag is None: + self.logger.error("Release tag is not set") + return + + if self.package_meta.release_type is None: + self.logger.error("Package release type is not set") + return + + try: + self.release_version = RedisVersion.parse(self.release_meta.tag) + self.logger.debug(f"Parsed release version: {self.release_version}") + except ValueError as e: + self.logger.error(f"Failed to parse release tag: {e}") + return + + # Determine which cask file to download based on channel + if self.package_meta.homebrew_channel == HomebrewChannel.STABLE: + cask_file = "Casks/redis.rb" + elif self.package_meta.homebrew_channel == HomebrewChannel.RC: + cask_file = "Casks/redis-rc.rb" + else: + self.logger.error( + f"Unknown homebrew channel: {self.package_meta.homebrew_channel}" + ) + return + + self.logger.debug( + f"Downloading cask file: {cask_file} from {self.package_meta.repo}@{self.package_meta.ref}" + ) + + # Start async task to download the cask file from package repo and ref + self.task = asyncio.create_task( + self.github_client.download_file( + self.package_meta.repo, cask_file, self.package_meta.ref + ) + ) + + def update(self) -> Status: + """Process downloaded cask file and classify version.""" + if self.package_meta.ephemeral.is_version_acceptable is not None: + return Status.SUCCESS + + try: + assert self.task is not None + + # Wait for download to complete + if not self.task.done(): + return Status.RUNNING + + # Get the downloaded content + cask_content = self.task.result() + if cask_content is None: + self.logger.error("Failed to download cask file") + return Status.FAILURE + + # Parse version from cask file + # Look for: version "X.Y.Z" + version_match = re.search( + r'^\s*version\s+"([^"]+)"', cask_content, re.MULTILINE + ) + if not version_match: + self.logger.error("Could not find version declaration in cask file") + return Status.FAILURE + + version_str = version_match.group(1) + self.logger.debug(f"Found version in cask file: {version_str}") + + # Parse the cask version + try: + self.cask_version = RedisVersion.parse(version_str) + self.logger.info( + f"Cask version: {self.cask_version}, Release version: {self.release_version}" + ) + except ValueError as e: + self.logger.error(f"Failed to parse cask version '{version_str}': {e}") + return Status.FAILURE + + # Compare versions: cask version >= release version means acceptable + assert self.release_version is not None + self.package_meta.remote_version = str(self.cask_version) + log_prepend = "" + prepend_color = "green" + if self.release_version >= self.cask_version: + self.package_meta.ephemeral.is_version_acceptable = True + self.feedback_message = ( + f"release {self.release_version} >= cask {self.cask_version}" + ) + log_prepend = "Version acceptable: " + else: + self.package_meta.ephemeral.is_version_acceptable = False + log_prepend = "Version NOT acceptable: " + prepend_color = "yellow" + self.feedback_message = ( + f"release {self.release_version} < cask {self.cask_version}" + ) + if self.log_once( + "homebrew_version_classified", + self.package_meta.ephemeral.log_once_flags, + ): + self.logger.info( + f"[{prepend_color}]{log_prepend}{self.feedback_message}[/]" + ) + return Status.SUCCESS + + except Exception as e: + return self.log_exception_and_return_failure(e) diff --git a/src/redis_release/bht/behaviours_snap.py b/src/redis_release/bht/behaviours_snap.py new file mode 100644 index 0000000..0de899a --- /dev/null +++ b/src/redis_release/bht/behaviours_snap.py @@ -0,0 +1,281 @@ +import asyncio +import json +from typing import Optional + +from py_trees.common import Status + +from redis_release.bht.behaviours import LoggingAction, ReleaseAction, logger +from redis_release.bht.state import ReleaseMeta, SnapMeta, Workflow +from redis_release.github_client_async import GitHubClientAsync +from redis_release.models import RedisVersion, ReleaseType, SnapRiskLevel + + +class SnapWorkflowInputs(ReleaseAction): + def __init__( + self, + name: str, + workflow: Workflow, + package_meta: SnapMeta, + release_meta: ReleaseMeta, + log_prefix: str = "", + ) -> None: + self.workflow = workflow + self.package_meta = package_meta + self.release_meta = release_meta + super().__init__(name=f"{name} - snap", log_prefix=log_prefix) + + def update(self) -> Status: + if self.package_meta.release_type is not None: + self.workflow.inputs["release_type"] = self.package_meta.release_type.value + if self.release_meta.tag is not None: + self.workflow.inputs["release_tag"] = self.release_meta.tag + if self.package_meta.snap_risk_level is not None: + self.workflow.inputs["risk_level"] = self.package_meta.snap_risk_level.value + return Status.SUCCESS + + +class DetectSnapReleaseAndRiskLevel(ReleaseAction): + """Detect Snap release type and risk level based on release version. + + Logic: + - is_internal: sets release_type to INTERNAL, risk_level to CANDIDATE + - is_ga: sets release_type to PUBLIC, risk_level to STABLE + - is_rc: sets release_type to PUBLIC, risk_level to CANDIDATE + """ + + def __init__( + self, + name: str, + package_meta: SnapMeta, + release_meta: ReleaseMeta, + log_prefix: str = "", + ) -> None: + self.package_meta = package_meta + self.release_meta = release_meta + self.release_version: Optional[RedisVersion] = None + super().__init__(name=name, log_prefix=log_prefix) + + def initialise(self) -> None: + if self.release_meta.tag is None: + self.logger.error("Release tag is not set") + return + if self.release_version is not None: + return + + self.feedback_message = "" + try: + self.release_version = RedisVersion.parse(self.release_meta.tag) + except ValueError as e: + self.logger.error(f"Failed to parse release tag: {e}") + return + + def update(self) -> Status: + if self.release_meta.tag is None: + logger.error("Release tag is not set") + return Status.FAILURE + + if ( + self.package_meta.snap_risk_level is not None + and self.package_meta.release_type is not None + ): + return Status.SUCCESS + else: + assert self.release_version is not None + if self.package_meta.release_type is None: + if self.release_version.is_internal: + self.package_meta.release_type = ReleaseType.INTERNAL + self.package_meta.snap_risk_level = SnapRiskLevel.CANDIDATE + else: + self.package_meta.release_type = ReleaseType.PUBLIC + + if self.package_meta.snap_risk_level is None: + if self.release_version.is_ga: + self.package_meta.snap_risk_level = SnapRiskLevel.STABLE + else: + # other versions go to CANDIDATE + self.package_meta.snap_risk_level = SnapRiskLevel.CANDIDATE + + self.feedback_message = f"release_type: {self.package_meta.release_type.value}, snap_risk_level: {self.package_meta.snap_risk_level.value}" + + if self.log_once( + "snap_risk_level_detected", self.package_meta.ephemeral.log_once_flags + ): + self.logger.info( + f"Snap release_type: {self.package_meta.release_type}, snap_risk_level: {self.package_meta.snap_risk_level}" + ) + + return Status.SUCCESS + + +class ClassifySnapVersion(ReleaseAction): + """Classify Snap version by downloading and parsing .redis_versions.json file. + + This behavior downloads the .redis_versions.json file from the snap repository, + extracts the version for the appropriate risk level (stable/candidate/edge), + and compares it with the release tag version to determine if the version is acceptable. + + The version is acceptable if: release_version >= remote_version for the risk level. + """ + + def __init__( + self, + name: str, + package_meta: SnapMeta, + release_meta: ReleaseMeta, + github_client: GitHubClientAsync, + log_prefix: str = "", + ) -> None: + self.package_meta = package_meta + self.release_meta = release_meta + self.github_client = github_client + self.task: Optional[asyncio.Task] = None + self.release_version: Optional[RedisVersion] = None + self.remote_version: Optional[RedisVersion] = None + super().__init__(name=name, log_prefix=log_prefix) + + def initialise(self) -> None: + """Initialize by validating inputs and starting download task.""" + if self.package_meta.ephemeral.is_version_acceptable is not None: + return + + self.feedback_message = "" + # Validate snap_risk_level is set + if self.package_meta.snap_risk_level is None: + self.logger.error("Snap risk level is not set") + return + + # Validate repo and ref are set + if not self.package_meta.repo: + self.logger.error("Package repository is not set") + return + + if not self.package_meta.ref: + self.logger.error("Package ref is not set") + return + + # Parse release version from tag + if self.release_meta.tag is None: + self.logger.error("Release tag is not set") + return + + if self.package_meta.release_type is None: + self.logger.error("Package release type is not set") + return + + try: + self.release_version = RedisVersion.parse(self.release_meta.tag) + self.logger.debug(f"Parsed release version: {self.release_version}") + except ValueError as e: + self.logger.error(f"Failed to parse release tag: {e}") + return + + # Download .redis_versions.json file + versions_file = ".redis_versions.json" + self.logger.debug( + f"Downloading versions file: {versions_file} from {self.package_meta.repo}@{self.package_meta.ref}" + ) + + # Start async task to download the versions file from package repo and ref + self.task = asyncio.create_task( + self.github_client.download_file( + self.package_meta.repo, versions_file, self.package_meta.ref + ) + ) + + def update(self) -> Status: + """Process downloaded versions file and classify version.""" + if self.package_meta.ephemeral.is_version_acceptable is not None: + return Status.SUCCESS + + try: + assert self.task is not None + + # Wait for download to complete + if not self.task.done(): + return Status.RUNNING + + # Get the downloaded content + versions_content = self.task.result() + if versions_content is None: + self.logger.error("Failed to download .redis_versions.json file") + return Status.FAILURE + + # Parse JSON content + try: + versions_data = json.loads(versions_content) + except json.JSONDecodeError as e: + self.logger.error(f"Failed to parse .redis_versions.json: {e}") + return Status.FAILURE + + # Get version for the current risk level + assert self.package_meta.snap_risk_level is not None + risk_level_key = self.package_meta.snap_risk_level.value + if risk_level_key not in versions_data: + self.logger.error( + f"Risk level '{risk_level_key}' not found in .redis_versions.json" + ) + return Status.FAILURE + + version_str = versions_data[risk_level_key] + self.logger.debug( + f"Found version for {risk_level_key} risk level: {version_str}" + ) + + try: + self.remote_version = RedisVersion.parse(version_str) + self.logger.info( + f"Remote version: {self.remote_version}, Release version: {self.release_version}" + ) + except ValueError as e: + self.logger.error( + f"Failed to parse remote version '{version_str}': {e}" + ) + return Status.FAILURE + + # Compare versions: release version >= remote version means acceptable + assert self.release_version is not None + self.package_meta.remote_version = str(self.remote_version) + log_prepend = "" + prepend_color = "green" + if self.release_version >= self.remote_version: + self.package_meta.ephemeral.is_version_acceptable = True + self.feedback_message = ( + f"release {self.release_version} >= remote {self.remote_version}" + ) + log_prepend = "Version acceptable: " + else: + self.package_meta.ephemeral.is_version_acceptable = False + log_prepend = "Version NOT acceptable: " + prepend_color = "yellow" + self.feedback_message = ( + f"release {self.release_version} < remote {self.remote_version}" + ) + if self.log_once( + "snap_version_classified", + self.package_meta.ephemeral.log_once_flags, + ): + self.logger.info( + f"[{prepend_color}]{log_prepend}{self.feedback_message}[/]" + ) + return Status.SUCCESS + + except Exception as e: + return self.log_exception_and_return_failure(e) + + +class NeedToReleaseSnap(LoggingAction): + def __init__( + self, + name: str, + package_meta: SnapMeta, + release_meta: ReleaseMeta, + log_prefix: str = "", + ) -> None: + self.package_meta = package_meta + self.release_meta = release_meta + super().__init__(name=name, log_prefix=log_prefix) + + def update(self) -> Status: + if self.package_meta.ephemeral.is_version_acceptable is True: + return Status.SUCCESS + return Status.FAILURE diff --git a/src/redis_release/bht/composites.py b/src/redis_release/bht/composites.py index 327c4dc..393239b 100644 --- a/src/redis_release/bht/composites.py +++ b/src/redis_release/bht/composites.py @@ -21,7 +21,6 @@ from ..github_client_async import GitHubClientAsync from .behaviours import ( - ClassifyHomebrewVersion, ExtractArtifactResult, GetWorkflowArtifactsList, IdentifyTargetRef, @@ -32,8 +31,10 @@ ) from .behaviours import TriggerWorkflow as TriggerWorkflow from .behaviours import UpdateWorkflowStatusUntilCompletion +from .behaviours_homebrew import ClassifyHomebrewVersion +from .behaviours_snap import ClassifySnapVersion from .decorators import ConditionGuard, FlagGuard, StatusFlagGuard -from .state import HomebrewMeta, Package, PackageMeta, ReleaseMeta, Workflow +from .state import HomebrewMeta, Package, PackageMeta, ReleaseMeta, SnapMeta, Workflow class ParallelBarrier(Composite): @@ -418,3 +419,27 @@ def __init__( "classify_remote_versions", log_prefix=log_prefix, ) + + +class ClassifySnapVersionGuarded(StatusFlagGuard): + def __init__( + self, + name: str, + package_meta: SnapMeta, + release_meta: ReleaseMeta, + github_client: GitHubClientAsync, + log_prefix: str = "", + ) -> None: + super().__init__( + None if name == "" else name, + ClassifySnapVersion( + "Classify Snap Version", + package_meta, + release_meta, + github_client, + log_prefix=log_prefix, + ), + package_meta.ephemeral, + "classify_remote_versions", + log_prefix=log_prefix, + ) diff --git a/src/redis_release/bht/tree.py b/src/redis_release/bht/tree.py index b501925..680e89f 100644 --- a/src/redis_release/bht/tree.py +++ b/src/redis_release/bht/tree.py @@ -342,7 +342,9 @@ def create_by_name(self, name: str) -> Union[Selector, Sequence, Behaviour]: package, release_meta, package, github_client, "" ) elif name == "package_release_branch": - return get_factory(self.package_type).create_package_release_tree_branch( + return get_factory( + self.package_type + ).create_package_release_execute_workflows_tree_branch( package, release_meta, package, github_client, "" ) elif name == "package_release_goal_branch": diff --git a/src/redis_release/bht/tree_factory.py b/src/redis_release/bht/tree_factory.py index 3ef6c1b..9b1e9c8 100644 --- a/src/redis_release/bht/tree_factory.py +++ b/src/redis_release/bht/tree_factory.py @@ -4,7 +4,7 @@ import logging from abc import ABC -from typing import Dict, List, Optional, Union, cast +from typing import Dict, List, Optional, Protocol, Union, cast from py_trees.behaviour import Behaviour from py_trees.behaviours import Failure as AlwaysFailure @@ -14,16 +14,21 @@ from ..github_client_async import GitHubClientAsync from ..models import PackageType from .backchain import create_PPA, latch_chains -from .behaviours import ( +from .behaviours import GenericWorkflowInputs, NeedToPublishRelease +from .behaviours_docker import DockerWorkflowInputs +from .behaviours_homebrew import ( DetectHombrewReleaseAndChannel, - DockerWorkflowInputs, - GenericWorkflowInputs, HomewbrewWorkflowInputs, - NeedToPublishRelease, NeedToReleaseHomebrew, ) +from .behaviours_snap import ( + DetectSnapReleaseAndRiskLevel, + NeedToReleaseSnap, + SnapWorkflowInputs, +) from .composites import ( ClassifyHomebrewVersionGuarded, + ClassifySnapVersionGuarded, ResetPackageStateGuarded, RestartPackageGuarded, RestartWorkflowGuarded, @@ -44,12 +49,103 @@ PackageMeta, ReleaseMeta, ReleaseState, + SnapMeta, Workflow, ) logger = logging.getLogger(__name__) +class GenericPackageFactoryProtocol(Protocol): + """Protocol defining the interface for package-specific tree factories.""" + + def create_package_release_goal_tree_branch( + self, + package: Package, + release_meta: ReleaseMeta, + default_package: Package, + github_client: GitHubClientAsync, + package_name: str, + ) -> Union[Selector, Sequence]: ... + + def create_build_workflow_inputs( + self, + name: str, + workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: ... + + def create_publish_workflow_inputs( + self, + name: str, + workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: ... + + def create_workflow_complete_tree_branch( + self, + workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + github_client: GitHubClientAsync, + log_prefix: str, + trigger_preconditions: Optional[List[Union[Sequence, Selector]]] = None, + ) -> Union[Selector, Sequence]: ... + + def create_package_release_execute_workflows_tree_branch( + self, + package: Package, + release_meta: ReleaseMeta, + default_package: Package, + github_client: GitHubClientAsync, + package_name: str, + ) -> Union[Selector, Sequence]: ... + + def create_build_workflow_tree_branch( + self, + package: Package, + release_meta: ReleaseMeta, + default_package: Package, + github_client: GitHubClientAsync, + package_name: str, + ) -> Union[Selector, Sequence]: ... + + def create_publish_workflow_tree_branch( + self, + build_workflow: Workflow, + publish_workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + default_publish_workflow: Workflow, + github_client: GitHubClientAsync, + package_name: str, + ) -> Union[Selector, Sequence]: ... + + def create_workflow_with_result_tree_branch( + self, + artifact_name: str, + workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + github_client: GitHubClientAsync, + package_name: str, + trigger_preconditions: Optional[List[Union[Sequence, Selector]]] = None, + ) -> Union[Selector, Sequence]: ... + + def create_extract_result_tree_branch( + self, + artifact_name: str, + workflow: Workflow, + package_meta: PackageMeta, + github_client: GitHubClientAsync, + log_prefix: str, + ) -> Union[Selector, Sequence]: ... + + class GenericPackageFactory(ABC): """Default factory for packages without specific customizations.""" @@ -61,7 +157,7 @@ def create_package_release_goal_tree_branch( github_client: GitHubClientAsync, package_name: str, ) -> Union[Selector, Sequence]: - package_release = self.create_package_release_tree_branch( + package_release = self.create_package_release_execute_workflows_tree_branch( package, release_meta, default_package, github_client, package_name ) return Selector( @@ -149,7 +245,7 @@ def create_workflow_complete_tree_branch( ) return workflow_complete - def create_package_release_tree_branch( + def create_package_release_execute_workflows_tree_branch( self, package: Package, release_meta: ReleaseMeta, @@ -386,59 +482,13 @@ class RPMFactory(GenericPackageFactory): pass -class HomebrewFactory(GenericPackageFactory): - def create_package_release_goal_tree_branch( - self, - package: Package, - release_meta: ReleaseMeta, - default_package: Package, - github_client: GitHubClientAsync, - package_name: str, - ) -> Union[Selector, Sequence]: - package_release = self.create_package_release_tree_branch( - package, release_meta, default_package, github_client, package_name - ) - need_to_release = NeedToReleaseHomebrew( - "Need To Release?", - cast(HomebrewMeta, package.meta), - release_meta, - log_prefix=package_name, - ) - release_goal = Selector( - f"Release Workflows {package_name} Goal", - memory=False, - children=[Inverter("Not", need_to_release), package_release], - ) - reset_package_state = ResetPackageStateGuarded( - "", - package, - default_package, - log_prefix=package_name, - ) - return Sequence( - f"Release {package_name}", - memory=False, - children=[ - reset_package_state, - DetectHombrewReleaseAndChannel( - "Detect Homebrew Channel", - cast(HomebrewMeta, package.meta), - release_meta, - log_prefix=package_name, - ), - ClassifyHomebrewVersionGuarded( - "", - cast(HomebrewMeta, package.meta), - release_meta, - github_client, - log_prefix=package_name, - ), - release_goal, - ], - ) +class PackageWithValidation: + """ + Mixin class for packages that have validation step before release, e.g. Homebrew and Snap + """ - def create_package_release_tree_branch( - self, + def create_package_release_execute_workflows_tree_branch( + self: GenericPackageFactoryProtocol, package: Package, release_meta: ReleaseMeta, default_package: Package, @@ -471,7 +521,7 @@ def create_package_release_tree_branch( return package_release def create_workflow_complete_tree_branch( - self, + self: GenericPackageFactoryProtocol, workflow: Workflow, package_meta: PackageMeta, release_meta: ReleaseMeta, @@ -513,6 +563,58 @@ def create_workflow_complete_tree_branch( ) return workflow_complete + +class HomebrewFactory(GenericPackageFactory, PackageWithValidation): + def create_package_release_goal_tree_branch( + self, + package: Package, + release_meta: ReleaseMeta, + default_package: Package, + github_client: GitHubClientAsync, + package_name: str, + ) -> Union[Selector, Sequence]: + package_release = self.create_package_release_execute_workflows_tree_branch( + package, release_meta, default_package, github_client, package_name + ) + need_to_release = NeedToReleaseHomebrew( + "Need To Release?", + cast(HomebrewMeta, package.meta), + release_meta, + log_prefix=package_name, + ) + release_goal = Selector( + f"Release Workflows {package_name} Goal", + memory=False, + children=[Inverter("Not", need_to_release), package_release], + ) + reset_package_state = ResetPackageStateGuarded( + "", + package, + default_package, + log_prefix=package_name, + ) + return Sequence( + f"Release {package_name}", + memory=False, + children=[ + reset_package_state, + DetectHombrewReleaseAndChannel( + "Detect Homebrew Channel", + cast(HomebrewMeta, package.meta), + release_meta, + log_prefix=package_name, + ), + ClassifyHomebrewVersionGuarded( + "", + cast(HomebrewMeta, package.meta), + release_meta, + github_client, + log_prefix=package_name, + ), + release_goal, + ], + ) + def create_build_workflow_inputs( self, name: str, @@ -529,6 +631,7 @@ def create_build_workflow_inputs( release_meta, log_prefix=log_prefix, ) + def create_publish_workflow_inputs( self, name: str, @@ -547,6 +650,91 @@ def create_publish_workflow_inputs( ) +class SnapFactory(GenericPackageFactory, PackageWithValidation): + def create_package_release_goal_tree_branch( + self, + package: Package, + release_meta: ReleaseMeta, + default_package: Package, + github_client: GitHubClientAsync, + package_name: str, + ) -> Union[Selector, Sequence]: + package_release = self.create_package_release_execute_workflows_tree_branch( + package, release_meta, default_package, github_client, package_name + ) + need_to_release = NeedToReleaseSnap( + "Need To Release?", + cast(SnapMeta, package.meta), + release_meta, + log_prefix=package_name, + ) + release_goal = Selector( + f"Release Workflows {package_name} Goal", + memory=False, + children=[Inverter("Not", need_to_release), package_release], + ) + reset_package_state = ResetPackageStateGuarded( + "", + package, + default_package, + log_prefix=package_name, + ) + return Sequence( + f"Release {package_name}", + memory=False, + children=[ + reset_package_state, + DetectSnapReleaseAndRiskLevel( + "Detect Homebrew Channel", + cast(SnapMeta, package.meta), + release_meta, + log_prefix=package_name, + ), + ClassifySnapVersionGuarded( + "", + cast(SnapMeta, package.meta), + release_meta, + github_client, + log_prefix=package_name, + ), + release_goal, + ], + ) + + def create_build_workflow_inputs( + self, + name: str, + workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: + + return SnapWorkflowInputs( + name, + workflow, + cast(SnapMeta, package_meta), + release_meta, + log_prefix=log_prefix, + ) + + def create_publish_workflow_inputs( + self, + name: str, + workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: + + return SnapWorkflowInputs( + name, + workflow, + cast(SnapMeta, package_meta), + release_meta, + log_prefix=log_prefix, + ) + # Factory registry _FACTORIES: Dict[PackageType, GenericPackageFactory] = { @@ -554,6 +742,7 @@ def create_publish_workflow_inputs( PackageType.DEBIAN: DebianFactory(), PackageType.RPM: RPMFactory(), PackageType.HOMEBREW: HomebrewFactory(), + PackageType.SNAP: SnapFactory(), } _DEFAULT_FACTORY = GenericPackageFactory() From 141e6fc931310c85b7dd4e3af12e1200798dc5a9 Mon Sep 17 00:00:00 2001 From: Petar Shtuchkin Date: Tue, 25 Nov 2025 17:02:35 +0200 Subject: [PATCH 02/13] Change snap ref to master instead of main --- config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.yaml b/config.yaml index b4e4107..41ee497 100644 --- a/config.yaml +++ b/config.yaml @@ -56,7 +56,7 @@ packages: package_type: snap repo: redislabsdev/redis-snap # homebrew has one fixed release branch: main - ref: main + ref: master build_workflow: release_build_and_test.yml build_inputs: {} publish_internal_release: yes From ba5a7d64a0d126384434b5c8d650bf8d4771dc7e Mon Sep 17 00:00:00 2001 From: Petar Shtuchkin Date: Tue, 25 Nov 2025 19:23:22 +0200 Subject: [PATCH 03/13] Moving around the code --- src/redis_release/bht/behaviours_debian.py | 0 src/redis_release/bht/behaviours_rpm.py | 0 src/redis_release/bht/tree.py | 4 +- src/redis_release/bht/tree_factory.py | 733 +----------------- src/redis_release/bht/tree_factory_debian.py | 5 + src/redis_release/bht/tree_factory_docker.py | 33 + src/redis_release/bht/tree_factory_generic.py | 410 ++++++++++ .../bht/tree_factory_homebrew.py | 113 +++ .../bht/tree_factory_protocol.py | 97 +++ src/redis_release/bht/tree_factory_rpm.py | 5 + src/redis_release/bht/tree_factory_snap.py | 113 +++ 11 files changed, 785 insertions(+), 728 deletions(-) create mode 100644 src/redis_release/bht/behaviours_debian.py create mode 100644 src/redis_release/bht/behaviours_rpm.py create mode 100644 src/redis_release/bht/tree_factory_debian.py create mode 100644 src/redis_release/bht/tree_factory_docker.py create mode 100644 src/redis_release/bht/tree_factory_generic.py create mode 100644 src/redis_release/bht/tree_factory_homebrew.py create mode 100644 src/redis_release/bht/tree_factory_protocol.py create mode 100644 src/redis_release/bht/tree_factory_rpm.py create mode 100644 src/redis_release/bht/tree_factory_snap.py diff --git a/src/redis_release/bht/behaviours_debian.py b/src/redis_release/bht/behaviours_debian.py new file mode 100644 index 0000000..e69de29 diff --git a/src/redis_release/bht/behaviours_rpm.py b/src/redis_release/bht/behaviours_rpm.py new file mode 100644 index 0000000..e69de29 diff --git a/src/redis_release/bht/tree.py b/src/redis_release/bht/tree.py index 680e89f..5b14130 100644 --- a/src/redis_release/bht/tree.py +++ b/src/redis_release/bht/tree.py @@ -1,6 +1,6 @@ """ -This module contains tree initialization, larger branches creation -and utility functions to run or inspect the tree. +This module contains tree initialization and utility functions to run or inspect +the tree. """ import asyncio diff --git a/src/redis_release/bht/tree_factory.py b/src/redis_release/bht/tree_factory.py index 9b1e9c8..fbffced 100644 --- a/src/redis_release/bht/tree_factory.py +++ b/src/redis_release/bht/tree_factory.py @@ -3,739 +3,20 @@ """ import logging -from abc import ABC -from typing import Dict, List, Optional, Protocol, Union, cast +from typing import Dict, Optional, Union -from py_trees.behaviour import Behaviour -from py_trees.behaviours import Failure as AlwaysFailure -from py_trees.composites import Selector, Sequence -from py_trees.decorators import Inverter +from redis_release.bht.tree_factory_debian import DebianFactory +from redis_release.bht.tree_factory_docker import DockerFactory +from redis_release.bht.tree_factory_generic import GenericPackageFactory +from redis_release.bht.tree_factory_homebrew import HomebrewFactory +from redis_release.bht.tree_factory_rpm import RPMFactory +from redis_release.bht.tree_factory_snap import SnapFactory -from ..github_client_async import GitHubClientAsync from ..models import PackageType -from .backchain import create_PPA, latch_chains -from .behaviours import GenericWorkflowInputs, NeedToPublishRelease -from .behaviours_docker import DockerWorkflowInputs -from .behaviours_homebrew import ( - DetectHombrewReleaseAndChannel, - HomewbrewWorkflowInputs, - NeedToReleaseHomebrew, -) -from .behaviours_snap import ( - DetectSnapReleaseAndRiskLevel, - NeedToReleaseSnap, - SnapWorkflowInputs, -) -from .composites import ( - ClassifyHomebrewVersionGuarded, - ClassifySnapVersionGuarded, - ResetPackageStateGuarded, - RestartPackageGuarded, - RestartWorkflowGuarded, -) -from .ppas import ( - create_attach_release_handle_ppa, - create_detect_release_type_ppa, - create_download_artifacts_ppa, - create_extract_artifact_result_ppa, - create_find_workflow_by_uuid_ppa, - create_identify_target_ref_ppa, - create_trigger_workflow_ppa, - create_workflow_completion_ppa, -) -from .state import ( - HomebrewMeta, - Package, - PackageMeta, - ReleaseMeta, - ReleaseState, - SnapMeta, - Workflow, -) logger = logging.getLogger(__name__) -class GenericPackageFactoryProtocol(Protocol): - """Protocol defining the interface for package-specific tree factories.""" - - def create_package_release_goal_tree_branch( - self, - package: Package, - release_meta: ReleaseMeta, - default_package: Package, - github_client: GitHubClientAsync, - package_name: str, - ) -> Union[Selector, Sequence]: ... - - def create_build_workflow_inputs( - self, - name: str, - workflow: Workflow, - package_meta: PackageMeta, - release_meta: ReleaseMeta, - log_prefix: str, - ) -> Behaviour: ... - - def create_publish_workflow_inputs( - self, - name: str, - workflow: Workflow, - package_meta: PackageMeta, - release_meta: ReleaseMeta, - log_prefix: str, - ) -> Behaviour: ... - - def create_workflow_complete_tree_branch( - self, - workflow: Workflow, - package_meta: PackageMeta, - release_meta: ReleaseMeta, - github_client: GitHubClientAsync, - log_prefix: str, - trigger_preconditions: Optional[List[Union[Sequence, Selector]]] = None, - ) -> Union[Selector, Sequence]: ... - - def create_package_release_execute_workflows_tree_branch( - self, - package: Package, - release_meta: ReleaseMeta, - default_package: Package, - github_client: GitHubClientAsync, - package_name: str, - ) -> Union[Selector, Sequence]: ... - - def create_build_workflow_tree_branch( - self, - package: Package, - release_meta: ReleaseMeta, - default_package: Package, - github_client: GitHubClientAsync, - package_name: str, - ) -> Union[Selector, Sequence]: ... - - def create_publish_workflow_tree_branch( - self, - build_workflow: Workflow, - publish_workflow: Workflow, - package_meta: PackageMeta, - release_meta: ReleaseMeta, - default_publish_workflow: Workflow, - github_client: GitHubClientAsync, - package_name: str, - ) -> Union[Selector, Sequence]: ... - - def create_workflow_with_result_tree_branch( - self, - artifact_name: str, - workflow: Workflow, - package_meta: PackageMeta, - release_meta: ReleaseMeta, - github_client: GitHubClientAsync, - package_name: str, - trigger_preconditions: Optional[List[Union[Sequence, Selector]]] = None, - ) -> Union[Selector, Sequence]: ... - - def create_extract_result_tree_branch( - self, - artifact_name: str, - workflow: Workflow, - package_meta: PackageMeta, - github_client: GitHubClientAsync, - log_prefix: str, - ) -> Union[Selector, Sequence]: ... - - -class GenericPackageFactory(ABC): - """Default factory for packages without specific customizations.""" - - def create_package_release_goal_tree_branch( - self, - package: Package, - release_meta: ReleaseMeta, - default_package: Package, - github_client: GitHubClientAsync, - package_name: str, - ) -> Union[Selector, Sequence]: - package_release = self.create_package_release_execute_workflows_tree_branch( - package, release_meta, default_package, github_client, package_name - ) - return Selector( - f"Package Release {package_name} Goal", - memory=False, - children=[AlwaysFailure("Yes"), package_release], - ) - - def create_build_workflow_inputs( - self, - name: str, - workflow: Workflow, - package_meta: PackageMeta, - release_meta: ReleaseMeta, - log_prefix: str, - ) -> Behaviour: - return GenericWorkflowInputs( - name, workflow, package_meta, release_meta, log_prefix=log_prefix - ) - - def create_publish_workflow_inputs( - self, - name: str, - workflow: Workflow, - package_meta: PackageMeta, - release_meta: ReleaseMeta, - log_prefix: str, - ) -> Behaviour: - return GenericWorkflowInputs( - name, workflow, package_meta, release_meta, log_prefix=log_prefix - ) - - def create_workflow_complete_tree_branch( - self, - workflow: Workflow, - package_meta: PackageMeta, - release_meta: ReleaseMeta, - github_client: GitHubClientAsync, - log_prefix: str, - trigger_preconditions: Optional[List[Union[Sequence, Selector]]] = None, - ) -> Union[Selector, Sequence]: - """ - - Args: - trigger_preconditions: List of preconditions to add to the workflow trigger - """ - workflow_complete = create_workflow_completion_ppa( - workflow, - package_meta, - github_client, - log_prefix, - ) - find_workflow_by_uud = create_find_workflow_by_uuid_ppa( - workflow, - package_meta, - github_client, - log_prefix, - ) - trigger_workflow = create_trigger_workflow_ppa( - workflow, - package_meta, - release_meta, - github_client, - log_prefix, - ) - if trigger_preconditions: - latch_chains(trigger_workflow, *trigger_preconditions) - identify_target_ref = create_identify_target_ref_ppa( - package_meta, - release_meta, - github_client, - log_prefix, - ) - detect_release_type = create_detect_release_type_ppa( - package_meta, - release_meta, - log_prefix, - ) - latch_chains( - workflow_complete, - find_workflow_by_uud, - trigger_workflow, - identify_target_ref, - detect_release_type, - ) - return workflow_complete - - def create_package_release_execute_workflows_tree_branch( - self, - package: Package, - release_meta: ReleaseMeta, - default_package: Package, - github_client: GitHubClientAsync, - package_name: str, - ) -> Union[Selector, Sequence]: - build = self.create_build_workflow_tree_branch( - package, - release_meta, - default_package, - github_client, - package_name, - ) - build.name = f"Build {package_name}" - publish = self.create_publish_workflow_tree_branch( - package.build, - package.publish, - package.meta, - release_meta, - default_package.publish, - github_client, - package_name, - ) - reset_package_state = ResetPackageStateGuarded( - "", - package, - default_package, - log_prefix=package_name, - ) - publish.name = f"Publish {package_name}" - package_release = Sequence( - f"Package Release {package_name}", - memory=False, - children=[reset_package_state, build, publish], - ) - return package_release - - def create_build_workflow_tree_branch( - self, - package: Package, - release_meta: ReleaseMeta, - default_package: Package, - github_client: GitHubClientAsync, - package_name: str, - ) -> Union[Selector, Sequence]: - - build_workflow_args = create_PPA( - "Set Build Workflow Inputs", - self.create_build_workflow_inputs( - "Set Build Workflow Inputs", - package.build, - package.meta, - release_meta, - log_prefix=f"{package_name}.build", - ), - ) - - build_workflow = self.create_workflow_with_result_tree_branch( - "release_handle", - package.build, - package.meta, - release_meta, - github_client, - f"{package_name}.build", - trigger_preconditions=[build_workflow_args], - ) - assert isinstance(build_workflow, Selector) - - reset_package_state = RestartPackageGuarded( - "BuildRestartCondition", - package, - package.build, - default_package, - log_prefix=f"{package_name}.build", - ) - build_workflow.add_child(reset_package_state) - - return build_workflow - - def create_publish_workflow_tree_branch( - self, - build_workflow: Workflow, - publish_workflow: Workflow, - package_meta: PackageMeta, - release_meta: ReleaseMeta, - default_publish_workflow: Workflow, - github_client: GitHubClientAsync, - package_name: str, - ) -> Union[Selector, Sequence]: - attach_release_handle = create_attach_release_handle_ppa( - build_workflow, publish_workflow, log_prefix=f"{package_name}.publish" - ) - publish_workflow_args = create_PPA( - "Set Publish Workflow Inputs", - self.create_publish_workflow_inputs( - "Set Publish Workflow Inputs", - publish_workflow, - package_meta, - release_meta, - log_prefix=f"{package_name}.publish", - ), - ) - workflow_result = self.create_workflow_with_result_tree_branch( - "release_info", - publish_workflow, - package_meta, - release_meta, - github_client, - f"{package_name}.publish", - trigger_preconditions=[publish_workflow_args, attach_release_handle], - ) - not_need_to_publish = Inverter( - "Not", - NeedToPublishRelease( - "Need To Publish?", - package_meta, - release_meta, - log_prefix=f"{package_name}.publish", - ), - ) - reset_publish_workflow_state = RestartWorkflowGuarded( - "PublishRestartCondition", - publish_workflow, - package_meta, - default_publish_workflow, - log_prefix=f"{package_name}.publish", - ) - return Selector( - "Publish", - memory=False, - children=[ - not_need_to_publish, - workflow_result, - reset_publish_workflow_state, - ], - ) - - def create_workflow_with_result_tree_branch( - self, - artifact_name: str, - workflow: Workflow, - package_meta: PackageMeta, - release_meta: ReleaseMeta, - github_client: GitHubClientAsync, - package_name: str, - trigger_preconditions: Optional[List[Union[Sequence, Selector]]] = None, - ) -> Union[Selector, Sequence]: - """ - Creates a workflow process that succedes when the workflow - is successful and a result artifact is extracted and json decoded. - - Args: - trigger_preconditions: List of preconditions to add to the workflow trigger - """ - workflow_result = self.create_extract_result_tree_branch( - artifact_name, - workflow, - package_meta, - github_client, - package_name, - ) - workflow_complete = self.create_workflow_complete_tree_branch( - workflow, - package_meta, - release_meta, - github_client, - package_name, - trigger_preconditions, - ) - - latch_chains(workflow_result, workflow_complete) - - return workflow_result - - def create_extract_result_tree_branch( - self, - artifact_name: str, - workflow: Workflow, - package_meta: PackageMeta, - github_client: GitHubClientAsync, - log_prefix: str, - ) -> Union[Selector, Sequence]: - extract_artifact_result = create_extract_artifact_result_ppa( - artifact_name, - workflow, - package_meta, - github_client, - log_prefix, - ) - download_artifacts = create_download_artifacts_ppa( - workflow, - package_meta, - github_client, - log_prefix, - ) - latch_chains(extract_artifact_result, download_artifacts) - return extract_artifact_result - - -class DockerFactory(GenericPackageFactory): - """Factory for Docker packages.""" - - def create_build_workflow_inputs( - self, - name: str, - workflow: Workflow, - package_meta: PackageMeta, - release_meta: ReleaseMeta, - log_prefix: str, - ) -> Behaviour: - return DockerWorkflowInputs( - name, workflow, package_meta, release_meta, log_prefix=log_prefix - ) - - def create_publish_workflow_inputs( - self, - name: str, - workflow: Workflow, - package_meta: PackageMeta, - release_meta: ReleaseMeta, - log_prefix: str, - ) -> Behaviour: - return DockerWorkflowInputs( - name, workflow, package_meta, release_meta, log_prefix=log_prefix - ) - - -class DebianFactory(GenericPackageFactory): - pass - - -class RPMFactory(GenericPackageFactory): - pass - - -class PackageWithValidation: - """ - Mixin class for packages that have validation step before release, e.g. Homebrew and Snap - """ - - def create_package_release_execute_workflows_tree_branch( - self: GenericPackageFactoryProtocol, - package: Package, - release_meta: ReleaseMeta, - default_package: Package, - github_client: GitHubClientAsync, - package_name: str, - ) -> Union[Selector, Sequence]: - build = self.create_build_workflow_tree_branch( - package, - release_meta, - default_package, - github_client, - package_name, - ) - build.name = f"Build {package_name}" - publish = self.create_publish_workflow_tree_branch( - package.build, - package.publish, - package.meta, - release_meta, - default_package.publish, - github_client, - package_name, - ) - publish.name = f"Publish {package_name}" - package_release = Sequence( - f"Execute Workflows {package_name}", - memory=False, - children=[build, publish], - ) - return package_release - - def create_workflow_complete_tree_branch( - self: GenericPackageFactoryProtocol, - workflow: Workflow, - package_meta: PackageMeta, - release_meta: ReleaseMeta, - github_client: GitHubClientAsync, - log_prefix: str, - trigger_preconditions: Optional[List[Union[Sequence, Selector]]] = None, - ) -> Union[Selector, Sequence]: - """ - - Args: - trigger_preconditions: List of preconditions to add to the workflow trigger - """ - workflow_complete = create_workflow_completion_ppa( - workflow, - package_meta, - github_client, - log_prefix, - ) - find_workflow_by_uud = create_find_workflow_by_uuid_ppa( - workflow, - package_meta, - github_client, - log_prefix, - ) - trigger_workflow = create_trigger_workflow_ppa( - workflow, - package_meta, - release_meta, - github_client, - log_prefix, - ) - if trigger_preconditions: - latch_chains(trigger_workflow, *trigger_preconditions) - - latch_chains( - workflow_complete, - find_workflow_by_uud, - trigger_workflow, - ) - return workflow_complete - - -class HomebrewFactory(GenericPackageFactory, PackageWithValidation): - def create_package_release_goal_tree_branch( - self, - package: Package, - release_meta: ReleaseMeta, - default_package: Package, - github_client: GitHubClientAsync, - package_name: str, - ) -> Union[Selector, Sequence]: - package_release = self.create_package_release_execute_workflows_tree_branch( - package, release_meta, default_package, github_client, package_name - ) - need_to_release = NeedToReleaseHomebrew( - "Need To Release?", - cast(HomebrewMeta, package.meta), - release_meta, - log_prefix=package_name, - ) - release_goal = Selector( - f"Release Workflows {package_name} Goal", - memory=False, - children=[Inverter("Not", need_to_release), package_release], - ) - reset_package_state = ResetPackageStateGuarded( - "", - package, - default_package, - log_prefix=package_name, - ) - return Sequence( - f"Release {package_name}", - memory=False, - children=[ - reset_package_state, - DetectHombrewReleaseAndChannel( - "Detect Homebrew Channel", - cast(HomebrewMeta, package.meta), - release_meta, - log_prefix=package_name, - ), - ClassifyHomebrewVersionGuarded( - "", - cast(HomebrewMeta, package.meta), - release_meta, - github_client, - log_prefix=package_name, - ), - release_goal, - ], - ) - - def create_build_workflow_inputs( - self, - name: str, - workflow: Workflow, - package_meta: PackageMeta, - release_meta: ReleaseMeta, - log_prefix: str, - ) -> Behaviour: - - return HomewbrewWorkflowInputs( - name, - workflow, - cast(HomebrewMeta, package_meta), - release_meta, - log_prefix=log_prefix, - ) - - def create_publish_workflow_inputs( - self, - name: str, - workflow: Workflow, - package_meta: PackageMeta, - release_meta: ReleaseMeta, - log_prefix: str, - ) -> Behaviour: - - return HomewbrewWorkflowInputs( - name, - workflow, - cast(HomebrewMeta, package_meta), - release_meta, - log_prefix=log_prefix, - ) - - -class SnapFactory(GenericPackageFactory, PackageWithValidation): - def create_package_release_goal_tree_branch( - self, - package: Package, - release_meta: ReleaseMeta, - default_package: Package, - github_client: GitHubClientAsync, - package_name: str, - ) -> Union[Selector, Sequence]: - package_release = self.create_package_release_execute_workflows_tree_branch( - package, release_meta, default_package, github_client, package_name - ) - need_to_release = NeedToReleaseSnap( - "Need To Release?", - cast(SnapMeta, package.meta), - release_meta, - log_prefix=package_name, - ) - release_goal = Selector( - f"Release Workflows {package_name} Goal", - memory=False, - children=[Inverter("Not", need_to_release), package_release], - ) - reset_package_state = ResetPackageStateGuarded( - "", - package, - default_package, - log_prefix=package_name, - ) - return Sequence( - f"Release {package_name}", - memory=False, - children=[ - reset_package_state, - DetectSnapReleaseAndRiskLevel( - "Detect Homebrew Channel", - cast(SnapMeta, package.meta), - release_meta, - log_prefix=package_name, - ), - ClassifySnapVersionGuarded( - "", - cast(SnapMeta, package.meta), - release_meta, - github_client, - log_prefix=package_name, - ), - release_goal, - ], - ) - - def create_build_workflow_inputs( - self, - name: str, - workflow: Workflow, - package_meta: PackageMeta, - release_meta: ReleaseMeta, - log_prefix: str, - ) -> Behaviour: - - return SnapWorkflowInputs( - name, - workflow, - cast(SnapMeta, package_meta), - release_meta, - log_prefix=log_prefix, - ) - - def create_publish_workflow_inputs( - self, - name: str, - workflow: Workflow, - package_meta: PackageMeta, - release_meta: ReleaseMeta, - log_prefix: str, - ) -> Behaviour: - - return SnapWorkflowInputs( - name, - workflow, - cast(SnapMeta, package_meta), - release_meta, - log_prefix=log_prefix, - ) - - # Factory registry _FACTORIES: Dict[PackageType, GenericPackageFactory] = { PackageType.DOCKER: DockerFactory(), diff --git a/src/redis_release/bht/tree_factory_debian.py b/src/redis_release/bht/tree_factory_debian.py new file mode 100644 index 0000000..4796f64 --- /dev/null +++ b/src/redis_release/bht/tree_factory_debian.py @@ -0,0 +1,5 @@ +from redis_release.bht.tree_factory_generic import GenericPackageFactory + + +class DebianFactory(GenericPackageFactory): + pass diff --git a/src/redis_release/bht/tree_factory_docker.py b/src/redis_release/bht/tree_factory_docker.py new file mode 100644 index 0000000..4e1d8ed --- /dev/null +++ b/src/redis_release/bht/tree_factory_docker.py @@ -0,0 +1,33 @@ +from py_trees.behaviour import Behaviour + +from redis_release.bht.behaviours_docker import DockerWorkflowInputs +from redis_release.bht.state import PackageMeta, ReleaseMeta, Workflow +from redis_release.bht.tree_factory_generic import GenericPackageFactory + + +class DockerFactory(GenericPackageFactory): + """Factory for Docker packages.""" + + def create_build_workflow_inputs( + self, + name: str, + workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: + return DockerWorkflowInputs( + name, workflow, package_meta, release_meta, log_prefix=log_prefix + ) + + def create_publish_workflow_inputs( + self, + name: str, + workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: + return DockerWorkflowInputs( + name, workflow, package_meta, release_meta, log_prefix=log_prefix + ) diff --git a/src/redis_release/bht/tree_factory_generic.py b/src/redis_release/bht/tree_factory_generic.py new file mode 100644 index 0000000..523797f --- /dev/null +++ b/src/redis_release/bht/tree_factory_generic.py @@ -0,0 +1,410 @@ +from abc import ABC +from typing import List, Optional, Union + +from py_trees.behaviour import Behaviour +from py_trees.behaviours import Failure as AlwaysFailure +from py_trees.composites import Selector, Sequence +from py_trees.decorators import Inverter + +from redis_release.bht.backchain import create_PPA, latch_chains +from redis_release.bht.behaviours import GenericWorkflowInputs, NeedToPublishRelease +from redis_release.bht.composites import ( + ResetPackageStateGuarded, + RestartPackageGuarded, + RestartWorkflowGuarded, +) +from redis_release.bht.ppas import ( + create_attach_release_handle_ppa, + create_detect_release_type_ppa, + create_download_artifacts_ppa, + create_extract_artifact_result_ppa, + create_find_workflow_by_uuid_ppa, + create_identify_target_ref_ppa, + create_trigger_workflow_ppa, + create_workflow_completion_ppa, +) +from redis_release.bht.state import Package, PackageMeta, ReleaseMeta, Workflow +from redis_release.bht.tree_factory_protocol import GenericPackageFactoryProtocol +from redis_release.github_client_async import GitHubClientAsync + + +class GenericPackageFactory(ABC): + """Default factory for packages without specific customizations.""" + + def create_package_release_goal_tree_branch( + self, + package: Package, + release_meta: ReleaseMeta, + default_package: Package, + github_client: GitHubClientAsync, + package_name: str, + ) -> Union[Selector, Sequence]: + package_release = self.create_package_release_execute_workflows_tree_branch( + package, release_meta, default_package, github_client, package_name + ) + return Selector( + f"Package Release {package_name} Goal", + memory=False, + children=[AlwaysFailure("Yes"), package_release], + ) + + def create_build_workflow_inputs( + self, + name: str, + workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: + return GenericWorkflowInputs( + name, workflow, package_meta, release_meta, log_prefix=log_prefix + ) + + def create_publish_workflow_inputs( + self, + name: str, + workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: + return GenericWorkflowInputs( + name, workflow, package_meta, release_meta, log_prefix=log_prefix + ) + + def create_workflow_complete_tree_branch( + self, + workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + github_client: GitHubClientAsync, + log_prefix: str, + trigger_preconditions: Optional[List[Union[Sequence, Selector]]] = None, + ) -> Union[Selector, Sequence]: + """ + + Args: + trigger_preconditions: List of preconditions to add to the workflow trigger + """ + workflow_complete = create_workflow_completion_ppa( + workflow, + package_meta, + github_client, + log_prefix, + ) + find_workflow_by_uud = create_find_workflow_by_uuid_ppa( + workflow, + package_meta, + github_client, + log_prefix, + ) + trigger_workflow = create_trigger_workflow_ppa( + workflow, + package_meta, + release_meta, + github_client, + log_prefix, + ) + if trigger_preconditions: + latch_chains(trigger_workflow, *trigger_preconditions) + identify_target_ref = create_identify_target_ref_ppa( + package_meta, + release_meta, + github_client, + log_prefix, + ) + detect_release_type = create_detect_release_type_ppa( + package_meta, + release_meta, + log_prefix, + ) + latch_chains( + workflow_complete, + find_workflow_by_uud, + trigger_workflow, + identify_target_ref, + detect_release_type, + ) + return workflow_complete + + def create_package_release_execute_workflows_tree_branch( + self, + package: Package, + release_meta: ReleaseMeta, + default_package: Package, + github_client: GitHubClientAsync, + package_name: str, + ) -> Union[Selector, Sequence]: + build = self.create_build_workflow_tree_branch( + package, + release_meta, + default_package, + github_client, + package_name, + ) + build.name = f"Build {package_name}" + publish = self.create_publish_workflow_tree_branch( + package.build, + package.publish, + package.meta, + release_meta, + default_package.publish, + github_client, + package_name, + ) + reset_package_state = ResetPackageStateGuarded( + "", + package, + default_package, + log_prefix=package_name, + ) + publish.name = f"Publish {package_name}" + package_release = Sequence( + f"Package Release {package_name}", + memory=False, + children=[reset_package_state, build, publish], + ) + return package_release + + def create_build_workflow_tree_branch( + self, + package: Package, + release_meta: ReleaseMeta, + default_package: Package, + github_client: GitHubClientAsync, + package_name: str, + ) -> Union[Selector, Sequence]: + + build_workflow_args = create_PPA( + "Set Build Workflow Inputs", + self.create_build_workflow_inputs( + "Set Build Workflow Inputs", + package.build, + package.meta, + release_meta, + log_prefix=f"{package_name}.build", + ), + ) + + build_workflow = self.create_workflow_with_result_tree_branch( + "release_handle", + package.build, + package.meta, + release_meta, + github_client, + f"{package_name}.build", + trigger_preconditions=[build_workflow_args], + ) + assert isinstance(build_workflow, Selector) + + reset_package_state = RestartPackageGuarded( + "BuildRestartCondition", + package, + package.build, + default_package, + log_prefix=f"{package_name}.build", + ) + build_workflow.add_child(reset_package_state) + + return build_workflow + + def create_publish_workflow_tree_branch( + self, + build_workflow: Workflow, + publish_workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + default_publish_workflow: Workflow, + github_client: GitHubClientAsync, + package_name: str, + ) -> Union[Selector, Sequence]: + attach_release_handle = create_attach_release_handle_ppa( + build_workflow, publish_workflow, log_prefix=f"{package_name}.publish" + ) + publish_workflow_args = create_PPA( + "Set Publish Workflow Inputs", + self.create_publish_workflow_inputs( + "Set Publish Workflow Inputs", + publish_workflow, + package_meta, + release_meta, + log_prefix=f"{package_name}.publish", + ), + ) + workflow_result = self.create_workflow_with_result_tree_branch( + "release_info", + publish_workflow, + package_meta, + release_meta, + github_client, + f"{package_name}.publish", + trigger_preconditions=[publish_workflow_args, attach_release_handle], + ) + not_need_to_publish = Inverter( + "Not", + NeedToPublishRelease( + "Need To Publish?", + package_meta, + release_meta, + log_prefix=f"{package_name}.publish", + ), + ) + reset_publish_workflow_state = RestartWorkflowGuarded( + "PublishRestartCondition", + publish_workflow, + package_meta, + default_publish_workflow, + log_prefix=f"{package_name}.publish", + ) + return Selector( + "Publish", + memory=False, + children=[ + not_need_to_publish, + workflow_result, + reset_publish_workflow_state, + ], + ) + + def create_workflow_with_result_tree_branch( + self, + artifact_name: str, + workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + github_client: GitHubClientAsync, + package_name: str, + trigger_preconditions: Optional[List[Union[Sequence, Selector]]] = None, + ) -> Union[Selector, Sequence]: + """ + Creates a workflow process that succedes when the workflow + is successful and a result artifact is extracted and json decoded. + + Args: + trigger_preconditions: List of preconditions to add to the workflow trigger + """ + workflow_result = self.create_extract_result_tree_branch( + artifact_name, + workflow, + package_meta, + github_client, + package_name, + ) + workflow_complete = self.create_workflow_complete_tree_branch( + workflow, + package_meta, + release_meta, + github_client, + package_name, + trigger_preconditions, + ) + + latch_chains(workflow_result, workflow_complete) + + return workflow_result + + def create_extract_result_tree_branch( + self, + artifact_name: str, + workflow: Workflow, + package_meta: PackageMeta, + github_client: GitHubClientAsync, + log_prefix: str, + ) -> Union[Selector, Sequence]: + extract_artifact_result = create_extract_artifact_result_ppa( + artifact_name, + workflow, + package_meta, + github_client, + log_prefix, + ) + download_artifacts = create_download_artifacts_ppa( + workflow, + package_meta, + github_client, + log_prefix, + ) + latch_chains(extract_artifact_result, download_artifacts) + return extract_artifact_result + + +class PackageWithValidation: + """ + Mixin class for packages that have validation step before release, e.g. Homebrew and Snap + """ + + def create_package_release_execute_workflows_tree_branch( + self: GenericPackageFactoryProtocol, + package: Package, + release_meta: ReleaseMeta, + default_package: Package, + github_client: GitHubClientAsync, + package_name: str, + ) -> Union[Selector, Sequence]: + build = self.create_build_workflow_tree_branch( + package, + release_meta, + default_package, + github_client, + package_name, + ) + build.name = f"Build {package_name}" + publish = self.create_publish_workflow_tree_branch( + package.build, + package.publish, + package.meta, + release_meta, + default_package.publish, + github_client, + package_name, + ) + publish.name = f"Publish {package_name}" + package_release = Sequence( + f"Execute Workflows {package_name}", + memory=False, + children=[build, publish], + ) + return package_release + + def create_workflow_complete_tree_branch( + self: GenericPackageFactoryProtocol, + workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + github_client: GitHubClientAsync, + log_prefix: str, + trigger_preconditions: Optional[List[Union[Sequence, Selector]]] = None, + ) -> Union[Selector, Sequence]: + """ + + Args: + trigger_preconditions: List of preconditions to add to the workflow trigger + """ + workflow_complete = create_workflow_completion_ppa( + workflow, + package_meta, + github_client, + log_prefix, + ) + find_workflow_by_uud = create_find_workflow_by_uuid_ppa( + workflow, + package_meta, + github_client, + log_prefix, + ) + trigger_workflow = create_trigger_workflow_ppa( + workflow, + package_meta, + release_meta, + github_client, + log_prefix, + ) + if trigger_preconditions: + latch_chains(trigger_workflow, *trigger_preconditions) + + latch_chains( + workflow_complete, + find_workflow_by_uud, + trigger_workflow, + ) + return workflow_complete diff --git a/src/redis_release/bht/tree_factory_homebrew.py b/src/redis_release/bht/tree_factory_homebrew.py new file mode 100644 index 0000000..e833a81 --- /dev/null +++ b/src/redis_release/bht/tree_factory_homebrew.py @@ -0,0 +1,113 @@ +from typing import Union, cast + +from py_trees.behaviour import Behaviour +from py_trees.composites import Selector, Sequence +from py_trees.decorators import Inverter + +from redis_release.bht.behaviours_homebrew import ( + DetectHombrewReleaseAndChannel, + HomewbrewWorkflowInputs, + NeedToReleaseHomebrew, +) +from redis_release.bht.composites import ( + ClassifyHomebrewVersionGuarded, + ResetPackageStateGuarded, +) +from redis_release.bht.state import ( + HomebrewMeta, + Package, + PackageMeta, + ReleaseMeta, + Workflow, +) +from redis_release.bht.tree_factory_generic import ( + GenericPackageFactory, + PackageWithValidation, +) +from redis_release.github_client_async import GitHubClientAsync + + +class HomebrewFactory(GenericPackageFactory, PackageWithValidation): + def create_package_release_goal_tree_branch( + self, + package: Package, + release_meta: ReleaseMeta, + default_package: Package, + github_client: GitHubClientAsync, + package_name: str, + ) -> Union[Selector, Sequence]: + package_release = self.create_package_release_execute_workflows_tree_branch( + package, release_meta, default_package, github_client, package_name + ) + need_to_release = NeedToReleaseHomebrew( + "Need To Release?", + cast(HomebrewMeta, package.meta), + release_meta, + log_prefix=package_name, + ) + release_goal = Selector( + f"Release Workflows {package_name} Goal", + memory=False, + children=[Inverter("Not", need_to_release), package_release], + ) + reset_package_state = ResetPackageStateGuarded( + "", + package, + default_package, + log_prefix=package_name, + ) + return Sequence( + f"Release {package_name}", + memory=False, + children=[ + reset_package_state, + DetectHombrewReleaseAndChannel( + "Detect Homebrew Channel", + cast(HomebrewMeta, package.meta), + release_meta, + log_prefix=package_name, + ), + ClassifyHomebrewVersionGuarded( + "", + cast(HomebrewMeta, package.meta), + release_meta, + github_client, + log_prefix=package_name, + ), + release_goal, + ], + ) + + def create_build_workflow_inputs( + self, + name: str, + workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: + + return HomewbrewWorkflowInputs( + name, + workflow, + cast(HomebrewMeta, package_meta), + release_meta, + log_prefix=log_prefix, + ) + + def create_publish_workflow_inputs( + self, + name: str, + workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: + + return HomewbrewWorkflowInputs( + name, + workflow, + cast(HomebrewMeta, package_meta), + release_meta, + log_prefix=log_prefix, + ) diff --git a/src/redis_release/bht/tree_factory_protocol.py b/src/redis_release/bht/tree_factory_protocol.py new file mode 100644 index 0000000..54ae2da --- /dev/null +++ b/src/redis_release/bht/tree_factory_protocol.py @@ -0,0 +1,97 @@ +from typing import List, Optional, Protocol, Union + +from py_trees.behaviour import Behaviour +from py_trees.composites import Selector, Sequence + +from redis_release.bht.state import Package, PackageMeta, ReleaseMeta, Workflow +from redis_release.github_client_async import GitHubClientAsync + + +class GenericPackageFactoryProtocol(Protocol): + """Protocol defining the interface for package-specific tree factories.""" + + def create_package_release_goal_tree_branch( + self, + package: Package, + release_meta: ReleaseMeta, + default_package: Package, + github_client: GitHubClientAsync, + package_name: str, + ) -> Union[Selector, Sequence]: ... + + def create_build_workflow_inputs( + self, + name: str, + workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: ... + + def create_publish_workflow_inputs( + self, + name: str, + workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: ... + + def create_workflow_complete_tree_branch( + self, + workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + github_client: GitHubClientAsync, + log_prefix: str, + trigger_preconditions: Optional[List[Union[Sequence, Selector]]] = None, + ) -> Union[Selector, Sequence]: ... + + def create_package_release_execute_workflows_tree_branch( + self, + package: Package, + release_meta: ReleaseMeta, + default_package: Package, + github_client: GitHubClientAsync, + package_name: str, + ) -> Union[Selector, Sequence]: ... + + def create_build_workflow_tree_branch( + self, + package: Package, + release_meta: ReleaseMeta, + default_package: Package, + github_client: GitHubClientAsync, + package_name: str, + ) -> Union[Selector, Sequence]: ... + + def create_publish_workflow_tree_branch( + self, + build_workflow: Workflow, + publish_workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + default_publish_workflow: Workflow, + github_client: GitHubClientAsync, + package_name: str, + ) -> Union[Selector, Sequence]: ... + + def create_workflow_with_result_tree_branch( + self, + artifact_name: str, + workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + github_client: GitHubClientAsync, + package_name: str, + trigger_preconditions: Optional[List[Union[Sequence, Selector]]] = None, + ) -> Union[Selector, Sequence]: ... + + def create_extract_result_tree_branch( + self, + artifact_name: str, + workflow: Workflow, + package_meta: PackageMeta, + github_client: GitHubClientAsync, + log_prefix: str, + ) -> Union[Selector, Sequence]: ... diff --git a/src/redis_release/bht/tree_factory_rpm.py b/src/redis_release/bht/tree_factory_rpm.py new file mode 100644 index 0000000..85ee537 --- /dev/null +++ b/src/redis_release/bht/tree_factory_rpm.py @@ -0,0 +1,5 @@ +from redis_release.bht.tree_factory_generic import GenericPackageFactory + + +class RPMFactory(GenericPackageFactory): + pass diff --git a/src/redis_release/bht/tree_factory_snap.py b/src/redis_release/bht/tree_factory_snap.py new file mode 100644 index 0000000..eb878eb --- /dev/null +++ b/src/redis_release/bht/tree_factory_snap.py @@ -0,0 +1,113 @@ +from typing import Union, cast + +from py_trees.behaviour import Behaviour +from py_trees.composites import Selector, Sequence +from py_trees.decorators import Inverter + +from redis_release.bht.behaviours_snap import ( + DetectSnapReleaseAndRiskLevel, + NeedToReleaseSnap, + SnapWorkflowInputs, +) +from redis_release.bht.composites import ( + ClassifySnapVersionGuarded, + ResetPackageStateGuarded, +) +from redis_release.bht.state import ( + Package, + PackageMeta, + ReleaseMeta, + SnapMeta, + Workflow, +) +from redis_release.bht.tree_factory_generic import ( + GenericPackageFactory, + PackageWithValidation, +) +from redis_release.github_client_async import GitHubClientAsync + + +class SnapFactory(GenericPackageFactory, PackageWithValidation): + def create_package_release_goal_tree_branch( + self, + package: Package, + release_meta: ReleaseMeta, + default_package: Package, + github_client: GitHubClientAsync, + package_name: str, + ) -> Union[Selector, Sequence]: + package_release = self.create_package_release_execute_workflows_tree_branch( + package, release_meta, default_package, github_client, package_name + ) + need_to_release = NeedToReleaseSnap( + "Need To Release?", + cast(SnapMeta, package.meta), + release_meta, + log_prefix=package_name, + ) + release_goal = Selector( + f"Release Workflows {package_name} Goal", + memory=False, + children=[Inverter("Not", need_to_release), package_release], + ) + reset_package_state = ResetPackageStateGuarded( + "", + package, + default_package, + log_prefix=package_name, + ) + return Sequence( + f"Release {package_name}", + memory=False, + children=[ + reset_package_state, + DetectSnapReleaseAndRiskLevel( + "Detect Homebrew Channel", + cast(SnapMeta, package.meta), + release_meta, + log_prefix=package_name, + ), + ClassifySnapVersionGuarded( + "", + cast(SnapMeta, package.meta), + release_meta, + github_client, + log_prefix=package_name, + ), + release_goal, + ], + ) + + def create_build_workflow_inputs( + self, + name: str, + workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: + + return SnapWorkflowInputs( + name, + workflow, + cast(SnapMeta, package_meta), + release_meta, + log_prefix=log_prefix, + ) + + def create_publish_workflow_inputs( + self, + name: str, + workflow: Workflow, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: + + return SnapWorkflowInputs( + name, + workflow, + cast(SnapMeta, package_meta), + release_meta, + log_prefix=log_prefix, + ) From b3b87272d0f51da4e88bd5eb450d9882e5646733 Mon Sep 17 00:00:00 2001 From: Petar Shtuchkin Date: Tue, 25 Nov 2025 21:33:16 +0200 Subject: [PATCH 04/13] NeedToRelease, ParallelBarrier memory, reorganize code --- config.yaml | 4 +- src/redis_release/bht/behaviours_debian.py | 25 ++++++++++++ src/redis_release/bht/behaviours_docker.py | 24 +++++++++++- src/redis_release/bht/behaviours_homebrew.py | 39 ++++++++++--------- src/redis_release/bht/behaviours_rpm.py | 25 ++++++++++++ src/redis_release/bht/composites.py | 4 +- src/redis_release/bht/tree_factory_debian.py | 15 ++++++- src/redis_release/bht/tree_factory_docker.py | 16 +++++++- src/redis_release/bht/tree_factory_generic.py | 17 +++++++- .../bht/tree_factory_protocol.py | 8 ++++ src/redis_release/bht/tree_factory_rpm.py | 13 ++++++- 11 files changed, 165 insertions(+), 25 deletions(-) diff --git a/config.yaml b/config.yaml index 41ee497..8b99a93 100644 --- a/config.yaml +++ b/config.yaml @@ -56,8 +56,10 @@ packages: package_type: snap repo: redislabsdev/redis-snap # homebrew has one fixed release branch: main - ref: master + #ref: master + ref: release_automation build_workflow: release_build_and_test.yml + build_timeout_minutes: 60 build_inputs: {} publish_internal_release: yes publish_workflow: release_publish.yml diff --git a/src/redis_release/bht/behaviours_debian.py b/src/redis_release/bht/behaviours_debian.py index e69de29..95b9069 100644 --- a/src/redis_release/bht/behaviours_debian.py +++ b/src/redis_release/bht/behaviours_debian.py @@ -0,0 +1,25 @@ +from py_trees.common import Status + +from redis_release.bht.behaviours import LoggingAction +from redis_release.bht.state import PackageMeta, ReleaseMeta + +# Conditions + + +class NeedToReleaseDebian(LoggingAction): + """Check if Debian package needs to be released.""" + + def __init__( + self, + name: str, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str = "", + ) -> None: + self.package_meta = package_meta + self.release_meta = release_meta + super().__init__(name=name, log_prefix=log_prefix) + + def update(self) -> Status: + # Debian packages are always released + return Status.SUCCESS diff --git a/src/redis_release/bht/behaviours_docker.py b/src/redis_release/bht/behaviours_docker.py index 5a0a1e4..d739e30 100644 --- a/src/redis_release/bht/behaviours_docker.py +++ b/src/redis_release/bht/behaviours_docker.py @@ -1,6 +1,6 @@ from py_trees.common import Status -from redis_release.bht.behaviours import ReleaseAction +from redis_release.bht.behaviours import LoggingAction, ReleaseAction from redis_release.bht.state import PackageMeta, ReleaseMeta, Workflow @@ -24,3 +24,25 @@ def __init__( def update(self) -> Status: return Status.SUCCESS + + +# Conditions + + +class NeedToReleaseDocker(LoggingAction): + """Check if Docker package needs to be released.""" + + def __init__( + self, + name: str, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str = "", + ) -> None: + self.package_meta = package_meta + self.release_meta = release_meta + super().__init__(name=name, log_prefix=log_prefix) + + def update(self) -> Status: + # Docker packages are always released + return Status.SUCCESS diff --git a/src/redis_release/bht/behaviours_homebrew.py b/src/redis_release/bht/behaviours_homebrew.py index 3bcb02f..849962c 100644 --- a/src/redis_release/bht/behaviours_homebrew.py +++ b/src/redis_release/bht/behaviours_homebrew.py @@ -10,24 +10,6 @@ from redis_release.models import HomebrewChannel, RedisVersion, ReleaseType -class NeedToReleaseHomebrew(LoggingAction): - def __init__( - self, - name: str, - package_meta: HomebrewMeta, - release_meta: ReleaseMeta, - log_prefix: str = "", - ) -> None: - self.package_meta = package_meta - self.release_meta = release_meta - super().__init__(name=name, log_prefix=log_prefix) - - def update(self) -> Status: - if self.package_meta.ephemeral.is_version_acceptable is True: - return Status.SUCCESS - return Status.FAILURE - - class HomewbrewWorkflowInputs(ReleaseAction): def __init__( self, @@ -271,3 +253,24 @@ def update(self) -> Status: except Exception as e: return self.log_exception_and_return_failure(e) + + +# Conditions + + +class NeedToReleaseHomebrew(LoggingAction): + def __init__( + self, + name: str, + package_meta: HomebrewMeta, + release_meta: ReleaseMeta, + log_prefix: str = "", + ) -> None: + self.package_meta = package_meta + self.release_meta = release_meta + super().__init__(name=name, log_prefix=log_prefix) + + def update(self) -> Status: + if self.package_meta.ephemeral.is_version_acceptable is True: + return Status.SUCCESS + return Status.FAILURE diff --git a/src/redis_release/bht/behaviours_rpm.py b/src/redis_release/bht/behaviours_rpm.py index e69de29..b351dfb 100644 --- a/src/redis_release/bht/behaviours_rpm.py +++ b/src/redis_release/bht/behaviours_rpm.py @@ -0,0 +1,25 @@ +from py_trees.common import Status + +from redis_release.bht.behaviours import LoggingAction +from redis_release.bht.state import PackageMeta, ReleaseMeta + +# Conditions + + +class NeedToReleaseRPM(LoggingAction): + """Check if RPM package needs to be released.""" + + def __init__( + self, + name: str, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str = "", + ) -> None: + self.package_meta = package_meta + self.release_meta = release_meta + super().__init__(name=name, log_prefix=log_prefix) + + def update(self) -> Status: + # RPM packages are always released + return Status.SUCCESS diff --git a/src/redis_release/bht/composites.py b/src/redis_release/bht/composites.py index 393239b..d9c8aad 100644 --- a/src/redis_release/bht/composites.py +++ b/src/redis_release/bht/composites.py @@ -61,8 +61,10 @@ class ParallelBarrier(Composite): def __init__( self, name: str, + memory: bool = True, children: Optional[TypingSequence[Behaviour]] = None, ): + self.memory = memory super().__init__(name, children) def tick(self) -> Iterator[Behaviour]: @@ -84,7 +86,7 @@ def tick(self) -> Iterator[Behaviour]: # Tick all children, skipping those that have already converged for child in self.children: # Skip children that have already converged (synchronized mode) - if child.status in [Status.SUCCESS, Status.FAILURE]: + if self.memory and child.status in [Status.SUCCESS, Status.FAILURE]: continue # Tick the child for node in child.tick(): diff --git a/src/redis_release/bht/tree_factory_debian.py b/src/redis_release/bht/tree_factory_debian.py index 4796f64..c1caa27 100644 --- a/src/redis_release/bht/tree_factory_debian.py +++ b/src/redis_release/bht/tree_factory_debian.py @@ -1,5 +1,18 @@ +from py_trees.behaviour import Behaviour + +from redis_release.bht.behaviours_debian import NeedToReleaseDebian +from redis_release.bht.state import PackageMeta, ReleaseMeta from redis_release.bht.tree_factory_generic import GenericPackageFactory class DebianFactory(GenericPackageFactory): - pass + def create_need_to_release_behaviour( + self, + name: str, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: + return NeedToReleaseDebian( + name, package_meta, release_meta, log_prefix=log_prefix + ) diff --git a/src/redis_release/bht/tree_factory_docker.py b/src/redis_release/bht/tree_factory_docker.py index 4e1d8ed..19544d4 100644 --- a/src/redis_release/bht/tree_factory_docker.py +++ b/src/redis_release/bht/tree_factory_docker.py @@ -1,6 +1,9 @@ from py_trees.behaviour import Behaviour -from redis_release.bht.behaviours_docker import DockerWorkflowInputs +from redis_release.bht.behaviours_docker import ( + DockerWorkflowInputs, + NeedToReleaseDocker, +) from redis_release.bht.state import PackageMeta, ReleaseMeta, Workflow from redis_release.bht.tree_factory_generic import GenericPackageFactory @@ -31,3 +34,14 @@ def create_publish_workflow_inputs( return DockerWorkflowInputs( name, workflow, package_meta, release_meta, log_prefix=log_prefix ) + + def create_need_to_release_behaviour( + self, + name: str, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: + return NeedToReleaseDocker( + name, package_meta, release_meta, log_prefix=log_prefix + ) diff --git a/src/redis_release/bht/tree_factory_generic.py b/src/redis_release/bht/tree_factory_generic.py index 523797f..731039e 100644 --- a/src/redis_release/bht/tree_factory_generic.py +++ b/src/redis_release/bht/tree_factory_generic.py @@ -3,6 +3,7 @@ from py_trees.behaviour import Behaviour from py_trees.behaviours import Failure as AlwaysFailure +from py_trees.behaviours import Success as AlwaysSuccess from py_trees.composites import Selector, Sequence from py_trees.decorators import Inverter @@ -45,7 +46,7 @@ def create_package_release_goal_tree_branch( return Selector( f"Package Release {package_name} Goal", memory=False, - children=[AlwaysFailure("Yes"), package_release], + children=[AlwaysFailure("Always"), package_release], ) def create_build_workflow_inputs( @@ -327,6 +328,20 @@ def create_extract_result_tree_branch( latch_chains(extract_artifact_result, download_artifacts) return extract_artifact_result + def create_need_to_release_behaviour( + self, + name: str, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: + """Create a behaviour that checks if the package needs to be released. + + Default implementation always returns SUCCESS (always release). + Override in subclasses for package-specific logic. + """ + return AlwaysSuccess(name) + class PackageWithValidation: """ diff --git a/src/redis_release/bht/tree_factory_protocol.py b/src/redis_release/bht/tree_factory_protocol.py index 54ae2da..46ad571 100644 --- a/src/redis_release/bht/tree_factory_protocol.py +++ b/src/redis_release/bht/tree_factory_protocol.py @@ -95,3 +95,11 @@ def create_extract_result_tree_branch( github_client: GitHubClientAsync, log_prefix: str, ) -> Union[Selector, Sequence]: ... + + def create_need_to_release_behaviour( + self, + name: str, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: ... diff --git a/src/redis_release/bht/tree_factory_rpm.py b/src/redis_release/bht/tree_factory_rpm.py index 85ee537..62113dc 100644 --- a/src/redis_release/bht/tree_factory_rpm.py +++ b/src/redis_release/bht/tree_factory_rpm.py @@ -1,5 +1,16 @@ +from py_trees.behaviour import Behaviour + +from redis_release.bht.behaviours_rpm import NeedToReleaseRPM +from redis_release.bht.state import PackageMeta, ReleaseMeta from redis_release.bht.tree_factory_generic import GenericPackageFactory class RPMFactory(GenericPackageFactory): - pass + def create_need_to_release_behaviour( + self, + name: str, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: + return NeedToReleaseRPM(name, package_meta, release_meta, log_prefix=log_prefix) From 68cdad0a75beafa3be2e8524fd1923c302c930b1 Mon Sep 17 00:00:00 2001 From: Petar Shtuchkin Date: Tue, 25 Nov 2025 21:35:08 +0200 Subject: [PATCH 05/13] Try to use parallel without memory --- src/redis_release/bht/tree.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/redis_release/bht/tree.py b/src/redis_release/bht/tree.py index 5b14130..80ae914 100644 --- a/src/redis_release/bht/tree.py +++ b/src/redis_release/bht/tree.py @@ -192,6 +192,7 @@ def create_root_node( root = ParallelBarrier( "Redis Release", + memory=False, children=[], ) for package_name, package in state.packages.items(): From 2a6dee50662d4ac1a4eb219d3ba2bc0e7edecf09 Mon Sep 17 00:00:00 2001 From: Petar Shtuchkin Date: Tue, 25 Nov 2025 21:57:13 +0200 Subject: [PATCH 06/13] One-step slack format --- src/redis_release/bht/tree.py | 1 + src/redis_release/cli.py | 30 ++++++++++++++++---- src/redis_release/models.py | 8 ++++++ src/redis_release/slack_bot.py | 10 ++++++- src/redis_release/state_slack.py | 47 ++++++++++++++++++++++++-------- 5 files changed, 77 insertions(+), 19 deletions(-) diff --git a/src/redis_release/bht/tree.py b/src/redis_release/bht/tree.py index 80ae914..b958dd8 100644 --- a/src/redis_release/bht/tree.py +++ b/src/redis_release/bht/tree.py @@ -142,6 +142,7 @@ def initialize_tree_and_state( args.slack_channel_id, args.slack_thread_ts, args.slack_reply_broadcast, + args.slack_format, ) # Capture the non-None printer in the closure printer = slack_printer diff --git a/src/redis_release/cli.py b/src/redis_release/cli.py index 6a068c9..69b111e 100644 --- a/src/redis_release/cli.py +++ b/src/redis_release/cli.py @@ -271,6 +271,11 @@ def slack_bot( "--authorized-user", help="User ID authorized to run releases (can be specified multiple times). If not specified, all users are authorized", ), + slack_format: str = typer.Option( + "default", + "--slack-format", + help="Slack message format: 'default' shows all steps, 'one-step' shows only the last step", + ), ) -> None: """Run Slack bot that listens for status requests. @@ -285,20 +290,33 @@ def slack_bot( Requires Socket Mode to be enabled in your Slack app configuration. """ + from redis_release.models import SlackFormat from redis_release.slack_bot import run_bot setup_logging() config_path = config_file or "config.yaml" + # Parse slack_format + try: + slack_format_enum = SlackFormat(slack_format) + except ValueError: + logger.error( + f"Invalid slack format: {slack_format}. Must be 'default' or 'one-step'" + ) + raise typer.BadParameter( + f"Invalid slack format: {slack_format}. Must be 'default' or 'one-step'" + ) + logger.info("Starting Slack bot...") asyncio.run( run_bot( - config_path, - slack_bot_token, - slack_app_token, - reply_in_thread, - broadcast_to_channel, - authorized_users, + config_path=config_path, + slack_bot_token=slack_bot_token, + slack_app_token=slack_app_token, + reply_in_thread=reply_in_thread, + broadcast_to_channel=broadcast_to_channel, + authorized_users=authorized_users, + slack_format=slack_format_enum, ) ) diff --git a/src/redis_release/models.py b/src/redis_release/models.py index de7975c..4e64029 100644 --- a/src/redis_release/models.py +++ b/src/redis_release/models.py @@ -60,6 +60,13 @@ class ReleaseType(str, Enum): INTERNAL = "internal" +class SlackFormat(str, Enum): + """Slack message format enumeration.""" + + DEFAULT = "default" + ONE_STEP = "one-step" + + class WorkflowStatus(str, Enum): """Workflow status enumeration.""" @@ -241,3 +248,4 @@ class ReleaseArgs(BaseModel): slack_channel_id: Optional[str] = None slack_thread_ts: Optional[str] = None slack_reply_broadcast: bool = False + slack_format: SlackFormat = SlackFormat.DEFAULT diff --git a/src/redis_release/slack_bot.py b/src/redis_release/slack_bot.py index bbdb770..f492240 100644 --- a/src/redis_release/slack_bot.py +++ b/src/redis_release/slack_bot.py @@ -13,7 +13,7 @@ from redis_release.bht.tree import async_tick_tock, initialize_tree_and_state from redis_release.config import Config, load_config -from redis_release.models import ReleaseArgs +from redis_release.models import ReleaseArgs, SlackFormat from redis_release.state_manager import S3StateStorage, StateManager from redis_release.state_slack import init_slack_printer @@ -34,6 +34,7 @@ def __init__( reply_in_thread: bool = True, broadcast_to_channel: bool = False, authorized_users: Optional[List[str]] = None, + slack_format: SlackFormat = SlackFormat.DEFAULT, ): """Initialize the bot. @@ -44,11 +45,13 @@ def __init__( reply_in_thread: If True, reply in thread. If False, reply in main channel broadcast_to_channel: If True and reply_in_thread is True, also show in main channel authorized_users: List of user IDs authorized to run releases. If None, all users are authorized + slack_format: Slack message format (default or one-step) """ self.config = config self.reply_in_thread = reply_in_thread self.broadcast_to_channel = broadcast_to_channel self.authorized_users = authorized_users or [] + self.slack_format = slack_format # Get tokens from args or environment bot_token = slack_bot_token or os.environ.get("SLACK_BOT_TOKEN") @@ -261,6 +264,7 @@ def run_release_in_thread() -> None: slack_channel_id=channel, slack_thread_ts=thread_ts if self.reply_in_thread else None, slack_reply_broadcast=reply_broadcast, + slack_format=self.slack_format, ) # Run the release @@ -355,6 +359,7 @@ async def _post_status( slack_channel_id=channel, thread_ts=thread_ts if self.reply_in_thread else None, reply_broadcast=self.broadcast_to_channel, + slack_format=self.slack_format, ) printer.update_message(state) @@ -391,6 +396,7 @@ async def run_bot( reply_in_thread: bool = True, broadcast_to_channel: bool = False, authorized_users: Optional[List[str]] = None, + slack_format: SlackFormat = SlackFormat.DEFAULT, ) -> None: """Run the Slack bot. @@ -401,6 +407,7 @@ async def run_bot( reply_in_thread: If True, reply in thread. If False, reply in main channel broadcast_to_channel: If True and reply_in_thread is True, also show in main channel authorized_users: List of user IDs authorized to run releases. If None, all users are authorized + slack_format: Slack message format (default or one-step) """ # Load config config = load_config(config_path) @@ -413,6 +420,7 @@ async def run_bot( reply_in_thread=reply_in_thread, broadcast_to_channel=broadcast_to_channel, authorized_users=authorized_users, + slack_format=slack_format, ) await bot.start() diff --git a/src/redis_release/state_slack.py b/src/redis_release/state_slack.py index 4286c6a..0542ec7 100644 --- a/src/redis_release/state_slack.py +++ b/src/redis_release/state_slack.py @@ -9,6 +9,7 @@ from slack_sdk import WebClient from slack_sdk.errors import SlackApiError +from redis_release.models import SlackFormat from redis_release.state_display import DisplayModel, StepStatus from .bht.state import ( @@ -44,6 +45,7 @@ def init_slack_printer( slack_channel_id: Optional[str], thread_ts: Optional[str] = None, reply_broadcast: bool = False, + slack_format: SlackFormat = SlackFormat.DEFAULT, ) -> "SlackStatePrinter": """Initialize SlackStatePrinter with validation. @@ -52,6 +54,7 @@ def init_slack_printer( slack_channel_id: Slack channel ID to post to thread_ts: Optional thread timestamp to post messages in a thread reply_broadcast: If True and thread_ts is set, also show in main channel + slack_format: Slack message format (default or one-step) Returns: SlackStatePrinter instance @@ -69,7 +72,9 @@ def init_slack_printer( "Slack token not provided. Use slack_token argument or set SLACK_BOT_TOKEN environment variable" ) - return SlackStatePrinter(token, slack_channel_id, thread_ts, reply_broadcast) + return SlackStatePrinter( + token, slack_channel_id, thread_ts, reply_broadcast, slack_format + ) class SlackStatePrinter: @@ -81,6 +86,7 @@ def __init__( slack_channel_id: str, thread_ts: Optional[str] = None, reply_broadcast: bool = False, + slack_format: SlackFormat = SlackFormat.DEFAULT, ): """Initialize the Slack printer. @@ -89,11 +95,13 @@ def __init__( slack_channel_id: Slack channel ID to post messages to thread_ts: Optional thread timestamp to post messages in a thread reply_broadcast: If True and thread_ts is set, also show in main channel + slack_format: Slack message format (default or one-step) """ self.client = WebClient(token=slack_token) self.channel_id: str = slack_channel_id self.thread_ts = thread_ts self.reply_broadcast = reply_broadcast + self.slack_format = slack_format self.message_ts: Optional[str] = None self.last_blocks_json: Optional[str] = None self.started_at = datetime.now(timezone.utc) @@ -409,16 +417,31 @@ def _format_steps_for_slack( details: List[str] = [] details.append(f"*{prefix}*") - for step_status, step_name, step_message in steps: - if step_status == StepStatus.SUCCEEDED: - details.append(f"• ✅ {step_name}") - elif step_status == StepStatus.RUNNING: - details.append(f"• ⏳ {step_name}") - elif step_status == StepStatus.NOT_STARTED: - details.append(f"• ⚪ {step_name}") - else: # FAILED or INCORRECT - msg = f" ({step_message})" if step_message else "" - details.append(f"• ❌ {step_name}{msg}") - break + # If one-step format, only show the last step + if self.slack_format == SlackFormat.ONE_STEP: + if steps: + step_status, step_name, step_message = steps[-1] + if step_status == StepStatus.SUCCEEDED: + details.append(f"• ✅ {step_name}") + elif step_status == StepStatus.RUNNING: + details.append(f"• ⏳ {step_name}") + elif step_status == StepStatus.NOT_STARTED: + details.append(f"• ⚪ {step_name}") + else: # FAILED or INCORRECT + msg = f" ({step_message})" if step_message else "" + details.append(f"• ❌ {step_name}{msg}") + else: + # Default format: show all steps + for step_status, step_name, step_message in steps: + if step_status == StepStatus.SUCCEEDED: + details.append(f"• ✅ {step_name}") + elif step_status == StepStatus.RUNNING: + details.append(f"• ⏳ {step_name}") + elif step_status == StepStatus.NOT_STARTED: + details.append(f"• ⚪ {step_name}") + else: # FAILED or INCORRECT + msg = f" ({step_message})" if step_message else "" + details.append(f"• ❌ {step_name}{msg}") + break return details From 94075de22ce6a4c0819485bb2b6e69409351ca68 Mon Sep 17 00:00:00 2001 From: Petar Shtuchkin Date: Wed, 26 Nov 2025 09:56:45 +0200 Subject: [PATCH 07/13] Use exactly one line for one-step mode --- src/redis_release/state_slack.py | 38 +++++++++++--------------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/src/redis_release/state_slack.py b/src/redis_release/state_slack.py index 0542ec7..e23029b 100644 --- a/src/redis_release/state_slack.py +++ b/src/redis_release/state_slack.py @@ -417,31 +417,19 @@ def _format_steps_for_slack( details: List[str] = [] details.append(f"*{prefix}*") - # If one-step format, only show the last step + for step_status, step_name, step_message in steps: + if step_status == StepStatus.SUCCEEDED: + details.append(f"• ✅ {step_name}") + elif step_status == StepStatus.RUNNING: + details.append(f"• ⏳ {step_name}") + elif step_status == StepStatus.NOT_STARTED: + details.append(f"• ⚪ {step_name}") + else: # FAILED or INCORRECT + msg = f" ({step_message})" if step_message else "" + details.append(f"• ❌ {step_name}{msg}") + break + if self.slack_format == SlackFormat.ONE_STEP: - if steps: - step_status, step_name, step_message = steps[-1] - if step_status == StepStatus.SUCCEEDED: - details.append(f"• ✅ {step_name}") - elif step_status == StepStatus.RUNNING: - details.append(f"• ⏳ {step_name}") - elif step_status == StepStatus.NOT_STARTED: - details.append(f"• ⚪ {step_name}") - else: # FAILED or INCORRECT - msg = f" ({step_message})" if step_message else "" - details.append(f"• ❌ {step_name}{msg}") - else: - # Default format: show all steps - for step_status, step_name, step_message in steps: - if step_status == StepStatus.SUCCEEDED: - details.append(f"• ✅ {step_name}") - elif step_status == StepStatus.RUNNING: - details.append(f"• ⏳ {step_name}") - elif step_status == StepStatus.NOT_STARTED: - details.append(f"• ⚪ {step_name}") - else: # FAILED or INCORRECT - msg = f" ({step_message})" if step_message else "" - details.append(f"• ❌ {step_name}{msg}") - break + details = details[-1:] return details From 2dfd33730e58c1a327d3960783c2d2907163751b Mon Sep 17 00:00:00 2001 From: Petar Shtuchkin Date: Wed, 26 Nov 2025 10:04:12 +0200 Subject: [PATCH 08/13] Remove double header, try to fix one-step --- src/redis_release/state_slack.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/redis_release/state_slack.py b/src/redis_release/state_slack.py index e23029b..80f545b 100644 --- a/src/redis_release/state_slack.py +++ b/src/redis_release/state_slack.py @@ -400,6 +400,9 @@ def _collect_workflow_details_slack( self._format_steps_for_slack(workflow_status[1], workflow_name) ) + if self.slack_format == SlackFormat.ONE_STEP: + details = details[-1:] + return "\n".join(details) def _format_steps_for_slack( @@ -415,7 +418,7 @@ def _format_steps_for_slack( List of formatted step strings """ details: List[str] = [] - details.append(f"*{prefix}*") + # details.append(f"*{prefix}*") for step_status, step_name, step_message in steps: if step_status == StepStatus.SUCCEEDED: @@ -429,7 +432,4 @@ def _format_steps_for_slack( details.append(f"• ❌ {step_name}{msg}") break - if self.slack_format == SlackFormat.ONE_STEP: - details = details[-1:] - return details From eb22970047c22d80455d6d88701c8b70c19f5d03 Mon Sep 17 00:00:00 2001 From: Petar Shtuchkin Date: Fri, 28 Nov 2025 17:46:56 +0200 Subject: [PATCH 09/13] Allow unstable releases for snap --- config.yaml | 2 +- src/redis_release/bht/behaviours_snap.py | 43 +++++++++++++++------- src/redis_release/bht/tree_factory_snap.py | 2 +- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/config.yaml b/config.yaml index 8b99a93..a7270f8 100644 --- a/config.yaml +++ b/config.yaml @@ -54,7 +54,7 @@ packages: publish_inputs: {} snap: package_type: snap - repo: redislabsdev/redis-snap + repo: redis/redis-snap # homebrew has one fixed release branch: main #ref: master ref: release_automation diff --git a/src/redis_release/bht/behaviours_snap.py b/src/redis_release/bht/behaviours_snap.py index 0de899a..cd5bbc2 100644 --- a/src/redis_release/bht/behaviours_snap.py +++ b/src/redis_release/bht/behaviours_snap.py @@ -63,6 +63,8 @@ def initialise(self) -> None: return self.feedback_message = "" + if self.release_meta.tag == "unstable": + return try: self.release_version = RedisVersion.parse(self.release_meta.tag) except ValueError as e: @@ -80,20 +82,24 @@ def update(self) -> Status: ): return Status.SUCCESS else: - assert self.release_version is not None - if self.package_meta.release_type is None: - if self.release_version.is_internal: - self.package_meta.release_type = ReleaseType.INTERNAL - self.package_meta.snap_risk_level = SnapRiskLevel.CANDIDATE - else: - self.package_meta.release_type = ReleaseType.PUBLIC - - if self.package_meta.snap_risk_level is None: - if self.release_version.is_ga: - self.package_meta.snap_risk_level = SnapRiskLevel.STABLE - else: - # other versions go to CANDIDATE - self.package_meta.snap_risk_level = SnapRiskLevel.CANDIDATE + if self.release_meta.tag == "unstable": + self.package_meta.release_type = ReleaseType.PUBLIC + self.package_meta.snap_risk_level = SnapRiskLevel.EDGE + else: + assert self.release_version is not None + if self.package_meta.release_type is None: + if self.release_version.is_internal: + self.package_meta.release_type = ReleaseType.INTERNAL + self.package_meta.snap_risk_level = SnapRiskLevel.CANDIDATE + else: + self.package_meta.release_type = ReleaseType.PUBLIC + + if self.package_meta.snap_risk_level is None: + if self.release_version.is_ga: + self.package_meta.snap_risk_level = SnapRiskLevel.STABLE + else: + # other versions go to CANDIDATE + self.package_meta.snap_risk_level = SnapRiskLevel.CANDIDATE self.feedback_message = f"release_type: {self.package_meta.release_type.value}, snap_risk_level: {self.package_meta.snap_risk_level.value}" @@ -162,6 +168,15 @@ def initialise(self) -> None: self.logger.error("Package release type is not set") return + if self.package_meta.snap_risk_level is None: + self.logger.error("Snap risk level is not set") + return + + if self.release_meta.tag == "unstable": + self.package_meta.ephemeral.is_version_acceptable = True + self.package_meta.remote_version = "unstable" + return + try: self.release_version = RedisVersion.parse(self.release_meta.tag) self.logger.debug(f"Parsed release version: {self.release_version}") diff --git a/src/redis_release/bht/tree_factory_snap.py b/src/redis_release/bht/tree_factory_snap.py index eb878eb..51292c7 100644 --- a/src/redis_release/bht/tree_factory_snap.py +++ b/src/redis_release/bht/tree_factory_snap.py @@ -62,7 +62,7 @@ def create_package_release_goal_tree_branch( children=[ reset_package_state, DetectSnapReleaseAndRiskLevel( - "Detect Homebrew Channel", + "Detect Snap Release and Risk Level", cast(SnapMeta, package.meta), release_meta, log_prefix=package_name, From cc85d234b9004b85cd1218012f8d8b320f97af1f Mon Sep 17 00:00:00 2001 From: Petar Shtuchkin Date: Fri, 28 Nov 2025 18:06:48 +0200 Subject: [PATCH 10/13] Rework need to release for debian --- src/redis_release/bht/behaviours.py | 44 +++++++++++++++------- src/redis_release/bht/behaviours_debian.py | 38 ++++++++++++++++++- 2 files changed, 66 insertions(+), 16 deletions(-) diff --git a/src/redis_release/bht/behaviours.py b/src/redis_release/bht/behaviours.py index c8dd461..932ea3e 100644 --- a/src/redis_release/bht/behaviours.py +++ b/src/redis_release/bht/behaviours.py @@ -874,26 +874,42 @@ def __init__( ) -> None: self.release_meta = release_meta self.package_meta = package_meta + self.release_version: Optional[RedisVersion] = None super().__init__(name=name, log_prefix=log_prefix) + def initialise(self) -> None: + if self.package_meta.release_type is not None: + return + if self.release_meta.tag is None: + self.logger.error("Release tag is not set") + return + self.release_version = RedisVersion.parse(self.release_meta.tag) + def update(self) -> Status: + result: Status = Status.FAILURE + if self.package_meta.release_type is not None: - if self.log_once( - "release_type_detected", self.package_meta.ephemeral.log_once_flags - ): - self.logger.info( - f"Detected release type: {self.package_meta.release_type}" - ) - return Status.SUCCESS - if self.release_meta.tag and re.search(r"-int\d*$", self.release_meta.tag): - self.package_meta.release_type = ReleaseType.INTERNAL + result = Status.SUCCESS + self.feedback_message = f"Release type: {self.package_meta.release_type}" + elif self.release_version is not None: + if self.release_version.is_internal: + self.package_meta.release_type = ReleaseType.INTERNAL + else: + self.package_meta.release_type = ReleaseType.PUBLIC + result = Status.SUCCESS + self.feedback_message = f"Release type: {self.package_meta.release_type}" else: - self.package_meta.release_type = ReleaseType.PUBLIC - self.log_once( + self.feedback_message = "Failed to detect release type" + result = Status.FAILURE + + if self.log_once( "release_type_detected", self.release_meta.ephemeral.log_once_flags - ) - self.logger.info(f"Detected release type: {self.package_meta.release_type}") - return Status.SUCCESS + ): + if result == Status.SUCCESS: + self.logger.info(f"[green]{self.feedback_message}[/green]") + else: + self.logger.error(f"[red]{self.feedback_message}[/red]") + return result class IsForceRebuild(LoggingAction): diff --git a/src/redis_release/bht/behaviours_debian.py b/src/redis_release/bht/behaviours_debian.py index 95b9069..93c36b3 100644 --- a/src/redis_release/bht/behaviours_debian.py +++ b/src/redis_release/bht/behaviours_debian.py @@ -1,7 +1,10 @@ +from typing import Optional + from py_trees.common import Status from redis_release.bht.behaviours import LoggingAction from redis_release.bht.state import PackageMeta, ReleaseMeta +from redis_release.models import RedisVersion # Conditions @@ -18,8 +21,39 @@ def __init__( ) -> None: self.package_meta = package_meta self.release_meta = release_meta + self.release_version: Optional[RedisVersion] = None + super().__init__(name=name, log_prefix=log_prefix) + def initialise(self) -> None: + if self.release_meta.tag is None: + self.logger.error("Release tag is not set") + return + if self.release_version is not None: + return + + if self.release_meta.tag == "unstable": + return + try: + self.release_version = RedisVersion.parse(self.release_meta.tag) + except ValueError as e: + self.logger.error(f"Failed to parse release tag: {e}") + return + pass + def update(self) -> Status: - # Debian packages are always released - return Status.SUCCESS + result: Status = Status.FAILURE + if self.release_meta.tag is None: + self.feedback_message = "Release tag is not set" + result = Status.FAILURE + if self.release_meta.tag == "unstable": + self.feedback_message = "Skip unstable release for debian" + result = Status.FAILURE + + if self.release_version is not None: + self.feedback_message = "Need to release debian" + result = Status.SUCCESS + + if self.log_once("need_to_release", self.package_meta.ephemeral.log_once_flags): + self.logger.info(self.feedback_message) + return result From 4f32c6a9f9b2ca62ba163c9997d7805c9f2011ea Mon Sep 17 00:00:00 2001 From: Petar Shtuchkin Date: Fri, 28 Nov 2025 22:05:07 +0200 Subject: [PATCH 11/13] Package specific DetectReleaseType and NeedToRelease --- src/redis_release/bht/behaviours.py | 48 --------- src/redis_release/bht/behaviours_debian.py | 61 ++++++++++- src/redis_release/bht/behaviours_docker.py | 102 +++++++++++++++++- src/redis_release/bht/behaviours_homebrew.py | 46 +++++++- src/redis_release/bht/behaviours_rpm.py | 95 +++++++++++++++- src/redis_release/bht/behaviours_snap.py | 29 +++++ src/redis_release/bht/ppas.py | 16 +-- src/redis_release/bht/tree_factory_debian.py | 16 ++- src/redis_release/bht/tree_factory_docker.py | 12 +++ src/redis_release/bht/tree_factory_generic.py | 39 +++++-- .../bht/tree_factory_homebrew.py | 16 +++ .../bht/tree_factory_protocol.py | 8 ++ src/redis_release/bht/tree_factory_rpm.py | 13 ++- src/redis_release/bht/tree_factory_snap.py | 16 +++ 14 files changed, 438 insertions(+), 79 deletions(-) diff --git a/src/redis_release/bht/behaviours.py b/src/redis_release/bht/behaviours.py index 932ea3e..cd96a89 100644 --- a/src/redis_release/bht/behaviours.py +++ b/src/redis_release/bht/behaviours.py @@ -864,54 +864,6 @@ def update(self) -> Status: return Status.SUCCESS -class DetectReleaseType(LoggingAction): - def __init__( - self, - name: str, - package_meta: PackageMeta, - release_meta: ReleaseMeta, - log_prefix: str = "", - ) -> None: - self.release_meta = release_meta - self.package_meta = package_meta - self.release_version: Optional[RedisVersion] = None - super().__init__(name=name, log_prefix=log_prefix) - - def initialise(self) -> None: - if self.package_meta.release_type is not None: - return - if self.release_meta.tag is None: - self.logger.error("Release tag is not set") - return - self.release_version = RedisVersion.parse(self.release_meta.tag) - - def update(self) -> Status: - result: Status = Status.FAILURE - - if self.package_meta.release_type is not None: - result = Status.SUCCESS - self.feedback_message = f"Release type: {self.package_meta.release_type}" - elif self.release_version is not None: - if self.release_version.is_internal: - self.package_meta.release_type = ReleaseType.INTERNAL - else: - self.package_meta.release_type = ReleaseType.PUBLIC - result = Status.SUCCESS - self.feedback_message = f"Release type: {self.package_meta.release_type}" - else: - self.feedback_message = "Failed to detect release type" - result = Status.FAILURE - - if self.log_once( - "release_type_detected", self.release_meta.ephemeral.log_once_flags - ): - if result == Status.SUCCESS: - self.logger.info(f"[green]{self.feedback_message}[/green]") - else: - self.logger.error(f"[red]{self.feedback_message}[/red]") - return result - - class IsForceRebuild(LoggingAction): def __init__( self, name: str, package_meta: PackageMeta, log_prefix: str = "" diff --git a/src/redis_release/bht/behaviours_debian.py b/src/redis_release/bht/behaviours_debian.py index 93c36b3..ebb809c 100644 --- a/src/redis_release/bht/behaviours_debian.py +++ b/src/redis_release/bht/behaviours_debian.py @@ -4,11 +4,66 @@ from redis_release.bht.behaviours import LoggingAction from redis_release.bht.state import PackageMeta, ReleaseMeta -from redis_release.models import RedisVersion +from redis_release.models import RedisVersion, ReleaseType # Conditions +class DetectReleaseTypeDebian(LoggingAction): + """Detect release type for Debian packages based on version.""" + + def __init__( + self, + name: str, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str = "", + ) -> None: + self.release_meta = release_meta + self.package_meta = package_meta + self.release_version: Optional[RedisVersion] = None + super().__init__(name=name, log_prefix=log_prefix) + + def initialise(self) -> None: + if self.package_meta.release_type is not None: + return + if self.release_meta.tag is None: + self.logger.error("Release tag is not set") + return + if self.release_meta.tag == "unstable": + return + self.release_version = RedisVersion.parse(self.release_meta.tag) + + def update(self) -> Status: + result: Status = Status.FAILURE + + if self.package_meta.release_type is not None: + result = Status.SUCCESS + self.feedback_message = f"Release type for debian (from state): {self.package_meta.release_type}" + elif self.release_version is not None: + # Debian only publishes GA versions + if self.release_version.is_ga: + self.package_meta.release_type = ReleaseType.PUBLIC + else: + self.package_meta.release_type = ReleaseType.INTERNAL + result = Status.SUCCESS + self.feedback_message = ( + f"Detected release type for debian: {self.package_meta.release_type}" + ) + else: + self.feedback_message = "Failed to detect release type" + result = Status.FAILURE + + if self.log_once( + "release_type_detected", self.package_meta.ephemeral.log_once_flags + ): + if result == Status.SUCCESS: + self.logger.info(f"[green]{self.feedback_message}[/green]") + else: + self.logger.error(f"[red]{self.feedback_message}[/red]") + return result + + class NeedToReleaseDebian(LoggingAction): """Check if Debian package needs to be released.""" @@ -47,11 +102,11 @@ def update(self) -> Status: self.feedback_message = "Release tag is not set" result = Status.FAILURE if self.release_meta.tag == "unstable": - self.feedback_message = "Skip unstable release for debian" + self.feedback_message = "No, skip unstable release for debian" result = Status.FAILURE if self.release_version is not None: - self.feedback_message = "Need to release debian" + self.feedback_message = "Yes, need to release debian" result = Status.SUCCESS if self.log_once("need_to_release", self.package_meta.ephemeral.log_once_flags): diff --git a/src/redis_release/bht/behaviours_docker.py b/src/redis_release/bht/behaviours_docker.py index d739e30..d0ca2f1 100644 --- a/src/redis_release/bht/behaviours_docker.py +++ b/src/redis_release/bht/behaviours_docker.py @@ -1,7 +1,10 @@ +from typing import Optional + from py_trees.common import Status from redis_release.bht.behaviours import LoggingAction, ReleaseAction from redis_release.bht.state import PackageMeta, ReleaseMeta, Workflow +from redis_release.models import RedisVersion, ReleaseType class DockerWorkflowInputs(ReleaseAction): @@ -29,6 +32,60 @@ def update(self) -> Status: # Conditions +class DetectReleaseTypeDocker(LoggingAction): + """Detect release type for Docker packages based on version.""" + + def __init__( + self, + name: str, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str = "", + ) -> None: + self.release_meta = release_meta + self.package_meta = package_meta + self.release_version: Optional[RedisVersion] = None + super().__init__(name=name, log_prefix=log_prefix) + + def initialise(self) -> None: + if self.package_meta.release_type is not None: + return + if self.release_meta.tag is None: + self.logger.error("Release tag is not set") + return + if self.release_meta.tag == "unstable": + return + self.release_version = RedisVersion.parse(self.release_meta.tag) + + def update(self) -> Status: + result: Status = Status.FAILURE + + if self.package_meta.release_type is not None: + result = Status.SUCCESS + self.feedback_message = f"Release type for docker (from state): {self.package_meta.release_type}" + elif self.release_version is not None: + if self.release_version.is_internal: + self.package_meta.release_type = ReleaseType.INTERNAL + else: + self.package_meta.release_type = ReleaseType.PUBLIC + result = Status.SUCCESS + self.feedback_message = ( + f"Detected release type for docker: {self.package_meta.release_type}" + ) + else: + self.feedback_message = "Failed to detect release type" + result = Status.FAILURE + + if self.log_once( + "release_type_detected", self.package_meta.ephemeral.log_once_flags + ): + if result == Status.SUCCESS: + self.logger.info(f"[green]{self.feedback_message}[/green]") + else: + self.logger.error(f"[red]{self.feedback_message}[/red]") + return result + + class NeedToReleaseDocker(LoggingAction): """Check if Docker package needs to be released.""" @@ -41,8 +98,49 @@ def __init__( ) -> None: self.package_meta = package_meta self.release_meta = release_meta + self.release_version: Optional[RedisVersion] = None + super().__init__(name=name, log_prefix=log_prefix) + def initialise(self) -> None: + if self.release_meta.tag is None: + self.logger.error("Release tag is not set") + return + if self.release_version is not None: + return + + if self.release_meta.tag == "unstable": + return + try: + self.release_version = RedisVersion.parse(self.release_meta.tag) + except ValueError as e: + self.logger.error(f"Failed to parse release tag: {e}") + return + pass + def update(self) -> Status: - # Docker packages are always released - return Status.SUCCESS + result: Status = Status.FAILURE + if self.release_meta.tag is None: + self.feedback_message = "Release tag is not set" + result = Status.FAILURE + if self.release_meta.tag == "unstable": + self.feedback_message = "Skip unstable release for docker" + result = Status.FAILURE + + if self.release_version is not None: + if self.release_version.major < 8: + self.feedback_message = ( + f"Skip release for docker {str(self.release_version)} < 8.0" + ) + result = Status.FAILURE + else: + self.feedback_message = ( + f"Need to release docker version {str(self.release_version)}" + ) + result = Status.SUCCESS + + if self.log_once("need_to_release", self.package_meta.ephemeral.log_once_flags): + color_open = "" if result == Status.SUCCESS else "yellow" + color_close = "" if result == Status.SUCCESS else "[/]" + self.logger.info(f"{color_open}{self.feedback_message}{color_close}") + return result diff --git a/src/redis_release/bht/behaviours_homebrew.py b/src/redis_release/bht/behaviours_homebrew.py index 849962c..f3d0ee0 100644 --- a/src/redis_release/bht/behaviours_homebrew.py +++ b/src/redis_release/bht/behaviours_homebrew.py @@ -53,7 +53,8 @@ def initialise(self) -> None: return if self.release_version is not None: return - + if self.release_meta.tag == "unstable": + return self.feedback_message = "" try: self.release_version = RedisVersion.parse(self.release_meta.tag) @@ -72,6 +73,15 @@ def update(self) -> Status: ): return Status.SUCCESS else: + if self.release_meta.tag == "unstable": + self.feedback_message = "Skip unstable release for Homebrew" + if self.log_once( + "homebrew_channel_detected", + self.package_meta.ephemeral.log_once_flags, + ): + self.logger.info(self.feedback_message) + return Status.SUCCESS + assert self.release_version is not None if self.package_meta.release_type is None: if self.release_version.is_internal: @@ -131,6 +141,12 @@ def initialise(self) -> None: if self.package_meta.ephemeral.is_version_acceptable is not None: return + if self.release_meta.tag == "unstable": + self.package_meta.ephemeral.is_version_acceptable = False + # we need to set remote version to not None as it is a sign of successful classify step + self.package_meta.remote_version = "unstable" + return + self.feedback_message = "" # Validate homebrew_channel is set if self.package_meta.homebrew_channel is None: @@ -258,6 +274,34 @@ def update(self) -> Status: # Conditions +class DetectReleaseTypeHomebrew(LoggingAction): + """Check that release_type is set for Homebrew packages. + + Homebrew packages should have release_type set by DetectHombrewReleaseAndChannel. + This behavior just validates that it's set and fails if not. + """ + + def __init__( + self, + name: str, + package_meta: HomebrewMeta, + release_meta: ReleaseMeta, + log_prefix: str = "", + ) -> None: + self.package_meta = package_meta + self.release_meta = release_meta + super().__init__(name=name, log_prefix=log_prefix) + + def update(self) -> Status: + if self.package_meta.release_type is not None: + self.feedback_message = f"Release type: {self.package_meta.release_type}" + return Status.SUCCESS + else: + self.feedback_message = "Release type is not set" + self.logger.error("Release type is not set for Homebrew package") + return Status.FAILURE + + class NeedToReleaseHomebrew(LoggingAction): def __init__( self, diff --git a/src/redis_release/bht/behaviours_rpm.py b/src/redis_release/bht/behaviours_rpm.py index b351dfb..d6dda5f 100644 --- a/src/redis_release/bht/behaviours_rpm.py +++ b/src/redis_release/bht/behaviours_rpm.py @@ -1,11 +1,71 @@ +from typing import Optional + from py_trees.common import Status from redis_release.bht.behaviours import LoggingAction from redis_release.bht.state import PackageMeta, ReleaseMeta +from redis_release.models import RedisVersion, ReleaseType # Conditions +class DetectReleaseTypeRPM(LoggingAction): + """Detect release type for RPM packages based on version.""" + + def __init__( + self, + name: str, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str = "", + ) -> None: + self.release_meta = release_meta + self.package_meta = package_meta + self.release_version: Optional[RedisVersion] = None + super().__init__(name=name, log_prefix=log_prefix) + + def initialise(self) -> None: + if self.package_meta.release_type is not None: + return + if self.release_meta.tag is None: + self.logger.error("Release tag is not set") + return + if self.release_meta.tag == "unstable": + return + self.release_version = RedisVersion.parse(self.release_meta.tag) + + def update(self) -> Status: + result: Status = Status.FAILURE + + if self.package_meta.release_type is not None: + result = Status.SUCCESS + self.feedback_message = ( + f"Release type for RPM (from state): {self.package_meta.release_type}" + ) + elif self.release_version is not None: + # RPM only publishes GA versions + if self.release_version.is_ga: + self.package_meta.release_type = ReleaseType.PUBLIC + else: + self.package_meta.release_type = ReleaseType.INTERNAL + result = Status.SUCCESS + self.feedback_message = ( + f"Detected release type for RPM: {self.package_meta.release_type}" + ) + else: + self.feedback_message = "Failed to detect release type" + result = Status.FAILURE + + if self.log_once( + "release_type_detected", self.package_meta.ephemeral.log_once_flags + ): + if result == Status.SUCCESS: + self.logger.info(f"[green]{self.feedback_message}[/green]") + else: + self.logger.error(f"[red]{self.feedback_message}[/red]") + return result + + class NeedToReleaseRPM(LoggingAction): """Check if RPM package needs to be released.""" @@ -18,8 +78,39 @@ def __init__( ) -> None: self.package_meta = package_meta self.release_meta = release_meta + self.release_version: Optional[RedisVersion] = None + super().__init__(name=name, log_prefix=log_prefix) + def initialise(self) -> None: + if self.release_meta.tag is None: + self.logger.error("Release tag is not set") + return + if self.release_version is not None: + return + + if self.release_meta.tag == "unstable": + return + try: + self.release_version = RedisVersion.parse(self.release_meta.tag) + except ValueError as e: + self.logger.error(f"Failed to parse release tag: {e}") + return + pass + def update(self) -> Status: - # RPM packages are always released - return Status.SUCCESS + result: Status = Status.FAILURE + if self.release_meta.tag is None: + self.feedback_message = "Release tag is not set" + result = Status.FAILURE + if self.release_meta.tag == "unstable": + self.feedback_message = "Skip unstable release for RPM" + result = Status.FAILURE + + if self.release_version is not None: + self.feedback_message = "Need to release RPM" + result = Status.SUCCESS + + if self.log_once("need_to_release", self.package_meta.ephemeral.log_once_flags): + self.logger.info(self.feedback_message) + return result diff --git a/src/redis_release/bht/behaviours_snap.py b/src/redis_release/bht/behaviours_snap.py index cd5bbc2..cdadc35 100644 --- a/src/redis_release/bht/behaviours_snap.py +++ b/src/redis_release/bht/behaviours_snap.py @@ -174,6 +174,7 @@ def initialise(self) -> None: if self.release_meta.tag == "unstable": self.package_meta.ephemeral.is_version_acceptable = True + # we need to set remote version to not None as it is a sign of successful classify step self.package_meta.remote_version = "unstable" return @@ -278,6 +279,34 @@ def update(self) -> Status: return self.log_exception_and_return_failure(e) +class DetectReleaseTypeSnap(LoggingAction): + """Check that release_type is set for Snap packages. + + Snap packages should have release_type set by DetectSnapReleaseAndRiskLevel. + This behavior just validates that it's set and fails if not. + """ + + def __init__( + self, + name: str, + package_meta: SnapMeta, + release_meta: ReleaseMeta, + log_prefix: str = "", + ) -> None: + self.package_meta = package_meta + self.release_meta = release_meta + super().__init__(name=name, log_prefix=log_prefix) + + def update(self) -> Status: + if self.package_meta.release_type is not None: + self.feedback_message = f"Release type: {self.package_meta.release_type}" + return Status.SUCCESS + else: + self.feedback_message = "Release type is not set" + self.logger.error("Release type is not set for Snap package") + return Status.FAILURE + + class NeedToReleaseSnap(LoggingAction): def __init__( self, diff --git a/src/redis_release/bht/ppas.py b/src/redis_release/bht/ppas.py index c50c65f..98f6533 100644 --- a/src/redis_release/bht/ppas.py +++ b/src/redis_release/bht/ppas.py @@ -1,6 +1,8 @@ """ Here we define PPAs (Postcondition-Precondition-Action) composites to be used in backchaining. +Morse specific PPAs are defined directly in the tree factory files. + See backchain.py for more details on backchaining. Chains are formed and latched in `tree_factory.py` @@ -15,7 +17,6 @@ from .backchain import create_PPA from .behaviours import ( AttachReleaseHandleToPublishWorkflow, - DetectReleaseType, HasWorkflowArtifacts, HasWorkflowResult, IsTargetRefIdentified, @@ -136,19 +137,6 @@ def create_identify_target_ref_ppa( ) -def create_detect_release_type_ppa( - package_meta: PackageMeta, - release_meta: ReleaseMeta, - log_prefix: str, -) -> Union[Selector, Sequence]: - return create_PPA( - "Detect Release Type", - DetectReleaseType( - "Detect Release Type", package_meta, release_meta, log_prefix=log_prefix - ), - ) - - def create_download_artifacts_ppa( workflow: Workflow, package_meta: PackageMeta, diff --git a/src/redis_release/bht/tree_factory_debian.py b/src/redis_release/bht/tree_factory_debian.py index c1caa27..925c684 100644 --- a/src/redis_release/bht/tree_factory_debian.py +++ b/src/redis_release/bht/tree_factory_debian.py @@ -1,6 +1,9 @@ from py_trees.behaviour import Behaviour -from redis_release.bht.behaviours_debian import NeedToReleaseDebian +from redis_release.bht.behaviours_debian import ( + DetectReleaseTypeDebian, + NeedToReleaseDebian, +) from redis_release.bht.state import PackageMeta, ReleaseMeta from redis_release.bht.tree_factory_generic import GenericPackageFactory @@ -16,3 +19,14 @@ def create_need_to_release_behaviour( return NeedToReleaseDebian( name, package_meta, release_meta, log_prefix=log_prefix ) + + def create_detect_release_type_behaviour( + self, + name: str, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: + return DetectReleaseTypeDebian( + name, package_meta, release_meta, log_prefix=log_prefix + ) diff --git a/src/redis_release/bht/tree_factory_docker.py b/src/redis_release/bht/tree_factory_docker.py index 19544d4..0864d09 100644 --- a/src/redis_release/bht/tree_factory_docker.py +++ b/src/redis_release/bht/tree_factory_docker.py @@ -1,6 +1,7 @@ from py_trees.behaviour import Behaviour from redis_release.bht.behaviours_docker import ( + DetectReleaseTypeDocker, DockerWorkflowInputs, NeedToReleaseDocker, ) @@ -45,3 +46,14 @@ def create_need_to_release_behaviour( return NeedToReleaseDocker( name, package_meta, release_meta, log_prefix=log_prefix ) + + def create_detect_release_type_behaviour( + self, + name: str, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: + return DetectReleaseTypeDocker( + name, package_meta, release_meta, log_prefix=log_prefix + ) diff --git a/src/redis_release/bht/tree_factory_generic.py b/src/redis_release/bht/tree_factory_generic.py index 731039e..a62d667 100644 --- a/src/redis_release/bht/tree_factory_generic.py +++ b/src/redis_release/bht/tree_factory_generic.py @@ -16,7 +16,6 @@ ) from redis_release.bht.ppas import ( create_attach_release_handle_ppa, - create_detect_release_type_ppa, create_download_artifacts_ppa, create_extract_artifact_result_ppa, create_find_workflow_by_uuid_ppa, @@ -46,7 +45,18 @@ def create_package_release_goal_tree_branch( return Selector( f"Package Release {package_name} Goal", memory=False, - children=[AlwaysFailure("Always"), package_release], + children=[ + Inverter( + "Not", + self.create_need_to_release_behaviour( + f"Need To Release {package_name}?", + package.meta, + release_meta, + log_prefix=package_name, + ), + ), + package_release, + ], ) def create_build_workflow_inputs( @@ -114,17 +124,23 @@ def create_workflow_complete_tree_branch( github_client, log_prefix, ) - detect_release_type = create_detect_release_type_ppa( - package_meta, - release_meta, - log_prefix, + + detect_release_type = create_PPA( + "Detect Release Type", + self.create_detect_release_type_behaviour( + f"Detect Release Type", + package_meta, + release_meta, + log_prefix=log_prefix, + ), ) + latch_chains( workflow_complete, find_workflow_by_uud, trigger_workflow, - identify_target_ref, detect_release_type, + identify_target_ref, ) return workflow_complete @@ -342,6 +358,15 @@ def create_need_to_release_behaviour( """ return AlwaysSuccess(name) + def create_detect_release_type_behaviour( + self, + name: str, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: + raise NotImplementedError + class PackageWithValidation: """ diff --git a/src/redis_release/bht/tree_factory_homebrew.py b/src/redis_release/bht/tree_factory_homebrew.py index e833a81..c154281 100644 --- a/src/redis_release/bht/tree_factory_homebrew.py +++ b/src/redis_release/bht/tree_factory_homebrew.py @@ -6,6 +6,7 @@ from redis_release.bht.behaviours_homebrew import ( DetectHombrewReleaseAndChannel, + DetectReleaseTypeHomebrew, HomewbrewWorkflowInputs, NeedToReleaseHomebrew, ) @@ -111,3 +112,18 @@ def create_publish_workflow_inputs( release_meta, log_prefix=log_prefix, ) + + def create_detect_release_type_behaviour( + self, + name: str, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: + """Homebrew packages check that release_type is already set.""" + return DetectReleaseTypeHomebrew( + name, + cast(HomebrewMeta, package_meta), + release_meta, + log_prefix=log_prefix, + ) diff --git a/src/redis_release/bht/tree_factory_protocol.py b/src/redis_release/bht/tree_factory_protocol.py index 46ad571..486e07c 100644 --- a/src/redis_release/bht/tree_factory_protocol.py +++ b/src/redis_release/bht/tree_factory_protocol.py @@ -103,3 +103,11 @@ def create_need_to_release_behaviour( release_meta: ReleaseMeta, log_prefix: str, ) -> Behaviour: ... + + def create_detect_release_type_behaviour( + self, + name: str, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: ... diff --git a/src/redis_release/bht/tree_factory_rpm.py b/src/redis_release/bht/tree_factory_rpm.py index 62113dc..87e92d4 100644 --- a/src/redis_release/bht/tree_factory_rpm.py +++ b/src/redis_release/bht/tree_factory_rpm.py @@ -1,6 +1,6 @@ from py_trees.behaviour import Behaviour -from redis_release.bht.behaviours_rpm import NeedToReleaseRPM +from redis_release.bht.behaviours_rpm import DetectReleaseTypeRPM, NeedToReleaseRPM from redis_release.bht.state import PackageMeta, ReleaseMeta from redis_release.bht.tree_factory_generic import GenericPackageFactory @@ -14,3 +14,14 @@ def create_need_to_release_behaviour( log_prefix: str, ) -> Behaviour: return NeedToReleaseRPM(name, package_meta, release_meta, log_prefix=log_prefix) + + def create_detect_release_type_behaviour( + self, + name: str, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: + return DetectReleaseTypeRPM( + name, package_meta, release_meta, log_prefix=log_prefix + ) diff --git a/src/redis_release/bht/tree_factory_snap.py b/src/redis_release/bht/tree_factory_snap.py index 51292c7..d6f496b 100644 --- a/src/redis_release/bht/tree_factory_snap.py +++ b/src/redis_release/bht/tree_factory_snap.py @@ -5,6 +5,7 @@ from py_trees.decorators import Inverter from redis_release.bht.behaviours_snap import ( + DetectReleaseTypeSnap, DetectSnapReleaseAndRiskLevel, NeedToReleaseSnap, SnapWorkflowInputs, @@ -111,3 +112,18 @@ def create_publish_workflow_inputs( release_meta, log_prefix=log_prefix, ) + + def create_detect_release_type_behaviour( + self, + name: str, + package_meta: PackageMeta, + release_meta: ReleaseMeta, + log_prefix: str, + ) -> Behaviour: + """Snap packages check that release_type is already set.""" + return DetectReleaseTypeSnap( + name, + cast(SnapMeta, package_meta), + release_meta, + log_prefix=log_prefix, + ) From 4498768638f14e10d9c40d787e13c32878e9fea9 Mon Sep 17 00:00:00 2001 From: Petar Shtuchkin Date: Fri, 28 Nov 2025 22:10:04 +0200 Subject: [PATCH 12/13] Comment Fixes --- src/redis_release/bht/behaviours_debian.py | 5 +++-- src/redis_release/bht/behaviours_docker.py | 6 +++--- src/redis_release/bht/behaviours_homebrew.py | 6 +++--- src/redis_release/bht/behaviours_rpm.py | 5 +++-- src/redis_release/bht/behaviours_snap.py | 3 +++ src/redis_release/bht/ppas.py | 2 +- 6 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/redis_release/bht/behaviours_debian.py b/src/redis_release/bht/behaviours_debian.py index ebb809c..eb4735d 100644 --- a/src/redis_release/bht/behaviours_debian.py +++ b/src/redis_release/bht/behaviours_debian.py @@ -6,8 +6,6 @@ from redis_release.bht.state import PackageMeta, ReleaseMeta from redis_release.models import RedisVersion, ReleaseType -# Conditions - class DetectReleaseTypeDebian(LoggingAction): """Detect release type for Debian packages based on version.""" @@ -64,6 +62,9 @@ def update(self) -> Status: return result +# Conditions + + class NeedToReleaseDebian(LoggingAction): """Check if Debian package needs to be released.""" diff --git a/src/redis_release/bht/behaviours_docker.py b/src/redis_release/bht/behaviours_docker.py index d0ca2f1..709188a 100644 --- a/src/redis_release/bht/behaviours_docker.py +++ b/src/redis_release/bht/behaviours_docker.py @@ -29,9 +29,6 @@ def update(self) -> Status: return Status.SUCCESS -# Conditions - - class DetectReleaseTypeDocker(LoggingAction): """Detect release type for Docker packages based on version.""" @@ -86,6 +83,9 @@ def update(self) -> Status: return result +# Conditions + + class NeedToReleaseDocker(LoggingAction): """Check if Docker package needs to be released.""" diff --git a/src/redis_release/bht/behaviours_homebrew.py b/src/redis_release/bht/behaviours_homebrew.py index f3d0ee0..c24c426 100644 --- a/src/redis_release/bht/behaviours_homebrew.py +++ b/src/redis_release/bht/behaviours_homebrew.py @@ -271,9 +271,6 @@ def update(self) -> Status: return self.log_exception_and_return_failure(e) -# Conditions - - class DetectReleaseTypeHomebrew(LoggingAction): """Check that release_type is set for Homebrew packages. @@ -302,6 +299,9 @@ def update(self) -> Status: return Status.FAILURE +# Conditions + + class NeedToReleaseHomebrew(LoggingAction): def __init__( self, diff --git a/src/redis_release/bht/behaviours_rpm.py b/src/redis_release/bht/behaviours_rpm.py index d6dda5f..14b7e0c 100644 --- a/src/redis_release/bht/behaviours_rpm.py +++ b/src/redis_release/bht/behaviours_rpm.py @@ -6,8 +6,6 @@ from redis_release.bht.state import PackageMeta, ReleaseMeta from redis_release.models import RedisVersion, ReleaseType -# Conditions - class DetectReleaseTypeRPM(LoggingAction): """Detect release type for RPM packages based on version.""" @@ -66,6 +64,9 @@ def update(self) -> Status: return result +# Conditions + + class NeedToReleaseRPM(LoggingAction): """Check if RPM package needs to be released.""" diff --git a/src/redis_release/bht/behaviours_snap.py b/src/redis_release/bht/behaviours_snap.py index cdadc35..6b17e30 100644 --- a/src/redis_release/bht/behaviours_snap.py +++ b/src/redis_release/bht/behaviours_snap.py @@ -307,6 +307,9 @@ def update(self) -> Status: return Status.FAILURE +# Conditions + + class NeedToReleaseSnap(LoggingAction): def __init__( self, diff --git a/src/redis_release/bht/ppas.py b/src/redis_release/bht/ppas.py index 98f6533..1ff8913 100644 --- a/src/redis_release/bht/ppas.py +++ b/src/redis_release/bht/ppas.py @@ -1,7 +1,7 @@ """ Here we define PPAs (Postcondition-Precondition-Action) composites to be used in backchaining. -Morse specific PPAs are defined directly in the tree factory files. +More specific PPAs are defined directly in the tree factory files. See backchain.py for more details on backchaining. From 9e6e3d166178623194a858f5ec1892693a09fb68 Mon Sep 17 00:00:00 2001 From: Petar Shtuchkin Date: Mon, 1 Dec 2025 21:32:09 +0200 Subject: [PATCH 13/13] Do not release rpm < 8.0 --- src/redis_release/bht/behaviours_docker.py | 2 +- src/redis_release/bht/behaviours_rpm.py | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/redis_release/bht/behaviours_docker.py b/src/redis_release/bht/behaviours_docker.py index 709188a..5582df4 100644 --- a/src/redis_release/bht/behaviours_docker.py +++ b/src/redis_release/bht/behaviours_docker.py @@ -140,7 +140,7 @@ def update(self) -> Status: result = Status.SUCCESS if self.log_once("need_to_release", self.package_meta.ephemeral.log_once_flags): - color_open = "" if result == Status.SUCCESS else "yellow" + color_open = "" if result == Status.SUCCESS else "[yellow]" color_close = "" if result == Status.SUCCESS else "[/]" self.logger.info(f"{color_open}{self.feedback_message}{color_close}") return result diff --git a/src/redis_release/bht/behaviours_rpm.py b/src/redis_release/bht/behaviours_rpm.py index 14b7e0c..ed5b559 100644 --- a/src/redis_release/bht/behaviours_rpm.py +++ b/src/redis_release/bht/behaviours_rpm.py @@ -109,9 +109,22 @@ def update(self) -> Status: result = Status.FAILURE if self.release_version is not None: - self.feedback_message = "Need to release RPM" - result = Status.SUCCESS + if self.release_version.major < 8: + self.feedback_message = ( + f"Skip release for RPM {str(self.release_version)} < 8.0" + ) + result = Status.FAILURE + else: + self.feedback_message = ( + f"Need to release RPM version {str(self.release_version)}" + ) + result = Status.SUCCESS + else: + self.feedback_message = "Failed to parse release version" + result = Status.FAILURE if self.log_once("need_to_release", self.package_meta.ephemeral.log_once_flags): - self.logger.info(self.feedback_message) + color_open = "" if result == Status.SUCCESS else "[yellow]" + color_close = "" if result == Status.SUCCESS else "[/]" + self.logger.info(f"{color_open}{self.feedback_message}{color_close}") return result