diff --git a/build_info.json b/build_info.json new file mode 100644 index 000000000..10935b7ef --- /dev/null +++ b/build_info.json @@ -0,0 +1,201 @@ +{ + "images": { + "mongodbOperator": { + "patch": { + "repository": "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes", + "platforms": [ + "linux/amd64" + ] + }, + "staging": { + "repository": "quay.io/mongodb/mongodb-kubernetes-stg", + "platforms": [ + "linux/arm64", + "linux/amd64" + ] + }, + "release": { + "repository": "quay.io/mongodb/mongodb-kubernetes", + "platforms": [ + "linux/arm64", + "linux/amd64" + ] + } + }, + "initDatabase": { + "patch": { + "repository": "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes-init-database", + "platforms": [ + "linux/amd64" + ] + }, + "staging": { + "repository": "quay.io/mongodb/mongodb-kubernetes-init-database-stg", + "platforms": [ + "linux/arm64", + "linux/amd64" + ] + }, + "release": { + "repository": "quay.io/mongodb/mongodb-kubernetes-init-database", + "platforms": [ + "linux/arm64", + "linux/amd64" + ] + } + }, + "initAppDb": { + "patch": { + "repository": "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes-init-appdb", + "platforms": [ + "linux/amd64" + ] + }, + "staging": { + "repository": "quay.io/mongodb/mongodb-kubernetes-init-appdb-stg", + "platforms": [ + "linux/arm64", + "linux/amd64" + ] + }, + "release": { + "repository": "quay.io/mongodb/mongodb-kubernetes-init-appdb", + "platforms": [ + "linux/arm64", + "linux/amd64" + ] + } + }, + "initOpsManager": { + "patch": { + "repository": "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes-init-ops-manager", + "platforms": [ + "linux/amd64" + ] + }, + "staging": { + "repository": "quay.io/mongodb/mongodb-kubernetes-init-ops-manager-stg", + "platforms": [ + "linux/arm64", + "linux/amd64" + ] + }, + "release": { + "repository": "quay.io/mongodb/mongodb-kubernetes-init-ops-manager", + "platforms": [ + "linux/arm64", + "linux/amd64" + ] + } + }, + "database": { + "patch": { + "repository": "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes-database", + "platforms": [ + "linux/amd64" + ] + }, + "staging": { + "repository": "quay.io/mongodb/mongodb-kubernetes-database-stg", + "platforms": [ + "linux/arm64", + "linux/amd64" + ] + }, + "release": { + "repository": "quay.io/mongodb/mongodb-kubernetes-database", + "platforms": [ + "linux/arm64", + "linux/amd64" + ] + } + }, + "readinessprobe": { + "patch": { + "repository": "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes-readinessprobe", + "platforms": [ + "linux/amd64" + ] + }, + "staging": { + "repository": "quay.io/mongodb/mongodb-kubernetes-readinessprobe-stg", + "platforms": [ + "linux/arm64", + "linux/amd64" + ] + }, + "release": { + "version": "1.0.22", + "repository": "quay.io/mongodb/mongodb-kubernetes-readinessprobe", + "platforms": [ + "linux/arm64", + "linux/amd64" + ] + } + }, + "operator-version-upgrade-post-start-hook": { + "patch": { + "repository": "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes-operator-version-upgrade-post-start-hook", + "platforms": [ + "linux/amd64" + ] + }, + "staging": { + "repository": "quay.io/mongodb/mongodb-kubernetes-operator-version-upgrade-post-start-hook-stg", + "platforms": [ + "linux/arm64", + "linux/amd64" + ] + }, + "release": { + "version": "1.0.9", + "repository": "quay.io/mongodb/mongodb-kubernetes-operator-version-upgrade-post-start-hook", + "platforms": [ + "linux/arm64", + "linux/amd64" + ] + } + } + }, + "binaries": { + "kubectl-mongodb": { + "patch": { + "s3-store": "s3://kubectl-mongodb/dev", + "platforms": [ + "linux/amd64" + ] + }, + "staging": { + "s3-store": "s3://kubectl-mongodb/staging", + "platforms": [ + "darwin/amd64", + "darwin/arm64", + "linux/amd64", + "linux/arm64" + ] + }, + "release": { + "s3-store": "s3://kubectl-mongodb/prod", + "platforms": [ + "darwin/amd64", + "darwin/arm64", + "linux/amd64", + "linux/arm64" + ] + } + } + }, + "helm-charts": { + "mongodb-kubernetes": { + "patch": { + "repository": "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/helm-charts" + }, + "staging": { + "repository": "quay.io/mongodb/helm-charts-stg" + }, + "release": { + "repository": "quay.io/mongodb/helm-charts" + } + } + } +} diff --git a/pyproject.toml b/pyproject.toml index b7d3a4329..a4f290a52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.black] line-length = 120 -target-version = ['py39'] +target-version = ['py313'] include = '\.pyi?$' [tool.isort] diff --git a/scripts/release/build/__init__.py b/scripts/release/build/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/release/build/build_info.py b/scripts/release/build/build_info.py new file mode 100644 index 000000000..37222223c --- /dev/null +++ b/scripts/release/build/build_info.py @@ -0,0 +1,111 @@ +import json +from typing import Dict + +from scripts.release.build.build_scenario import BuildScenario +from scripts.release.constants import DEFAULT_REPOSITORY_PATH, DEFAULT_CHANGELOG_PATH, RELEASE_INITIAL_VERSION_ENV_VAR, \ + get_initial_version, get_initial_commit_sha + + +class ImageInfo(dict): + def __init__(self, repository: str, platforms: list[str], version: str): + super().__init__() + self.repository = repository + self.platforms = platforms + self.version = version + + def to_json(self): + return {"repository": self.repository, "platforms": self.platforms, "version": self.version} + + +class BinaryInfo(dict): + def __init__(self, s3_store: str, platforms: list[str], version: str): + super().__init__() + self.s3_store = s3_store + self.platforms = platforms + self.version = version + + def to_json(self): + return {"platforms": self.platforms, "version": self.version} + + +class HelmChartInfo(dict): + def __init__(self, repository: str, version: str): + super().__init__() + self.repository = repository + self.version = version + + def to_json(self): + return {"repository": self.repository, "version": self.version} + + +class BuildInfo(dict): + def __init__( + self, images: Dict[str, ImageInfo], binaries: Dict[str, BinaryInfo], helm_charts: Dict[str, HelmChartInfo] + ): + super().__init__() + self.images = images + self.binaries = binaries + self.helm_charts = helm_charts + + def __dict__(self): + return { + "images": {name: images.__dict__ for name, images in self.images.items()}, + "binaries": {name: bin.__dict__ for name, bin in self.binaries.items()}, + "helm-charts": {name: chart.__dict__ for name, chart in self.helm_charts.items()}, + } + + def to_json(self): + return { + "images": {name: images.to_json() for name, images in self.images.items()}, + "binaries": {name: bin.to_json() for name, bin in self.binaries.items()}, + "helm-charts": {name: chart.to_json() for name, chart in self.helm_charts.items()}, + } + + +def load_build_info(scenario: BuildScenario, + repository_path: str = DEFAULT_REPOSITORY_PATH, + changelog_sub_path: str = DEFAULT_CHANGELOG_PATH, + initial_commit_sha: str = None, + initial_version: str = None) -> BuildInfo: + f""" + Load build information based on the specified scenario. + + :param scenario: BuildScenario enum value indicating the build scenario (e.g., PATCH, STAGING, RELEASE). + :param repository_path: Path to the Git repository. Default is the current directory `{DEFAULT_REPOSITORY_PATH}`. + :param changelog_sub_path: Path to the changelog directory relative to the repository root. Default is '{DEFAULT_CHANGELOG_PATH}'. + :param initial_commit_sha: SHA of the initial commit to start from if no previous version tag is found. If not provided, it will be determined based on `{RELEASE_INITIAL_VERSION_ENV_VAR} environment variable. + :param initial_version: Initial version to use if no previous version tag is found. If not provided, it will be determined based on `{RELEASE_INITIAL_VERSION_ENV_VAR}` environment variable. + :return: BuildInfo object containing images, binaries, and helm charts information for specified scenario. + """ + + if not initial_commit_sha: + initial_commit_sha = get_initial_commit_sha() + if not initial_version: + initial_version = get_initial_version() + + version = scenario.get_version(repository_path, changelog_sub_path, initial_commit_sha, initial_version) + + with open("build_info.json", "r") as f: + build_info = json.load(f) + + images = {} + for name, env_data in build_info["images"].items(): + data = env_data[scenario] + # Only update the image_version if it is not already set in the build_info.json file + image_version = data.get("version") + if not image_version: + image_version = version + + images[name] = ImageInfo(repository=data["repository"], platforms=data["platforms"], version=image_version) + + binaries = {} + for name, env_data in build_info["binaries"].items(): + data = env_data[scenario] + binaries[name] = BinaryInfo(s3_store=data["s3-store"], platforms=data["platforms"], version=version) + + helm_charts = {} + for name, env_data in build_info["helm-charts"].items(): + data = env_data[scenario] + helm_charts[name] = HelmChartInfo(repository=data["repository"], version=version) + + return BuildInfo(images=images, binaries=binaries, helm_charts=helm_charts) diff --git a/scripts/release/build/build_info_test.py b/scripts/release/build/build_info_test.py new file mode 100644 index 000000000..9d33a909e --- /dev/null +++ b/scripts/release/build/build_info_test.py @@ -0,0 +1,199 @@ +import os + +from scripts.release.build.build_info import ( + BinaryInfo, + BuildInfo, + HelmChartInfo, + ImageInfo, + load_build_info, +) +from git import Repo +from scripts.release.build.build_scenario import BuildScenario + + +def test_load_build_info_patch(git_repo: Repo): + build_id = "688364423f9b6c00072b3556" + os.environ["BUILD_ID"] = build_id + + expected_build_info = BuildInfo( + images={ + "mongodbOperator": ImageInfo( + repository="268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes", + platforms=["linux/amd64"], + version=build_id, + ), + "initDatabase": ImageInfo( + repository="268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes-init-database", + platforms=["linux/amd64"], + version=build_id, + ), + "initAppDb": ImageInfo( + repository="268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes-init-appdb", + platforms=["linux/amd64"], + version=build_id, + ), + "initOpsManager": ImageInfo( + repository="268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes-init-ops-manager", + platforms=["linux/amd64"], + version=build_id, + ), + "database": ImageInfo( + repository="268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes-database", + platforms=["linux/amd64"], + version=build_id, + ), + "readinessprobe": ImageInfo( + repository="268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes-readinessprobe", + platforms=["linux/amd64"], + version=build_id, + ), + "operator-version-upgrade-post-start-hook": ImageInfo( + repository="268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/mongodb-kubernetes-operator-version-upgrade-post-start-hook", + platforms=["linux/amd64"], + version=build_id, + ), + }, + binaries={ + "kubectl-mongodb": BinaryInfo( + s3_store="s3://kubectl-mongodb/dev", + platforms=["linux/amd64"], + version=build_id, + ) + }, + helm_charts={ + "mongodb-kubernetes": HelmChartInfo( + repository="268558157000.dkr.ecr.us-east-1.amazonaws.com/dev/helm-charts", + version=build_id, + ) + }, + ) + + build_info = load_build_info(BuildScenario.PATCH, git_repo.working_dir) + + assert build_info.__dict__() == expected_build_info.__dict__() + + +def test_load_build_info_staging(git_repo: Repo): + initial_commit = list(git_repo.iter_commits(reverse=True))[4] + git_repo.git.checkout(initial_commit) + expecter_commit_sha = initial_commit.hexsha[:8] + + expected_build_info = BuildInfo( + images={ + "mongodbOperator": ImageInfo( + repository="quay.io/mongodb/mongodb-kubernetes-stg", + platforms=["linux/arm64", "linux/amd64"], + version=expecter_commit_sha, + ), + "initDatabase": ImageInfo( + repository="quay.io/mongodb/mongodb-kubernetes-init-database-stg", + platforms=["linux/arm64", "linux/amd64"], + version=expecter_commit_sha, + ), + "initAppDb": ImageInfo( + repository="quay.io/mongodb/mongodb-kubernetes-init-appdb-stg", + platforms=["linux/arm64", "linux/amd64"], + version=expecter_commit_sha, + ), + "initOpsManager": ImageInfo( + repository="quay.io/mongodb/mongodb-kubernetes-init-ops-manager-stg", + platforms=["linux/arm64", "linux/amd64"], + version=expecter_commit_sha, + ), + "database": ImageInfo( + repository="quay.io/mongodb/mongodb-kubernetes-database-stg", + platforms=["linux/arm64", "linux/amd64"], + version=expecter_commit_sha, + ), + "readinessprobe": ImageInfo( + repository="quay.io/mongodb/mongodb-kubernetes-readinessprobe-stg", + platforms=["linux/arm64", "linux/amd64"], + version=expecter_commit_sha, + ), + "operator-version-upgrade-post-start-hook": ImageInfo( + repository="quay.io/mongodb/mongodb-kubernetes-operator-version-upgrade-post-start-hook-stg", + platforms=["linux/arm64", "linux/amd64"], + version=expecter_commit_sha, + ), + }, + binaries={ + "kubectl-mongodb": BinaryInfo( + s3_store="s3://kubectl-mongodb/staging", + platforms=["darwin/amd64", "darwin/arm64", "linux/amd64", "linux/arm64"], + version=expecter_commit_sha, + ) + }, + helm_charts={ + "mongodb-kubernetes": HelmChartInfo( + repository="quay.io/mongodb/helm-charts-stg", + version=expecter_commit_sha, + ) + }, + ) + + build_info = load_build_info(BuildScenario.STAGING, git_repo.working_dir) + + assert build_info.__dict__() == expected_build_info.__dict__() + + +def test_load_build_info_release(git_repo: Repo, readinessprobe_version: str, + operator_version_upgrade_post_start_hook_version: str): + version = "1.2.0" + git_repo.git.checkout(version) + + expected_build_info = BuildInfo( + images={ + "mongodbOperator": ImageInfo( + repository="quay.io/mongodb/mongodb-kubernetes", + platforms=["linux/arm64", "linux/amd64"], + version=version, + ), + "initDatabase": ImageInfo( + repository="quay.io/mongodb/mongodb-kubernetes-init-database", + platforms=["linux/arm64", "linux/amd64"], + version=version, + ), + "initAppDb": ImageInfo( + repository="quay.io/mongodb/mongodb-kubernetes-init-appdb", + platforms=["linux/arm64", "linux/amd64"], + version=version, + ), + "initOpsManager": ImageInfo( + repository="quay.io/mongodb/mongodb-kubernetes-init-ops-manager", + platforms=["linux/arm64", "linux/amd64"], + version=version, + ), + "database": ImageInfo( + repository="quay.io/mongodb/mongodb-kubernetes-database", + platforms=["linux/arm64", "linux/amd64"], + version=version, + ), + "readinessprobe": ImageInfo( + repository="quay.io/mongodb/mongodb-kubernetes-readinessprobe", + platforms=["linux/arm64", "linux/amd64"], + version=readinessprobe_version, + ), + "operator-version-upgrade-post-start-hook": ImageInfo( + repository="quay.io/mongodb/mongodb-kubernetes-operator-version-upgrade-post-start-hook", + platforms=["linux/arm64", "linux/amd64"], + version=operator_version_upgrade_post_start_hook_version, + ), + }, + binaries={ + "kubectl-mongodb": BinaryInfo( + s3_store="s3://kubectl-mongodb/prod", + platforms=["darwin/amd64", "darwin/arm64", "linux/amd64", "linux/arm64"], + version=version, + ) + }, + helm_charts={ + "mongodb-kubernetes": HelmChartInfo( + repository="quay.io/mongodb/helm-charts", + version=version, + ) + }, + ) + + build_info = load_build_info(BuildScenario.RELEASE, git_repo.working_dir) + + assert build_info.__dict__() == expected_build_info.__dict__() diff --git a/scripts/release/build/build_scenario.py b/scripts/release/build/build_scenario.py new file mode 100644 index 000000000..9dc28b8af --- /dev/null +++ b/scripts/release/build/build_scenario.py @@ -0,0 +1,31 @@ +import os +from enum import StrEnum + +from git import Repo + +from scripts.release.version import calculate_next_version + +COMMIT_SHA_LENGTH = 8 + + +class BuildScenario(StrEnum): + RELEASE = "release" # Official release triggered by a git tag + PATCH = "patch" # CI build for a patch/pull request + STAGING = "staging" # CI build from a merge to the master + + def get_version(self, repository_path: str, changelog_sub_path: str, initial_commit_sha: str = None, + initial_version: str = None) -> str: + repo = Repo(repository_path) + + match self: + case BuildScenario.PATCH: + build_id = os.environ["BUILD_ID"] + if not build_id: + raise ValueError(f"BUILD_ID environment variable is not set for `{self}` build scenario") + return build_id + case BuildScenario.STAGING: + return repo.head.object.hexsha[:COMMIT_SHA_LENGTH] + case BuildScenario.RELEASE: + return calculate_next_version(repo, changelog_sub_path, initial_commit_sha, initial_version) + + raise ValueError(f"Unknown build scenario: {self}") diff --git a/scripts/release/build/build_scenario_test.py b/scripts/release/build/build_scenario_test.py new file mode 100644 index 000000000..a883b6934 --- /dev/null +++ b/scripts/release/build/build_scenario_test.py @@ -0,0 +1,36 @@ +import os + +from git import Repo + +from scripts.release.build.build_scenario import BuildScenario +from scripts.release.constants import DEFAULT_CHANGELOG_PATH + + +class TestGetVersionForBuildScenario: + + def test_patch_build_scenario(self, git_repo: Repo): + os.environ["BUILD_ID"] = "688364423f9b6c00072b3556" + expected_version = os.environ["BUILD_ID"] + + version = BuildScenario.PATCH.get_version(repository_path=git_repo.working_dir, + changelog_sub_path=DEFAULT_CHANGELOG_PATH) + + assert version == expected_version + + def test_staging_build_scenario(self, git_repo: Repo): + initial_commit = list(git_repo.iter_commits(reverse=True))[4] + git_repo.git.checkout(initial_commit) + expected_version = initial_commit.hexsha[:8] + + version = BuildScenario.STAGING.get_version(repository_path=git_repo.working_dir, + changelog_sub_path=DEFAULT_CHANGELOG_PATH) + + assert version == expected_version + + def test_release_build_scenario(self, git_repo: Repo): + git_repo.git.checkout("1.2.0") + + version = BuildScenario.RELEASE.get_version(repository_path=git_repo.working_dir, + changelog_sub_path=DEFAULT_CHANGELOG_PATH) + + assert version == "1.2.0" diff --git a/scripts/release/build/conftest.py b/scripts/release/build/conftest.py new file mode 100644 index 000000000..ae820b2da --- /dev/null +++ b/scripts/release/build/conftest.py @@ -0,0 +1,26 @@ +import json +from typing import Dict + +from _pytest.fixtures import fixture + + +def get_manually_upgradable_versions() -> Dict[str, str]: + with open("build_info.json", "r") as f: + build_info = json.load(f) + + return { + "readinessprobe": build_info["images"]["readinessprobe"]["release"]["version"], + "operator_version_upgrade_post_start_hook": build_info["images"]["operator-version-upgrade-post-start-hook"][ + "release" + ]["version"], + } + + +@fixture(scope="module") +def readinessprobe_version() -> str: + return get_manually_upgradable_versions()["readinessprobe"] + + +@fixture(scope="module") +def operator_version_upgrade_post_start_hook_version() -> str: + return get_manually_upgradable_versions()["operator_version_upgrade_post_start_hook"] diff --git a/scripts/release/calculate_next_version.py b/scripts/release/calculate_next_version.py index 4015301ad..34cc888fa 100644 --- a/scripts/release/calculate_next_version.py +++ b/scripts/release/calculate_next_version.py @@ -3,11 +3,12 @@ from git import Repo -from scripts.release.changelog import ( +from scripts.release.constants import ( DEFAULT_CHANGELOG_PATH, - DEFAULT_INITIAL_GIT_TAG_VERSION, + DEFAULT_RELEASE_INITIAL_VERSION, + DEFAULT_REPOSITORY_PATH, ) -from scripts.release.release_notes import calculate_next_version_with_changelog +from scripts.release.version import calculate_next_version if __name__ == "__main__": parser = argparse.ArgumentParser( @@ -17,7 +18,7 @@ parser.add_argument( "-p", "--path", - default=".", + default=DEFAULT_REPOSITORY_PATH, metavar="", action="store", type=pathlib.Path, @@ -43,18 +44,16 @@ parser.add_argument( "-v", "--initial-version", - default=DEFAULT_INITIAL_GIT_TAG_VERSION, + default=DEFAULT_RELEASE_INITIAL_VERSION, metavar="", action="store", type=str, - help=f"Version to use if no previous version tag is found. Default is '{DEFAULT_INITIAL_GIT_TAG_VERSION}'", + help=f"Version to use if no previous version tag is found. Default is '{DEFAULT_RELEASE_INITIAL_VERSION}'", ) args = parser.parse_args() repo = Repo(args.path) - version, _ = calculate_next_version_with_changelog( - repo, args.changelog_path, args.initial_commit_sha, args.initial_version - ) + version = calculate_next_version(repo, args.changelog_path, args.initial_commit_sha, args.initial_version) print(version) diff --git a/scripts/release/changelog.py b/scripts/release/changelog.py index a56064083..458947b4e 100644 --- a/scripts/release/changelog.py +++ b/scripts/release/changelog.py @@ -6,8 +6,6 @@ import frontmatter from git import Commit, Repo -DEFAULT_CHANGELOG_PATH = "changelog/" -DEFAULT_INITIAL_GIT_TAG_VERSION = "1.0.0" FILENAME_DATE_FORMAT = "%Y%m%d" FRONTMATTER_DATE_FORMAT = "%Y-%m-%d" MAX_TITLE_LENGTH = 50 diff --git a/scripts/release/check_changelog.py b/scripts/release/check_changelog.py index 6728e9ad0..277d16d5b 100644 --- a/scripts/release/check_changelog.py +++ b/scripts/release/check_changelog.py @@ -3,7 +3,8 @@ from git import Repo -from scripts.release.changelog import DEFAULT_CHANGELOG_PATH, get_changelog_entries +from scripts.release.changelog import get_changelog_entries +from scripts.release.constants import DEFAULT_CHANGELOG_PATH, DEFAULT_REPOSITORY_PATH def str2bool(v): @@ -25,7 +26,7 @@ def str2bool(v): parser.add_argument( "-p", "--path", - default=".", + default=DEFAULT_REPOSITORY_PATH, metavar="", action="store", type=pathlib.Path, diff --git a/scripts/release/conftest.py b/scripts/release/conftest.py index f5d339067..76410ba44 100644 --- a/scripts/release/conftest.py +++ b/scripts/release/conftest.py @@ -1,11 +1,14 @@ +import json import os import shutil import tempfile +from typing import Dict from _pytest.fixtures import fixture from git import Repo -from scripts.release.changelog import DEFAULT_CHANGELOG_PATH +from scripts.release.build.conftest import get_manually_upgradable_versions +from scripts.release.constants import DEFAULT_CHANGELOG_PATH @fixture(scope="session") @@ -162,3 +165,13 @@ def add_file(repo_path: str, src_file_path: str, dst_file_path: str | None = Non src_path = os.path.join("scripts/release/testdata", src_file_path) return shutil.copy(src_path, dst_path) + + +@fixture(scope="module") +def readinessprobe_version() -> str: + return get_manually_upgradable_versions()["readinessprobe"] + + +@fixture(scope="module") +def operator_version_upgrade_post_start_hook_version() -> str: + return get_manually_upgradable_versions()["operator_version_upgrade_post_start_hook"] diff --git a/scripts/release/constants.py b/scripts/release/constants.py new file mode 100644 index 000000000..694bba706 --- /dev/null +++ b/scripts/release/constants.py @@ -0,0 +1,16 @@ +import os + +RELEASE_INITIAL_VERSION_ENV_VAR = "RELEASE_INITIAL_VERSION" +RELEASE_INITIAL_COMMIT_SHA_ENV_VAR = "RELEASE_INITIAL_COMMIT_SHA" + +DEFAULT_RELEASE_INITIAL_VERSION = "1.0.0" +DEFAULT_CHANGELOG_PATH = "changelog/" +DEFAULT_REPOSITORY_PATH = "." + + +def get_initial_version() -> str | None: + return os.getenv(RELEASE_INITIAL_VERSION_ENV_VAR) + + +def get_initial_commit_sha() -> str | None: + return os.getenv(RELEASE_INITIAL_COMMIT_SHA_ENV_VAR) diff --git a/scripts/release/create_changelog.py b/scripts/release/create_changelog.py index 1fcac4b13..15a83f39d 100644 --- a/scripts/release/create_changelog.py +++ b/scripts/release/create_changelog.py @@ -3,12 +3,12 @@ import os from scripts.release.changelog import ( - DEFAULT_CHANGELOG_PATH, FRONTMATTER_DATE_FORMAT, ChangeKind, get_changelog_filename, parse_change_date, ) +from scripts.release.constants import DEFAULT_CHANGELOG_PATH if __name__ == "__main__": parser = argparse.ArgumentParser( diff --git a/scripts/release/release_info.py b/scripts/release/release_info.py new file mode 100644 index 000000000..40fc7f3bc --- /dev/null +++ b/scripts/release/release_info.py @@ -0,0 +1,85 @@ +import argparse +import json +import pathlib + +from scripts.release.build.build_info import load_build_info +from scripts.release.build.build_scenario import BuildScenario +from scripts.release.constants import ( + DEFAULT_CHANGELOG_PATH, + DEFAULT_RELEASE_INITIAL_VERSION, + DEFAULT_REPOSITORY_PATH, +) + + +def create_release_info_json( + repository_path: str, changelog_sub_path: str, initial_commit_sha: str = None, initial_version: str = None +) -> str: + build_info = load_build_info( + scenario=BuildScenario.RELEASE, + repository_path=repository_path, + changelog_sub_path=changelog_sub_path, + initial_commit_sha=initial_commit_sha, + initial_version=initial_version, + ) + + return json.dumps(build_info.to_json(), indent=2) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Create relevant release artifacts information in JSON format.", + formatter_class=argparse.RawTextHelpFormatter, + ) + parser.add_argument( + "-p", + "--path", + default=DEFAULT_REPOSITORY_PATH, + metavar="", + action="store", + type=pathlib.Path, + help="Path to the Git repository. Default is the current directory '.'", + ) + parser.add_argument( + "-c", + "--changelog-path", + default=DEFAULT_CHANGELOG_PATH, + metavar="", + action="store", + type=str, + help=f"Path to the changelog directory relative to a current working directory. Default is '{DEFAULT_CHANGELOG_PATH}'", + ) + parser.add_argument( + "-s", + "--initial-commit-sha", + metavar="", + action="store", + type=str, + help="SHA of the initial commit to start from if no previous version tag is found.", + ) + parser.add_argument( + "-v", + "--initial-version", + default=DEFAULT_RELEASE_INITIAL_VERSION, + metavar="", + action="store", + type=str, + help=f"Version to use if no previous version tag is found. Default is '{DEFAULT_RELEASE_INITIAL_VERSION}'", + ) + parser.add_argument( + "--output", + "-o", + metavar="", + type=pathlib.Path, + help="Path to save the release information file. If not provided, prints to stdout.", + ) + args = parser.parse_args() + + release_info = create_release_info_json( + args.path, args.changelog_path, args.initial_commit_sha, args.initial_version + ) + + if args.output is not None: + with open(args.output, "w") as file: + file.write(release_info) + else: + print(release_info) diff --git a/scripts/release/release_info_test.py b/scripts/release/release_info_test.py new file mode 100644 index 000000000..2f820037a --- /dev/null +++ b/scripts/release/release_info_test.py @@ -0,0 +1,65 @@ +import json + +from git import Repo + +from scripts.release.constants import DEFAULT_CHANGELOG_PATH +from scripts.release.release_info import create_release_info_json + + +def test_create_release_info_json( + git_repo: Repo, readinessprobe_version: str, operator_version_upgrade_post_start_hook_version: str +): + git_repo.git.checkout("1.2.0") + + expected_json = { + "images": { + "mongodbOperator": { + "repository": "quay.io/mongodb/mongodb-kubernetes", + "platforms": ["linux/arm64", "linux/amd64"], + "version": "1.2.0", + }, + "initDatabase": { + "repository": "quay.io/mongodb/mongodb-kubernetes-init-database", + "platforms": ["linux/arm64", "linux/amd64"], + "version": "1.2.0", + }, + "initAppDb": { + "repository": "quay.io/mongodb/mongodb-kubernetes-init-appdb", + "platforms": ["linux/arm64", "linux/amd64"], + "version": "1.2.0", + }, + "initOpsManager": { + "repository": "quay.io/mongodb/mongodb-kubernetes-init-ops-manager", + "platforms": ["linux/arm64", "linux/amd64"], + "version": "1.2.0", + }, + "database": { + "repository": "quay.io/mongodb/mongodb-kubernetes-database", + "platforms": ["linux/arm64", "linux/amd64"], + "version": "1.2.0", + }, + "readinessprobe": { + "repository": "quay.io/mongodb/mongodb-kubernetes-readinessprobe", + "platforms": ["linux/arm64", "linux/amd64"], + "version": readinessprobe_version, + }, + "operator-version-upgrade-post-start-hook": { + "repository": "quay.io/mongodb/mongodb-kubernetes-operator-version-upgrade-post-start-hook", + "platforms": ["linux/arm64", "linux/amd64"], + "version": operator_version_upgrade_post_start_hook_version, + }, + }, + "binaries": { + "kubectl-mongodb": { + "platforms": ["darwin/amd64", "darwin/arm64", "linux/amd64", "linux/arm64"], + "version": "1.2.0", + } + }, + "helm-charts": {"mongodb-kubernetes": {"repository": "quay.io/mongodb/helm-charts", "version": "1.2.0"}}, + } + expected_release_info_json = json.dumps(expected_json, indent=2) + release_info_json = create_release_info_json( + repository_path=git_repo.working_dir, changelog_sub_path=DEFAULT_CHANGELOG_PATH + ) + + assert release_info_json == expected_release_info_json diff --git a/scripts/release/release_notes.py b/scripts/release/release_notes.py index fdeb2ddb1..d01ebdc06 100644 --- a/scripts/release/release_notes.py +++ b/scripts/release/release_notes.py @@ -5,23 +5,23 @@ from jinja2 import Template from scripts.release.changelog import ( - DEFAULT_CHANGELOG_PATH, - DEFAULT_INITIAL_GIT_TAG_VERSION, - ChangeEntry, ChangeKind, - get_changelog_entries, +) +from scripts.release.constants import ( + DEFAULT_CHANGELOG_PATH, + DEFAULT_RELEASE_INITIAL_VERSION, + DEFAULT_REPOSITORY_PATH, ) from scripts.release.version import ( - calculate_next_release_version, - find_previous_version, + calculate_next_version_with_changelog, ) def generate_release_notes( repository_path: str, changelog_sub_path: str, - initial_commit_sha: str | None, - initial_version: str, + initial_commit_sha: str = None, + initial_version: str = None, ) -> str: f"""Generate a release notes based on the changes since the previous version tag. @@ -55,23 +55,6 @@ def generate_release_notes( return template.render(parameters) -def calculate_next_version_with_changelog( - repo: Repo, changelog_sub_path: str, initial_commit_sha: str | None, initial_version: str -) -> (str, list[ChangeEntry]): - previous_version_tag, previous_version_commit = find_previous_version(repo, initial_commit_sha) - - changelog: list[ChangeEntry] = get_changelog_entries(previous_version_commit, repo, changelog_sub_path) - changelog_kinds = list(set(entry.kind for entry in changelog)) - - # If there is no previous version tag, we start with the initial version tag - if not previous_version_tag: - version = initial_version - else: - version = calculate_next_release_version(previous_version_tag.name, changelog_kinds) - - return version, changelog - - if __name__ == "__main__": parser = argparse.ArgumentParser( description="Generate release notes based on the changes since the previous version tag.", @@ -80,7 +63,7 @@ def calculate_next_version_with_changelog( parser.add_argument( "-p", "--path", - default=".", + default=DEFAULT_REPOSITORY_PATH, metavar="", action="store", type=pathlib.Path, @@ -106,11 +89,11 @@ def calculate_next_version_with_changelog( parser.add_argument( "-v", "--initial-version", - default=DEFAULT_INITIAL_GIT_TAG_VERSION, + default=DEFAULT_RELEASE_INITIAL_VERSION, metavar="", action="store", type=str, - help=f"Version to use if no previous version tag is found. Default is '{DEFAULT_INITIAL_GIT_TAG_VERSION}'", + help=f"Version to use if no previous version tag is found. Default is '{DEFAULT_RELEASE_INITIAL_VERSION}'", ) parser.add_argument( "--output", diff --git a/scripts/release/release_notes_test.py b/scripts/release/release_notes_test.py index 068febe82..f808fb16a 100644 --- a/scripts/release/release_notes_test.py +++ b/scripts/release/release_notes_test.py @@ -1,10 +1,10 @@ from git import Repo -from scripts.release.changelog import ( +from scripts.release.conftest import git_repo +from scripts.release.constants import ( DEFAULT_CHANGELOG_PATH, - DEFAULT_INITIAL_GIT_TAG_VERSION, + DEFAULT_RELEASE_INITIAL_VERSION, ) -from scripts.release.conftest import git_repo from scripts.release.release_notes import generate_release_notes @@ -12,7 +12,9 @@ def test_generate_release_notes_before_1_0_0(git_repo: Repo): initial_commit = list(git_repo.iter_commits(reverse=True))[0] git_repo.git.checkout(initial_commit) release_notes = generate_release_notes( - git_repo.working_dir, DEFAULT_CHANGELOG_PATH, None, DEFAULT_INITIAL_GIT_TAG_VERSION + repository_path=git_repo.working_dir, + changelog_sub_path=DEFAULT_CHANGELOG_PATH, + initial_version=DEFAULT_RELEASE_INITIAL_VERSION, ) with open("scripts/release/testdata/release_notes_1.0.0_empty.md") as file: assert release_notes == file.read() @@ -73,7 +75,9 @@ def test_generate_release_notes_1_2_4(git_repo: Repo): def checkout_and_assert_release_notes(git_repo: Repo, tag: str): git_repo.git.checkout(tag) release_notes = generate_release_notes( - git_repo.working_dir, DEFAULT_CHANGELOG_PATH, None, DEFAULT_INITIAL_GIT_TAG_VERSION + repository_path=git_repo.working_dir, + changelog_sub_path=DEFAULT_CHANGELOG_PATH, + initial_version=DEFAULT_RELEASE_INITIAL_VERSION, ) with open(f"scripts/release/testdata/release_notes_{tag}.md") as file: assert release_notes == file.read() diff --git a/scripts/release/test_git_repo.mmd b/scripts/release/testdata/test_git_repo.mmd similarity index 100% rename from scripts/release/test_git_repo.mmd rename to scripts/release/testdata/test_git_repo.mmd diff --git a/scripts/release/version.py b/scripts/release/version.py index 105fc7ee4..8ef0220b9 100644 --- a/scripts/release/version.py +++ b/scripts/release/version.py @@ -1,7 +1,36 @@ import semver from git import Commit, Repo, TagReference -from scripts.release.changelog import ChangeKind +from scripts.release.changelog import ( + ChangeEntry, + ChangeKind, + get_changelog_entries, +) + + +def calculate_next_version( + repo: Repo, changelog_sub_path: str, initial_commit_sha: str | None, initial_version: str | None +) -> str: + return calculate_next_version_with_changelog(repo, changelog_sub_path, initial_commit_sha, initial_version)[0] + + +def calculate_next_version_with_changelog( + repo: Repo, changelog_sub_path: str, initial_commit_sha: str | None, initial_version: str | None +) -> (str, list[ChangeEntry]): + previous_version_tag, previous_version_commit = find_previous_version(repo, initial_commit_sha) + + changelog: list[ChangeEntry] = get_changelog_entries(previous_version_commit, repo, changelog_sub_path) + changelog_kinds = list(set(entry.kind for entry in changelog)) + + # If there is no previous version tag, we start with the initial version tag + if not previous_version_tag: + if not initial_version: + raise ValueError("No previous version tag found and no initial version provided.") + version = initial_version + else: + version = increment_previous_version(previous_version_tag.name, changelog_kinds) + + return version, changelog def find_previous_version(repo: Repo, initial_commit_sha: str = None) -> (TagReference | None, Commit): @@ -38,7 +67,7 @@ def find_previous_version_tag(repo: Repo) -> TagReference | None: return sorted_tags[0] -def calculate_next_release_version(previous_version_str: str, changelog: list[ChangeKind]) -> str: +def increment_previous_version(previous_version_str: str, changelog: list[ChangeKind]) -> str: previous_version = semver.VersionInfo.parse(previous_version_str) if ChangeKind.BREAKING in changelog: diff --git a/scripts/release/version_test.py b/scripts/release/version_test.py index c75adef77..1b1d6d1d8 100644 --- a/scripts/release/version_test.py +++ b/scripts/release/version_test.py @@ -1,7 +1,9 @@ import unittest from scripts.release.changelog import ChangeKind -from scripts.release.version import calculate_next_release_version +from scripts.release.version import ( + increment_previous_version, +) class TestCalculateNextReleaseVersion(unittest.TestCase): @@ -9,67 +11,67 @@ class TestCalculateNextReleaseVersion(unittest.TestCase): def test_bump_major_version(self): previous_version = "1.2.3" changelog = [ChangeKind.BREAKING] - next_version = calculate_next_release_version(previous_version, changelog) + next_version = increment_previous_version(previous_version, changelog) self.assertEqual(next_version, "2.0.0") def test_bump_minor_version(self): previous_version = "1.2.3" changelog = [ChangeKind.FEATURE] - next_version = calculate_next_release_version(previous_version, changelog) + next_version = increment_previous_version(previous_version, changelog) self.assertEqual(next_version, "1.3.0") def test_bump_patch_version(self): previous_version = "1.2.3" changelog = [ChangeKind.FIX] - next_version = calculate_next_release_version(previous_version, changelog) + next_version = increment_previous_version(previous_version, changelog) self.assertEqual(next_version, "1.2.4") def test_bump_patch_version_other_changes(self): previous_version = "1.2.3" changelog = [ChangeKind.OTHER] - next_version = calculate_next_release_version(previous_version, changelog) + next_version = increment_previous_version(previous_version, changelog) self.assertEqual(next_version, "1.2.4") def test_bump_patch_version_no_changes(self): previous_version = "1.2.3" changelog = [] - next_version = calculate_next_release_version(previous_version, changelog) + next_version = increment_previous_version(previous_version, changelog) self.assertEqual(next_version, "1.2.4") def test_feature_takes_precedence(self): # Test that FEATURE has precedence over FIX previous_version = "1.2.3" changelog = [ChangeKind.FEATURE, ChangeKind.FIX] - next_version = calculate_next_release_version(previous_version, changelog) + next_version = increment_previous_version(previous_version, changelog) self.assertEqual(next_version, "1.3.0") def test_breaking_takes_precedence(self): # Test that BREAKING has precedence over FEATURE and FIX previous_version = "1.2.3" changelog = [ChangeKind.FEATURE, ChangeKind.BREAKING, ChangeKind.FIX, ChangeKind.OTHER] - next_version = calculate_next_release_version(previous_version, changelog) + next_version = increment_previous_version(previous_version, changelog) self.assertEqual(next_version, "2.0.0") def test_multiple_breaking_changes(self): previous_version = "1.2.3" changelog = [ChangeKind.BREAKING, ChangeKind.BREAKING, ChangeKind.FEATURE, ChangeKind.FIX, ChangeKind.OTHER] - next_version = calculate_next_release_version(previous_version, changelog) + next_version = increment_previous_version(previous_version, changelog) self.assertEqual(next_version, "2.0.0") def test_multiple_feature_changes(self): previous_version = "1.2.3" changelog = [ChangeKind.FEATURE, ChangeKind.FEATURE, ChangeKind.FIX, ChangeKind.OTHER] - next_version = calculate_next_release_version(previous_version, changelog) + next_version = increment_previous_version(previous_version, changelog) self.assertEqual(next_version, "1.3.0") def test_multiple_fix_changes(self): previous_version = "1.2.3" changelog = [ChangeKind.FIX, ChangeKind.FIX, ChangeKind.OTHER] - next_version = calculate_next_release_version(previous_version, changelog) + next_version = increment_previous_version(previous_version, changelog) self.assertEqual(next_version, "1.2.4") def test_multiple_other_changes(self): previous_version = "1.2.3" changelog = [ChangeKind.OTHER, ChangeKind.OTHER] - next_version = calculate_next_release_version(previous_version, changelog) + next_version = increment_previous_version(previous_version, changelog) self.assertEqual(next_version, "1.2.4")