From 65fd607fcda69f78a11e0ce18512cf8ea44e36dc 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/46] 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 c5ca572c5..a9d13d3ca 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,4 @@ logs-debug/ docs/**/log/* docs/**/test.sh.run.log +/.venv/ 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 993753e212b52278216a7be3b36f5ad2a2bbd357 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/46] 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 f41c9f363367a2a29ebad1f99d198d43006279d9 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/46] 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 30d5b04dbfc2c462ca9c4a6e9aa2d2a1b5957c59 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/46] 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 a8e6782d17d40fae870ff7a42cba2e5f319ee01b 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/46] 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 6de370363f34d0236727740870baceb75d854527 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/46] 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 c353737a9b01fc74b5050ccb1b0c2590f5fe11cd 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/46] 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 c220f2eb0c83fb58cb2844f93fa7281e93466baf 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/46] 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 891d8214a7d0e801840813defc02ca548bef8493 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/46] 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 a2cdcb4de9713b1f81ffb3388809ee7fda431890 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/46] 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 5465b987d3ee3d62ac3ffbf445f4b17b81ad401d 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/46] 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 20fded08dd54634e3ca967f59bd0c58082eaaa09 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/46] 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 df195d12df6eb423934922b2c157d23e29e3f845 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/46] 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 aebd6345c5445c74619eb92e56dac5ecf56910f6 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/46] 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 371499a9c6b2a40f1a5136513c7cfaef86e201b9 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/46] 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 a7d7f607b26a675f972b5d971348d50ef876910f 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/46] 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 9e365ab9d..41134b740 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,6 +33,7 @@ pytest-mock==3.14.1 wrapt==1.17.2 botocore==1.38.33 boto3==1.38.33 +python-frontmatter==1.1.0 # from kubeobject freezegun==1.5.2 diff --git a/scripts/release/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 136a939f8009bba53a82cb62d036fb739b8a2058 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/46] 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 5dfa8cd2f326095b1736f03bbe2da1f8da01878c 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/46] 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 ce499279798569924fc5b9b2e4d6db454d4c9739 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/46] 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 240f2c9d142f105bc232feb3c2ea19f21e55d895 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/46] 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 4a97699c9a9cd5cbe5e415e8d0075febe11d0fa3 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/46] 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 76f0f7453498b04f3aa8d7f954bab23a3d00eba9 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/46] 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 c9b685721cb704d9396b145579c1fcf45c7ddb63 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/46] 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 e51357b6c2b32fc850ef3680bac682c07a08fbc2 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/46] 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 896db6524b45370471ad5cedb8842671913996fc 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/46] remove venv from .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index a9d13d3ca..c5ca572c5 100644 --- a/.gitignore +++ b/.gitignore @@ -94,4 +94,3 @@ logs-debug/ docs/**/log/* docs/**/test.sh.run.log -/.venv/ From d55f32289d45793ae8b04d732e63cc19b7218cc6 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/46] 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 5b35ab0b3ee5622293df0c2d23d3b6393ee9ec80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Tue, 15 Jul 2025 11:05:33 +0200 Subject: [PATCH 27/46] Adding changelog file for testing --- ...ure_test_something_that_is_very_long_and_i_dont_know.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 changelog/20250715_feature_test_something_that_is_very_long_and_i_dont_know.md diff --git a/changelog/20250715_feature_test_something_that_is_very_long_and_i_dont_know.md b/changelog/20250715_feature_test_something_that_is_very_long_and_i_dont_know.md new file mode 100644 index 000000000..89a8b4b28 --- /dev/null +++ b/changelog/20250715_feature_test_something_that_is_very_long_and_i_dont_know.md @@ -0,0 +1,7 @@ +--- +title: Test something that is very long and I don't know if I like it +kind: feature +date: 2025-07-15 +--- + +Some changes From 672338020a3290275e3c6ed25889680d53d358e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Tue, 15 Jul 2025 11:06:45 +0200 Subject: [PATCH 28/46] Adding GHA workflow --- .github/workflows/require_changelog.yml | 30 ++++++++++ scripts/release/check_changelog.py | 78 +++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 .github/workflows/require_changelog.yml create mode 100644 scripts/release/check_changelog.py diff --git a/.github/workflows/require_changelog.yml b/.github/workflows/require_changelog.yml new file mode 100644 index 000000000..adb41a390 --- /dev/null +++ b/.github/workflows/require_changelog.yml @@ -0,0 +1,30 @@ +name: Validate Changelog Requirement +on: + pull_request: + branches: + - master + - release-* + label: + branches: + - master + - release-* + +jobs: + validate-changelog: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + with: + fetch-depth: '0' + - name: setup python + uses: actions/setup-python@v5 + with: + python-version: 3.13 # Make this env var + cache: 'pip' # caching pip dependencies + - run: pip install -r requirements.txt + - name: Check if changelog entry file was added in this PR + run: python scripts/release/check_changelog.py --base-sha "$BASE_SHA" --fail-on-no-changes $FAIL_ON_NO_CHANGES + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + FAIL_ON_NO_CHANGES: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-changelog') }} diff --git a/scripts/release/check_changelog.py b/scripts/release/check_changelog.py new file mode 100644 index 000000000..49a919e55 --- /dev/null +++ b/scripts/release/check_changelog.py @@ -0,0 +1,78 @@ +import argparse +import pathlib + +from git import Repo + +from scripts.release.changelog import DEFAULT_CHANGELOG_PATH, get_changelog_entries + +def str2bool(v): + if isinstance(v, bool): + return v + if v.lower() in ('yes', 'true', 't', 'y', '1'): + return True + elif v.lower() in ('no', 'false', 'f', 'n', '0'): + return False + else: + raise argparse.ArgumentTypeError('Boolean value expected.') + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Check if there are changelog entries", + formatter_class=argparse.RawTextHelpFormatter, + ) + parser.add_argument( + "-p", + "--path", + default=".", + metavar="", + action="store", + type=pathlib.Path, + help="Path to the Git repository. Default is the current directory '.'", + ) + parser.add_argument( + "-c", + "--changelog-path", + default=DEFAULT_CHANGELOG_PATH, + metavar="", + action="store", + type=str, + help=f"Path to the changelog directory relative to the repository root. Default is '{DEFAULT_CHANGELOG_PATH}'", + ) + parser.add_argument( + "-t", + "--base-sha", + metavar="", + action="store", + required=True, + type=str, + help="Base commit SHA to compare against. This should be the SHA of the base branch the Pull Request is targeting.", + ) + parser.add_argument( + "-f", + "--fail-on-no-changes", + default=True, + metavar="", + action="store", + type=str2bool, + nargs='?', + const=True, + help="Fail if no changelog entries are found. Default is True.", + ) + args = parser.parse_args() + + repo = Repo(args.path) + base_commit = repo.commit(args.base_sha) + + try: + changelog = get_changelog_entries(base_commit, repo, args.changelog_path) + except Exception as e: + print(f"Error retrieving changelog entries. Possible validation issues: {e}") + exit(1) + + if not changelog: + print("No changelog entries found.") + if args.fail_on_no_changes: + print("Exiting with error due to no changelog entries found.") + exit(1) + else: + print("Changelog entries found and validated") From a723387e0cbd7b986b3b431575cbc9ee91b30f3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Tue, 15 Jul 2025 11:11:05 +0200 Subject: [PATCH 29/46] Adding GHA workflow --- .github/workflows/require_changelog.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/require_changelog.yml b/.github/workflows/require_changelog.yml index adb41a390..6b147179a 100644 --- a/.github/workflows/require_changelog.yml +++ b/.github/workflows/require_changelog.yml @@ -1,4 +1,4 @@ -name: Validate Changelog Requirement +run-name: Validate Changelog Requirement for PRs on: pull_request: branches: @@ -22,7 +22,10 @@ jobs: with: python-version: 3.13 # Make this env var cache: 'pip' # caching pip dependencies - - run: pip install -r requirements.txt + - name: Install dependencies + run: | + pip install --upgrade pip + pip install -r requirements.txt - name: Check if changelog entry file was added in this PR run: python scripts/release/check_changelog.py --base-sha "$BASE_SHA" --fail-on-no-changes $FAIL_ON_NO_CHANGES env: From 16b128572b1f8191fc0ba83e2ae7b8365650bc06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Tue, 15 Jul 2025 11:16:45 +0200 Subject: [PATCH 30/46] Adding GHA workflow --- .github/workflows/require_changelog.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/require_changelog.yml b/.github/workflows/require_changelog.yml index 6b147179a..fda8e8e09 100644 --- a/.github/workflows/require_changelog.yml +++ b/.github/workflows/require_changelog.yml @@ -1,4 +1,4 @@ -run-name: Validate Changelog Requirement for PRs +name: Validate Changelog Requirement on: pull_request: branches: @@ -24,6 +24,7 @@ jobs: cache: 'pip' # caching pip dependencies - name: Install dependencies run: | + sudo apt-get install libldap2-dev libsasl2-dev # Required for python-ldap pip install --upgrade pip pip install -r requirements.txt - name: Check if changelog entry file was added in this PR From 48f250731de56f948ae32b87263676227b40fbeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Tue, 15 Jul 2025 12:19:52 +0200 Subject: [PATCH 31/46] Fixing GHA workflow --- .github/workflows/require_changelog.yml | 2 +- scripts/release/check_changelog.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/require_changelog.yml b/.github/workflows/require_changelog.yml index fda8e8e09..78f699b9d 100644 --- a/.github/workflows/require_changelog.yml +++ b/.github/workflows/require_changelog.yml @@ -28,7 +28,7 @@ jobs: pip install --upgrade pip pip install -r requirements.txt - name: Check if changelog entry file was added in this PR - run: python scripts/release/check_changelog.py --base-sha "$BASE_SHA" --fail-on-no-changes $FAIL_ON_NO_CHANGES + run: python -m scripts.release.check_changelog --base-sha "$BASE_SHA" --fail-on-no-changes $FAIL_ON_NO_CHANGES env: BASE_SHA: ${{ github.event.pull_request.base.sha }} FAIL_ON_NO_CHANGES: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-changelog') }} diff --git a/scripts/release/check_changelog.py b/scripts/release/check_changelog.py index 49a919e55..b3caf2bca 100644 --- a/scripts/release/check_changelog.py +++ b/scripts/release/check_changelog.py @@ -5,15 +5,17 @@ from scripts.release.changelog import DEFAULT_CHANGELOG_PATH, get_changelog_entries + def str2bool(v): if isinstance(v, bool): return v - if v.lower() in ('yes', 'true', 't', 'y', '1'): + if v.lower() in ("yes", "true", "t", "y", "1"): return True - elif v.lower() in ('no', 'false', 'f', 'n', '0'): + elif v.lower() in ("no", "false", "f", "n", "0"): return False else: - raise argparse.ArgumentTypeError('Boolean value expected.') + raise argparse.ArgumentTypeError("Boolean value expected.") + if __name__ == "__main__": parser = argparse.ArgumentParser( @@ -54,8 +56,7 @@ def str2bool(v): metavar="", action="store", type=str2bool, - nargs='?', - const=True, + nargs="?", help="Fail if no changelog entries are found. Default is True.", ) args = parser.parse_args() From bf50605e9d9f0d4fed905742fd45b8ef0c85fe17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Tue, 15 Jul 2025 12:23:39 +0200 Subject: [PATCH 32/46] Removing changelog file --- ...ure_test_something_that_is_very_long_and_i_dont_know.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 changelog/20250715_feature_test_something_that_is_very_long_and_i_dont_know.md diff --git a/changelog/20250715_feature_test_something_that_is_very_long_and_i_dont_know.md b/changelog/20250715_feature_test_something_that_is_very_long_and_i_dont_know.md deleted file mode 100644 index 89a8b4b28..000000000 --- a/changelog/20250715_feature_test_something_that_is_very_long_and_i_dont_know.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Test something that is very long and I don't know if I like it -kind: feature -date: 2025-07-15 ---- - -Some changes From 5d066f7ff22bf78b9ea949a820925680a046cee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Tue, 15 Jul 2025 15:17:05 +0200 Subject: [PATCH 33/46] Adding release notes action --- .github/actions/setup-ubuntu-host/action.yml | 20 ++++++++++++ .github/workflows/preview_release_notes.yml | 32 ++++++++++++++++++++ .github/workflows/require_changelog.yml | 17 ++++------- scripts/release/check_changelog.py | 2 +- 4 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 .github/actions/setup-ubuntu-host/action.yml create mode 100644 .github/workflows/preview_release_notes.yml diff --git a/.github/actions/setup-ubuntu-host/action.yml b/.github/actions/setup-ubuntu-host/action.yml new file mode 100644 index 000000000..8fd831f1d --- /dev/null +++ b/.github/actions/setup-ubuntu-host/action.yml @@ -0,0 +1,20 @@ +name: 'Setup Ubuntu Runner Host' +inputs: + python-version: + description: 'Python version to use' + required: true + default: '3.13' +runs: + using: "composite" + steps: + - name: setup python + uses: actions/setup-python@v5 + with: + python-version: ${{inputs.python-version}} + cache: 'pip' # caching pip dependencies + - name: Install dependencies + shell: bash + run: | + sudo apt-get install libldap2-dev libsasl2-dev # Required for python-ldap + pip install --upgrade pip + pip install -r requirements.txt diff --git a/.github/workflows/preview_release_notes.yml b/.github/workflows/preview_release_notes.yml new file mode 100644 index 000000000..b600bc657 --- /dev/null +++ b/.github/workflows/preview_release_notes.yml @@ -0,0 +1,32 @@ +name: Preview Release Notes +on: + push: + branches: + - master + - release-* + pull_request: + branches: + - master + - release-* + +jobs: + preview_release_notes: + name: Preview Release Notes + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + fetch-depth: '0' + - name: Setup host + uses: ./.github/actions/setup-ubuntu-host + with: + python-version: '${{ vars.PYTHON_VERSION }}' + - name: Generate Release Notes + id: generate_release_notes + run: python -m scripts.release.release_notes -s $INITIAL_COMMIT_SHA -v $INITIAL_VERSION -o release_notes_tmp.md + env: + INITIAL_COMMIT_SHA: ${{ vars.RELEASE_INITIAL_COMMIT_SHA }} + INITIAL_VERSION: ${{ vars.RELEASE_INITIAL_VERSION }} + - name: Summarize results + run: echo $(cat release_notes_tmp.md) >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/require_changelog.yml b/.github/workflows/require_changelog.yml index 78f699b9d..1d9e33d8b 100644 --- a/.github/workflows/require_changelog.yml +++ b/.github/workflows/require_changelog.yml @@ -11,24 +11,19 @@ on: jobs: validate-changelog: + name: Check for valid changelog entry in Pull Request runs-on: ubuntu-latest steps: - - name: Check out repository code + - name: Check out repository uses: actions/checkout@v4 with: fetch-depth: '0' - - name: setup python - uses: actions/setup-python@v5 + - name: Setup host + uses: ./.github/actions/setup-ubuntu-host with: - python-version: 3.13 # Make this env var - cache: 'pip' # caching pip dependencies - - name: Install dependencies - run: | - sudo apt-get install libldap2-dev libsasl2-dev # Required for python-ldap - pip install --upgrade pip - pip install -r requirements.txt + python-version: '${{ vars.PYTHON_VERSION }}' - name: Check if changelog entry file was added in this PR - run: python -m scripts.release.check_changelog --base-sha "$BASE_SHA" --fail-on-no-changes $FAIL_ON_NO_CHANGES + run: python -m scripts.release.check_changelog -b $BASE_SHA -f $FAIL_ON_NO_CHANGES >> $GITHUB_STEP_SUMMARY env: BASE_SHA: ${{ github.event.pull_request.base.sha }} FAIL_ON_NO_CHANGES: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-changelog') }} diff --git a/scripts/release/check_changelog.py b/scripts/release/check_changelog.py index b3caf2bca..6728e9ad0 100644 --- a/scripts/release/check_changelog.py +++ b/scripts/release/check_changelog.py @@ -41,7 +41,7 @@ def str2bool(v): help=f"Path to the changelog directory relative to the repository root. Default is '{DEFAULT_CHANGELOG_PATH}'", ) parser.add_argument( - "-t", + "-b", "--base-sha", metavar="", action="store", From fcd74d6d7476036e77fc0705b2e59b44651e9974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Tue, 15 Jul 2025 15:40:27 +0200 Subject: [PATCH 34/46] test --- changelog/20250715_other_test.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 changelog/20250715_other_test.md diff --git a/changelog/20250715_other_test.md b/changelog/20250715_other_test.md new file mode 100644 index 000000000..09a50102b --- /dev/null +++ b/changelog/20250715_other_test.md @@ -0,0 +1,6 @@ +--- +title: Test +kind: other +date: 2025-07-15 +--- + From 35a904daa5e74f4fcd17e8c2cf84086a3dbd0ec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Tue, 15 Jul 2025 15:58:26 +0200 Subject: [PATCH 35/46] wip --- .github/workflows/preview_release_notes.yml | 13 ++++++++++++- .github/workflows/require_changelog.yml | 7 +++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/preview_release_notes.yml b/.github/workflows/preview_release_notes.yml index b600bc657..fa8ea3bae 100644 --- a/.github/workflows/preview_release_notes.yml +++ b/.github/workflows/preview_release_notes.yml @@ -13,6 +13,9 @@ jobs: preview_release_notes: name: Preview Release Notes runs-on: ubuntu-latest + permissions: + contents: read + issues: write steps: - name: Check out repository uses: actions/checkout@v4 @@ -24,9 +27,17 @@ jobs: python-version: '${{ vars.PYTHON_VERSION }}' - name: Generate Release Notes id: generate_release_notes - run: python -m scripts.release.release_notes -s $INITIAL_COMMIT_SHA -v $INITIAL_VERSION -o release_notes_tmp.md + run: | + python -m scripts.release.release_notes -s $INITIAL_COMMIT_SHA -v $INITIAL_VERSION -o release_notes_tmp.md env: INITIAL_COMMIT_SHA: ${{ vars.RELEASE_INITIAL_COMMIT_SHA }} INITIAL_VERSION: ${{ vars.RELEASE_INITIAL_VERSION }} - name: Summarize results run: echo $(cat release_notes_tmp.md) >> $GITHUB_STEP_SUMMARY + - name: Update PR comment + if: github.event_name == 'pull_request' + run: | + gh issue comment $ISSUE --edit-last --create-if-none --body-file release_notes_tmp.md + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE: ${{ github.event.pull_request.html_url }} diff --git a/.github/workflows/require_changelog.yml b/.github/workflows/require_changelog.yml index 1d9e33d8b..3a382dac4 100644 --- a/.github/workflows/require_changelog.yml +++ b/.github/workflows/require_changelog.yml @@ -11,8 +11,10 @@ on: jobs: validate-changelog: - name: Check for valid changelog entry in Pull Request + name: Check for valid changelog entry runs-on: ubuntu-latest + permissions: + contents: read steps: - name: Check out repository uses: actions/checkout@v4 @@ -23,7 +25,8 @@ jobs: with: python-version: '${{ vars.PYTHON_VERSION }}' - name: Check if changelog entry file was added in this PR - run: python -m scripts.release.check_changelog -b $BASE_SHA -f $FAIL_ON_NO_CHANGES >> $GITHUB_STEP_SUMMARY + run: | + python -m scripts.release.check_changelog -b $BASE_SHA -f $FAIL_ON_NO_CHANGES | tee >> $GITHUB_STEP_SUMMARY env: BASE_SHA: ${{ github.event.pull_request.base.sha }} FAIL_ON_NO_CHANGES: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-changelog') }} From 5d72a98b10467dbcb426001354a0cf067f4dec64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Wed, 16 Jul 2025 12:35:27 +0200 Subject: [PATCH 36/46] Change permissions --- .github/workflows/preview_release_notes.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_release_notes.yml b/.github/workflows/preview_release_notes.yml index fa8ea3bae..8d6e5efcf 100644 --- a/.github/workflows/preview_release_notes.yml +++ b/.github/workflows/preview_release_notes.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - issues: write + pull-requests: write steps: - name: Check out repository uses: actions/checkout@v4 From 8eae2aa1986efd4ad05fe5fe35d8075f0b1db797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Wed, 16 Jul 2025 12:41:30 +0200 Subject: [PATCH 37/46] Fix summary --- .github/workflows/preview_release_notes.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_release_notes.yml b/.github/workflows/preview_release_notes.yml index 8d6e5efcf..94f84db8b 100644 --- a/.github/workflows/preview_release_notes.yml +++ b/.github/workflows/preview_release_notes.yml @@ -33,7 +33,7 @@ jobs: INITIAL_COMMIT_SHA: ${{ vars.RELEASE_INITIAL_COMMIT_SHA }} INITIAL_VERSION: ${{ vars.RELEASE_INITIAL_VERSION }} - name: Summarize results - run: echo $(cat release_notes_tmp.md) >> $GITHUB_STEP_SUMMARY + run: cat release_notes_tmp.md | tee >> $GITHUB_STEP_SUMMARY - name: Update PR comment if: github.event_name == 'pull_request' run: | From 3a796292cdaae3930c114d3084f37d772ac53ee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Wed, 16 Jul 2025 12:46:23 +0200 Subject: [PATCH 38/46] Add more changes --- .github/workflows/preview_release_notes.yml | 2 +- changelog/20250715_other_test.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/preview_release_notes.yml b/.github/workflows/preview_release_notes.yml index 94f84db8b..499fb36cf 100644 --- a/.github/workflows/preview_release_notes.yml +++ b/.github/workflows/preview_release_notes.yml @@ -33,7 +33,7 @@ jobs: INITIAL_COMMIT_SHA: ${{ vars.RELEASE_INITIAL_COMMIT_SHA }} INITIAL_VERSION: ${{ vars.RELEASE_INITIAL_VERSION }} - name: Summarize results - run: cat release_notes_tmp.md | tee >> $GITHUB_STEP_SUMMARY + run: cat release_notes_tmp.md >> $GITHUB_STEP_SUMMARY - name: Update PR comment if: github.event_name == 'pull_request' run: | diff --git a/changelog/20250715_other_test.md b/changelog/20250715_other_test.md index 09a50102b..853a69dbf 100644 --- a/changelog/20250715_other_test.md +++ b/changelog/20250715_other_test.md @@ -4,3 +4,4 @@ kind: other date: 2025-07-15 --- +* Some test change that is not related to OIDC authentication. From c9850f1705df5b90436a782137d5a5f907d7a02b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Wed, 16 Jul 2025 14:48:00 +0200 Subject: [PATCH 39/46] Update activity types --- .github/workflows/require_changelog.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/require_changelog.yml b/.github/workflows/require_changelog.yml index 3a382dac4..be626377b 100644 --- a/.github/workflows/require_changelog.yml +++ b/.github/workflows/require_changelog.yml @@ -4,10 +4,12 @@ on: branches: - master - release-* - label: - branches: - - master - - release-* + types: + - opened + - synchronize + - reopened + - labeled + - unlabeled jobs: validate-changelog: From 2b484ba198aeeb431df511150f5d6733546b3da1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Wed, 16 Jul 2025 16:12:41 +0200 Subject: [PATCH 40/46] Remove test changelog file --- changelog/20250715_other_test.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 changelog/20250715_other_test.md diff --git a/changelog/20250715_other_test.md b/changelog/20250715_other_test.md deleted file mode 100644 index 853a69dbf..000000000 --- a/changelog/20250715_other_test.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Test -kind: other -date: 2025-07-15 ---- - -* Some test change that is not related to OIDC authentication. From a6c3360e119d1cd12d4723e8656953c14033b4f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Wed, 16 Jul 2025 16:20:33 +0200 Subject: [PATCH 41/46] Fix missing pipefail --- .github/workflows/preview_release_notes.yml | 6 ++---- .github/workflows/require_changelog.yml | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/preview_release_notes.yml b/.github/workflows/preview_release_notes.yml index 499fb36cf..84ecb16c7 100644 --- a/.github/workflows/preview_release_notes.yml +++ b/.github/workflows/preview_release_notes.yml @@ -27,8 +27,7 @@ jobs: python-version: '${{ vars.PYTHON_VERSION }}' - name: Generate Release Notes id: generate_release_notes - run: | - python -m scripts.release.release_notes -s $INITIAL_COMMIT_SHA -v $INITIAL_VERSION -o release_notes_tmp.md + run: python -m scripts.release.release_notes -s $INITIAL_COMMIT_SHA -v $INITIAL_VERSION -o release_notes_tmp.md env: INITIAL_COMMIT_SHA: ${{ vars.RELEASE_INITIAL_COMMIT_SHA }} INITIAL_VERSION: ${{ vars.RELEASE_INITIAL_VERSION }} @@ -36,8 +35,7 @@ jobs: run: cat release_notes_tmp.md >> $GITHUB_STEP_SUMMARY - name: Update PR comment if: github.event_name == 'pull_request' - run: | - gh issue comment $ISSUE --edit-last --create-if-none --body-file release_notes_tmp.md + run: gh issue comment $ISSUE --edit-last --create-if-none --body-file release_notes_tmp.md env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ISSUE: ${{ github.event.pull_request.html_url }} diff --git a/.github/workflows/require_changelog.yml b/.github/workflows/require_changelog.yml index be626377b..d5811d36d 100644 --- a/.github/workflows/require_changelog.yml +++ b/.github/workflows/require_changelog.yml @@ -28,6 +28,7 @@ jobs: python-version: '${{ vars.PYTHON_VERSION }}' - name: Check if changelog entry file was added in this PR run: | + set -o pipefail python -m scripts.release.check_changelog -b $BASE_SHA -f $FAIL_ON_NO_CHANGES | tee >> $GITHUB_STEP_SUMMARY env: BASE_SHA: ${{ github.event.pull_request.base.sha }} From d6e789df9aea475a56fbc0a868d722223731aeb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Wed, 16 Jul 2025 17:03:09 +0200 Subject: [PATCH 42/46] Added documentation for changelogs --- .github/pull_request_template.md | 23 ++++++++++----- CONTRIBUTING.md | 50 ++++++++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e442424cb..c8d5c75e3 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -7,17 +7,24 @@ ## Checklist + - [ ] Have you linked a jira ticket and/or is the ticket in the title? - [ ] Have you checked whether your jira ticket required DOCSP changes? -- [ ] Have you checked for release_note changes? +- [ ] Have you added changelog file? + - use `skip-changelog` label if not needed + - refer + to [Changelog files and Release Notes](https://github.com/mongodb/mongodb-kubernetes/blob/master/CONTRIBUTING.md#changelog-files-and-release-notes) + section in CONTRIBUTING.md for more details ## Reminder (Please remove this when merging) + - Please try to Approve or Reject Changes the PR, keep PRs in review as short as possible -- Our Short Guide for PRs: [Link](https://docs.google.com/document/d/1T93KUtdvONq43vfTfUt8l92uo4e4SEEvFbIEKOxGr44/edit?tab=t.0) +- Our Short Guide for + PRs: [Link](https://docs.google.com/document/d/1T93KUtdvONq43vfTfUt8l92uo4e4SEEvFbIEKOxGr44/edit?tab=t.0) - Remember the following Communication Standards - use comment prefixes for clarity: - * **blocking**: Must be addressed before approval. - * **follow-up**: Can be addressed in a later PR or ticket. - * **q**: Clarifying question. - * **nit**: Non-blocking suggestions. - * **note**: Side-note, non-actionable. Example: Praise - * --> no prefix is considered a question + * **blocking**: Must be addressed before approval. + * **follow-up**: Can be addressed in a later PR or ticket. + * **q**: Clarifying question. + * **nit**: Non-blocking suggestions. + * **note**: Side-note, non-actionable. Example: Praise + * --> no prefix is considered a question diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d289d9dae..a7ef4f6c9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,51 @@ # Summary + Contributing to the Mongodb Controllers for Kubernetes (MCK) project -Pull requests are always welcome, and the MCK dev team appreciates any help the community can give to help make MongoDB better. +Pull requests are always welcome, and the MCK dev team appreciates any help the community can give to help make MongoDB +better. + +## PR Prerequisites -# PR Prerequisites -* Please ensure you have signed our Contributor Agreement. You can find it [here](https://www.mongodb.com/legal/contributor-agreement). +* Please ensure you have signed our Contributor Agreement. You can find + it [here](https://www.mongodb.com/legal/contributor-agreement). * Please ensure that all commits are signed. +* Create a changelog file that will describe the changes you made. Use the `skip-changelog` label if your changes do not + require a changelog entry. + +## Changelog files and Release Notes + +Each Pull Request usually has a changelog file that describes the changes made in the PR using Markdown syntax. +Changelog files are placed in the `changelog/` directory and used to generate the Release Notes for the +upcoming release. Preview of the Release Notes is automatically added as comment to each Pull Request. +The changelog file needs to follow the naming convention +`YYYYMMDD--.md`. To create changelog file please use the +`scripts/release/create_changelog.py` script. Example usage: + +```console +python3 -m scripts.release.create_changelog --kind fix "Fix that I want to describe in the changelog" +``` + +For more options, run the script with `--help`: + +```console +python3 -m scripts.release.create_changelog --help +usage: create_changelog.py [-h] [-c ] [-d ] [-e] -k title + +Utility to easily create a new changelog entry file. + +positional arguments: + title Title for the changelog entry + +options: + -h, --help show this help message and exit + -c, --changelog-path + Path to the changelog directory relative to the repository root. Default is changelog/ + -d, --date Date in 'YYYY-MM-DD' format to use for the changelog entry. Default is today's date + -e, --editor Open the created changelog entry in the default editor (if set, otherwise uses 'vi'). Default is True + -k, --kind Kind of the changelog entry: + - 'prelude' for prelude entries + - 'breaking, major' for breaking change entries + - 'feat, feature' for feature entries + - 'fix, bugfix, hotfix, patch' for bugfix entries + - everything else will be treated as other entries +``` From 8a5cd182a51176aca0685f09d07aeb9008e108f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Thu, 17 Jul 2025 10:25:40 +0200 Subject: [PATCH 43/46] Remove unnecessary default value --- .github/actions/setup-ubuntu-host/action.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/actions/setup-ubuntu-host/action.yml b/.github/actions/setup-ubuntu-host/action.yml index 8fd831f1d..8ab3c2a4c 100644 --- a/.github/actions/setup-ubuntu-host/action.yml +++ b/.github/actions/setup-ubuntu-host/action.yml @@ -3,7 +3,6 @@ inputs: python-version: description: 'Python version to use' required: true - default: '3.13' runs: using: "composite" steps: From d956a1380b630e0aa99dbe90fef606567cdd8c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Thu, 17 Jul 2025 13:09:33 +0200 Subject: [PATCH 44/46] Don't post PR comment on forks - the token has only read permissions --- .github/workflows/preview_release_notes.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/preview_release_notes.yml b/.github/workflows/preview_release_notes.yml index 84ecb16c7..58563de0f 100644 --- a/.github/workflows/preview_release_notes.yml +++ b/.github/workflows/preview_release_notes.yml @@ -34,7 +34,8 @@ jobs: - name: Summarize results run: cat release_notes_tmp.md >> $GITHUB_STEP_SUMMARY - name: Update PR comment - if: github.event_name == 'pull_request' + # If the PR is from a fork, we cannot update the comment using read only permissions + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository run: gh issue comment $ISSUE --edit-last --create-if-none --body-file release_notes_tmp.md env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 0286ddff630f5451650916829398708aa0df9d92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Thu, 17 Jul 2025 13:36:37 +0200 Subject: [PATCH 45/46] Add disclaimer to PR comment --- .github/workflows/preview_release_notes.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview_release_notes.yml b/.github/workflows/preview_release_notes.yml index 58563de0f..0badd51cb 100644 --- a/.github/workflows/preview_release_notes.yml +++ b/.github/workflows/preview_release_notes.yml @@ -31,12 +31,16 @@ jobs: env: INITIAL_COMMIT_SHA: ${{ vars.RELEASE_INITIAL_COMMIT_SHA }} INITIAL_VERSION: ${{ vars.RELEASE_INITIAL_VERSION }} + - name: Add disclaimer to release notes preview + run: | + echo -e "_:warning: (this preview might not be accurate if the PR is not rebased on current master branch)_\n" > release_notes_preview.md + cat release_notes_tmp.md >> release_notes_preview.md - name: Summarize results - run: cat release_notes_tmp.md >> $GITHUB_STEP_SUMMARY + run: cat release_notes_preview.md >> $GITHUB_STEP_SUMMARY - name: Update PR comment # If the PR is from a fork, we cannot update the comment using read only permissions if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository - run: gh issue comment $ISSUE --edit-last --create-if-none --body-file release_notes_tmp.md + run: gh issue comment $ISSUE --edit-last --create-if-none --body-file release_notes_preview.md env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ISSUE: ${{ github.event.pull_request.html_url }} From b14bd5c1ef49560a1de81a1b0a038b35c1e19c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kara=C5=9B?= Date: Mon, 28 Jul 2025 15:55:37 +0200 Subject: [PATCH 46/46] Review fixes --- CONTRIBUTING.md | 10 +++++----- .../testdata/changelog/20250609_chore_not_oidc.md | 7 ------- 2 files changed, 5 insertions(+), 12 deletions(-) delete mode 100644 scripts/release/testdata/changelog/20250609_chore_not_oidc.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a7ef4f6c9..a7e4b1f4c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,13 +39,13 @@ positional arguments: options: -h, --help show this help message and exit -c, --changelog-path - Path to the changelog directory relative to the repository root. Default is changelog/ + Path to the changelog directory relative to a current working directory. Default is 'changelog/' -d, --date Date in 'YYYY-MM-DD' format to use for the changelog entry. Default is today's date -e, --editor Open the created changelog entry in the default editor (if set, otherwise uses 'vi'). Default is True -k, --kind Kind of the changelog entry: - 'prelude' for prelude entries - - 'breaking, major' for breaking change entries - - 'feat, feature' for feature entries - - 'fix, bugfix, hotfix, patch' for bugfix entries - - everything else will be treated as other entries + - 'breaking' for breaking change entries + - 'feature' for feature entries + - 'fix' for bugfix entries + - 'other' for other entries ``` diff --git a/scripts/release/testdata/changelog/20250609_chore_not_oidc.md b/scripts/release/testdata/changelog/20250609_chore_not_oidc.md deleted file mode 100644 index 34fe38427..000000000 --- a/scripts/release/testdata/changelog/20250609_chore_not_oidc.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Some chore type of change -kind: chore -date: 2025-06-09 ---- - -* Fixing CI/CD pipeline issues.