diff --git a/requirements.txt b/requirements.txt index 9e365ab9d..41134b740 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,6 +33,7 @@ pytest-mock==3.14.1 wrapt==1.17.2 botocore==1.38.33 boto3==1.38.33 +python-frontmatter==1.1.0 # from kubeobject freezegun==1.5.2 diff --git a/scripts/release/__init__.py b/scripts/release/__init__.py new file mode 100644 index 000000000..1d6f2e809 --- /dev/null +++ b/scripts/release/__init__.py @@ -0,0 +1 @@ +# Makes 'release' a Python package. diff --git a/scripts/release/calculate_next_version.py b/scripts/release/calculate_next_version.py new file mode 100644 index 000000000..4015301ad --- /dev/null +++ b/scripts/release/calculate_next_version.py @@ -0,0 +1,60 @@ +import argparse +import pathlib + +from git import Repo + +from scripts.release.changelog import ( + DEFAULT_CHANGELOG_PATH, + DEFAULT_INITIAL_GIT_TAG_VERSION, +) +from scripts.release.release_notes import calculate_next_version_with_changelog + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Calculate the next version based on the changes since the previous version tag.", + formatter_class=argparse.RawTextHelpFormatter, + ) + parser.add_argument( + "-p", + "--path", + default=".", + 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 the repository root. 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_INITIAL_GIT_TAG_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}'", + ) + 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 + ) + + print(version) diff --git a/scripts/release/changelog.py b/scripts/release/changelog.py new file mode 100644 index 000000000..f771b418d --- /dev/null +++ b/scripts/release/changelog.py @@ -0,0 +1,160 @@ +import datetime +import os +import re +from enum import StrEnum + +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 + +PRELUDE_ENTRIES = ["prelude"] +BREAKING_CHANGE_ENTRIES = ["breaking", "major"] +FEATURE_ENTRIES = ["feat", "feature"] +BUGFIX_ENTRIES = ["fix", "bugfix", "hotfix", "patch"] + + +class ChangeKind(StrEnum): + PRELUDE = "prelude" + BREAKING = "breaking" + FEATURE = "feature" + FIX = "fix" + OTHER = "other" + + +class ChangeEntry: + def __init__(self, date: datetime, kind: ChangeKind, title: str, contents: str): + self.date = date + self.kind = kind + self.title = title + self.contents = contents + + +def get_changelog_entries( + base_commit: Commit, + repo: Repo, + changelog_sub_path: str, +) -> list[ChangeEntry]: + changelog = [] + + # Compare base commit with current working tree + diff_index = base_commit.diff(other=repo.head.commit, paths=changelog_sub_path) + + # No changes since the previous version + if not diff_index: + return changelog + + # Traverse added Diff objects only (change type 'A' for added files) + for diff_item in diff_index.iter_change_type("A"): + file_path = diff_item.b_path + + change_entry = extract_changelog_entry(repo.working_dir, file_path) + changelog.append(change_entry) + + return changelog + + +def extract_changelog_entry(working_dir: str, file_path: str) -> ChangeEntry: + file_name = os.path.basename(file_path) + date, kind = extract_date_and_kind_from_file_name(file_name) + + abs_file_path = os.path.join(working_dir, file_path) + with open(abs_file_path, "r") as file: + file_content = file.read() + + change_entry = extract_changelog_entry_from_contents(file_content) + + if change_entry.date != date: + raise Exception( + f"{file_name} - date in front matter '{change_entry.date}' does not match date extracted from file name '{date}'" + ) + + if change_entry.kind != kind: + raise Exception( + f"{file_name} - kind in front matter '{change_entry.kind}' does not match kind extracted from file name '{kind}'" + ) + + return change_entry + + +def extract_date_and_kind_from_file_name(file_name: str) -> (datetime, ChangeKind): + match = re.match(r"(\d{8})_([a-zA-Z]+)_(.+)\.md", file_name) + if not match: + raise Exception(f"{file_name} - doesn't match expected pattern") + + date_str, kind_str, _ = match.groups() + try: + date = parse_change_date(date_str, FILENAME_DATE_FORMAT) + except Exception as e: + raise Exception(f"{file_name} - {e}") + + kind = get_change_kind(kind_str) + + return date, kind + + +def parse_change_date(date_str: str, date_format: str) -> datetime: + try: + date = datetime.datetime.strptime(date_str, date_format).date() + except Exception: + raise Exception(f"date '{date_str}' is not in the expected format {date_format}") + + return date + + +def get_change_kind(kind_str: str) -> ChangeKind: + if kind_str.lower() in PRELUDE_ENTRIES: + return ChangeKind.PRELUDE + if kind_str.lower() in BREAKING_CHANGE_ENTRIES: + return ChangeKind.BREAKING + elif kind_str.lower() in FEATURE_ENTRIES: + return ChangeKind.FEATURE + elif kind_str.lower() in BUGFIX_ENTRIES: + return ChangeKind.FIX + else: + return ChangeKind.OTHER + + +def extract_changelog_entry_from_contents(file_contents: str) -> ChangeEntry: + data = frontmatter.loads(file_contents) + + kind = get_change_kind(str(data["kind"])) + date = parse_change_date(str(data["date"]), FRONTMATTER_DATE_FORMAT) + ## Add newline to contents so the Markdown file also contains a newline at the end + contents = data.content + "\n" + + return ChangeEntry(date=date, title=str(data["title"]), kind=kind, contents=contents) + + +def get_changelog_filename(title: str, kind: ChangeKind, date: datetime) -> str: + sanitized_title = sanitize_title(title) + filename_date = datetime.datetime.strftime(date, FILENAME_DATE_FORMAT) + + return f"{filename_date}_{kind}_{sanitized_title}.md" + + +def sanitize_title(title: str) -> str: + # Only keep alphanumeric characters, dashes, underscores and spaces + regex = re.compile("[^a-zA-Z0-9-_ ]+") + title = regex.sub("", title) + + # Replace multiple dashes, underscores and spaces with underscores + regex_underscore = re.compile("[-_ ]+") + title = regex_underscore.sub(" ", title).strip() + + # Lowercase and split by space + words = [word.lower() for word in title.split(" ")] + + result = words[0] + + for word in words[1:]: + if len(result) + len("_") + len(word) <= MAX_TITLE_LENGTH: + result = result + "_" + word + else: + break + + return result diff --git a/scripts/release/changelog_test.py b/scripts/release/changelog_test.py new file mode 100644 index 000000000..320968420 --- /dev/null +++ b/scripts/release/changelog_test.py @@ -0,0 +1,138 @@ +import datetime +import unittest + +from scripts.release.changelog import ( + MAX_TITLE_LENGTH, + ChangeKind, + extract_changelog_entry_from_contents, + extract_date_and_kind_from_file_name, + sanitize_title, +) + + +class TestExtractChangelogDataFromFileName(unittest.TestCase): + def test_prelude(self): + date, kind = extract_date_and_kind_from_file_name("20250502_prelude_release_notes.md") + self.assertEqual(date, datetime.date(2025, 5, 2)) + self.assertEqual(kind, ChangeKind.PRELUDE) + + def test_breaking_changes(self): + date, kind = extract_date_and_kind_from_file_name("20250101_breaking_api_update.md") + self.assertEqual(date, datetime.date(2025, 1, 1)) + self.assertEqual(kind, ChangeKind.BREAKING) + + date, kind = extract_date_and_kind_from_file_name("20250508_breaking_remove_deprecated.md") + self.assertEqual(date, datetime.date(2025, 5, 8)) + self.assertEqual(kind, ChangeKind.BREAKING) + + date, kind = extract_date_and_kind_from_file_name("20250509_major_schema_change.md") + self.assertEqual(date, datetime.date(2025, 5, 9)) + self.assertEqual(kind, ChangeKind.BREAKING) + + def test_features(self): + date, kind = extract_date_and_kind_from_file_name("20250509_feature_new_dashboard.md") + self.assertEqual(date, datetime.date(2025, 5, 9)) + self.assertEqual(kind, ChangeKind.FEATURE) + + date, kind = extract_date_and_kind_from_file_name("20250511_feat_add_metrics.md") + self.assertEqual(date, datetime.date(2025, 5, 11)) + self.assertEqual(kind, ChangeKind.FEATURE) + + def test_fixes(self): + date, kind = extract_date_and_kind_from_file_name("20251210_fix_olm_missing_images.md") + self.assertEqual(date, datetime.date(2025, 12, 10)) + self.assertEqual(kind, ChangeKind.FIX) + + date, kind = extract_date_and_kind_from_file_name("20251010_bugfix_memory_leak.md") + self.assertEqual(date, datetime.date(2025, 10, 10)) + self.assertEqual(kind, ChangeKind.FIX) + + date, kind = extract_date_and_kind_from_file_name("20250302_hotfix_security_issue.md") + self.assertEqual(date, datetime.date(2025, 3, 2)) + self.assertEqual(kind, ChangeKind.FIX) + + date, kind = extract_date_and_kind_from_file_name("20250301_patch_typo_correction.md") + self.assertEqual(date, datetime.date(2025, 3, 1)) + self.assertEqual(kind, ChangeKind.FIX) + + def test_other(self): + date, kind = extract_date_and_kind_from_file_name("20250520_docs_update_readme.md") + self.assertEqual(date, datetime.date(2025, 5, 20)) + self.assertEqual(kind, ChangeKind.OTHER) + + date, kind = extract_date_and_kind_from_file_name("20250610_refactor_codebase.md") + self.assertEqual(date, datetime.date(2025, 6, 10)) + self.assertEqual(kind, ChangeKind.OTHER) + + def test_invalid_date(self): + with self.assertRaises(Exception) as context: + extract_date_and_kind_from_file_name("20250640_refactor_codebase.md") + self.assertEqual( + str(context.exception), + "20250640_refactor_codebase.md - date '20250640' is not in the expected format %Y%m%d", + ) + + def test_wrong_file_name_format_date(self): + with self.assertRaises(Exception) as context: + extract_date_and_kind_from_file_name("202yas_refactor_codebase.md") + self.assertEqual(str(context.exception), "202yas_refactor_codebase.md - doesn't match expected pattern") + + def test_wrong_file_name_format_missing_title(self): + with self.assertRaises(Exception) as context: + extract_date_and_kind_from_file_name("20250620_change.md") + self.assertEqual(str(context.exception), "20250620_change.md - doesn't match expected pattern") + + +def test_strip_changelog_entry_frontmatter(): + file_contents = """ +--- +title: This is my change +kind: feature +date: 2025-07-10 +--- + +* **MongoDB**: public search preview release of MongoDB Search (Community Edition) is now available. + * Added new property [spec.search](https://www.mongodb.com/docs/kubernetes/current/mongodb/specification/#spec-search) to enable MongoDB Search. +""" + + change_entry = extract_changelog_entry_from_contents(file_contents) + + assert change_entry.title == "This is my change" + assert change_entry.kind == ChangeKind.FEATURE + assert change_entry.date == datetime.date(2025, 7, 10) + assert ( + change_entry.contents + == """* **MongoDB**: public search preview release of MongoDB Search (Community Edition) is now available. + * Added new property [spec.search](https://www.mongodb.com/docs/kubernetes/current/mongodb/specification/#spec-search) to enable MongoDB Search. +""" + ) + + +class TestSanitizeTitle(unittest.TestCase): + def test_basic_case(self): + self.assertEqual(sanitize_title("Simple Title"), "simple_title") + + def test_non_alphabetic_chars(self): + self.assertEqual(sanitize_title("Title tha@t-_ contain's strange char&s!"), "title_that_contains_strange_chars") + + def test_with_numbers_and_dashes(self): + self.assertEqual(sanitize_title("Title with 123 numbers to-go!"), "title_with_123_numbers_to_go") + + def test_mixed_case(self): + self.assertEqual(sanitize_title("MiXeD CaSe TiTlE"), "mixed_case_title") + + def test_length_limit(self): + long_title = "This is a very long title that should be truncated because it exceeds the maximum length" + sanitized_title = sanitize_title(long_title) + self.assertTrue(len(sanitized_title) <= MAX_TITLE_LENGTH) + self.assertEqual(sanitized_title, "this_is_a_very_long_title_that_should_be_truncated") + + def test_leading_trailing_spaces(self): + sanitized_title = sanitize_title(" Title with spaces ") + self.assertEqual(sanitized_title, "title_with_spaces") + + def test_empty_title(self): + self.assertEqual(sanitize_title(""), "") + + def test_only_non_alphabetic(self): + self.assertEqual(sanitize_title("!@#"), "") diff --git a/scripts/release/conftest.py b/scripts/release/conftest.py new file mode 100644 index 000000000..cd5487c47 --- /dev/null +++ b/scripts/release/conftest.py @@ -0,0 +1,164 @@ +import os +import shutil +import tempfile + +from _pytest.fixtures import fixture +from git import Repo + +from scripts.release.changelog import DEFAULT_CHANGELOG_PATH + + +@fixture(scope="session") +def git_repo(change_log_path: str = DEFAULT_CHANGELOG_PATH) -> Repo: + """ + Create a temporary git repository for testing. + Visual representation of the repository structure is in test_git_repo.mmd (mermaid/gitgraph https://mermaid.js.org/syntax/gitgraph.html). + Whenever you modify or add new commits please update the git graph as well. + """ + + repo_dir = tempfile.mkdtemp() + repo = Repo.init(repo_dir) + changelog_path = os.path.join(repo_dir, change_log_path) + os.mkdir(changelog_path) + + ## First commit and 1.0.0 tag + repo.git.checkout("-b", "master") + new_file = create_new_file(repo_dir, "new-file.txt", "Initial content\n") + repo.index.add(new_file) + repo.index.commit("initial commit") + changelog_file = add_file(repo_dir, "changelog/20250506_prelude_mck.md") + repo.index.add(changelog_file) + repo.index.commit("release notes prelude MCK") + repo.create_tag("1.0.0", message="Initial release") + + ## Bug fixes and 1.0.1 tag + file_name = create_new_file(repo_dir, "another-file.txt", "Added more content\n") + changelog_file = add_file(repo_dir, "changelog/20250510_fix_olm_missing_images.md") + repo.index.add([file_name, changelog_file]) + repo.index.commit("olm missing images fix") + changelog_file = add_file(repo_dir, "changelog/20250510_fix_watched_list_in_helm.md") + repo.index.add(changelog_file) + repo.index.commit("fix watched list in helm") + repo.create_tag("1.0.1", message="Bug fix release") + + ## Private search preview and 1.1.0 tag (with changelog fix) + changelog_file = add_file(repo_dir, "changelog/20250523_feature_community_search_preview.md") + repo.index.add(changelog_file) + repo.index.commit("private search preview") + changelog_file = add_file( + repo_dir, + "changelog/20250523_feature_community_search_preview_UPDATED.md", + "changelog/20250523_feature_community_search_preview.md", + ) + repo.index.add(changelog_file) + repo.index.commit("add limitations in changelog for private search preview") + repo.create_tag("1.1.0", message="Public search preview release") + + ## OIDC release and 1.2.0 tag + changelog_file_1 = add_file(repo_dir, "changelog/20250609_chore_not_oidc.md") + changelog_file_2 = add_file(repo_dir, "changelog/20250610_feature_oidc.md") + changelog_file_3 = add_file(repo_dir, "changelog/20250611_other_not_oidc.md") + repo.index.add([changelog_file_1, changelog_file_2, changelog_file_3]) + repo.index.commit("OIDC integration") + repo.create_tag("1.2.0", message="OIDC integration release") + + ## Static architecture release and 2.0.0 tag + changelog_file = add_file(repo_dir, "changelog/20250612_breaking_static_as_default.md") + repo.index.add(changelog_file) + repo.index.commit("Static architecture as default") + changelog_file = add_file(repo_dir, "changelog/20250616_feature_om_no_service_mesh.md") + repo.index.add(changelog_file) + repo.index.commit("Ops Manager no service mesh support") + changelog_file_1 = add_file(repo_dir, "changelog/20250620_fix_static_container.md") + changelog_file_2 = add_file(repo_dir, "changelog/20250622_fix_external_access.md") + repo.index.add([changelog_file_1, changelog_file_2]) + fix_commit = repo.index.commit("Fixes for static architecture") + changelog_file = add_file(repo_dir, "changelog/20250623_prelude_static.md") + repo.index.add(changelog_file) + repo.index.commit("Release notes prelude for static architecture") + repo.create_tag("2.0.0", message="Static architecture release") + + ## Create release-1.x branch and backport fix + repo.git.checkout("1.2.0") + release_1_x_branch = repo.create_head("release-1.x").checkout() + repo.git.cherry_pick(fix_commit.hexsha) + repo.create_tag("1.2.1", message="Bug fix release") + + ## Bug fixes and 2.0.1 tag + repo.git.checkout("master") + file_name = create_new_file(repo_dir, "bugfix-placeholder.go", "Bugfix in go\n") + changelog_file = add_file(repo_dir, "changelog/20250701_fix_placeholder.md") + repo.index.add([file_name, changelog_file]) + fix_commit_1 = repo.index.commit("placeholder fix") + changelog_file = add_file(repo_dir, "changelog/20250702_fix_clusterspeclist_validation.md") + repo.index.add(changelog_file) + fix_commit_2 = repo.index.commit("fix clusterspeclist validation") + repo.create_tag("2.0.1", message="Bug fix release") + + ## Backport fixes to release-1.x branch + repo.git.checkout(release_1_x_branch) + repo.git.cherry_pick(fix_commit_1.hexsha) + repo.git.cherry_pick(fix_commit_2.hexsha) + repo.create_tag("1.2.2", message="Bug fix release") + + ## Bug fix and 2.0.2 tag + repo.git.checkout("master") + changelog_file = add_file(repo_dir, "changelog/20250707_fix_proxy_env_var.md") + repo.index.add(changelog_file) + fix_commit = repo.index.commit("fix proxy env var validation") + repo.create_tag("2.0.2", message="Bug fix release") + + ## Backport fixes to release-1.x branch + repo.git.checkout(release_1_x_branch) + repo.git.cherry_pick(fix_commit) + repo.create_tag("1.2.3", message="Bug fix release") + + ## Static architecture release and 3.0.0 tag + repo.git.checkout("master") + changelog_file_1 = add_file(repo_dir, "changelog/20250710_breaking_mongodbmulti_refactor.md") + changelog_file_2 = add_file(repo_dir, "changelog/20250710_prelude_mongodbmulti_refactor.md") + repo.index.add([changelog_file_1, changelog_file_2]) + repo.index.commit("Moved MongoDBMulti into single MongoDB resource") + changelog_file = add_file(repo_dir, "changelog/20250711_feature_public_search.md") + repo.index.add(changelog_file) + repo.index.commit("Public search support") + changelog_file = add_file(repo_dir, "changelog/20250712_fix_mongodbuser_phase.md") + repo.index.add(changelog_file) + fix_commit = repo.index.commit("MongoDBUser phase update fix") + repo.create_tag("3.0.0", message="MongoDBMulti integration with MongoDB resource") + + ## Create release-2.x branch and backport fix + repo.git.checkout("2.0.2") + release_2_x_branch = repo.create_head("release-2.x").checkout() + repo.git.cherry_pick(fix_commit.hexsha) + repo.create_tag("2.0.3", message="Bug fix release") + + ## Backport fixes to release-1.x branch + fix_commit = release_2_x_branch.commit + repo.git.checkout(release_1_x_branch) + repo.git.cherry_pick(fix_commit) + repo.create_tag("1.2.4", message="Bug fix release") + + return repo + + +def create_new_file(repo_path: str, file_path: str, file_content: str): + """Create a new file in the repository.""" + + file_name = os.path.join(repo_path, file_path) + with open(file_name, "a") as f: + f.write(file_content) + + return file_name + + +def add_file(repo_path: str, src_file_path: str, dst_file_path: str | None = None): + """Adds a file in the repository path.""" + + if not dst_file_path: + dst_file_path = src_file_path + + dst_path = os.path.join(repo_path, dst_file_path) + src_path = os.path.join("scripts/release/testdata", src_file_path) + + return shutil.copy(src_path, dst_path) diff --git a/scripts/release/create_changelog.py b/scripts/release/create_changelog.py new file mode 100644 index 000000000..4029bf74a --- /dev/null +++ b/scripts/release/create_changelog.py @@ -0,0 +1,88 @@ +import argparse +import datetime +import os + +from scripts.release.changelog import ( + BREAKING_CHANGE_ENTRIES, + BUGFIX_ENTRIES, + DEFAULT_CHANGELOG_PATH, + FEATURE_ENTRIES, + FRONTMATTER_DATE_FORMAT, + PRELUDE_ENTRIES, + get_change_kind, + get_changelog_filename, + parse_change_date, +) + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Utility to easily create a new changelog entry file.", + formatter_class=argparse.RawTextHelpFormatter, + ) + parser.add_argument( + "-c", + "--changelog-path", + default=DEFAULT_CHANGELOG_PATH, + metavar="", + action="store", + type=str, + help=f"Path to the changelog directory relative to the repository root. Default is {DEFAULT_CHANGELOG_PATH}", + ) + parser.add_argument( + "-d", + "--date", + default=datetime.datetime.now().strftime(FRONTMATTER_DATE_FORMAT), + metavar="", + action="store", + type=str, + help=f"Date in 'YYYY-MM-DD' format to use for the changelog entry. Default is today's date", + ) + parser.add_argument( + "-e", + "--editor", + action="store_true", + help="Open the created changelog entry in the default editor (if set, otherwise uses 'vi'). Default is True", + ) + parser.add_argument( + "-k", + "--kind", + action="store", + metavar="", + required=True, + type=str, + help=f"""Kind of the changelog entry: + - '{", ".join(PRELUDE_ENTRIES)}' for prelude entries + - '{", ".join(BREAKING_CHANGE_ENTRIES)}' for breaking change entries + - '{", ".join(FEATURE_ENTRIES)}' for feature entries + - '{", ".join(BUGFIX_ENTRIES)}' for bugfix entries + - everything else will be treated as other entries""", + ) + parser.add_argument("title", type=str, help="Title for the changelog entry") + args = parser.parse_args() + + title = args.title + date_str = args.date + date = parse_change_date(args.date, FRONTMATTER_DATE_FORMAT) + kind = get_change_kind(args.kind) + filename = get_changelog_filename(title, kind, date) + + working_dir = os.getcwd() + changelog_path = os.path.join(working_dir, args.changelog_path, filename) + + # Create directory if it doesn't exist + os.makedirs(os.path.dirname(changelog_path), exist_ok=True) + + # Create the file + with open(changelog_path, "w") as f: + # Add frontmatter based on args + f.write("---\n") + f.write(f"title: {title}\n") + f.write(f"kind: {str(kind)}\n") + f.write(f"date: {date_str}\n") + f.write("---\n\n") + + if args.editor: + editor = os.environ.get("EDITOR", "vi") # Fallback to vim if EDITOR is not set + os.system(f'{editor} "{changelog_path}"') + + print(f"Created changelog entry at: {changelog_path}") diff --git a/scripts/release/release_notes.py b/scripts/release/release_notes.py new file mode 100644 index 000000000..38ced7306 --- /dev/null +++ b/scripts/release/release_notes.py @@ -0,0 +1,132 @@ +import argparse +import pathlib + +from git import Repo +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.version import ( + calculate_next_release_version, + find_previous_version, +) + + +def generate_release_notes( + repository_path: str = ".", + changelog_sub_path: str = DEFAULT_CHANGELOG_PATH, + initial_commit_sha: str = None, + initial_version: str = DEFAULT_INITIAL_GIT_TAG_VERSION, +) -> str: + f"""Generate a release notes based on the changes since the previous version tag. + + Parameters: + repository_path: Path to the Git repository. Default is the current directory '.'. + changelog_sub_path: Path to the changelog directory relative to the repository root. Default is '{DEFAULT_CHANGELOG_PATH}. + initial_commit_sha: SHA of the initial commit to start from if no previous version tag is found. + initial_version: Version to use if no previous version tag is found. Default is '{DEFAULT_INITIAL_GIT_TAG_VERSION}'. + + Returns: + Formatted release notes as a string. + """ + repo = Repo(repository_path) + + version, changelog = calculate_next_version_with_changelog( + repo, changelog_sub_path, initial_commit_sha, initial_version + ) + + with open("scripts/release/release_notes_tpl.md", "r") as f: + template = Template(f.read()) + + parameters = { + "version": version, + "preludes": [c.contents for c in changelog if c.kind == ChangeKind.PRELUDE], + "breaking_changes": [c.contents for c in changelog if c.kind == ChangeKind.BREAKING], + "features": [c.contents for c in changelog if c.kind == ChangeKind.FEATURE], + "fixes": [c.contents for c in changelog if c.kind == ChangeKind.FIX], + "others": [c.contents for c in changelog if c.kind == ChangeKind.OTHER], + } + + 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.", + formatter_class=argparse.RawTextHelpFormatter, + ) + parser.add_argument( + "-p", + "--path", + default=".", + 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 the repository root. 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_INITIAL_GIT_TAG_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}'", + ) + parser.add_argument( + "--output", + "-o", + metavar="", + type=pathlib.Path, + help="Path to save the release notes. If not provided, prints to stdout.", + ) + args = parser.parse_args() + + release_notes = generate_release_notes( + 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_notes) + else: + print(release_notes) diff --git a/scripts/release/release_notes_test.py b/scripts/release/release_notes_test.py new file mode 100644 index 000000000..3f32a433c --- /dev/null +++ b/scripts/release/release_notes_test.py @@ -0,0 +1,71 @@ +from git import Repo + +from scripts.release.conftest import git_repo +from scripts.release.release_notes import generate_release_notes + + +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) + with open("scripts/release/testdata/release_notes_1.0.0_empty.md") as file: + assert release_notes == file.read() + + +def test_generate_release_notes_1_0_0(git_repo: Repo): + checkout_and_assert_release_notes(git_repo, "1.0.0") + + +def test_generate_release_notes_1_0_1(git_repo: Repo): + checkout_and_assert_release_notes(git_repo, "1.0.1") + + +def test_generate_release_notes_1_1_0(git_repo: Repo): + checkout_and_assert_release_notes(git_repo, "1.1.0") + + +def test_generate_release_notes_1_2_0(git_repo: Repo): + checkout_and_assert_release_notes(git_repo, "1.2.0") + + +def test_generate_release_notes_2_0_0(git_repo: Repo): + checkout_and_assert_release_notes(git_repo, "2.0.0") + + +def test_generate_release_notes_1_2_1(git_repo: Repo): + checkout_and_assert_release_notes(git_repo, "1.2.1") + + +def test_generate_release_notes_2_0_1(git_repo: Repo): + checkout_and_assert_release_notes(git_repo, "2.0.1") + + +def test_generate_release_notes_1_2_2(git_repo: Repo): + checkout_and_assert_release_notes(git_repo, "1.2.2") + + +def test_generate_release_notes_2_0_2(git_repo: Repo): + checkout_and_assert_release_notes(git_repo, "2.0.2") + + +def test_generate_release_notes_1_2_3(git_repo: Repo): + checkout_and_assert_release_notes(git_repo, "1.2.3") + + +def test_generate_release_notes_3_0_0(git_repo: Repo): + checkout_and_assert_release_notes(git_repo, "3.0.0") + + +def test_generate_release_notes_2_0_3(git_repo: Repo): + checkout_and_assert_release_notes(git_repo, "2.0.3") + + +def test_generate_release_notes_1_2_4(git_repo: Repo): + checkout_and_assert_release_notes(git_repo, "1.2.4") + + +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) + with open(f"scripts/release/testdata/release_notes_{tag}.md") as file: + assert release_notes == file.read() diff --git a/scripts/release/release_notes_tpl.md b/scripts/release/release_notes_tpl.md new file mode 100644 index 000000000..fce3c8ad7 --- /dev/null +++ b/scripts/release/release_notes_tpl.md @@ -0,0 +1,34 @@ +# MCK {{ version }} Release Notes +{% if preludes %} +{% for prelude in preludes -%} +{{- prelude }} +{%- endfor -%} +{%- endif -%} +{% if breaking_changes %} +## Breaking Changes + +{% for change in breaking_changes -%} +{{- change -}} +{%- endfor -%} +{%- endif -%} +{% if features %} +## New Features + +{% for feature in features -%} +{{- feature -}} +{%- endfor -%} +{%- endif -%} +{% if fixes %} +## Bug Fixes + +{% for fix in fixes -%} +{{- fix -}} +{%- endfor -%} +{%- endif -%} +{% if others %} +## Other Changes + +{% for other in others -%} +{{- other -}} +{%- endfor -%} +{%- endif -%} diff --git a/scripts/release/test_git_repo.mmd b/scripts/release/test_git_repo.mmd new file mode 100644 index 000000000..dad684d00 --- /dev/null +++ b/scripts/release/test_git_repo.mmd @@ -0,0 +1,45 @@ +%%{ + init: { + 'logLevel': 'debug', + 'theme': 'dark', + 'gitGraph': { + 'showBranches': true, + 'mainBranchName': 'master', + 'parallelCommits': 'true' + } + } +}%% +gitGraph + commit id: "initial commit" + commit id: "release notes prelude MCK" tag: "1.0.0" + commit id: "olm missing images fix" + commit id: "fix watched list in helm" tag: "1.0.1" + commit id: "private search preview" + commit id: "add limitations in changelog for private search preview" tag: "1.1.0" + commit id: "OIDC integration" tag: "1.2.0" + branch release-1.x + commit id: "Static architecture as default" + commit id: "Ops Manager no service mesh support" + commit id: "Fixes for static architecture" + commit id: "Release notes prelude for static architecture" tag: "2.0.0" + checkout release-1.x + commit id: "Cherry-pick: Fixes for static architecture" tag: "1.2.1" + checkout master + commit id: "placeholder fix" + commit id: "fix clusterspeclist validation" tag: "2.0.1" + checkout release-1.x + commit id: "Cherry-pick: placeholder fix" + commit id: "Cherry-pick: fix clusterspeclist validation" tag: "1.2.2" + checkout master + commit id: "fix proxy env var validation" tag: "2.0.2" + branch release-2.x + checkout release-1.x + commit id: "Cherry-pick: fix proxy env var validation" tag: "1.2.3" + checkout master + commit id: "Moved MongoDBMulti into single MongoDB resource" + commit id: "Public search support" + commit id: "MongoDBUser phase update fix" tag: "3.0.0" + checkout release-2.x + commit id: "cherry-pick from master: MongoDBUser phase update fix" tag: "2.0.3" + checkout release-1.x + commit id: "cherry-pick from release-2.x: MongoDBUser phase update fix" tag: "1.2.4" diff --git a/scripts/release/testdata/changelog/20250506_prelude_mck.md b/scripts/release/testdata/changelog/20250506_prelude_mck.md new file mode 100644 index 000000000..e96ed298e --- /dev/null +++ b/scripts/release/testdata/changelog/20250506_prelude_mck.md @@ -0,0 +1,40 @@ +--- +title: MCK release announcement +kind: prelude +date: 2025-05-06 +--- + +Exciting news for MongoDB on Kubernetes\! We're happy to announce the first release of MongoDB Controllers for Kubernetes (MCK), a unified open-source operator merging our support of MongoDB Community and Enterprise in Kubernetes. + +**Acronyms** + +* **MCK:** MongoDB Controllers for Kubernetes +* **MCO:** MongoDB Community Operator +* **MEKO:** MongoDB Enterprise Kubernetes Operator + +**TL;DR:** + +* MCK: A unified MongoDB Kubernetes Operator, merging MCO and MEKO. +* This initial release provides the combined functionality of the latest MCO and MEKO so migration is seamless: no changes are required in your current deployments. +* No impact on current contracts or agreements. +* We are adopting Semantic Versioning (SemVer), so any future breaking changes will only occur in new major versions of the Operator. +* MCO End-of-Life (EOL): Support for MCO is best efforts, with no formal EOL for each version. For the last version of MCO, we will continue to offer best efforts guidance, but there will be no further releases. +* MEKO End-of-Life (EOL): No change to the [current EOL](https://www.mongodb.com/docs/kubernetes-operator/current/reference/support-lifecycle/) for each individual MEKO version. + +**About the First MCK Release** + +MongoDB is unifying its Kubernetes offerings with the introduction of MongoDB Controllers for Kubernetes (MCK). This new operator is an open-source project and represents a merge of the previous MongoDB Community Operator (MCO) and the MongoDB Enterprise Kubernetes Operator (MEKO). + +This release brings MongoDB Community and Enterprise editions together under a single, unified operator, making it easier to manage, scale, and upgrade your deployments. While the first version simply brings the capabilities of both into a single Operator, future changes will build on this to more closely align how Community and Enterprise are managed in Kubernetes, to offer an even more seamless and streamlined experience. As an open-source project, it now allows for community contributions, helping drive quicker bug fixes and ongoing innovation. + +**License** + +Customers with contracts that allowed use of the Enterprise Operator will still be able to leverage the new replacement, allowing customers to adopt it without contract changes. The Operator itself is licensed under the Apache 2.0, and a license file [included in the repository](#) provides further detail. License entitlements for all other MongoDB products and tools remain unchanged (for example Enterprise Server and Ops Manager) \- if in doubt, contact your MongoDB account team. + +**Migration** + +Migration from MCO and MEKO to MCK is seamless: your MongoDB deployments are not impacted by the upgrade and require no changes. Simply follow the upgrade instructions provided in the MCK documentation. See our [migration guidance](https://www.mongodb.com/docs/kubernetes/current/tutorial/migrate-to-mck/). + +**Deprecation and EOL for MCO and MEKO** + +We will continue best efforts support of MCO for 6 months (until November, 2025), and versions of MEKO will remain supported according to the current [current EOL](https://www.mongodb.com/docs/kubernetes-operator/current/reference/support-lifecycle/) guidance. All future bug fixes and improvements will be released in new versions of MCK. We encourage all users to plan their migration to MCK within these timelines. diff --git a/scripts/release/testdata/changelog/20250510_fix_olm_missing_images.md b/scripts/release/testdata/changelog/20250510_fix_olm_missing_images.md new file mode 100644 index 000000000..121fdba9a --- /dev/null +++ b/scripts/release/testdata/changelog/20250510_fix_olm_missing_images.md @@ -0,0 +1,7 @@ +--- +title: OLM missing images fix +kind: fix +date: 2025-05-10 +--- + +* Fix missing agent images in the operator bundle in OpenShift catalog and operatorhub.io. diff --git a/scripts/release/testdata/changelog/20250510_fix_watched_list_in_helm.md b/scripts/release/testdata/changelog/20250510_fix_watched_list_in_helm.md new file mode 100644 index 000000000..d1c74a290 --- /dev/null +++ b/scripts/release/testdata/changelog/20250510_fix_watched_list_in_helm.md @@ -0,0 +1,7 @@ +--- +title: OLM missing images fix +kind: fix +date: 2025-05-10 +--- + +* **MongoDBCommunity** resource was missing from watched list in Helm Charts diff --git a/scripts/release/testdata/changelog/20250523_feature_community_search_preview.md b/scripts/release/testdata/changelog/20250523_feature_community_search_preview.md new file mode 100644 index 000000000..25883a031 --- /dev/null +++ b/scripts/release/testdata/changelog/20250523_feature_community_search_preview.md @@ -0,0 +1,9 @@ +--- +title: Community Search (Private Preview) +kind: feature +date: 2025-05-23 +--- + +* **MongoDBSearch (Community Private Preview)**: Added support for deploying MongoDB Search (Community Private Preview Edition) that enables full-text and vector search capabilities for MongoDBCommunity deployments. + * Added new MongoDB CRD which is watched by default by the operator. + * For more information please see: [docs/community-search/quick-start/README.md](docs/community-search/quick-start/README.md) diff --git a/scripts/release/testdata/changelog/20250523_feature_community_search_preview_UPDATED.md b/scripts/release/testdata/changelog/20250523_feature_community_search_preview_UPDATED.md new file mode 100644 index 000000000..856683a27 --- /dev/null +++ b/scripts/release/testdata/changelog/20250523_feature_community_search_preview_UPDATED.md @@ -0,0 +1,12 @@ +--- +title: Community Search (Private Preview) updated +kind: feature +date: 2025-05-23 +--- + +* **MongoDBSearch (Community Private Preview)**: Added support for deploying MongoDB Search (Community Private Preview Edition) that enables full-text and vector search capabilities for MongoDBCommunity deployments. + * Added new MongoDB CRD which is watched by default by the operator. + * For more information please see: [docs/community-search/quick-start/README.md](docs/community-search/quick-start/README.md) + * Private Preview phase comes with some limitations: + * minimum MongoDB Community version: 8.0. + * TLS must be disabled in MongoDB (communication between mongot and mongod is in plaintext for now). diff --git a/scripts/release/testdata/changelog/20250609_chore_not_oidc.md b/scripts/release/testdata/changelog/20250609_chore_not_oidc.md new file mode 100644 index 000000000..34fe38427 --- /dev/null +++ b/scripts/release/testdata/changelog/20250609_chore_not_oidc.md @@ -0,0 +1,7 @@ +--- +title: Some chore type of change +kind: chore +date: 2025-06-09 +--- + +* Fixing CI/CD pipeline issues. diff --git a/scripts/release/testdata/changelog/20250610_feature_oidc.md b/scripts/release/testdata/changelog/20250610_feature_oidc.md new file mode 100644 index 000000000..d3e760670 --- /dev/null +++ b/scripts/release/testdata/changelog/20250610_feature_oidc.md @@ -0,0 +1,15 @@ +--- +title: OIDC Authentication Support +kind: feature +date: 2025-06-10 +--- + +* **MongoDB**, **MongoDBMulti**: Added support for OpenID Connect (OIDC) user authentication. + * OIDC authentication can be configured with `spec.security.authentication.modes=OIDC` and `spec.security.authentication.oidcProviderConfigs` settings. + * Minimum MongoDB version requirements: + * `7.0.11`, `8.0.0` + * Only supported with MongoDB Enterprise Server + * For more information please see: + * [Secure Client Authentication with OIDC](https://www.mongodb.com/docs/kubernetes/upcoming/tutorial/secure-client-connections/) + * [Manage Database Users using OIDC](https://www.mongodb.com/docs/kubernetes/upcoming/manage-users/) + * [Authentication and Authorization with OIDC/OAuth 2.0](https://www.mongodb.com/docs/manual/core/oidc/security-oidc/) diff --git a/scripts/release/testdata/changelog/20250611_other_not_oidc.md b/scripts/release/testdata/changelog/20250611_other_not_oidc.md new file mode 100644 index 000000000..3df1ed90d --- /dev/null +++ b/scripts/release/testdata/changelog/20250611_other_not_oidc.md @@ -0,0 +1,7 @@ +--- +title: Some other change +kind: other +date: 2025-06-11 +--- + +* Some other change that is not related to OIDC authentication. diff --git a/scripts/release/testdata/changelog/20250612_breaking_static_as_default.md b/scripts/release/testdata/changelog/20250612_breaking_static_as_default.md new file mode 100644 index 000000000..c7a548c1a --- /dev/null +++ b/scripts/release/testdata/changelog/20250612_breaking_static_as_default.md @@ -0,0 +1,7 @@ +--- +title: Make static architecture default +kind: breaking +date: 2025-06-12 +--- + +* **MongoDB**, **MongoDBMulti**: Static architecture is now the default for MongoDB and MongoDBMulti resources. diff --git a/scripts/release/testdata/changelog/20250616_feature_om_no_service_mesh.md b/scripts/release/testdata/changelog/20250616_feature_om_no_service_mesh.md new file mode 100644 index 000000000..0d1f96412 --- /dev/null +++ b/scripts/release/testdata/changelog/20250616_feature_om_no_service_mesh.md @@ -0,0 +1,13 @@ +--- +title: OM no Service Mesh support +kind: feature +date: 2025-06-16 +--- + +* **MongoDBOpsManager**, **AppDB**: Introduced support for OpsManager and Application Database deployments across multiple Kubernetes clusters without requiring a Service Mesh. + * New property [spec.applicationDatabase.externalAccess](TBD) used for common service configuration or in single cluster deployments + * Added support for existing, but unused property [spec.applicationDatabase.clusterSpecList.externalAccess](TBD) + * You can define annotations for external services managed by the operator that contain placeholders which will be automatically replaced to the proper values: + * AppDB: [spec.applicationDatabase.externalAccess.externalService.annotations](TBD) + * MongoDBOpsManager: Due to different way of configuring external service placeholders are not yet supported + * More details can be found in the [public documentation](TBD). diff --git a/scripts/release/testdata/changelog/20250620_fix_static_container.md b/scripts/release/testdata/changelog/20250620_fix_static_container.md new file mode 100644 index 000000000..b3d52a03a --- /dev/null +++ b/scripts/release/testdata/changelog/20250620_fix_static_container.md @@ -0,0 +1,7 @@ +--- +title: Fix for static container architecture +kind: fix +date: 2025-06-20 +--- + +* Fixed a bug where workloads in the `static` container architecture were still downloading binaries. This occurred when the operator was running with the default container architecture set to `non-static`, but the workload was deployed with the `static` architecture using the `mongodb.com/v1.architecture: "static"` annotation. diff --git a/scripts/release/testdata/changelog/20250622_fix_external_access.md b/scripts/release/testdata/changelog/20250622_fix_external_access.md new file mode 100644 index 000000000..4b7682036 --- /dev/null +++ b/scripts/release/testdata/changelog/20250622_fix_external_access.md @@ -0,0 +1,7 @@ +--- +title: External Access Fix +kind: fix +date: 2025-06-22 +--- + +* **MongoDB**: Operator now correctly applies the external service customization based on `spec.externalAccess` and `spec.mongos.clusterSpecList.externalAccess` configuration. Previously it was ignored, but only for Multi Cluster Sharded Clusters. diff --git a/scripts/release/testdata/changelog/20250623_prelude_static.md b/scripts/release/testdata/changelog/20250623_prelude_static.md new file mode 100644 index 000000000..b78325d6e --- /dev/null +++ b/scripts/release/testdata/changelog/20250623_prelude_static.md @@ -0,0 +1,7 @@ +--- +title: Static architecture as default prelude +kind: prelude +date: 2025-06-23 +--- + +This change is making `static` architecture a default and deprecates the `non-static` architecture. diff --git a/scripts/release/testdata/changelog/20250701_fix_placeholder.md b/scripts/release/testdata/changelog/20250701_fix_placeholder.md new file mode 100644 index 000000000..befe2ff9e --- /dev/null +++ b/scripts/release/testdata/changelog/20250701_fix_placeholder.md @@ -0,0 +1,7 @@ +--- +title: Fixed placeholder names for `mongos` in Single Cluster Sharded +kind: fix +date: 2025-07-01 +--- + +* **MongoDB**: Fixed placeholder name for `mongos` in Single Cluster Sharded with External Domain set. Previously it was called `mongodProcessDomain` and `mongodProcessFQDN` now they're called `mongosProcessDomain` and `mongosProcessFQDN`. diff --git a/scripts/release/testdata/changelog/20250702_fix_clusterspeclist_validation.md b/scripts/release/testdata/changelog/20250702_fix_clusterspeclist_validation.md new file mode 100644 index 000000000..0343439c5 --- /dev/null +++ b/scripts/release/testdata/changelog/20250702_fix_clusterspeclist_validation.md @@ -0,0 +1,7 @@ +--- +title: clusterSpecList validation fix +kind: fix +date: 2025-07-02 +--- + +* **MongoDB**, **MongoDBMultiCluster**, **MongoDBOpsManager**: In case of losing one of the member clusters we no longer emit validation errors if the failed cluster still exists in the `clusterSpecList`. This allows easier reconfiguration of the deployments as part of disaster recovery procedure. diff --git a/scripts/release/testdata/changelog/20250707_fix_proxy_env_var.md b/scripts/release/testdata/changelog/20250707_fix_proxy_env_var.md new file mode 100644 index 000000000..8cf1c4528 --- /dev/null +++ b/scripts/release/testdata/changelog/20250707_fix_proxy_env_var.md @@ -0,0 +1,7 @@ +--- +title: Fixed handling proxy environment variables in the operator pod +kind: fix +date: 2025-07-07 +--- + +* Fixed handling proxy environment variables in the operator pod. The environment variables [`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`] when set on the operator pod, can now be propagated to the MongoDB agents by also setting the environment variable `MDB_PROPAGATE_PROXY_ENV=true`. diff --git a/scripts/release/testdata/changelog/20250710_breaking_mongodbmulti_refactor.md b/scripts/release/testdata/changelog/20250710_breaking_mongodbmulti_refactor.md new file mode 100644 index 000000000..20554eceb --- /dev/null +++ b/scripts/release/testdata/changelog/20250710_breaking_mongodbmulti_refactor.md @@ -0,0 +1,7 @@ +--- +title: Combine MongoDB and MongoDBMulti Resources +kind: breaking +date: 2025-07-10 +--- + +* **MongoDB**, **MongoDBMulti**: Combined both resources into single **MongoDB** resource. diff --git a/scripts/release/testdata/changelog/20250710_prelude_mongodbmulti_refactor.md b/scripts/release/testdata/changelog/20250710_prelude_mongodbmulti_refactor.md new file mode 100644 index 000000000..c43526f90 --- /dev/null +++ b/scripts/release/testdata/changelog/20250710_prelude_mongodbmulti_refactor.md @@ -0,0 +1,7 @@ +--- +title: MongoDB Multi Refactor Prelude +kind: prelude +date: 2025-07-10 +--- + +This is a new major release of the MongoDB Kubernetes Operator (MCK) with significant changes and improvements. diff --git a/scripts/release/testdata/changelog/20250711_feature_public_search.md b/scripts/release/testdata/changelog/20250711_feature_public_search.md new file mode 100644 index 000000000..720e598cd --- /dev/null +++ b/scripts/release/testdata/changelog/20250711_feature_public_search.md @@ -0,0 +1,8 @@ +--- +title: Search Preview Release GA +kind: feature +date: 2025-07-11 +--- + +* **MongoDB**: public search preview release of MongoDB Search (Community Edition) is now available. + * Added new property [spec.search](https://www.mongodb.com/docs/kubernetes/current/mongodb/specification/#spec-search) to enable MongoDB Search. diff --git a/scripts/release/testdata/changelog/20250712_fix_mongodbuser_phase.md b/scripts/release/testdata/changelog/20250712_fix_mongodbuser_phase.md new file mode 100644 index 000000000..09b7a149a --- /dev/null +++ b/scripts/release/testdata/changelog/20250712_fix_mongodbuser_phase.md @@ -0,0 +1,7 @@ +--- +title: Fix MongoDBUser Phase Bug +kind: fix +date: 2025-07-12 +--- + +* Fixes the bug when status of `MongoDBUser` was being set to `Updated` prematurely. For example, new users were not immediately usable following `MongoDBUser` creation despite the operator reporting `Updated` state. diff --git a/scripts/release/testdata/release_notes_1.0.0.md b/scripts/release/testdata/release_notes_1.0.0.md new file mode 100644 index 000000000..b7523d179 --- /dev/null +++ b/scripts/release/testdata/release_notes_1.0.0.md @@ -0,0 +1,36 @@ +# MCK 1.0.0 Release Notes + +Exciting news for MongoDB on Kubernetes\! We're happy to announce the first release of MongoDB Controllers for Kubernetes (MCK), a unified open-source operator merging our support of MongoDB Community and Enterprise in Kubernetes. + +**Acronyms** + +* **MCK:** MongoDB Controllers for Kubernetes +* **MCO:** MongoDB Community Operator +* **MEKO:** MongoDB Enterprise Kubernetes Operator + +**TL;DR:** + +* MCK: A unified MongoDB Kubernetes Operator, merging MCO and MEKO. +* This initial release provides the combined functionality of the latest MCO and MEKO so migration is seamless: no changes are required in your current deployments. +* No impact on current contracts or agreements. +* We are adopting Semantic Versioning (SemVer), so any future breaking changes will only occur in new major versions of the Operator. +* MCO End-of-Life (EOL): Support for MCO is best efforts, with no formal EOL for each version. For the last version of MCO, we will continue to offer best efforts guidance, but there will be no further releases. +* MEKO End-of-Life (EOL): No change to the [current EOL](https://www.mongodb.com/docs/kubernetes-operator/current/reference/support-lifecycle/) for each individual MEKO version. + +**About the First MCK Release** + +MongoDB is unifying its Kubernetes offerings with the introduction of MongoDB Controllers for Kubernetes (MCK). This new operator is an open-source project and represents a merge of the previous MongoDB Community Operator (MCO) and the MongoDB Enterprise Kubernetes Operator (MEKO). + +This release brings MongoDB Community and Enterprise editions together under a single, unified operator, making it easier to manage, scale, and upgrade your deployments. While the first version simply brings the capabilities of both into a single Operator, future changes will build on this to more closely align how Community and Enterprise are managed in Kubernetes, to offer an even more seamless and streamlined experience. As an open-source project, it now allows for community contributions, helping drive quicker bug fixes and ongoing innovation. + +**License** + +Customers with contracts that allowed use of the Enterprise Operator will still be able to leverage the new replacement, allowing customers to adopt it without contract changes. The Operator itself is licensed under the Apache 2.0, and a license file [included in the repository](#) provides further detail. License entitlements for all other MongoDB products and tools remain unchanged (for example Enterprise Server and Ops Manager) \- if in doubt, contact your MongoDB account team. + +**Migration** + +Migration from MCO and MEKO to MCK is seamless: your MongoDB deployments are not impacted by the upgrade and require no changes. Simply follow the upgrade instructions provided in the MCK documentation. See our [migration guidance](https://www.mongodb.com/docs/kubernetes/current/tutorial/migrate-to-mck/). + +**Deprecation and EOL for MCO and MEKO** + +We will continue best efforts support of MCO for 6 months (until November, 2025), and versions of MEKO will remain supported according to the current [current EOL](https://www.mongodb.com/docs/kubernetes-operator/current/reference/support-lifecycle/) guidance. All future bug fixes and improvements will be released in new versions of MCK. We encourage all users to plan their migration to MCK within these timelines. diff --git a/scripts/release/testdata/release_notes_1.0.0_empty.md b/scripts/release/testdata/release_notes_1.0.0_empty.md new file mode 100644 index 000000000..14ce7e214 --- /dev/null +++ b/scripts/release/testdata/release_notes_1.0.0_empty.md @@ -0,0 +1 @@ +# MCK 1.0.0 Release Notes diff --git a/scripts/release/testdata/release_notes_1.0.1.md b/scripts/release/testdata/release_notes_1.0.1.md new file mode 100644 index 000000000..18e59fc25 --- /dev/null +++ b/scripts/release/testdata/release_notes_1.0.1.md @@ -0,0 +1,6 @@ +# MCK 1.0.1 Release Notes + +## Bug Fixes + +* Fix missing agent images in the operator bundle in OpenShift catalog and operatorhub.io. +* **MongoDBCommunity** resource was missing from watched list in Helm Charts diff --git a/scripts/release/testdata/release_notes_1.1.0.md b/scripts/release/testdata/release_notes_1.1.0.md new file mode 100644 index 000000000..d6d630cae --- /dev/null +++ b/scripts/release/testdata/release_notes_1.1.0.md @@ -0,0 +1,10 @@ +# MCK 1.1.0 Release Notes + +## New Features + +* **MongoDBSearch (Community Private Preview)**: Added support for deploying MongoDB Search (Community Private Preview Edition) that enables full-text and vector search capabilities for MongoDBCommunity deployments. + * Added new MongoDB CRD which is watched by default by the operator. + * For more information please see: [docs/community-search/quick-start/README.md](docs/community-search/quick-start/README.md) + * Private Preview phase comes with some limitations: + * minimum MongoDB Community version: 8.0. + * TLS must be disabled in MongoDB (communication between mongot and mongod is in plaintext for now). diff --git a/scripts/release/testdata/release_notes_1.2.0.md b/scripts/release/testdata/release_notes_1.2.0.md new file mode 100644 index 000000000..ffb1af8e0 --- /dev/null +++ b/scripts/release/testdata/release_notes_1.2.0.md @@ -0,0 +1,18 @@ +# MCK 1.2.0 Release Notes + +## New Features + +* **MongoDB**, **MongoDBMulti**: Added support for OpenID Connect (OIDC) user authentication. + * OIDC authentication can be configured with `spec.security.authentication.modes=OIDC` and `spec.security.authentication.oidcProviderConfigs` settings. + * Minimum MongoDB version requirements: + * `7.0.11`, `8.0.0` + * Only supported with MongoDB Enterprise Server + * For more information please see: + * [Secure Client Authentication with OIDC](https://www.mongodb.com/docs/kubernetes/upcoming/tutorial/secure-client-connections/) + * [Manage Database Users using OIDC](https://www.mongodb.com/docs/kubernetes/upcoming/manage-users/) + * [Authentication and Authorization with OIDC/OAuth 2.0](https://www.mongodb.com/docs/manual/core/oidc/security-oidc/) + +## Other Changes + +* Fixing CI/CD pipeline issues. +* Some other change that is not related to OIDC authentication. diff --git a/scripts/release/testdata/release_notes_1.2.1.md b/scripts/release/testdata/release_notes_1.2.1.md new file mode 100644 index 000000000..3a661f22f --- /dev/null +++ b/scripts/release/testdata/release_notes_1.2.1.md @@ -0,0 +1,6 @@ +# MCK 1.2.1 Release Notes + +## Bug Fixes + +* Fixed a bug where workloads in the `static` container architecture were still downloading binaries. This occurred when the operator was running with the default container architecture set to `non-static`, but the workload was deployed with the `static` architecture using the `mongodb.com/v1.architecture: "static"` annotation. +* **MongoDB**: Operator now correctly applies the external service customization based on `spec.externalAccess` and `spec.mongos.clusterSpecList.externalAccess` configuration. Previously it was ignored, but only for Multi Cluster Sharded Clusters. diff --git a/scripts/release/testdata/release_notes_1.2.2.md b/scripts/release/testdata/release_notes_1.2.2.md new file mode 100644 index 000000000..647bd5d06 --- /dev/null +++ b/scripts/release/testdata/release_notes_1.2.2.md @@ -0,0 +1,6 @@ +# MCK 1.2.2 Release Notes + +## Bug Fixes + +* **MongoDB**: Fixed placeholder name for `mongos` in Single Cluster Sharded with External Domain set. Previously it was called `mongodProcessDomain` and `mongodProcessFQDN` now they're called `mongosProcessDomain` and `mongosProcessFQDN`. +* **MongoDB**, **MongoDBMultiCluster**, **MongoDBOpsManager**: In case of losing one of the member clusters we no longer emit validation errors if the failed cluster still exists in the `clusterSpecList`. This allows easier reconfiguration of the deployments as part of disaster recovery procedure. diff --git a/scripts/release/testdata/release_notes_1.2.3.md b/scripts/release/testdata/release_notes_1.2.3.md new file mode 100644 index 000000000..b16648823 --- /dev/null +++ b/scripts/release/testdata/release_notes_1.2.3.md @@ -0,0 +1,5 @@ +# MCK 1.2.3 Release Notes + +## Bug Fixes + +* Fixed handling proxy environment variables in the operator pod. The environment variables [`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`] when set on the operator pod, can now be propagated to the MongoDB agents by also setting the environment variable `MDB_PROPAGATE_PROXY_ENV=true`. diff --git a/scripts/release/testdata/release_notes_1.2.4.md b/scripts/release/testdata/release_notes_1.2.4.md new file mode 100644 index 000000000..deb6121d0 --- /dev/null +++ b/scripts/release/testdata/release_notes_1.2.4.md @@ -0,0 +1,5 @@ +# MCK 1.2.4 Release Notes + +## Bug Fixes + +* Fixes the bug when status of `MongoDBUser` was being set to `Updated` prematurely. For example, new users were not immediately usable following `MongoDBUser` creation despite the operator reporting `Updated` state. diff --git a/scripts/release/testdata/release_notes_2.0.0.md b/scripts/release/testdata/release_notes_2.0.0.md new file mode 100644 index 000000000..20057efd0 --- /dev/null +++ b/scripts/release/testdata/release_notes_2.0.0.md @@ -0,0 +1,22 @@ +# MCK 2.0.0 Release Notes + +This change is making `static` architecture a default and deprecates the `non-static` architecture. + +## Breaking Changes + +* **MongoDB**, **MongoDBMulti**: Static architecture is now the default for MongoDB and MongoDBMulti resources. + +## New Features + +* **MongoDBOpsManager**, **AppDB**: Introduced support for OpsManager and Application Database deployments across multiple Kubernetes clusters without requiring a Service Mesh. + * New property [spec.applicationDatabase.externalAccess](TBD) used for common service configuration or in single cluster deployments + * Added support for existing, but unused property [spec.applicationDatabase.clusterSpecList.externalAccess](TBD) + * You can define annotations for external services managed by the operator that contain placeholders which will be automatically replaced to the proper values: + * AppDB: [spec.applicationDatabase.externalAccess.externalService.annotations](TBD) + * MongoDBOpsManager: Due to different way of configuring external service placeholders are not yet supported + * More details can be found in the [public documentation](TBD). + +## Bug Fixes + +* Fixed a bug where workloads in the `static` container architecture were still downloading binaries. This occurred when the operator was running with the default container architecture set to `non-static`, but the workload was deployed with the `static` architecture using the `mongodb.com/v1.architecture: "static"` annotation. +* **MongoDB**: Operator now correctly applies the external service customization based on `spec.externalAccess` and `spec.mongos.clusterSpecList.externalAccess` configuration. Previously it was ignored, but only for Multi Cluster Sharded Clusters. diff --git a/scripts/release/testdata/release_notes_2.0.1.md b/scripts/release/testdata/release_notes_2.0.1.md new file mode 100644 index 000000000..7310f81e6 --- /dev/null +++ b/scripts/release/testdata/release_notes_2.0.1.md @@ -0,0 +1,6 @@ +# MCK 2.0.1 Release Notes + +## Bug Fixes + +* **MongoDB**: Fixed placeholder name for `mongos` in Single Cluster Sharded with External Domain set. Previously it was called `mongodProcessDomain` and `mongodProcessFQDN` now they're called `mongosProcessDomain` and `mongosProcessFQDN`. +* **MongoDB**, **MongoDBMultiCluster**, **MongoDBOpsManager**: In case of losing one of the member clusters we no longer emit validation errors if the failed cluster still exists in the `clusterSpecList`. This allows easier reconfiguration of the deployments as part of disaster recovery procedure. diff --git a/scripts/release/testdata/release_notes_2.0.2.md b/scripts/release/testdata/release_notes_2.0.2.md new file mode 100644 index 000000000..852b79839 --- /dev/null +++ b/scripts/release/testdata/release_notes_2.0.2.md @@ -0,0 +1,5 @@ +# MCK 2.0.2 Release Notes + +## Bug Fixes + +* Fixed handling proxy environment variables in the operator pod. The environment variables [`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`] when set on the operator pod, can now be propagated to the MongoDB agents by also setting the environment variable `MDB_PROPAGATE_PROXY_ENV=true`. diff --git a/scripts/release/testdata/release_notes_2.0.3.md b/scripts/release/testdata/release_notes_2.0.3.md new file mode 100644 index 000000000..7772ce1cf --- /dev/null +++ b/scripts/release/testdata/release_notes_2.0.3.md @@ -0,0 +1,5 @@ +# MCK 2.0.3 Release Notes + +## Bug Fixes + +* Fixes the bug when status of `MongoDBUser` was being set to `Updated` prematurely. For example, new users were not immediately usable following `MongoDBUser` creation despite the operator reporting `Updated` state. diff --git a/scripts/release/testdata/release_notes_3.0.0.md b/scripts/release/testdata/release_notes_3.0.0.md new file mode 100644 index 000000000..a74f2b491 --- /dev/null +++ b/scripts/release/testdata/release_notes_3.0.0.md @@ -0,0 +1,16 @@ +# MCK 3.0.0 Release Notes + +This is a new major release of the MongoDB Kubernetes Operator (MCK) with significant changes and improvements. + +## Breaking Changes + +* **MongoDB**, **MongoDBMulti**: Combined both resources into single **MongoDB** resource. + +## New Features + +* **MongoDB**: public search preview release of MongoDB Search (Community Edition) is now available. + * Added new property [spec.search](https://www.mongodb.com/docs/kubernetes/current/mongodb/specification/#spec-search) to enable MongoDB Search. + +## Bug Fixes + +* Fixes the bug when status of `MongoDBUser` was being set to `Updated` prematurely. For example, new users were not immediately usable following `MongoDBUser` creation despite the operator reporting `Updated` state. diff --git a/scripts/release/version.py b/scripts/release/version.py new file mode 100644 index 000000000..105fc7ee4 --- /dev/null +++ b/scripts/release/version.py @@ -0,0 +1,50 @@ +import semver +from git import Commit, Repo, TagReference + +from scripts.release.changelog import ChangeKind + + +def find_previous_version(repo: Repo, initial_commit_sha: str = None) -> (TagReference | None, Commit): + """Find the most recent version that is an ancestor of the current HEAD commit.""" + + previous_version_tag = find_previous_version_tag(repo) + + # If there is no previous version tag, we start with the initial commit + if not previous_version_tag: + # If no initial commit SHA provided, use the first commit in the repository + if not initial_commit_sha: + initial_commit_sha = list(repo.iter_commits(reverse=True))[0].hexsha + + return None, repo.commit(initial_commit_sha) + + return previous_version_tag, previous_version_tag.commit + + +def find_previous_version_tag(repo: Repo) -> TagReference | None: + """Find the most recent version tag that is an ancestor of the current HEAD commit.""" + + head_commit = repo.head.commit + + # Filter tags that are ancestors of the current HEAD commit + ancestor_tags = filter(lambda t: repo.is_ancestor(t.commit, head_commit) and t.commit != head_commit, repo.tags) + + # Filter valid SemVer tags and sort them + valid_tags = filter(lambda t: semver.VersionInfo.is_valid(t.name), ancestor_tags) + sorted_tags = sorted(valid_tags, key=lambda t: semver.VersionInfo.parse(t.name), reverse=True) + + if not sorted_tags: + return None + + return sorted_tags[0] + + +def calculate_next_release_version(previous_version_str: str, changelog: list[ChangeKind]) -> str: + previous_version = semver.VersionInfo.parse(previous_version_str) + + if ChangeKind.BREAKING in changelog: + return str(previous_version.bump_major()) + + if ChangeKind.FEATURE in changelog: + return str(previous_version.bump_minor()) + + return str(previous_version.bump_patch()) diff --git a/scripts/release/version_test.py b/scripts/release/version_test.py new file mode 100644 index 000000000..c75adef77 --- /dev/null +++ b/scripts/release/version_test.py @@ -0,0 +1,75 @@ +import unittest + +from scripts.release.changelog import ChangeKind +from scripts.release.version import calculate_next_release_version + + +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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + self.assertEqual(next_version, "1.2.4")