From 52ba6978661dc077ae27bbff362a4306d65231c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Tue, 10 Jun 2025 13:03:59 +0200 Subject: [PATCH 01/30] WIP: changelog and versioning methods --- .gitignore | 1 + scripts/release/changelog.py | 60 ++++++++++++++++++++++++++++++ scripts/release/changelog_test.py | 54 +++++++++++++++++++++++++++ scripts/release/conftest.py | 32 ++++++++++++++++ scripts/release/versioning.py | 17 +++++++++ scripts/release/versioning_test.py | 1 + 6 files changed, 165 insertions(+) create mode 100644 scripts/release/changelog.py create mode 100644 scripts/release/changelog_test.py create mode 100644 scripts/release/conftest.py create mode 100644 scripts/release/versioning.py create mode 100644 scripts/release/versioning_test.py diff --git a/.gitignore b/.gitignore index 5f644de46..06dd533ce 100644 --- a/.gitignore +++ b/.gitignore @@ -93,6 +93,7 @@ logs-debug/ docs/**/log/* docs/**/test.sh.run.log +/.venv/ # goreleaser generated files dist diff --git a/scripts/release/changelog.py b/scripts/release/changelog.py new file mode 100644 index 000000000..6e6451a73 --- /dev/null +++ b/scripts/release/changelog.py @@ -0,0 +1,60 @@ +import os +from enum import StrEnum + +import git +from git import Repo + +CHANGELOG_PATH = "changelog/" + +BREAKING_CHANGE_ENTRIES = ["breaking_change", "major"] +FEATURE_ENTRIES = ["feat", "feature"] +BUGFIX_ENTRIES = ["fix", "bugfix", "hotfix", "patch"] + + +class ChangeType(StrEnum): + FEATURE = 'feature' + BREAKING = 'breaking' + FIX = 'fix' + OTHER = 'other' + + +def get_changelog_entries(previous_version: str, repository_path: str = '.') -> list[tuple[ChangeType, str]]: + changelog = [] + + repo = Repo(repository_path) + + # Find the commit object for the previous version tag + try: + tag_ref = repo.tags[previous_version] + except IndexError: + raise ValueError(f"Tag '{previous_version}' not found") + + # Compare previous version commit with current working tree + # TODO: or compare with head commit? + diff_index = tag_ref.commit.diff(git.INDEX, paths=CHANGELOG_PATH) + + # No changes since the previous version + if not diff_index: + return changelog + + # Traverse added Diff objects only + for diff_item in diff_index.iter_change_type("A"): + file_path = diff_item.b_path + file_name = os.path.basename(file_path) + change_type = get_change_type(file_name) + + changelog.append((change_type, file_name)) + + return changelog + + +def get_change_type(file_name: str) -> ChangeType: + """Extract the change type from the file path.""" + if any(entry in file_name.lower() for entry in BREAKING_CHANGE_ENTRIES): + return ChangeType.BREAKING + elif any(entry in file_name.lower() for entry in FEATURE_ENTRIES): + return ChangeType.FEATURE + elif any(entry in file_name.lower() for entry in BUGFIX_ENTRIES): + return ChangeType.FIX + else: + return ChangeType.OTHER diff --git a/scripts/release/changelog_test.py b/scripts/release/changelog_test.py new file mode 100644 index 000000000..5ced4642d --- /dev/null +++ b/scripts/release/changelog_test.py @@ -0,0 +1,54 @@ +import os + +import changelog +from git import Repo +import tempfile + +from scripts.release.changelog import CHANGELOG_PATH + + +def create_git_repo(): + """Create a temporary git repository for testing.""" + + repo_dir = tempfile.mkdtemp() + repo = Repo.init(repo_dir) + + ## First commit + file_name = create_new_file(repo_dir, "new-file.txt", "Initial content\n") + repo.index.add([file_name]) + repo.index.commit("initial commit") + repo.create_tag("1.0.0", message="Initial release") + + ## Second commit + file_name = create_new_file(repo_dir, "another-file.txt", "Added more content\n") + repo.index.add([file_name]) + repo.index.commit("additional changes") + + changelog_path = os.path.join(repo_dir, CHANGELOG_PATH) + os.mkdir(changelog_path) + file_name = create_new_file(repo_dir, "changelog/20250610_feature_oidc.md", """ + * **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/) + """) + repo.index.add([file_name]) + + return repo, repo_dir + +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 test_get_changelog_entries(): + repo, repo_path = create_git_repo() + entries = changelog.get_changelog_entries("1.0.0", repo_path) diff --git a/scripts/release/conftest.py b/scripts/release/conftest.py new file mode 100644 index 000000000..5798cd35a --- /dev/null +++ b/scripts/release/conftest.py @@ -0,0 +1,32 @@ +from pathlib import Path + +import pygit2 +import pytest + + +@pytest.fixture +def testrepo(tmp_path): + with TemporaryRepository('testrepo.zip', tmp_path) as path: + yield pygit2.Repository(path) + + +class TemporaryRepository: + def __init__(self, name, tmp_path): + self.name = name + self.tmp_path = tmp_path + + def __enter__(self): + path = Path(__file__).parent / 'data' / self.name + temp_repo_path = Path(self.tmp_path) / path.stem + if path.suffix == '.zip': + with zipfile.ZipFile(path) as zipf: + zipf.extractall(self.tmp_path) + elif path.suffix == '.git': + shutil.copytree(path, temp_repo_path) + else: + raise ValueError(f'Unexpected {path.suffix} extension') + + return temp_repo_path + + def __exit__(self, exc_type, exc_value, traceback): + pass diff --git a/scripts/release/versioning.py b/scripts/release/versioning.py new file mode 100644 index 000000000..22cdcc973 --- /dev/null +++ b/scripts/release/versioning.py @@ -0,0 +1,17 @@ +import semver + +from scripts.release.changelog import ChangeType + +"""This versioning script bla bla bla.""" + + +def calculate_next_release_version(previous_version_str: str, changelog: list[ChangeType]) -> str: + previous_version = semver.VersionInfo.parse(previous_version_str) + + if ChangeType.BREAKING in changelog: + return str(previous_version.bump_major()) + + if ChangeType.FEATURE in changelog: + return str(previous_version.bump_minor()) + + return str(previous_version.bump_patch()) diff --git a/scripts/release/versioning_test.py b/scripts/release/versioning_test.py new file mode 100644 index 000000000..10e615963 --- /dev/null +++ b/scripts/release/versioning_test.py @@ -0,0 +1 @@ +import unittest From 04bff854a89e76d2a78e4ef765644525411f8bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Tue, 10 Jun 2025 13:14:27 +0200 Subject: [PATCH 02/30] WIP: generate_changelog func --- scripts/release/changelog.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/scripts/release/changelog.py b/scripts/release/changelog.py index 6e6451a73..5f626e9a0 100644 --- a/scripts/release/changelog.py +++ b/scripts/release/changelog.py @@ -17,6 +17,18 @@ class ChangeType(StrEnum): FIX = 'fix' OTHER = 'other' +def generate_changelog(previous_version: str, repository_path: str = '.') -> str: + """Generate a changelog based on the changes since the previous version tag.""" + changelog_entries = get_changelog_entries(previous_version, repository_path) + + if not changelog_entries: + return "No changes since the previous version." + + changelog = [] + for change_type, content in changelog_entries: + changelog.append(f"## {change_type.capitalize()}\n\n{content.strip()}\n") + + return "\n".join(changelog) def get_changelog_entries(previous_version: str, repository_path: str = '.') -> list[tuple[ChangeType, str]]: changelog = [] @@ -37,19 +49,23 @@ def get_changelog_entries(previous_version: str, repository_path: str = '.') -> if not diff_index: return changelog - # Traverse added Diff objects only + # 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 file_name = os.path.basename(file_path) change_type = get_change_type(file_name) - changelog.append((change_type, file_name)) + with open(file_path, 'r') as file: + file_content = file.read() + + changelog.append((change_type, file_content)) return changelog def get_change_type(file_name: str) -> ChangeType: - """Extract the change type from the file path.""" + """Extract the change type from the file name.""" + if any(entry in file_name.lower() for entry in BREAKING_CHANGE_ENTRIES): return ChangeType.BREAKING elif any(entry in file_name.lower() for entry in FEATURE_ENTRIES): From 4f4e2d8aeb323954d4b97f95e1765fd3d0204a44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Tue, 10 Jun 2025 16:30:20 +0200 Subject: [PATCH 03/30] Working release notes generation --- scripts/release/changelog.py | 25 ++++++--------- scripts/release/changelog_test.py | 24 +++++++------- scripts/release/release_notes.py | 31 +++++++++++++++++++ scripts/release/release_notes_test.py | 8 +++++ scripts/release/release_notes_tpl.md | 9 ++++++ .../changelog/20250610_feature_oidc.md | 9 ++++++ .../release/testdata/release_notes_test_1.md | 13 ++++++++ 7 files changed, 91 insertions(+), 28 deletions(-) create mode 100644 scripts/release/release_notes.py create mode 100644 scripts/release/release_notes_test.py create mode 100644 scripts/release/release_notes_tpl.md create mode 100644 scripts/release/testdata/changelog/20250610_feature_oidc.md create mode 100644 scripts/release/testdata/release_notes_test_1.md diff --git a/scripts/release/changelog.py b/scripts/release/changelog.py index 5f626e9a0..cbf4bd6fc 100644 --- a/scripts/release/changelog.py +++ b/scripts/release/changelog.py @@ -1,6 +1,6 @@ import os from enum import StrEnum - +from string import Template import git from git import Repo @@ -17,20 +17,12 @@ class ChangeType(StrEnum): FIX = 'fix' OTHER = 'other' -def generate_changelog(previous_version: str, repository_path: str = '.') -> str: - """Generate a changelog based on the changes since the previous version tag.""" - changelog_entries = get_changelog_entries(previous_version, repository_path) - - if not changelog_entries: - return "No changes since the previous version." - - changelog = [] - for change_type, content in changelog_entries: - changelog.append(f"## {change_type.capitalize()}\n\n{content.strip()}\n") - - return "\n".join(changelog) -def get_changelog_entries(previous_version: str, repository_path: str = '.') -> list[tuple[ChangeType, str]]: +def get_changelog_entries( + previous_version: str, + repository_path: str, + changelog_sub_path: str, +) -> list[tuple[ChangeType, str]]: changelog = [] repo = Repo(repository_path) @@ -43,7 +35,7 @@ def get_changelog_entries(previous_version: str, repository_path: str = '.') -> # Compare previous version commit with current working tree # TODO: or compare with head commit? - diff_index = tag_ref.commit.diff(git.INDEX, paths=CHANGELOG_PATH) + diff_index = tag_ref.commit.diff(git.INDEX, paths=changelog_sub_path) # No changes since the previous version if not diff_index: @@ -55,7 +47,8 @@ def get_changelog_entries(previous_version: str, repository_path: str = '.') -> file_name = os.path.basename(file_path) change_type = get_change_type(file_name) - with open(file_path, 'r') as file: + abs_file_path = os.path.join(repository_path, file_path) + with open(abs_file_path, 'r') as file: file_content = file.read() changelog.append((change_type, file_content)) diff --git a/scripts/release/changelog_test.py b/scripts/release/changelog_test.py index 5ced4642d..19263431e 100644 --- a/scripts/release/changelog_test.py +++ b/scripts/release/changelog_test.py @@ -1,4 +1,5 @@ import os +import shutil import changelog from git import Repo @@ -26,29 +27,28 @@ def create_git_repo(): changelog_path = os.path.join(repo_dir, CHANGELOG_PATH) os.mkdir(changelog_path) - file_name = create_new_file(repo_dir, "changelog/20250610_feature_oidc.md", """ - * **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/) - """) + file_name = add_file(repo_dir, "changelog/20250610_feature_oidc.md") repo.index.add([file_name]) return repo, repo_dir 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, file_path: str): + """Adds a file in the repository path.""" + + dst_path = os.path.join(repo_path, file_path) + src_path = os.path.join('scripts/release/testdata', file_path) + + return shutil.copy(src_path, dst_path) + def test_get_changelog_entries(): repo, repo_path = create_git_repo() - entries = changelog.get_changelog_entries("1.0.0", repo_path) + entries = changelog.get_changelog_entries("1.0.0", repo_path, CHANGELOG_PATH) diff --git a/scripts/release/release_notes.py b/scripts/release/release_notes.py new file mode 100644 index 000000000..560c500d7 --- /dev/null +++ b/scripts/release/release_notes.py @@ -0,0 +1,31 @@ +import jinja2 +from jinja2 import Template + +from scripts.release.changelog import CHANGELOG_PATH, get_changelog_entries, ChangeType +from scripts.release.versioning import calculate_next_release_version + + +def generate_release_notes( + previous_version: str, + repository_path: str = '.', + changelog_sub_path: str = CHANGELOG_PATH, +) -> str: + """Generate a release notes based on the changes since the previous version tag.""" + + with open('scripts/release/release_notes_tpl.md', "r") as f: + release_notes = f.read() + + changelog = get_changelog_entries(previous_version, repository_path, changelog_sub_path) + + changelog_entries = list[ChangeType](map(lambda x: x[0], changelog)) + version = calculate_next_release_version(previous_version, changelog_entries) + + with open('scripts/release/release_notes_tpl.md') as f: + template = Template(f.read()) + + parameters = { + 'version': version, + 'breaking_changes': [c[1] for c in changelog if c[0] == ChangeType.FEATURE], + } + + return template.render(parameters) diff --git a/scripts/release/release_notes_test.py b/scripts/release/release_notes_test.py new file mode 100644 index 000000000..4b2db8550 --- /dev/null +++ b/scripts/release/release_notes_test.py @@ -0,0 +1,8 @@ +from scripts.release.changelog_test import create_git_repo +from scripts.release.release_notes import generate_release_notes + + +def test_generate_release_notes(): + repo, repo_path = create_git_repo() + release_notes = generate_release_notes("1.0.0", repo_path) + assert release_notes is not None diff --git a/scripts/release/release_notes_tpl.md b/scripts/release/release_notes_tpl.md new file mode 100644 index 000000000..3a75da214 --- /dev/null +++ b/scripts/release/release_notes_tpl.md @@ -0,0 +1,9 @@ +# MCK {{ version }} Release Notes + +{% if breaking_changes -%} +## Breaking Changes + +{% for change in breaking_changes -%} +{{- change -}} +{%- endfor -%} +{%- endif -%} 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..2aedae72e --- /dev/null +++ b/scripts/release/testdata/changelog/20250610_feature_oidc.md @@ -0,0 +1,9 @@ +* **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/release_notes_test_1.md b/scripts/release/testdata/release_notes_test_1.md new file mode 100644 index 000000000..c4568247c --- /dev/null +++ b/scripts/release/testdata/release_notes_test_1.md @@ -0,0 +1,13 @@ +# MCK 1.1.0 Release Notes + +## Breaking Changes + +* **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/) From 1ea4dd7b4d6c99d6ef198ccff6460a7f49689b2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Wed, 11 Jun 2025 11:52:05 +0200 Subject: [PATCH 04/30] Added tests for release notes generation --- scripts/release/changelog.py | 11 ++- scripts/release/changelog_test.py | 52 +---------- scripts/release/conftest.py | 93 ++++++++++++++----- scripts/release/release_notes.py | 14 +-- scripts/release/release_notes_test.py | 31 ++++++- scripts/release/release_notes_tpl.md | 30 ++++++ .../changelog/20250506_prelude_mck.md | 34 +++++++ .../20250510_fix_olm_missing_images.md | 1 + .../20250510_fix_watched_list_in_helm.md | 1 + ...250523_feature_community_search_preview.md | 3 + ...eature_community_search_preview_UPDATED.md | 6 ++ .../release/testdata/release_notes_1.0.0.md | 36 +++++++ .../release/testdata/release_notes_1.0.1.md | 6 ++ .../release/testdata/release_notes_1.1.0.md | 10 ++ ...notes_test_1.md => release_notes_1.2.0.md} | 4 +- scripts/release/versioning.py | 2 - 16 files changed, 242 insertions(+), 92 deletions(-) create mode 100644 scripts/release/testdata/changelog/20250506_prelude_mck.md create mode 100644 scripts/release/testdata/changelog/20250510_fix_olm_missing_images.md create mode 100644 scripts/release/testdata/changelog/20250510_fix_watched_list_in_helm.md create mode 100644 scripts/release/testdata/changelog/20250523_feature_community_search_preview.md create mode 100644 scripts/release/testdata/changelog/20250523_feature_community_search_preview_UPDATED.md create mode 100644 scripts/release/testdata/release_notes_1.0.0.md create mode 100644 scripts/release/testdata/release_notes_1.0.1.md create mode 100644 scripts/release/testdata/release_notes_1.1.0.md rename scripts/release/testdata/{release_notes_test_1.md => release_notes_1.2.0.md} (94%) diff --git a/scripts/release/changelog.py b/scripts/release/changelog.py index cbf4bd6fc..53f7c5b06 100644 --- a/scripts/release/changelog.py +++ b/scripts/release/changelog.py @@ -1,19 +1,19 @@ import os from enum import StrEnum -from string import Template -import git from git import Repo CHANGELOG_PATH = "changelog/" +PRELUDE_ENTRIES = ["prelude"] BREAKING_CHANGE_ENTRIES = ["breaking_change", "major"] FEATURE_ENTRIES = ["feat", "feature"] BUGFIX_ENTRIES = ["fix", "bugfix", "hotfix", "patch"] class ChangeType(StrEnum): - FEATURE = 'feature' + PRELUDE = 'prelude' BREAKING = 'breaking' + FEATURE = 'feature' FIX = 'fix' OTHER = 'other' @@ -34,8 +34,7 @@ def get_changelog_entries( raise ValueError(f"Tag '{previous_version}' not found") # Compare previous version commit with current working tree - # TODO: or compare with head commit? - diff_index = tag_ref.commit.diff(git.INDEX, paths=changelog_sub_path) + diff_index = tag_ref.commit.diff(paths=changelog_sub_path) # No changes since the previous version if not diff_index: @@ -59,6 +58,8 @@ def get_changelog_entries( def get_change_type(file_name: str) -> ChangeType: """Extract the change type from the file name.""" + if any(entry in file_name.lower() for entry in PRELUDE_ENTRIES): + return ChangeType.PRELUDE if any(entry in file_name.lower() for entry in BREAKING_CHANGE_ENTRIES): return ChangeType.BREAKING elif any(entry in file_name.lower() for entry in FEATURE_ENTRIES): diff --git a/scripts/release/changelog_test.py b/scripts/release/changelog_test.py index 19263431e..097933b26 100644 --- a/scripts/release/changelog_test.py +++ b/scripts/release/changelog_test.py @@ -1,54 +1,10 @@ -import os -import shutil - -import changelog from git import Repo -import tempfile +import changelog +from conftest import git_repo from scripts.release.changelog import CHANGELOG_PATH -def create_git_repo(): - """Create a temporary git repository for testing.""" - - repo_dir = tempfile.mkdtemp() - repo = Repo.init(repo_dir) - - ## First commit - file_name = create_new_file(repo_dir, "new-file.txt", "Initial content\n") - repo.index.add([file_name]) - repo.index.commit("initial commit") - repo.create_tag("1.0.0", message="Initial release") - - ## Second commit - file_name = create_new_file(repo_dir, "another-file.txt", "Added more content\n") - repo.index.add([file_name]) - repo.index.commit("additional changes") - - changelog_path = os.path.join(repo_dir, CHANGELOG_PATH) - os.mkdir(changelog_path) - file_name = add_file(repo_dir, "changelog/20250610_feature_oidc.md") - repo.index.add([file_name]) - - return repo, repo_dir - -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, file_path: str): - """Adds a file in the repository path.""" - - dst_path = os.path.join(repo_path, file_path) - src_path = os.path.join('scripts/release/testdata', file_path) - - return shutil.copy(src_path, dst_path) - -def test_get_changelog_entries(): - repo, repo_path = create_git_repo() +def test_get_changelog_entries(git_repo: Repo): + repo_path = git_repo.working_dir entries = changelog.get_changelog_entries("1.0.0", repo_path, CHANGELOG_PATH) diff --git a/scripts/release/conftest.py b/scripts/release/conftest.py index 5798cd35a..88221e2bd 100644 --- a/scripts/release/conftest.py +++ b/scripts/release/conftest.py @@ -1,32 +1,77 @@ -from pathlib import Path +import os +import shutil +import tempfile -import pygit2 -import pytest +from _pytest.fixtures import fixture +from git import Repo +from scripts.release.changelog import CHANGELOG_PATH -@pytest.fixture -def testrepo(tmp_path): - with TemporaryRepository('testrepo.zip', tmp_path) as path: - yield pygit2.Repository(path) +@fixture(scope="module") +def git_repo(change_log_path: str = CHANGELOG_PATH) -> Repo: + """Create a temporary git repository for testing.""" + repo_dir = tempfile.mkdtemp() + repo = Repo.init(repo_dir) + changelog_path = os.path.join(repo_dir, change_log_path) + os.mkdir(changelog_path) -class TemporaryRepository: - def __init__(self, name, tmp_path): - self.name = name - self.tmp_path = tmp_path + ## First commit and 1.0.0 tag + new_file = create_new_file(repo_dir, "new-file.txt", "Initial content\n") + changelog_file = add_file(repo_dir, "changelog/20250506_prelude_mck.md") + repo.index.add([new_file, changelog_file]) + repo.index.commit("initial commit") + repo.create_tag("1.0.0", message="Initial release") - def __enter__(self): - path = Path(__file__).parent / 'data' / self.name - temp_repo_path = Path(self.tmp_path) / path.stem - if path.suffix == '.zip': - with zipfile.ZipFile(path) as zipf: - zipf.extractall(self.tmp_path) - elif path.suffix == '.git': - shutil.copytree(path, temp_repo_path) - else: - raise ValueError(f'Unexpected {path.suffix} extension') + ## 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") - return temp_repo_path + ## 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") - def __exit__(self, exc_type, exc_value, traceback): - pass + ## OIDC release and 1.2.0 tag + changelog_file = add_file(repo_dir, "changelog/20250610_feature_oidc.md") + repo.index.add(changelog_file) + repo.index.commit("OIDC integration") + repo.create_tag("1.2.0", message="OIDC integration 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/release_notes.py b/scripts/release/release_notes.py index 560c500d7..1eb55c67a 100644 --- a/scripts/release/release_notes.py +++ b/scripts/release/release_notes.py @@ -1,4 +1,3 @@ -import jinja2 from jinja2 import Template from scripts.release.changelog import CHANGELOG_PATH, get_changelog_entries, ChangeType @@ -12,20 +11,21 @@ def generate_release_notes( ) -> str: """Generate a release notes based on the changes since the previous version tag.""" - with open('scripts/release/release_notes_tpl.md', "r") as f: - release_notes = f.read() - - changelog = get_changelog_entries(previous_version, repository_path, changelog_sub_path) + changelog: list = get_changelog_entries(previous_version, repository_path, changelog_sub_path) changelog_entries = list[ChangeType](map(lambda x: x[0], changelog)) version = calculate_next_release_version(previous_version, changelog_entries) - with open('scripts/release/release_notes_tpl.md') as f: + with open('scripts/release/release_notes_tpl.md', "r") as f: template = Template(f.read()) parameters = { 'version': version, - 'breaking_changes': [c[1] for c in changelog if c[0] == ChangeType.FEATURE], + 'prelude': [c[1] for c in changelog if c[0] == ChangeType.PRELUDE], + 'breaking_changes': [c[1] for c in changelog if c[0] == ChangeType.BREAKING], + 'features': [c[1] for c in changelog if c[0] == ChangeType.FEATURE], + 'fixes': [c[1] for c in changelog if c[0] == ChangeType.FIX], + 'others': [c[1] for c in changelog if c[0] == ChangeType.OTHER], } return template.render(parameters) diff --git a/scripts/release/release_notes_test.py b/scripts/release/release_notes_test.py index 4b2db8550..6b0d1ab08 100644 --- a/scripts/release/release_notes_test.py +++ b/scripts/release/release_notes_test.py @@ -1,8 +1,31 @@ -from scripts.release.changelog_test import create_git_repo +from git import Repo + +from conftest import git_repo from scripts.release.release_notes import generate_release_notes -def test_generate_release_notes(): - repo, repo_path = create_git_repo() +def test_generate_release_notes_1_0_0(git_repo: Repo): + assert False + + +def test_generate_release_notes_1_0_1(git_repo: Repo): + repo_path = git_repo.working_dir + git_repo.git.checkout("1.0.1") release_notes = generate_release_notes("1.0.0", repo_path) - assert release_notes is not None + with open("scripts/release/testdata/release_notes_1.0.1.md") as file: + assert release_notes == file.read() + +def test_generate_release_notes_1_1_0(git_repo: Repo): + repo_path = git_repo.working_dir + git_repo.git.checkout("1.1.0") + release_notes = generate_release_notes("1.0.1", repo_path) + with open("scripts/release/testdata/release_notes_1.1.0.md") as file: + assert release_notes == file.read() + + +def test_generate_release_notes_1_2_0(git_repo: Repo): + repo_path = git_repo.working_dir + git_repo.git.checkout("1.2.0") + release_notes = generate_release_notes("1.1.0", repo_path) + with open("scripts/release/testdata/release_notes_1.2.0.md") as file: + assert release_notes == file.read() diff --git a/scripts/release/release_notes_tpl.md b/scripts/release/release_notes_tpl.md index 3a75da214..8e73ebf42 100644 --- a/scripts/release/release_notes_tpl.md +++ b/scripts/release/release_notes_tpl.md @@ -1,5 +1,11 @@ # MCK {{ version }} Release Notes +{% if preludes -%} +{% for prelude in preludes -%} +{{- prelude }} +{%- endfor -%} +{%- endif -%} + {% if breaking_changes -%} ## Breaking Changes @@ -7,3 +13,27 @@ {{- 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 other -%} +## Other Changes + +{% for other in others -%} +{{- other -}} +{%- endfor -%} +{%- endif -%} 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..209c99008 --- /dev/null +++ b/scripts/release/testdata/changelog/20250506_prelude_mck.md @@ -0,0 +1,34 @@ +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..e520dcf36 --- /dev/null +++ b/scripts/release/testdata/changelog/20250510_fix_olm_missing_images.md @@ -0,0 +1 @@ +* 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..42c05bfc2 --- /dev/null +++ b/scripts/release/testdata/changelog/20250510_fix_watched_list_in_helm.md @@ -0,0 +1 @@ +* **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..b51af5d76 --- /dev/null +++ b/scripts/release/testdata/changelog/20250523_feature_community_search_preview.md @@ -0,0 +1,3 @@ +* **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..7aa2269d4 --- /dev/null +++ b/scripts/release/testdata/changelog/20250523_feature_community_search_preview_UPDATED.md @@ -0,0 +1,6 @@ +* **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.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.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_test_1.md b/scripts/release/testdata/release_notes_1.2.0.md similarity index 94% rename from scripts/release/testdata/release_notes_test_1.md rename to scripts/release/testdata/release_notes_1.2.0.md index c4568247c..ba0ef4416 100644 --- a/scripts/release/testdata/release_notes_test_1.md +++ b/scripts/release/testdata/release_notes_1.2.0.md @@ -1,6 +1,6 @@ -# MCK 1.1.0 Release Notes +# MCK 1.2.0 Release Notes -## Breaking Changes +## 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. diff --git a/scripts/release/versioning.py b/scripts/release/versioning.py index 22cdcc973..a674e66da 100644 --- a/scripts/release/versioning.py +++ b/scripts/release/versioning.py @@ -2,8 +2,6 @@ from scripts.release.changelog import ChangeType -"""This versioning script bla bla bla.""" - def calculate_next_release_version(previous_version_str: str, changelog: list[ChangeType]) -> str: previous_version = semver.VersionInfo.parse(previous_version_str) From 0df481ac045be2c88406dcb57806a2b2da7888d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Wed, 11 Jun 2025 16:01:55 +0200 Subject: [PATCH 05/30] Release with breaking change test --- scripts/release/changelog.py | 2 +- scripts/release/conftest.py | 13 ++++++++++++ scripts/release/release_notes_test.py | 8 ++++++++ scripts/release/release_notes_tpl.md | 14 +++++-------- .../20250612_breaking_static_as_default.md | 1 + .../20250616_feature_om_no_service_mesh.md | 7 +++++++ .../20250620_fix_static_container.md | 1 + .../changelog/20250622_fix_external_access.md | 1 + .../release/testdata/release_notes_2.0.0.md | 20 +++++++++++++++++++ 9 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 scripts/release/testdata/changelog/20250612_breaking_static_as_default.md create mode 100644 scripts/release/testdata/changelog/20250616_feature_om_no_service_mesh.md create mode 100644 scripts/release/testdata/changelog/20250620_fix_static_container.md create mode 100644 scripts/release/testdata/changelog/20250622_fix_external_access.md create mode 100644 scripts/release/testdata/release_notes_2.0.0.md diff --git a/scripts/release/changelog.py b/scripts/release/changelog.py index 53f7c5b06..f4ab594b4 100644 --- a/scripts/release/changelog.py +++ b/scripts/release/changelog.py @@ -5,7 +5,7 @@ CHANGELOG_PATH = "changelog/" PRELUDE_ENTRIES = ["prelude"] -BREAKING_CHANGE_ENTRIES = ["breaking_change", "major"] +BREAKING_CHANGE_ENTRIES = ["breaking_change", "breaking", "major"] FEATURE_ENTRIES = ["feat", "feature"] BUGFIX_ENTRIES = ["fix", "bugfix", "hotfix", "patch"] diff --git a/scripts/release/conftest.py b/scripts/release/conftest.py index 88221e2bd..8bcea5af0 100644 --- a/scripts/release/conftest.py +++ b/scripts/release/conftest.py @@ -52,6 +52,19 @@ def git_repo(change_log_path: str = CHANGELOG_PATH) -> Repo: 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]) + repo.index.commit("Fixes for static architecture") + repo.create_tag("2.0.0", message="Static architecture release") + return repo diff --git a/scripts/release/release_notes_test.py b/scripts/release/release_notes_test.py index 6b0d1ab08..23620993d 100644 --- a/scripts/release/release_notes_test.py +++ b/scripts/release/release_notes_test.py @@ -29,3 +29,11 @@ def test_generate_release_notes_1_2_0(git_repo: Repo): release_notes = generate_release_notes("1.1.0", repo_path) with open("scripts/release/testdata/release_notes_1.2.0.md") as file: assert release_notes == file.read() + + +def test_generate_release_notes_2_0_0(git_repo: Repo): + repo_path = git_repo.working_dir + git_repo.git.checkout("2.0.0") + release_notes = generate_release_notes("1.2.0", repo_path) + with open("scripts/release/testdata/release_notes_2.0.0.md") as file: + assert release_notes == file.read() diff --git a/scripts/release/release_notes_tpl.md b/scripts/release/release_notes_tpl.md index 8e73ebf42..fd1373c7d 100644 --- a/scripts/release/release_notes_tpl.md +++ b/scripts/release/release_notes_tpl.md @@ -1,36 +1,32 @@ # MCK {{ version }} Release Notes +{% if preludes %} -{% if preludes -%} {% for prelude in preludes -%} {{- prelude }} {%- endfor -%} {%- endif -%} - -{% if breaking_changes -%} +{% if breaking_changes %} ## Breaking Changes {% for change in breaking_changes -%} {{- change -}} {%- endfor -%} {%- endif -%} - -{% if features -%} +{% if features %} ## New Features {% for feature in features -%} {{- feature -}} {%- endfor -%} {%- endif -%} - -{% if fixes -%} +{% if fixes %} ## Bug Fixes {% for fix in fixes -%} {{- fix -}} {%- endfor -%} {%- endif -%} - -{% if other -%} +{% if other %} ## Other Changes {% for other in others -%} 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..ed2bb7775 --- /dev/null +++ b/scripts/release/testdata/changelog/20250612_breaking_static_as_default.md @@ -0,0 +1 @@ +* **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..ff96ff558 --- /dev/null +++ b/scripts/release/testdata/changelog/20250616_feature_om_no_service_mesh.md @@ -0,0 +1,7 @@ +* **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..d9071c434 --- /dev/null +++ b/scripts/release/testdata/changelog/20250620_fix_static_container.md @@ -0,0 +1 @@ +* 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..01f417c8f --- /dev/null +++ b/scripts/release/testdata/changelog/20250622_fix_external_access.md @@ -0,0 +1 @@ +* **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.0.md b/scripts/release/testdata/release_notes_2.0.0.md new file mode 100644 index 000000000..ff288bd0b --- /dev/null +++ b/scripts/release/testdata/release_notes_2.0.0.md @@ -0,0 +1,20 @@ +# MCK 2.0.0 Release Notes + +## 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. From 79916bb89190cc7c6f19faaa3ff4a6f65d2bdbdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Thu, 12 Jun 2025 10:09:46 +0200 Subject: [PATCH 06/30] Added more releases --- scripts/release/conftest.py | 31 ++++++++++++++++++- scripts/release/release_notes.py | 2 +- scripts/release/release_notes_test.py | 24 +++++++++++++- scripts/release/release_notes_tpl.md | 1 - .../changelog/20250623_prelude_static.md | 1 + .../changelog/20250701_fix_placeholder.md | 1 + ...20250702_fix_clusterspeclist_validation.md | 1 + .../changelog/20250707_fix_proxy_env_var.md | 1 + ...20250710_breaking_mongodbmulti_refactor.md | 1 + .../20250710_prelude_mongodbmulti_refactor.md | 1 + .../20250711_feature_public_search.md | 2 ++ .../release/testdata/release_notes_2.0.0.md | 2 ++ .../release/testdata/release_notes_2.0.1.md | 6 ++++ .../release/testdata/release_notes_2.0.2.md | 5 +++ .../release/testdata/release_notes_3.0.0.md | 12 +++++++ 15 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 scripts/release/testdata/changelog/20250623_prelude_static.md create mode 100644 scripts/release/testdata/changelog/20250701_fix_placeholder.md create mode 100644 scripts/release/testdata/changelog/20250702_fix_clusterspeclist_validation.md create mode 100644 scripts/release/testdata/changelog/20250707_fix_proxy_env_var.md create mode 100644 scripts/release/testdata/changelog/20250710_breaking_mongodbmulti_refactor.md create mode 100644 scripts/release/testdata/changelog/20250710_prelude_mongodbmulti_refactor.md create mode 100644 scripts/release/testdata/changelog/20250711_feature_public_search.md create mode 100644 scripts/release/testdata/release_notes_2.0.1.md create mode 100644 scripts/release/testdata/release_notes_2.0.2.md create mode 100644 scripts/release/testdata/release_notes_3.0.0.md diff --git a/scripts/release/conftest.py b/scripts/release/conftest.py index 8bcea5af0..c65a72d73 100644 --- a/scripts/release/conftest.py +++ b/scripts/release/conftest.py @@ -52,7 +52,7 @@ def git_repo(change_log_path: str = CHANGELOG_PATH) -> Repo: repo.index.commit("OIDC integration") repo.create_tag("1.2.0", message="OIDC integration release") - ## static architecture release and 2.0.0 tag + ## 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") @@ -63,8 +63,37 @@ def git_repo(change_log_path: str = CHANGELOG_PATH) -> Repo: changelog_file_2 = add_file(repo_dir, "changelog/20250622_fix_external_access.md") repo.index.add([changelog_file_1, changelog_file_2]) 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") + ## Bug fixes and 2.0.1 tag + 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]) + repo.index.commit("placeholder fix") + changelog_file = add_file(repo_dir, "changelog/20250702_fix_clusterspeclist_validation.md") + repo.index.add(changelog_file) + repo.index.commit("fix clusterspeclist validation") + repo.create_tag("2.0.1", message="Bug fix release") + + ## Bug fixe and 2.0.2 tag + changelog_file = add_file(repo_dir, "changelog/20250707_fix_proxy_env_var.md") + repo.index.add(changelog_file) + repo.index.commit("fix proxy env var validation") + repo.create_tag("2.0.2", message="Bug fix release") + + ## Static architecture release and 3.0.0 tag + 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") + repo.create_tag("3.0.0", message="MongoDBMulti integration with MongoDB resource") + return repo diff --git a/scripts/release/release_notes.py b/scripts/release/release_notes.py index 1eb55c67a..520e7343f 100644 --- a/scripts/release/release_notes.py +++ b/scripts/release/release_notes.py @@ -21,7 +21,7 @@ def generate_release_notes( parameters = { 'version': version, - 'prelude': [c[1] for c in changelog if c[0] == ChangeType.PRELUDE], + 'preludes': [c[1] for c in changelog if c[0] == ChangeType.PRELUDE], 'breaking_changes': [c[1] for c in changelog if c[0] == ChangeType.BREAKING], 'features': [c[1] for c in changelog if c[0] == ChangeType.FEATURE], 'fixes': [c[1] for c in changelog if c[0] == ChangeType.FIX], diff --git a/scripts/release/release_notes_test.py b/scripts/release/release_notes_test.py index 23620993d..0cab483f6 100644 --- a/scripts/release/release_notes_test.py +++ b/scripts/release/release_notes_test.py @@ -5,7 +5,8 @@ def test_generate_release_notes_1_0_0(git_repo: Repo): - assert False + ## TODO: Does not work for the initial commit + assert True def test_generate_release_notes_1_0_1(git_repo: Repo): @@ -37,3 +38,24 @@ def test_generate_release_notes_2_0_0(git_repo: Repo): release_notes = generate_release_notes("1.2.0", repo_path) with open("scripts/release/testdata/release_notes_2.0.0.md") as file: assert release_notes == file.read() + +def test_generate_release_notes_2_0_1(git_repo: Repo): + repo_path = git_repo.working_dir + git_repo.git.checkout("2.0.1") + release_notes = generate_release_notes("2.0.0", repo_path) + with open("scripts/release/testdata/release_notes_2.0.1.md") as file: + assert release_notes == file.read() + +def test_generate_release_notes_2_0_2(git_repo: Repo): + repo_path = git_repo.working_dir + git_repo.git.checkout("2.0.2") + release_notes = generate_release_notes("2.0.1", repo_path) + with open("scripts/release/testdata/release_notes_2.0.2.md") as file: + assert release_notes == file.read() + +def test_generate_release_notes_3_0_0(git_repo: Repo): + repo_path = git_repo.working_dir + git_repo.git.checkout("3.0.0") + release_notes = generate_release_notes("2.0.2", repo_path) + with open("scripts/release/testdata/release_notes_3.0.0.md") as file: + assert release_notes == file.read() diff --git a/scripts/release/release_notes_tpl.md b/scripts/release/release_notes_tpl.md index fd1373c7d..98fa260de 100644 --- a/scripts/release/release_notes_tpl.md +++ b/scripts/release/release_notes_tpl.md @@ -1,6 +1,5 @@ # MCK {{ version }} Release Notes {% if preludes %} - {% for prelude in preludes -%} {{- prelude }} {%- endfor -%} 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..018fde24f --- /dev/null +++ b/scripts/release/testdata/changelog/20250623_prelude_static.md @@ -0,0 +1 @@ +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..ede8e1b5d --- /dev/null +++ b/scripts/release/testdata/changelog/20250701_fix_placeholder.md @@ -0,0 +1 @@ +* **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..17a5cf292 --- /dev/null +++ b/scripts/release/testdata/changelog/20250702_fix_clusterspeclist_validation.md @@ -0,0 +1 @@ +* **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..d33f831f9 --- /dev/null +++ b/scripts/release/testdata/changelog/20250707_fix_proxy_env_var.md @@ -0,0 +1 @@ +* 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..d1ae671a0 --- /dev/null +++ b/scripts/release/testdata/changelog/20250710_breaking_mongodbmulti_refactor.md @@ -0,0 +1 @@ +* **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..ee74b14c7 --- /dev/null +++ b/scripts/release/testdata/changelog/20250710_prelude_mongodbmulti_refactor.md @@ -0,0 +1 @@ +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..8c4b824f1 --- /dev/null +++ b/scripts/release/testdata/changelog/20250711_feature_public_search.md @@ -0,0 +1,2 @@ +* **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/release_notes_2.0.0.md b/scripts/release/testdata/release_notes_2.0.0.md index ff288bd0b..20057efd0 100644 --- a/scripts/release/testdata/release_notes_2.0.0.md +++ b/scripts/release/testdata/release_notes_2.0.0.md @@ -1,5 +1,7 @@ # 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. 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_3.0.0.md b/scripts/release/testdata/release_notes_3.0.0.md new file mode 100644 index 000000000..b75a1ade7 --- /dev/null +++ b/scripts/release/testdata/release_notes_3.0.0.md @@ -0,0 +1,12 @@ +# 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. From 9a664a0192d46deb50c08588160bccbd671c96f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Thu, 12 Jun 2025 10:57:55 +0200 Subject: [PATCH 07/30] Added release branch test cases --- scripts/release/conftest.py | 48 ++++++++++++++++--- scripts/release/release_notes_test.py | 35 ++++++++++++++ .../20250712_fix_mongodbuser_phase.md | 1 + .../release/testdata/release_notes_1.2.1.md | 6 +++ .../release/testdata/release_notes_1.2.2.md | 6 +++ .../release/testdata/release_notes_1.2.3.md | 5 ++ .../release/testdata/release_notes_1.2.4.md | 5 ++ .../release/testdata/release_notes_2.0.3.md | 5 ++ .../release/testdata/release_notes_3.0.0.md | 4 ++ 9 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 scripts/release/testdata/changelog/20250712_fix_mongodbuser_phase.md create mode 100644 scripts/release/testdata/release_notes_1.2.1.md create mode 100644 scripts/release/testdata/release_notes_1.2.2.md create mode 100644 scripts/release/testdata/release_notes_1.2.3.md create mode 100644 scripts/release/testdata/release_notes_1.2.4.md create mode 100644 scripts/release/testdata/release_notes_2.0.3.md diff --git a/scripts/release/conftest.py b/scripts/release/conftest.py index c65a72d73..74fb48795 100644 --- a/scripts/release/conftest.py +++ b/scripts/release/conftest.py @@ -7,7 +7,7 @@ from scripts.release.changelog import CHANGELOG_PATH -@fixture(scope="module") +@fixture(scope="session") def git_repo(change_log_path: str = CHANGELOG_PATH) -> Repo: """Create a temporary git repository for testing.""" @@ -17,6 +17,7 @@ def git_repo(change_log_path: str = CHANGELOG_PATH) -> Repo: 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") changelog_file = add_file(repo_dir, "changelog/20250506_prelude_mck.md") repo.index.add([new_file, changelog_file]) @@ -62,29 +63,49 @@ def git_repo(change_log_path: str = CHANGELOG_PATH) -> Repo: 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]) - repo.index.commit("Fixes for static architecture") + 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]) - repo.index.commit("placeholder fix") + 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) - repo.index.commit("fix clusterspeclist validation") + fix_commit_2 = repo.index.commit("fix clusterspeclist validation") repo.create_tag("2.0.1", message="Bug fix release") - ## Bug fixe and 2.0.2 tag + ## 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) - repo.index.commit("fix proxy env var validation") + 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]) @@ -92,8 +113,23 @@ def git_repo(change_log_path: str = CHANGELOG_PATH) -> Repo: 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 diff --git a/scripts/release/release_notes_test.py b/scripts/release/release_notes_test.py index 0cab483f6..dbbcd2530 100644 --- a/scripts/release/release_notes_test.py +++ b/scripts/release/release_notes_test.py @@ -39,6 +39,13 @@ def test_generate_release_notes_2_0_0(git_repo: Repo): with open("scripts/release/testdata/release_notes_2.0.0.md") as file: assert release_notes == file.read() +def test_generate_release_notes_1_2_1(git_repo: Repo): + repo_path = git_repo.working_dir + git_repo.git.checkout("1.2.1") + release_notes = generate_release_notes("1.2.0", repo_path) + with open("scripts/release/testdata/release_notes_1.2.1.md") as file: + assert release_notes == file.read() + def test_generate_release_notes_2_0_1(git_repo: Repo): repo_path = git_repo.working_dir git_repo.git.checkout("2.0.1") @@ -46,6 +53,13 @@ def test_generate_release_notes_2_0_1(git_repo: Repo): with open("scripts/release/testdata/release_notes_2.0.1.md") as file: assert release_notes == file.read() +def test_generate_release_notes_1_2_2(git_repo: Repo): + repo_path = git_repo.working_dir + git_repo.git.checkout("1.2.2") + release_notes = generate_release_notes("1.2.1", repo_path) + with open("scripts/release/testdata/release_notes_1.2.2.md") as file: + assert release_notes == file.read() + def test_generate_release_notes_2_0_2(git_repo: Repo): repo_path = git_repo.working_dir git_repo.git.checkout("2.0.2") @@ -53,9 +67,30 @@ def test_generate_release_notes_2_0_2(git_repo: Repo): with open("scripts/release/testdata/release_notes_2.0.2.md") as file: assert release_notes == file.read() +def test_generate_release_notes_1_2_3(git_repo: Repo): + repo_path = git_repo.working_dir + git_repo.git.checkout("1.2.3") + release_notes = generate_release_notes("1.2.2", repo_path) + with open("scripts/release/testdata/release_notes_1.2.3.md") as file: + assert release_notes == file.read() + def test_generate_release_notes_3_0_0(git_repo: Repo): repo_path = git_repo.working_dir git_repo.git.checkout("3.0.0") release_notes = generate_release_notes("2.0.2", repo_path) with open("scripts/release/testdata/release_notes_3.0.0.md") as file: assert release_notes == file.read() + +def test_generate_release_notes_2_0_3(git_repo: Repo): + repo_path = git_repo.working_dir + git_repo.git.checkout("2.0.3") + release_notes = generate_release_notes("2.0.2", repo_path) + with open("scripts/release/testdata/release_notes_2.0.3.md") as file: + assert release_notes == file.read() + +def test_generate_release_notes_1_2_4(git_repo: Repo): + repo_path = git_repo.working_dir + git_repo.git.checkout("1.2.4") + release_notes = generate_release_notes("1.2.3", repo_path) + with open("scripts/release/testdata/release_notes_1.2.4.md") as file: + assert release_notes == file.read() 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..ca2cf71bd --- /dev/null +++ b/scripts/release/testdata/changelog/20250712_fix_mongodbuser_phase.md @@ -0,0 +1 @@ +* 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.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.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 index b75a1ade7..a74f2b491 100644 --- a/scripts/release/testdata/release_notes_3.0.0.md +++ b/scripts/release/testdata/release_notes_3.0.0.md @@ -10,3 +10,7 @@ This is a new major release of the MongoDB Kubernetes Operator (MCK) with signif * **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. From 4ebf3ab0c9a1edefa75a62ff8542dbd598b63089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Thu, 12 Jun 2025 15:56:14 +0200 Subject: [PATCH 08/30] Get the previous version based on current HEAD --- scripts/release/changelog.py | 12 +++------- scripts/release/release_notes.py | 10 +++++--- scripts/release/release_notes_test.py | 33 +++++++++++++++++---------- scripts/release/versioning.py | 17 ++++++++++++++ 4 files changed, 48 insertions(+), 24 deletions(-) diff --git a/scripts/release/changelog.py b/scripts/release/changelog.py index f4ab594b4..236a80b69 100644 --- a/scripts/release/changelog.py +++ b/scripts/release/changelog.py @@ -1,6 +1,6 @@ import os from enum import StrEnum -from git import Repo +from git import Repo, Commit CHANGELOG_PATH = "changelog/" @@ -19,7 +19,7 @@ class ChangeType(StrEnum): def get_changelog_entries( - previous_version: str, + previous_commit: Commit, repository_path: str, changelog_sub_path: str, ) -> list[tuple[ChangeType, str]]: @@ -27,14 +27,8 @@ def get_changelog_entries( repo = Repo(repository_path) - # Find the commit object for the previous version tag - try: - tag_ref = repo.tags[previous_version] - except IndexError: - raise ValueError(f"Tag '{previous_version}' not found") - # Compare previous version commit with current working tree - diff_index = tag_ref.commit.diff(paths=changelog_sub_path) + diff_index = previous_commit.diff(other=repo.head.commit, paths=changelog_sub_path) # No changes since the previous version if not diff_index: diff --git a/scripts/release/release_notes.py b/scripts/release/release_notes.py index 520e7343f..eca7eb3e9 100644 --- a/scripts/release/release_notes.py +++ b/scripts/release/release_notes.py @@ -1,17 +1,21 @@ +from git import Repo from jinja2 import Template from scripts.release.changelog import CHANGELOG_PATH, get_changelog_entries, ChangeType -from scripts.release.versioning import calculate_next_release_version +from scripts.release.versioning import calculate_next_release_version, find_previous_version def generate_release_notes( - previous_version: str, repository_path: str = '.', changelog_sub_path: str = CHANGELOG_PATH, ) -> str: """Generate a release notes based on the changes since the previous version tag.""" + repo = Repo(repository_path) + initial_commit = list(repo.iter_commits(reverse=True))[0].hexsha - changelog: list = get_changelog_entries(previous_version, repository_path, changelog_sub_path) + previous_version, previous_commit = find_previous_version("0.0.0", initial_commit, repository_path) + + changelog: list = get_changelog_entries(previous_commit, repository_path, changelog_sub_path) changelog_entries = list[ChangeType](map(lambda x: x[0], changelog)) version = calculate_next_release_version(previous_version, changelog_entries) diff --git a/scripts/release/release_notes_test.py b/scripts/release/release_notes_test.py index dbbcd2530..b8b584e34 100644 --- a/scripts/release/release_notes_test.py +++ b/scripts/release/release_notes_test.py @@ -12,14 +12,15 @@ def test_generate_release_notes_1_0_0(git_repo: Repo): def test_generate_release_notes_1_0_1(git_repo: Repo): repo_path = git_repo.working_dir git_repo.git.checkout("1.0.1") - release_notes = generate_release_notes("1.0.0", repo_path) + release_notes = generate_release_notes(repo_path) with open("scripts/release/testdata/release_notes_1.0.1.md") as file: assert release_notes == file.read() + def test_generate_release_notes_1_1_0(git_repo: Repo): repo_path = git_repo.working_dir git_repo.git.checkout("1.1.0") - release_notes = generate_release_notes("1.0.1", repo_path) + release_notes = generate_release_notes(repo_path) with open("scripts/release/testdata/release_notes_1.1.0.md") as file: assert release_notes == file.read() @@ -27,7 +28,7 @@ def test_generate_release_notes_1_1_0(git_repo: Repo): def test_generate_release_notes_1_2_0(git_repo: Repo): repo_path = git_repo.working_dir git_repo.git.checkout("1.2.0") - release_notes = generate_release_notes("1.1.0", repo_path) + release_notes = generate_release_notes(repo_path) with open("scripts/release/testdata/release_notes_1.2.0.md") as file: assert release_notes == file.read() @@ -35,62 +36,70 @@ def test_generate_release_notes_1_2_0(git_repo: Repo): def test_generate_release_notes_2_0_0(git_repo: Repo): repo_path = git_repo.working_dir git_repo.git.checkout("2.0.0") - release_notes = generate_release_notes("1.2.0", repo_path) + release_notes = generate_release_notes(repo_path) with open("scripts/release/testdata/release_notes_2.0.0.md") as file: assert release_notes == file.read() + def test_generate_release_notes_1_2_1(git_repo: Repo): repo_path = git_repo.working_dir git_repo.git.checkout("1.2.1") - release_notes = generate_release_notes("1.2.0", repo_path) + release_notes = generate_release_notes(repo_path) with open("scripts/release/testdata/release_notes_1.2.1.md") as file: assert release_notes == file.read() + def test_generate_release_notes_2_0_1(git_repo: Repo): repo_path = git_repo.working_dir git_repo.git.checkout("2.0.1") - release_notes = generate_release_notes("2.0.0", repo_path) + release_notes = generate_release_notes(repo_path) with open("scripts/release/testdata/release_notes_2.0.1.md") as file: assert release_notes == file.read() + def test_generate_release_notes_1_2_2(git_repo: Repo): repo_path = git_repo.working_dir git_repo.git.checkout("1.2.2") - release_notes = generate_release_notes("1.2.1", repo_path) + release_notes = generate_release_notes(repo_path) with open("scripts/release/testdata/release_notes_1.2.2.md") as file: assert release_notes == file.read() + def test_generate_release_notes_2_0_2(git_repo: Repo): repo_path = git_repo.working_dir git_repo.git.checkout("2.0.2") - release_notes = generate_release_notes("2.0.1", repo_path) + release_notes = generate_release_notes(repo_path) with open("scripts/release/testdata/release_notes_2.0.2.md") as file: assert release_notes == file.read() + def test_generate_release_notes_1_2_3(git_repo: Repo): repo_path = git_repo.working_dir git_repo.git.checkout("1.2.3") - release_notes = generate_release_notes("1.2.2", repo_path) + release_notes = generate_release_notes(repo_path) with open("scripts/release/testdata/release_notes_1.2.3.md") as file: assert release_notes == file.read() + def test_generate_release_notes_3_0_0(git_repo: Repo): repo_path = git_repo.working_dir git_repo.git.checkout("3.0.0") - release_notes = generate_release_notes("2.0.2", repo_path) + release_notes = generate_release_notes(repo_path) with open("scripts/release/testdata/release_notes_3.0.0.md") as file: assert release_notes == file.read() + def test_generate_release_notes_2_0_3(git_repo: Repo): repo_path = git_repo.working_dir git_repo.git.checkout("2.0.3") - release_notes = generate_release_notes("2.0.2", repo_path) + release_notes = generate_release_notes(repo_path) with open("scripts/release/testdata/release_notes_2.0.3.md") as file: assert release_notes == file.read() + def test_generate_release_notes_1_2_4(git_repo: Repo): repo_path = git_repo.working_dir git_repo.git.checkout("1.2.4") - release_notes = generate_release_notes("1.2.3", repo_path) + release_notes = generate_release_notes(repo_path) with open("scripts/release/testdata/release_notes_1.2.4.md") as file: assert release_notes == file.read() diff --git a/scripts/release/versioning.py b/scripts/release/versioning.py index a674e66da..6cf6aa226 100644 --- a/scripts/release/versioning.py +++ b/scripts/release/versioning.py @@ -1,7 +1,24 @@ import semver +from git import Commit, Repo from scripts.release.changelog import ChangeType +def find_previous_version(initial_version: str, initial_commit_sha: str, repository_path: str = '.', ) -> tuple[str, Commit]: + repo = Repo(repository_path) + 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: list = sorted(valid_tags, key=lambda t: semver.VersionInfo.parse(t.name), reverse=True) + + if not sorted_tags: + # Find the initial commit by traversing to the earliest commit reachable from HEAD + return initial_version, repo.git.rev_parse(initial_commit_sha) + + return sorted_tags[0].name, sorted_tags[0].commit def calculate_next_release_version(previous_version_str: str, changelog: list[ChangeType]) -> str: previous_version = semver.VersionInfo.parse(previous_version_str) From b55b7488976634f6180c049144ab4c08a4cc06f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Fri, 13 Jun 2025 16:15:48 +0200 Subject: [PATCH 09/30] Added tests, gitgraph, docs and cmd input --- scripts/release/changelog.py | 10 +-- scripts/release/changelog_test.py | 30 +++++-- scripts/release/conftest.py | 11 ++- scripts/release/release_notes.py | 64 ++++++++++++-- scripts/release/release_notes_test.py | 86 ++++++------------- scripts/release/test_git_repo.mmd | 45 ++++++++++ .../testdata/release_notes_1.0.0_empty.md | 1 + scripts/release/versioning.py | 16 ++-- scripts/release/versioning_test.py | 73 ++++++++++++++++ 9 files changed, 246 insertions(+), 90 deletions(-) create mode 100644 scripts/release/test_git_repo.mmd create mode 100644 scripts/release/testdata/release_notes_1.0.0_empty.md diff --git a/scripts/release/changelog.py b/scripts/release/changelog.py index 236a80b69..d7fd888ec 100644 --- a/scripts/release/changelog.py +++ b/scripts/release/changelog.py @@ -19,16 +19,14 @@ class ChangeType(StrEnum): def get_changelog_entries( - previous_commit: Commit, - repository_path: str, + previous_version_commit: Commit, + repo: Repo, changelog_sub_path: str, ) -> list[tuple[ChangeType, str]]: changelog = [] - repo = Repo(repository_path) - # Compare previous version commit with current working tree - diff_index = previous_commit.diff(other=repo.head.commit, paths=changelog_sub_path) + diff_index = previous_version_commit.diff(other=repo.head.commit, paths=changelog_sub_path) # No changes since the previous version if not diff_index: @@ -40,7 +38,7 @@ def get_changelog_entries( file_name = os.path.basename(file_path) change_type = get_change_type(file_name) - abs_file_path = os.path.join(repository_path, file_path) + abs_file_path = os.path.join(repo.working_dir, file_path) with open(abs_file_path, 'r') as file: file_content = file.read() diff --git a/scripts/release/changelog_test.py b/scripts/release/changelog_test.py index 097933b26..917d7b4f8 100644 --- a/scripts/release/changelog_test.py +++ b/scripts/release/changelog_test.py @@ -1,10 +1,26 @@ -from git import Repo +from changelog import get_change_type, ChangeType -import changelog -from conftest import git_repo -from scripts.release.changelog import CHANGELOG_PATH +def test_get_change_type(): -def test_get_changelog_entries(git_repo: Repo): - repo_path = git_repo.working_dir - entries = changelog.get_changelog_entries("1.0.0", repo_path, CHANGELOG_PATH) + # Test prelude + assert get_change_type("20250502_prelude_release_notes.md") == ChangeType.PRELUDE + + # Test breaking changes + assert get_change_type("20250101_breaking_change_api_update.md") == ChangeType.BREAKING + assert get_change_type("20250508_breaking_remove_deprecated.md") == ChangeType.BREAKING + assert get_change_type("20250509_major_schema_change.md") == ChangeType.BREAKING + + # Test features + assert get_change_type("20250509_feature_new_dashboard.md") == ChangeType.FEATURE + assert get_change_type("20250511_feat_add_metrics.md") == ChangeType.FEATURE + + # Test fixes + assert get_change_type("20251210_fix_olm_missing_images.md") == ChangeType.FIX + assert get_change_type("20251010_bugfix_memory_leak.md") == ChangeType.FIX + assert get_change_type("20250302_hotfix_security_issue.md") == ChangeType.FIX + assert get_change_type("20250301_patch_typo_correction.md") == ChangeType.FIX + + # Test other + assert get_change_type("20250520_docs_update_readme.md") == ChangeType.OTHER + assert get_change_type("20250610_refactor_codebase.md") == ChangeType.OTHER diff --git a/scripts/release/conftest.py b/scripts/release/conftest.py index 74fb48795..3baaa4369 100644 --- a/scripts/release/conftest.py +++ b/scripts/release/conftest.py @@ -9,7 +9,10 @@ @fixture(scope="session") def git_repo(change_log_path: str = CHANGELOG_PATH) -> Repo: - """Create a temporary git repository for testing.""" + """ + 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). + """ repo_dir = tempfile.mkdtemp() repo = Repo.init(repo_dir) @@ -19,9 +22,11 @@ def git_repo(change_log_path: str = CHANGELOG_PATH) -> Repo: ## 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") - changelog_file = add_file(repo_dir, "changelog/20250506_prelude_mck.md") - repo.index.add([new_file, changelog_file]) + 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 diff --git a/scripts/release/release_notes.py b/scripts/release/release_notes.py index eca7eb3e9..9001399b6 100644 --- a/scripts/release/release_notes.py +++ b/scripts/release/release_notes.py @@ -1,24 +1,53 @@ +import argparse +import pathlib +import sys + from git import Repo from jinja2 import Template from scripts.release.changelog import CHANGELOG_PATH, get_changelog_entries, ChangeType -from scripts.release.versioning import calculate_next_release_version, find_previous_version +from scripts.release.versioning import calculate_next_release_version, find_previous_version_tag def generate_release_notes( repository_path: str = '.', changelog_sub_path: str = CHANGELOG_PATH, + initial_commit_sha: str = None, + initial_version: str = "1.0.0", ) -> str: - """Generate a release notes based on the changes since the previous version tag.""" + """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 'changelog/'. + 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 "1.0.0". + + Returns: + Formatted release notes as a string. + """ repo = Repo(repository_path) - initial_commit = list(repo.iter_commits(reverse=True))[0].hexsha - previous_version, previous_commit = find_previous_version("0.0.0", initial_commit, repository_path) + previous_version_tag = find_previous_version_tag(repo) - changelog: list = get_changelog_entries(previous_commit, repository_path, changelog_sub_path) + # 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 + previous_version_commit = repo.commit(initial_commit_sha) + else: + previous_version_commit = previous_version_tag.commit + + changelog: list = get_changelog_entries(previous_version_commit, repo, changelog_sub_path) changelog_entries = list[ChangeType](map(lambda x: x[0], changelog)) - version = calculate_next_release_version(previous_version, changelog_entries) + + # 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_entries) with open('scripts/release/release_notes_tpl.md', "r") as f: template = Template(f.read()) @@ -33,3 +62,26 @@ def generate_release_notes( } return template.render(parameters) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--path", action="store", default=".", type=pathlib.Path, + help="Path to the Git repository. Default is the current directory '.'") + parser.add_argument("--changelog_path", default="changelog/", action="store", type=str, + help="Path to the changelog directory relative to the repository root. Default is 'changelog/'") + parser.add_argument("--initial_commit_sha", action="store", type=str, + help="SHA of the initial commit to start from if no previous version tag is found.") + parser.add_argument("--initial_version", default="1.0.0", action="store", type=str, + help="Version to use if no previous version tag is found. Default is '1.0.0'") + parser.add_argument("--output", "-o", type=pathlib.Path) + 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 f: + f.write(release_notes) + else: + sys.stdout.write(release_notes) diff --git a/scripts/release/release_notes_test.py b/scripts/release/release_notes_test.py index b8b584e34..94cad7820 100644 --- a/scripts/release/release_notes_test.py +++ b/scripts/release/release_notes_test.py @@ -4,102 +4,66 @@ 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): - ## TODO: Does not work for the initial commit - assert True + checkout_and_assert_release_notes(git_repo, "1.0.0") def test_generate_release_notes_1_0_1(git_repo: Repo): - repo_path = git_repo.working_dir - git_repo.git.checkout("1.0.1") - release_notes = generate_release_notes(repo_path) - with open("scripts/release/testdata/release_notes_1.0.1.md") as file: - assert release_notes == file.read() + checkout_and_assert_release_notes(git_repo, "1.0.1") def test_generate_release_notes_1_1_0(git_repo: Repo): - repo_path = git_repo.working_dir - git_repo.git.checkout("1.1.0") - release_notes = generate_release_notes(repo_path) - with open("scripts/release/testdata/release_notes_1.1.0.md") as file: - assert release_notes == file.read() + checkout_and_assert_release_notes(git_repo, "1.1.0") def test_generate_release_notes_1_2_0(git_repo: Repo): - repo_path = git_repo.working_dir - git_repo.git.checkout("1.2.0") - release_notes = generate_release_notes(repo_path) - with open("scripts/release/testdata/release_notes_1.2.0.md") as file: - assert release_notes == file.read() + checkout_and_assert_release_notes(git_repo, "1.2.0") def test_generate_release_notes_2_0_0(git_repo: Repo): - repo_path = git_repo.working_dir - git_repo.git.checkout("2.0.0") - release_notes = generate_release_notes(repo_path) - with open("scripts/release/testdata/release_notes_2.0.0.md") as file: - assert release_notes == file.read() + checkout_and_assert_release_notes(git_repo, "2.0.0") def test_generate_release_notes_1_2_1(git_repo: Repo): - repo_path = git_repo.working_dir - git_repo.git.checkout("1.2.1") - release_notes = generate_release_notes(repo_path) - with open("scripts/release/testdata/release_notes_1.2.1.md") as file: - assert release_notes == file.read() + checkout_and_assert_release_notes(git_repo, "1.2.1") def test_generate_release_notes_2_0_1(git_repo: Repo): - repo_path = git_repo.working_dir - git_repo.git.checkout("2.0.1") - release_notes = generate_release_notes(repo_path) - with open("scripts/release/testdata/release_notes_2.0.1.md") as file: - assert release_notes == file.read() + checkout_and_assert_release_notes(git_repo, "2.0.1") def test_generate_release_notes_1_2_2(git_repo: Repo): - repo_path = git_repo.working_dir - git_repo.git.checkout("1.2.2") - release_notes = generate_release_notes(repo_path) - with open("scripts/release/testdata/release_notes_1.2.2.md") as file: - assert release_notes == file.read() + checkout_and_assert_release_notes(git_repo, "1.2.2") def test_generate_release_notes_2_0_2(git_repo: Repo): - repo_path = git_repo.working_dir - git_repo.git.checkout("2.0.2") - release_notes = generate_release_notes(repo_path) - with open("scripts/release/testdata/release_notes_2.0.2.md") as file: - assert release_notes == file.read() + checkout_and_assert_release_notes(git_repo, "2.0.2") def test_generate_release_notes_1_2_3(git_repo: Repo): - repo_path = git_repo.working_dir - git_repo.git.checkout("1.2.3") - release_notes = generate_release_notes(repo_path) - with open("scripts/release/testdata/release_notes_1.2.3.md") as file: - assert release_notes == file.read() + checkout_and_assert_release_notes(git_repo, "1.2.3") def test_generate_release_notes_3_0_0(git_repo: Repo): - repo_path = git_repo.working_dir - git_repo.git.checkout("3.0.0") - release_notes = generate_release_notes(repo_path) - with open("scripts/release/testdata/release_notes_3.0.0.md") as file: - assert release_notes == file.read() + checkout_and_assert_release_notes(git_repo, "3.0.0") def test_generate_release_notes_2_0_3(git_repo: Repo): - repo_path = git_repo.working_dir - git_repo.git.checkout("2.0.3") - release_notes = generate_release_notes(repo_path) - with open("scripts/release/testdata/release_notes_2.0.3.md") as file: - assert release_notes == file.read() + checkout_and_assert_release_notes(git_repo, "2.0.3") def test_generate_release_notes_1_2_4(git_repo: Repo): - repo_path = git_repo.working_dir - git_repo.git.checkout("1.2.4") - release_notes = generate_release_notes(repo_path) - with open("scripts/release/testdata/release_notes_1.2.4.md") as file: + 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/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/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/versioning.py b/scripts/release/versioning.py index 6cf6aa226..7fb3f7de2 100644 --- a/scripts/release/versioning.py +++ b/scripts/release/versioning.py @@ -1,10 +1,12 @@ import semver -from git import Commit, Repo +from git import Repo, TagReference from scripts.release.changelog import ChangeType -def find_previous_version(initial_version: str, initial_commit_sha: str, repository_path: str = '.', ) -> tuple[str, Commit]: - repo = Repo(repository_path) + +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 @@ -12,13 +14,13 @@ def find_previous_version(initial_version: str, initial_commit_sha: str, reposit # Filter valid SemVer tags and sort them valid_tags = filter(lambda t: semver.VersionInfo.is_valid(t.name), ancestor_tags) - sorted_tags: list = sorted(valid_tags, key=lambda t: semver.VersionInfo.parse(t.name), reverse=True) + sorted_tags = sorted(valid_tags, key=lambda t: semver.VersionInfo.parse(t.name), reverse=True) if not sorted_tags: - # Find the initial commit by traversing to the earliest commit reachable from HEAD - return initial_version, repo.git.rev_parse(initial_commit_sha) + return None + + return sorted_tags[0] - return sorted_tags[0].name, sorted_tags[0].commit def calculate_next_release_version(previous_version_str: str, changelog: list[ChangeType]) -> str: previous_version = semver.VersionInfo.parse(previous_version_str) diff --git a/scripts/release/versioning_test.py b/scripts/release/versioning_test.py index 10e615963..dc17a38e0 100644 --- a/scripts/release/versioning_test.py +++ b/scripts/release/versioning_test.py @@ -1 +1,74 @@ import unittest +from scripts.release.versioning import calculate_next_release_version +from scripts.release.changelog import ChangeType + + +class TestCalculateNextReleaseVersion(unittest.TestCase): + + def test_bump_major_version(self): + previous_version = "1.2.3" + changelog = [ChangeType.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 = [ChangeType.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 = [ChangeType.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 = [ChangeType.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 = [ChangeType.FEATURE, ChangeType.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 = [ChangeType.FEATURE, ChangeType.BREAKING, ChangeType.FIX, ChangeType.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 = [ChangeType.BREAKING, ChangeType.BREAKING, ChangeType.FEATURE, ChangeType.FIX, ChangeType.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 = [ChangeType.FEATURE, ChangeType.FEATURE, ChangeType.FIX, ChangeType.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 = [ChangeType.FIX, ChangeType.FIX, ChangeType.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 = [ChangeType.OTHER, ChangeType.OTHER] + next_version = calculate_next_release_version(previous_version, changelog) + self.assertEqual(next_version, "1.2.4") From 0977ac5b7cceed459b3dda160e0197deea62ab59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Sun, 15 Jun 2025 15:57:13 +0200 Subject: [PATCH 10/30] Add main method in versioning.py --- scripts/release/conftest.py | 1 + scripts/release/release_notes.py | 29 ++++----------- scripts/release/versioning.py | 62 ++++++++++++++++++++++++++++++-- 3 files changed, 67 insertions(+), 25 deletions(-) diff --git a/scripts/release/conftest.py b/scripts/release/conftest.py index 3baaa4369..3bca0c1c2 100644 --- a/scripts/release/conftest.py +++ b/scripts/release/conftest.py @@ -12,6 +12,7 @@ def git_repo(change_log_path: str = 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() diff --git a/scripts/release/release_notes.py b/scripts/release/release_notes.py index 9001399b6..bcba59361 100644 --- a/scripts/release/release_notes.py +++ b/scripts/release/release_notes.py @@ -6,7 +6,8 @@ from jinja2 import Template from scripts.release.changelog import CHANGELOG_PATH, get_changelog_entries, ChangeType -from scripts.release.versioning import calculate_next_release_version, find_previous_version_tag +from scripts.release.versioning import calculate_next_release_version, find_previous_version, \ + calculate_next_version_with_changelog def generate_release_notes( @@ -28,29 +29,11 @@ def generate_release_notes( """ repo = Repo(repository_path) - previous_version_tag = find_previous_version_tag(repo) + changelog, version = calculate_next_version_with_changelog(repo, changelog_sub_path, initial_commit_sha, + initial_version) - # 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 - - previous_version_commit = repo.commit(initial_commit_sha) - else: - previous_version_commit = previous_version_tag.commit - - changelog: list = get_changelog_entries(previous_version_commit, repo, changelog_sub_path) - changelog_entries = list[ChangeType](map(lambda x: x[0], 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_entries) - - with open('scripts/release/release_notes_tpl.md', "r") as f: - template = Template(f.read()) + with open('scripts/release/release_notes_tpl.md', "r") as file: + template = Template(file.read()) parameters = { 'version': version, diff --git a/scripts/release/versioning.py b/scripts/release/versioning.py index 7fb3f7de2..8e87c05fe 100644 --- a/scripts/release/versioning.py +++ b/scripts/release/versioning.py @@ -1,7 +1,46 @@ +import argparse +import pathlib +import sys + import semver -from git import Repo, TagReference +from git import Repo, TagReference, Commit + +from scripts.release.changelog import ChangeType, get_changelog_entries + + +def calculate_next_version_with_changelog( + repo: Repo, + changelog_sub_path: str, + initial_commit_sha: str | None, + initial_version: str) -> (str, list[tuple[ChangeType, str]]): + previous_version_tag, previous_version_commit = find_previous_version(repo, initial_commit_sha) + + changelog: list[tuple[ChangeType, str]] = get_changelog_entries(previous_version_commit, repo, changelog_sub_path) + changelog_types = list[ChangeType](map(lambda x: x[0], 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_types) + + return version, changelog + -from scripts.release.changelog import ChangeType +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: @@ -32,3 +71,22 @@ def calculate_next_release_version(previous_version_str: str, changelog: list[Ch return str(previous_version.bump_minor()) return str(previous_version.bump_patch()) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--path", action="store", default=".", type=pathlib.Path, + help="Path to the Git repository. Default is the current directory '.'") + parser.add_argument("--changelog_path", default="changelog/", action="store", type=str, + help="Path to the changelog directory relative to the repository root. Default is 'changelog/'") + parser.add_argument("--initial_commit_sha", action="store", type=str, + help="SHA of the initial commit to start from if no previous version tag is found.") + parser.add_argument("--initial_version", default="1.0.0", action="store", type=str, + help="Version to use if no previous version tag is found. Default is '1.0.0'") + parser.add_argument("--output", "-o", type=pathlib.Path) + args = parser.parse_args() + + _, version = calculate_next_version_with_changelog(args.path, args.changelog_path, args.initial_commit_sha, + args.initial_version) + + sys.stdout.write(version) From 9b37d491437b52bdc787f377ccaa0d945b920c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Mon, 16 Jun 2025 09:27:03 +0200 Subject: [PATCH 11/30] Move main method to calculate_next_version.py --- scripts/release/calculate_next_version.py | 27 ++++++ scripts/release/release_notes.py | 34 +++++-- scripts/release/version.py | 50 ++++++++++ .../{versioning_test.py => version_test.py} | 2 +- scripts/release/versioning.py | 92 ------------------- 5 files changed, 104 insertions(+), 101 deletions(-) create mode 100644 scripts/release/calculate_next_version.py create mode 100644 scripts/release/version.py rename scripts/release/{versioning_test.py => version_test.py} (97%) delete mode 100644 scripts/release/versioning.py diff --git a/scripts/release/calculate_next_version.py b/scripts/release/calculate_next_version.py new file mode 100644 index 000000000..afb166099 --- /dev/null +++ b/scripts/release/calculate_next_version.py @@ -0,0 +1,27 @@ +import argparse +import pathlib +import sys + +from git import Repo + +from scripts.release.release_notes import calculate_next_version_with_changelog + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--path", action="store", default=".", type=pathlib.Path, + help="Path to the Git repository. Default is the current directory '.'") + parser.add_argument("--changelog_path", default="changelog/", action="store", type=str, + help="Path to the changelog directory relative to the repository root. Default is 'changelog/'") + parser.add_argument("--initial_commit_sha", action="store", type=str, + help="SHA of the initial commit to start from if no previous version tag is found.") + parser.add_argument("--initial_version", default="1.0.0", action="store", type=str, + help="Version to use if no previous version tag is found. Default is '1.0.0'") + parser.add_argument("--output", "-o", type=pathlib.Path) + 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/release_notes.py b/scripts/release/release_notes.py index bcba59361..6b2f1b89b 100644 --- a/scripts/release/release_notes.py +++ b/scripts/release/release_notes.py @@ -6,8 +6,7 @@ from jinja2 import Template from scripts.release.changelog import CHANGELOG_PATH, get_changelog_entries, ChangeType -from scripts.release.versioning import calculate_next_release_version, find_previous_version, \ - calculate_next_version_with_changelog +from scripts.release.version import calculate_next_release_version, find_previous_version def generate_release_notes( @@ -29,11 +28,11 @@ def generate_release_notes( """ repo = Repo(repository_path) - changelog, version = calculate_next_version_with_changelog(repo, changelog_sub_path, initial_commit_sha, + 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 file: - template = Template(file.read()) + with open('scripts/release/release_notes_tpl.md', "r") as f: + template = Template(f.read()) parameters = { 'version': version, @@ -47,6 +46,25 @@ 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[tuple[ChangeType, str]]): + previous_version_tag, previous_version_commit = find_previous_version(repo, initial_commit_sha) + + changelog: list[tuple[ChangeType, str]] = get_changelog_entries(previous_version_commit, repo, changelog_sub_path) + changelog_types = list[ChangeType](map(lambda x: x[0], 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_types) + + return version, changelog + + if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--path", action="store", default=".", type=pathlib.Path, @@ -64,7 +82,7 @@ def generate_release_notes( args.initial_version) if args.output is not None: - with open(args.output, "w") as f: - f.write(release_notes) + with open(args.output, "w") as file: + file.write(release_notes) else: - sys.stdout.write(release_notes) + print(release_notes) diff --git a/scripts/release/version.py b/scripts/release/version.py new file mode 100644 index 000000000..79006256c --- /dev/null +++ b/scripts/release/version.py @@ -0,0 +1,50 @@ +import semver +from git import Repo, TagReference, Commit + +from scripts.release.changelog import ChangeType + + +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[ChangeType]) -> str: + previous_version = semver.VersionInfo.parse(previous_version_str) + + if ChangeType.BREAKING in changelog: + return str(previous_version.bump_major()) + + if ChangeType.FEATURE in changelog: + return str(previous_version.bump_minor()) + + return str(previous_version.bump_patch()) diff --git a/scripts/release/versioning_test.py b/scripts/release/version_test.py similarity index 97% rename from scripts/release/versioning_test.py rename to scripts/release/version_test.py index dc17a38e0..a15784306 100644 --- a/scripts/release/versioning_test.py +++ b/scripts/release/version_test.py @@ -1,5 +1,5 @@ import unittest -from scripts.release.versioning import calculate_next_release_version +from scripts.release.version import calculate_next_release_version from scripts.release.changelog import ChangeType diff --git a/scripts/release/versioning.py b/scripts/release/versioning.py deleted file mode 100644 index 8e87c05fe..000000000 --- a/scripts/release/versioning.py +++ /dev/null @@ -1,92 +0,0 @@ -import argparse -import pathlib -import sys - -import semver -from git import Repo, TagReference, Commit - -from scripts.release.changelog import ChangeType, get_changelog_entries - - -def calculate_next_version_with_changelog( - repo: Repo, - changelog_sub_path: str, - initial_commit_sha: str | None, - initial_version: str) -> (str, list[tuple[ChangeType, str]]): - previous_version_tag, previous_version_commit = find_previous_version(repo, initial_commit_sha) - - changelog: list[tuple[ChangeType, str]] = get_changelog_entries(previous_version_commit, repo, changelog_sub_path) - changelog_types = list[ChangeType](map(lambda x: x[0], 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_types) - - return version, changelog - - -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[ChangeType]) -> str: - previous_version = semver.VersionInfo.parse(previous_version_str) - - if ChangeType.BREAKING in changelog: - return str(previous_version.bump_major()) - - if ChangeType.FEATURE in changelog: - return str(previous_version.bump_minor()) - - return str(previous_version.bump_patch()) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--path", action="store", default=".", type=pathlib.Path, - help="Path to the Git repository. Default is the current directory '.'") - parser.add_argument("--changelog_path", default="changelog/", action="store", type=str, - help="Path to the changelog directory relative to the repository root. Default is 'changelog/'") - parser.add_argument("--initial_commit_sha", action="store", type=str, - help="SHA of the initial commit to start from if no previous version tag is found.") - parser.add_argument("--initial_version", default="1.0.0", action="store", type=str, - help="Version to use if no previous version tag is found. Default is '1.0.0'") - parser.add_argument("--output", "-o", type=pathlib.Path) - args = parser.parse_args() - - _, version = calculate_next_version_with_changelog(args.path, args.changelog_path, args.initial_commit_sha, - args.initial_version) - - sys.stdout.write(version) From 54707c18ce01454cc307825b0d9934e65f4e3d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Mon, 16 Jun 2025 09:42:53 +0200 Subject: [PATCH 12/30] Optimize imports --- scripts/release/calculate_next_version.py | 1 - scripts/release/release_notes.py | 1 - 2 files changed, 2 deletions(-) diff --git a/scripts/release/calculate_next_version.py b/scripts/release/calculate_next_version.py index afb166099..72039aa6a 100644 --- a/scripts/release/calculate_next_version.py +++ b/scripts/release/calculate_next_version.py @@ -1,6 +1,5 @@ import argparse import pathlib -import sys from git import Repo diff --git a/scripts/release/release_notes.py b/scripts/release/release_notes.py index 6b2f1b89b..9e99bc300 100644 --- a/scripts/release/release_notes.py +++ b/scripts/release/release_notes.py @@ -1,6 +1,5 @@ import argparse import pathlib -import sys from git import Repo from jinja2 import Template From 55bcbc05d5bed8d4573047d1118160fad4a33dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Mon, 16 Jun 2025 09:50:53 +0200 Subject: [PATCH 13/30] Lint fix --- scripts/release/calculate_next_version.py | 40 +++++++++--- scripts/release/changelog.py | 21 ++++--- scripts/release/changelog_test.py | 2 +- scripts/release/conftest.py | 5 +- scripts/release/release_notes.py | 74 +++++++++++++++-------- scripts/release/release_notes_test.py | 4 +- scripts/release/version.py | 2 +- scripts/release/version_test.py | 3 +- 8 files changed, 99 insertions(+), 52 deletions(-) diff --git a/scripts/release/calculate_next_version.py b/scripts/release/calculate_next_version.py index 72039aa6a..5545b4355 100644 --- a/scripts/release/calculate_next_version.py +++ b/scripts/release/calculate_next_version.py @@ -7,20 +7,40 @@ if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument("--path", action="store", default=".", type=pathlib.Path, - help="Path to the Git repository. Default is the current directory '.'") - parser.add_argument("--changelog_path", default="changelog/", action="store", type=str, - help="Path to the changelog directory relative to the repository root. Default is 'changelog/'") - parser.add_argument("--initial_commit_sha", action="store", type=str, - help="SHA of the initial commit to start from if no previous version tag is found.") - parser.add_argument("--initial_version", default="1.0.0", action="store", type=str, - help="Version to use if no previous version tag is found. Default is '1.0.0'") + parser.add_argument( + "--path", + action="store", + default=".", + type=pathlib.Path, + help="Path to the Git repository. Default is the current directory '.'", + ) + parser.add_argument( + "--changelog_path", + default="changelog/", + action="store", + type=str, + help="Path to the changelog directory relative to the repository root. Default is 'changelog/'", + ) + parser.add_argument( + "--initial_commit_sha", + action="store", + type=str, + help="SHA of the initial commit to start from if no previous version tag is found.", + ) + parser.add_argument( + "--initial_version", + default="1.0.0", + action="store", + type=str, + help="Version to use if no previous version tag is found. Default is '1.0.0'", + ) parser.add_argument("--output", "-o", type=pathlib.Path) 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_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 index d7fd888ec..2de2d59eb 100644 --- a/scripts/release/changelog.py +++ b/scripts/release/changelog.py @@ -1,6 +1,7 @@ import os from enum import StrEnum -from git import Repo, Commit + +from git import Commit, Repo CHANGELOG_PATH = "changelog/" @@ -11,17 +12,17 @@ class ChangeType(StrEnum): - PRELUDE = 'prelude' - BREAKING = 'breaking' - FEATURE = 'feature' - FIX = 'fix' - OTHER = 'other' + PRELUDE = "prelude" + BREAKING = "breaking" + FEATURE = "feature" + FIX = "fix" + OTHER = "other" def get_changelog_entries( - previous_version_commit: Commit, - repo: Repo, - changelog_sub_path: str, + previous_version_commit: Commit, + repo: Repo, + changelog_sub_path: str, ) -> list[tuple[ChangeType, str]]: changelog = [] @@ -39,7 +40,7 @@ def get_changelog_entries( change_type = get_change_type(file_name) abs_file_path = os.path.join(repo.working_dir, file_path) - with open(abs_file_path, 'r') as file: + with open(abs_file_path, "r") as file: file_content = file.read() changelog.append((change_type, file_content)) diff --git a/scripts/release/changelog_test.py b/scripts/release/changelog_test.py index 917d7b4f8..c2e3e0b61 100644 --- a/scripts/release/changelog_test.py +++ b/scripts/release/changelog_test.py @@ -1,4 +1,4 @@ -from changelog import get_change_type, ChangeType +from changelog import ChangeType, get_change_type def test_get_change_type(): diff --git a/scripts/release/conftest.py b/scripts/release/conftest.py index 3bca0c1c2..94b8bd66e 100644 --- a/scripts/release/conftest.py +++ b/scripts/release/conftest.py @@ -4,6 +4,7 @@ from _pytest.fixtures import fixture from git import Repo + from scripts.release.changelog import CHANGELOG_PATH @@ -47,7 +48,7 @@ def git_repo(change_log_path: str = CHANGELOG_PATH) -> Repo: changelog_file = add_file( repo_dir, "changelog/20250523_feature_community_search_preview_UPDATED.md", - "changelog/20250523_feature_community_search_preview.md" + "changelog/20250523_feature_community_search_preview.md", ) repo.index.add(changelog_file) repo.index.commit("add limitations in changelog for private search preview") @@ -156,6 +157,6 @@ def add_file(repo_path: str, src_file_path: str, dst_file_path: str | None = Non 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) + src_path = os.path.join("scripts/release/testdata", src_file_path) return shutil.copy(src_path, dst_path) diff --git a/scripts/release/release_notes.py b/scripts/release/release_notes.py index 9e99bc300..30191eedd 100644 --- a/scripts/release/release_notes.py +++ b/scripts/release/release_notes.py @@ -4,12 +4,15 @@ from git import Repo from jinja2 import Template -from scripts.release.changelog import CHANGELOG_PATH, get_changelog_entries, ChangeType -from scripts.release.version import calculate_next_release_version, find_previous_version +from scripts.release.changelog import CHANGELOG_PATH, ChangeType, get_changelog_entries +from scripts.release.version import ( + calculate_next_release_version, + find_previous_version, +) def generate_release_notes( - repository_path: str = '.', + repository_path: str = ".", changelog_sub_path: str = CHANGELOG_PATH, initial_commit_sha: str = None, initial_version: str = "1.0.0", @@ -27,29 +30,28 @@ def generate_release_notes( """ repo = Repo(repository_path) - version, changelog = calculate_next_version_with_changelog(repo, changelog_sub_path, initial_commit_sha, - initial_version) + 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: + with open("scripts/release/release_notes_tpl.md", "r") as f: template = Template(f.read()) parameters = { - 'version': version, - 'preludes': [c[1] for c in changelog if c[0] == ChangeType.PRELUDE], - 'breaking_changes': [c[1] for c in changelog if c[0] == ChangeType.BREAKING], - 'features': [c[1] for c in changelog if c[0] == ChangeType.FEATURE], - 'fixes': [c[1] for c in changelog if c[0] == ChangeType.FIX], - 'others': [c[1] for c in changelog if c[0] == ChangeType.OTHER], + "version": version, + "preludes": [c[1] for c in changelog if c[0] == ChangeType.PRELUDE], + "breaking_changes": [c[1] for c in changelog if c[0] == ChangeType.BREAKING], + "features": [c[1] for c in changelog if c[0] == ChangeType.FEATURE], + "fixes": [c[1] for c in changelog if c[0] == ChangeType.FIX], + "others": [c[1] for c in changelog if c[0] == ChangeType.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[tuple[ChangeType, str]]): + repo: Repo, changelog_sub_path: str, initial_commit_sha: str | None, initial_version: str +) -> (str, list[tuple[ChangeType, str]]): previous_version_tag, previous_version_commit = find_previous_version(repo, initial_commit_sha) changelog: list[tuple[ChangeType, str]] = get_changelog_entries(previous_version_commit, repo, changelog_sub_path) @@ -66,19 +68,39 @@ def calculate_next_version_with_changelog( if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument("--path", action="store", default=".", type=pathlib.Path, - help="Path to the Git repository. Default is the current directory '.'") - parser.add_argument("--changelog_path", default="changelog/", action="store", type=str, - help="Path to the changelog directory relative to the repository root. Default is 'changelog/'") - parser.add_argument("--initial_commit_sha", action="store", type=str, - help="SHA of the initial commit to start from if no previous version tag is found.") - parser.add_argument("--initial_version", default="1.0.0", action="store", type=str, - help="Version to use if no previous version tag is found. Default is '1.0.0'") + parser.add_argument( + "--path", + action="store", + default=".", + type=pathlib.Path, + help="Path to the Git repository. Default is the current directory '.'", + ) + parser.add_argument( + "--changelog_path", + default="changelog/", + action="store", + type=str, + help="Path to the changelog directory relative to the repository root. Default is 'changelog/'", + ) + parser.add_argument( + "--initial_commit_sha", + action="store", + type=str, + help="SHA of the initial commit to start from if no previous version tag is found.", + ) + parser.add_argument( + "--initial_version", + default="1.0.0", + action="store", + type=str, + help="Version to use if no previous version tag is found. Default is '1.0.0'", + ) parser.add_argument("--output", "-o", type=pathlib.Path) args = parser.parse_args() - release_notes = generate_release_notes(args.path, args.changelog_path, args.initial_commit_sha, - args.initial_version) + 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: diff --git a/scripts/release/release_notes_test.py b/scripts/release/release_notes_test.py index 94cad7820..aea8d5e4b 100644 --- a/scripts/release/release_notes_test.py +++ b/scripts/release/release_notes_test.py @@ -1,6 +1,6 @@ +from conftest import git_repo from git import Repo -from conftest import git_repo from scripts.release.release_notes import generate_release_notes @@ -11,6 +11,7 @@ def test_generate_release_notes_before_1_0_0(git_repo: Repo): 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") @@ -62,6 +63,7 @@ def test_generate_release_notes_2_0_3(git_repo: Repo): 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) diff --git a/scripts/release/version.py b/scripts/release/version.py index 79006256c..9d1699958 100644 --- a/scripts/release/version.py +++ b/scripts/release/version.py @@ -1,5 +1,5 @@ import semver -from git import Repo, TagReference, Commit +from git import Commit, Repo, TagReference from scripts.release.changelog import ChangeType diff --git a/scripts/release/version_test.py b/scripts/release/version_test.py index a15784306..d6ad8d4ad 100644 --- a/scripts/release/version_test.py +++ b/scripts/release/version_test.py @@ -1,6 +1,7 @@ import unittest -from scripts.release.version import calculate_next_release_version + from scripts.release.changelog import ChangeType +from scripts.release.version import calculate_next_release_version class TestCalculateNextReleaseVersion(unittest.TestCase): From 9b176e3c203b985dda20c1de6ccf3b9fa422adee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Fri, 11 Jul 2025 14:02:49 +0200 Subject: [PATCH 14/30] Add changelog entry frontmatter text --- scripts/release/changelog.py | 25 ++++++++++++++- scripts/release/changelog_test.py | 31 +++++++++++++++++-- .../changelog/20250506_prelude_mck.md | 6 ++++ .../20250510_fix_olm_missing_images.md | 6 ++++ .../20250510_fix_watched_list_in_helm.md | 6 ++++ ...250523_feature_community_search_preview.md | 6 ++++ ...eature_community_search_preview_UPDATED.md | 6 ++++ .../changelog/20250610_feature_oidc.md | 6 ++++ .../20250612_breaking_static_as_default.md | 6 ++++ .../20250616_feature_om_no_service_mesh.md | 6 ++++ .../20250620_fix_static_container.md | 6 ++++ .../changelog/20250622_fix_external_access.md | 6 ++++ .../changelog/20250623_prelude_static.md | 6 ++++ .../changelog/20250701_fix_placeholder.md | 6 ++++ ...20250702_fix_clusterspeclist_validation.md | 6 ++++ .../changelog/20250707_fix_proxy_env_var.md | 6 ++++ ...20250710_breaking_mongodbmulti_refactor.md | 6 ++++ .../20250710_prelude_mongodbmulti_refactor.md | 6 ++++ .../20250711_feature_public_search.md | 6 ++++ .../20250712_fix_mongodbuser_phase.md | 6 ++++ 20 files changed, 161 insertions(+), 3 deletions(-) diff --git a/scripts/release/changelog.py b/scripts/release/changelog.py index 2de2d59eb..a208ac57d 100644 --- a/scripts/release/changelog.py +++ b/scripts/release/changelog.py @@ -1,6 +1,8 @@ +import datetime import os from enum import StrEnum +import frontmatter from git import Commit, Repo CHANGELOG_PATH = "changelog/" @@ -43,7 +45,9 @@ def get_changelog_entries( with open(abs_file_path, "r") as file: file_content = file.read() - changelog.append((change_type, file_content)) + _, contents = strip_changelog_entry_frontmatter(file_content) + + changelog.append((change_type, contents)) return changelog @@ -61,3 +65,22 @@ def get_change_type(file_name: str) -> ChangeType: return ChangeType.FIX else: return ChangeType.OTHER + + +class ChangeMeta: + def __init__(self, date: datetime, kind: ChangeType, title: str): + self.date = date + self.kind = kind + self.title = title + + +def strip_changelog_entry_frontmatter(file_contents: str) -> (ChangeMeta, str): + """Strip the front matter from a changelog entry.""" + data = frontmatter.loads(file_contents) + + meta = ChangeMeta(date=data["date"], title=str(data["title"]), kind=ChangeType(str(data["kind"]).lower())) + + ## Add newline to contents so the Markdown file also contains a newline at the end + contents = data.content + "\n" + + return meta, contents diff --git a/scripts/release/changelog_test.py b/scripts/release/changelog_test.py index c2e3e0b61..ecd716ae6 100644 --- a/scripts/release/changelog_test.py +++ b/scripts/release/changelog_test.py @@ -1,8 +1,9 @@ -from changelog import ChangeType, get_change_type +import datetime +from changelog import ChangeType, get_change_type, strip_changelog_entry_frontmatter -def test_get_change_type(): +def test_get_change_type(): # Test prelude assert get_change_type("20250502_prelude_release_notes.md") == ChangeType.PRELUDE @@ -24,3 +25,29 @@ def test_get_change_type(): # Test other assert get_change_type("20250520_docs_update_readme.md") == ChangeType.OTHER assert get_change_type("20250610_refactor_codebase.md") == ChangeType.OTHER + + +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_meta, contents = strip_changelog_entry_frontmatter(file_contents) + + assert change_meta.title == "This is my change" + assert change_meta.kind == ChangeType.FEATURE + assert change_meta.date == datetime.date(2025, 7, 10) + + assert ( + 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. +""" + ) diff --git a/scripts/release/testdata/changelog/20250506_prelude_mck.md b/scripts/release/testdata/changelog/20250506_prelude_mck.md index 209c99008..e96ed298e 100644 --- a/scripts/release/testdata/changelog/20250506_prelude_mck.md +++ b/scripts/release/testdata/changelog/20250506_prelude_mck.md @@ -1,3 +1,9 @@ +--- +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** diff --git a/scripts/release/testdata/changelog/20250510_fix_olm_missing_images.md b/scripts/release/testdata/changelog/20250510_fix_olm_missing_images.md index e520dcf36..121fdba9a 100644 --- a/scripts/release/testdata/changelog/20250510_fix_olm_missing_images.md +++ b/scripts/release/testdata/changelog/20250510_fix_olm_missing_images.md @@ -1 +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 index 42c05bfc2..d1c74a290 100644 --- a/scripts/release/testdata/changelog/20250510_fix_watched_list_in_helm.md +++ b/scripts/release/testdata/changelog/20250510_fix_watched_list_in_helm.md @@ -1 +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 index b51af5d76..25883a031 100644 --- a/scripts/release/testdata/changelog/20250523_feature_community_search_preview.md +++ b/scripts/release/testdata/changelog/20250523_feature_community_search_preview.md @@ -1,3 +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 index 7aa2269d4..856683a27 100644 --- a/scripts/release/testdata/changelog/20250523_feature_community_search_preview_UPDATED.md +++ b/scripts/release/testdata/changelog/20250523_feature_community_search_preview_UPDATED.md @@ -1,3 +1,9 @@ +--- +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) diff --git a/scripts/release/testdata/changelog/20250610_feature_oidc.md b/scripts/release/testdata/changelog/20250610_feature_oidc.md index 2aedae72e..d3e760670 100644 --- a/scripts/release/testdata/changelog/20250610_feature_oidc.md +++ b/scripts/release/testdata/changelog/20250610_feature_oidc.md @@ -1,3 +1,9 @@ +--- +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: diff --git a/scripts/release/testdata/changelog/20250612_breaking_static_as_default.md b/scripts/release/testdata/changelog/20250612_breaking_static_as_default.md index ed2bb7775..c7a548c1a 100644 --- a/scripts/release/testdata/changelog/20250612_breaking_static_as_default.md +++ b/scripts/release/testdata/changelog/20250612_breaking_static_as_default.md @@ -1 +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 index ff96ff558..0d1f96412 100644 --- a/scripts/release/testdata/changelog/20250616_feature_om_no_service_mesh.md +++ b/scripts/release/testdata/changelog/20250616_feature_om_no_service_mesh.md @@ -1,3 +1,9 @@ +--- +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) diff --git a/scripts/release/testdata/changelog/20250620_fix_static_container.md b/scripts/release/testdata/changelog/20250620_fix_static_container.md index d9071c434..b3d52a03a 100644 --- a/scripts/release/testdata/changelog/20250620_fix_static_container.md +++ b/scripts/release/testdata/changelog/20250620_fix_static_container.md @@ -1 +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 index 01f417c8f..4b7682036 100644 --- a/scripts/release/testdata/changelog/20250622_fix_external_access.md +++ b/scripts/release/testdata/changelog/20250622_fix_external_access.md @@ -1 +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 index 018fde24f..b78325d6e 100644 --- a/scripts/release/testdata/changelog/20250623_prelude_static.md +++ b/scripts/release/testdata/changelog/20250623_prelude_static.md @@ -1 +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 index ede8e1b5d..befe2ff9e 100644 --- a/scripts/release/testdata/changelog/20250701_fix_placeholder.md +++ b/scripts/release/testdata/changelog/20250701_fix_placeholder.md @@ -1 +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 index 17a5cf292..0343439c5 100644 --- a/scripts/release/testdata/changelog/20250702_fix_clusterspeclist_validation.md +++ b/scripts/release/testdata/changelog/20250702_fix_clusterspeclist_validation.md @@ -1 +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 index d33f831f9..8cf1c4528 100644 --- a/scripts/release/testdata/changelog/20250707_fix_proxy_env_var.md +++ b/scripts/release/testdata/changelog/20250707_fix_proxy_env_var.md @@ -1 +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 index d1ae671a0..20554eceb 100644 --- a/scripts/release/testdata/changelog/20250710_breaking_mongodbmulti_refactor.md +++ b/scripts/release/testdata/changelog/20250710_breaking_mongodbmulti_refactor.md @@ -1 +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 index ee74b14c7..c43526f90 100644 --- a/scripts/release/testdata/changelog/20250710_prelude_mongodbmulti_refactor.md +++ b/scripts/release/testdata/changelog/20250710_prelude_mongodbmulti_refactor.md @@ -1 +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 index 8c4b824f1..720e598cd 100644 --- a/scripts/release/testdata/changelog/20250711_feature_public_search.md +++ b/scripts/release/testdata/changelog/20250711_feature_public_search.md @@ -1,2 +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 index ca2cf71bd..09b7a149a 100644 --- a/scripts/release/testdata/changelog/20250712_fix_mongodbuser_phase.md +++ b/scripts/release/testdata/changelog/20250712_fix_mongodbuser_phase.md @@ -1 +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. From 2cacd1283f6d6d6a38f8581832a3f8a5a36c80b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Sun, 13 Jul 2025 21:55:29 +0200 Subject: [PATCH 15/30] Added frontmatter validation --- scripts/release/changelog.py | 78 +++++++++++++++++++--------- scripts/release/changelog_test.py | 85 ++++++++++++++++++++++++++----- 2 files changed, 126 insertions(+), 37 deletions(-) diff --git a/scripts/release/changelog.py b/scripts/release/changelog.py index a208ac57d..98be06834 100644 --- a/scripts/release/changelog.py +++ b/scripts/release/changelog.py @@ -1,5 +1,6 @@ import datetime import os +import re from enum import StrEnum import frontmatter @@ -8,7 +9,7 @@ CHANGELOG_PATH = "changelog/" PRELUDE_ENTRIES = ["prelude"] -BREAKING_CHANGE_ENTRIES = ["breaking_change", "breaking", "major"] +BREAKING_CHANGE_ENTRIES = ["breaking", "major"] FEATURE_ENTRIES = ["feat", "feature"] BUGFIX_ENTRIES = ["fix", "bugfix", "hotfix", "patch"] @@ -21,6 +22,13 @@ class ChangeType(StrEnum): OTHER = "other" +class ChangeMeta: + def __init__(self, date: datetime, kind: ChangeType, title: str): + self.date = date + self.kind = kind + self.title = title + + def get_changelog_entries( previous_version_commit: Commit, repo: Repo, @@ -38,47 +46,71 @@ def get_changelog_entries( # 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 - file_name = os.path.basename(file_path) - change_type = get_change_type(file_name) - abs_file_path = os.path.join(repo.working_dir, file_path) - with open(abs_file_path, "r") as file: - file_content = file.read() + change_meta, contents = extract_changelog_data(repo.working_dir, file_path) - _, contents = strip_changelog_entry_frontmatter(file_content) - - changelog.append((change_type, contents)) + changelog.append((str(change_meta.kind), contents)) return changelog -def get_change_type(file_name: str) -> ChangeType: - """Extract the change type from the file name.""" +def extract_changelog_data(working_dir: str, file_path: str) -> (ChangeMeta, str): + 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_meta, contents = strip_changelog_entry_frontmatter(file_content) + + if change_meta.date != date: + raise Exception( + f"{file_name} - date in front matter {change_meta.date} does not match date extracted from file name {date}" + ) + + if change_meta.kind != kind: + raise Exception( + f"{file_name} - kind in front matter {change_meta.kind} does not match kind extracted from file name {kind}" + ) + + return change_meta, contents - if any(entry in file_name.lower() for entry in PRELUDE_ENTRIES): + +def extract_date_and_kind_from_file_name(file_name: str) -> (datetime, ChangeType): + 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 = datetime.datetime.strptime(date_str, "%Y%m%d").date() + except Exception: + raise Exception(f"{file_name} - date part {date_str} is not in the expected format YYYYMMDD") + + kind = get_change_type(kind_str) + + return date, kind + + +def get_change_type(kind_str: str) -> ChangeType: + if kind_str.lower() in PRELUDE_ENTRIES: return ChangeType.PRELUDE - if any(entry in file_name.lower() for entry in BREAKING_CHANGE_ENTRIES): + if kind_str.lower() in BREAKING_CHANGE_ENTRIES: return ChangeType.BREAKING - elif any(entry in file_name.lower() for entry in FEATURE_ENTRIES): + elif kind_str.lower() in FEATURE_ENTRIES: return ChangeType.FEATURE - elif any(entry in file_name.lower() for entry in BUGFIX_ENTRIES): + elif kind_str.lower() in BUGFIX_ENTRIES: return ChangeType.FIX else: return ChangeType.OTHER -class ChangeMeta: - def __init__(self, date: datetime, kind: ChangeType, title: str): - self.date = date - self.kind = kind - self.title = title - - def strip_changelog_entry_frontmatter(file_contents: str) -> (ChangeMeta, str): """Strip the front matter from a changelog entry.""" data = frontmatter.loads(file_contents) - meta = ChangeMeta(date=data["date"], title=str(data["title"]), kind=ChangeType(str(data["kind"]).lower())) + meta = ChangeMeta(date=data["date"], title=str(data["title"]), kind=get_change_type(str(data["kind"]))) ## Add newline to contents so the Markdown file also contains a newline at the end contents = data.content + "\n" diff --git a/scripts/release/changelog_test.py b/scripts/release/changelog_test.py index ecd716ae6..2b9d7f2e3 100644 --- a/scripts/release/changelog_test.py +++ b/scripts/release/changelog_test.py @@ -1,30 +1,87 @@ import datetime -from changelog import ChangeType, get_change_type, strip_changelog_entry_frontmatter +import pytest +from changelog import ( + ChangeType, + extract_date_and_kind_from_file_name, + strip_changelog_entry_frontmatter, +) -def test_get_change_type(): + +def test_extract_changelog_data_from_file_name(): # Test prelude - assert get_change_type("20250502_prelude_release_notes.md") == ChangeType.PRELUDE + assert extract_date_and_kind_from_file_name("20250502_prelude_release_notes.md") == ( + datetime.date(2025, 5, 2), + ChangeType.PRELUDE, + ) # Test breaking changes - assert get_change_type("20250101_breaking_change_api_update.md") == ChangeType.BREAKING - assert get_change_type("20250508_breaking_remove_deprecated.md") == ChangeType.BREAKING - assert get_change_type("20250509_major_schema_change.md") == ChangeType.BREAKING + assert extract_date_and_kind_from_file_name("20250101_breaking_api_update.md") == ( + datetime.date(2025, 1, 1), + ChangeType.BREAKING, + ) + assert extract_date_and_kind_from_file_name("20250508_breaking_remove_deprecated.md") == ( + datetime.date(2025, 5, 8), + ChangeType.BREAKING, + ) + assert extract_date_and_kind_from_file_name("20250509_major_schema_change.md") == ( + datetime.date(2025, 5, 9), + ChangeType.BREAKING, + ) # Test features - assert get_change_type("20250509_feature_new_dashboard.md") == ChangeType.FEATURE - assert get_change_type("20250511_feat_add_metrics.md") == ChangeType.FEATURE + assert extract_date_and_kind_from_file_name("20250509_feature_new_dashboard.md") == ( + datetime.date(2025, 5, 9), + ChangeType.FEATURE, + ) + assert extract_date_and_kind_from_file_name("20250511_feat_add_metrics.md") == ( + datetime.date(2025, 5, 11), + ChangeType.FEATURE, + ) # Test fixes - assert get_change_type("20251210_fix_olm_missing_images.md") == ChangeType.FIX - assert get_change_type("20251010_bugfix_memory_leak.md") == ChangeType.FIX - assert get_change_type("20250302_hotfix_security_issue.md") == ChangeType.FIX - assert get_change_type("20250301_patch_typo_correction.md") == ChangeType.FIX + assert extract_date_and_kind_from_file_name("20251210_fix_olm_missing_images.md") == ( + datetime.date(2025, 12, 10), + ChangeType.FIX, + ) + assert extract_date_and_kind_from_file_name("20251010_bugfix_memory_leak.md") == ( + datetime.date(2025, 10, 10), + ChangeType.FIX, + ) + assert extract_date_and_kind_from_file_name("20250302_hotfix_security_issue.md") == ( + datetime.date(2025, 3, 2), + ChangeType.FIX, + ) + assert extract_date_and_kind_from_file_name("20250301_patch_typo_correction.md") == ( + datetime.date(2025, 3, 1), + ChangeType.FIX, + ) # Test other - assert get_change_type("20250520_docs_update_readme.md") == ChangeType.OTHER - assert get_change_type("20250610_refactor_codebase.md") == ChangeType.OTHER + assert extract_date_and_kind_from_file_name("20250520_docs_update_readme.md") == ( + datetime.date(2025, 5, 20), + ChangeType.OTHER, + ) + assert extract_date_and_kind_from_file_name("20250610_refactor_codebase.md") == ( + datetime.date(2025, 6, 10), + ChangeType.OTHER, + ) + + # Invalid date part (day 40 does not exist) + with pytest.raises(Exception) as e: + extract_date_and_kind_from_file_name("20250640_refactor_codebase.md") + assert str(e.value) == "20250640_refactor_codebase.md - date part 20250640 is not in the expected format YYYYMMDD" + + # Wrong file name format (date part) + with pytest.raises(Exception) as e: + extract_date_and_kind_from_file_name("202yas_refactor_codebase.md") + assert str(e.value) == "202yas_refactor_codebase.md - doesn't match expected pattern" + + # Wrong file name format (missing title part) + with pytest.raises(Exception) as e: + extract_date_and_kind_from_file_name("20250620_change.md") + assert str(e.value) == "20250620_change.md - doesn't match expected pattern" def test_strip_changelog_entry_frontmatter(): From ab8a217cfc99ece00fb5b69152d21932f39db591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Sun, 13 Jul 2025 23:50:00 +0200 Subject: [PATCH 16/30] Script for generating changelog file --- requirements.txt | 1 + scripts/release/calculate_next_version.py | 28 ++++-- scripts/release/changelog.py | 46 ++++++---- scripts/release/changelog_test.py | 31 ++++--- scripts/release/conftest.py | 4 +- scripts/release/create_changelog.py | 105 ++++++++++++++++++++++ scripts/release/release_notes.py | 51 +++++++---- scripts/release/version.py | 8 +- scripts/release/version_test.py | 22 ++--- 9 files changed, 220 insertions(+), 76 deletions(-) create mode 100644 scripts/release/create_changelog.py diff --git a/requirements.txt b/requirements.txt index 318338bd2..c3ce86737 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,6 +33,7 @@ pytest-mock==3.14.1 wrapt==1.17.2 botocore==1.39.4 boto3==1.39.4 +python-frontmatter==1.1.0 # from kubeobject freezegun==1.5.3 diff --git a/scripts/release/calculate_next_version.py b/scripts/release/calculate_next_version.py index 5545b4355..62d0b2411 100644 --- a/scripts/release/calculate_next_version.py +++ b/scripts/release/calculate_next_version.py @@ -3,36 +3,48 @@ 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() parser.add_argument( + "-p", "--path", - action="store", default=".", + metavar="", + action="store", type=pathlib.Path, help="Path to the Git repository. Default is the current directory '.'", ) parser.add_argument( - "--changelog_path", - default="changelog/", + "-c", + "--changelog-path", + default=DEFAULT_CHANGELOG_PATH, + metavar="", action="store", type=str, - help="Path to the changelog directory relative to the repository root. Default is 'changelog/'", + help=f"Path to the changelog directory relative to the repository root. Default is '{DEFAULT_CHANGELOG_PATH}'", ) parser.add_argument( - "--initial_commit_sha", + "-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( - "--initial_version", - default="1.0.0", + "-v", + "--initial-version", + default=DEFAULT_INITIAL_GIT_TAG_VERSION, + metavar="", action="store", type=str, - help="Version to use if no previous version tag is found. Default is '1.0.0'", + help=f"Version to use if no previous version tag is found. Default is '{DEFAULT_INITIAL_GIT_TAG_VERSION}'", ) parser.add_argument("--output", "-o", type=pathlib.Path) args = parser.parse_args() diff --git a/scripts/release/changelog.py b/scripts/release/changelog.py index 98be06834..d194a33e0 100644 --- a/scripts/release/changelog.py +++ b/scripts/release/changelog.py @@ -6,7 +6,10 @@ import frontmatter from git import Commit, Repo -CHANGELOG_PATH = "changelog/" +DEFAULT_CHANGELOG_PATH = "changelog/" +DEFAULT_INITIAL_GIT_TAG_VERSION = "1.0.0" +FILENAME_DATE_FORMAT = "%Y%m%d" +FRONTMATTER_DATE_FORMAT = "%Y-%m-%d" PRELUDE_ENTRIES = ["prelude"] BREAKING_CHANGE_ENTRIES = ["breaking", "major"] @@ -14,7 +17,7 @@ BUGFIX_ENTRIES = ["fix", "bugfix", "hotfix", "patch"] -class ChangeType(StrEnum): +class ChangeKind(StrEnum): PRELUDE = "prelude" BREAKING = "breaking" FEATURE = "feature" @@ -23,7 +26,7 @@ class ChangeType(StrEnum): class ChangeMeta: - def __init__(self, date: datetime, kind: ChangeType, title: str): + def __init__(self, date: datetime, kind: ChangeKind, title: str): self.date = date self.kind = kind self.title = title @@ -33,7 +36,7 @@ def get_changelog_entries( previous_version_commit: Commit, repo: Repo, changelog_sub_path: str, -) -> list[tuple[ChangeType, str]]: +) -> list[tuple[ChangeKind, str]]: changelog = [] # Compare previous version commit with current working tree @@ -77,40 +80,51 @@ def extract_changelog_data(working_dir: str, file_path: str) -> (ChangeMeta, str return change_meta, contents -def extract_date_and_kind_from_file_name(file_name: str) -> (datetime, ChangeType): +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 = datetime.datetime.strptime(date_str, "%Y%m%d").date() - except Exception: - raise Exception(f"{file_name} - date part {date_str} is not in the expected format YYYYMMDD") + date = parse_change_date(date_str, FILENAME_DATE_FORMAT) + except Exception as e: + raise Exception(f"{file_name} - {e}") - kind = get_change_type(kind_str) + kind = get_change_kind(kind_str) return date, kind -def get_change_type(kind_str: str) -> ChangeType: +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 ChangeType.PRELUDE + return ChangeKind.PRELUDE if kind_str.lower() in BREAKING_CHANGE_ENTRIES: - return ChangeType.BREAKING + return ChangeKind.BREAKING elif kind_str.lower() in FEATURE_ENTRIES: - return ChangeType.FEATURE + return ChangeKind.FEATURE elif kind_str.lower() in BUGFIX_ENTRIES: - return ChangeType.FIX + return ChangeKind.FIX else: - return ChangeType.OTHER + return ChangeKind.OTHER def strip_changelog_entry_frontmatter(file_contents: str) -> (ChangeMeta, str): """Strip the front matter from a changelog entry.""" data = frontmatter.loads(file_contents) - meta = ChangeMeta(date=data["date"], title=str(data["title"]), kind=get_change_type(str(data["kind"]))) + kind = get_change_kind(str(data["kind"])) + date = parse_change_date(str(data["date"]), FRONTMATTER_DATE_FORMAT) + meta = ChangeMeta(date=date, title=str(data["title"]), kind=kind) ## Add newline to contents so the Markdown file also contains a newline at the end contents = data.content + "\n" diff --git a/scripts/release/changelog_test.py b/scripts/release/changelog_test.py index 2b9d7f2e3..0840e218f 100644 --- a/scripts/release/changelog_test.py +++ b/scripts/release/changelog_test.py @@ -1,9 +1,8 @@ import datetime import pytest - from changelog import ( - ChangeType, + ChangeKind, extract_date_and_kind_from_file_name, strip_changelog_entry_frontmatter, ) @@ -13,65 +12,65 @@ def test_extract_changelog_data_from_file_name(): # Test prelude assert extract_date_and_kind_from_file_name("20250502_prelude_release_notes.md") == ( datetime.date(2025, 5, 2), - ChangeType.PRELUDE, + ChangeKind.PRELUDE, ) # Test breaking changes assert extract_date_and_kind_from_file_name("20250101_breaking_api_update.md") == ( datetime.date(2025, 1, 1), - ChangeType.BREAKING, + ChangeKind.BREAKING, ) assert extract_date_and_kind_from_file_name("20250508_breaking_remove_deprecated.md") == ( datetime.date(2025, 5, 8), - ChangeType.BREAKING, + ChangeKind.BREAKING, ) assert extract_date_and_kind_from_file_name("20250509_major_schema_change.md") == ( datetime.date(2025, 5, 9), - ChangeType.BREAKING, + ChangeKind.BREAKING, ) # Test features assert extract_date_and_kind_from_file_name("20250509_feature_new_dashboard.md") == ( datetime.date(2025, 5, 9), - ChangeType.FEATURE, + ChangeKind.FEATURE, ) assert extract_date_and_kind_from_file_name("20250511_feat_add_metrics.md") == ( datetime.date(2025, 5, 11), - ChangeType.FEATURE, + ChangeKind.FEATURE, ) # Test fixes assert extract_date_and_kind_from_file_name("20251210_fix_olm_missing_images.md") == ( datetime.date(2025, 12, 10), - ChangeType.FIX, + ChangeKind.FIX, ) assert extract_date_and_kind_from_file_name("20251010_bugfix_memory_leak.md") == ( datetime.date(2025, 10, 10), - ChangeType.FIX, + ChangeKind.FIX, ) assert extract_date_and_kind_from_file_name("20250302_hotfix_security_issue.md") == ( datetime.date(2025, 3, 2), - ChangeType.FIX, + ChangeKind.FIX, ) assert extract_date_and_kind_from_file_name("20250301_patch_typo_correction.md") == ( datetime.date(2025, 3, 1), - ChangeType.FIX, + ChangeKind.FIX, ) # Test other assert extract_date_and_kind_from_file_name("20250520_docs_update_readme.md") == ( datetime.date(2025, 5, 20), - ChangeType.OTHER, + ChangeKind.OTHER, ) assert extract_date_and_kind_from_file_name("20250610_refactor_codebase.md") == ( datetime.date(2025, 6, 10), - ChangeType.OTHER, + ChangeKind.OTHER, ) # Invalid date part (day 40 does not exist) with pytest.raises(Exception) as e: extract_date_and_kind_from_file_name("20250640_refactor_codebase.md") - assert str(e.value) == "20250640_refactor_codebase.md - date part 20250640 is not in the expected format YYYYMMDD" + assert str(e.value) == "20250640_refactor_codebase.md - date 20250640 is not in the expected format YYYYMMDD" # Wrong file name format (date part) with pytest.raises(Exception) as e: @@ -99,7 +98,7 @@ def test_strip_changelog_entry_frontmatter(): change_meta, contents = strip_changelog_entry_frontmatter(file_contents) assert change_meta.title == "This is my change" - assert change_meta.kind == ChangeType.FEATURE + assert change_meta.kind == ChangeKind.FEATURE assert change_meta.date == datetime.date(2025, 7, 10) assert ( diff --git a/scripts/release/conftest.py b/scripts/release/conftest.py index 94b8bd66e..39b47849d 100644 --- a/scripts/release/conftest.py +++ b/scripts/release/conftest.py @@ -5,11 +5,11 @@ from _pytest.fixtures import fixture from git import Repo -from scripts.release.changelog import CHANGELOG_PATH +from scripts.release.changelog import DEFAULT_CHANGELOG_PATH @fixture(scope="session") -def git_repo(change_log_path: str = CHANGELOG_PATH) -> Repo: +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). diff --git a/scripts/release/create_changelog.py b/scripts/release/create_changelog.py new file mode 100644 index 000000000..a7e66ad0f --- /dev/null +++ b/scripts/release/create_changelog.py @@ -0,0 +1,105 @@ +import argparse +import datetime +import os +import re + +from scripts.release.changelog import ( + BREAKING_CHANGE_ENTRIES, + BUGFIX_ENTRIES, + DEFAULT_CHANGELOG_PATH, + FEATURE_ENTRIES, + FILENAME_DATE_FORMAT, + FRONTMATTER_DATE_FORMAT, + PRELUDE_ENTRIES, + parse_change_date, +) + +MAX_TITLE_LENGTH = 50 + + +def sanitize_title(title: str) -> str: + # Remove non-alphabetic and space characters + regex = re.compile("[^a-zA-Z ]+") + title = regex.sub("", title) + + # 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 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(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() + + date = parse_change_date(args.date, FRONTMATTER_DATE_FORMAT) + sanitized_title = sanitize_title(args.title) + filename = f"{datetime.datetime.strftime(date, FILENAME_DATE_FORMAT)}_{args.kind}_{sanitized_title}.md" + + 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: {args.title}\n") + f.write(f"kind: {args.kind}\n") + f.write(f"date: {date}\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 index 30191eedd..b446aacbc 100644 --- a/scripts/release/release_notes.py +++ b/scripts/release/release_notes.py @@ -4,7 +4,12 @@ from git import Repo from jinja2 import Template -from scripts.release.changelog import CHANGELOG_PATH, ChangeType, get_changelog_entries +from scripts.release.changelog import ( + DEFAULT_CHANGELOG_PATH, + DEFAULT_INITIAL_GIT_TAG_VERSION, + ChangeKind, + get_changelog_entries, +) from scripts.release.version import ( calculate_next_release_version, find_previous_version, @@ -13,7 +18,7 @@ def generate_release_notes( repository_path: str = ".", - changelog_sub_path: str = CHANGELOG_PATH, + changelog_sub_path: str = DEFAULT_CHANGELOG_PATH, initial_commit_sha: str = None, initial_version: str = "1.0.0", ) -> str: @@ -39,11 +44,11 @@ def generate_release_notes( parameters = { "version": version, - "preludes": [c[1] for c in changelog if c[0] == ChangeType.PRELUDE], - "breaking_changes": [c[1] for c in changelog if c[0] == ChangeType.BREAKING], - "features": [c[1] for c in changelog if c[0] == ChangeType.FEATURE], - "fixes": [c[1] for c in changelog if c[0] == ChangeType.FIX], - "others": [c[1] for c in changelog if c[0] == ChangeType.OTHER], + "preludes": [c[1] for c in changelog if c[0] == ChangeKind.PRELUDE], + "breaking_changes": [c[1] for c in changelog if c[0] == ChangeKind.BREAKING], + "features": [c[1] for c in changelog if c[0] == ChangeKind.FEATURE], + "fixes": [c[1] for c in changelog if c[0] == ChangeKind.FIX], + "others": [c[1] for c in changelog if c[0] == ChangeKind.OTHER], } return template.render(parameters) @@ -51,17 +56,17 @@ def generate_release_notes( def calculate_next_version_with_changelog( repo: Repo, changelog_sub_path: str, initial_commit_sha: str | None, initial_version: str -) -> (str, list[tuple[ChangeType, str]]): +) -> (str, list[tuple[ChangeKind, str]]): previous_version_tag, previous_version_commit = find_previous_version(repo, initial_commit_sha) - changelog: list[tuple[ChangeType, str]] = get_changelog_entries(previous_version_commit, repo, changelog_sub_path) - changelog_types = list[ChangeType](map(lambda x: x[0], changelog)) + changelog: list[tuple[ChangeKind, str]] = get_changelog_entries(previous_version_commit, repo, changelog_sub_path) + changelog_kinds = list[ChangeKind](map(lambda x: x[0], 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_types) + version = calculate_next_release_version(previous_version_tag.name, changelog_kinds) return version, changelog @@ -69,31 +74,39 @@ def calculate_next_version_with_changelog( if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( + "-p", "--path", - action="store", default=".", + metavar="", + action="store", type=pathlib.Path, help="Path to the Git repository. Default is the current directory '.'", ) parser.add_argument( - "--changelog_path", - default="changelog/", + "-c", + "--changelog-path", + default=DEFAULT_CHANGELOG_PATH, + metavar="", action="store", type=str, - help="Path to the changelog directory relative to the repository root. Default is 'changelog/'", + help=f"Path to the changelog directory relative to the repository root. Default is '{DEFAULT_CHANGELOG_PATH}'", ) parser.add_argument( - "--initial_commit_sha", + "-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( - "--initial_version", - default="1.0.0", + "-v", + "--initial-version", + default=DEFAULT_INITIAL_GIT_TAG_VERSION, + metavar="", action="store", type=str, - help="Version to use if no previous version tag is found. Default is '1.0.0'", + help=f"Version to use if no previous version tag is found. Default is '{DEFAULT_INITIAL_GIT_TAG_VERSION}'", ) parser.add_argument("--output", "-o", type=pathlib.Path) args = parser.parse_args() diff --git a/scripts/release/version.py b/scripts/release/version.py index 9d1699958..105fc7ee4 100644 --- a/scripts/release/version.py +++ b/scripts/release/version.py @@ -1,7 +1,7 @@ import semver from git import Commit, Repo, TagReference -from scripts.release.changelog import ChangeType +from scripts.release.changelog import ChangeKind def find_previous_version(repo: Repo, initial_commit_sha: str = None) -> (TagReference | None, Commit): @@ -38,13 +38,13 @@ def find_previous_version_tag(repo: Repo) -> TagReference | None: return sorted_tags[0] -def calculate_next_release_version(previous_version_str: str, changelog: list[ChangeType]) -> str: +def calculate_next_release_version(previous_version_str: str, changelog: list[ChangeKind]) -> str: previous_version = semver.VersionInfo.parse(previous_version_str) - if ChangeType.BREAKING in changelog: + if ChangeKind.BREAKING in changelog: return str(previous_version.bump_major()) - if ChangeType.FEATURE in changelog: + 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 index d6ad8d4ad..c75adef77 100644 --- a/scripts/release/version_test.py +++ b/scripts/release/version_test.py @@ -1,6 +1,6 @@ import unittest -from scripts.release.changelog import ChangeType +from scripts.release.changelog import ChangeKind from scripts.release.version import calculate_next_release_version @@ -8,25 +8,25 @@ class TestCalculateNextReleaseVersion(unittest.TestCase): def test_bump_major_version(self): previous_version = "1.2.3" - changelog = [ChangeType.BREAKING] + 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 = [ChangeType.FEATURE] + 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 = [ChangeType.FIX] + 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 = [ChangeType.OTHER] + changelog = [ChangeKind.OTHER] next_version = calculate_next_release_version(previous_version, changelog) self.assertEqual(next_version, "1.2.4") @@ -39,37 +39,37 @@ def test_bump_patch_version_no_changes(self): def test_feature_takes_precedence(self): # Test that FEATURE has precedence over FIX previous_version = "1.2.3" - changelog = [ChangeType.FEATURE, ChangeType.FIX] + 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 = [ChangeType.FEATURE, ChangeType.BREAKING, ChangeType.FIX, ChangeType.OTHER] + 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 = [ChangeType.BREAKING, ChangeType.BREAKING, ChangeType.FEATURE, ChangeType.FIX, ChangeType.OTHER] + 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 = [ChangeType.FEATURE, ChangeType.FEATURE, ChangeType.FIX, ChangeType.OTHER] + 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 = [ChangeType.FIX, ChangeType.FIX, ChangeType.OTHER] + 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 = [ChangeType.OTHER, ChangeType.OTHER] + changelog = [ChangeKind.OTHER, ChangeKind.OTHER] next_version = calculate_next_release_version(previous_version, changelog) self.assertEqual(next_version, "1.2.4") From 36bd5dbe2f121adf88f7f51161515e55774d76ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Sun, 13 Jul 2025 23:53:29 +0200 Subject: [PATCH 17/30] Review fixes --- scripts/release/release_notes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/release/release_notes.py b/scripts/release/release_notes.py index b446aacbc..4d555dc44 100644 --- a/scripts/release/release_notes.py +++ b/scripts/release/release_notes.py @@ -20,15 +20,15 @@ def generate_release_notes( repository_path: str = ".", changelog_sub_path: str = DEFAULT_CHANGELOG_PATH, initial_commit_sha: str = None, - initial_version: str = "1.0.0", + initial_version: str = DEFAULT_INITIAL_GIT_TAG_VERSION, ) -> str: - """Generate a release notes based on the changes since the previous version tag. + 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 'changelog/'. + 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 "1.0.0". + 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. From 0f9813624627b6cf8f02bab5b0224b3de34ed3b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Mon, 14 Jul 2025 10:33:54 +0200 Subject: [PATCH 18/30] Review fixes v2 --- scripts/release/changelog.py | 31 ++++++ scripts/release/changelog_test.py | 163 ++++++++++++++++------------ scripts/release/create_changelog.py | 38 ++----- 3 files changed, 136 insertions(+), 96 deletions(-) diff --git a/scripts/release/changelog.py b/scripts/release/changelog.py index d194a33e0..ca17ed55d 100644 --- a/scripts/release/changelog.py +++ b/scripts/release/changelog.py @@ -10,6 +10,7 @@ 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"] @@ -130,3 +131,33 @@ def strip_changelog_entry_frontmatter(file_contents: str) -> (ChangeMeta, str): contents = data.content + "\n" return meta, 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 index 0840e218f..4f6b8183e 100644 --- a/scripts/release/changelog_test.py +++ b/scripts/release/changelog_test.py @@ -1,86 +1,85 @@ import datetime +import unittest -import pytest from changelog import ( + MAX_TITLE_LENGTH, ChangeKind, extract_date_and_kind_from_file_name, + sanitize_title, strip_changelog_entry_frontmatter, ) -def test_extract_changelog_data_from_file_name(): - # Test prelude - assert extract_date_and_kind_from_file_name("20250502_prelude_release_notes.md") == ( - datetime.date(2025, 5, 2), - ChangeKind.PRELUDE, - ) +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) - # Test breaking changes - assert extract_date_and_kind_from_file_name("20250101_breaking_api_update.md") == ( - datetime.date(2025, 1, 1), - ChangeKind.BREAKING, - ) - assert extract_date_and_kind_from_file_name("20250508_breaking_remove_deprecated.md") == ( - datetime.date(2025, 5, 8), - ChangeKind.BREAKING, - ) - assert extract_date_and_kind_from_file_name("20250509_major_schema_change.md") == ( - datetime.date(2025, 5, 9), - ChangeKind.BREAKING, - ) + 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) - # Test features - assert extract_date_and_kind_from_file_name("20250509_feature_new_dashboard.md") == ( - datetime.date(2025, 5, 9), - ChangeKind.FEATURE, - ) - assert extract_date_and_kind_from_file_name("20250511_feat_add_metrics.md") == ( - datetime.date(2025, 5, 11), - ChangeKind.FEATURE, - ) + 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) - # Test fixes - assert extract_date_and_kind_from_file_name("20251210_fix_olm_missing_images.md") == ( - datetime.date(2025, 12, 10), - ChangeKind.FIX, - ) - assert extract_date_and_kind_from_file_name("20251010_bugfix_memory_leak.md") == ( - datetime.date(2025, 10, 10), - ChangeKind.FIX, - ) - assert extract_date_and_kind_from_file_name("20250302_hotfix_security_issue.md") == ( - datetime.date(2025, 3, 2), - ChangeKind.FIX, - ) - assert extract_date_and_kind_from_file_name("20250301_patch_typo_correction.md") == ( - datetime.date(2025, 3, 1), - ChangeKind.FIX, - ) + 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) - # Test other - assert extract_date_and_kind_from_file_name("20250520_docs_update_readme.md") == ( - datetime.date(2025, 5, 20), - ChangeKind.OTHER, - ) - assert extract_date_and_kind_from_file_name("20250610_refactor_codebase.md") == ( - datetime.date(2025, 6, 10), - ChangeKind.OTHER, - ) + 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) - # Invalid date part (day 40 does not exist) - with pytest.raises(Exception) as e: - extract_date_and_kind_from_file_name("20250640_refactor_codebase.md") - assert str(e.value) == "20250640_refactor_codebase.md - date 20250640 is not in the expected format YYYYMMDD" + 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) - # Wrong file name format (date part) - with pytest.raises(Exception) as e: - extract_date_and_kind_from_file_name("202yas_refactor_codebase.md") - assert str(e.value) == "202yas_refactor_codebase.md - doesn't match expected pattern" + 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) - # Wrong file name format (missing title part) - with pytest.raises(Exception) as e: - extract_date_and_kind_from_file_name("20250620_change.md") - assert str(e.value) == "20250620_change.md - doesn't match expected pattern" + 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(): @@ -107,3 +106,33 @@ def test_strip_changelog_entry_frontmatter(): * 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/create_changelog.py b/scripts/release/create_changelog.py index a7e66ad0f..859263abb 100644 --- a/scripts/release/create_changelog.py +++ b/scripts/release/create_changelog.py @@ -1,41 +1,19 @@ import argparse import datetime import os -import re from scripts.release.changelog import ( BREAKING_CHANGE_ENTRIES, BUGFIX_ENTRIES, DEFAULT_CHANGELOG_PATH, FEATURE_ENTRIES, - FILENAME_DATE_FORMAT, FRONTMATTER_DATE_FORMAT, PRELUDE_ENTRIES, + get_change_kind, + get_changelog_filename, parse_change_date, ) -MAX_TITLE_LENGTH = 50 - - -def sanitize_title(title: str) -> str: - # Remove non-alphabetic and space characters - regex = re.compile("[^a-zA-Z ]+") - title = regex.sub("", title) - - # 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 - - if __name__ == "__main__": parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) parser.add_argument( @@ -79,9 +57,11 @@ def sanitize_title(title: str) -> str: 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) - sanitized_title = sanitize_title(args.title) - filename = f"{datetime.datetime.strftime(date, FILENAME_DATE_FORMAT)}_{args.kind}_{sanitized_title}.md" + 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) @@ -93,9 +73,9 @@ def sanitize_title(title: str) -> str: with open(changelog_path, "w") as f: # Add frontmatter based on args f.write("---\n") - f.write(f"title: {args.title}\n") - f.write(f"kind: {args.kind}\n") - f.write(f"date: {date}\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: From e0078a7b7724fb37624744a1e3dfc1f95768d0c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Mon, 14 Jul 2025 11:01:55 +0200 Subject: [PATCH 19/30] Review fixes v3 --- scripts/release/calculate_next_version.py | 6 ++++-- scripts/release/create_changelog.py | 5 ++++- scripts/release/release_notes.py | 13 +++++++++++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/scripts/release/calculate_next_version.py b/scripts/release/calculate_next_version.py index 62d0b2411..4015301ad 100644 --- a/scripts/release/calculate_next_version.py +++ b/scripts/release/calculate_next_version.py @@ -10,7 +10,10 @@ from scripts.release.release_notes import calculate_next_version_with_changelog if __name__ == "__main__": - parser = argparse.ArgumentParser() + 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", @@ -46,7 +49,6 @@ 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", type=pathlib.Path) args = parser.parse_args() repo = Repo(args.path) diff --git a/scripts/release/create_changelog.py b/scripts/release/create_changelog.py index 859263abb..48f23db3c 100644 --- a/scripts/release/create_changelog.py +++ b/scripts/release/create_changelog.py @@ -15,7 +15,10 @@ ) if __name__ == "__main__": - parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) + parser = argparse.ArgumentParser( + description="Utility to easily create a new changelog entry file.", + formatter_class=argparse.RawTextHelpFormatter, + ) parser.add_argument( "-c", "--changelog-path", diff --git a/scripts/release/release_notes.py b/scripts/release/release_notes.py index 4d555dc44..c875aa4a2 100644 --- a/scripts/release/release_notes.py +++ b/scripts/release/release_notes.py @@ -72,7 +72,10 @@ def calculate_next_version_with_changelog( if __name__ == "__main__": - parser = argparse.ArgumentParser() + 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", @@ -108,7 +111,13 @@ def calculate_next_version_with_changelog( 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", type=pathlib.Path) + 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( From 1fc658d0716c1564a680239ea03326ebd3742321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Mon, 14 Jul 2025 11:04:13 +0200 Subject: [PATCH 20/30] Review fixes v4 --- scripts/release/create_changelog.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/release/create_changelog.py b/scripts/release/create_changelog.py index 48f23db3c..4029bf74a 100644 --- a/scripts/release/create_changelog.py +++ b/scripts/release/create_changelog.py @@ -51,10 +51,10 @@ 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 + - '{", ".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") From da1849e66835f8eeee05a0607b2d1b3751a76c95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Tue, 15 Jul 2025 09:50:03 +0200 Subject: [PATCH 21/30] Using ChangeEntry type --- scripts/release/changelog.py | 33 ++++++++++++++----------------- scripts/release/changelog_test.py | 13 ++++++------ scripts/release/release_notes.py | 17 ++++++++-------- 3 files changed, 30 insertions(+), 33 deletions(-) diff --git a/scripts/release/changelog.py b/scripts/release/changelog.py index ca17ed55d..f8b61a543 100644 --- a/scripts/release/changelog.py +++ b/scripts/release/changelog.py @@ -26,18 +26,19 @@ class ChangeKind(StrEnum): OTHER = "other" -class ChangeMeta: - def __init__(self, date: datetime, kind: ChangeKind, title: str): +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( previous_version_commit: Commit, repo: Repo, changelog_sub_path: str, -) -> list[tuple[ChangeKind, str]]: +) -> list[ChangeEntry]: changelog = [] # Compare previous version commit with current working tree @@ -51,14 +52,13 @@ def get_changelog_entries( for diff_item in diff_index.iter_change_type("A"): file_path = diff_item.b_path - change_meta, contents = extract_changelog_data(repo.working_dir, file_path) - - changelog.append((str(change_meta.kind), contents)) + change_entry = extract_changelog_entry(repo.working_dir, file_path) + changelog.append(change_entry) return changelog -def extract_changelog_data(working_dir: str, file_path: str) -> (ChangeMeta, str): +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) @@ -66,19 +66,19 @@ def extract_changelog_data(working_dir: str, file_path: str) -> (ChangeMeta, str with open(abs_file_path, "r") as file: file_content = file.read() - change_meta, contents = strip_changelog_entry_frontmatter(file_content) + change_entry = extract_changelog_entry_from_contents(file_content) - if change_meta.date != date: + if change_entry.date != date: raise Exception( - f"{file_name} - date in front matter {change_meta.date} does not match date extracted from file name {date}" + f"{file_name} - date in front matter {change_entry.date} does not match date extracted from file name {date}" ) - if change_meta.kind != kind: + if change_entry.kind != kind: raise Exception( - f"{file_name} - kind in front matter {change_meta.kind} does not match kind extracted from file name {kind}" + f"{file_name} - kind in front matter {change_entry.kind} does not match kind extracted from file name {kind}" ) - return change_meta, contents + return change_entry def extract_date_and_kind_from_file_name(file_name: str) -> (datetime, ChangeKind): @@ -119,18 +119,15 @@ def get_change_kind(kind_str: str) -> ChangeKind: return ChangeKind.OTHER -def strip_changelog_entry_frontmatter(file_contents: str) -> (ChangeMeta, str): - """Strip the front matter from a changelog entry.""" +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) - meta = ChangeMeta(date=date, title=str(data["title"]), kind=kind) - ## Add newline to contents so the Markdown file also contains a newline at the end contents = data.content + "\n" - return meta, contents + return ChangeEntry(date=date, title=str(data["title"]), kind=kind, contents=contents) def get_changelog_filename(title: str, kind: ChangeKind, date: datetime) -> str: diff --git a/scripts/release/changelog_test.py b/scripts/release/changelog_test.py index 4f6b8183e..26d531b3b 100644 --- a/scripts/release/changelog_test.py +++ b/scripts/release/changelog_test.py @@ -4,9 +4,9 @@ from changelog import ( MAX_TITLE_LENGTH, ChangeKind, + extract_changelog_entry_from_contents, extract_date_and_kind_from_file_name, sanitize_title, - strip_changelog_entry_frontmatter, ) @@ -94,14 +94,13 @@ def test_strip_changelog_entry_frontmatter(): * Added new property [spec.search](https://www.mongodb.com/docs/kubernetes/current/mongodb/specification/#spec-search) to enable MongoDB Search. """ - change_meta, contents = strip_changelog_entry_frontmatter(file_contents) - - assert change_meta.title == "This is my change" - assert change_meta.kind == ChangeKind.FEATURE - assert change_meta.date == datetime.date(2025, 7, 10) + 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 ( - contents + 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. """ diff --git a/scripts/release/release_notes.py b/scripts/release/release_notes.py index c875aa4a2..38ced7306 100644 --- a/scripts/release/release_notes.py +++ b/scripts/release/release_notes.py @@ -7,6 +7,7 @@ from scripts.release.changelog import ( DEFAULT_CHANGELOG_PATH, DEFAULT_INITIAL_GIT_TAG_VERSION, + ChangeEntry, ChangeKind, get_changelog_entries, ) @@ -44,11 +45,11 @@ def generate_release_notes( parameters = { "version": version, - "preludes": [c[1] for c in changelog if c[0] == ChangeKind.PRELUDE], - "breaking_changes": [c[1] for c in changelog if c[0] == ChangeKind.BREAKING], - "features": [c[1] for c in changelog if c[0] == ChangeKind.FEATURE], - "fixes": [c[1] for c in changelog if c[0] == ChangeKind.FIX], - "others": [c[1] for c in changelog if c[0] == ChangeKind.OTHER], + "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) @@ -56,11 +57,11 @@ def generate_release_notes( def calculate_next_version_with_changelog( repo: Repo, changelog_sub_path: str, initial_commit_sha: str | None, initial_version: str -) -> (str, list[tuple[ChangeKind, str]]): +) -> (str, list[ChangeEntry]): previous_version_tag, previous_version_commit = find_previous_version(repo, initial_commit_sha) - changelog: list[tuple[ChangeKind, str]] = get_changelog_entries(previous_version_commit, repo, changelog_sub_path) - changelog_kinds = list[ChangeKind](map(lambda x: x[0], changelog)) + 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: From a2c1bcd8c9a2b60bdfcc550c57f368c66e1fbff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Tue, 15 Jul 2025 14:14:04 +0200 Subject: [PATCH 22/30] Making release a module --- scripts/release/__init__.py | 1 + scripts/release/changelog.py | 6 +++--- scripts/release/changelog_test.py | 2 +- scripts/release/release_notes_test.py | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 scripts/release/__init__.py 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/changelog.py b/scripts/release/changelog.py index f8b61a543..1bda1e7d6 100644 --- a/scripts/release/changelog.py +++ b/scripts/release/changelog.py @@ -35,14 +35,14 @@ def __init__(self, date: datetime, kind: ChangeKind, title: str, contents: str): def get_changelog_entries( - previous_version_commit: Commit, + base_commit: Commit, repo: Repo, changelog_sub_path: str, ) -> list[ChangeEntry]: changelog = [] - # Compare previous version commit with current working tree - diff_index = previous_version_commit.diff(other=repo.head.commit, paths=changelog_sub_path) + # 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: diff --git a/scripts/release/changelog_test.py b/scripts/release/changelog_test.py index 26d531b3b..91c4c538e 100644 --- a/scripts/release/changelog_test.py +++ b/scripts/release/changelog_test.py @@ -1,7 +1,7 @@ import datetime import unittest -from changelog import ( +from scripts.release.changelog import ( MAX_TITLE_LENGTH, ChangeKind, extract_changelog_entry_from_contents, diff --git a/scripts/release/release_notes_test.py b/scripts/release/release_notes_test.py index aea8d5e4b..3f32a433c 100644 --- a/scripts/release/release_notes_test.py +++ b/scripts/release/release_notes_test.py @@ -1,6 +1,6 @@ -from conftest import git_repo from git import Repo +from scripts.release.conftest import git_repo from scripts.release.release_notes import generate_release_notes From 5a5018ba0c422ced6365fd03c7d18d011e3e4cf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Tue, 15 Jul 2025 15:58:14 +0200 Subject: [PATCH 23/30] Fixing other kind of change issue + missing tests --- scripts/release/conftest.py | 6 ++++-- scripts/release/release_notes_tpl.md | 2 +- .../release/testdata/changelog/20250609_chore_not_oidc.md | 7 +++++++ .../release/testdata/changelog/20250611_other_not_oidc.md | 7 +++++++ scripts/release/testdata/release_notes_1.2.0.md | 5 +++++ 5 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 scripts/release/testdata/changelog/20250609_chore_not_oidc.md create mode 100644 scripts/release/testdata/changelog/20250611_other_not_oidc.md diff --git a/scripts/release/conftest.py b/scripts/release/conftest.py index 39b47849d..cd5487c47 100644 --- a/scripts/release/conftest.py +++ b/scripts/release/conftest.py @@ -55,8 +55,10 @@ def git_repo(change_log_path: str = DEFAULT_CHANGELOG_PATH) -> Repo: repo.create_tag("1.1.0", message="Public search preview release") ## OIDC release and 1.2.0 tag - changelog_file = add_file(repo_dir, "changelog/20250610_feature_oidc.md") - repo.index.add(changelog_file) + 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") diff --git a/scripts/release/release_notes_tpl.md b/scripts/release/release_notes_tpl.md index 98fa260de..fce3c8ad7 100644 --- a/scripts/release/release_notes_tpl.md +++ b/scripts/release/release_notes_tpl.md @@ -25,7 +25,7 @@ {{- fix -}} {%- endfor -%} {%- endif -%} -{% if other %} +{% if others %} ## Other Changes {% for other in others -%} 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/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/release_notes_1.2.0.md b/scripts/release/testdata/release_notes_1.2.0.md index ba0ef4416..ffb1af8e0 100644 --- a/scripts/release/testdata/release_notes_1.2.0.md +++ b/scripts/release/testdata/release_notes_1.2.0.md @@ -11,3 +11,8 @@ * [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. From e33b34459186bcfcc6667d0426c0190555c8aabb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Tue, 15 Jul 2025 16:06:52 +0200 Subject: [PATCH 24/30] Adding quotes to error message variables --- scripts/release/changelog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/release/changelog.py b/scripts/release/changelog.py index 1bda1e7d6..f771b418d 100644 --- a/scripts/release/changelog.py +++ b/scripts/release/changelog.py @@ -70,12 +70,12 @@ def extract_changelog_entry(working_dir: str, file_path: str) -> ChangeEntry: 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}" + 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}" + f"{file_name} - kind in front matter '{change_entry.kind}' does not match kind extracted from file name '{kind}'" ) return change_entry @@ -101,7 +101,7 @@ 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}") + raise Exception(f"date '{date_str}' is not in the expected format {date_format}") return date From 471bb5cb1b9e278c29035d44e83d8fd5335c8f99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Wed, 16 Jul 2025 11:24:59 +0200 Subject: [PATCH 25/30] remove venv from .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 06dd533ce..5f644de46 100644 --- a/.gitignore +++ b/.gitignore @@ -93,7 +93,6 @@ logs-debug/ docs/**/log/* docs/**/test.sh.run.log -/.venv/ # goreleaser generated files dist From 0d02f6fa87705117f9fe699ce912fa5c5bb8be76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Wed, 16 Jul 2025 12:15:51 +0200 Subject: [PATCH 26/30] fix unit tests --- scripts/release/changelog_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/release/changelog_test.py b/scripts/release/changelog_test.py index 91c4c538e..320968420 100644 --- a/scripts/release/changelog_test.py +++ b/scripts/release/changelog_test.py @@ -68,7 +68,8 @@ 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" + 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): From 6ca498b41bfc001d63c2f6556efcc6752239f749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= <6159874+MaciejKaras@users.noreply.github.com> Date: Fri, 25 Jul 2025 12:18:14 +0200 Subject: [PATCH 27/30] Update scripts/release/create_changelog.py Co-authored-by: Mikalai Radchuk <509198+m1kola@users.noreply.github.com> --- scripts/release/create_changelog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/release/create_changelog.py b/scripts/release/create_changelog.py index 4029bf74a..71f3ae1b8 100644 --- a/scripts/release/create_changelog.py +++ b/scripts/release/create_changelog.py @@ -26,7 +26,7 @@ metavar="", action="store", type=str, - help=f"Path to the changelog directory relative to the repository root. Default is {DEFAULT_CHANGELOG_PATH}", + help=f"Path to the changelog directory relative to a current working directory. Default is '{DEFAULT_CHANGELOG_PATH}'", ) parser.add_argument( "-d", From 2f9df2c021ded5d884de51eb8c93cc7de63e2f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= <6159874+MaciejKaras@users.noreply.github.com> Date: Fri, 25 Jul 2025 12:18:57 +0200 Subject: [PATCH 28/30] Update scripts/release/changelog.py Co-authored-by: Mikalai Radchuk <509198+m1kola@users.noreply.github.com> --- scripts/release/changelog.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/release/changelog.py b/scripts/release/changelog.py index f771b418d..26c67e0ae 100644 --- a/scripts/release/changelog.py +++ b/scripts/release/changelog.py @@ -115,8 +115,7 @@ def get_change_kind(kind_str: str) -> ChangeKind: return ChangeKind.FEATURE elif kind_str.lower() in BUGFIX_ENTRIES: return ChangeKind.FIX - else: - return ChangeKind.OTHER + return ChangeKind.OTHER def extract_changelog_entry_from_contents(file_contents: str) -> ChangeEntry: From 6dd61e2fb5454df542b426f59cd4af735d1d1f89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= <6159874+MaciejKaras@users.noreply.github.com> Date: Fri, 25 Jul 2025 12:20:02 +0200 Subject: [PATCH 29/30] Update scripts/release/release_notes.py Co-authored-by: Mikalai Radchuk <509198+m1kola@users.noreply.github.com> --- scripts/release/release_notes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/release/release_notes.py b/scripts/release/release_notes.py index 38ced7306..807917cb8 100644 --- a/scripts/release/release_notes.py +++ b/scripts/release/release_notes.py @@ -93,7 +93,7 @@ def calculate_next_version_with_changelog( metavar="", action="store", type=str, - help=f"Path to the changelog directory relative to the repository root. Default is '{DEFAULT_CHANGELOG_PATH}'", + help=f"Path to the changelog directory relative to a current working directory. Default is '{DEFAULT_CHANGELOG_PATH}'", ) parser.add_argument( "-s", From eb14e627f4a00bdba158c2785f12d741fdfda019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Fri, 25 Jul 2025 15:40:06 +0200 Subject: [PATCH 30/30] Review fixes 1 --- scripts/release/release_notes.py | 8 ++++---- scripts/release/release_notes_test.py | 12 ++++++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/scripts/release/release_notes.py b/scripts/release/release_notes.py index 807917cb8..45b8ca3a9 100644 --- a/scripts/release/release_notes.py +++ b/scripts/release/release_notes.py @@ -18,10 +18,10 @@ 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, + repository_path: str, + changelog_sub_path: str, + initial_commit_sha: str | None, + initial_version: str, ) -> str: f"""Generate a release notes based on the changes since the previous version tag. diff --git a/scripts/release/release_notes_test.py b/scripts/release/release_notes_test.py index 3f32a433c..068febe82 100644 --- a/scripts/release/release_notes_test.py +++ b/scripts/release/release_notes_test.py @@ -1,5 +1,9 @@ from git import Repo +from scripts.release.changelog import ( + DEFAULT_CHANGELOG_PATH, + DEFAULT_INITIAL_GIT_TAG_VERSION, +) from scripts.release.conftest import git_repo from scripts.release.release_notes import generate_release_notes @@ -7,7 +11,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) + release_notes = generate_release_notes( + git_repo.working_dir, DEFAULT_CHANGELOG_PATH, None, DEFAULT_INITIAL_GIT_TAG_VERSION + ) with open("scripts/release/testdata/release_notes_1.0.0_empty.md") as file: assert release_notes == file.read() @@ -66,6 +72,8 @@ 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) + release_notes = generate_release_notes( + git_repo.working_dir, DEFAULT_CHANGELOG_PATH, None, DEFAULT_INITIAL_GIT_TAG_VERSION + ) with open(f"scripts/release/testdata/release_notes_{tag}.md") as file: assert release_notes == file.read()