From 87efb020937699454ad0b7a32e8a3e3f1b03e86b Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Fri, 5 Dec 2025 16:10:33 +0100 Subject: [PATCH 01/18] release on master merges axuillary images --- .evergreen.yml | 47 ++++- scripts/release/atomic_pipeline.py | 26 ++- .../build/image_build_configuration.py | 4 +- scripts/release/pipeline.py | 29 ++- scripts/release/pipeline.sh | 8 +- scripts/release/release_on_merge.py | 171 ++++++++++++++++++ 6 files changed, 262 insertions(+), 23 deletions(-) create mode 100644 scripts/release/release_on_merge.py diff --git a/.evergreen.yml b/.evergreen.yml index 79b181e3e..a870fa259 100644 --- a/.evergreen.yml +++ b/.evergreen.yml @@ -287,19 +287,36 @@ tasks: commands: - func: lint_repo - # pct only triggers this variant once a new agent image is out + # Runs on every merge - detects release.json changes and releases appropriate images + - name: release_on_merge + allowed_requesters: [ "patch" , "commit"] + commands: + - func: clone + - func: python_venv + - func: setup_building_host + - func: quay_login + - func: setup_docker_sbom + - command: subprocess.exec + params: + working_dir: src/github.com/mongodb/mongodb-kubernetes + binary: scripts/dev/run_python.sh + args: + - scripts/release/release_on_merge.py + + # Legacy task - triggers release_on_merge for manual patches - name: release_agent - # this enables us to run this variant either manually (patch) which pct does or during an OM bump (github_pr) - allowed_requesters: [ "patch", "github_pr" ] + allowed_requesters: [ "patch" ] commands: - func: clone - func: setup_building_host - func: quay_login - func: setup_docker_sbom - - func: pipeline - vars: - image_name: agent - build_scenario: release + - command: subprocess.exec + params: + working_dir: src/github.com/mongodb/mongodb-kubernetes + binary: scripts/dev/run_python.sh + args: + - scripts/release/release_on_merge.py - name: migrate_all_agents # this enables us to run this variant manually to build all the agents for the new agent registry @@ -1940,7 +1957,7 @@ buildvariants: - name: publish_om60_images display_name: publish_om60_images tags: [ "manual_patch" ] - allowed_requesters: [ "patch", "github_pr" ] + allowed_requesters: [ "patch", "commit" ] run_on: - release-ubuntu2404-small # This is required for CISA attestation https://jira.mongodb.org/browse/DEVPROD-17780 depends_on: @@ -1955,7 +1972,7 @@ buildvariants: - name: publish_om70_images display_name: publish_om70_images tags: [ "manual_patch" ] - allowed_requesters: [ "patch", "github_pr" ] + allowed_requesters: [ "patch", "commit" ] run_on: - release-ubuntu2404-small # This is required for CISA attestation https://jira.mongodb.org/browse/DEVPROD-17780 depends_on: @@ -1970,7 +1987,7 @@ buildvariants: - name: publish_om80_images display_name: publish_om80_images tags: [ "manual_patch" ] - allowed_requesters: [ "patch", "github_pr" ] + allowed_requesters: [ "patch", "commit" ] run_on: - release-ubuntu2404-small # This is required for CISA attestation https://jira.mongodb.org/browse/DEVPROD-17780 depends_on: @@ -1990,3 +2007,13 @@ buildvariants: - ubuntu2404-large tasks: - name: migrate_all_agents + + # Runs on every merge to master and releases images auxiliary to the operator release like OM and the Agent + - name: release_on_merge + display_name: release_on_merge + allowed_requesters: [ "patch" , "commit"] + run_on: + - release-ubuntu2404-small + patchable: false # Only run on commit builds + tasks: + - name: release_on_merge diff --git a/scripts/release/atomic_pipeline.py b/scripts/release/atomic_pipeline.py index e94ed8709..e1689ae23 100755 --- a/scripts/release/atomic_pipeline.py +++ b/scripts/release/atomic_pipeline.py @@ -16,7 +16,6 @@ from lib.base_logger import logger from scripts.release.agent.detect_ops_manager_changes import ( - detect_ops_manager_changes, get_all_agents_for_rebuild, get_currently_used_agents, ) @@ -331,21 +330,34 @@ def build_upgrade_hook_image(build_configuration: ImageBuildConfiguration): def build_agent(build_configuration: ImageBuildConfiguration): """ - Build the agent only for the latest operator for patches and operator releases. + Build the agent image(s). + Requires explicit agent selection via one of: + 1. Explicit version: --version and --tools-version + 2. All agents: --all-agents flag + 3. Currently used agents: --current-agents flag """ - if build_configuration.all_agents: + if build_configuration.version and build_configuration.agent_tools_version: + agent_versions_to_build = [(build_configuration.version, build_configuration.agent_tools_version)] + logger.info( + f"building explicit agent version: {build_configuration.version} with tools {build_configuration.agent_tools_version}" + ) + elif build_configuration.all_agents: agent_versions_to_build = get_all_agents_for_rebuild() logger.info("building all agents") elif build_configuration.currently_used_agents: agent_versions_to_build = get_currently_used_agents() - logger.info("building current used agents") + logger.info("building currently used agents") else: - agent_versions_to_build = detect_ops_manager_changes() - logger.info("building agents for changed OM versions") + raise ValueError( + "Agent build requires explicit selection. Use one of:\n" + " --version --agent-tools-version (for specific agent)\n" + " --all-agents (for all agents in release.json)\n" + " --current-agents (for currently used agents)" + ) if not agent_versions_to_build: - logger.info("No changes detected, skipping agent build") + logger.warning("No agent versions found to build") return logger.info(f"Building Agent versions: {agent_versions_to_build}") diff --git a/scripts/release/build/image_build_configuration.py b/scripts/release/build/image_build_configuration.py index 16c8f7766..216d036c9 100644 --- a/scripts/release/build/image_build_configuration.py +++ b/scripts/release/build/image_build_configuration.py @@ -4,8 +4,7 @@ from scripts.release.build.build_scenario import BuildScenario from scripts.release.build.image_build_process import ImageBuilder -SUPPORTED_PLATFORMS = ["darwin/amd64", "darwin/arm64", "linux/amd64", "linux/arm64", "linux/s390x", - "linux/ppc64le"] +SUPPORTED_PLATFORMS = ["darwin/amd64", "darwin/arm64", "linux/amd64", "linux/arm64", "linux/s390x", "linux/ppc64le"] @dataclass @@ -27,6 +26,7 @@ class ImageBuildConfiguration: all_agents: bool = False currently_used_agents: bool = False architecture_suffix: bool = False + agent_tools_version: Optional[str] = None # Explicit tools version for agent builds def is_release_scenario(self) -> bool: return self.scenario == BuildScenario.RELEASE diff --git a/scripts/release/pipeline.py b/scripts/release/pipeline.py index 32c2db904..dd7451ee4 100644 --- a/scripts/release/pipeline.py +++ b/scripts/release/pipeline.py @@ -139,9 +139,24 @@ def image_build_config_from_args(args) -> ImageBuildConfiguration: if type(builder) is PodmanImageBuilder and len(platforms) > 1: raise ValueError("Cannot use Podman builder with multi-platform builds") - # Validate version - only agent can have None version as the versions are managed by the agent - # which are externally retrieved from release.json - if version is None and image != "agent": + # Get agent_tools_version for agent builds (from --agent-tools-version arg) + agent_tools_version = getattr(args, "agent_tools_version", None) + + # Validate version requirements + if image == "agent": + # Agent builds require explicit selection: version+agent_tools_version OR --all-agents OR --current-agents + has_explicit_version = version is not None and agent_tools_version is not None + has_agent_flag = args.all_agents or args.current_agents + if not has_explicit_version and not has_agent_flag: + raise ValueError( + "Agent build requires explicit selection. Use one of:\n" + " --version --agent-tools-version (for specific agent)\n" + " --all-agents (for all agents in release.json)\n" + " --current-agents (for currently used agents)" + ) + if version is not None and agent_tools_version is None: + raise ValueError("For agent builds with explicit version, --agent-tools-version must also be provided.") + elif version is None: raise ValueError(f"Version cannot be empty for {image}.") return ImageBuildConfiguration( @@ -160,6 +175,7 @@ def image_build_config_from_args(args) -> ImageBuildConfiguration: all_agents=args.all_agents, currently_used_agents=args.current_agents, architecture_suffix=architecture_suffix, + agent_tools_version=agent_tools_version, ) @@ -283,6 +299,13 @@ def main(): action="store_true", help="Build all currently used agent images.", ) + parser.add_argument( + "--agent-tools-version", + metavar="", + action="store", + type=str, + help="Tools version to use when building agent image. Required when --version is provided for agent builds.", + ) parser.add_argument( "--architecture-suffix", action=argparse.BooleanOptionalAction, diff --git a/scripts/release/pipeline.sh b/scripts/release/pipeline.sh index f28a00556..266d17a89 100755 --- a/scripts/release/pipeline.sh +++ b/scripts/release/pipeline.sh @@ -9,7 +9,8 @@ args+=(--build-scenario "${BUILD_SCENARIO_OVERRIDE:-${BUILD_SCENARIO}}") case ${IMAGE_NAME} in "agent") - IMAGE_VERSION="" + # Can also use --all-agents or --current-agents flags instead + IMAGE_VERSION="${AGENT_VERSION:-}" ;; "ops-manager") @@ -33,6 +34,11 @@ if [[ "${IMAGE_VERSION:-}" != "" ]]; then args+=(--version "${IMAGE_VERSION}") fi +# For agent builds, pass tools version if explicitly provided +if [[ "${IMAGE_NAME}" == "agent" && "${TOOLS_VERSION:-}" != "" ]]; then + args+=(--agent-tools-version "${TOOLS_VERSION}") +fi + if [[ "${FLAGS:-}" != "" ]]; then IFS=" " read -ra flags <<< "${FLAGS}" args+=("${flags[@]}") diff --git a/scripts/release/release_on_merge.py b/scripts/release/release_on_merge.py new file mode 100644 index 000000000..a5de5f3a3 --- /dev/null +++ b/scripts/release/release_on_merge.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +""" +Releases all agent and ops-manager images on every merge to master. + +Releases: +1. cloud_manager agent (from release.json) +2. For each OM version defined as anchors in .evergreen.yml: + - The ops-manager image + - The corresponding agent (from release.json opsManagerMapping) + +skip_if_exists handles already-published images. + +Usage: + python release_on_merge.py [--dry-run] +""" +import json +import logging +import re +import subprocess +import sys +from pathlib import Path +from typing import Dict, List, Optional + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) + +# Track commands for dry-run summary +_dry_run_commands: List[str] = [] + + +def run_command(cmd: List[str], dry_run: bool = False) -> bool: + """Run a command and return success status.""" + cmd_str = " ".join(cmd) + logger.info(f"Running: {cmd_str}") + if dry_run: + _dry_run_commands.append(cmd_str) + return True + try: + subprocess.run(cmd, check=True) + return True + except subprocess.CalledProcessError as e: + logger.error(f"Command failed with exit code {e.returncode}") + return False + + +def print_dry_run_summary(): + """Print summary of all commands that would be run.""" + if not _dry_run_commands: + return + print("\n" + "=" * 80) + print("DRY RUN SUMMARY - Commands that would be executed:") + for i, cmd in enumerate(_dry_run_commands, 1): + print(f"{i}. {cmd}") + print("=" * 80 + "\n") + + +def load_release_json() -> Dict: + with open("release.json", "r") as f: + return json.load(f) + + +def get_ops_manager_mapping(release_data: Dict) -> Dict: + return release_data.get("supportedImages", {}).get("mongodb-agent", {}).get("opsManagerMapping", {}) + + +def get_latest_om_versions_from_evergreen_yaml() -> Dict[str, str]: + """ + Extract OM versions from .evergreen.yml anchors. + + Returns: {"60": "6.0.27", "70": "7.0.19", "80": "8.0.16"} + """ + versions = {} + evergreen_path = Path(".evergreen.yml") + + if not evergreen_path.exists(): + logger.error(".evergreen.yml not found") + return versions + + content = evergreen_path.read_text() + + # Match patterns like: - &ops_manager_60_latest 6.0.27 # + pattern = r'-\s*&ops_manager_(\d+)_latest\s+(\S+)\s+#' + + for match in re.finditer(pattern, content): + major = match.group(1) # "60", "70", "80" + version = match.group(2) # "6.0.27", "7.0.19", "8.0.16" + versions[major] = version + logger.info(f"Found OM {major}: {version}") + + return versions + + +def release_agent(agent_version: str, tools_version: str, dry_run: bool = False) -> bool: + cmd = [ + "python", "scripts/release/pipeline.py", + "agent", + "--build-scenario", "release", + "--version", agent_version, + "--agent-tools-version", tools_version, + ] + return run_command(cmd, dry_run) + + +def release_ops_manager(om_version: str, dry_run: bool = False) -> bool: + cmd = [ + "python", "scripts/release/pipeline.py", + "ops-manager", + "--build-scenario", "release", + "--version", om_version, + ] + return run_command(cmd, dry_run) + + +def main(): + import argparse + + parser = argparse.ArgumentParser(description="Release all images on merge") + parser.add_argument("--dry-run", action="store_true", help="Print commands without executing") + args = parser.parse_args() + + release_data = load_release_json() + ops_manager_mapping = get_ops_manager_mapping(release_data) + latest_om_versions_per_major = get_latest_om_versions_from_evergreen_yaml() + + success = True + + # 1. Release latest cloud_manager agent + cloud_manager_agent = ops_manager_mapping.get("cloud_manager") + cloud_manager_tools = ops_manager_mapping.get("cloud_manager_tools") + + if cloud_manager_agent and cloud_manager_tools: + logger.info(f"=== Releasing cloud_manager agent: {cloud_manager_agent} ===") + if not release_agent(cloud_manager_agent, cloud_manager_tools, args.dry_run): + success = False + else: + logger.warning("cloud_manager agent not found in release.json") + + # 2. Release each OM version and its agent + om_mapping = ops_manager_mapping.get("ops_manager", {}) + + for major, om_version in latest_om_versions_per_major.items(): + logger.info(f"=== Processing OM {major} ({om_version}) ===") + + # Release ops-manager image + logger.info(f"Releasing ops-manager {om_version}") + if not release_ops_manager(om_version, args.dry_run): + success = False + + # Release agent for this OM version + agent_info = om_mapping.get(om_version, {}) + agent_version = agent_info.get("agent_version") + tools_version = agent_info.get("tools_version") + + if agent_version and tools_version: + logger.info(f"Releasing agent {agent_version} for OM {om_version}") + if not release_agent(agent_version, tools_version, args.dry_run): + success = False + else: + logger.warning(f"No agent found for OM {om_version} in release.json") + + if args.dry_run: + print_dry_run_summary() + + return 0 if success else 1 + + +if __name__ == "__main__": + sys.exit(main()) From c1f962c6f90235572e88c61649f30357a28b6a1b Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Fri, 5 Dec 2025 16:48:01 +0100 Subject: [PATCH 02/18] summary at the end --- scripts/release/release_on_merge.py | 70 ++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 20 deletions(-) diff --git a/scripts/release/release_on_merge.py b/scripts/release/release_on_merge.py index a5de5f3a3..35d1f7878 100644 --- a/scripts/release/release_on_merge.py +++ b/scripts/release/release_on_merge.py @@ -19,7 +19,7 @@ import subprocess import sys from pathlib import Path -from typing import Dict, List, Optional +from typing import Dict, List logging.basicConfig( level=logging.INFO, @@ -27,16 +27,14 @@ ) logger = logging.getLogger(__name__) -# Track commands for dry-run summary -_dry_run_commands: List[str] = [] +# Track releases for summary +_releases: List[Dict[str, str]] = [] def run_command(cmd: List[str], dry_run: bool = False) -> bool: - """Run a command and return success status.""" cmd_str = " ".join(cmd) logger.info(f"Running: {cmd_str}") if dry_run: - _dry_run_commands.append(cmd_str) return True try: subprocess.run(cmd, check=True) @@ -46,15 +44,39 @@ def run_command(cmd: List[str], dry_run: bool = False) -> bool: return False -def print_dry_run_summary(): - """Print summary of all commands that would be run.""" - if not _dry_run_commands: +def track_release(release_type: str, version: str, status: str, context: str = ""): + _releases.append({ + "type": release_type, + "version": version, + "status": status, + "context": context, + }) + + +def print_summary(dry_run: bool = False): + """Print summary of all releases.""" + if not _releases: return - print("\n" + "=" * 80) - print("DRY RUN SUMMARY - Commands that would be executed:") - for i, cmd in enumerate(_dry_run_commands, 1): - print(f"{i}. {cmd}") - print("=" * 80 + "\n") + + def icon(status: str) -> str: + if dry_run: + return "○" + return "✓" if status == "success" else "✗" + + print("\n" + "=" * 60) + print("DRY RUN SUMMARY:" if dry_run else "RELEASE SUMMARY:") + print("=" * 60) + + for release_type, label in [("agent", "Agents"), ("ops-manager", "Ops Manager")]: + items = [r for r in _releases if r["type"] == release_type] + if items: + print(f"\n{label}:") + for r in items: + ctx = f" ({r['context']})" if r.get("context") else "" + print(f" {icon(r['status'])} {r['version']}{ctx}") + + print(f"\nTotal: {len(_releases)} releases") + print("=" * 60 + "\n") def load_release_json() -> Dict: @@ -93,7 +115,8 @@ def get_latest_om_versions_from_evergreen_yaml() -> Dict[str, str]: return versions -def release_agent(agent_version: str, tools_version: str, dry_run: bool = False) -> bool: +def release_agent(agent_version: str, tools_version: str, context: str, dry_run: bool = False) -> bool: + """Release an agent image.""" cmd = [ "python", "scripts/release/pipeline.py", "agent", @@ -101,17 +124,24 @@ def release_agent(agent_version: str, tools_version: str, dry_run: bool = False) "--version", agent_version, "--agent-tools-version", tools_version, ] - return run_command(cmd, dry_run) + success = run_command(cmd, dry_run) + status = "pending" if dry_run else ("success" if success else "failed") + track_release("agent", agent_version, status, context) + return success def release_ops_manager(om_version: str, dry_run: bool = False) -> bool: + """Release an ops-manager image.""" cmd = [ "python", "scripts/release/pipeline.py", "ops-manager", "--build-scenario", "release", "--version", om_version, ] - return run_command(cmd, dry_run) + success = run_command(cmd, dry_run) + status = "pending" if dry_run else ("success" if success else "failed") + track_release("ops-manager", om_version, status) + return success def main(): @@ -133,7 +163,7 @@ def main(): if cloud_manager_agent and cloud_manager_tools: logger.info(f"=== Releasing cloud_manager agent: {cloud_manager_agent} ===") - if not release_agent(cloud_manager_agent, cloud_manager_tools, args.dry_run): + if not release_agent(cloud_manager_agent, cloud_manager_tools, "cloud_manager", args.dry_run): success = False else: logger.warning("cloud_manager agent not found in release.json") @@ -156,13 +186,13 @@ def main(): if agent_version and tools_version: logger.info(f"Releasing agent {agent_version} for OM {om_version}") - if not release_agent(agent_version, tools_version, args.dry_run): + if not release_agent(agent_version, tools_version, f"OM {om_version}", args.dry_run): success = False else: logger.warning(f"No agent found for OM {om_version} in release.json") - if args.dry_run: - print_dry_run_summary() + # Always print summary + print_summary(args.dry_run) return 0 if success else 1 From 472d3c8a7441e0017325d36472996010eaf83cec Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Fri, 5 Dec 2025 16:49:32 +0100 Subject: [PATCH 03/18] remove publishing --- .evergreen.yml | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/.evergreen.yml b/.evergreen.yml index a870fa259..c8fcdcae8 100644 --- a/.evergreen.yml +++ b/.evergreen.yml @@ -1954,25 +1954,10 @@ buildvariants: - name: backup_csv_images_limit_3 - name: backup_csv_images_all - - name: publish_om60_images - display_name: publish_om60_images - tags: [ "manual_patch" ] - allowed_requesters: [ "patch", "commit" ] - run_on: - - release-ubuntu2404-small # This is required for CISA attestation https://jira.mongodb.org/browse/DEVPROD-17780 - depends_on: - - variant: e2e_om60_kind_ubi - name: '*' - - variant: e2e_static_om60_kind_ubi - name: '*' - tasks: - - name: publish_ops_manager - - name: release_agent - - name: publish_om70_images display_name: publish_om70_images tags: [ "manual_patch" ] - allowed_requesters: [ "patch", "commit" ] + allowed_requesters: [ "patch"] run_on: - release-ubuntu2404-small # This is required for CISA attestation https://jira.mongodb.org/browse/DEVPROD-17780 depends_on: @@ -1987,7 +1972,7 @@ buildvariants: - name: publish_om80_images display_name: publish_om80_images tags: [ "manual_patch" ] - allowed_requesters: [ "patch", "commit" ] + allowed_requesters: [ "patch" ] run_on: - release-ubuntu2404-small # This is required for CISA attestation https://jira.mongodb.org/browse/DEVPROD-17780 depends_on: From 803e10e9c5f1461a14f134aad184e3a2196ee61a Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Fri, 5 Dec 2025 16:51:30 +0100 Subject: [PATCH 04/18] remove publishing --- .evergreen.yml | 37 ------------------------------------- 1 file changed, 37 deletions(-) diff --git a/.evergreen.yml b/.evergreen.yml index c8fcdcae8..bf4b63127 100644 --- a/.evergreen.yml +++ b/.evergreen.yml @@ -303,21 +303,6 @@ tasks: args: - scripts/release/release_on_merge.py - # Legacy task - triggers release_on_merge for manual patches - - name: release_agent - allowed_requesters: [ "patch" ] - commands: - - func: clone - - func: setup_building_host - - func: quay_login - - func: setup_docker_sbom - - command: subprocess.exec - params: - working_dir: src/github.com/mongodb/mongodb-kubernetes - binary: scripts/dev/run_python.sh - args: - - scripts/release/release_on_merge.py - - name: migrate_all_agents # this enables us to run this variant manually to build all the agents for the new agent registry allowed_requesters: [ "patch" ] @@ -1901,26 +1886,6 @@ buildvariants: tasks: - name: build_om_images - # It will be called by pct while bumping the agent cloud manager image - - name: release_agent - display_name: release_agent - tags: [ "manual_patch", "release_agent" ] - run_on: - - release-ubuntu2404-small # This is required for CISA attestation https://jira.mongodb.org/browse/DEVPROD-17780 - depends_on: - - variant: init_test_run - name: build_agent_images_ubi # this ensures the agent gets released to ECR as well - - variant: e2e_multi_cluster_kind - name: '*' - - variant: e2e_static_multi_cluster_2_clusters - name: '*' - - variant: e2e_mdb_kind_ubi_cloudqa - name: '*' - - variant: e2e_static_mdb_kind_ubi_cloudqa - name: '*' - tasks: - - name: release_agent - # Only called manually, It's used for testing the task release_agents in case the release.json # has not changed, and you still want to push the images to registry. - name: manual_release_all_agents @@ -1967,7 +1932,6 @@ buildvariants: name: '*' tasks: - name: publish_ops_manager - - name: release_agent - name: publish_om80_images display_name: publish_om80_images @@ -1982,7 +1946,6 @@ buildvariants: name: '*' tasks: - name: publish_ops_manager - - name: release_agent - name: migrate_all_agents display_name: migrate_all_agents From 5442979676890edc12998eca5005908bfa957f1f Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Fri, 5 Dec 2025 16:58:36 +0100 Subject: [PATCH 05/18] remove publishing --- .evergreen.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/.evergreen.yml b/.evergreen.yml index bf4b63127..2c002853b 100644 --- a/.evergreen.yml +++ b/.evergreen.yml @@ -303,6 +303,19 @@ tasks: args: - scripts/release/release_on_merge.py + - name: release_agent + # this enables us to run this variant manually (patch) + allowed_requesters: [ "patch" ] + commands: + - func: clone + - func: setup_building_host + - func: quay_login + - func: setup_docker_sbom + - func: pipeline + vars: + image_name: agent + build_scenario: release + - name: migrate_all_agents # this enables us to run this variant manually to build all the agents for the new agent registry allowed_requesters: [ "patch" ] @@ -1886,6 +1899,26 @@ buildvariants: tasks: - name: build_om_images + # It will be called by pct while bumping the agent cloud manager image + - name: release_agent + display_name: release_agent + tags: [ "manual_patch", "release_agent" ] + run_on: + - release-ubuntu2404-small # This is required for CISA attestation https://jira.mongodb.org/browse/DEVPROD-17780 + depends_on: + - variant: init_test_run + name: build_agent_images_ubi # this ensures the agent gets released to ECR as well + - variant: e2e_multi_cluster_kind + name: '*' + - variant: e2e_static_multi_cluster_2_clusters + name: '*' + - variant: e2e_mdb_kind_ubi_cloudqa + name: '*' + - variant: e2e_static_mdb_kind_ubi_cloudqa + name: '*' + tasks: + - name: release_agent + # Only called manually, It's used for testing the task release_agents in case the release.json # has not changed, and you still want to push the images to registry. - name: manual_release_all_agents @@ -1932,6 +1965,7 @@ buildvariants: name: '*' tasks: - name: publish_ops_manager + - name: release_agent - name: publish_om80_images display_name: publish_om80_images @@ -1946,6 +1980,7 @@ buildvariants: name: '*' tasks: - name: publish_ops_manager + - name: release_agent - name: migrate_all_agents display_name: migrate_all_agents From 1a48d704630626e13181d883e0694b353862aa39 Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Fri, 5 Dec 2025 17:10:33 +0100 Subject: [PATCH 06/18] linter --- scripts/release/release_on_merge.py | 37 ++++++++++++++++++----------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/scripts/release/release_on_merge.py b/scripts/release/release_on_merge.py index 35d1f7878..d6e9c511c 100644 --- a/scripts/release/release_on_merge.py +++ b/scripts/release/release_on_merge.py @@ -45,12 +45,14 @@ def run_command(cmd: List[str], dry_run: bool = False) -> bool: def track_release(release_type: str, version: str, status: str, context: str = ""): - _releases.append({ - "type": release_type, - "version": version, - "status": status, - "context": context, - }) + _releases.append( + { + "type": release_type, + "version": version, + "status": status, + "context": context, + } + ) def print_summary(dry_run: bool = False): @@ -104,7 +106,7 @@ def get_latest_om_versions_from_evergreen_yaml() -> Dict[str, str]: content = evergreen_path.read_text() # Match patterns like: - &ops_manager_60_latest 6.0.27 # - pattern = r'-\s*&ops_manager_(\d+)_latest\s+(\S+)\s+#' + pattern = r"-\s*&ops_manager_(\d+)_latest\s+(\S+)\s+#" for match in re.finditer(pattern, content): major = match.group(1) # "60", "70", "80" @@ -118,11 +120,15 @@ def get_latest_om_versions_from_evergreen_yaml() -> Dict[str, str]: def release_agent(agent_version: str, tools_version: str, context: str, dry_run: bool = False) -> bool: """Release an agent image.""" cmd = [ - "python", "scripts/release/pipeline.py", + "python", + "scripts/release/pipeline.py", "agent", - "--build-scenario", "release", - "--version", agent_version, - "--agent-tools-version", tools_version, + "--build-scenario", + "release", + "--version", + agent_version, + "--agent-tools-version", + tools_version, ] success = run_command(cmd, dry_run) status = "pending" if dry_run else ("success" if success else "failed") @@ -133,10 +139,13 @@ def release_agent(agent_version: str, tools_version: str, context: str, dry_run: def release_ops_manager(om_version: str, dry_run: bool = False) -> bool: """Release an ops-manager image.""" cmd = [ - "python", "scripts/release/pipeline.py", + "python", + "scripts/release/pipeline.py", "ops-manager", - "--build-scenario", "release", - "--version", om_version, + "--build-scenario", + "release", + "--version", + om_version, ] success = run_command(cmd, dry_run) status = "pending" if dry_run else ("success" if success else "failed") From 569c6e31bab074eac115acaabcb4ddda6ed32cbc Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Fri, 5 Dec 2025 17:19:13 +0100 Subject: [PATCH 07/18] remove duplicate --- scripts/release/atomic_pipeline.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/scripts/release/atomic_pipeline.py b/scripts/release/atomic_pipeline.py index e1689ae23..17b173d10 100755 --- a/scripts/release/atomic_pipeline.py +++ b/scripts/release/atomic_pipeline.py @@ -329,18 +329,11 @@ def build_upgrade_hook_image(build_configuration: ImageBuildConfiguration): def build_agent(build_configuration: ImageBuildConfiguration): - """ - Build the agent image(s). - - Requires explicit agent selection via one of: - 1. Explicit version: --version and --tools-version - 2. All agents: --all-agents flag - 3. Currently used agents: --current-agents flag - """ + """Build the agent image(s). Validation happens in pipeline.py.""" if build_configuration.version and build_configuration.agent_tools_version: agent_versions_to_build = [(build_configuration.version, build_configuration.agent_tools_version)] logger.info( - f"building explicit agent version: {build_configuration.version} with tools {build_configuration.agent_tools_version}" + f"building agent {build_configuration.version} with tools {build_configuration.agent_tools_version}" ) elif build_configuration.all_agents: agent_versions_to_build = get_all_agents_for_rebuild() @@ -349,12 +342,7 @@ def build_agent(build_configuration: ImageBuildConfiguration): agent_versions_to_build = get_currently_used_agents() logger.info("building currently used agents") else: - raise ValueError( - "Agent build requires explicit selection. Use one of:\n" - " --version --agent-tools-version (for specific agent)\n" - " --all-agents (for all agents in release.json)\n" - " --current-agents (for currently used agents)" - ) + raise ValueError("No agent selection provided - this should be caught by pipeline.py validation") if not agent_versions_to_build: logger.warning("No agent versions found to build") From 3bf351d0f4a602b506556657475ebfc619ea9ad7 Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Mon, 8 Dec 2025 08:56:33 +0100 Subject: [PATCH 08/18] remove not used code --- ...anager_changes.py => agents_to_rebuild.py} | 71 ----- scripts/release/atomic_pipeline.py | 2 +- .../release/tests/test_agents_to_rebuild.py | 197 ++++++++++++++ .../tests/test_detect_ops_manager_changes.py | 255 ------------------ 4 files changed, 198 insertions(+), 327 deletions(-) rename scripts/release/agent/{detect_ops_manager_changes.py => agents_to_rebuild.py} (71%) create mode 100644 scripts/release/tests/test_agents_to_rebuild.py delete mode 100644 scripts/release/tests/test_detect_ops_manager_changes.py diff --git a/scripts/release/agent/detect_ops_manager_changes.py b/scripts/release/agent/agents_to_rebuild.py similarity index 71% rename from scripts/release/agent/detect_ops_manager_changes.py rename to scripts/release/agent/agents_to_rebuild.py index cf5b70740..b1f447385 100644 --- a/scripts/release/agent/detect_ops_manager_changes.py +++ b/scripts/release/agent/agents_to_rebuild.py @@ -19,32 +19,6 @@ logger = logging.getLogger(__name__) -def get_content_from_git(commit: str, file_path: str) -> Optional[str]: - try: - result = subprocess.run( - ["git", "show", f"{commit}:{file_path}"], capture_output=True, text=True, check=True, timeout=30 - ) - return result.stdout - except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e: - logger.error(f"Failed to get {file_path} from git commit {commit}: {e}") - return None - - -def load_release_json_from_master() -> Optional[Dict]: - base_revision = "origin/master" - - content = get_content_from_git(base_revision, "release.json") - if not content: - logger.error(f"Could not retrieve release.json from {base_revision}") - return None - - try: - return json.loads(content) - except json.JSONDecodeError as e: - logger.error(f"Invalid JSON in base release.json: {e}") - return None - - def load_current_release_json() -> Optional[Dict]: try: with open("release.json", "r") as f: @@ -60,28 +34,6 @@ def extract_ops_manager_mapping(release_data: Dict) -> Dict: return release_data.get("supportedImages", {}).get("mongodb-agent", {}).get("opsManagerMapping", {}) -def get_changed_agents(current_mapping: Dict, base_mapping: Dict) -> List[Tuple[str, str]]: - """Returns list of (agent_version, tools_version) tuples for added/changed agents""" - added_agents = [] - - current_om_mapping = current_mapping.get("ops_manager", {}) - master_om_mapping = base_mapping.get("ops_manager", {}) - - for om_version, agent_tools_version in current_om_mapping.items(): - if om_version not in master_om_mapping or master_om_mapping[om_version] != agent_tools_version: - added_agents.append((agent_tools_version["agent_version"], agent_tools_version["tools_version"])) - - current_cm = current_mapping.get("cloud_manager") - master_cm = base_mapping.get("cloud_manager") - current_cm_tools = current_mapping.get("cloud_manager_tools") - master_cm_tools = base_mapping.get("cloud_manager_tools") - - if current_cm != master_cm or current_cm_tools != master_cm_tools: - added_agents.append((current_cm, current_cm_tools)) - - return list(set(added_agents)) - - def get_tools_version_for_agent(agent_version: str) -> str: """Get tools version for a given agent version from release.json""" release_data = load_current_release_json() @@ -206,26 +158,3 @@ def get_currently_used_agents() -> List[Tuple[str, str]]: except Exception as e: logger.error(f"Error getting currently used agents: {e}") return [] - - -def detect_ops_manager_changes() -> List[Tuple[str, str]]: - """Returns (has_changes, changed_agents_list)""" - logger.info("=== Detecting OM Mapping Changes (Local vs Base) ===") - - current_release = load_current_release_json() - if not current_release: - logger.error("Could not load current local release.json") - return [] - - master_release = load_release_json_from_master() - if not master_release: - logger.warning("Could not load base release.json, assuming changes exist") - return [] - - current_mapping = extract_ops_manager_mapping(current_release) - base_mapping = extract_ops_manager_mapping(master_release) - - if current_mapping != base_mapping: - return get_changed_agents(current_mapping, base_mapping) - else: - return [] diff --git a/scripts/release/atomic_pipeline.py b/scripts/release/atomic_pipeline.py index 17b173d10..838ab61cc 100755 --- a/scripts/release/atomic_pipeline.py +++ b/scripts/release/atomic_pipeline.py @@ -15,7 +15,7 @@ from opentelemetry import trace from lib.base_logger import logger -from scripts.release.agent.detect_ops_manager_changes import ( +from scripts.release.agent.agents_to_rebuild import ( get_all_agents_for_rebuild, get_currently_used_agents, ) diff --git a/scripts/release/tests/test_agents_to_rebuild.py b/scripts/release/tests/test_agents_to_rebuild.py new file mode 100644 index 000000000..129696eb6 --- /dev/null +++ b/scripts/release/tests/test_agents_to_rebuild.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +""" +Tests for scripts.release.agent.agents_to_rebuild.py +""" +import json +import unittest +from unittest.mock import mock_open, patch + +from scripts.release.agent.agents_to_rebuild import ( + extract_ops_manager_mapping, + get_all_agents_for_rebuild, + get_currently_used_agents, + get_tools_version_for_agent, + load_current_release_json, +) + + +class TestDetectOpsManagerChanges(unittest.TestCase): + + def setUp(self): + """Set up test fixtures""" + self.master_release_data = { + "supportedImages": { + "mongodb-agent": { + "opsManagerMapping": { + "cloud_manager": "13.37.0.9590-1", + "cloud_manager_tools": "100.12.2", + "ops_manager": { + "6.0.26": {"agent_version": "12.0.34.7888-1", "tools_version": "100.10.0"}, + "7.0.11": {"agent_version": "107.0.11.8645-1", "tools_version": "100.10.0"}, + }, + } + } + } + } + + self.current_release_data = { + "supportedImages": { + "mongodb-agent": { + "opsManagerMapping": { + "cloud_manager": "13.37.0.9590-1", + "cloud_manager_tools": "100.12.2", + "ops_manager": { + "6.0.26": {"agent_version": "12.0.34.7888-1", "tools_version": "100.10.0"}, + "7.0.11": {"agent_version": "107.0.11.8645-1", "tools_version": "100.10.0"}, + }, + } + } + } + } + + self.evergreen_content = """ +variables: + - &ops_manager_60_latest 6.0.27 # The order/index is important + - &ops_manager_70_latest 7.0.17 # The order/index is important + - &ops_manager_80_latest 8.0.12 # The order/index is important +""" + + def test_extract_ops_manager_mapping_valid(self): + """Test extracting opsManagerMapping from valid release data""" + mapping = extract_ops_manager_mapping(self.master_release_data) + expected = { + "cloud_manager": "13.37.0.9590-1", + "cloud_manager_tools": "100.12.2", + "ops_manager": { + "6.0.26": {"agent_version": "12.0.34.7888-1", "tools_version": "100.10.0"}, + "7.0.11": {"agent_version": "107.0.11.8645-1", "tools_version": "100.10.0"}, + }, + } + self.assertEqual(mapping, expected) + + def test_extract_ops_manager_mapping_empty(self): + """Test extracting from empty/invalid data""" + self.assertEqual(extract_ops_manager_mapping({}), {}) + self.assertEqual(extract_ops_manager_mapping(None), {}) + + def test_extract_ops_manager_mapping_missing_keys(self): + """Test extracting when keys are missing""" + incomplete_data = {"supportedImages": {}} + self.assertEqual(extract_ops_manager_mapping(incomplete_data), {}) + + @patch("builtins.open", new_callable=mock_open) + @patch("os.path.exists", return_value=True) + def test_load_current_release_json_success(self, mock_exists, mock_file): + """Test successfully loading current release.json""" + mock_file.return_value.read.return_value = json.dumps(self.current_release_data) + + result = load_current_release_json() + self.assertEqual(result, self.current_release_data) + + @patch("builtins.open", side_effect=FileNotFoundError) + def test_load_current_release_json_not_found(self, mock_file): + """Test handling missing release.json""" + result = load_current_release_json() + self.assertIsNone(result) + + @patch("builtins.open", new_callable=mock_open) + @patch("os.path.exists", return_value=True) + def test_load_current_release_json_invalid_json(self, mock_exists, mock_file): + """Test handling invalid JSON in release.json""" + mock_file.return_value.read.return_value = "invalid json" + + result = load_current_release_json() + self.assertIsNone(result) + + @patch("scripts.release.agent.agents_to_rebuild.load_current_release_json") + def test_get_tools_version_for_agent_found(self, mock_load): + """Test getting tools version for a known agent version""" + mock_load.return_value = self.current_release_data + result = get_tools_version_for_agent("12.0.34.7888-1") + self.assertEqual(result, "100.10.0") + + @patch("scripts.release.agent.agents_to_rebuild.load_current_release_json") + def test_get_tools_version_for_agent_not_found(self, mock_load): + """Test getting tools version for unknown agent falls back to cloud_manager_tools""" + mock_load.return_value = self.current_release_data + result = get_tools_version_for_agent("unknown-agent-version") + self.assertEqual(result, "100.12.2") + + @patch("scripts.release.agent.agents_to_rebuild.load_current_release_json") + def test_get_tools_version_for_agent_no_release_data(self, mock_load): + """Test getting tools version when release.json cannot be loaded""" + mock_load.return_value = None + result = get_tools_version_for_agent("any-version") + self.assertEqual(result, "100.12.2") + + @patch("scripts.release.agent.agents_to_rebuild.load_current_release_json") + def test_get_all_agents_for_rebuild(self, mock_load): + """Test getting all agents for rebuild from release.json""" + release_data = { + "agentVersion": "13.37.0.9590-1", + "supportedImages": { + "mongodb-agent": { + "opsManagerMapping": { + "cloud_manager": "13.37.0.9590-1", + "cloud_manager_tools": "100.12.2", + "ops_manager": { + "6.0.26": {"agent_version": "12.0.34.7888-1", "tools_version": "100.10.0"}, + "7.0.11": {"agent_version": "107.0.11.8645-1", "tools_version": "100.10.0"}, + }, + } + } + }, + } + mock_load.return_value = release_data + result = get_all_agents_for_rebuild() + + # Should contain ops_manager agents, cloud_manager agent, and main agent + self.assertIn(("12.0.34.7888-1", "100.10.0"), result) + self.assertIn(("107.0.11.8645-1", "100.10.0"), result) + self.assertIn(("13.37.0.9590-1", "100.12.2"), result) + + @patch("scripts.release.agent.agents_to_rebuild.load_current_release_json") + def test_get_all_agents_for_rebuild_no_release_data(self, mock_load): + """Test getting all agents when release.json cannot be loaded""" + mock_load.return_value = None + result = get_all_agents_for_rebuild() + self.assertEqual(result, []) + + @patch("scripts.release.agent.agents_to_rebuild.glob.glob") + @patch("scripts.release.agent.agents_to_rebuild.load_current_release_json") + def test_get_currently_used_agents_no_release_data(self, mock_load, mock_glob): + """Test getting currently used agents when release.json cannot be loaded""" + mock_load.return_value = None + result = get_currently_used_agents() + self.assertEqual(result, []) + + @patch("scripts.release.agent.agents_to_rebuild.glob.glob") + @patch("scripts.release.agent.agents_to_rebuild.load_current_release_json") + @patch("builtins.open", new_callable=mock_open) + @patch("os.path.isfile", return_value=True) + def test_get_currently_used_agents_with_context_files(self, mock_isfile, mock_file, mock_load, mock_glob): + """Test getting currently used agents from context files""" + release_data = { + "agentVersion": "13.37.0.9590-1", + "supportedImages": { + "mongodb-agent": { + "opsManagerMapping": { + "cloud_manager": "13.37.0.9590-1", + "cloud_manager_tools": "100.12.2", + "ops_manager": {}, + } + } + }, + } + mock_load.return_value = release_data + mock_glob.return_value = ["scripts/dev/contexts/test_context"] + mock_file.return_value.read.return_value = "export AGENT_VERSION=12.0.34.7888-1\n" + + result = get_currently_used_agents() + + self.assertIn(("12.0.34.7888-1", "100.12.2"), result) # falls back to cloud_manager_tools + self.assertIn(("13.37.0.9590-1", "100.12.2"), result) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/release/tests/test_detect_ops_manager_changes.py b/scripts/release/tests/test_detect_ops_manager_changes.py deleted file mode 100644 index 35b199cb5..000000000 --- a/scripts/release/tests/test_detect_ops_manager_changes.py +++ /dev/null @@ -1,255 +0,0 @@ -#!/usr/bin/env python3 -""" -Tests for scripts.release.agent.detect_ops_manager_changes.py -""" -import json -import subprocess -import unittest -from unittest.mock import MagicMock, mock_open, patch - -from scripts.release.agent.detect_ops_manager_changes import ( - detect_ops_manager_changes, - extract_ops_manager_mapping, - get_content_from_git, - load_current_release_json, -) - - -class TestDetectOpsManagerChanges(unittest.TestCase): - - def setUp(self): - """Set up test fixtures""" - self.master_release_data = { - "supportedImages": { - "mongodb-agent": { - "opsManagerMapping": { - "cloud_manager": "13.37.0.9590-1", - "cloud_manager_tools": "100.12.2", - "ops_manager": { - "6.0.26": {"agent_version": "12.0.34.7888-1", "tools_version": "100.10.0"}, - "7.0.11": {"agent_version": "107.0.11.8645-1", "tools_version": "100.10.0"}, - }, - } - } - } - } - - self.current_release_data = { - "supportedImages": { - "mongodb-agent": { - "opsManagerMapping": { - "cloud_manager": "13.37.0.9590-1", - "cloud_manager_tools": "100.12.2", - "ops_manager": { - "6.0.26": {"agent_version": "12.0.34.7888-1", "tools_version": "100.10.0"}, - "7.0.11": {"agent_version": "107.0.11.8645-1", "tools_version": "100.10.0"}, - }, - } - } - } - } - - self.evergreen_content = """ -variables: - - &ops_manager_60_latest 6.0.27 # The order/index is important - - &ops_manager_70_latest 7.0.17 # The order/index is important - - &ops_manager_80_latest 8.0.12 # The order/index is important -""" - - def test_extract_ops_manager_mapping_valid(self): - """Test extracting opsManagerMapping from valid release data""" - mapping = extract_ops_manager_mapping(self.master_release_data) - expected = { - "cloud_manager": "13.37.0.9590-1", - "cloud_manager_tools": "100.12.2", - "ops_manager": { - "6.0.26": {"agent_version": "12.0.34.7888-1", "tools_version": "100.10.0"}, - "7.0.11": {"agent_version": "107.0.11.8645-1", "tools_version": "100.10.0"}, - }, - } - self.assertEqual(mapping, expected) - - def test_extract_ops_manager_mapping_empty(self): - """Test extracting from empty/invalid data""" - self.assertEqual(extract_ops_manager_mapping({}), {}) - self.assertEqual(extract_ops_manager_mapping(None), {}) - - def test_extract_ops_manager_mapping_missing_keys(self): - """Test extracting when keys are missing""" - incomplete_data = {"supportedImages": {}} - self.assertEqual(extract_ops_manager_mapping(incomplete_data), {}) - - @patch("builtins.open", new_callable=mock_open) - @patch("os.path.exists", return_value=True) - def test_load_current_release_json_success(self, mock_exists, mock_file): - """Test successfully loading current release.json""" - mock_file.return_value.read.return_value = json.dumps(self.current_release_data) - - result = load_current_release_json() - self.assertEqual(result, self.current_release_data) - - @patch("builtins.open", side_effect=FileNotFoundError) - def test_load_current_release_json_not_found(self, mock_file): - """Test handling missing release.json""" - result = load_current_release_json() - self.assertIsNone(result) - - @patch("builtins.open", new_callable=mock_open) - @patch("os.path.exists", return_value=True) - def test_load_current_release_json_invalid_json(self, mock_exists, mock_file): - """Test handling invalid JSON in release.json""" - mock_file.return_value.read.return_value = "invalid json" - - result = load_current_release_json() - self.assertIsNone(result) - - @patch("subprocess.run") - def test_safe_git_show_success(self, mock_run): - """Test successful git show operation""" - mock_result = MagicMock() - mock_result.stdout = json.dumps(self.master_release_data) - mock_run.return_value = mock_result - - result = get_content_from_git("abc123", "release.json") - self.assertEqual(result, json.dumps(self.master_release_data)) - - mock_run.assert_called_once_with( - ["git", "show", "abc123:release.json"], capture_output=True, text=True, check=True, timeout=30 - ) - - @patch("subprocess.run", side_effect=subprocess.CalledProcessError(1, "git")) - def test_safe_git_show_failure(self, mock_run): - """Test git show failure handling""" - result = get_content_from_git("abc123", "release.json") - self.assertIsNone(result) - - def test_no_changes_detected(self): - """Test when no changes are detected""" - with ( - patch( - "scripts.release.agent.detect_ops_manager_changes.load_current_release_json", - return_value=self.current_release_data, - ), - patch( - "scripts.release.agent.detect_ops_manager_changes.load_release_json_from_master", - return_value=self.master_release_data, - ), - ): - - changed_agents = detect_ops_manager_changes() - self.assertEqual(changed_agents, []) - - def test_new_ops_manager_version_added(self): - """Test detection when new OM version is added""" - modified_current = json.loads(json.dumps(self.current_release_data)) - modified_current["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["ops_manager"]["8.0.0"] = { - "agent_version": "108.0.0.8694-1", - "tools_version": "100.10.0", - } - - with ( - patch( - "scripts.release.agent.detect_ops_manager_changes.load_current_release_json", - return_value=modified_current, - ), - patch( - "scripts.release.agent.detect_ops_manager_changes.load_release_json_from_master", - return_value=self.master_release_data, - ), - ): - - changed_agents = detect_ops_manager_changes() - self.assertIn(("108.0.0.8694-1", "100.10.0"), changed_agents) - - def test_cloud_manager_changed(self): - """Test detection when cloud_manager is changed""" - modified_current = json.loads(json.dumps(self.current_release_data)) - modified_current["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["cloud_manager"] = "13.38.0.9600-1" - - with ( - patch( - "scripts.release.agent.detect_ops_manager_changes.load_current_release_json", - return_value=modified_current, - ), - patch( - "scripts.release.agent.detect_ops_manager_changes.load_release_json_from_master", - return_value=self.master_release_data, - ), - ): - - changed_agents = detect_ops_manager_changes() - self.assertIn(("13.38.0.9600-1", "100.12.2"), changed_agents) - - def test_cloud_manager_tools_changed(self): - """Test detection when cloud_manager_tools is changed""" - modified_current = json.loads(json.dumps(self.current_release_data)) - modified_current["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["cloud_manager_tools"] = "100.13.0" - - with ( - patch( - "scripts.release.agent.detect_ops_manager_changes.load_current_release_json", - return_value=modified_current, - ), - patch( - "scripts.release.agent.detect_ops_manager_changes.load_release_json_from_master", - return_value=self.master_release_data, - ), - ): - - changed_agents = detect_ops_manager_changes() - self.assertIn(("13.37.0.9590-1", "100.13.0"), changed_agents) - - def test_both_om_and_cm_changed(self): - """Test detection when both OM version and cloud manager are changed""" - modified_current = json.loads(json.dumps(self.current_release_data)) - modified_current["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["ops_manager"]["8.0.0"] = { - "agent_version": "108.0.0.8694-1", - "tools_version": "100.10.0", - } - modified_current["supportedImages"]["mongodb-agent"]["opsManagerMapping"]["cloud_manager"] = "13.38.0.9600-1" - - with ( - patch( - "scripts.release.agent.detect_ops_manager_changes.load_current_release_json", - return_value=modified_current, - ), - patch( - "scripts.release.agent.detect_ops_manager_changes.load_release_json_from_master", - return_value=self.master_release_data, - ), - ): - - changed_agents = detect_ops_manager_changes() - self.assertIn(("108.0.0.8694-1", "100.10.0"), changed_agents) - self.assertIn(("13.38.0.9600-1", "100.12.2"), changed_agents) - self.assertEqual(len(changed_agents), 2) - - def test_current_release_load_failure(self): - """Test handling when current release.json cannot be loaded""" - with ( - patch("scripts.release.agent.detect_ops_manager_changes.load_current_release_json", return_value=None), - patch( - "scripts.release.agent.detect_ops_manager_changes.load_release_json_from_master", - return_value=self.master_release_data, - ), - ): - - changed_agents = detect_ops_manager_changes() - self.assertEqual(changed_agents, []) - - def test_base_release_load_failure_fail_safe(self): - """Test fail-safe behavior when base release.json cannot be loaded""" - with ( - patch( - "scripts.release.agent.detect_ops_manager_changes.load_current_release_json", - return_value=self.current_release_data, - ), - patch("scripts.release.agent.detect_ops_manager_changes.load_release_json_from_master", return_value=None), - ): - - changed_agents = detect_ops_manager_changes() - self.assertEqual(changed_agents, []) - - -if __name__ == "__main__": - unittest.main() From 6de3dddc9d29fe12f500246efd8850e0be201e05 Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Mon, 8 Dec 2025 08:57:31 +0100 Subject: [PATCH 09/18] remove not used code --- .../release/tests/test_agents_to_rebuild.py | 81 ------------------- 1 file changed, 81 deletions(-) diff --git a/scripts/release/tests/test_agents_to_rebuild.py b/scripts/release/tests/test_agents_to_rebuild.py index 129696eb6..c182f8355 100644 --- a/scripts/release/tests/test_agents_to_rebuild.py +++ b/scripts/release/tests/test_agents_to_rebuild.py @@ -56,73 +56,6 @@ def setUp(self): - &ops_manager_80_latest 8.0.12 # The order/index is important """ - def test_extract_ops_manager_mapping_valid(self): - """Test extracting opsManagerMapping from valid release data""" - mapping = extract_ops_manager_mapping(self.master_release_data) - expected = { - "cloud_manager": "13.37.0.9590-1", - "cloud_manager_tools": "100.12.2", - "ops_manager": { - "6.0.26": {"agent_version": "12.0.34.7888-1", "tools_version": "100.10.0"}, - "7.0.11": {"agent_version": "107.0.11.8645-1", "tools_version": "100.10.0"}, - }, - } - self.assertEqual(mapping, expected) - - def test_extract_ops_manager_mapping_empty(self): - """Test extracting from empty/invalid data""" - self.assertEqual(extract_ops_manager_mapping({}), {}) - self.assertEqual(extract_ops_manager_mapping(None), {}) - - def test_extract_ops_manager_mapping_missing_keys(self): - """Test extracting when keys are missing""" - incomplete_data = {"supportedImages": {}} - self.assertEqual(extract_ops_manager_mapping(incomplete_data), {}) - - @patch("builtins.open", new_callable=mock_open) - @patch("os.path.exists", return_value=True) - def test_load_current_release_json_success(self, mock_exists, mock_file): - """Test successfully loading current release.json""" - mock_file.return_value.read.return_value = json.dumps(self.current_release_data) - - result = load_current_release_json() - self.assertEqual(result, self.current_release_data) - - @patch("builtins.open", side_effect=FileNotFoundError) - def test_load_current_release_json_not_found(self, mock_file): - """Test handling missing release.json""" - result = load_current_release_json() - self.assertIsNone(result) - - @patch("builtins.open", new_callable=mock_open) - @patch("os.path.exists", return_value=True) - def test_load_current_release_json_invalid_json(self, mock_exists, mock_file): - """Test handling invalid JSON in release.json""" - mock_file.return_value.read.return_value = "invalid json" - - result = load_current_release_json() - self.assertIsNone(result) - - @patch("scripts.release.agent.agents_to_rebuild.load_current_release_json") - def test_get_tools_version_for_agent_found(self, mock_load): - """Test getting tools version for a known agent version""" - mock_load.return_value = self.current_release_data - result = get_tools_version_for_agent("12.0.34.7888-1") - self.assertEqual(result, "100.10.0") - - @patch("scripts.release.agent.agents_to_rebuild.load_current_release_json") - def test_get_tools_version_for_agent_not_found(self, mock_load): - """Test getting tools version for unknown agent falls back to cloud_manager_tools""" - mock_load.return_value = self.current_release_data - result = get_tools_version_for_agent("unknown-agent-version") - self.assertEqual(result, "100.12.2") - - @patch("scripts.release.agent.agents_to_rebuild.load_current_release_json") - def test_get_tools_version_for_agent_no_release_data(self, mock_load): - """Test getting tools version when release.json cannot be loaded""" - mock_load.return_value = None - result = get_tools_version_for_agent("any-version") - self.assertEqual(result, "100.12.2") @patch("scripts.release.agent.agents_to_rebuild.load_current_release_json") def test_get_all_agents_for_rebuild(self, mock_load): @@ -150,20 +83,6 @@ def test_get_all_agents_for_rebuild(self, mock_load): self.assertIn(("107.0.11.8645-1", "100.10.0"), result) self.assertIn(("13.37.0.9590-1", "100.12.2"), result) - @patch("scripts.release.agent.agents_to_rebuild.load_current_release_json") - def test_get_all_agents_for_rebuild_no_release_data(self, mock_load): - """Test getting all agents when release.json cannot be loaded""" - mock_load.return_value = None - result = get_all_agents_for_rebuild() - self.assertEqual(result, []) - - @patch("scripts.release.agent.agents_to_rebuild.glob.glob") - @patch("scripts.release.agent.agents_to_rebuild.load_current_release_json") - def test_get_currently_used_agents_no_release_data(self, mock_load, mock_glob): - """Test getting currently used agents when release.json cannot be loaded""" - mock_load.return_value = None - result = get_currently_used_agents() - self.assertEqual(result, []) @patch("scripts.release.agent.agents_to_rebuild.glob.glob") @patch("scripts.release.agent.agents_to_rebuild.load_current_release_json") From fe2246ed3415b15b4f016d6d7038d0c8e620cdca Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Mon, 8 Dec 2025 09:11:19 +0100 Subject: [PATCH 10/18] remove all release agent logic --- .evergreen.yml | 35 ----------------------------- scripts/release/release_on_merge.py | 2 +- 2 files changed, 1 insertion(+), 36 deletions(-) diff --git a/.evergreen.yml b/.evergreen.yml index 2c002853b..bf4b63127 100644 --- a/.evergreen.yml +++ b/.evergreen.yml @@ -303,19 +303,6 @@ tasks: args: - scripts/release/release_on_merge.py - - name: release_agent - # this enables us to run this variant manually (patch) - allowed_requesters: [ "patch" ] - commands: - - func: clone - - func: setup_building_host - - func: quay_login - - func: setup_docker_sbom - - func: pipeline - vars: - image_name: agent - build_scenario: release - - name: migrate_all_agents # this enables us to run this variant manually to build all the agents for the new agent registry allowed_requesters: [ "patch" ] @@ -1899,26 +1886,6 @@ buildvariants: tasks: - name: build_om_images - # It will be called by pct while bumping the agent cloud manager image - - name: release_agent - display_name: release_agent - tags: [ "manual_patch", "release_agent" ] - run_on: - - release-ubuntu2404-small # This is required for CISA attestation https://jira.mongodb.org/browse/DEVPROD-17780 - depends_on: - - variant: init_test_run - name: build_agent_images_ubi # this ensures the agent gets released to ECR as well - - variant: e2e_multi_cluster_kind - name: '*' - - variant: e2e_static_multi_cluster_2_clusters - name: '*' - - variant: e2e_mdb_kind_ubi_cloudqa - name: '*' - - variant: e2e_static_mdb_kind_ubi_cloudqa - name: '*' - tasks: - - name: release_agent - # Only called manually, It's used for testing the task release_agents in case the release.json # has not changed, and you still want to push the images to registry. - name: manual_release_all_agents @@ -1965,7 +1932,6 @@ buildvariants: name: '*' tasks: - name: publish_ops_manager - - name: release_agent - name: publish_om80_images display_name: publish_om80_images @@ -1980,7 +1946,6 @@ buildvariants: name: '*' tasks: - name: publish_ops_manager - - name: release_agent - name: migrate_all_agents display_name: migrate_all_agents diff --git a/scripts/release/release_on_merge.py b/scripts/release/release_on_merge.py index d6e9c511c..f81938520 100644 --- a/scripts/release/release_on_merge.py +++ b/scripts/release/release_on_merge.py @@ -177,7 +177,7 @@ def main(): else: logger.warning("cloud_manager agent not found in release.json") - # 2. Release each OM version and its agent + # 2. Release each OM Major version and its agent om_mapping = ops_manager_mapping.get("ops_manager", {}) for major, om_version in latest_om_versions_per_major.items(): From 1a24d0d688fd1c8cf01e9ef0cfb1df513d60cfd3 Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Mon, 8 Dec 2025 10:00:16 +0100 Subject: [PATCH 11/18] remove all release agent logic --- .evergreen-functions.yml | 1 + .evergreen.yml | 9 ++++-- scripts/release/atomic_pipeline.py | 14 ++++----- .../build/image_build_configuration.py | 2 -- scripts/release/pipeline.py | 30 ++----------------- scripts/release/pipeline.sh | 5 ++-- 6 files changed, 19 insertions(+), 42 deletions(-) diff --git a/.evergreen-functions.yml b/.evergreen-functions.yml index b56dd947a..83b6f4d08 100644 --- a/.evergreen-functions.yml +++ b/.evergreen-functions.yml @@ -496,6 +496,7 @@ functions: IMAGE_NAME: ${image_name} BUILD_SCENARIO_OVERRIDE: ${build_scenario} FLAGS: ${flags} + AGENT_VERSION_OVERRIDE: ${agent_version} teardown_cloud_qa_all: - command: shell.exec diff --git a/.evergreen.yml b/.evergreen.yml index bf4b63127..9d847aa7d 100644 --- a/.evergreen.yml +++ b/.evergreen.yml @@ -345,8 +345,9 @@ tasks: - func: quay_login - func: pipeline vars: + agent_version: all image_name: agent - flags: "--parallel --all-agents --skip-if-exists=false" + flags: "--parallel --skip-if-exists=false" - name: rebuild_currently_used_agents # this enables us to run this manually (patch) and rebuild current agent versions to verify @@ -358,8 +359,9 @@ tasks: - func: quay_login - func: pipeline vars: + agent_version: current image_name: agent - flags: "--parallel --current-agents --skip-if-exists=false" + flags: "--parallel --skip-if-exists=false" - name: build_kubectl_mongodb_plugin commands: @@ -465,7 +467,8 @@ tasks: - func: pipeline vars: image_name: agent - flags: "--parallel --all-agents" + agent_version: all + flags: "--parallel" - name: build_init_database_image_ubi commands: diff --git a/scripts/release/atomic_pipeline.py b/scripts/release/atomic_pipeline.py index 838ab61cc..19c936fdd 100755 --- a/scripts/release/atomic_pipeline.py +++ b/scripts/release/atomic_pipeline.py @@ -330,17 +330,17 @@ def build_upgrade_hook_image(build_configuration: ImageBuildConfiguration): def build_agent(build_configuration: ImageBuildConfiguration): """Build the agent image(s). Validation happens in pipeline.py.""" - if build_configuration.version and build_configuration.agent_tools_version: - agent_versions_to_build = [(build_configuration.version, build_configuration.agent_tools_version)] - logger.info( - f"building agent {build_configuration.version} with tools {build_configuration.agent_tools_version}" - ) - elif build_configuration.all_agents: + version = build_configuration.version + + if version == "all": agent_versions_to_build = get_all_agents_for_rebuild() logger.info("building all agents") - elif build_configuration.currently_used_agents: + elif version == "current": agent_versions_to_build = get_currently_used_agents() logger.info("building currently used agents") + elif version and build_configuration.agent_tools_version: + agent_versions_to_build = [(version, build_configuration.agent_tools_version)] + logger.info(f"building agent {version} with tools {build_configuration.agent_tools_version}") else: raise ValueError("No agent selection provided - this should be caught by pipeline.py validation") diff --git a/scripts/release/build/image_build_configuration.py b/scripts/release/build/image_build_configuration.py index 216d036c9..da1007235 100644 --- a/scripts/release/build/image_build_configuration.py +++ b/scripts/release/build/image_build_configuration.py @@ -23,8 +23,6 @@ class ImageBuildConfiguration: # Agent specific parallel: bool = False parallel_factor: int = 0 - all_agents: bool = False - currently_used_agents: bool = False architecture_suffix: bool = False agent_tools_version: Optional[str] = None # Explicit tools version for agent builds diff --git a/scripts/release/pipeline.py b/scripts/release/pipeline.py index dd7451ee4..9238d74bc 100644 --- a/scripts/release/pipeline.py +++ b/scripts/release/pipeline.py @@ -142,21 +142,7 @@ def image_build_config_from_args(args) -> ImageBuildConfiguration: # Get agent_tools_version for agent builds (from --agent-tools-version arg) agent_tools_version = getattr(args, "agent_tools_version", None) - # Validate version requirements - if image == "agent": - # Agent builds require explicit selection: version+agent_tools_version OR --all-agents OR --current-agents - has_explicit_version = version is not None and agent_tools_version is not None - has_agent_flag = args.all_agents or args.current_agents - if not has_explicit_version and not has_agent_flag: - raise ValueError( - "Agent build requires explicit selection. Use one of:\n" - " --version --agent-tools-version (for specific agent)\n" - " --all-agents (for all agents in release.json)\n" - " --current-agents (for currently used agents)" - ) - if version is not None and agent_tools_version is None: - raise ValueError("For agent builds with explicit version, --agent-tools-version must also be provided.") - elif version is None: + if version is None: raise ValueError(f"Version cannot be empty for {image}.") return ImageBuildConfiguration( @@ -172,8 +158,6 @@ def image_build_config_from_args(args) -> ImageBuildConfiguration: skip_if_exists=skip_if_exists, parallel=args.parallel, parallel_factor=args.parallel_factor, - all_agents=args.all_agents, - currently_used_agents=args.current_agents, architecture_suffix=architecture_suffix, agent_tools_version=agent_tools_version, ) @@ -289,22 +273,12 @@ def main(): type=int, help="Number of agent builds to run in parallel, defaults to number of cores", ) - parser.add_argument( - "--all-agents", - action="store_true", - help="Build all agent images.", - ) - parser.add_argument( - "--current-agents", - action="store_true", - help="Build all currently used agent images.", - ) parser.add_argument( "--agent-tools-version", metavar="", action="store", type=str, - help="Tools version to use when building agent image. Required when --version is provided for agent builds.", + help="Tools version to use when building agent image. Required when --version is an explicit version (not 'all' or 'current').", ) parser.add_argument( "--architecture-suffix", diff --git a/scripts/release/pipeline.sh b/scripts/release/pipeline.sh index 266d17a89..53b965173 100755 --- a/scripts/release/pipeline.sh +++ b/scripts/release/pipeline.sh @@ -9,8 +9,9 @@ args+=(--build-scenario "${BUILD_SCENARIO_OVERRIDE:-${BUILD_SCENARIO}}") case ${IMAGE_NAME} in "agent") - # Can also use --all-agents or --current-agents flags instead - IMAGE_VERSION="${AGENT_VERSION:-}" + # For agent: version can be "all", "current", or explicit version (requires TOOLS_VERSION) + # AGENT_VERSION_OVERRIDE takes precedence, then AGENT_VERSION from context. + IMAGE_VERSION="${AGENT_VERSION_OVERRIDE:-${AGENT_VERSION:-}}" ;; "ops-manager") From 855593f3592157b5055be70fde2f7f73756ce851 Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Mon, 8 Dec 2025 10:08:19 +0100 Subject: [PATCH 12/18] remove all release agent logic --- scripts/release/pipeline.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/scripts/release/pipeline.py b/scripts/release/pipeline.py index 9238d74bc..3c653fdbf 100644 --- a/scripts/release/pipeline.py +++ b/scripts/release/pipeline.py @@ -142,7 +142,22 @@ def image_build_config_from_args(args) -> ImageBuildConfiguration: # Get agent_tools_version for agent builds (from --agent-tools-version arg) agent_tools_version = getattr(args, "agent_tools_version", None) - if version is None: + # Validate version requirements + if image == "agent": + # Agent builds: version can be "all", "current", or explicit version (requires agent_tools_version) + if version is None: + raise ValueError( + "Agent build requires --version. Use one of:\n" + " --version all (for all agents in release.json)\n" + " --version current (for currently used agents)\n" + " --version --agent-tools-version (for specific agent)" + ) + is_special_version = version in ("all", "current") + if not is_special_version and agent_tools_version is None: + raise ValueError( + f"For agent builds with explicit version '{version}', --agent-tools-version must also be provided." + ) + elif version is None: raise ValueError(f"Version cannot be empty for {image}.") return ImageBuildConfiguration( From e55021e1482f544c48c7dbdceb13c4e3f4056efb Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Mon, 8 Dec 2025 10:14:42 +0100 Subject: [PATCH 13/18] linter --- scripts/release/tests/test_agents_to_rebuild.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/release/tests/test_agents_to_rebuild.py b/scripts/release/tests/test_agents_to_rebuild.py index c182f8355..77e8bc135 100644 --- a/scripts/release/tests/test_agents_to_rebuild.py +++ b/scripts/release/tests/test_agents_to_rebuild.py @@ -56,7 +56,6 @@ def setUp(self): - &ops_manager_80_latest 8.0.12 # The order/index is important """ - @patch("scripts.release.agent.agents_to_rebuild.load_current_release_json") def test_get_all_agents_for_rebuild(self, mock_load): """Test getting all agents for rebuild from release.json""" @@ -83,7 +82,6 @@ def test_get_all_agents_for_rebuild(self, mock_load): self.assertIn(("107.0.11.8645-1", "100.10.0"), result) self.assertIn(("13.37.0.9590-1", "100.12.2"), result) - @patch("scripts.release.agent.agents_to_rebuild.glob.glob") @patch("scripts.release.agent.agents_to_rebuild.load_current_release_json") @patch("builtins.open", new_callable=mock_open) From 017b91bea749b182ab863c7c5151f6f3387dfa71 Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Mon, 8 Dec 2025 13:41:13 +0100 Subject: [PATCH 14/18] address feedback --- .evergreen.yml | 12 +++--- ...e_on_merge.py => release_om_and_agents.py} | 39 ++++++++++++------- 2 files changed, 31 insertions(+), 20 deletions(-) rename scripts/release/{release_on_merge.py => release_om_and_agents.py} (86%) diff --git a/.evergreen.yml b/.evergreen.yml index 7f9b61636..346dfede1 100644 --- a/.evergreen.yml +++ b/.evergreen.yml @@ -288,7 +288,7 @@ tasks: - func: lint_repo # Runs on every merge - detects release.json changes and releases appropriate images - - name: release_on_merge + - name: release_om_and_agents allowed_requesters: [ "patch" , "commit"] commands: - func: clone @@ -301,7 +301,7 @@ tasks: working_dir: src/github.com/mongodb/mongodb-kubernetes binary: scripts/dev/run_python.sh args: - - scripts/release/release_on_merge.py + - scripts/release/release_om_and_agents.py - name: migrate_all_agents # this enables us to run this variant manually to build all the agents for the new agent registry @@ -754,7 +754,7 @@ task_groups: - e2e_sharded_cluster_scram_sha_256_switch_project - e2e_replica_set_scram_sha_1_switch_project - e2e_sharded_cluster_scram_sha_1_switch_project - # TODO CLOUDP-349093 - Disabled these tests as they don't use the password secret, and project migrations aren't fully supported yet. + # TODO CLOUDP-349093 - Disabled these tests as they don't use the password secret, and project migrations aren't fully supported yet. # e2e_sharded_cluster_x509_switch_project # e2e_replica_set_x509_switch_project # e2e_replica_set_ldap_switch_project @@ -1969,11 +1969,11 @@ buildvariants: - name: migrate_all_agents # Runs on every merge to master and releases images auxiliary to the operator release like OM and the Agent - - name: release_on_merge - display_name: release_on_merge + - name: release_om_and_agents + display_name: release_om_and_agents allowed_requesters: [ "patch" , "commit"] run_on: - release-ubuntu2404-small patchable: false # Only run on commit builds tasks: - - name: release_on_merge + - name: release_om_and_agents diff --git a/scripts/release/release_on_merge.py b/scripts/release/release_om_and_agents.py similarity index 86% rename from scripts/release/release_on_merge.py rename to scripts/release/release_om_and_agents.py index f81938520..f4f7b101e 100644 --- a/scripts/release/release_on_merge.py +++ b/scripts/release/release_om_and_agents.py @@ -11,16 +11,18 @@ skip_if_exists handles already-published images. Usage: - python release_on_merge.py [--dry-run] + python release_om_and_agents.py [--dry-run] """ import json import logging -import re import subprocess import sys from pathlib import Path from typing import Dict, List +import semver +import yaml + logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", @@ -92,7 +94,7 @@ def get_ops_manager_mapping(release_data: Dict) -> Dict: def get_latest_om_versions_from_evergreen_yaml() -> Dict[str, str]: """ - Extract OM versions from .evergreen.yml anchors. + Extract OM versions from .evergreen.yml anchors Returns: {"60": "6.0.27", "70": "7.0.19", "80": "8.0.16"} """ @@ -103,16 +105,24 @@ def get_latest_om_versions_from_evergreen_yaml() -> Dict[str, str]: logger.error(".evergreen.yml not found") return versions - content = evergreen_path.read_text() - - # Match patterns like: - &ops_manager_60_latest 6.0.27 # - pattern = r"-\s*&ops_manager_(\d+)_latest\s+(\S+)\s+#" - - for match in re.finditer(pattern, content): - major = match.group(1) # "60", "70", "80" - version = match.group(2) # "6.0.27", "7.0.19", "8.0.16" - versions[major] = version - logger.info(f"Found OM {major}: {version}") + with open(evergreen_path, "r") as f: + data = yaml.safe_load(f) + + # Extract version strings from the variables list + # The YAML structure is: variables: [6.0.27, 7.0.19, 8.0.16, ...] + variables = data.get("variables", []) + + for var in variables: + # Try to parse as semver + try: + version_info = semver.VersionInfo.parse(var) + # Use major*10 + minor as the key (e.g., 6.0.x -> "60", 7.0.x -> "70") + major_key = f"{version_info.major}{version_info.minor}" + versions[major_key] = var + logger.info(f"Found OM {major_key}: {var}") + except ValueError: + # Not a valid semver string, skip it + continue return versions @@ -198,7 +208,8 @@ def main(): if not release_agent(agent_version, tools_version, f"OM {om_version}", args.dry_run): success = False else: - logger.warning(f"No agent found for OM {om_version} in release.json") + logger.critical(f"No agent found for OM {om_version} in release.json") + sys.exit(1) # Always print summary print_summary(args.dry_run) From 6ab771483d4ee8031d62276cd64d19e5b8ab977b Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Mon, 8 Dec 2025 14:37:13 +0100 Subject: [PATCH 15/18] consistent raising errors --- .evergreen.yml | 28 -------- scripts/release/release_om_and_agents.py | 82 +++++++++++++++--------- 2 files changed, 53 insertions(+), 57 deletions(-) diff --git a/.evergreen.yml b/.evergreen.yml index 346dfede1..bc6c0906d 100644 --- a/.evergreen.yml +++ b/.evergreen.yml @@ -1931,34 +1931,6 @@ buildvariants: - name: backup_csv_images_limit_3 - name: backup_csv_images_all - - name: publish_om70_images - display_name: publish_om70_images - tags: [ "manual_patch" ] - allowed_requesters: [ "patch"] - run_on: - - release-ubuntu2404-small # This is required for CISA attestation https://jira.mongodb.org/browse/DEVPROD-17780 - depends_on: - - variant: e2e_om70_kind_ubi - name: '*' - - variant: e2e_static_om70_kind_ubi - name: '*' - tasks: - - name: publish_ops_manager - - - name: publish_om80_images - display_name: publish_om80_images - tags: [ "manual_patch" ] - allowed_requesters: [ "patch" ] - run_on: - - release-ubuntu2404-small # This is required for CISA attestation https://jira.mongodb.org/browse/DEVPROD-17780 - depends_on: - - variant: e2e_om80_kind_ubi - name: '*' - - variant: e2e_static_om80_kind_ubi - name: '*' - tasks: - - name: publish_ops_manager - - name: migrate_all_agents display_name: migrate_all_agents tags: [ "manual_patch" ] diff --git a/scripts/release/release_om_and_agents.py b/scripts/release/release_om_and_agents.py index f4f7b101e..c32f35b84 100644 --- a/scripts/release/release_om_and_agents.py +++ b/scripts/release/release_om_and_agents.py @@ -13,6 +13,7 @@ Usage: python release_om_and_agents.py [--dry-run] """ +import argparse import json import logging import subprocess @@ -94,36 +95,55 @@ def get_ops_manager_mapping(release_data: Dict) -> Dict: def get_latest_om_versions_from_evergreen_yaml() -> Dict[str, str]: """ - Extract OM versions from .evergreen.yml anchors + Extract OM versions from .evergreen.yml anchors using YAML parsing and semver. Returns: {"60": "6.0.27", "70": "7.0.19", "80": "8.0.16"} + Raises: RuntimeError if file is missing, malformed, or contains no valid versions """ versions = {} evergreen_path = Path(".evergreen.yml") if not evergreen_path.exists(): - logger.error(".evergreen.yml not found") - return versions + raise RuntimeError(f".evergreen.yml not found at {evergreen_path.absolute()}") - with open(evergreen_path, "r") as f: - data = yaml.safe_load(f) + try: + with open(evergreen_path, "r") as f: + data = yaml.safe_load(f) + except yaml.YAMLError as e: + raise RuntimeError(f"Failed to parse .evergreen.yml: {e}") from e + except Exception as e: + raise RuntimeError(f"Failed to read .evergreen.yml: {e}") from e # Extract version strings from the variables list # The YAML structure is: variables: [6.0.27, 7.0.19, 8.0.16, ...] variables = data.get("variables", []) + if not variables: + raise RuntimeError("No 'variables' list found in .evergreen.yml") + for var in variables: + # Skip non-string entries (like dicts, lists, etc.) + if not isinstance(var, str): + logger.debug(f"Skipping non-string variable: {type(var).__name__}") + continue + # Try to parse as semver try: version_info = semver.VersionInfo.parse(var) - # Use major*10 + minor as the key (e.g., 6.0.x -> "60", 7.0.x -> "70") + # Use major + minor as the key (e.g., 6.0.x -> "60", 7.0.x -> "70") major_key = f"{version_info.major}{version_info.minor}" versions[major_key] = var logger.info(f"Found OM {major_key}: {var}") - except ValueError: - # Not a valid semver string, skip it + except (ValueError, TypeError): + # Not a valid semver string, skip it silently (expected for non-version variables) continue + if not versions: + raise RuntimeError( + "No valid OM versions found in .evergreen.yml variables list. " + "Expected semver strings like '6.0.27', '7.0.19', '8.0.16'" + ) + return versions @@ -164,8 +184,6 @@ def release_ops_manager(om_version: str, dry_run: bool = False) -> bool: def main(): - import argparse - parser = argparse.ArgumentParser(description="Release all images on merge") parser.add_argument("--dry-run", action="store_true", help="Print commands without executing") args = parser.parse_args() @@ -174,19 +192,28 @@ def main(): ops_manager_mapping = get_ops_manager_mapping(release_data) latest_om_versions_per_major = get_latest_om_versions_from_evergreen_yaml() - success = True + release_cm_agent(args, ops_manager_mapping) + release_om_and_agent(args, latest_om_versions_per_major, ops_manager_mapping) + # Always print summary + print_summary(args.dry_run) + return 0 + + +def release_cm_agent(args, ops_manager_mapping: dict): # 1. Release latest cloud_manager agent cloud_manager_agent = ops_manager_mapping.get("cloud_manager") cloud_manager_tools = ops_manager_mapping.get("cloud_manager_tools") - if cloud_manager_agent and cloud_manager_tools: - logger.info(f"=== Releasing cloud_manager agent: {cloud_manager_agent} ===") - if not release_agent(cloud_manager_agent, cloud_manager_tools, "cloud_manager", args.dry_run): - success = False - else: - logger.warning("cloud_manager agent not found in release.json") + if not cloud_manager_agent or not cloud_manager_tools: + raise RuntimeError("cloud_manager agent or tools not found in release.json opsManagerMapping") + + logger.info(f"=== Releasing cloud_manager agent: {cloud_manager_agent} ===") + if not release_agent(cloud_manager_agent, cloud_manager_tools, "cloud_manager", args.dry_run): + raise RuntimeError(f"Failed to release cloud_manager agent {cloud_manager_agent}") + +def release_om_and_agent(args, latest_om_versions_per_major: dict[str, str], ops_manager_mapping: dict): # 2. Release each OM Major version and its agent om_mapping = ops_manager_mapping.get("ops_manager", {}) @@ -196,25 +223,22 @@ def main(): # Release ops-manager image logger.info(f"Releasing ops-manager {om_version}") if not release_ops_manager(om_version, args.dry_run): - success = False + raise RuntimeError(f"Failed to release ops-manager {om_version}") # Release agent for this OM version agent_info = om_mapping.get(om_version, {}) agent_version = agent_info.get("agent_version") tools_version = agent_info.get("tools_version") - if agent_version and tools_version: - logger.info(f"Releasing agent {agent_version} for OM {om_version}") - if not release_agent(agent_version, tools_version, f"OM {om_version}", args.dry_run): - success = False - else: - logger.critical(f"No agent found for OM {om_version} in release.json") - sys.exit(1) - - # Always print summary - print_summary(args.dry_run) + if not agent_version or not tools_version: + raise RuntimeError( + f"Agent mapping incomplete for OM {om_version} in release.json. " + f"Found: agent_version={agent_version}, tools_version={tools_version}" + ) - return 0 if success else 1 + logger.info(f"Releasing agent {agent_version} for OM {om_version}") + if not release_agent(agent_version, tools_version, f"OM {om_version}", args.dry_run): + raise RuntimeError(f"Failed to release agent {agent_version} for OM {om_version}") if __name__ == "__main__": From a062e12ad03dbb6468b531aa8d6357fd422396b8 Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Mon, 8 Dec 2025 14:51:10 +0100 Subject: [PATCH 16/18] consistent raising errors --- scripts/release/release_om_and_agents.py | 34 +++++++++++------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/scripts/release/release_om_and_agents.py b/scripts/release/release_om_and_agents.py index c32f35b84..881bfeac1 100644 --- a/scripts/release/release_om_and_agents.py +++ b/scripts/release/release_om_and_agents.py @@ -19,7 +19,7 @@ import subprocess import sys from pathlib import Path -from typing import Dict, List +from typing import Dict, List, Set import semver import yaml @@ -93,14 +93,14 @@ def get_ops_manager_mapping(release_data: Dict) -> Dict: return release_data.get("supportedImages", {}).get("mongodb-agent", {}).get("opsManagerMapping", {}) -def get_latest_om_versions_from_evergreen_yaml() -> Dict[str, str]: +def get_latest_om_versions_from_evergreen_yaml() -> Set[str]: """ Extract OM versions from .evergreen.yml anchors using YAML parsing and semver. - Returns: {"60": "6.0.27", "70": "7.0.19", "80": "8.0.16"} + Returns: {"6.0.27", "7.0.19", "8.0.16"} Raises: RuntimeError if file is missing, malformed, or contains no valid versions """ - versions = {} + versions = set() evergreen_path = Path(".evergreen.yml") if not evergreen_path.exists(): @@ -122,22 +122,18 @@ def get_latest_om_versions_from_evergreen_yaml() -> Dict[str, str]: raise RuntimeError("No 'variables' list found in .evergreen.yml") for var in variables: - # Skip non-string entries (like dicts, lists, etc.) if not isinstance(var, str): logger.debug(f"Skipping non-string variable: {type(var).__name__}") continue - # Try to parse as semver try: - version_info = semver.VersionInfo.parse(var) - # Use major + minor as the key (e.g., 6.0.x -> "60", 7.0.x -> "70") - major_key = f"{version_info.major}{version_info.minor}" - versions[major_key] = var - logger.info(f"Found OM {major_key}: {var}") - except (ValueError, TypeError): - # Not a valid semver string, skip it silently (expected for non-version variables) + semver.VersionInfo.parse(var) + except (ValueError, TypeError) as e: + logger.debug(f"Skipping non-semver variable '{var}': {e}") continue + versions.add(var) + if not versions: raise RuntimeError( "No valid OM versions found in .evergreen.yml variables list. " @@ -190,10 +186,10 @@ def main(): release_data = load_release_json() ops_manager_mapping = get_ops_manager_mapping(release_data) - latest_om_versions_per_major = get_latest_om_versions_from_evergreen_yaml() + latest_om_versions = get_latest_om_versions_from_evergreen_yaml() release_cm_agent(args, ops_manager_mapping) - release_om_and_agent(args, latest_om_versions_per_major, ops_manager_mapping) + release_om_and_agent(args, latest_om_versions, ops_manager_mapping) # Always print summary print_summary(args.dry_run) @@ -213,12 +209,12 @@ def release_cm_agent(args, ops_manager_mapping: dict): raise RuntimeError(f"Failed to release cloud_manager agent {cloud_manager_agent}") -def release_om_and_agent(args, latest_om_versions_per_major: dict[str, str], ops_manager_mapping: dict): - # 2. Release each OM Major version and its agent +def release_om_and_agent(args, latest_om_versions: set[str], ops_manager_mapping: dict): + # 2. Release each OM version and its agent om_mapping = ops_manager_mapping.get("ops_manager", {}) - for major, om_version in latest_om_versions_per_major.items(): - logger.info(f"=== Processing OM {major} ({om_version}) ===") + for om_version in sorted(latest_om_versions): + logger.info(f"=== Processing OM {om_version} ===") # Release ops-manager image logger.info(f"Releasing ops-manager {om_version}") From 0b6d0e5611768a99f7a0927ddf9740871534f9ae Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Mon, 8 Dec 2025 15:07:40 +0100 Subject: [PATCH 17/18] address feedback --- scripts/release/release_om_and_agents.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/scripts/release/release_om_and_agents.py b/scripts/release/release_om_and_agents.py index 881bfeac1..a5b261bce 100644 --- a/scripts/release/release_om_and_agents.py +++ b/scripts/release/release_om_and_agents.py @@ -215,13 +215,6 @@ def release_om_and_agent(args, latest_om_versions: set[str], ops_manager_mapping for om_version in sorted(latest_om_versions): logger.info(f"=== Processing OM {om_version} ===") - - # Release ops-manager image - logger.info(f"Releasing ops-manager {om_version}") - if not release_ops_manager(om_version, args.dry_run): - raise RuntimeError(f"Failed to release ops-manager {om_version}") - - # Release agent for this OM version agent_info = om_mapping.get(om_version, {}) agent_version = agent_info.get("agent_version") tools_version = agent_info.get("tools_version") @@ -232,6 +225,12 @@ def release_om_and_agent(args, latest_om_versions: set[str], ops_manager_mapping f"Found: agent_version={agent_version}, tools_version={tools_version}" ) + # Release ops-manager image + logger.info(f"Releasing ops-manager {om_version}") + if not release_ops_manager(om_version, args.dry_run): + raise RuntimeError(f"Failed to release ops-manager {om_version}") + + # Release agent for this OM version logger.info(f"Releasing agent {agent_version} for OM {om_version}") if not release_agent(agent_version, tools_version, f"OM {om_version}", args.dry_run): raise RuntimeError(f"Failed to release agent {agent_version} for OM {om_version}") From bff15bfc42f8a662c2f9076c9a08978ccbc12576 Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Wed, 10 Dec 2025 15:53:09 +0100 Subject: [PATCH 18/18] constants --- scripts/release/pipeline.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/release/pipeline.py b/scripts/release/pipeline.py index 3c653fdbf..1048a983f 100644 --- a/scripts/release/pipeline.py +++ b/scripts/release/pipeline.py @@ -62,6 +62,9 @@ ) from scripts.release.build.image_build_process import PodmanImageBuilder +CURRENT_AGENTS = "current" +ALL_AGENTS = "all" + """ The goal of main.py, image_build_configuration.py and build_context.py is to provide a single source of truth for the build configuration. All parameters that depend on the the build environment (local dev, evg, etc) should be resolved here and @@ -152,7 +155,7 @@ def image_build_config_from_args(args) -> ImageBuildConfiguration: " --version current (for currently used agents)\n" " --version --agent-tools-version (for specific agent)" ) - is_special_version = version in ("all", "current") + is_special_version = version in (ALL_AGENTS, CURRENT_AGENTS) if not is_special_version and agent_tools_version is None: raise ValueError( f"For agent builds with explicit version '{version}', --agent-tools-version must also be provided."